use anyhow::Result;
use crate::agent::{self, RunOpts};
use crate::store::Store;
use crate::types::{AgentKind, TaskStatus};
use super::RunArgs;
use std::process::Command;
pub(super) fn validate_command_preflight_with<F>(
agent: &dyn agent::Agent,
args: &RunArgs,
effective_model: Option<&str>,
which: F,
) -> Result<()>
where
F: Fn(&str) -> bool,
{
validate_command_preflight_with_runner(
agent,
args,
effective_model,
which,
&preflight_cli_command_runner,
)
}
fn validate_command_preflight_with_runner<W>(
agent: &dyn agent::Agent,
args: &RunArgs,
effective_model: Option<&str>,
which: W,
run: &agent::CliCommandRunner<'_>,
) -> Result<()>
where
W: Fn(&str) -> bool,
{
if args.sandbox
&& crate::sandbox::can_sandbox(agent.kind())
&& !crate::sandbox::is_available()
{
anyhow::bail!(
"--sandbox requires Apple Container CLI. Install: brew install container; or omit --sandbox"
);
}
let opts = RunOpts {
dir: None,
output: args.output.clone(),
result_file: args.result_file.clone(),
model: effective_model.map(str::to_string),
budget: args.budget,
read_only: args.read_only,
sandbox: args.sandbox,
context_files: Vec::new(),
session_id: None,
env: None,
env_forward: None,
};
let cmd = agent
.build_command("__aid_preflight__", &opts)
.map_err(|err| anyhow::anyhow!("{err:#}"))?;
if args.container.is_some() || args.sandbox || args.dry_run {
return Ok(());
}
let program = cmd.get_program().to_string_lossy();
agent::ensure_resolved_binary_available_with(&args.agent_name, &program, which)?;
agent.validate_cli_with(run)
}
fn run_cli_command(program: &str, args: &[&str]) -> Result<agent::CliCommandOutput> {
let output = Command::new(program).args(args).output()?;
Ok(agent::CliCommandOutput {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
#[cfg(test)]
fn preflight_cli_command_runner(_program: &str, _args: &[&str]) -> Result<agent::CliCommandOutput> {
Ok(agent::CliCommandOutput {
success: true,
stdout: "--full-auto\n--approve-for-me\n".to_string(),
stderr: String::new(),
})
}
#[cfg(not(test))]
fn preflight_cli_command_runner(program: &str, args: &[&str]) -> Result<agent::CliCommandOutput> {
run_cli_command(program, args)
}
pub(super) fn validate_dispatch(args: &RunArgs, agent_kind: &AgentKind) -> Vec<String> {
let mut warnings = Vec::new();
let prompt_len = args.prompt.chars().count();
if prompt_len < 10 {
warnings.push("Prompt is very short, agent may not have enough context".to_string());
}
if matches!(
agent_kind,
AgentKind::Codex
| AgentKind::Copilot
| AgentKind::Claude
| AgentKind::OpenCode
| AgentKind::CommandCode
| AgentKind::Cursor
| AgentKind::Qwen
| AgentKind::Kilo
| AgentKind::MiMoCode
| AgentKind::Grok
) && args.dir.is_none() && args.worktree.is_none() && !args.read_only
{
let profile = agent::classifier::classify(
&args.prompt,
agent::classifier::count_file_mentions(&args.prompt),
prompt_len,
);
if !matches!(
profile.category,
agent::classifier::TaskCategory::Research | agent::classifier::TaskCategory::Documentation
) {
warnings.push("Code agent without --dir may not be able to write files".to_string());
}
}
if prompt_len > 5000 {
warnings.push(format!(
"Very long prompt ({prompt_len} chars), consider using --context files instead"
));
}
if matches!(agent_kind, AgentKind::Gemini) && args.worktree.is_some() {
warnings.push("Research agent with --worktree is unusual, did you mean a code agent?".to_string());
}
warnings
}
pub(super) enum IdConflict {
None,
ReplaceWaiting,
Running,
AutoSuffix(String),
}
pub(super) fn resolve_id_conflict(store: &Store, id: &str) -> Result<IdConflict> {
let Some(existing) = store.get_task(id)? else {
return Ok(IdConflict::None);
};
match existing.status {
TaskStatus::Waiting => Ok(IdConflict::ReplaceWaiting),
TaskStatus::Running => Ok(IdConflict::Running),
_ => {
for suffix in 2..=99 {
let candidate = format!("{id}-{suffix}");
if store.get_task(&candidate)?.is_none() {
return Ok(IdConflict::AutoSuffix(candidate));
}
}
anyhow::bail!("Too many tasks with ID prefix '{id}' (checked up to -99)");
}
}
}