use std::collections::HashMap;
use std::fs;
use std::path::Path;
use serde_json::{Value, json};
use crate::adapters::{adapter_for, format_shadow_banner};
use crate::core::{AvailableSkill, ConditionEntry, ConditionsRecord, RunContext};
use crate::pipeline::io::now_iso8601;
use super::super::dispatch::{
DispatchTaskOpts, ManifestContext, build_dispatch_task, build_manifest, get_skill_description,
};
use super::super::fixtures::fixture_pairs;
use super::super::runbook::{RunbookContext, build_runbook};
use super::super::staging::skills_dir_for_harness;
use super::super::util::unguarded_notice;
use super::super::{RunError, write_json};
use super::envs::{EnvLayoutInput, env_targets, task_env_root};
use super::{Resolved, RunOptions, Staged};
use crate::cli::command_target_args;
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 = |env_root: &Path, cond_slug: Option<&str>| -> Option<String> {
cond_slug.map(|slug| {
skills_dir_for_harness(env_root, ctx.harness)
.join(slug)
.join("SKILL.md")
.to_string_lossy()
.into_owned()
})
};
let available_skills_for = |env_root: &Path,
cond_skill_path: Option<&str>,
cond_slug: Option<&str>|
-> Vec<AvailableSkill> {
if opts.no_stage {
return Vec::new();
}
let mut skills: Vec<AvailableSkill> = staged
.sibling_meta
.iter()
.map(|(name, description)| AvailableSkill {
name: name.clone(),
path: skills_dir_for_harness(env_root, ctx.harness)
.join(name)
.join("SKILL.md")
.to_string_lossy()
.into_owned(),
description: description.clone(),
})
.collect();
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(env_root, cond_slug).unwrap_or_else(|| csp.to_string()),
description: get_skill_description(Path::new(csp)),
});
}
skills
};
let mut fixtures_by_eval: HashMap<&str, Vec<String>> = HashMap::new();
for ev in &r.selected_evals {
let dests = fixture_pairs(ev, &ctx.skill_subdir)?
.into_iter()
.map(|(dest, _source)| dest)
.collect();
fixtures_by_eval.insert(ev.id.as_str(), dests);
}
let multi_group = r.groups.len() > 1;
let mut tasks = Vec::new();
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(),
),
] {
for group in &r.groups {
let env_root = task_env_root(&r.iteration_dir, &group.id, cond_name);
let env_root_str = env_root.to_string_lossy().into_owned();
let staged_path = staged_skill_path_for(&env_root, cond_slug);
let available_skills = available_skills_for(&env_root, cond_skill_path, cond_slug);
for eval_id in &group.eval_ids {
let ev = r
.selected_evals
.iter()
.find(|e| &e.id == eval_id)
.expect("group eval ids are drawn from selected_evals");
let cond_dir = r
.iteration_dir
.join(format!("eval-{}", ev.id))
.join(cond_name);
let runs = ev.runs.unwrap_or(opts.runs);
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))
};
fs::create_dir_all(&run_dir)?;
let outputs_rel = match run_index {
None => format!("eval-{}/{cond_name}", ev.id),
Some(k) => format!("eval-{}/{cond_name}/run-{k}", ev.id),
};
let outputs_dir = env_root.join(".eval-magic-outputs").join(outputs_rel);
fs::create_dir_all(&outputs_dir)?;
let fixtures = fixtures_by_eval
.get(ev.id.as_str())
.cloned()
.unwrap_or_default();
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: available_skills.clone(),
harness: ctx.harness,
run_tag: Some(&r.run_tag),
run_index,
group: multi_group.then_some(group.id.as_str()),
eval_root: Some(env_root_str.as_str()),
})?);
}
}
}
}
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_armed(),
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 mut 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,
});
let groups: Vec<Value> = r
.groups
.iter()
.map(|g| {
let envs: Vec<Value> = [r.cond_a, r.cond_b]
.iter()
.map(|cond| {
json!({
"condition": cond,
"dir": task_env_root(&r.iteration_dir, &g.id, cond).to_string_lossy(),
})
})
.collect();
json!({
"id": g.id,
"evals": g.eval_ids,
"rationale": g.rationale,
"envs": envs,
})
})
.collect();
dispatch_json
.as_object_mut()
.expect("dispatch_json is a JSON object")
.insert("groups".to_string(), Value::Array(groups));
write_json(&dispatch_json_path, &dispatch_json)?;
let target_args = command_target_args(ctx);
let runbook = build_runbook(&RunbookContext {
harness: ctx.harness,
skill_name: &ctx.skill_name,
iteration: r.iteration,
iteration_dir: &r.iteration_dir,
mode: r.mode,
cond_a: r.cond_a,
cond_b: r.cond_b,
num_tasks: tasks.len(),
target_args: &target_args,
guard: opts.guard_armed(),
agent_model: opts.agent_model,
});
fs::write(r.iteration_dir.join("RUNBOOK.md"), runbook)?;
Ok(tasks.len())
}
pub(super) fn post_build(
ctx: &RunContext,
opts: &RunOptions,
r: &Resolved,
) -> Result<(), RunError> {
let targets = env_targets(&EnvLayoutInput {
iteration_dir: &r.iteration_dir,
groups: &r.groups,
cond_a: r.cond_a,
cond_b: r.cond_b,
skill_path_a: r.skill_path_a.as_deref(),
skill_path_b: r.skill_path_b.as_deref(),
});
if opts.guard_armed() && !opts.dry_run {
let adapter = adapter_for(ctx.harness);
let exe = std::env::current_exe()?;
for target in &targets {
adapter.install_guard(&target.root, &exe, None)?;
}
if let Some(msg) = adapter.guard_armed_message() {
println!("{msg}");
}
}
if !opts.dry_run
&& let Some(notice) = unguarded_notice(opts.no_stage)
{
eprintln!("{notice}");
}
let mut names: Vec<&str> = vec![ctx.skill_name.as_str()];
names.extend(ctx.sibling_skill_names.iter().map(String::as_str));
let scan_root = targets
.first()
.map(|t| t.root.as_path())
.unwrap_or(ctx.stage_root.as_path());
if let Some(report) = adapter_for(ctx.harness).detect_shadowed_skills(scan_root, &names) {
write_json(&r.iteration_dir.join("plugin-shadow.json"), &report)?;
eprintln!("{}", format_shadow_banner(&report));
}
Ok(())
}