use anyhow::Result;
use chrono::Local;
use std::{path::{Path, PathBuf}, sync::Arc};
use crate::{agent, paths, project::{self, ProjectConfig}, session};
use crate::{store::Store, types::*};
use super::run_dispatch_claim::insert_task_claiming_id;
use super::run_dispatch_resolve::{AgentSetup, apply_project_defaults, maybe_insert_held_route_event, resolve_agent_setup};
use super::run_dispatch_worktree::{
WorktreeSetup, clear_worktree_lock, fail_claimed_task, persist_project_identity,
persist_worktree_setup, prepare_worktree_deps, resolve_task_project_id, setup_worktree,
};
use super::run_task_profile::{
apply_category_and_result_defaults, persist_declaration, should_auto_result_file,
validate_critical_rigor, validate_egress,
};
use super::run_validate::{validate_command_preflight_with, validate_dispatch};
use super::{RunArgs, resolve_max_duration_mins, resolve_prompt_input, run_prompt};
pub(super) struct PreparedDispatch {
pub detected_project: Option<ProjectConfig>,
pub agent_kind: AgentKind,
pub agent_display_name: String,
pub requested_skills: Vec<String>,
pub effective_model: Option<String>,
pub budget_active: bool,
pub agent: Box<dyn agent::Agent>,
pub task_id: TaskId,
pub task: Task,
pub log_path: PathBuf,
pub workgroup: Option<Workgroup>,
pub repo_path: Option<String>,
pub wt_path: Option<String>,
pub effective_dir: Option<String>,
}
struct DispatchContext {
detected_project: Option<ProjectConfig>,
agent_setup: AgentSetup,
had_explicit_result_file: bool,
}
struct ClaimedDispatch {
task_id: TaskId,
task: Task,
log_path: PathBuf,
workgroup: Option<Workgroup>,
explicit_repo_path: Option<String>,
}
pub(super) fn prepare_dispatch(store: &Arc<Store>, args: &mut RunArgs) -> Result<PreparedDispatch> {
prepare_dispatch_with(store, args, crate::agent::env::which_exists)
}
pub(super) fn prepare_dispatch_with<W>(
store: &Arc<Store>,
args: &mut RunArgs,
which: W,
) -> Result<PreparedDispatch>
where
W: Fn(&str) -> bool,
{
super::run_delegation::apply_nested_delegation(store, args)?;
args.prompt = resolve_prompt_input(&args.prompt, args.prompt_file.as_deref())?;
args.prompt_file = None;
args.max_duration_mins = resolve_max_duration_mins(args.timeout, args.max_duration_mins);
let context = resolve_dispatch_context(store, args)?;
let claimed = claim_dispatch(store, args, &context, which)?;
finish_dispatch(store, args, context, claimed)
}
fn resolve_dispatch_context(store: &Arc<Store>, args: &mut RunArgs) -> Result<DispatchContext> {
let had_explicit_result_file = args
.result_file_required
.unwrap_or_else(|| args.result_file.is_some());
args.result_file_required = Some(had_explicit_result_file);
let detected_project = match args.dir.as_deref() {
Some(dir) => project::detect_project_in(Path::new(dir)),
None => project::detect_project(),
};
apply_project_defaults(args, detected_project.as_ref());
validate_critical_rigor(args)?;
validate_egress(args)?;
let agent_setup = resolve_agent_setup(store, args)?;
let agent_name = agent_setup.custom_agent_name.as_deref().unwrap_or_else(|| agent_setup.agent_kind.as_str());
let policy = crate::timeout_policy::TimeoutPolicy::resolve(agent_name, args.idle_timeout_secs, args.max_duration_mins, detected_project.as_ref());
args.timeout_policy = policy; args.max_duration_mins = Some(policy.max_duration_mins());
args.env = crate::timeout_policy::env_with_policy(args.env.take(), policy);
Ok(DispatchContext { detected_project, agent_setup, had_explicit_result_file })
}
fn claim_dispatch<W>(
store: &Arc<Store>,
args: &mut RunArgs,
context: &DispatchContext,
which: W,
) -> Result<ClaimedDispatch>
where
W: Fn(&str) -> bool,
{
let explicit_id = args.existing_task_id.is_some(); let mut task_id = initial_task_id(args)?;
let mut log_path = paths::log_path(task_id.as_str());
let workgroup = run_prompt::load_workgroup(store, args.group.as_deref())?;
let explicit_repo_path = crate::repo_root::resolve_explicit_repo_path(args.repo_root.as_deref(), args.repo.as_deref())?;
let caller = session::current_caller();
let mut task = pending_task(
args,
&context.agent_setup,
&task_id,
&log_path,
explicit_repo_path.clone(),
caller,
context.detected_project.as_ref(),
);
apply_category_and_result_defaults(args, &mut task, context.had_explicit_result_file);
for warning in validate_dispatch(args, &context.agent_setup.agent_kind) {
aid_warn!("[aid] Warning: {warning}");
}
validate_command_preflight_with(
context.agent_setup.agent.as_ref(),
args,
context.agent_setup.effective_model.as_deref(),
which,
)?;
insert_task_claiming_id(store, &mut task, &mut task_id, &mut log_path, explicit_id)?;
maybe_insert_held_route_event(store, &task_id, &context.agent_setup, args.dry_run);
persist_declaration(store, &task_id, args)?;
Ok(ClaimedDispatch { task_id, task, log_path, workgroup, explicit_repo_path })
}
fn attach_worktree(
store: &Arc<Store>,
args: &mut RunArgs,
context: &DispatchContext,
claimed: &mut ClaimedDispatch,
) -> Result<WorktreeSetup> {
let setup = match setup_worktree(
store,
args,
context.detected_project.as_ref(),
&context.agent_setup,
&claimed.task_id,
claimed.explicit_repo_path.as_deref(),
) {
Ok(setup) => setup,
Err(err) => {
fail_claimed_task(store, &claimed.task_id, &err)?;
return Err(err);
}
};
if let Err(err) = persist_worktree_setup(store, &claimed.task_id, &mut claimed.task, &setup) {
clear_worktree_lock(setup.wt_path.as_deref(), claimed.task_id.as_str());
fail_claimed_task(store, &claimed.task_id, &err)?;
return Err(err);
}
persist_project_identity(
store,
&claimed.task_id,
&mut claimed.task,
context.detected_project.as_ref(),
args.dir.as_deref(),
&setup,
)?;
if setup.emit_gitbutler_setup_hint {
super::run_dispatch_resolve::insert_gitbutler_setup_hint(store, &claimed.task_id);
}
if let Err(err) = super::run_dispatch_guard::ensure_worktree_task_not_repo_root(
&claimed.task, setup.effective_dir.as_deref(), setup.repo_path.as_deref(),
) {
clear_worktree_lock(setup.wt_path.as_deref(), claimed.task_id.as_str());
fail_claimed_task(store, &claimed.task_id, &err)?;
return Err(err);
}
prepare_worktree_deps(store, args, &claimed.task_id, &setup)?;
Ok(setup)
}
fn finish_dispatch(
store: &Arc<Store>,
args: &mut RunArgs,
context: DispatchContext,
mut claimed: ClaimedDispatch,
) -> Result<PreparedDispatch> {
let setup = attach_worktree(store, args, &context, &mut claimed)?;
if should_auto_result_file(args, context.had_explicit_result_file) {
let result_file = crate::cmd::report_mode::task_result_file(claimed.task_id.as_str());
args.result_file = Some(result_file.clone());
args.result_file_required = Some(false);
aid_info!("[aid] Audit report mode: auto-set --result-file {result_file}");
}
let mut dispatch_args = args.clone();
dispatch_args.model = context.agent_setup.effective_model.clone();
dispatch_args.model_source = args.model_source;
store.update_task_dispatch_args(
claimed.task_id.as_str(), &dispatch_args.dispatch_args_json()?,
)?;
Ok(prepared_dispatch(
context.detected_project,
context.agent_setup,
claimed.task_id,
claimed.task,
claimed.log_path,
claimed.workgroup,
setup,
))
}
fn initial_task_id(args: &RunArgs) -> Result<TaskId> {
let Some(id) = args.existing_task_id.clone() else { return Ok(TaskId::generate()) };
crate::sanitize::validate_task_id(id.as_str())?;
Ok(id)
}
fn pending_task(
args: &RunArgs,
agent_setup: &AgentSetup,
task_id: &TaskId,
log_path: &Path,
repo_path: Option<String>,
caller: Option<session::CallerSession>,
detected_project: Option<&ProjectConfig>,
) -> Task {
let project_id = resolve_task_project_id(detected_project, repo_path.as_deref(), args.dir.as_deref());
Task {
id: task_id.clone(), agent: agent_setup.agent_kind, custom_agent_name: agent_setup.custom_agent_name.clone(),
prompt: args.prompt.clone(), resolved_prompt: None, category: None, status: TaskStatus::Pending,
parent_task_id: args.parent_task_id.clone(), workgroup_id: args.group.clone(),
caller_kind: caller.as_ref().map(|item| item.kind.clone()),
caller_session_id: caller.as_ref().map(|item| item.session_id.clone()),
agent_session_id: None, repo_path, project_id, worktree_path: None, effective_dir: None, worktree_branch: None, final_head_sha: None, final_branch: None, start_sha: None,
log_path: Some(log_path.to_string_lossy().to_string()), output_path: args.output.clone(),
tokens: None, prompt_tokens: None, duration_ms: None, requested_model: agent_setup.effective_model.clone(), observed_model: None, attribution_source: None,
cost_usd: None, exit_code: None, created_at: Local::now(), completed_at: None,
verify: args.verify.clone(), verify_status: if verify_required(args.verify.as_deref()) { VerifyStatus::Pending } else { VerifyStatus::Skipped }, pending_reason: None,
read_only: args.read_only, budget: args.budget, audit_verdict: None, audit_report_path: None,
delivery_assessment: None,
}
}
fn prepared_dispatch(
detected_project: Option<ProjectConfig>,
agent_setup: AgentSetup,
task_id: TaskId,
task: Task,
log_path: PathBuf,
workgroup: Option<Workgroup>,
setup: WorktreeSetup,
) -> PreparedDispatch {
PreparedDispatch {
detected_project, agent_kind: agent_setup.agent_kind,
agent_display_name: agent_setup.agent_display_name,
requested_skills: agent_setup.requested_skills,
effective_model: agent_setup.effective_model, budget_active: agent_setup.budget_active,
agent: agent_setup.agent, task_id, task, log_path, workgroup,
repo_path: setup.repo_path, wt_path: setup.wt_path, effective_dir: setup.effective_dir,
}
}
#[cfg(test)] #[path = "run_dispatch_prepare_tests.rs"] mod tests;
#[cfg(test)] #[path = "run_dispatch_verify_tests.rs"] mod verify_tests;
#[cfg(test)] #[path = "run_dispatch_preflight_tests.rs"] mod preflight_tests;