//! Isolated native Planning fixtures. Records here are test inputs, never live
//! carrier evidence. The production CLI must validate every fixture transition.
use std::{
collections::BTreeSet,
fs,
path::Path,
process::{Command, Output},
time::{SystemTime, UNIX_EPOCH},
};
use sha2::{Digest, Sha256};
use shepherd_cli::shepherd::run::RunStatus;
use shepherd_cli::{
BindRootDispatchRequest, DispatchService, DispatchStore,
portable_path::portable_absolute_path,
shepherd::{
Harness, RunState,
dispatch::{
AgentId, AgentType, AttachmentKind, CapabilityProbe, CarrierAttachmentExpectation,
DispatchId, DispatchRecord, DispatchStart, GitCommit, NativeIdentity, PathAuthority,
PendingDispatch, PendingLaunchState, ProjectId, Role, RunId, SessionId,
StartupAttachment, StopRequest, WorkKind,
},
registry::{DispatchSingletonInput, DispatchSingletonPublicationInput, Registry},
},
};
pub(crate) fn invoke(root: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(args)
.current_dir(root)
.env("SHEPHERD_HOME", root.join("isolated-home"))
.env("SHEPHERD_MODEL_QUOTA", "128")
.output()
.expect("native fixture command")
}
fn accepted(root: &Path, args: &[&str]) -> Output {
let output = invoke(root, args);
assert!(
output.status.success(),
"{args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
output
}
fn hash(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
pub(crate) fn seed(root: &Path, run: &str) {
seed_for_lanes(root, run, 1);
}
fn deliverable_id(index: usize) -> String {
if index == 0 {
"native-validator".into()
} else {
format!("native-validator-{index}")
}
}
fn seed_for_lanes(root: &Path, run: &str, lane_count: usize) {
let directory = root.join(format!(".shepherd/runs/{run}"));
let source = include_str!("../fixtures/seed-contract/valid/seed.md")
.replace("v999", run)
.replace(
"Do not start execution from seed verification.",
"Do not publish or mutate external systems.",
)
.replace(
"Keep the run planted during verification.",
"Use native planned and executing transitions.",
)
.replace(
"Do not start execution.",
"No releases or external mutations.",
);
let additions = (1..lane_count).map(|index| format!(
" - id: {}\n result: \"Native fixture slice {index} reports a verified outcome.\"\n sources: [MESH-1]\n acceptance: \"The positive fixture exits 0 and the negative fixtures exit 1.\"\n", deliverable_id(index)
)).collect::<String>();
let source = source.replacen("constraints:\n", &format!("{additions}constraints:\n"), 1);
fs::write(directory.join("seed.md"), source).expect("typed fixture seed");
fs::write(
directory.join("mesh.md"),
include_str!("../fixtures/seed-contract/valid/mesh.md"),
)
.expect("fixture mesh");
accepted(
root,
&[
"run",
"set",
run,
"--seed",
&format!(".shepherd/runs/{run}/seed.md"),
],
);
}
pub(crate) fn materialize(root: &Path, run: &str, lanes: &[&str], ceiling: usize) -> String {
assert!(!lanes.is_empty() && ceiling > 0 && ceiling <= 128);
let host = std::thread::available_parallelism()
.map(usize::from)
.unwrap_or(1);
let effective = host.min(ceiling);
let directory = root.join(format!(".shepherd/runs/{run}"));
let config_path = root.join(".shepherd/shepherd.toml");
let mut config: toml::Value = fs::read_to_string(&config_path)
.ok()
.map(|text| toml::from_str(&text).expect("fixture config"))
.unwrap_or_else(|| toml::Value::Table(Default::default()));
let spawn = config
.as_table_mut()
.expect("fixture config table")
.entry("spawn")
.or_insert_with(|| toml::Value::Table(Default::default()));
spawn.as_table_mut().expect("fixture spawn table").insert(
"max_parallel".into(),
toml::Value::Integer(i64::try_from(ceiling).expect("small ceiling")),
);
fs::write(
config_path,
toml::to_string(&config).expect("fixture config TOML"),
)
.expect("fixture config");
let cargo = lanes
.iter()
.enumerate()
.map(|(i, lane)| format!("{lane}=plan-fixture-{i}"))
.collect::<Vec<_>>()
.join(", ");
let conductors = lanes
.iter()
.map(|lane| format!("{lane}=conductor"))
.collect::<Vec<_>>()
.join(", ");
let schedule = if lanes.len() >= 4 {
lanes
.iter()
.map(|lane| format!("{lane}@{effective}"))
.collect::<Vec<_>>()
.join(", ")
} else {
String::new()
};
let scale = if lanes.len() >= 6 {
"typed-gate"
} else {
"none"
};
let deliverables = (0..lanes.len())
.map(deliverable_id)
.collect::<Vec<_>>()
.join(", ");
let mut plan = format!(
"# Plan: {run}\n\
## Scope contract\n\
- Schema: shepherd.plan/2\n- Run: {run}\n\
- Seed: .shepherd/runs/{run}/seed.md\n- Mesh: .shepherd/runs/{run}/mesh.md\n\
- Planning evidence: .shepherd/runs/{run}/phase0.md\n- Goal: verify all native fixture boundaries\n\
- Deliverables: [{deliverables}]\n- Lanes: [{}]\n\
- Root roles: [shepherd, planter]\n- Child lead roles: [engineer, conductor]\n\
- Planning lead: engineer\n- Engineer count: 1\n- Review rejection limit: 3\n\
- Fourth rejection: malignant-revoke-quarantine-preserve-evidence-no-resume-root-lineage-replacement\n\
- Root continuation: fresh-root-preferred-new-run-binding-clears-child-authority\n\
- Exclusions: [native state, external systems]\n\
## Assumptions and decisions\n| id | statement | source | owner | blocking | evidence/disposition |\n| a-1 | fixture data only | phase0 | engineer | false | accepted |\n\
## Interfaces\n| id | version | producer | consumers | acceptance |\n| source | 1 | baseline | all | exact |\n\
## Phases\n| phase | predecessor frontier | node ids | disjointness proof |\n| fixture | baseline | all | exact paths |\n\
## Capacity\n```yaml\nlogical_lane_limit: {}\nhost_process_ceiling: {host}\nproject_spawn_max_parallel: {ceiling}\nplan_process_ceiling: {ceiling}\nparent_role_cap: {ceiling}\nrun_budget: {ceiling}\nsimultaneous_process_ceiling: {effective}\nper_lane_child_wave_ceiling: {ceiling}\ndisk_min_mib: 1024\nmodel_quota: {ceiling}\nbackpressure: queue-fair\ncargo_targets: [{cargo}]\nconductors: [{conductors}]\nschedule: [{schedule}]\nscale_outcome: {scale}\n```\n## Nodes\n",
lanes.join(", "),
lanes.len()
);
fs::create_dir_all(root.join("src")).expect("fixture source directory");
for (index, lane) in lanes.iter().enumerate() {
let deliveries = deliverable_id(index);
plan.push_str(&format!("### node-{index}\n```yaml\nid: node-{index}\nseed_deliverables: [{deliveries}]\nlane: {lane}\nrole: coder\nwork_kind: production\noutcome: lane fixture {index} verified\nowns: [src/plan-fixture-{index}.rs]\nforbidden: [.shepherd]\nconsumes: [source@1]\nproduces: [fixture-{index}@1]\ndepends_on: []\nred: {{command: [cargo, test], expects: failure, reason: missing behavior}}\ngreen: {{command: [cargo, test], expects: success, reason: verified behavior}}\neval: {{command: [cargo, test], threshold: 80}}\nevidence: .shepherd/runs/{run}/lanes/{lane}/evidence.json\nreview: auditor: bounded fixture acceptance\nfailure_route: conductor-to-root\nrollback: revert owned fixture path\n```\n"));
fs::write(
root.join(format!("src/plan-fixture-{index}.rs")),
"fn fixture() {}\n",
)
.expect("fixture source");
}
fs::write(directory.join("plan.md"), plan).expect("fixture plan");
accepted(root, &["plan", "materialize", "--run", run]);
for args in [
vec!["add", "."],
vec![
"-c",
"user.name=Shepherd Tests",
"-c",
"user.email=shepherd-tests@example.invalid",
"commit",
"--quiet",
"-m",
"native planning fixture",
],
] {
assert!(
Command::new("git")
.args(args)
.current_dir(root)
.status()
.expect("fixture git")
.success()
);
}
let head = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(root)
.output()
.expect("fixture HEAD");
assert!(head.status.success());
let baseline = String::from_utf8(head.stdout)
.expect("HEAD UTF-8")
.trim()
.to_owned();
// Produced by the same function the CLI verifies against. A hand-rolled
// copy of the formula drifted on Windows -- it wrote `path:<root>` while
// production observes `windows:<root>:<created>:<written>` -- and every
// fixture built here was refused as a symlinked or replaced worktree.
// Canonicalize first: the CLI observes its root through `getcwd`, which has
// already resolved links, while `env::temp_dir()` hands this fixture the
// unresolved `/var/...` spelling on macOS and the identity refuses a path
// reached through a symlink.
let canonical = fs::canonicalize(root).expect("canonical fixture worktree");
let identity =
shepherd_cli::cmd::planning::worktree_identity(&canonical).expect("fixture identity");
let probes = serde_json::json!({"schema":"shepherd.plan-probes/1", "baseline":baseline, "worktree_identity":identity,
"probes":(0..lanes.len()).map(|index| serde_json::json!({"kind":"path", "path":format!("src/plan-fixture-{index}.rs"),"expectation":"modify","path_kind":"file"})).collect::<Vec<_>>()});
fs::write(
directory.join("plan-probes.json"),
serde_json::to_vec(&probes).expect("fixture probes"),
)
.expect("fixture probes");
baseline
}
fn now() -> i64 {
i64::try_from(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_millis(),
)
.expect("bounded clock")
}
/// Typed authority fixture only. Real provider activation is covered separately
/// by planning_orientation.rs; no synthetic carrier claim is reported as live.
pub(crate) fn orientation_record(
root: &Path,
run: &str,
role: Role,
id: &str,
path: &str,
started_at: i64,
) {
let canonical_root = fs::canonicalize(root).expect("canonical fixture root");
let root = canonical_root.as_path();
let project: serde_json::Value =
serde_json::from_slice(&fs::read(root.join(".shepherd/project.json")).expect("project"))
.expect("project JSON");
let state = RunState::load(&root.join(format!(".shepherd/runs/{run}/run.json")))
.expect("fixture state");
let project_id =
ProjectId::new(project["id"].as_str().expect("project id")).expect("project id");
let root_session = SessionId::new(format!("fixture-planning-root-{run}")).expect("root");
let started_at = now().max(started_at);
let store = DispatchStore::new(root.join(".shepherd/runs"));
let service = DispatchService::with_project_root(store.clone(), project_id.clone(), root);
let mut registry =
Registry::open_migrated(root.join(".shepherd/shepherd.db")).expect("fixture registry");
registry.execute("INSERT INTO projects (id,name,created_at,updated_at) VALUES (?1,?2,?3,?3) ON CONFLICT(id) DO NOTHING", (project_id.as_str(), "typed orientation fixture", started_at)).expect("fixture project");
let parent = if role == Role::Engineer {
service
.bind_root(
BindRootDispatchRequest {
schema: "shepherd.dispatch-request/1".into(),
run: Some(run.into()),
harness: Harness::ClaudeCode,
session_id: root_session.to_string(),
role_carrier: "shepherd:shepherd".into(),
mode: shepherd::dispatch::RootMode::Planting,
lease_ms: 600_000,
},
started_at,
)
.expect("native fixture root binding");
None
} else {
Some(
AgentId::new(
registry
.load_dispatch_singleton(project_id.as_str(), run, "engineer", "__run__")
.expect("fixture singleton")
.expect("fixture Engineer first")
.agent_id,
)
.expect("parent"),
)
};
let prefix = format!(".shepherd/runs/{run}");
let result = format!("{prefix}/{path}");
let scope = if role == Role::Engineer {
vec![format!("{prefix}/phase0.md"), format!("{prefix}/plan.md")]
} else {
vec![]
};
let skill = if role == Role::Engineer {
"planning"
} else if role == Role::Discovery {
"researching"
} else {
"reviewing"
};
let launch: [u8; 32] = Sha256::digest(format!("fixture-launch-{id}")).into();
let nonce: [u8; 32] = Sha256::digest(format!("fixture-nonce-{id}")).into();
let hex = |bytes: &[u8]| {
bytes
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
};
let read_scope = fs::read(root.join(&result))
.ok()
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).ok())
.and_then(|report| report["read_scope"].as_array().cloned())
.map(|values| {
values
.iter()
.map(|value| {
PathAuthority::new(value.as_str().expect("read scope")).expect("scope")
})
.collect()
})
.unwrap_or_else(|| {
vec![PathAuthority::exact(format!("{prefix}/seed.md")).expect("seed scope")]
});
let pending = PendingDispatch {
schema: "shepherd.pending-dispatch/2".into(),
launch_id_hash: launch,
parent_process_hash: [2; 32],
project_id: project_id.clone(),
project_filesystem_id: service.project_filesystem_id().expect("filesystem"),
run: RunId::new(run).expect("run"),
run_status: RunStatus::Planted.into(),
root_session_id: root_session.clone(),
caller_role: if role == Role::Engineer {
Role::Shepherd
} else {
Role::Engineer
},
parent_dispatch_id: parent
.as_ref()
.map(|id| DispatchId::new(id.as_str()).expect("parent")),
replaces_agent_id: None,
role,
work_kind: if role == Role::Engineer {
WorkKind::Planning
} else if role == Role::Discovery {
WorkKind::Research
} else {
WorkKind::Review
},
lane: None,
baseline_commit: GitCommit::new("04".repeat(20)).expect("fixture baseline"),
read_scope,
write_scope: scope
.iter()
.map(|value| PathAuthority::new(value).expect("write scope"))
.collect(),
result_artifact: PathAuthority::exact(&result).expect("result"),
review_artifact: PathAuthority::exact(format!("{prefix}/reviews/{id}.json"))
.expect("review"),
task_path: PathAuthority::exact(format!("{prefix}/seed.md")).expect("task"),
task_sha256: [5; 32],
expected_child_session_id: SessionId::new(format!("session-{id}")).expect("child session"),
expected_attachment: CarrierAttachmentExpectation {
target: Harness::ClaudeCode,
role,
agent_id: AgentId::new(id).expect("agent"),
// Spelled exactly the way production writes it. Building this
// from `Path::display()` produced a native Windows path and every
// typed pending fixture failed the validator's no-backslash check.
installed_carrier_path: portable_absolute_path(
&root.join(format!("test-fixture-carrier-{id}.md")),
),
candidate_sha256: [9; 32],
carrier_sha256: [6; 32],
compiler_tree_sha256: [7; 32],
startup_skill: skill.into(),
skill_bundle_sha256: [8; 32],
attachment_kind: AttachmentKind::ClaudePreload,
},
expires_at: started_at + 600_000,
launch_state: PendingLaunchState::Active,
claimed_at: Some(started_at),
child_process_hash: Some([10; 32]),
activated_at: Some(started_at),
nonce_sha256: nonce,
};
pending.validate().expect("valid typed pending fixture");
store
.publish_pending(&pending)
.expect("publish typed fixture pending");
let capability_contract = role.dispatch_capability_contract().expect("fixture role");
let observed: BTreeSet<_> = capability_contract
.required
.union(&capability_contract.optional)
.cloned()
.collect();
let mut dispatch = DispatchRecord::start(DispatchStart {
project_id,
run: RunId::new(run).expect("run"),
root_session_id: root_session,
run_incarnation: state.run_incarnation,
nonce: hash(format!("fixture-record-{id}").as_bytes()),
harness: Harness::ClaudeCode,
agent_id: AgentId::new(id).expect("agent"),
agent_type: AgentType::new(format!("shepherd:{role}")).expect("type"),
role,
lane: None,
parent_agent_id: parent,
session_id: SessionId::new(format!("session-{id}")).expect("session"),
write_scope: scope,
model: Some("fixture".into()),
capability_contract,
capability_probe: CapabilityProbe::new(observed, "fixture", "fixture", None, 1)
.expect("probe"),
startup_attachment: Some(StartupAttachment {
skill: skill.into(),
bundle_digest: hex(&[8; 32]),
}),
attachment_nonce: Some(hex(&nonce)),
result_artifact: Some(result.clone()),
result_nonce: Some("cd".repeat(32)),
review_artifact: None,
review_nonce: None,
started_at,
lease_expires_at: pending.expires_at,
resumes_agent_id: None,
})
.expect("fixture dispatch");
if role != Role::Engineer {
dispatch
.stop(StopRequest {
agent_id: dispatch.agent_id.clone(),
expected_revision: dispatch.revision,
stopped_at: started_at + 1,
result_artifact: Some(result),
})
.expect("fixture stop");
}
let mut record_json = serde_json::to_string_pretty(&dispatch).expect("dispatch JSON");
record_json.push('\n');
if role == Role::Engineer {
let publication = registry
.transaction(|tx| {
tx.prepare_dispatch_singleton(&DispatchSingletonPublicationInput {
nonce: dispatch.nonce.clone(),
claim: DispatchSingletonInput {
project_id: dispatch.project_id.to_string(),
run_id: run.into(),
role: role.to_string(),
lane_id: None,
agent_id: id.into(),
harness: dispatch.harness.to_string(),
agent_type: dispatch.agent_type.to_string(),
parent_agent_id: None,
session_id: dispatch.session_id.to_string(),
write_scope: dispatch.write_scope.clone(),
claimed_at: started_at,
resumes_agent_id: None,
},
record_path: format!("{run}/dispatch/{id}.json"),
record_sha256: hash(record_json.as_bytes()),
record_json,
prepared_at: started_at,
})
})
.expect("prepare native singleton fixture");
store
.publish_singleton_prepared(&publication)
.expect("publish native singleton fixture");
registry
.transaction(|tx| tx.mark_dispatch_singleton_published(&dispatch.nonce, started_at))
.expect("commit native singleton fixture");
} else {
fs::write(
root.join(format!(".shepherd/runs/{run}/dispatch/{id}.json")),
record_json,
)
.expect("fixture dispatch file");
}
}
fn report(root: &Path, run: &str, role: &str, id: &str) -> serde_json::Value {
let path = format!(".shepherd/runs/{run}/seed.md");
let evidence = serde_json::json!({"path":path,"sha256":hash(&fs::read(root.join(&path)).expect("seed evidence")),"line":1});
serde_json::json!({"schema":"shepherd.orientation-report/1","run":run,"result_id":id,"kind":role,"role":role,
"read_scope":[path],"status":"complete","summary":format!("fixture {role}"),
"assumptions":[{"id":format!("{id}-assumption"),"statement":"bounded fixture"}],
"claims":[{"id":format!("{id}-claim"),"statement":"fixture observed","evidence":[evidence]}],
"evidence":[evidence],"caveats":[]})
}
/// Include exact reviewed planning bytes in the deterministic Critic fixture.
pub(crate) fn add_planning_evidence(root: &Path, run: &str, report: &mut serde_json::Value) {
let prefix = format!(".shepherd/runs/{run}");
let topology: serde_json::Value = serde_json::from_slice(
&fs::read(root.join(format!("{prefix}/graph/topology.json"))).expect("topology"),
)
.expect("topology JSON");
let mut paths = vec![
"seed.md".to_owned(),
"mesh.md".to_owned(),
"plan.md".to_owned(),
"phase0.md".to_owned(),
"plan-probes.json".to_owned(),
"graph/topology.json".to_owned(),
];
for lane in topology["lanes"].as_array().expect("topology lanes") {
paths.push(format!(
"lanes/{}/plan.md",
lane["id"].as_str().expect("lane")
));
}
for relative in paths {
let path = format!("{prefix}/{relative}");
let evidence = serde_json::json!({
"path": path, "sha256": hash(&fs::read(root.join(&path)).expect("reviewed input")), "line": 1
});
let scope = report["read_scope"].as_array_mut().expect("read scope");
if !scope.iter().any(|entry| entry.as_str() == Some(&path)) {
scope.push(path.clone().into());
}
let entries = report["evidence"].as_array_mut().expect("evidence");
if !entries.iter().any(|entry| entry["path"] == path) {
entries.push(evidence);
}
}
report["read_scope"]
.as_array_mut()
.expect("read scope")
.sort_by(|left, right| left.as_str().cmp(&right.as_str()));
}
/// Initialize a real native planning checkpoint for an isolated test repository.
/// Call before installing a root binding; returns the committed fixture baseline.
pub(crate) fn prepare_execution(root: &Path, run: &str, lanes: &[&str], ceiling: usize) -> String {
assert!(
root.join(".git").exists(),
"initialize the isolated fixture repository first"
);
accepted(root, &["init", "--confirm"]);
accepted(root, &["run", "init", run]);
if lanes.len() == 1 {
seed(root, run);
} else {
seed_for_lanes(root, run, lanes.len());
}
let directory = root.join(format!(".shepherd/runs/{run}"));
fs::create_dir_all(directory.join("reports")).expect("fixture reports");
let seed_path = format!(".shepherd/runs/{run}/seed.md");
let seed_hash = hash(&fs::read(root.join(&seed_path)).expect("fixture seed"));
let phase0 = format!(
"# Orientation\n\n## Run and seed\n- run: {run}\n- verified seed path: {seed_path}\n- seed hash: {seed_hash}\n- planted observation: status=planted\n\n## Auditor briefs\n### fixture-auditor\n- exact read scope: {seed_path}\n- output path: reports/fixture-auditor.json\n\n## Discovery briefs\n### fixture-discovery\n- exact read scope: {seed_path}\n- output path: reports/fixture-discovery.json\n\n## Assumptions and decisions\n- Fixture inputs only.\n\n## Coverage map\n- Native validator is covered.\n\n## Self-review\n- Native checks decide acceptance.\n\n## Critic loop\n- Wait for native pre before Critic.\n"
);
fs::write(directory.join("phase0.md"), phase0).expect("fixture phase0");
orientation_record(
root,
run,
Role::Engineer,
"fixture-engineer",
"phase0.md",
1,
);
for (role, id) in [
(Role::Auditor, "fixture-auditor"),
(Role::Discovery, "fixture-discovery"),
] {
let path = format!("reports/{id}.json");
fs::write(
directory.join(&path),
serde_json::to_vec(&report(root, run, role.as_str(), id)).expect("report JSON"),
)
.expect("report");
orientation_record(root, run, role, id, &path, 1);
}
accepted(root, &["run", "orientation", "pre", run]);
let pre_bytes = fs::read(directory.join("orientation-pre.json")).expect("pre");
let pre: serde_json::Value = serde_json::from_slice(&pre_bytes).expect("pre JSON");
let baseline = materialize(root, run, lanes, ceiling);
let mut critic = report(root, run, "critic", "fixture-critic");
add_planning_evidence(root, run, &mut critic);
critic["orientation_pre_sha256"] = hash(&pre_bytes).into();
critic["verdict"] = serde_json::json!({"decision":"GREEN","findings":[],"corrections":[],"blockers":[],"citations":[{"claim_id":"fixture-auditor-claim"}]});
fs::write(
directory.join("reports/fixture-critic.json"),
serde_json::to_vec(&critic).expect("critic JSON"),
)
.expect("critic");
orientation_record(
root,
run,
Role::Critic,
"fixture-critic",
"reports/fixture-critic.json",
pre["created_at"].as_i64().expect("pre time") + 1,
);
accepted(root, &["run", "transition", run, "--to", "planned"]);
baseline
}
/// Open the exact native plan prepared above, without hand-written lane state.
pub(crate) fn open_execution(root: &Path, run: &str, lanes: &[&str], ceiling: usize) -> String {
let baseline = prepare_execution(root, run, lanes, ceiling);
accepted(root, &["sprint", "open", "--run", run]);
retire_engineer(root, run);
baseline
}
fn retire_engineer(root: &Path, run: &str) {
let canonical_root = fs::canonicalize(root).expect("canonical fixture root");
let root = canonical_root.as_path();
let store = DispatchStore::new(root.join(".shepherd/runs"));
let run = RunId::new(run).expect("run");
let record = store
.load_for_run(&run, &AgentId::new("fixture-engineer").expect("agent"))
.expect("active fixture Engineer");
let now = now();
store
.stop_verified_for_run(
&NativeIdentity {
harness: record.harness,
project_id: record.project_id.clone(),
run,
lane: None,
session_id: record.session_id.clone(),
agent_id: Some(record.agent_id.clone()),
agent_type: Some(record.agent_type.clone()),
role: Some(record.role),
tool_call_id: None,
now,
root_binding: None,
},
StopRequest {
agent_id: record.agent_id.clone(),
expected_revision: record.revision,
stopped_at: now,
result_artifact: record.result_artifact.clone(),
},
)
.expect("retire fixture lead through native transition");
}