ralph_workflow/app/runner/
setup_helpers.rs1use crate::agents::AgentRegistry;
2use crate::checkpoint::PipelinePhase;
3use crate::cli::{create_prompt_from_template, prompt_template_selection};
4use crate::logger::{Colors, Logger};
5use crate::workspace::Workspace;
6
7use super::super::effect::{AppEffect, AppEffectHandler, AppEffectResult};
8use super::super::effectful;
9use super::super::validation::{validate_agent_commands, validate_can_commit};
10
11pub struct AgentSetupParams<'a> {
21 pub(crate) config: &'a crate::config::Config,
22 pub(crate) registry: &'a AgentRegistry,
23 pub(crate) developer_agent: &'a str,
24 pub(crate) reviewer_agent: &'a str,
25 pub(crate) config_path: &'a std::path::Path,
26 pub(crate) colors: Colors,
27 pub(crate) logger: &'a Logger,
28 pub(crate) working_dir_override: Option<&'a std::path::Path>,
31}
32
33pub fn validate_and_setup_agents<H: AppEffectHandler>(
38 params: &AgentSetupParams<'_>,
39 handler: &mut H,
40) -> anyhow::Result<Option<std::path::PathBuf>> {
41 let AgentSetupParams {
42 config,
43 registry,
44 developer_agent,
45 reviewer_agent,
46 config_path,
47 colors,
48 logger,
49 working_dir_override,
50 } = params;
51 validate_agent_commands(
53 config,
54 registry,
55 developer_agent,
56 reviewer_agent,
57 config_path,
58 )?;
59
60 validate_can_commit(
62 config,
63 registry,
64 developer_agent,
65 reviewer_agent,
66 config_path,
67 )?;
68
69 let repo_root = if let Some(override_dir) = working_dir_override {
71 let result = handler.execute(AppEffect::SetCurrentDir {
73 path: override_dir.to_path_buf(),
74 });
75 if let AppEffectResult::Error(e) = result {
76 anyhow::bail!("Failed to set working directory: {e}");
77 }
78 override_dir.to_path_buf()
79 } else {
80 let require_result = handler.execute(AppEffect::GitRequireRepo);
82 if let AppEffectResult::Error(e) = require_result {
83 anyhow::bail!("Not in a git repository: {e}");
84 }
85
86 let root_result = handler.execute(AppEffect::GitGetRepoRoot);
87 let root = match root_result {
88 AppEffectResult::Path(p) => p,
89 AppEffectResult::Error(e) => {
90 anyhow::bail!("Failed to get repo root: {e}");
91 }
92 _ => anyhow::bail!("Unexpected result from GitGetRepoRoot"),
93 };
94
95 let set_result = handler.execute(AppEffect::SetCurrentDir { path: root.clone() });
96 if let AppEffectResult::Error(e) = set_result {
97 anyhow::bail!("Failed to set working directory: {e}");
98 }
99 root
100 };
101
102 let should_continue = setup_git_and_prompt_file(config, *colors, logger, handler)?;
104 if should_continue.is_none() {
105 return Ok(None);
106 }
107
108 Ok(Some(repo_root))
109}
110
111fn setup_git_and_prompt_file<H: AppEffectHandler>(
116 config: &crate::config::Config,
117 colors: Colors,
118 logger: &Logger,
119 handler: &mut H,
120) -> anyhow::Result<Option<()>> {
121 let prompt_exists =
122 effectful::check_prompt_exists_effectful(handler).map_err(|e| anyhow::anyhow!("{e}"))?;
123
124 if config.behavior.interactive && !prompt_exists {
127 if let Some(template_name) = prompt_template_selection(colors) {
128 create_prompt_from_template(&template_name, colors)?;
129 logger.info(""); logger.info(
131 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
132 );
133 logger.info("Tip: Edit PROMPT.md, then run: ralph");
134 return Ok(None);
135 }
136 logger.info(""); logger.error("PROMPT.md not found in current directory.");
138 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
139 logger.info(""); logger.info("To get started:");
141 logger.info(" ralph --init # Smart setup wizard");
142 logger.info(" ralph --init bug-fix # Create from Work Guide");
143 logger.info(" ralph --list-work-guides # See all Work Guides");
144 logger.info(""); return Ok(None);
146 }
147
148 if !prompt_exists {
150 logger.error("PROMPT.md not found in current directory.");
151 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
152 logger.info(""); logger.info("Quick start:");
154 logger.info(" ralph --init # Smart setup wizard");
155 logger.info(" ralph --init bug-fix # Create from Work Guide");
156 logger.info(" ralph --list-work-guides # See all Work Guides");
157 logger.info(""); logger.info("Use -i flag for interactive mode to be prompted for template selection.");
159 logger.info(""); return Ok(None);
161 }
162
163 Ok(Some(()))
164}
165
166pub(crate) fn setup_interrupt_context_for_pipeline(
171 phase: PipelinePhase,
172 total_iterations: u32,
173 total_reviewer_passes: u32,
174 execution_history: &crate::checkpoint::ExecutionHistory,
175 prompt_history: &std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>,
176 run_context: &crate::checkpoint::RunContext,
177 workspace: std::sync::Arc<dyn Workspace>,
178) {
179 use crate::interrupt::{set_interrupt_context, InterruptContext};
180
181 let (iteration, reviewer_pass) = match phase {
183 PipelinePhase::Development => (1, 0),
184 PipelinePhase::Review => (total_iterations, 1),
185 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
186 (total_iterations, total_reviewer_passes)
187 }
188 _ => (0, 0),
189 };
190
191 let context = InterruptContext {
192 phase,
193 iteration,
194 total_iterations,
195 reviewer_pass,
196 total_reviewer_passes,
197 run_context: run_context.clone(),
198 execution_history: execution_history.clone(),
199 prompt_history: prompt_history.clone(),
200 workspace,
201 };
202
203 set_interrupt_context(context);
204}
205
206pub(crate) fn update_interrupt_context_from_phase(
218 execution_history: &crate::checkpoint::ExecutionHistory,
219 prompt_history: std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>,
220 phase: PipelinePhase,
221 total_iterations: u32,
222 total_reviewer_passes: u32,
223 run_context: &crate::checkpoint::RunContext,
224 workspace: std::sync::Arc<dyn Workspace>,
225) {
226 use crate::interrupt::{set_interrupt_context, InterruptContext};
227
228 let (iteration, reviewer_pass) = match phase {
230 PipelinePhase::Development => {
231 let iter = run_context.actual_developer_runs.max(1);
233 (iter, 0)
234 }
235 PipelinePhase::Review => (total_iterations, run_context.actual_reviewer_runs.max(1)),
236 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
237 (total_iterations, total_reviewer_passes)
238 }
239 _ => (0, 0),
240 };
241
242 let context = InterruptContext {
243 phase,
244 iteration,
245 total_iterations,
246 reviewer_pass,
247 total_reviewer_passes,
248 run_context: run_context.clone(),
249 execution_history: execution_history.clone(),
250 prompt_history,
251 workspace,
252 };
253
254 set_interrupt_context(context);
255}
256
257pub(crate) const fn defer_clear_interrupt_context() -> InterruptContextGuard {
263 InterruptContextGuard
264}
265
266pub(crate) struct InterruptContextGuard;
272
273impl Drop for InterruptContextGuard {
274 fn drop(&mut self) {
275 crate::interrupt::clear_interrupt_context();
276 }
277}