use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::adapters::adapter_for;
use crate::core::{Harness, Mode, RunContext, capabilities_for};
use super::RunError;
use super::orchestrate::RunOptions;
pub(crate) fn condition_names_for(mode: Mode) -> (&'static str, &'static str) {
match mode {
Mode::NewSkill => ("with_skill", "without_skill"),
Mode::Revision => ("old_skill", "new_skill"),
}
}
pub(crate) fn next_iteration(workspace_skill_dir: &Path, override_n: Option<u32>) -> u32 {
if let Some(n) = override_n {
return n;
}
let Ok(entries) = fs::read_dir(workspace_skill_dir) else {
return 1;
};
let max = entries
.flatten()
.filter_map(|e| {
e.file_name()
.to_string_lossy()
.strip_prefix("iteration-")
.and_then(|s| s.parse::<u32>().ok())
})
.max();
max.map_or(1, |m| m + 1)
}
pub(crate) fn unguarded_notice(no_stage: bool) -> Option<String> {
if !no_stage {
return None;
}
Some(
"\nℹ --no-stage run is unguarded — the write guard requires staging, so stray writes are \
only detected after the fact by detect-stray-writes (folded into `ingest`), never blocked."
.to_string(),
)
}
fn insession_dispatch_instruction(filter: &str) -> String {
format!(
"iterate the `tasks[]` entries in dispatch.json whose {filter} and \
dispatch each as a subagent, passing its `agent_description` verbatim as the subagent \
description (that string is the key that links each transcript back — without it tool \
calls, tokens, and duration come back empty)."
)
}
pub(crate) fn insession_dispatch_batch(condition: &str) -> String {
insession_dispatch_instruction(&format!("`condition` is `{condition}`"))
}
pub(crate) fn insession_dispatch_segment(condition: &str, group: &str) -> String {
insession_dispatch_instruction(&format!(
"`condition` is `{condition}` and `group` is `{group}`"
))
}
pub(crate) fn insession_reset_batch_command(
target_args: &str,
iteration: u32,
group: &str,
) -> String {
format!("eval-magic reset-batch{target_args} --iteration {iteration} --group {group}")
}
pub(crate) fn insession_switch_command(target_args: &str, iteration: u32, keep: &str) -> String {
format!("eval-magic switch-condition{target_args} --iteration {iteration} --condition {keep}")
}
pub(crate) fn insession_ingest_command(target_args: &str, iteration: u32) -> String {
format!(
"eval-magic ingest{target_args} --iteration {iteration}\n\
(ingest auto-resolves the subagents dir from CLAUDE_CODE_SESSION_ID; outside that \
session, add --session-id <id> or --subagents-dir <path>.)"
)
}
pub(crate) fn insession_isolated_handoff(env_dir: &Path) -> String {
format!(
"start the isolated run in a fresh session:\n \
1. cd {env}\n \
2. start a fresh Claude Code session there (`claude`)\n \
3. say: Read and follow RUNBOOK.md\n\
RUNBOOK.md walks the whole loop (dispatch → switch-condition → ingest → finalize) and \
writes benchmark.json; resume here to read it.",
env = env_dir.display()
)
}
pub(crate) fn resolve_plan_mode_profile(harness: Harness) -> Result<&'static str, RunError> {
Ok(adapter_for(harness).plan_mode_profile())
}
pub(crate) fn validate_harness_run_options(
opts: &RunOptions,
ctx: &RunContext,
) -> Result<(), RunError> {
let capabilities = capabilities_for(ctx.harness);
let mut unsupported: Vec<&str> = Vec::new();
if opts.guard && !capabilities.supports_guard {
unsupported.push("--guard");
}
if ctx.bootstrap_path.is_some()
&& opts.no_stage
&& !capabilities.supports_bootstrap_with_no_stage
{
unsupported.push("--bootstrap with --no-stage");
}
if opts.stage_name.is_some() && opts.no_stage && !capabilities.supports_stage_name_with_no_stage
{
unsupported.push("--stage-name with --no-stage");
}
if unsupported.is_empty() {
Ok(())
} else {
Err(RunError::msg(format!(
"Unsupported for --harness {}: {}.",
harness_label(ctx.harness),
unsupported.join(", ")
)))
}
}
pub(crate) fn make_run_nonce() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format!(
"{}-{:06x}",
to_base36(now.as_millis() as u64),
now.subsec_nanos() & 0x00ff_ffff
)
}
fn to_base36(mut n: u64) -> String {
const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
if n == 0 {
return "0".to_string();
}
let mut out = Vec::new();
while n > 0 {
out.push(DIGITS[(n % 36) as usize]);
n /= 36;
}
out.reverse();
String::from_utf8(out).unwrap()
}
pub(crate) fn mode_str(mode: Mode) -> &'static str {
match mode {
Mode::NewSkill => "new-skill",
Mode::Revision => "revision",
}
}
pub(crate) fn harness_label(harness: Harness) -> &'static str {
adapter_for(harness).label()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{DetectInput, RunMode, detect_run_context};
use std::fs;
fn ctx_for(harness: Harness, run_mode: RunMode) -> (tempfile::TempDir, RunContext) {
let tmp = tempfile::TempDir::new().unwrap();
let skill = tmp.path().join("widget");
fs::create_dir_all(&skill).unwrap();
fs::write(
skill.join("SKILL.md"),
"---\nname: widget\ndescription: t\n---\n\nbody\n",
)
.unwrap();
let ctx = detect_run_context(DetectInput {
skill: Some(skill.display().to_string()),
harness: Some(harness),
run_mode: Some(run_mode),
cwd: Some(tmp.path().to_path_buf()),
..Default::default()
})
.unwrap();
(tmp, ctx)
}
#[test]
fn claude_hybrid_allows_guard() {
let (_t, ctx) = ctx_for(Harness::ClaudeCode, RunMode::Hybrid);
let opts = RunOptions {
guard: true,
..Default::default()
};
assert!(validate_harness_run_options(&opts, &ctx).is_ok());
}
#[test]
fn claude_headless_allows_guard() {
let (_t, ctx) = ctx_for(Harness::ClaudeCode, RunMode::Headless);
let opts = RunOptions {
guard: true,
..Default::default()
};
assert!(validate_harness_run_options(&opts, &ctx).is_ok());
}
#[test]
fn claude_interactive_allows_guard() {
let (_t, ctx) = ctx_for(Harness::ClaudeCode, RunMode::Interactive);
let opts = RunOptions {
guard: true,
..Default::default()
};
assert!(validate_harness_run_options(&opts, &ctx).is_ok());
}
#[test]
fn unguarded_notice_when_no_stage() {
let notice = unguarded_notice(true).unwrap();
assert!(
notice.to_lowercase().contains("unguarded"),
"calls the run unguarded: {notice}"
);
assert!(
notice.contains("detect-stray-writes"),
"names the after-the-fact backstop: {notice}"
);
}
#[test]
fn no_unguarded_notice_when_staging() {
assert!(unguarded_notice(false).is_none());
}
#[test]
fn isolated_handoff_points_into_env_and_at_the_runbook() {
let env = Path::new("/work/.eval-magic/widget/iteration-3/env");
let handoff = insession_isolated_handoff(env);
assert!(
handoff.contains("/work/.eval-magic/widget/iteration-3/env"),
"names the env to cd into: {handoff}"
);
assert!(handoff.contains("cd "), "spells out the cd step: {handoff}");
assert!(
handoff.contains("Read and follow RUNBOOK.md"),
"hands off to the runbook in a fresh session: {handoff}"
);
assert!(
handoff.contains("fresh"),
"names the fresh isolated session: {handoff}"
);
assert!(
!handoff.contains("one batch at a time"),
"the dispatch loop lives in RUNBOOK.md now, not the summary: {handoff}"
);
}
#[test]
fn opencode_plan_mode_profile_resolves() {
let profile = resolve_plan_mode_profile(Harness::OpenCode).unwrap();
assert!(profile.contains("OpenCode plan mode is active"));
assert!(profile.contains("plan agent"));
}
#[test]
fn harness_label_opencode() {
assert_eq!(harness_label(Harness::OpenCode), "opencode");
}
#[test]
fn codex_plan_mode_profile_resolves() {
let profile = resolve_plan_mode_profile(Harness::Codex).unwrap();
assert!(profile.contains("Codex plan mode is active"));
assert!(profile.contains("<proposed_plan>"));
assert!(!profile.contains("ExitPlanMode"));
}
#[test]
fn base36_roundtrips_small_values() {
assert_eq!(to_base36(0), "0");
assert_eq!(to_base36(35), "z");
assert_eq!(to_base36(36), "10");
}
#[test]
fn next_iteration_uses_override_then_scans() {
let tmp = tempfile::TempDir::new().unwrap();
assert_eq!(next_iteration(tmp.path(), Some(7)), 7);
assert_eq!(next_iteration(&tmp.path().join("nope"), None), 1);
fs::create_dir_all(tmp.path().join("iteration-1")).unwrap();
fs::create_dir_all(tmp.path().join("iteration-4")).unwrap();
fs::create_dir_all(tmp.path().join("not-an-iteration")).unwrap();
assert_eq!(next_iteration(tmp.path(), None), 5);
}
}