#![expect(unused, reason = "extraction; PHASE-03 prunes")]
#![expect(
clippy::fn_params_excessive_bools,
reason = "classifier gathers multiple facts; refactoring out of scope for SL-116"
)]
use super::allowlist::{
Allowlist, allowlist_violations, is_withheld, parse_allowlist, select_copies,
};
use super::marker::{DISPATCH_WORKER_AGENT_TYPE, marker_present, write_marker};
use super::provision::run_provision;
use super::shared::{
gather_fork_worktree, gather_tree_clean, is_linked_worktree, matches, resolve_commit,
resolve_common_dir, target_dir_for_branch,
};
use crate::fsutil::{self, CopyOutcome};
use crate::git;
use crate::root;
use anyhow::{Context, bail};
use std::fs;
use std::io::{self, ErrorKind, Write};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Stamp {
Ok,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StampRefusal {
MissingCwd,
BadDir,
MissingAgentType,
AlreadyMarked,
}
impl StampRefusal {
pub(crate) fn token(self) -> &'static str {
match self {
StampRefusal::MissingCwd => "missing-cwd",
StampRefusal::BadDir => "bad-dir",
StampRefusal::MissingAgentType => "missing-agent-type",
StampRefusal::AlreadyMarked => "already-marked",
}
}
}
pub(crate) fn classify_stamp(
agent_type: &str,
cwd_present: bool,
cwd_is_under_repo_linked_worktree: bool,
already_marked: bool,
) -> Result<Stamp, StampRefusal> {
if !cwd_present {
return Err(StampRefusal::MissingCwd);
}
if !cwd_is_under_repo_linked_worktree {
return Err(StampRefusal::BadDir);
}
if agent_type != DISPATCH_WORKER_AGENT_TYPE {
return Err(StampRefusal::MissingAgentType);
}
if already_marked {
return Err(StampRefusal::AlreadyMarked);
}
Ok(Stamp::Ok)
}
#[derive(Debug, Default, serde::Deserialize)]
struct SubagentPayload {
#[serde(default)]
cwd: Option<String>,
#[serde(default)]
agent_type: Option<String>,
}
fn cwd_shares_repo(repo: &Path, cwd: &Path) -> bool {
let repo_common = git::git_text(repo, &["rev-parse", "--git-common-dir"])
.ok()
.and_then(|c| resolve_common_dir(repo, &c).ok());
let cwd_common = git::git_text(cwd, &["rev-parse", "--git-common-dir"])
.ok()
.and_then(|c| resolve_common_dir(cwd, &c).ok());
match (repo_common, cwd_common) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
pub(crate) fn run_stamp_subagent(path: Option<PathBuf>) -> anyhow::Result<()> {
let mut raw = String::new();
io::Read::read_to_string(&mut io::stdin(), &mut raw).context("read SubagentStart payload")?;
let payload: SubagentPayload = serde_json::from_str(&raw).unwrap_or_default();
let agent_type = payload.agent_type.unwrap_or_default();
let cwd_str = payload.cwd.unwrap_or_default();
let cwd_present = !cwd_str.is_empty();
let repo = root::find(path, &root::default_markers())
.ok()
.and_then(|r| fs::canonicalize(&r).ok());
let cwd_canon = if cwd_present {
fs::canonicalize(&cwd_str).ok()
} else {
None
};
let cwd_valid = match (repo.as_deref(), cwd_canon.as_deref()) {
(Some(repo), Some(cwd)) => {
is_linked_worktree(cwd).unwrap_or(false) && cwd_shares_repo(repo, cwd)
}
_ => false,
};
let already_marked = cwd_canon.as_deref().is_some_and(marker_present);
match classify_stamp(&agent_type, cwd_present, cwd_valid, already_marked) {
Ok(Stamp::Ok) => {}
Err(refusal) => {
writeln!(io::stderr(), "stamp-refused: {}", refusal.token())?;
bail!("stamp-refused: {}", refusal.token());
}
}
let (Some(_anchor), Some(cwd)) = (repo, cwd_canon) else {
let token = StampRefusal::BadDir.token();
writeln!(io::stderr(), "stamp-refused: {token}")?;
bail!("stamp-refused: {token}");
};
let act = git::primary_worktree(&cwd)
.and_then(|source| run_provision(Some(source), &cwd))
.and_then(|()| write_marker(&cwd));
if let Err(cause) = act {
writeln!(
io::stderr(),
"STAMP FAILED for {} — worktree LEFT in place (not removed); orchestrator post-spawn check will catch the unstamped worker: {cause:#}",
cwd.display()
)?;
return Err(cause.context(format!("stamp worker worktree {}", cwd.display())));
}
writeln!(io::stderr(), "stamped worker worktree {}", cwd.display())?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum WorkerVerify {
Ok,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum WorkerVerifyRefusal {
NoWorkerHead,
NotIsolated,
Unstamped,
WrongBase,
BranchMismatch,
}
impl WorkerVerifyRefusal {
pub(crate) fn token(self) -> &'static str {
match self {
WorkerVerifyRefusal::NoWorkerHead => "no-worker-head",
WorkerVerifyRefusal::NotIsolated => "not-isolated",
WorkerVerifyRefusal::Unstamped => "unstamped",
WorkerVerifyRefusal::WrongBase => "wrong-base",
WorkerVerifyRefusal::BranchMismatch => "branch-mismatch",
}
}
}
pub(crate) fn classify_worker_verify(
head_resolved: bool,
is_isolated: bool,
marker_present: bool,
base_is_ancestor: bool,
head_is_branch_tip: bool,
) -> Result<WorkerVerify, WorkerVerifyRefusal> {
if !head_resolved {
return Err(WorkerVerifyRefusal::NoWorkerHead);
}
if !is_isolated {
return Err(WorkerVerifyRefusal::NotIsolated);
}
if !marker_present {
return Err(WorkerVerifyRefusal::Unstamped);
}
if !base_is_ancestor {
return Err(WorkerVerifyRefusal::WrongBase);
}
if !head_is_branch_tip {
return Err(WorkerVerifyRefusal::BranchMismatch);
}
Ok(WorkerVerify::Ok)
}
pub(crate) fn run_verify_worker(
base: &str,
dir: &Path,
branch: Option<&str>,
) -> anyhow::Result<()> {
let head_resolved = git::git_opt(dir, &["rev-parse", "--verify", "HEAD"])?.is_some();
let is_isolated = head_resolved && is_linked_worktree(dir)?;
let marker = marker_present(dir);
let base_is_ancestor = git::git_status_ok(dir, &["merge-base", "--is-ancestor", base, "HEAD"])?;
let head_is_branch_tip = match branch {
Some(s) => {
let head = git::git_opt(dir, &["rev-parse", "--verify", "HEAD"])?;
let tip = git::git_opt(dir, &["rev-parse", "--verify", &format!("{s}^{{commit}}")])?;
matches!((head, tip), (Some(h), Some(t)) if h == t)
}
None => true,
};
match classify_worker_verify(
head_resolved,
is_isolated,
marker,
base_is_ancestor,
head_is_branch_tip,
) {
Ok(WorkerVerify::Ok) => {
writeln!(
io::stderr(),
"verify-worker: base==B holds for {}",
dir.display()
)?;
Ok(())
}
Err(refusal) => {
writeln!(
io::stderr(),
"verify-worker-refused: {} ({})",
refusal.token(),
dir.display()
)?;
bail!("verify-worker-refused: {}", refusal.token());
}
}
}