use std::fs;
use std::path::Path;
use serde_json::json;
use crate::adapters::{
adapter_for, config_dir_from_env, detect_plugin_shadows, format_shadow_banner,
};
use crate::core::{AvailableSkill, ConditionEntry, ConditionsRecord, Harness, RunContext};
use crate::pipeline::io::now_iso8601;
use super::super::dispatch::{
DispatchTaskOpts, ManifestContext, build_dispatch_task, build_manifest, copy_fixtures,
get_skill_description,
};
use super::super::staging::skills_dir_for_harness;
use super::super::util::{staging_plugin_shadow_action, unguarded_notice};
use super::super::{RunError, write_json};
use super::{Resolved, RunOptions, Staged};
pub(super) fn write_dispatch(
ctx: &RunContext,
opts: &RunOptions,
r: &Resolved,
staged: &Staged,
) -> Result<usize, RunError> {
let conditions = ConditionsRecord {
mode: r.mode,
baseline: r.baseline.clone(),
conditions: vec![
ConditionEntry {
name: r.cond_a.to_string(),
skill_path: r.skill_path_a.clone(),
staged_skill_slug: Some(staged.cond_a_slug.clone()),
},
ConditionEntry {
name: r.cond_b.to_string(),
skill_path: r.skill_path_b.clone(),
staged_skill_slug: Some(staged.cond_b_slug.clone()),
},
],
timestamp: now_iso8601(),
harness: Some(ctx.harness),
run_nonce: Some(r.run_nonce.clone()),
runs: Some(opts.runs),
agent_model: opts.agent_model.map(str::to_owned),
judge_model: opts.judge_model.map(str::to_owned),
label: opts.label.map(str::to_owned),
};
write_json(&r.iteration_dir.join("conditions.json"), &conditions)?;
let staged_skill_path_for = |cond_slug: Option<&str>| -> Option<String> {
cond_slug.map(|slug| {
skills_dir_for_harness(&ctx.stage_root, ctx.harness)
.join(slug)
.join("SKILL.md")
.to_string_lossy()
.into_owned()
})
};
let available_skills_for =
|cond_skill_path: Option<&str>, cond_slug: Option<&str>| -> Vec<AvailableSkill> {
if opts.no_stage {
return Vec::new();
}
let mut skills = staged.sibling_skills.clone();
if let Some(csp) = cond_skill_path {
let name = match cond_slug {
Some(slug) if adapter_for(ctx.harness).advertises_staged_slug_name() => {
slug.to_string()
}
_ => ctx.skill_name.clone(),
};
skills.push(AvailableSkill {
name,
path: staged_skill_path_for(cond_slug).unwrap_or_else(|| csp.to_string()),
description: get_skill_description(Path::new(csp)),
});
}
skills
};
let mut tasks = Vec::new();
for ev in &r.selected_evals {
let eval_dir = r.iteration_dir.join(format!("eval-{}", ev.id));
fs::create_dir_all(&eval_dir)?;
for (cond_name, cond_skill_path, cond_slug) in [
(
r.cond_a,
r.skill_path_a.as_deref(),
staged.cond_a_slug.as_deref(),
),
(
r.cond_b,
r.skill_path_b.as_deref(),
staged.cond_b_slug.as_deref(),
),
] {
let cond_dir = eval_dir.join(cond_name);
let runs = ev.runs.unwrap_or(opts.runs);
let staged_path = staged_skill_path_for(cond_slug);
for run_idx in 1..=runs {
let (run_dir, run_index) = if runs == 1 {
(cond_dir.clone(), None)
} else {
(cond_dir.join(format!("run-{run_idx}")), Some(run_idx))
};
let outputs_dir = run_dir.join("outputs");
fs::create_dir_all(&outputs_dir)?;
let fixtures = copy_fixtures(ev, &ctx.skill_subdir, &run_dir)?;
let available_skills = available_skills_for(cond_skill_path, cond_slug);
let outputs_dir_str = outputs_dir.to_string_lossy().into_owned();
let run_dir_str = run_dir.to_string_lossy().into_owned();
tasks.push(build_dispatch_task(&DispatchTaskOpts {
eval_id: &ev.id,
condition: cond_name,
skill_path: cond_skill_path,
staged_skill_slug: cond_slug,
staged_skill_path: staged_path.as_deref(),
user_prompt: &ev.prompt,
fixtures,
outputs_dir: &outputs_dir_str,
cond_dir: &run_dir_str,
bootstrap_content: staged.bootstrap_content.as_deref(),
plan_mode_content: staged.plan_mode_content.as_deref(),
skill_name: &ctx.skill_name,
available_skills,
harness: ctx.harness,
run_tag: Some(&r.run_tag),
run_index,
})?);
}
}
}
let manifest_path = r.iteration_dir.join("dispatch-manifest.md");
fs::write(
&manifest_path,
build_manifest(
&ctx.skill_name,
r.mode,
r.baseline.as_deref(),
r.iteration,
&now_iso8601(),
&tasks,
ManifestContext {
harness: ctx.harness,
guard: opts.guard,
agent_model: opts.agent_model,
},
),
)?;
for task in &tasks {
fs::write(&task.dispatch_prompt_path, &task.dispatch_prompt)?;
}
let dispatch_json_path = r.iteration_dir.join("dispatch.json");
let dispatch_json = json!({
"skill_name": ctx.skill_name,
"iteration": r.iteration,
"run_nonce": r.run_nonce,
"iteration_dir": r.iteration_dir.to_string_lossy(),
"mode": r.mode,
"baseline": r.baseline,
"plan_mode": opts.plan_mode,
"runs": opts.runs,
"agent_model": conditions.agent_model,
"judge_model": conditions.judge_model,
"label": conditions.label,
"conditions": conditions.conditions,
"harness": ctx.harness,
"tasks": tasks,
});
write_json(&dispatch_json_path, &dispatch_json)?;
Ok(tasks.len())
}
pub(super) fn post_build(
ctx: &RunContext,
opts: &RunOptions,
r: &Resolved,
staged: &Staged,
) -> Result<(), RunError> {
if opts.guard && !opts.dry_run {
if opts.no_stage {
eprintln!("\n⚠ --guard requires staging enabled; skipping guard install.");
} else {
adapter_for(ctx.harness).install_guard(
&ctx.stage_root,
&ctx.workspace_root,
&std::env::current_exe()?,
None,
)?;
match ctx.harness {
Harness::ClaudeCode => println!(
"\n🛡 Write guard armed: a PreToolUse hook is staged in .claude/settings.local.json\n and will block writes/installs outside the eval sandbox during dispatches.\n It auto-expires in 6h and is removed on the next run; to remove it now:\n eval-magic teardown-guard"
),
Harness::Codex => println!(
"\n🛡 Write guard armed: a PreToolUse hook is staged in .codex/hooks.json\n and will block writes/installs outside the eval sandbox during Codex dispatches.\n Dispatch with codex exec --dangerously-bypass-hook-trust so the vetted eval hook runs.\n It auto-expires in 6h and is removed on the next run; to remove it now:\n eval-magic teardown-guard"
),
Harness::OpenCode => unreachable!(
"install_guard_for_harness rejects OpenCode before this message prints"
),
}
}
}
if !opts.dry_run
&& let Some(notice) = unguarded_notice(opts.no_stage)
{
eprintln!("{notice}");
}
if ctx.harness == Harness::ClaudeCode {
let mut names: Vec<&str> = vec![ctx.skill_name.as_str()];
names.extend(ctx.sibling_skill_names.iter().map(String::as_str));
let report = detect_plugin_shadows(&config_dir_from_env(), &ctx.stage_root, &names);
if !report.shadowed.is_empty() {
write_json(&r.iteration_dir.join("plugin-shadow.json"), &report)?;
eprintln!("{}", format_shadow_banner(&report));
}
if let Some(action) = staging_plugin_shadow_action(
ctx.harness,
opts.no_stage,
!report.shadowed.is_empty(),
staged.skills_dir_preexisted,
) {
eprintln!("{action}");
}
}
Ok(())
}