use std::path::PathBuf;
use clap::Subcommand;
mod shared;
pub(crate) use shared::is_linked_worktree;
mod allowlist;
mod jail;
pub(crate) use jail::JailPolicy;
mod pretooluse;
pub(crate) use pretooluse::run_pretooluse;
mod jail_prefix;
mod marker;
#[cfg(test)]
pub(crate) use marker::{Cause, DISPATCH_WORKER_AGENT_TYPE, describe_mode};
pub(crate) use marker::{
DUAL_CAUSE, env_worker_set, marker_present, resolve_mode, run_marker_clear, run_status,
};
mod coordinate;
mod create;
mod dispatch_record;
mod fork;
mod gc;
mod import;
mod inventory;
mod land;
mod provision;
mod subagent;
pub(crate) use coordinate::{coordinate, run_branch_point_check, run_coordinate};
pub(crate) use create::{ARMING_JAIL_FILE, ARMING_SUBPATH, run_create_fork};
pub(crate) use dispatch_record::{DispatchRecord, resolve_agent};
pub(crate) use fork::run_fork;
pub(crate) use gc::run_gc;
pub(crate) use import::{CLAUDE_PREFIX, DOCTRINE_PREFIX, gather_worktree_delta_paths, run_import};
pub(crate) use inventory::run_list;
pub(crate) use land::run_land;
pub(crate) use provision::{run_check_allowlist, run_provision};
pub(crate) use subagent::{run_stamp_subagent, run_verify_worker};
#[cfg(test)]
pub(crate) use coordinate::{CoordAction, CoordRefusal, base_has_slice_plan, classify_coordinate};
#[cfg(test)]
pub(crate) use gc::{GcPlan, GcRefusal, GcState, GcVerdict, classify_gc};
#[cfg(test)]
pub(crate) use land::{ForkState, LandRefusal, Merge, classify_land, no_such_fork_message};
#[cfg(test)]
pub(crate) use subagent::{
Stamp, StampRefusal, WorkerVerify, WorkerVerifyRefusal, classify_stamp, classify_worker_verify,
};
#[cfg(test)]
pub(crate) use dispatch_record::provision_dispatch_record;
#[cfg(test)]
pub(crate) use import::{Apply, Refusal, classify_import};
#[cfg(test)]
mod test_helpers;
#[cfg(test)]
pub(crate) use crate::globmatch::glob_matches;
#[cfg(test)]
pub(crate) use allowlist::{DERIVED_RUNTIME, WITHHELD};
#[derive(Subcommand)]
pub(crate) enum WorktreeCommand {
Provision {
fork: PathBuf,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
CheckAllowlist {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
BranchPointCheck {
#[arg(long)]
base: String,
#[arg(long)]
head: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Fork {
#[arg(long)]
base: String,
#[arg(long)]
branch: String,
#[arg(long)]
dir: PathBuf,
#[arg(long)]
worker: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
CreateFork,
Pretooluse,
JailPrefix {
#[arg(long)]
dir: PathBuf,
#[arg(long)]
main_root: Option<PathBuf>,
#[arg(long)]
out: PathBuf,
#[arg(long)]
network: bool,
#[arg(long)]
extra_rw: Vec<PathBuf>,
},
Coordinate {
#[arg(long)]
slice: u32,
#[arg(long)]
dir: PathBuf,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Import {
#[arg(long)]
base: String,
#[arg(long, conflicts_with = "from_worktree")]
fork: Option<String>,
#[arg(long = "from-worktree")]
from_worktree: Option<PathBuf>,
#[arg(long, value_parser = crate::slice::parse_cli_id)]
slice: Option<u32>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Land {
#[arg(long)]
fork: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Gc {
#[arg(long)]
fork: String,
#[arg(long)]
superseded_head: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
dry_run: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
#[arg(long)]
assert: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[arg(long)]
slice: Option<u32>,
#[arg(long)]
json: bool,
#[arg(long)]
no_landed: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
VerifyWorker {
#[arg(long)]
base: String,
#[arg(long)]
dir: PathBuf,
#[arg(long)]
branch: Option<String>,
},
Marker {
#[arg(long)]
clear: bool,
#[arg(long)]
operator: bool,
#[arg(long)]
stamp_subagent: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
pub(crate) fn dispatch(cmd: WorktreeCommand) -> anyhow::Result<()> {
match cmd {
WorktreeCommand::Provision { fork, path } => run_provision(path, &fork),
WorktreeCommand::CheckAllowlist { path } => run_check_allowlist(path),
WorktreeCommand::BranchPointCheck { base, head, path } => {
run_branch_point_check(path, &base, head)
}
WorktreeCommand::Fork {
base,
branch,
dir,
worker,
path,
} => run_fork(path, &base, &branch, &dir, worker),
WorktreeCommand::CreateFork => run_create_fork(),
WorktreeCommand::Pretooluse => run_pretooluse(),
WorktreeCommand::JailPrefix {
dir,
main_root,
out,
network,
extra_rw,
} => jail_prefix::run_jail_prefix(&dir, main_root.as_deref(), &out, network, &extra_rw),
WorktreeCommand::Coordinate { slice, dir, path } => {
let repo = crate::root::find(path.clone(), &crate::root::default_markers())?;
let authoring = crate::dtoml::load_doctrine_toml(&repo)?
.dispatch
.authoring_branch;
run_coordinate(path, slice, &dir, authoring.as_deref())
}
WorktreeCommand::Import {
base,
fork,
from_worktree,
slice,
path,
} => {
let selectors = match slice {
Some(id) => {
let root = crate::root::find(path.clone(), &crate::root::default_markers())?;
crate::slice::selectors(
&root,
id,
Some(crate::slice::SelectorIntent::DesignTarget),
)?
}
None => Vec::new(),
};
run_import(
path,
&base,
fork.as_deref(),
from_worktree.as_deref(),
&selectors,
)
}
WorktreeCommand::Land { fork, path } => run_land(path, &fork),
WorktreeCommand::Gc {
fork,
superseded_head,
force,
dry_run,
path,
} => run_gc(path, &fork, superseded_head.as_deref(), force, dry_run),
WorktreeCommand::Status { assert, path } => run_status(path, assert),
WorktreeCommand::List {
slice,
json,
no_landed,
path,
} => run_list(path, slice, json, no_landed),
WorktreeCommand::VerifyWorker { base, dir, branch } => {
run_verify_worker(&base, &dir, branch.as_deref())
}
WorktreeCommand::Marker {
clear,
operator,
stamp_subagent,
path,
} => {
if stamp_subagent {
run_stamp_subagent(path)
} else if clear {
run_marker_clear(path, operator)
} else {
anyhow::bail!("`worktree marker` requires `--clear` or `--stamp-subagent`")
}
}
}
}
#[cfg(test)]
mod tests {
use super::coordinate::ensure_base_corpus_fresh;
use super::test_helpers::{git, init_repo};
use super::*;
use glob::Pattern;
use std::fs;
use std::path::Path;
use super::fork::rollback_fork;
fn fork_state(exists: bool, has_live_worktree: bool, bears_marker: bool) -> ForkState {
ForkState {
exists,
has_live_worktree,
bears_marker,
}
}
#[test]
fn classify_land_precedence_and_ok() {
assert_eq!(
classify_land(true, "main", fork_state(true, true, false)),
Ok(Merge::Ok)
);
assert_eq!(
classify_land(false, "main", fork_state(false, false, true)),
Err(LandRefusal::TreeUnclean)
);
assert_eq!(
classify_land(true, "main", fork_state(false, false, true)),
Err(LandRefusal::NoSuchFork)
);
assert_eq!(
classify_land(true, "main", fork_state(true, false, false)),
Err(LandRefusal::WorktreeGone)
);
assert_eq!(
classify_land(true, "main", fork_state(true, true, true)),
Err(LandRefusal::DispatchFork)
);
}
#[test]
fn classify_land_ignores_head() {
let st = fork_state(true, true, false);
assert_eq!(
classify_land(true, "main", st),
classify_land(true, "detached-xyz", st)
);
}
#[test]
fn no_such_fork_message_preserves_token_and_hints_branch_not_path() {
let msg = no_such_fork_message("solo-foo");
assert!(
msg.contains("no-such-fork"),
"VT-golden token preserved: {msg}"
);
assert!(msg.contains("solo-foo"), "names the offending ref: {msg}");
assert!(
msg.contains("branch name"),
"hints it wants a branch: {msg}"
);
assert!(
msg.contains("not a worktree path"),
"hints the path confusion: {msg}"
);
}
#[test]
fn land_refusal_tokens_are_distinct_and_exhaustive() {
let all = [
LandRefusal::TreeUnclean,
LandRefusal::NoSuchFork,
LandRefusal::WorktreeGone,
LandRefusal::DispatchFork,
LandRefusal::MergeConflict,
LandRefusal::WedgedMerge,
LandRefusal::InconsistentMergeState,
];
let tokens: Vec<&str> = all.iter().map(|r| r.token()).collect();
assert_eq!(tokens.len(), 7, "exactly seven refusal tokens");
let unique: std::collections::BTreeSet<&str> = tokens.iter().copied().collect();
assert_eq!(unique.len(), 7, "every token is distinct");
assert_eq!(LandRefusal::WedgedMerge.token(), "wedged-merge");
assert_eq!(LandRefusal::MergeConflict.token(), "merge-conflict");
assert_eq!(
LandRefusal::InconsistentMergeState.token(),
"inconsistent-merge-state"
);
}
fn gc_state(
branch_exists: bool,
worktree_present: bool,
landed_verdict: Option<bool>,
) -> GcState {
GcState {
branch_exists,
worktree_present,
landed_verdict,
}
}
#[test]
fn classify_coordinate_create_resume_collide() {
assert_eq!(classify_coordinate(false, false), Ok(CoordAction::Create));
assert_eq!(classify_coordinate(false, true), Ok(CoordAction::Create));
assert_eq!(classify_coordinate(true, false), Ok(CoordAction::Resume));
assert_eq!(
classify_coordinate(true, true),
Err(CoordRefusal::LiveWorktree)
);
}
#[test]
fn coord_refusal_token_distinct() {
assert_eq!(CoordRefusal::LiveWorktree.token(), "coordination-live");
}
#[test]
fn classify_gc_landed_reaps_present_things_in_order() {
let v = classify_gc(gc_state(true, true, Some(true)), false, false, false);
assert_eq!(
v,
GcVerdict::Reap(GcPlan {
remove_worktree: true,
delete_branch: true,
})
);
}
#[test]
fn classify_gc_skips_absent_steps() {
let v = classify_gc(gc_state(true, false, Some(true)), false, false, false);
assert_eq!(
v,
GcVerdict::Reap(GcPlan {
remove_worktree: false,
delete_branch: true,
})
);
}
#[test]
fn classify_gc_branch_gone_reaps_nothing() {
let v = classify_gc(gc_state(false, false, None), false, false, false);
assert_eq!(
v,
GcVerdict::Reap(GcPlan {
remove_worktree: false,
delete_branch: false,
})
);
}
#[test]
fn classify_gc_not_landed_refuses_unless_overridden() {
let st = gc_state(true, true, Some(false));
assert_eq!(
classify_gc(st, false, false, false),
GcVerdict::Refuse(GcRefusal::NotLanded)
);
assert!(matches!(
classify_gc(st, true, false, false),
GcVerdict::Reap(_)
));
assert!(matches!(
classify_gc(st, false, true, false),
GcVerdict::Reap(_)
));
}
#[test]
fn classify_gc_dry_run_does_not_change_the_verdict() {
let landed = gc_state(true, true, Some(true));
assert_eq!(
classify_gc(landed, false, false, true),
classify_gc(landed, false, false, false)
);
let refused = gc_state(true, true, Some(false));
assert_eq!(
classify_gc(refused, false, false, true),
classify_gc(refused, false, false, false)
);
}
#[test]
fn gc_refusal_token_is_not_landed() {
assert_eq!(GcRefusal::NotLanded.token(), "not-landed");
}
#[test]
fn describe_mode_truth_table() {
let solo_plain = describe_mode(false, false, false);
assert!(!solo_plain.refused, "no signal ⇒ writes allowed");
assert_eq!(solo_plain.cause, Cause::None);
let marker_on_main = describe_mode(false, true, false);
assert!(
!marker_on_main.refused,
"marker without a linked worktree is inert ⇒ allowed"
);
assert_eq!(marker_on_main.cause, Cause::None);
let linked_no_marker = describe_mode(true, false, false);
assert!(!linked_no_marker.refused, "linked, no marker ⇒ allowed");
assert_eq!(linked_no_marker.cause, Cause::None);
let marker = describe_mode(true, true, false);
assert!(marker.refused);
assert_eq!(marker.cause, Cause::Marker);
assert!(
marker.is_stale_marker(),
"marker-only in a fork is the stale-marker case"
);
assert!(!marker.is_env_on_nonlinked());
let env_main = describe_mode(false, false, true);
assert!(env_main.refused);
assert_eq!(env_main.cause, Cause::Env);
assert!(env_main.is_env_on_nonlinked(), "env on main ⇒ dual-cause");
assert!(!env_main.is_stale_marker());
let env_linked = describe_mode(true, false, true);
assert!(env_linked.refused);
assert_eq!(env_linked.cause, Cause::Env);
assert!(!env_linked.is_env_on_nonlinked());
let both = describe_mode(true, true, true);
assert!(both.refused);
assert_eq!(both.cause, Cause::Both);
assert!(
!both.is_stale_marker(),
"both is not the marker-only stale case"
);
assert_eq!(solo_plain.cause_token(), "none");
assert_eq!(marker.cause_token(), "marker");
assert_eq!(env_main.cause_token(), "env");
assert_eq!(both.cause_token(), "both");
}
#[test]
fn withheld_globs_all_compile() {
for item in WITHHELD {
Pattern::new(item.glob).unwrap();
}
for g in DERIVED_RUNTIME {
Pattern::new(g).unwrap();
}
}
fn gitignore_representative(line: &str) -> String {
let base = line
.strip_suffix('/')
.map_or_else(|| line.to_string(), |dir| format!("{dir}/f"));
base.replace('*', "x")
}
fn classified(rep: &str) -> bool {
WITHHELD
.iter()
.any(|item| glob_matches(&Pattern::new(item.glob).unwrap(), rep))
|| DERIVED_RUNTIME
.iter()
.any(|g| glob_matches(&Pattern::new(g).unwrap(), rep))
}
#[test]
fn every_runtime_gitignore_glob_is_classified() {
let gitignore = fs::read_to_string(".gitignore").unwrap();
for raw in gitignore.lines() {
let line = raw.trim();
if !line.starts_with(".doctrine/") || line == ".doctrine/*" {
continue;
}
let rep = gitignore_representative(line);
assert!(
classified(&rep),
"unclassified runtime gitignore glob `{line}` (rep `{rep}`) — \
add it to WITHHELD or DERIVED_RUNTIME"
);
}
}
#[test]
fn is_linked_worktree_true_for_a_fork_false_for_the_primary_tree() {
let tmp = tempfile::tempdir().unwrap();
let primary = init_repo(&tmp.path().join("src"));
let fork = tmp.path().join("fork");
git(
&primary,
&[
"worktree",
"add",
"-q",
"-b",
"feat",
fork.to_str().unwrap(),
],
);
let fork = fs::canonicalize(&fork).unwrap();
assert!(is_linked_worktree(&fork).unwrap(), "a linked worktree");
assert!(!is_linked_worktree(&primary).unwrap(), "the primary tree");
}
#[test]
fn primary_worktree_resolves_to_the_main_tree_from_a_fork_or_itself() {
let tmp = tempfile::tempdir().unwrap();
let primary = init_repo(&tmp.path().join("src"));
let fork = tmp.path().join("fork");
git(
&primary,
&[
"worktree",
"add",
"-q",
"-b",
"feat",
fork.to_str().unwrap(),
],
);
let fork = fs::canonicalize(&fork).unwrap();
assert_eq!(
crate::git::primary_worktree(&fork).unwrap(),
primary,
"a linked worktree resolves to the main tree"
);
assert_eq!(
crate::git::primary_worktree(&primary).unwrap(),
primary,
"the main tree resolves to itself"
);
}
#[test]
fn rollback_fork_retracts_stale_worktree_entry_after_fs_reap() {
let tmp = tempfile::tempdir().unwrap();
let repo = init_repo(&tmp.path().join("src"));
let dir = tmp.path().join("orphan");
fs::create_dir_all(&dir).unwrap();
let debris = rollback_fork(&repo, "no-such-branch", &dir);
assert!(
debris.is_empty(),
"a fully fs-reaped rollback reports no debris; got: {debris:?}"
);
assert!(!dir.exists(), "the orphan dir was reaped");
}
fn commit_slice_plan(repo: &Path, slice: u32) {
let slice_dir = repo.join(format!(".doctrine/slice/{slice:03}"));
fs::create_dir_all(&slice_dir).unwrap();
fs::write(slice_dir.join("plan.toml"), "# plan\n").unwrap();
git(repo, &["add", "."]);
git(repo, &["commit", "-q", "-m", "add slice plan"]);
}
#[test]
fn base_has_slice_plan_tracks_presence_on_the_trunk_tree() {
let tmp = tempfile::tempdir().unwrap();
let repo = init_repo(&tmp.path().join("src"));
assert!(
!base_has_slice_plan(&repo, "main", 127).unwrap(),
"base lacking the slice plan ⇒ absent"
);
commit_slice_plan(&repo, 127);
assert!(
base_has_slice_plan(&repo, "main", 127).unwrap(),
"base carrying the slice plan ⇒ present"
);
assert!(
!base_has_slice_plan(&repo, "main", 99).unwrap(),
"an unrelated slice number stays absent"
);
}
#[test]
fn coordinate_refuses_create_when_base_lacks_the_slice_plan() {
let tmp = tempfile::tempdir().unwrap();
let repo = init_repo(&tmp.path().join("src"));
let dir = tmp.path().join("coord");
let Err(err) = coordinate(&repo, 127, &dir, None) else {
panic!("must refuse: base predates plan");
};
let msg = format!("{err:#}");
assert!(
msg.contains("DOCTRINE_TRUNK_REF"),
"refusal must hint DOCTRINE_TRUNK_REF; got: {msg}"
);
assert!(
msg.contains(".doctrine/slice/127/plan.toml"),
"refusal names the missing plan path; got: {msg}"
);
assert!(
!dir.exists(),
"no worktree dir is created on the early bail"
);
}
fn corpus_repo() -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let root = init_repo(&tmp.path().join("src"));
std::fs::create_dir_all(root.join(".doctrine")).unwrap();
std::fs::write(root.join(".doctrine/a.toml"), "v1").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-q", "-m", "corpus C0"]);
git(&root, &["checkout", "-q", "-b", "edge"]);
std::fs::write(root.join(".doctrine/b.toml"), "v1").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-q", "-m", "corpus C1"]);
git(&root, &["checkout", "-q", "main"]);
(tmp, root)
}
#[test]
fn ensure_base_corpus_fresh_refuses_when_base_predates_corpus() {
let (_tmp, root) = corpus_repo();
let Err(err) = ensure_base_corpus_fresh(&root, Some("edge"), "main") else {
panic!("must refuse: main predates edge's corpus");
};
assert!(
format!("{err:#}").contains(crate::corpus_guard::BASE_CORPUS_STALE),
"refusal carries BASE_CORPUS_STALE; got: {err:#}"
);
}
#[test]
fn ensure_base_corpus_fresh_ok_when_base_carries_corpus() {
let (_tmp, root) = corpus_repo();
ensure_base_corpus_fresh(&root, Some("edge"), "edge").expect("edge carries its corpus");
}
#[test]
fn ensure_base_corpus_fresh_noop_when_authoring_unset() {
let (_tmp, root) = corpus_repo();
ensure_base_corpus_fresh(&root, None, "main").expect("posture off ⇒ no-op");
}
#[test]
fn ensure_base_corpus_fresh_noop_when_no_corpus_yet() {
let tmp = tempfile::tempdir().unwrap();
let root = init_repo(&tmp.path().join("src")); ensure_base_corpus_fresh(&root, Some("main"), "main").expect("no corpus yet ⇒ no-op");
}
#[test]
fn ensure_base_corpus_fresh_refuses_when_authoring_unresolvable() {
let tmp = tempfile::tempdir().unwrap();
let root = init_repo(&tmp.path().join("src"));
assert!(
ensure_base_corpus_fresh(&root, Some("refs/heads/ghost"), "main").is_err(),
"an unresolvable authoring ref must fail closed"
);
}
#[test]
fn coordinate_refuses_create_when_base_predates_corpus() {
let (_tmp, root) = corpus_repo();
std::fs::create_dir_all(root.join(".doctrine/slice/127")).unwrap();
std::fs::write(root.join(".doctrine/slice/127/plan.toml"), "schema=1").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-q", "-m", "plan on main"]);
git(&root, &["checkout", "-q", "-B", "edge"]);
std::fs::write(root.join(".doctrine/c.toml"), "v1").unwrap();
git(&root, &["add", "."]);
git(&root, &["commit", "-q", "-m", "corpus C2 on edge only"]);
git(&root, &["checkout", "-q", "main"]);
let dir = root.parent().unwrap().join("coord");
let Err(err) = coordinate(&root, 127, &dir, Some("edge")) else {
panic!("must refuse: base predates edge corpus");
};
let msg = format!("{err:#}");
assert!(
msg.contains(crate::corpus_guard::BASE_CORPUS_STALE),
"refusal carries BASE_CORPUS_STALE; got: {msg}"
);
assert!(!dir.exists(), "no worktree dir created on the g2 bail");
}
#[test]
fn classify_stamp_ok_when_all_inputs_hold() {
assert_eq!(
classify_stamp(DISPATCH_WORKER_AGENT_TYPE, true, true, false),
Ok(Stamp::Ok)
);
}
#[test]
fn classify_stamp_missing_cwd_refuses() {
assert_eq!(
classify_stamp(DISPATCH_WORKER_AGENT_TYPE, false, false, false),
Err(StampRefusal::MissingCwd)
);
assert_eq!(StampRefusal::MissingCwd.token(), "missing-cwd");
}
#[test]
fn classify_stamp_bad_dir_refuses_when_cwd_present_but_invalid() {
assert_eq!(
classify_stamp(DISPATCH_WORKER_AGENT_TYPE, true, false, false),
Err(StampRefusal::BadDir)
);
assert_eq!(
classify_stamp("anything", true, false, false),
Err(StampRefusal::BadDir)
);
assert_eq!(StampRefusal::BadDir.token(), "bad-dir");
}
#[test]
fn classify_stamp_missing_agent_type_refuses() {
assert_eq!(
classify_stamp("", true, true, false),
Err(StampRefusal::MissingAgentType)
);
assert_eq!(
classify_stamp("some-other-agent", true, true, false),
Err(StampRefusal::MissingAgentType)
);
assert_eq!(StampRefusal::MissingAgentType.token(), "missing-agent-type");
}
#[test]
fn classify_stamp_already_marked_refuses() {
assert_eq!(
classify_stamp(DISPATCH_WORKER_AGENT_TYPE, true, true, true),
Err(StampRefusal::AlreadyMarked)
);
assert_eq!(StampRefusal::AlreadyMarked.token(), "already-marked");
}
#[test]
fn classify_worker_verify_ok_when_all_preconds_hold() {
assert_eq!(
classify_worker_verify(true, true, true, true, true),
Ok(WorkerVerify::Ok)
);
}
#[test]
fn classify_worker_verify_no_worker_head_refuses_first() {
assert_eq!(
classify_worker_verify(false, true, true, true, true),
Err(WorkerVerifyRefusal::NoWorkerHead)
);
assert_eq!(
classify_worker_verify(false, false, false, false, false),
Err(WorkerVerifyRefusal::NoWorkerHead)
);
assert_eq!(WorkerVerifyRefusal::NoWorkerHead.token(), "no-worker-head");
}
#[test]
fn classify_worker_verify_unstamped_names_itself_before_base() {
assert_eq!(
classify_worker_verify(true, true, false, false, true),
Err(WorkerVerifyRefusal::Unstamped)
);
assert_eq!(WorkerVerifyRefusal::Unstamped.token(), "unstamped");
}
#[test]
fn classify_worker_verify_wrong_base_refuses_last() {
assert_eq!(
classify_worker_verify(true, true, true, false, true),
Err(WorkerVerifyRefusal::WrongBase)
);
assert_eq!(WorkerVerifyRefusal::WrongBase.token(), "wrong-base");
}
#[test]
fn classify_worker_verify_not_isolated_refuses_after_head_before_marker() {
assert_eq!(
classify_worker_verify(true, false, true, true, true),
Err(WorkerVerifyRefusal::NotIsolated)
);
assert_eq!(
classify_worker_verify(true, false, false, false, false),
Err(WorkerVerifyRefusal::NotIsolated)
);
assert_eq!(WorkerVerifyRefusal::NotIsolated.token(), "not-isolated");
}
#[test]
fn classify_worker_verify_branch_mismatch_refuses_last() {
assert_eq!(
classify_worker_verify(true, true, true, true, false),
Err(WorkerVerifyRefusal::BranchMismatch)
);
assert_eq!(
classify_worker_verify(true, true, true, true, true),
Ok(WorkerVerify::Ok)
);
assert_eq!(
WorkerVerifyRefusal::BranchMismatch.token(),
"branch-mismatch"
);
}
#[test]
fn dispatch_worker_agent_def_name_matches_const() {
let manifest = crate::test_support::repo_root();
let def = manifest.join("install/agents/claude/dispatch-worker.md");
let text =
fs::read_to_string(&def).unwrap_or_else(|e| panic!("read {}: {e}", def.display()));
let name = text
.lines()
.find_map(|l| l.trim().strip_prefix("name:"))
.map(str::trim)
.unwrap_or_else(|| panic!("no `name:` frontmatter in {}", def.display()));
assert_eq!(
name, DISPATCH_WORKER_AGENT_TYPE,
"agent-def name must equal DISPATCH_WORKER_AGENT_TYPE"
);
}
#[test]
fn dispatch_agent_skill_subagent_type_matches_const() {
let manifest = crate::test_support::repo_root();
let skill = manifest.join("plugins/doctrine/skills/dispatch-agent/SKILL.md");
let text =
fs::read_to_string(&skill).unwrap_or_else(|e| panic!("read {}: {e}", skill.display()));
let pinned = text
.lines()
.find_map(|l| l.split_once("subagent_type:").map(|(_, rest)| rest))
.map(|rest| {
rest.trim()
.trim_start_matches('`')
.split([' ', '`', '#'])
.next()
.unwrap_or("")
.trim()
})
.unwrap_or_else(|| panic!("no `subagent_type:` line in {}", skill.display()));
assert_eq!(
pinned, DISPATCH_WORKER_AGENT_TYPE,
"/dispatch-agent subagent_type must equal DISPATCH_WORKER_AGENT_TYPE"
);
}
}