mod base;
use super::{Error, WORKER_ROLE, dispatch, fork_point, rebase_forward, role};
use crate::prompt::rebase_forward::{Replay, Replayed};
use crate::template::GitRunner;
use crate::workspace;
use std::path::Path;
const REASON: &str = "a retarget re-forks a running agent off another config commit (ARCH §2.2)";
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
Landed,
NoOp,
Conflicted(Vec<String>),
}
pub fn preflight(
workspace_dir: &Path,
agent_id: &str,
config_name: &str,
git: &dyn GitRunner,
) -> Result<Option<String>, Error> {
workspace::require(workspace_dir)?;
workspace::require_agent(workspace_dir, agent_id, REASON, git)?;
workspace::require_lineage(workspace_dir, config_name, git)
.map_err(|e| Error::from(fork_point::Error::from(e)))?;
let repo = workspace::repo_git(workspace_dir);
let branch = workspace::agent_ref(agent_id);
let spec = format!("{}^{{commit}}", workspace::config_ref(config_name));
let target = git
.run_capture(&repo, &["rev-parse", &spec])
.map_err(|source| Error::Git {
op: "retarget resolve target",
source,
})?
.trim()
.to_string();
if governing(workspace_dir, &branch, git)? == target {
return Ok(None);
}
let (role, tools) = grant_of(workspace_dir, &repo, agent_id, &branch, &target, git)?;
dispatch::require_described(
&repo,
&dispatch::Grant {
role: &role,
tools: &tools,
config_commit: &target,
},
git,
)?;
Ok(Some(target))
}
fn grant_of(
workspace_dir: &Path,
access: &Path,
agent_id: &str,
start: &str,
target: &str,
git: &dyn GitRunner,
) -> Result<(String, Vec<String>), Error> {
let role =
role::derive(access, start, agent_id, git)?.unwrap_or_else(|| WORKER_ROLE.to_string());
let tools = base::granted(workspace_dir, target, &role, git)?;
Ok((role, tools))
}
fn governing(workspace_dir: &Path, branch: &str, git: &dyn GitRunner) -> Result<String, Error> {
workspace::governing_config(workspace_dir, branch, git)
.map(|sha| sha.trim().to_string())
.map_err(|source| Error::Git {
op: "retarget governing config",
source,
})
}
pub fn land(
workspace_dir: &Path,
agent_id: &str,
worktree: &Path,
git: &dyn GitRunner,
) -> Result<Option<Outcome>, Error> {
let Some(target) = workspace::retarget::read(workspace_dir, agent_id, git) else {
return Ok(None);
};
let outcome = consume(workspace_dir, agent_id, worktree, &target, git);
workspace::retarget::clear(workspace_dir, agent_id, git).map_err(|source| Error::Git {
op: "retarget clear mark",
source,
})?;
outcome.map(Some)
}
fn consume(
workspace_dir: &Path,
agent_id: &str,
worktree: &Path,
target: &str,
git: &dyn GitRunner,
) -> Result<Outcome, Error> {
let branch = workspace::agent_ref(agent_id);
if governing(workspace_dir, &branch, git)? == target {
return Ok(Outcome::NoOp);
}
let dispatch_sha = role::founding_sha(worktree, &branch, agent_id, git)?.ok_or(Error::Git {
op: "retarget dispatch commit",
source: std::io::Error::other(format!(
"no dispatch commit founds [{agent_id}] on {branch} — a retarget re-forks the \
branch off its own founding commit (ARCH §2.2)"
)),
})?;
let (role, tools) = grant_of(
workspace_dir,
worktree,
agent_id,
&dispatch_sha,
target,
git,
)?;
let base = base::commit(
workspace_dir,
worktree,
agent_id,
&dispatch_sha,
&dispatch::Grant {
role: &role,
tools: &tools,
config_commit: target,
},
git,
)?;
let replayed = rebase_forward::run(
worktree,
&Replay {
branch_id: agent_id,
point: &dispatch_sha,
base: &base,
mark_id: agent_id,
mark_at: &branch,
},
git,
)?;
Ok(match replayed {
Replayed::Landed => Outcome::Landed,
Replayed::Conflicted(paths) => Outcome::Conflicted(paths),
})
}
#[cfg(test)]
pub(crate) mod tests;