use super::clock::{Clock, IdGen};
use super::subagent::{SpawnRequest, spawn_subagent_branch};
use super::{Error, PER_REPO_PROVIDERS_FILE, SOULS_DIR, WORKFLOW_FILE};
use crate::config::{PerRepoProviders, Workflow};
use crate::prompt::budget;
use crate::prompt::inbox::{self, Launcher};
use crate::template::GitRunner;
use crate::workspace;
use std::path::{Path, PathBuf};
pub struct ChildDispatchRequest<'a> {
pub repo: &'a Path,
pub parent_branch: &'a str,
pub parent_worktree: &'a Path,
pub role: &'a str,
pub goal: &'a str,
pub fork_point: Option<&'a str>,
}
pub fn run(
req: &ChildDispatchRequest<'_>,
git: &dyn GitRunner,
clock: &dyn Clock,
id_gen: &dyn IdGen,
launcher: &dyn Launcher,
) -> Result<String, Error> {
let sub_id = format!("{}-{}", clock.now_compact(), id_gen.short());
let sub_branch = format!("{}-{sub_id}", req.parent_branch);
let sub_worktree = workspace::agent_worktree(req.repo, &sub_branch);
let commit =
workspace::governing_config(req.repo, req.parent_branch, git).map_err(|source| {
Error::Git {
op: "governing config",
source,
}
})?;
if let Some(ex) = budget::check(req.repo, &sub_branch, &budgets(req, &commit, git)?) {
return Err(Error::DispatchRefused {
child: sub_branch,
parent: req.parent_branch.to_string(),
exhausted: ex,
});
}
let soul_rel = format!("{SOULS_DIR}/{}.md", req.role);
let soul = workspace::show_control(req.repo, &commit, &soul_rel, git).map_err(|source| {
Error::ControlRead {
path: PathBuf::from(format!("{commit}:{soul_rel}")),
source,
}
})?;
let providers_raw = workspace::show_control(req.repo, &commit, PER_REPO_PROVIDERS_FILE, git)
.map_err(|source| Error::ControlRead {
path: PathBuf::from(format!("{commit}:{PER_REPO_PROVIDERS_FILE}")),
source,
})?;
let providers = PerRepoProviders::parse(
&providers_raw,
Path::new(&format!("{commit}:{PER_REPO_PROVIDERS_FILE}")),
)?;
let granted = providers
.roles
.get(req.role)
.map(|assignment| assignment.tools.clone())
.unwrap_or_default();
let commit_subject = format!("dispatch: {} [{sub_branch}]", req.role);
spawn_subagent_branch(
&SpawnRequest {
parent_worktree: req.parent_worktree,
parent_branch: req.parent_branch,
sub_branch: &sub_branch,
sub_worktree: &sub_worktree,
fork_point: req.fork_point,
goal_text: req.goal,
soul_text: Some(&soul),
granted: &granted,
commit_subject: &commit_subject,
},
git,
)?;
inbox::deposit(req.repo, &sub_branch, req.parent_branch, req.goal, clock)?;
inbox::probe_and_launch(req.repo, &sub_branch, launcher).map_err(|source| {
Error::ExecutorLock {
path: inbox::inbox_dir(req.repo, &sub_branch),
source,
}
})?;
Ok(sub_branch)
}
pub fn run_procedure(
req: &ChildDispatchRequest<'_>,
git: &dyn GitRunner,
clock: &dyn Clock,
id_gen: &dyn IdGen,
launcher: &dyn Launcher,
) -> Result<(), Error> {
match run(req, git, clock, id_gen, launcher) {
Err(refused @ Error::DispatchRefused { .. }) => {
eprintln!("lernie: {refused}");
Ok(())
}
other => other.map(drop),
}
}
fn budgets(
req: &ChildDispatchRequest<'_>,
commit: &str,
git: &dyn GitRunner,
) -> Result<crate::config::Budgets, Error> {
let raw = workspace::show_control(req.repo, commit, WORKFLOW_FILE, git).map_err(|source| {
Error::ControlRead {
path: PathBuf::from(format!("{commit}:{WORKFLOW_FILE}")),
source,
}
})?;
Ok(Workflow::parse(&raw, &PathBuf::from(format!("{commit}:{WORKFLOW_FILE}")))?.budgets)
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_grant;