1pub mod config_init;
20pub mod context;
21pub mod detection;
22pub mod effect;
23pub mod effect_handler;
24pub mod effectful;
25pub mod event_loop;
26pub mod finalization;
27#[cfg(any(test, feature = "test-utils"))]
28pub mod mock_effect_handler;
29pub mod plumbing;
30pub mod resume;
31pub mod validation;
32
33use crate::agents::AgentRegistry;
34use crate::app::finalization::finalize_pipeline;
35use crate::banner::print_welcome_banner;
36use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
37
38use crate::checkpoint::{
39 save_checkpoint_with_workspace, CheckpointBuilder, PipelineCheckpoint, PipelinePhase,
40 RebaseState,
41};
42use crate::cli::{
43 create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
44 handle_list_available_agents, handle_list_providers, handle_show_baseline,
45 handle_template_commands, prompt_template_selection, Args,
46};
47
48use crate::executor::ProcessExecutor;
49use crate::files::protection::monitoring::PromptMonitor;
50use crate::files::{
51 create_prompt_backup_with_workspace, make_prompt_read_only_with_workspace,
52 update_status_with_workspace, validate_prompt_md_with_workspace,
53};
54use crate::git_helpers::{
55 abort_rebase, continue_rebase, get_conflicted_files, get_default_branch,
56 is_main_or_master_branch, rebase_onto, reset_start_commit, RebaseResult,
57};
58#[cfg(not(feature = "test-utils"))]
59use crate::git_helpers::{
60 cleanup_orphaned_marker, get_start_commit_summary, save_start_commit, start_agent_phase,
61};
62use crate::logger::Colors;
63use crate::logger::Logger;
64use crate::phases::PhaseContext;
65use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
66use crate::prompts::{get_stored_or_generate_prompt, template_context::TemplateContext};
67
68use config_init::initialize_config;
69use context::PipelineContext;
70use detection::detect_project_stack;
71use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
72use resume::{handle_resume_with_validation, offer_resume_if_checkpoint_exists};
73use validation::{
74 resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
75};
76
77pub fn run(args: Args, executor: std::sync::Arc<dyn ProcessExecutor>) -> anyhow::Result<()> {
97 let colors = Colors::new();
98 let logger = Logger::new(colors);
99
100 if let Some(ref override_dir) = args.working_dir_override {
103 std::env::set_current_dir(override_dir)?;
104 }
105
106 let Some(init_result) = initialize_config(&args, colors, &logger)? else {
108 return Ok(()); };
110
111 let config_init::ConfigInitResult {
112 config,
113 registry,
114 config_path,
115 config_sources,
116 } = init_result;
117
118 let validated = resolve_required_agents(&config)?;
120 let developer_agent = validated.developer_agent;
121 let reviewer_agent = validated.reviewer_agent;
122
123 if handle_listing_commands(&args, ®istry, colors) {
125 return Ok(());
126 }
127
128 if args.recovery.diagnose {
130 handle_diagnose(
131 colors,
132 &config,
133 ®istry,
134 &config_path,
135 &config_sources,
136 &*executor,
137 );
138 return Ok(());
139 }
140
141 validate_agent_chains(®istry, colors);
143
144 let mut handler = effect_handler::RealAppEffectHandler::new();
148 if handle_plumbing_commands(&args, &logger, colors, &mut handler, None)? {
149 return Ok(());
150 }
151
152 let Some(repo_root) = validate_and_setup_agents(
154 AgentSetupParams {
155 config: &config,
156 registry: ®istry,
157 developer_agent: &developer_agent,
158 reviewer_agent: &reviewer_agent,
159 config_path: &config_path,
160 colors,
161 logger: &logger,
162 working_dir_override: args.working_dir_override.as_deref(),
163 },
164 &mut handler,
165 )?
166 else {
167 return Ok(());
168 };
169
170 let workspace: std::sync::Arc<dyn crate::workspace::Workspace> =
172 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()));
173
174 (prepare_pipeline_or_exit(PipelinePreparationParams {
176 args,
177 config,
178 registry,
179 developer_agent,
180 reviewer_agent,
181 repo_root,
182 logger,
183 colors,
184 executor,
185 handler: &mut handler,
186 workspace,
187 })?)
188 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
189}
190
191#[cfg(feature = "test-utils")]
211pub fn run_with_config(
212 args: Args,
213 executor: std::sync::Arc<dyn ProcessExecutor>,
214 config: crate::config::Config,
215 registry: AgentRegistry,
216) -> anyhow::Result<()> {
217 let mut handler = effect_handler::RealAppEffectHandler::new();
219 run_with_config_and_resolver(
220 args,
221 executor,
222 config,
223 registry,
224 &crate::config::RealConfigEnvironment,
225 &mut handler,
226 None, )
228}
229
230#[cfg(feature = "test-utils")]
253pub fn run_with_config_and_resolver<
254 P: crate::config::ConfigEnvironment,
255 H: effect::AppEffectHandler,
256>(
257 args: Args,
258 executor: std::sync::Arc<dyn ProcessExecutor>,
259 config: crate::config::Config,
260 registry: AgentRegistry,
261 path_resolver: &P,
262 handler: &mut H,
263 workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
264) -> anyhow::Result<()> {
265 use crate::cli::{
266 handle_extended_help, handle_init_global_with, handle_init_prompt_with,
267 handle_list_work_guides, handle_smart_init_with,
268 };
269
270 let colors = Colors::new();
271 let logger = Logger::new(colors);
272
273 if let Some(ref override_dir) = args.working_dir_override {
275 std::env::set_current_dir(override_dir)?;
276 }
277
278 if args.recovery.extended_help {
280 handle_extended_help();
281 if args.work_guide_list.list_work_guides {
282 println!();
283 handle_list_work_guides(colors);
284 }
285 return Ok(());
286 }
287
288 if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
290 return Ok(());
291 }
292
293 if let Some(ref template_name) = args.init_prompt {
295 if handle_init_prompt_with(
296 template_name,
297 args.unified_init.force_init,
298 colors,
299 path_resolver,
300 )? {
301 return Ok(());
302 }
303 }
304
305 if args.unified_init.init.is_some()
307 && handle_smart_init_with(
308 args.unified_init.init.as_deref(),
309 args.unified_init.force_init,
310 colors,
311 path_resolver,
312 )?
313 {
314 return Ok(());
315 }
316
317 if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
319 return Ok(());
320 }
321
322 if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
324 return Ok(());
325 }
326
327 if args.legacy_init.init_legacy {
329 let repo_root = match handler.execute(effect::AppEffect::GitGetRepoRoot) {
330 effect::AppEffectResult::Path(p) => Some(p),
331 _ => None,
332 };
333 let legacy_path = repo_root.map_or_else(
334 || std::path::PathBuf::from(".agent/agents.toml"),
335 |root| root.join(".agent/agents.toml"),
336 );
337 if crate::cli::handle_init_legacy(colors, &legacy_path)? {
338 return Ok(());
339 }
340 }
341
342 let config_path = std::path::PathBuf::from("test-config");
344
345 let validated = resolve_required_agents(&config)?;
347 let developer_agent = validated.developer_agent;
348 let reviewer_agent = validated.reviewer_agent;
349
350 if handle_listing_commands(&args, ®istry, colors) {
352 return Ok(());
353 }
354
355 if args.recovery.diagnose {
357 handle_diagnose(colors, &config, ®istry, &config_path, &[], &*executor);
358 return Ok(());
359 }
360
361 if handle_plumbing_commands(
364 &args,
365 &logger,
366 colors,
367 handler,
368 workspace.as_ref().map(|w| w.as_ref()),
369 )? {
370 return Ok(());
371 }
372
373 let Some(repo_root) = validate_and_setup_agents(
375 AgentSetupParams {
376 config: &config,
377 registry: ®istry,
378 developer_agent: &developer_agent,
379 reviewer_agent: &reviewer_agent,
380 config_path: &config_path,
381 colors,
382 logger: &logger,
383 working_dir_override: args.working_dir_override.as_deref(),
384 },
385 handler,
386 )?
387 else {
388 return Ok(());
389 };
390
391 let workspace = workspace.unwrap_or_else(|| {
393 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
394 });
395
396 (prepare_pipeline_or_exit(PipelinePreparationParams {
398 args,
399 config,
400 registry,
401 developer_agent,
402 reviewer_agent,
403 repo_root,
404 logger,
405 colors,
406 executor,
407 handler,
408 workspace,
409 })?)
410 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
411}
412
413#[cfg(feature = "test-utils")]
417pub struct RunWithHandlersParams<'a, 'ctx, P, A, E>
418where
419 P: crate::config::ConfigEnvironment,
420 A: effect::AppEffectHandler,
421 E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
422{
423 pub args: Args,
424 pub executor: std::sync::Arc<dyn ProcessExecutor>,
425 pub config: crate::config::Config,
426 pub registry: AgentRegistry,
427 pub path_resolver: &'a P,
428 pub app_handler: &'a mut A,
429 pub effect_handler: &'a mut E,
430 pub workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
431 pub _marker: std::marker::PhantomData<&'ctx ()>,
433}
434
435#[cfg(feature = "test-utils")]
463pub fn run_with_config_and_handlers<'a, 'ctx, P, A, E>(
464 params: RunWithHandlersParams<'a, 'ctx, P, A, E>,
465) -> anyhow::Result<()>
466where
467 P: crate::config::ConfigEnvironment,
468 A: effect::AppEffectHandler,
469 E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
470{
471 let RunWithHandlersParams {
472 args,
473 executor,
474 config,
475 registry,
476 path_resolver,
477 app_handler,
478 effect_handler,
479 workspace,
480 ..
481 } = params;
482 use crate::cli::{
483 handle_extended_help, handle_init_global_with, handle_init_prompt_with,
484 handle_list_work_guides, handle_smart_init_with,
485 };
486
487 let colors = Colors::new();
488 let logger = Logger::new(colors);
489
490 if let Some(ref override_dir) = args.working_dir_override {
492 std::env::set_current_dir(override_dir)?;
493 }
494
495 if args.recovery.extended_help {
497 handle_extended_help();
498 if args.work_guide_list.list_work_guides {
499 println!();
500 handle_list_work_guides(colors);
501 }
502 return Ok(());
503 }
504
505 if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
507 return Ok(());
508 }
509
510 if let Some(ref template_name) = args.init_prompt {
512 if handle_init_prompt_with(
513 template_name,
514 args.unified_init.force_init,
515 colors,
516 path_resolver,
517 )? {
518 return Ok(());
519 }
520 }
521
522 if args.unified_init.init.is_some()
524 && handle_smart_init_with(
525 args.unified_init.init.as_deref(),
526 args.unified_init.force_init,
527 colors,
528 path_resolver,
529 )?
530 {
531 return Ok(());
532 }
533
534 if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
536 return Ok(());
537 }
538
539 if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
541 return Ok(());
542 }
543
544 if args.legacy_init.init_legacy {
546 let repo_root = match app_handler.execute(effect::AppEffect::GitGetRepoRoot) {
547 effect::AppEffectResult::Path(p) => Some(p),
548 _ => None,
549 };
550 let legacy_path = repo_root.map_or_else(
551 || std::path::PathBuf::from(".agent/agents.toml"),
552 |root| root.join(".agent/agents.toml"),
553 );
554 if crate::cli::handle_init_legacy(colors, &legacy_path)? {
555 return Ok(());
556 }
557 }
558
559 let config_path = std::path::PathBuf::from("test-config");
561
562 let validated = resolve_required_agents(&config)?;
564 let developer_agent = validated.developer_agent;
565 let reviewer_agent = validated.reviewer_agent;
566
567 if handle_listing_commands(&args, ®istry, colors) {
569 return Ok(());
570 }
571
572 if args.recovery.diagnose {
574 handle_diagnose(colors, &config, ®istry, &config_path, &[], &*executor);
575 return Ok(());
576 }
577
578 if handle_plumbing_commands(
581 &args,
582 &logger,
583 colors,
584 app_handler,
585 workspace.as_ref().map(|w| w.as_ref()),
586 )? {
587 return Ok(());
588 }
589
590 let Some(repo_root) = validate_and_setup_agents(
592 AgentSetupParams {
593 config: &config,
594 registry: ®istry,
595 developer_agent: &developer_agent,
596 reviewer_agent: &reviewer_agent,
597 config_path: &config_path,
598 colors,
599 logger: &logger,
600 working_dir_override: args.working_dir_override.as_deref(),
601 },
602 app_handler,
603 )?
604 else {
605 return Ok(());
606 };
607
608 let workspace = workspace.unwrap_or_else(|| {
610 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
611 });
612
613 let ctx = prepare_pipeline_or_exit(PipelinePreparationParams {
615 args,
616 config,
617 registry,
618 developer_agent,
619 reviewer_agent,
620 repo_root,
621 logger,
622 colors,
623 executor,
624 handler: app_handler,
625 workspace,
626 })?;
627
628 match ctx {
630 Some(ctx) => run_pipeline_with_effect_handler(&ctx, effect_handler),
631 None => Ok(()),
632 }
633}
634
635fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
639 if args.agent_list.list_agents {
640 handle_list_agents(registry);
641 return true;
642 }
643 if args.agent_list.list_available_agents {
644 handle_list_available_agents(registry);
645 return true;
646 }
647 if args.provider_list.list_providers {
648 handle_list_providers(colors);
649 return true;
650 }
651
652 let template_cmds = &args.template_commands;
654 if template_cmds.init_templates_enabled()
655 || template_cmds.validate
656 || template_cmds.show.is_some()
657 || template_cmds.list
658 || template_cmds.list_all
659 || template_cmds.variables.is_some()
660 || template_cmds.render.is_some()
661 {
662 let _ = handle_template_commands(template_cmds, colors);
663 return true;
664 }
665
666 false
667}
668
669fn handle_plumbing_commands<H: effect::AppEffectHandler>(
680 args: &Args,
681 logger: &Logger,
682 colors: Colors,
683 handler: &mut H,
684 workspace: Option<&dyn crate::workspace::Workspace>,
685) -> anyhow::Result<bool> {
686 use plumbing::{handle_apply_commit_with_handler, handle_show_commit_msg_with_workspace};
687
688 fn setup_working_dir_via_handler<H: effect::AppEffectHandler>(
690 override_dir: Option<&std::path::Path>,
691 handler: &mut H,
692 ) -> anyhow::Result<()> {
693 use effect::{AppEffect, AppEffectResult};
694
695 if let Some(dir) = override_dir {
696 match handler.execute(AppEffect::SetCurrentDir {
697 path: dir.to_path_buf(),
698 }) {
699 AppEffectResult::Ok => Ok(()),
700 AppEffectResult::Error(e) => anyhow::bail!(e),
701 other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
702 }
703 } else {
704 match handler.execute(AppEffect::GitRequireRepo) {
706 AppEffectResult::Ok => {}
707 AppEffectResult::Error(e) => anyhow::bail!(e),
708 other => anyhow::bail!("unexpected result from GitRequireRepo: {:?}", other),
709 }
710 let repo_root = match handler.execute(AppEffect::GitGetRepoRoot) {
712 AppEffectResult::Path(p) => p,
713 AppEffectResult::Error(e) => anyhow::bail!(e),
714 other => anyhow::bail!("unexpected result from GitGetRepoRoot: {:?}", other),
715 };
716 match handler.execute(AppEffect::SetCurrentDir { path: repo_root }) {
718 AppEffectResult::Ok => Ok(()),
719 AppEffectResult::Error(e) => anyhow::bail!(e),
720 other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
721 }
722 }
723 }
724
725 if args.commit_display.show_commit_msg {
727 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
728 if let Some(ws) = workspace {
729 return handle_show_commit_msg_with_workspace(ws).map(|()| true);
730 }
731 return handle_show_commit_msg().map(|()| true);
732 }
733
734 if args.commit_plumbing.apply_commit {
736 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
737 if let Some(ws) = workspace {
738 return handle_apply_commit_with_handler(ws, handler, logger, colors).map(|()| true);
739 }
740 return handle_apply_commit(logger, colors).map(|()| true);
741 }
742
743 if args.commit_display.reset_start_commit {
745 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
746
747 return match handler.execute(effect::AppEffect::GitResetStartCommit) {
749 effect::AppEffectResult::String(oid) => {
750 let short_oid = &oid[..8.min(oid.len())];
752 logger.success(&format!("Starting commit reference reset ({})", short_oid));
753 logger.info(".agent/start_commit has been updated");
754 Ok(true)
755 }
756 effect::AppEffectResult::Error(e) => {
757 logger.error(&format!("Failed to reset starting commit: {e}"));
758 anyhow::bail!("Failed to reset starting commit");
759 }
760 other => {
761 drop(other);
764 match reset_start_commit() {
765 Ok(result) => {
766 let short_oid = &result.oid[..8.min(result.oid.len())];
767 if result.fell_back_to_head {
768 logger.success(&format!(
769 "Starting commit reference reset to current HEAD ({})",
770 short_oid
771 ));
772 logger.info("On main/master branch - using HEAD as baseline");
773 } else if let Some(ref branch) = result.default_branch {
774 logger.success(&format!(
775 "Starting commit reference reset to merge-base with '{}' ({})",
776 branch, short_oid
777 ));
778 logger.info("Baseline set to common ancestor with default branch");
779 } else {
780 logger.success(&format!(
781 "Starting commit reference reset ({})",
782 short_oid
783 ));
784 }
785 logger.info(".agent/start_commit has been updated");
786 Ok(true)
787 }
788 Err(e) => {
789 logger.error(&format!("Failed to reset starting commit: {e}"));
790 anyhow::bail!("Failed to reset starting commit");
791 }
792 }
793 }
794 };
795 }
796
797 if args.commit_display.show_baseline {
799 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
800
801 return match handle_show_baseline() {
802 Ok(()) => Ok(true),
803 Err(e) => {
804 logger.error(&format!("Failed to show baseline: {e}"));
805 anyhow::bail!("Failed to show baseline");
806 }
807 };
808 }
809
810 Ok(false)
811}
812
813struct PipelinePreparationParams<'a, H: effect::AppEffectHandler> {
817 args: Args,
818 config: crate::config::Config,
819 registry: AgentRegistry,
820 developer_agent: String,
821 reviewer_agent: String,
822 repo_root: std::path::PathBuf,
823 logger: Logger,
824 colors: Colors,
825 executor: std::sync::Arc<dyn ProcessExecutor>,
826 handler: &'a mut H,
827 workspace: std::sync::Arc<dyn crate::workspace::Workspace>,
832}
833
834fn prepare_pipeline_or_exit<H: effect::AppEffectHandler>(
838 params: PipelinePreparationParams<'_, H>,
839) -> anyhow::Result<Option<PipelineContext>> {
840 let PipelinePreparationParams {
841 args,
842 config,
843 registry,
844 developer_agent,
845 reviewer_agent,
846 repo_root,
847 mut logger,
848 colors,
849 executor,
850 handler,
851 workspace,
852 } = params;
853
854 effectful::ensure_files_effectful(handler, config.isolation_mode)
856 .map_err(|e| anyhow::anyhow!("{}", e))?;
857
858 if config.isolation_mode {
860 effectful::reset_context_for_isolation_effectful(handler)
861 .map_err(|e| anyhow::anyhow!("{}", e))?;
862 }
863
864 logger = logger.with_log_file(".agent/logs/pipeline.log");
865
866 if args.recovery.dry_run {
868 let developer_display = registry.display_name(&developer_agent);
869 let reviewer_display = registry.display_name(&reviewer_agent);
870 handle_dry_run(
871 &logger,
872 colors,
873 &config,
874 &developer_display,
875 &reviewer_display,
876 &repo_root,
877 )?;
878 return Ok(None);
879 }
880
881 let template_context =
883 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
884
885 if args.rebase_flags.rebase_only {
887 handle_rebase_only(
888 &args,
889 &config,
890 &template_context,
891 &logger,
892 colors,
893 std::sync::Arc::clone(&executor),
894 &repo_root,
895 )?;
896 return Ok(None);
897 }
898
899 if args.commit_plumbing.generate_commit_msg {
901 handle_generate_commit_msg(plumbing::CommitGenerationConfig {
902 config: &config,
903 template_context: &template_context,
904 workspace: &*workspace,
905 registry: ®istry,
906 logger: &logger,
907 colors,
908 developer_agent: &developer_agent,
909 _reviewer_agent: &reviewer_agent,
910 executor: std::sync::Arc::clone(&executor),
911 })?;
912 return Ok(None);
913 }
914
915 let developer_display = registry.display_name(&developer_agent);
917 let reviewer_display = registry.display_name(&reviewer_agent);
918
919 let ctx = PipelineContext {
921 args,
922 config,
923 registry,
924 developer_agent,
925 reviewer_agent,
926 developer_display,
927 reviewer_display,
928 repo_root,
929 workspace,
930 logger,
931 colors,
932 template_context,
933 executor,
934 };
935 Ok(Some(ctx))
936}
937
938struct AgentSetupParams<'a> {
940 config: &'a crate::config::Config,
941 registry: &'a AgentRegistry,
942 developer_agent: &'a str,
943 reviewer_agent: &'a str,
944 config_path: &'a std::path::Path,
945 colors: Colors,
946 logger: &'a Logger,
947 working_dir_override: Option<&'a std::path::Path>,
950}
951
952fn validate_and_setup_agents<H: effect::AppEffectHandler>(
957 params: AgentSetupParams<'_>,
958 handler: &mut H,
959) -> anyhow::Result<Option<std::path::PathBuf>> {
960 let AgentSetupParams {
961 config,
962 registry,
963 developer_agent,
964 reviewer_agent,
965 config_path,
966 colors,
967 logger,
968 working_dir_override,
969 } = params;
970 validate_agent_commands(
972 config,
973 registry,
974 developer_agent,
975 reviewer_agent,
976 config_path,
977 )?;
978
979 validate_can_commit(
981 config,
982 registry,
983 developer_agent,
984 reviewer_agent,
985 config_path,
986 )?;
987
988 let repo_root = if let Some(override_dir) = working_dir_override {
990 handler.execute(effect::AppEffect::SetCurrentDir {
992 path: override_dir.to_path_buf(),
993 });
994 override_dir.to_path_buf()
995 } else {
996 let require_result = handler.execute(effect::AppEffect::GitRequireRepo);
998 if let effect::AppEffectResult::Error(e) = require_result {
999 anyhow::bail!("Not in a git repository: {}", e);
1000 }
1001
1002 let root_result = handler.execute(effect::AppEffect::GitGetRepoRoot);
1003 let root = match root_result {
1004 effect::AppEffectResult::Path(p) => p,
1005 effect::AppEffectResult::Error(e) => {
1006 anyhow::bail!("Failed to get repo root: {}", e);
1007 }
1008 _ => anyhow::bail!("Unexpected result from GitGetRepoRoot"),
1009 };
1010
1011 handler.execute(effect::AppEffect::SetCurrentDir { path: root.clone() });
1012 root
1013 };
1014
1015 let should_continue = setup_git_and_prompt_file(config, colors, logger, handler)?;
1017 if should_continue.is_none() {
1018 return Ok(None);
1019 }
1020
1021 Ok(Some(repo_root))
1022}
1023
1024fn setup_git_and_prompt_file<H: effect::AppEffectHandler>(
1029 config: &crate::config::Config,
1030 colors: Colors,
1031 logger: &Logger,
1032 handler: &mut H,
1033) -> anyhow::Result<Option<()>> {
1034 let prompt_exists =
1035 effectful::check_prompt_exists_effectful(handler).map_err(|e| anyhow::anyhow!("{}", e))?;
1036
1037 if config.behavior.interactive && !prompt_exists {
1040 if let Some(template_name) = prompt_template_selection(colors) {
1041 create_prompt_from_template(&template_name, colors)?;
1042 println!();
1043 logger.info(
1044 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
1045 );
1046 logger.info(&format!(
1047 "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
1048 config.commit_msg
1049 ));
1050 return Ok(None);
1051 }
1052 println!();
1053 logger.error("PROMPT.md not found in current directory.");
1054 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1055 println!();
1056 logger.info("To get started:");
1057 logger.info(" ralph --init # Smart setup wizard");
1058 logger.info(" ralph --init bug-fix # Create from Work Guide");
1059 logger.info(" ralph --list-work-guides # See all Work Guides");
1060 println!();
1061 return Ok(None);
1062 }
1063
1064 if !prompt_exists {
1066 logger.error("PROMPT.md not found in current directory.");
1067 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1068 println!();
1069 logger.info("Quick start:");
1070 logger.info(" ralph --init # Smart setup wizard");
1071 logger.info(" ralph --init bug-fix # Create from Work Guide");
1072 logger.info(" ralph --list-work-guides # See all Work Guides");
1073 println!();
1074 logger.info("Use -i flag for interactive mode to be prompted for template selection.");
1075 println!();
1076 return Ok(None);
1077 }
1078
1079 Ok(Some(()))
1080}
1081
1082fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
1084 run_pipeline_with_default_handler(ctx)
1086}
1087
1088fn run_pipeline_with_default_handler(ctx: &PipelineContext) -> anyhow::Result<()> {
1092 use crate::app::event_loop::EventLoopConfig;
1093 #[cfg(not(feature = "test-utils"))]
1094 use crate::reducer::MainEffectHandler;
1095 use crate::reducer::PipelineState;
1096
1097 let resume_result = offer_resume_if_checkpoint_exists(
1099 &ctx.args,
1100 &ctx.config,
1101 &ctx.registry,
1102 &ctx.logger,
1103 &ctx.developer_agent,
1104 &ctx.reviewer_agent,
1105 );
1106
1107 let resume_result = match resume_result {
1109 Some(result) => Some(result),
1110 None => handle_resume_with_validation(
1111 &ctx.args,
1112 &ctx.config,
1113 &ctx.registry,
1114 &ctx.logger,
1115 &ctx.developer_display,
1116 &ctx.reviewer_display,
1117 ),
1118 };
1119
1120 let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1121
1122 let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1124 use crate::checkpoint::RunContext;
1125 RunContext::from_checkpoint(checkpoint)
1126 } else {
1127 use crate::checkpoint::RunContext;
1128 RunContext::new()
1129 };
1130
1131 let config = if let Some(ref checkpoint) = resume_checkpoint {
1133 use crate::checkpoint::apply_checkpoint_to_config;
1134 let mut restored_config = ctx.config.clone();
1135 apply_checkpoint_to_config(&mut restored_config, checkpoint);
1136 ctx.logger.info("Restored configuration from checkpoint:");
1137 if checkpoint.cli_args.developer_iters > 0 {
1138 ctx.logger.info(&format!(
1139 " Developer iterations: {} (from checkpoint)",
1140 checkpoint.cli_args.developer_iters
1141 ));
1142 }
1143 if checkpoint.cli_args.reviewer_reviews > 0 {
1144 ctx.logger.info(&format!(
1145 " Reviewer passes: {} (from checkpoint)",
1146 checkpoint.cli_args.reviewer_reviews
1147 ));
1148 }
1149 restored_config
1150 } else {
1151 ctx.config.clone()
1152 };
1153
1154 if let Some(ref checkpoint) = resume_checkpoint {
1156 use crate::checkpoint::restore::restore_environment_from_checkpoint;
1157 let restored_count = restore_environment_from_checkpoint(checkpoint);
1158 if restored_count > 0 {
1159 ctx.logger.info(&format!(
1160 " Restored {} environment variable(s) from checkpoint",
1161 restored_count
1162 ));
1163 }
1164 }
1165
1166 let mut git_helpers = crate::git_helpers::GitHelpers::new();
1170
1171 #[cfg(feature = "test-utils")]
1172 {
1173 use crate::git_helpers::{
1174 cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1175 };
1176 cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1178 create_marker_with_workspace(&*ctx.workspace)?;
1179 }
1181 #[cfg(not(feature = "test-utils"))]
1182 {
1183 cleanup_orphaned_marker(&ctx.logger)?;
1184 start_agent_phase(&mut git_helpers)?;
1185 }
1186 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1187
1188 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1190 print_pipeline_info_with_config(ctx, &config);
1191 validate_prompt_and_setup_backup(ctx)?;
1192
1193 let mut prompt_monitor = setup_prompt_monitor(ctx);
1195
1196 let (_project_stack, review_guidelines) =
1198 detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1199
1200 print_review_guidelines(ctx, review_guidelines.as_ref());
1201 println!();
1202
1203 let (mut timer, mut stats) = (Timer::new(), Stats::new());
1205 let mut phase_ctx = create_phase_context_with_config(
1206 ctx,
1207 &config,
1208 &mut timer,
1209 &mut stats,
1210 review_guidelines.as_ref(),
1211 &run_context,
1212 resume_checkpoint.as_ref(),
1213 );
1214 save_start_commit_or_warn(ctx);
1215
1216 let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1219 checkpoint.phase
1220 } else {
1221 PipelinePhase::Planning
1222 };
1223 setup_interrupt_context_for_pipeline(
1224 initial_phase,
1225 config.developer_iters,
1226 config.reviewer_reviews,
1227 &phase_ctx.execution_history,
1228 &phase_ctx.prompt_history,
1229 &run_context,
1230 );
1231
1232 let _interrupt_guard = defer_clear_interrupt_context();
1234
1235 let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
1237 if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
1239 !checkpoint.cli_args.skip_rebase
1240 } else {
1241 ctx.args.rebase_flags.with_rebase
1243 }
1244 } else {
1245 ctx.args.rebase_flags.with_rebase
1246 };
1247
1248 if should_run_rebase {
1250 run_initial_rebase(ctx, &mut phase_ctx, &run_context, &*ctx.executor)?;
1251 update_interrupt_context_from_phase(
1253 &phase_ctx,
1254 PipelinePhase::Planning,
1255 config.developer_iters,
1256 config.reviewer_reviews,
1257 &run_context,
1258 );
1259 } else {
1260 if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
1262 let builder = CheckpointBuilder::new()
1263 .phase(PipelinePhase::Planning, 0, config.developer_iters)
1264 .reviewer_pass(0, config.reviewer_reviews)
1265 .skip_rebase(true) .capture_from_context(
1267 &config,
1268 &ctx.registry,
1269 &ctx.developer_agent,
1270 &ctx.reviewer_agent,
1271 &ctx.logger,
1272 &run_context,
1273 )
1274 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
1275 .with_execution_history(phase_ctx.execution_history.clone())
1276 .with_prompt_history(phase_ctx.clone_prompt_history());
1277
1278 if let Some(checkpoint) = builder.build() {
1279 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1280 }
1281 }
1282 update_interrupt_context_from_phase(
1284 &phase_ctx,
1285 PipelinePhase::Planning,
1286 config.developer_iters,
1287 config.reviewer_reviews,
1288 &run_context,
1289 );
1290 }
1291
1292 let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1298 PipelineState::from(checkpoint.clone())
1300 } else {
1301 PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1303 };
1304
1305 let event_loop_config = EventLoopConfig {
1307 max_iterations: 1000,
1308 enable_checkpointing: config.features.checkpoint_enabled,
1309 };
1310
1311 let execution_history_before = phase_ctx.execution_history.clone();
1313 let prompt_history_before = phase_ctx.clone_prompt_history();
1314
1315 #[cfg(feature = "test-utils")]
1318 let loop_result = {
1319 use crate::app::event_loop::run_event_loop_with_handler;
1320 use crate::reducer::mock_effect_handler::MockEffectHandler;
1321 let mut handler = MockEffectHandler::new(initial_state.clone());
1322 let phase_ctx_ref = &mut phase_ctx;
1323 run_event_loop_with_handler(
1324 phase_ctx_ref,
1325 Some(initial_state),
1326 event_loop_config,
1327 &mut handler,
1328 )
1329 };
1330 #[cfg(not(feature = "test-utils"))]
1331 let loop_result = {
1332 use crate::app::event_loop::run_event_loop_with_handler;
1333 let mut handler = MainEffectHandler::new(initial_state.clone());
1334 let phase_ctx_ref = &mut phase_ctx;
1335 run_event_loop_with_handler(
1336 phase_ctx_ref,
1337 Some(initial_state),
1338 event_loop_config,
1339 &mut handler,
1340 )
1341 };
1342
1343 let loop_result = loop_result?;
1345 if loop_result.completed {
1346 ctx.logger
1347 .success("Pipeline completed successfully via reducer event loop");
1348 ctx.logger.info(&format!(
1349 "Total events processed: {}",
1350 loop_result.events_processed
1351 ));
1352 } else {
1353 ctx.logger.warn("Pipeline exited without completion marker");
1354 }
1355
1356 if config.features.checkpoint_enabled {
1358 let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1359 let builder = CheckpointBuilder::new()
1360 .phase(
1361 PipelinePhase::Complete,
1362 config.developer_iters,
1363 config.developer_iters,
1364 )
1365 .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1366 .skip_rebase(skip_rebase)
1367 .capture_from_context(
1368 &config,
1369 &ctx.registry,
1370 &ctx.developer_agent,
1371 &ctx.reviewer_agent,
1372 &ctx.logger,
1373 &run_context,
1374 )
1375 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1376
1377 let builder = builder
1378 .with_execution_history(execution_history_before)
1379 .with_prompt_history(prompt_history_before);
1380
1381 if let Some(checkpoint) = builder.build() {
1382 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1383 }
1384 }
1385
1386 check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1388 update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1389
1390 finalize_pipeline(
1392 &mut agent_phase_guard,
1393 &ctx.logger,
1394 ctx.colors,
1395 &config,
1396 finalization::RuntimeStats {
1397 timer: &timer,
1398 stats: &stats,
1399 },
1400 prompt_monitor,
1401 Some(&*ctx.workspace),
1402 );
1403 Ok(())
1404}
1405
1406#[cfg(feature = "test-utils")]
1420pub fn run_pipeline_with_effect_handler<'ctx, H>(
1421 ctx: &PipelineContext,
1422 effect_handler: &mut H,
1423) -> anyhow::Result<()>
1424where
1425 H: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
1426{
1427 use crate::app::event_loop::EventLoopConfig;
1428 use crate::reducer::PipelineState;
1429
1430 let resume_result = offer_resume_if_checkpoint_exists(
1432 &ctx.args,
1433 &ctx.config,
1434 &ctx.registry,
1435 &ctx.logger,
1436 &ctx.developer_agent,
1437 &ctx.reviewer_agent,
1438 );
1439
1440 let resume_result = match resume_result {
1442 Some(result) => Some(result),
1443 None => handle_resume_with_validation(
1444 &ctx.args,
1445 &ctx.config,
1446 &ctx.registry,
1447 &ctx.logger,
1448 &ctx.developer_display,
1449 &ctx.reviewer_display,
1450 ),
1451 };
1452
1453 let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1454
1455 let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1457 use crate::checkpoint::RunContext;
1458 RunContext::from_checkpoint(checkpoint)
1459 } else {
1460 use crate::checkpoint::RunContext;
1461 RunContext::new()
1462 };
1463
1464 let config = if let Some(ref checkpoint) = resume_checkpoint {
1466 use crate::checkpoint::apply_checkpoint_to_config;
1467 let mut restored_config = ctx.config.clone();
1468 apply_checkpoint_to_config(&mut restored_config, checkpoint);
1469 restored_config
1470 } else {
1471 ctx.config.clone()
1472 };
1473
1474 let mut git_helpers = crate::git_helpers::GitHelpers::new();
1478
1479 #[cfg(feature = "test-utils")]
1480 {
1481 use crate::git_helpers::{
1482 cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1483 };
1484 cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1486 create_marker_with_workspace(&*ctx.workspace)?;
1487 }
1489 #[cfg(not(feature = "test-utils"))]
1490 {
1491 cleanup_orphaned_marker(&ctx.logger)?;
1492 start_agent_phase(&mut git_helpers)?;
1493 }
1494 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1495
1496 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1498 print_pipeline_info_with_config(ctx, &config);
1499 validate_prompt_and_setup_backup(ctx)?;
1500
1501 let mut prompt_monitor = setup_prompt_monitor(ctx);
1503
1504 let (_project_stack, review_guidelines) =
1506 detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1507
1508 print_review_guidelines(ctx, review_guidelines.as_ref());
1509 println!();
1510
1511 let (mut timer, mut stats) = (Timer::new(), Stats::new());
1513 let mut phase_ctx = create_phase_context_with_config(
1514 ctx,
1515 &config,
1516 &mut timer,
1517 &mut stats,
1518 review_guidelines.as_ref(),
1519 &run_context,
1520 resume_checkpoint.as_ref(),
1521 );
1522 save_start_commit_or_warn(ctx);
1523
1524 let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1526 checkpoint.phase
1527 } else {
1528 PipelinePhase::Planning
1529 };
1530 setup_interrupt_context_for_pipeline(
1531 initial_phase,
1532 config.developer_iters,
1533 config.reviewer_reviews,
1534 &phase_ctx.execution_history,
1535 &phase_ctx.prompt_history,
1536 &run_context,
1537 );
1538
1539 let _interrupt_guard = defer_clear_interrupt_context();
1541
1542 let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1544 PipelineState::from(checkpoint.clone())
1545 } else {
1546 PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1547 };
1548
1549 let event_loop_config = EventLoopConfig {
1551 max_iterations: 1000,
1552 enable_checkpointing: config.features.checkpoint_enabled,
1553 };
1554
1555 let execution_history_before = phase_ctx.execution_history.clone();
1557 let prompt_history_before = phase_ctx.clone_prompt_history();
1558
1559 effect_handler.update_state(initial_state.clone());
1561 let loop_result = {
1562 use crate::app::event_loop::run_event_loop_with_handler;
1563 let phase_ctx_ref = &mut phase_ctx;
1564 run_event_loop_with_handler(
1565 phase_ctx_ref,
1566 Some(initial_state),
1567 event_loop_config,
1568 effect_handler,
1569 )
1570 };
1571
1572 let loop_result = loop_result?;
1574 if loop_result.completed {
1575 ctx.logger
1576 .success("Pipeline completed successfully via reducer event loop");
1577 ctx.logger.info(&format!(
1578 "Total events processed: {}",
1579 loop_result.events_processed
1580 ));
1581 } else {
1582 ctx.logger.warn("Pipeline exited without completion marker");
1583 }
1584
1585 if config.features.checkpoint_enabled {
1587 let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1588 let builder = CheckpointBuilder::new()
1589 .phase(
1590 PipelinePhase::Complete,
1591 config.developer_iters,
1592 config.developer_iters,
1593 )
1594 .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1595 .skip_rebase(skip_rebase)
1596 .capture_from_context(
1597 &config,
1598 &ctx.registry,
1599 &ctx.developer_agent,
1600 &ctx.reviewer_agent,
1601 &ctx.logger,
1602 &run_context,
1603 )
1604 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1605
1606 let builder = builder
1607 .with_execution_history(execution_history_before)
1608 .with_prompt_history(prompt_history_before);
1609
1610 if let Some(checkpoint) = builder.build() {
1611 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1612 }
1613 }
1614
1615 check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1617 update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1618
1619 finalize_pipeline(
1621 &mut agent_phase_guard,
1622 &ctx.logger,
1623 ctx.colors,
1624 &config,
1625 finalization::RuntimeStats {
1626 timer: &timer,
1627 stats: &stats,
1628 },
1629 prompt_monitor,
1630 Some(&*ctx.workspace),
1631 );
1632 Ok(())
1633}
1634
1635fn setup_interrupt_context_for_pipeline(
1640 phase: PipelinePhase,
1641 total_iterations: u32,
1642 total_reviewer_passes: u32,
1643 execution_history: &crate::checkpoint::ExecutionHistory,
1644 prompt_history: &std::collections::HashMap<String, String>,
1645 run_context: &crate::checkpoint::RunContext,
1646) {
1647 use crate::interrupt::{set_interrupt_context, InterruptContext};
1648
1649 let (iteration, reviewer_pass) = match phase {
1651 PipelinePhase::Development => (1, 0),
1652 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1653 (total_iterations, 1)
1654 }
1655 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1656 (total_iterations, total_reviewer_passes)
1657 }
1658 _ => (0, 0),
1659 };
1660
1661 let context = InterruptContext {
1662 phase,
1663 iteration,
1664 total_iterations,
1665 reviewer_pass,
1666 total_reviewer_passes,
1667 run_context: run_context.clone(),
1668 execution_history: execution_history.clone(),
1669 prompt_history: prompt_history.clone(),
1670 };
1671
1672 set_interrupt_context(context);
1673}
1674
1675fn update_interrupt_context_from_phase(
1680 phase_ctx: &crate::phases::PhaseContext,
1681 phase: PipelinePhase,
1682 total_iterations: u32,
1683 total_reviewer_passes: u32,
1684 run_context: &crate::checkpoint::RunContext,
1685) {
1686 use crate::interrupt::{set_interrupt_context, InterruptContext};
1687
1688 let (iteration, reviewer_pass) = match phase {
1690 PipelinePhase::Development => {
1691 let iter = run_context.actual_developer_runs.max(1);
1693 (iter, 0)
1694 }
1695 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1696 (total_iterations, run_context.actual_reviewer_runs.max(1))
1697 }
1698 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1699 (total_iterations, total_reviewer_passes)
1700 }
1701 _ => (0, 0),
1702 };
1703
1704 let context = InterruptContext {
1705 phase,
1706 iteration,
1707 total_iterations,
1708 reviewer_pass,
1709 total_reviewer_passes,
1710 run_context: run_context.clone(),
1711 execution_history: phase_ctx.execution_history.clone(),
1712 prompt_history: phase_ctx.clone_prompt_history(),
1713 };
1714
1715 set_interrupt_context(context);
1716}
1717
1718fn defer_clear_interrupt_context() -> InterruptContextGuard {
1724 InterruptContextGuard
1725}
1726
1727struct InterruptContextGuard;
1733
1734impl Drop for InterruptContextGuard {
1735 fn drop(&mut self) {
1736 crate::interrupt::clear_interrupt_context();
1737 }
1738}
1739
1740fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
1742 let prompt_validation = validate_prompt_md_with_workspace(
1743 &*ctx.workspace,
1744 ctx.config.behavior.strict_validation,
1745 ctx.args.interactive,
1746 );
1747 for err in &prompt_validation.errors {
1748 ctx.logger.error(err);
1749 }
1750 for warn in &prompt_validation.warnings {
1751 ctx.logger.warn(warn);
1752 }
1753 if !prompt_validation.is_valid() {
1754 anyhow::bail!("PROMPT.md validation errors");
1755 }
1756
1757 match create_prompt_backup_with_workspace(&*ctx.workspace) {
1759 Ok(None) => {}
1760 Ok(Some(warning)) => {
1761 ctx.logger.warn(&format!(
1762 "PROMPT.md backup created but: {warning}. Continuing anyway."
1763 ));
1764 }
1765 Err(e) => {
1766 ctx.logger.warn(&format!(
1767 "Failed to create PROMPT.md backup: {e}. Continuing anyway."
1768 ));
1769 }
1770 }
1771
1772 match make_prompt_read_only_with_workspace(&*ctx.workspace) {
1774 None => {}
1775 Some(warning) => {
1776 ctx.logger.warn(&format!("{warning}. Continuing anyway."));
1777 }
1778 }
1779
1780 Ok(())
1781}
1782
1783fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
1785 match PromptMonitor::new() {
1786 Ok(mut monitor) => {
1787 if let Err(e) = monitor.start() {
1788 ctx.logger.warn(&format!(
1789 "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
1790 ));
1791 None
1792 } else {
1793 if ctx.config.verbosity.is_debug() {
1794 ctx.logger.info("Started real-time PROMPT.md monitoring");
1795 }
1796 Some(monitor)
1797 }
1798 }
1799 Err(e) => {
1800 ctx.logger.warn(&format!(
1801 "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
1802 ));
1803 None
1804 }
1805 }
1806}
1807
1808fn print_review_guidelines(
1810 ctx: &PipelineContext,
1811 review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
1812) {
1813 if let Some(guidelines) = review_guidelines {
1814 ctx.logger.info(&format!(
1815 "Review guidelines: {}{}{}",
1816 ctx.colors.dim(),
1817 guidelines.summary(),
1818 ctx.colors.reset()
1819 ));
1820 }
1821}
1822
1823fn create_phase_context_with_config<'ctx>(
1825 ctx: &'ctx PipelineContext,
1826 config: &'ctx crate::config::Config,
1827 timer: &'ctx mut Timer,
1828 stats: &'ctx mut Stats,
1829 review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
1830 run_context: &'ctx crate::checkpoint::RunContext,
1831 resume_checkpoint: Option<&PipelineCheckpoint>,
1832) -> PhaseContext<'ctx> {
1833 let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
1835 let exec_history = checkpoint
1836 .execution_history
1837 .clone()
1838 .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
1839 let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
1840 (exec_history, prompt_hist)
1841 } else {
1842 (
1843 crate::checkpoint::execution_history::ExecutionHistory::new(),
1844 std::collections::HashMap::new(),
1845 )
1846 };
1847
1848 PhaseContext {
1849 config,
1850 registry: &ctx.registry,
1851 logger: &ctx.logger,
1852 colors: &ctx.colors,
1853 timer,
1854 stats,
1855 developer_agent: &ctx.developer_agent,
1856 reviewer_agent: &ctx.reviewer_agent,
1857 review_guidelines,
1858 template_context: &ctx.template_context,
1859 run_context: run_context.clone(),
1860 execution_history,
1861 prompt_history,
1862 executor: &*ctx.executor,
1863 executor_arc: std::sync::Arc::clone(&ctx.executor),
1864 repo_root: &ctx.repo_root,
1865 workspace: &*ctx.workspace,
1866 }
1867}
1868
1869fn print_pipeline_info_with_config(ctx: &PipelineContext, config: &crate::config::Config) {
1871 ctx.logger.info(&format!(
1872 "Working directory: {}{}{}",
1873 ctx.colors.cyan(),
1874 ctx.repo_root.display(),
1875 ctx.colors.reset()
1876 ));
1877 ctx.logger.info(&format!(
1878 "Commit message: {}{}{}",
1879 ctx.colors.cyan(),
1880 config.commit_msg,
1881 ctx.colors.reset()
1882 ));
1883}
1884
1885fn save_start_commit_or_warn(ctx: &PipelineContext) {
1889 #[cfg(feature = "test-utils")]
1892 {
1893 if ctx.config.verbosity.is_debug() {
1895 ctx.logger.info("Start: 49cb8503 (+18 commits, STALE)");
1896 }
1897 ctx.logger
1898 .warn("Start commit is stale. Consider running: ralph --reset-start-commit");
1899 }
1900
1901 #[cfg(not(feature = "test-utils"))]
1902 {
1903 match save_start_commit() {
1904 Ok(()) => {
1905 if ctx.config.verbosity.is_debug() {
1906 ctx.logger
1907 .info("Saved starting commit for incremental diff generation");
1908 }
1909 }
1910 Err(e) => {
1911 ctx.logger.warn(&format!(
1912 "Failed to save starting commit: {e}. \
1913 Incremental diffs may be unavailable as a result."
1914 ));
1915 ctx.logger.info(
1916 "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
1917 );
1918 }
1919 }
1920
1921 match get_start_commit_summary() {
1923 Ok(summary) => {
1924 if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale
1925 {
1926 ctx.logger.info(&summary.format_compact());
1927 if summary.is_stale {
1928 ctx.logger.warn(
1929 "Start commit is stale. Consider running: ralph --reset-start-commit",
1930 );
1931 } else if summary.commits_since > 5 {
1932 ctx.logger
1933 .info("Tip: Run 'ralph --show-baseline' for more details");
1934 }
1935 }
1936 }
1937 Err(e) => {
1938 if ctx.config.verbosity.is_debug() {
1940 ctx.logger
1941 .warn(&format!("Failed to get start commit summary: {e}"));
1942 }
1943 }
1944 }
1945 }
1946}
1947
1948fn check_prompt_restoration(
1950 ctx: &PipelineContext,
1951 prompt_monitor: &mut Option<PromptMonitor>,
1952 phase: &str,
1953) {
1954 if let Some(ref mut monitor) = prompt_monitor {
1955 if monitor.check_and_restore() {
1956 ctx.logger.warn(&format!(
1957 "PROMPT.md was deleted and restored during {phase} phase"
1958 ));
1959 }
1960 }
1961}
1962
1963pub fn handle_rebase_only(
1968 _args: &Args,
1969 config: &crate::config::Config,
1970 template_context: &TemplateContext,
1971 logger: &Logger,
1972 colors: Colors,
1973 executor: std::sync::Arc<dyn ProcessExecutor>,
1974 repo_root: &std::path::Path,
1975) -> anyhow::Result<()> {
1976 if is_main_or_master_branch()? {
1978 logger.warn("Already on main/master branch - rebasing on main is not recommended");
1979 logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
1980 logger.info(" git worktree add ../feature-branch feature-branch");
1981 logger.info("This allows multiple AI agents to work on different features simultaneously.");
1982 logger.info("Proceeding with rebase anyway as requested...");
1983 }
1984
1985 logger.header("Rebase to default branch", Colors::cyan);
1986
1987 match run_rebase_to_default(logger, colors, &*executor) {
1988 Ok(RebaseResult::Success) => {
1989 logger.success("Rebase completed successfully");
1990 Ok(())
1991 }
1992 Ok(RebaseResult::NoOp { reason }) => {
1993 logger.info(&format!("No rebase needed: {reason}"));
1994 Ok(())
1995 }
1996 Ok(RebaseResult::Failed(err)) => {
1997 logger.error(&format!("Rebase failed: {err}"));
1998 anyhow::bail!("Rebase failed: {err}")
1999 }
2000 Ok(RebaseResult::Conflicts(_conflicts)) => {
2001 let conflicted_files = get_conflicted_files()?;
2003 if conflicted_files.is_empty() {
2004 logger.warn("Rebase reported conflicts but no conflicted files found");
2005 let _ = abort_rebase(&*executor);
2006 return Ok(());
2007 }
2008
2009 logger.warn(&format!(
2010 "Rebase resulted in {} conflict(s), attempting AI resolution",
2011 conflicted_files.len()
2012 ));
2013
2014 match try_resolve_conflicts_without_phase_ctx(
2016 &conflicted_files,
2017 config,
2018 template_context,
2019 logger,
2020 colors,
2021 std::sync::Arc::clone(&executor),
2022 repo_root,
2023 ) {
2024 Ok(true) => {
2025 logger.info("Continuing rebase after conflict resolution");
2027 match continue_rebase(&*executor) {
2028 Ok(()) => {
2029 logger.success("Rebase completed successfully after AI resolution");
2030 Ok(())
2031 }
2032 Err(e) => {
2033 logger.error(&format!("Failed to continue rebase: {e}"));
2034 let _ = abort_rebase(&*executor);
2035 anyhow::bail!("Rebase failed after conflict resolution")
2036 }
2037 }
2038 }
2039 Ok(false) => {
2040 logger.error("AI conflict resolution failed, aborting rebase");
2042 let _ = abort_rebase(&*executor);
2043 anyhow::bail!("Rebase conflicts could not be resolved by AI")
2044 }
2045 Err(e) => {
2046 logger.error(&format!("Conflict resolution error: {e}"));
2047 let _ = abort_rebase(&*executor);
2048 anyhow::bail!("Rebase conflict resolution failed: {e}")
2049 }
2050 }
2051 }
2052 Err(e) => {
2053 logger.error(&format!("Rebase failed: {e}"));
2054 anyhow::bail!("Rebase failed: {e}")
2055 }
2056 }
2057}
2058
2059fn run_rebase_to_default(
2072 logger: &Logger,
2073 colors: Colors,
2074 executor: &dyn ProcessExecutor,
2075) -> std::io::Result<RebaseResult> {
2076 let default_branch = get_default_branch()?;
2078 logger.info(&format!(
2079 "Rebasing onto {}{}{}",
2080 colors.cyan(),
2081 default_branch,
2082 colors.reset()
2083 ));
2084
2085 rebase_onto(&default_branch, executor)
2087}
2088
2089fn run_initial_rebase(
2103 ctx: &PipelineContext,
2104 phase_ctx: &mut PhaseContext<'_>,
2105 run_context: &crate::checkpoint::RunContext,
2106 executor: &dyn ProcessExecutor,
2107) -> anyhow::Result<()> {
2108 ctx.logger.header("Pre-development rebase", Colors::cyan);
2109
2110 let step = ExecutionStep::new(
2112 "PreRebase",
2113 0,
2114 "pre_rebase_start",
2115 StepOutcome::success(None, vec![]),
2116 );
2117 phase_ctx.execution_history.add_step(step);
2118
2119 if ctx.config.features.checkpoint_enabled {
2121 let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
2122 let mut builder = CheckpointBuilder::new()
2123 .phase(PipelinePhase::PreRebase, 0, ctx.config.developer_iters)
2124 .reviewer_pass(0, ctx.config.reviewer_reviews)
2125 .capture_from_context(
2126 &ctx.config,
2127 &ctx.registry,
2128 &ctx.developer_agent,
2129 &ctx.reviewer_agent,
2130 &ctx.logger,
2131 run_context,
2132 )
2133 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2134
2135 builder = builder
2137 .with_execution_history(phase_ctx.execution_history.clone())
2138 .with_prompt_history(phase_ctx.clone_prompt_history());
2139
2140 if let Some(mut checkpoint) = builder.build() {
2141 checkpoint.rebase_state = RebaseState::PreRebaseInProgress {
2142 upstream_branch: default_branch,
2143 };
2144 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2145 }
2146 }
2147
2148 match run_rebase_to_default(&ctx.logger, ctx.colors, &*ctx.executor) {
2149 Ok(RebaseResult::Success) => {
2150 ctx.logger.success("Rebase completed successfully");
2151 let step = ExecutionStep::new(
2153 "PreRebase",
2154 0,
2155 "pre_rebase_complete",
2156 StepOutcome::success(None, vec![]),
2157 );
2158 phase_ctx.execution_history.add_step(step);
2159
2160 if ctx.config.features.checkpoint_enabled {
2162 let builder = CheckpointBuilder::new()
2163 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2164 .reviewer_pass(0, ctx.config.reviewer_reviews)
2165 .skip_rebase(true) .capture_from_context(
2167 &ctx.config,
2168 &ctx.registry,
2169 &ctx.developer_agent,
2170 &ctx.reviewer_agent,
2171 &ctx.logger,
2172 run_context,
2173 )
2174 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2175 .with_execution_history(phase_ctx.execution_history.clone())
2176 .with_prompt_history(phase_ctx.clone_prompt_history());
2177
2178 if let Some(checkpoint) = builder.build() {
2179 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2180 }
2181 }
2182
2183 Ok(())
2184 }
2185 Ok(RebaseResult::NoOp { reason }) => {
2186 ctx.logger.info(&format!("No rebase needed: {reason}"));
2187 let step = ExecutionStep::new(
2189 "PreRebase",
2190 0,
2191 "pre_rebase_skipped",
2192 StepOutcome::skipped(reason.clone()),
2193 );
2194 phase_ctx.execution_history.add_step(step);
2195
2196 if ctx.config.features.checkpoint_enabled {
2198 let builder = CheckpointBuilder::new()
2199 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2200 .reviewer_pass(0, ctx.config.reviewer_reviews)
2201 .skip_rebase(true) .capture_from_context(
2203 &ctx.config,
2204 &ctx.registry,
2205 &ctx.developer_agent,
2206 &ctx.reviewer_agent,
2207 &ctx.logger,
2208 run_context,
2209 )
2210 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2211 .with_execution_history(phase_ctx.execution_history.clone())
2212 .with_prompt_history(phase_ctx.clone_prompt_history());
2213
2214 if let Some(checkpoint) = builder.build() {
2215 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2216 }
2217 }
2218
2219 Ok(())
2220 }
2221 Ok(RebaseResult::Conflicts(_conflicts)) => {
2222 let conflicted_files = get_conflicted_files()?;
2224 if conflicted_files.is_empty() {
2225 ctx.logger
2226 .warn("Rebase reported conflicts but no conflicted files found");
2227 let _ = abort_rebase(executor);
2228 return Ok(());
2229 }
2230
2231 let step = ExecutionStep::new(
2233 "PreRebase",
2234 0,
2235 "pre_rebase_conflict",
2236 StepOutcome::partial(
2237 "Rebase started".to_string(),
2238 format!("{} conflicts detected", conflicted_files.len()),
2239 ),
2240 );
2241 phase_ctx.execution_history.add_step(step);
2242
2243 if ctx.config.features.checkpoint_enabled {
2245 let mut builder = CheckpointBuilder::new()
2246 .phase(
2247 PipelinePhase::PreRebaseConflict,
2248 0,
2249 ctx.config.developer_iters,
2250 )
2251 .reviewer_pass(0, ctx.config.reviewer_reviews)
2252 .capture_from_context(
2253 &ctx.config,
2254 &ctx.registry,
2255 &ctx.developer_agent,
2256 &ctx.reviewer_agent,
2257 &ctx.logger,
2258 run_context,
2259 )
2260 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2261
2262 builder = builder
2264 .with_execution_history(phase_ctx.execution_history.clone())
2265 .with_prompt_history(phase_ctx.clone_prompt_history());
2266
2267 if let Some(mut checkpoint) = builder.build() {
2268 checkpoint.rebase_state = RebaseState::HasConflicts {
2269 files: conflicted_files.clone(),
2270 };
2271 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2272 }
2273 }
2274
2275 ctx.logger.warn(&format!(
2276 "Rebase resulted in {} conflict(s), attempting AI resolution",
2277 conflicted_files.len()
2278 ));
2279
2280 let resolution_ctx = ConflictResolutionContext {
2282 config: &ctx.config,
2283 template_context: &ctx.template_context,
2284 logger: &ctx.logger,
2285 colors: ctx.colors,
2286 executor_arc: std::sync::Arc::clone(&ctx.executor),
2287 workspace: &*ctx.workspace,
2288 };
2289 match try_resolve_conflicts_with_fallback(
2290 &conflicted_files,
2291 resolution_ctx,
2292 phase_ctx,
2293 "PreRebase",
2294 &*ctx.executor,
2295 ) {
2296 Ok(true) => {
2297 ctx.logger
2299 .info("Continuing rebase after conflict resolution");
2300 match continue_rebase(executor) {
2301 Ok(()) => {
2302 ctx.logger
2303 .success("Rebase completed successfully after AI resolution");
2304 let step = ExecutionStep::new(
2306 "PreRebase",
2307 0,
2308 "pre_rebase_resolution",
2309 StepOutcome::success(None, vec![]),
2310 );
2311 phase_ctx.execution_history.add_step(step);
2312
2313 if ctx.config.features.checkpoint_enabled {
2315 let builder = CheckpointBuilder::new()
2316 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2317 .reviewer_pass(0, ctx.config.reviewer_reviews)
2318 .skip_rebase(true) .capture_from_context(
2320 &ctx.config,
2321 &ctx.registry,
2322 &ctx.developer_agent,
2323 &ctx.reviewer_agent,
2324 &ctx.logger,
2325 run_context,
2326 )
2327 .with_executor_from_context(std::sync::Arc::clone(
2328 &ctx.executor,
2329 ))
2330 .with_execution_history(phase_ctx.execution_history.clone())
2331 .with_prompt_history(phase_ctx.clone_prompt_history());
2332
2333 if let Some(checkpoint) = builder.build() {
2334 let _ = save_checkpoint_with_workspace(
2335 &*ctx.workspace,
2336 &checkpoint,
2337 );
2338 }
2339 }
2340
2341 Ok(())
2342 }
2343 Err(e) => {
2344 ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
2345 let _ = abort_rebase(executor);
2346 let step = ExecutionStep::new(
2348 "PreRebase",
2349 0,
2350 "pre_rebase_resolution",
2351 StepOutcome::partial(
2352 "Conflicts resolved by AI".to_string(),
2353 format!("Failed to continue rebase: {e}"),
2354 ),
2355 );
2356 phase_ctx.execution_history.add_step(step);
2357 Ok(()) }
2359 }
2360 }
2361 Ok(false) => {
2362 ctx.logger
2364 .warn("AI conflict resolution failed, aborting rebase");
2365 let _ = abort_rebase(executor);
2366 let step = ExecutionStep::new(
2368 "PreRebase",
2369 0,
2370 "pre_rebase_resolution",
2371 StepOutcome::failure("AI conflict resolution failed".to_string(), true),
2372 );
2373 phase_ctx.execution_history.add_step(step);
2374 Ok(()) }
2376 Err(e) => {
2377 ctx.logger.error(&format!("Conflict resolution error: {e}"));
2378 let _ = abort_rebase(executor);
2379 let step = ExecutionStep::new(
2381 "PreRebase",
2382 0,
2383 "pre_rebase_resolution",
2384 StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
2385 );
2386 phase_ctx.execution_history.add_step(step);
2387 Ok(()) }
2389 }
2390 }
2391 Ok(RebaseResult::Failed(err)) => {
2392 ctx.logger.error(&format!("Rebase failed: {err}"));
2393 let _ = abort_rebase(&*ctx.executor);
2394 let step = ExecutionStep::new(
2396 "PreRebase",
2397 0,
2398 "pre_rebase_failed",
2399 StepOutcome::failure(format!("Rebase failed: {err}"), true),
2400 );
2401 phase_ctx.execution_history.add_step(step);
2402 Ok(()) }
2404 Err(e) => {
2405 ctx.logger
2406 .warn(&format!("Rebase failed, continuing without rebase: {e}"));
2407 let step = ExecutionStep::new(
2409 "PreRebase",
2410 0,
2411 "pre_rebase_error",
2412 StepOutcome::failure(format!("Rebase error: {e}"), true),
2413 );
2414 phase_ctx.execution_history.add_step(step);
2415 Ok(())
2416 }
2417 }
2418}
2419
2420enum ConflictResolutionResult {
2424 WithJson(String),
2426 FileEditsOnly,
2428 Failed,
2430}
2431
2432struct ConflictResolutionContext<'a> {
2437 config: &'a crate::config::Config,
2438 template_context: &'a TemplateContext,
2439 logger: &'a Logger,
2440 colors: Colors,
2441 executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2442 workspace: &'a dyn crate::workspace::Workspace,
2443}
2444
2445fn try_resolve_conflicts_with_fallback(
2450 conflicted_files: &[String],
2451 ctx: ConflictResolutionContext<'_>,
2452 phase_ctx: &mut PhaseContext<'_>,
2453 phase: &str,
2454 executor: &dyn ProcessExecutor,
2455) -> anyhow::Result<bool> {
2456 if conflicted_files.is_empty() {
2457 return Ok(false);
2458 }
2459
2460 ctx.logger.info(&format!(
2461 "Attempting AI conflict resolution for {} file(s)",
2462 conflicted_files.len()
2463 ));
2464
2465 let conflicts = collect_conflict_info_or_error(conflicted_files, ctx.logger)?;
2466
2467 let prompt_key = format!("{}_conflict_resolution", phase.to_lowercase());
2470 let (resolution_prompt, was_replayed) =
2471 get_stored_or_generate_prompt(&prompt_key, &phase_ctx.prompt_history, || {
2472 build_resolution_prompt(&conflicts, ctx.template_context)
2473 });
2474
2475 if !was_replayed {
2477 phase_ctx.capture_prompt(&prompt_key, &resolution_prompt);
2478 } else {
2479 ctx.logger.info(&format!(
2480 "Using stored prompt from checkpoint for determinism: {}",
2481 prompt_key
2482 ));
2483 }
2484
2485 match run_ai_conflict_resolution(
2486 &resolution_prompt,
2487 ctx.config,
2488 ctx.logger,
2489 ctx.colors,
2490 std::sync::Arc::clone(&ctx.executor_arc),
2491 ctx.workspace,
2492 ) {
2493 Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
2494 match parse_and_validate_resolved_files(&resolved_content, ctx.logger) {
2497 Ok(resolved_files) => {
2498 write_resolved_files(&resolved_files, ctx.logger)?;
2499 }
2500 Err(_) => {
2501 }
2505 }
2506
2507 let remaining_conflicts = get_conflicted_files()?;
2509 if remaining_conflicts.is_empty() {
2510 Ok(true)
2511 } else {
2512 ctx.logger.warn(&format!(
2513 "{} conflicts remain after AI resolution",
2514 remaining_conflicts.len()
2515 ));
2516 Ok(false)
2517 }
2518 }
2519 Ok(ConflictResolutionResult::FileEditsOnly) => {
2520 ctx.logger
2522 .info("Agent resolved conflicts via file edits (no JSON output)");
2523
2524 let remaining_conflicts = get_conflicted_files()?;
2526 if remaining_conflicts.is_empty() {
2527 ctx.logger.success("All conflicts resolved via file edits");
2528 Ok(true)
2529 } else {
2530 ctx.logger.warn(&format!(
2531 "{} conflicts remain after AI resolution",
2532 remaining_conflicts.len()
2533 ));
2534 Ok(false)
2535 }
2536 }
2537 Ok(ConflictResolutionResult::Failed) => {
2538 ctx.logger.warn("AI conflict resolution failed");
2539 ctx.logger.info("Attempting to continue rebase anyway...");
2540
2541 match crate::git_helpers::continue_rebase(executor) {
2543 Ok(()) => {
2544 ctx.logger.info("Successfully continued rebase");
2545 Ok(true)
2546 }
2547 Err(rebase_err) => {
2548 ctx.logger
2549 .warn(&format!("Failed to continue rebase: {rebase_err}"));
2550 Ok(false) }
2552 }
2553 }
2554 Err(e) => {
2555 ctx.logger
2556 .warn(&format!("AI conflict resolution failed: {e}"));
2557 ctx.logger.info("Attempting to continue rebase anyway...");
2558
2559 match crate::git_helpers::continue_rebase(executor) {
2561 Ok(()) => {
2562 ctx.logger.info("Successfully continued rebase");
2563 Ok(true)
2564 }
2565 Err(rebase_err) => {
2566 ctx.logger
2567 .warn(&format!("Failed to continue rebase: {rebase_err}"));
2568 Ok(false) }
2570 }
2571 }
2572 }
2573}
2574
2575fn try_resolve_conflicts_without_phase_ctx(
2580 conflicted_files: &[String],
2581 config: &crate::config::Config,
2582 template_context: &TemplateContext,
2583 logger: &Logger,
2584 colors: Colors,
2585 executor: std::sync::Arc<dyn ProcessExecutor>,
2586 repo_root: &std::path::Path,
2587) -> anyhow::Result<bool> {
2588 use crate::agents::AgentRegistry;
2589 use crate::checkpoint::execution_history::ExecutionHistory;
2590 use crate::checkpoint::RunContext;
2591 use crate::pipeline::{Stats, Timer};
2592
2593 let registry = AgentRegistry::new()?;
2595 let mut timer = Timer::new();
2596 let mut stats = Stats::default();
2597 let workspace = crate::workspace::WorkspaceFs::new(repo_root.to_path_buf());
2598
2599 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2600 let developer_agent = config.developer_agent.as_deref().unwrap_or("codex");
2601
2602 let executor_arc = std::sync::Arc::clone(&executor);
2604
2605 let mut phase_ctx = PhaseContext {
2606 config,
2607 registry: ®istry,
2608 logger,
2609 colors: &colors,
2610 timer: &mut timer,
2611 stats: &mut stats,
2612 developer_agent,
2613 reviewer_agent,
2614 review_guidelines: None,
2615 template_context,
2616 run_context: RunContext::new(),
2617 execution_history: ExecutionHistory::new(),
2618 prompt_history: std::collections::HashMap::new(),
2619 executor: &*executor,
2620 executor_arc: std::sync::Arc::clone(&executor_arc),
2621 repo_root,
2622 workspace: &workspace,
2623 };
2624
2625 let ctx = ConflictResolutionContext {
2626 config,
2627 template_context,
2628 logger,
2629 colors,
2630 executor_arc,
2631 workspace: &workspace,
2632 };
2633
2634 try_resolve_conflicts_with_fallback(
2635 conflicted_files,
2636 ctx,
2637 &mut phase_ctx,
2638 "RebaseOnly",
2639 &*executor,
2640 )
2641}
2642
2643fn collect_conflict_info_or_error(
2645 conflicted_files: &[String],
2646 logger: &Logger,
2647) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
2648 use crate::prompts::collect_conflict_info;
2649
2650 let conflicts = match collect_conflict_info(conflicted_files) {
2651 Ok(c) => c,
2652 Err(e) => {
2653 logger.error(&format!("Failed to collect conflict info: {e}"));
2654 anyhow::bail!("Failed to collect conflict info");
2655 }
2656 };
2657 Ok(conflicts)
2658}
2659
2660fn build_resolution_prompt(
2662 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2663 template_context: &TemplateContext,
2664) -> String {
2665 build_enhanced_resolution_prompt(conflicts, None::<()>, template_context)
2666 .unwrap_or_else(|_| String::new())
2667}
2668
2669fn build_enhanced_resolution_prompt(
2673 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2674 _branch_info: Option<()>, template_context: &TemplateContext,
2676) -> anyhow::Result<String> {
2677 use std::fs;
2678
2679 let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
2680 let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
2681
2682 Ok(
2684 crate::prompts::build_conflict_resolution_prompt_with_context(
2685 template_context,
2686 conflicts,
2687 prompt_md_content.as_deref(),
2688 plan_content.as_deref(),
2689 ),
2690 )
2691}
2692
2693fn run_ai_conflict_resolution(
2698 resolution_prompt: &str,
2699 config: &crate::config::Config,
2700 logger: &Logger,
2701 colors: Colors,
2702 executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2703 workspace: &dyn crate::workspace::Workspace,
2704) -> anyhow::Result<ConflictResolutionResult> {
2705 use crate::agents::AgentRegistry;
2706 use crate::files::result_extraction::extract_last_result;
2707 use crate::pipeline::{
2708 run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
2709 };
2710 use std::io;
2711 use std::path::Path;
2712
2713 let log_dir = ".agent/logs/rebase_conflict_resolution";
2717
2718 let registry = AgentRegistry::new()?;
2719 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2720
2721 let executor_ref: &dyn crate::executor::ProcessExecutor = &*executor_arc;
2723 let mut runtime = PipelineRuntime {
2724 timer: &mut crate::pipeline::Timer::new(),
2725 logger,
2726 colors: &colors,
2727 config,
2728 executor: executor_ref,
2729 executor_arc: std::sync::Arc::clone(&executor_arc),
2730 workspace,
2731 };
2732
2733 let validate_output: OutputValidator = |ws: &dyn crate::workspace::Workspace,
2736 log_dir_path: &Path,
2737 validation_logger: &crate::logger::Logger|
2738 -> io::Result<bool> {
2739 match extract_last_result(ws, log_dir_path) {
2740 Ok(Some(_)) => {
2741 Ok(true)
2743 }
2744 Ok(None) => {
2745 match crate::git_helpers::get_conflicted_files() {
2748 Ok(conflicts) if conflicts.is_empty() => {
2749 validation_logger
2750 .info("Agent resolved conflicts without JSON output (file edits only)");
2751 Ok(true) }
2753 Ok(conflicts) => {
2754 validation_logger.warn(&format!(
2755 "{} conflict(s) remain unresolved",
2756 conflicts.len()
2757 ));
2758 Ok(false) }
2760 Err(e) => {
2761 validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
2762 Ok(false) }
2764 }
2765 }
2766 Err(e) => {
2767 validation_logger.warn(&format!("Output validation check failed: {e}"));
2768 Ok(false) }
2770 }
2771 };
2772
2773 let mut fallback_config = FallbackConfig {
2774 role: crate::agents::AgentRole::Reviewer,
2775 base_label: "conflict resolution",
2776 prompt: resolution_prompt,
2777 logfile_prefix: log_dir,
2778 runtime: &mut runtime,
2779 registry: ®istry,
2780 primary_agent: reviewer_agent,
2781 output_validator: Some(validate_output),
2782 workspace,
2783 };
2784
2785 let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
2786
2787 if exit_code != 0 {
2788 return Ok(ConflictResolutionResult::Failed);
2789 }
2790
2791 let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
2794
2795 if remaining_conflicts.is_empty() {
2796 match extract_last_result(workspace, Path::new(log_dir)) {
2798 Ok(Some(content)) => {
2799 logger.info("Agent provided JSON output with resolved files");
2800 Ok(ConflictResolutionResult::WithJson(content))
2801 }
2802 Ok(None) => {
2803 logger.info("Agent resolved conflicts via file edits (no JSON output)");
2804 Ok(ConflictResolutionResult::FileEditsOnly)
2805 }
2806 Err(e) => {
2807 logger.warn(&format!(
2809 "Failed to extract JSON output but conflicts are resolved: {e}"
2810 ));
2811 Ok(ConflictResolutionResult::FileEditsOnly)
2812 }
2813 }
2814 } else {
2815 logger.warn(&format!(
2816 "{} conflict(s) remain after agent attempted resolution",
2817 remaining_conflicts.len()
2818 ));
2819 Ok(ConflictResolutionResult::Failed)
2820 }
2821}
2822
2823fn parse_and_validate_resolved_files(
2829 resolved_content: &str,
2830 logger: &Logger,
2831) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
2832 let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|_e| {
2833 anyhow::anyhow!("Agent did not provide JSON output (will verify via Git state)")
2836 })?;
2837
2838 let resolved_files = match json.get("resolved_files") {
2839 Some(v) if v.is_object() => v.as_object().unwrap(),
2840 _ => {
2841 logger.info("Agent output missing 'resolved_files' object");
2842 anyhow::bail!("Agent output missing 'resolved_files' object");
2843 }
2844 };
2845
2846 if resolved_files.is_empty() {
2847 logger.info("No resolved files in JSON output");
2848 anyhow::bail!("No files were resolved by the agent");
2849 }
2850
2851 Ok(resolved_files.clone())
2852}
2853
2854fn write_resolved_files(
2856 resolved_files: &serde_json::Map<String, serde_json::Value>,
2857 logger: &Logger,
2858) -> anyhow::Result<usize> {
2859 use std::fs;
2860
2861 let mut files_written = 0;
2862 for (path, content) in resolved_files {
2863 if let Some(content_str) = content.as_str() {
2864 fs::write(path, content_str).map_err(|e| {
2865 logger.error(&format!("Failed to write {path}: {e}"));
2866 anyhow::anyhow!("Failed to write {path}: {e}")
2867 })?;
2868 logger.info(&format!("Resolved and wrote: {path}"));
2869 files_written += 1;
2870 if let Err(e) = crate::git_helpers::git_add_all() {
2872 logger.warn(&format!("Failed to stage {path}: {e}"));
2873 }
2874 }
2875 }
2876
2877 logger.success(&format!("Successfully resolved {files_written} file(s)"));
2878 Ok(files_written)
2879}