use crate::agents::AgentRegistry;
use crate::checkpoint::PipelinePhase;
use crate::cli::{create_prompt_from_template, prompt_template_selection};
use crate::logger::{Colors, Logger};
use crate::workspace::Workspace;
use super::super::effect::{AppEffect, AppEffectHandler, AppEffectResult};
use super::super::effectful;
use super::super::validation::{validate_agent_commands, validate_can_commit};
pub struct AgentSetupParams<'a> {
pub(crate) config: &'a crate::config::Config,
pub(crate) registry: &'a AgentRegistry,
pub(crate) developer_agent: &'a str,
pub(crate) reviewer_agent: &'a str,
pub(crate) config_path: &'a std::path::Path,
pub(crate) colors: Colors,
pub(crate) logger: &'a Logger,
pub(crate) working_dir_override: Option<&'a std::path::Path>,
}
pub fn validate_and_setup_agents<H: AppEffectHandler>(
params: &AgentSetupParams<'_>,
handler: &mut H,
) -> anyhow::Result<Option<std::path::PathBuf>> {
let AgentSetupParams {
config,
registry,
developer_agent,
reviewer_agent,
config_path,
colors,
logger,
working_dir_override,
} = params;
validate_agent_commands(
config,
registry,
developer_agent,
reviewer_agent,
config_path,
)?;
validate_can_commit(
config,
registry,
developer_agent,
reviewer_agent,
config_path,
)?;
let repo_root = if let Some(override_dir) = working_dir_override {
let result = handler.execute(AppEffect::SetCurrentDir {
path: override_dir.to_path_buf(),
});
if let AppEffectResult::Error(e) = result {
anyhow::bail!("Failed to set working directory: {e}");
}
override_dir.to_path_buf()
} else {
let require_result = handler.execute(AppEffect::GitRequireRepo);
if let AppEffectResult::Error(e) = require_result {
anyhow::bail!("Not in a git repository: {e}");
}
let root_result = handler.execute(AppEffect::GitGetRepoRoot);
let root = match root_result {
AppEffectResult::Path(p) => p,
AppEffectResult::Error(e) => {
anyhow::bail!("Failed to get repo root: {e}");
}
_ => anyhow::bail!("Unexpected result from GitGetRepoRoot"),
};
let set_result = handler.execute(AppEffect::SetCurrentDir { path: root.clone() });
if let AppEffectResult::Error(e) = set_result {
anyhow::bail!("Failed to set working directory: {e}");
}
root
};
let should_continue = setup_git_and_prompt_file(config, *colors, logger, handler)?;
if should_continue.is_none() {
return Ok(None);
}
Ok(Some(repo_root))
}
fn setup_git_and_prompt_file<H: AppEffectHandler>(
config: &crate::config::Config,
colors: Colors,
logger: &Logger,
handler: &mut H,
) -> anyhow::Result<Option<()>> {
let prompt_exists =
effectful::check_prompt_exists_effectful(handler).map_err(|e| anyhow::anyhow!("{e}"))?;
if config.behavior.interactive && !prompt_exists {
if let Some(template_name) = prompt_template_selection(colors) {
create_prompt_from_template(&template_name, colors)?;
logger.info(""); logger.info(
"PROMPT.md created. Please edit it with your task details, then run ralph again.",
);
logger.info("Tip: Edit PROMPT.md, then run: ralph");
return Ok(None);
}
logger.info(""); logger.error("PROMPT.md not found in current directory.");
logger.warn("PROMPT.md is required to run the Ralph pipeline.");
logger.info(""); logger.info("To get started:");
logger.info(" ralph --init # Smart setup wizard");
logger.info(" ralph --init bug-fix # Create from Work Guide");
logger.info(" ralph --list-work-guides # See all Work Guides");
logger.info(""); return Ok(None);
}
if !prompt_exists {
logger.error("PROMPT.md not found in current directory.");
logger.warn("PROMPT.md is required to run the Ralph pipeline.");
logger.info(""); logger.info("Quick start:");
logger.info(" ralph --init # Smart setup wizard");
logger.info(" ralph --init bug-fix # Create from Work Guide");
logger.info(" ralph --list-work-guides # See all Work Guides");
logger.info(""); logger.info("Use -i flag for interactive mode to be prompted for template selection.");
logger.info(""); return Ok(None);
}
Ok(Some(()))
}
pub(crate) fn setup_interrupt_context_for_pipeline(
phase: PipelinePhase,
total_iterations: u32,
total_reviewer_passes: u32,
execution_history: &crate::checkpoint::ExecutionHistory,
prompt_history: &std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>,
run_context: &crate::checkpoint::RunContext,
workspace: std::sync::Arc<dyn Workspace>,
) {
use crate::interrupt::{set_interrupt_context, InterruptContext};
let (iteration, reviewer_pass) = match phase {
PipelinePhase::Development => (1, 0),
PipelinePhase::Review => (total_iterations, 1),
PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
(total_iterations, total_reviewer_passes)
}
_ => (0, 0),
};
let context = InterruptContext {
phase,
iteration,
total_iterations,
reviewer_pass,
total_reviewer_passes,
run_context: run_context.clone(),
execution_history: execution_history.clone(),
prompt_history: prompt_history.clone(),
workspace,
};
set_interrupt_context(context);
}
pub(crate) fn update_interrupt_context_from_phase(
execution_history: &crate::checkpoint::ExecutionHistory,
prompt_history: std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>,
phase: PipelinePhase,
total_iterations: u32,
total_reviewer_passes: u32,
run_context: &crate::checkpoint::RunContext,
workspace: std::sync::Arc<dyn Workspace>,
) {
use crate::interrupt::{set_interrupt_context, InterruptContext};
let (iteration, reviewer_pass) = match phase {
PipelinePhase::Development => {
let iter = run_context.actual_developer_runs.max(1);
(iter, 0)
}
PipelinePhase::Review => (total_iterations, run_context.actual_reviewer_runs.max(1)),
PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
(total_iterations, total_reviewer_passes)
}
_ => (0, 0),
};
let context = InterruptContext {
phase,
iteration,
total_iterations,
reviewer_pass,
total_reviewer_passes,
run_context: run_context.clone(),
execution_history: execution_history.clone(),
prompt_history,
workspace,
};
set_interrupt_context(context);
}
pub(crate) const fn defer_clear_interrupt_context() -> InterruptContextGuard {
InterruptContextGuard
}
pub(crate) struct InterruptContextGuard;
impl Drop for InterruptContextGuard {
fn drop(&mut self) {
crate::interrupt::clear_interrupt_context();
}
}