Skip to main content

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}