eval_magic/core/run_mode.rs
1//! Run mode — *how* an eval is dispatched, independent of *which* harness runs
2//! it.
3//!
4//! There are two dispatch **mechanisms** in the code today:
5//!
6//! - [`DispatchMechanism::InSession`] — the runner hands tasks to in-session
7//! subagents (Claude Code's Task tool). The reference is Claude Code.
8//! - [`DispatchMechanism::Cli`] — each task is dispatched through a one-shot
9//! harness CLI subprocess (`codex exec`). The reference is Codex.
10//!
11//! These two mechanisms underpin the three *user-facing* run modes documented in
12//! the README: **fully-interactive** rides on [`InSession`](DispatchMechanism::InSession);
13//! **headless** and **hybrid** both ride on [`Cli`](DispatchMechanism::Cli),
14//! differing only in whether a human/agent session drives the loop — not in how
15//! a single task reaches the harness.
16//!
17//! This is distinct from the comparison [`Mode`](crate::core::Mode)
18//! (`new-skill` / `revision`), which selects the two conditions being compared,
19//! not the dispatch path.
20
21use crate::core::Harness;
22
23/// How a single dispatch is delivered to a harness. The primary code axis for
24/// run-mode concerns (next-steps guidance, transcript source).
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum DispatchMechanism {
27 /// In-session subagent dispatch (Claude Code's Task tool).
28 InSession,
29 /// One-shot harness CLI subprocess dispatch (`codex exec`).
30 Cli,
31}
32
33/// Run-option support for a harness's currently wired dispatch mechanism.
34///
35/// This is intentionally narrower than full harness parity: it only describes
36/// options the generic `run` preflight must accept or reject before the build
37/// sequence starts. Harness-specific behavior still lives behind the adapter.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct HarnessRunCapabilities {
40 pub mechanism: DispatchMechanism,
41 pub supports_guard: bool,
42 pub supports_bootstrap_with_no_stage: bool,
43 pub supports_stage_name_with_no_stage: bool,
44}
45
46/// The focused capability table for generic `run` option validation.
47pub fn capabilities_for(harness: Harness) -> HarnessRunCapabilities {
48 match harness {
49 Harness::ClaudeCode => HarnessRunCapabilities {
50 mechanism: DispatchMechanism::InSession,
51 supports_guard: true,
52 supports_bootstrap_with_no_stage: true,
53 supports_stage_name_with_no_stage: true,
54 },
55 Harness::Codex => HarnessRunCapabilities {
56 mechanism: DispatchMechanism::Cli,
57 supports_guard: true,
58 supports_bootstrap_with_no_stage: false,
59 supports_stage_name_with_no_stage: false,
60 },
61 Harness::OpenCode => HarnessRunCapabilities {
62 mechanism: DispatchMechanism::Cli,
63 supports_guard: false,
64 supports_bootstrap_with_no_stage: true,
65 supports_stage_name_with_no_stage: true,
66 },
67 }
68}
69
70/// The dispatch mechanism a harness uses today. This is the single, documented
71/// place where the current 1:1 harness↔mechanism coupling lives — when a harness
72/// gains a second mechanism (e.g. a true headless Claude Code mode), the choice
73/// stops being derivable from the harness alone and this is the seam that grows
74/// to take an explicit selection.
75pub fn mechanism_for(harness: Harness) -> DispatchMechanism {
76 capabilities_for(harness).mechanism
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn maps_each_harness_to_its_mechanism_today() {
85 assert_eq!(
86 mechanism_for(Harness::ClaudeCode),
87 DispatchMechanism::InSession
88 );
89 assert_eq!(mechanism_for(Harness::Codex), DispatchMechanism::Cli);
90 assert_eq!(mechanism_for(Harness::OpenCode), DispatchMechanism::Cli);
91 }
92
93 #[test]
94 fn capabilities_capture_run_option_support_by_harness() {
95 let claude = capabilities_for(Harness::ClaudeCode);
96 assert_eq!(claude.mechanism, DispatchMechanism::InSession);
97 assert!(claude.supports_guard);
98 assert!(claude.supports_bootstrap_with_no_stage);
99 assert!(claude.supports_stage_name_with_no_stage);
100
101 let codex = capabilities_for(Harness::Codex);
102 assert_eq!(codex.mechanism, DispatchMechanism::Cli);
103 assert!(codex.supports_guard);
104 assert!(!codex.supports_bootstrap_with_no_stage);
105 assert!(!codex.supports_stage_name_with_no_stage);
106
107 let opencode = capabilities_for(Harness::OpenCode);
108 assert_eq!(opencode.mechanism, DispatchMechanism::Cli);
109 assert!(!opencode.supports_guard);
110 assert!(opencode.supports_bootstrap_with_no_stage);
111 assert!(opencode.supports_stage_name_with_no_stage);
112 }
113}