Skip to main content

oneharness_core/domain/
report.rs

1//! The serializable shapes that make up oneharness's output contract.
2//!
3//! The JSON report carries a `schema_version`: consumers depend on it, so fields
4//! are added, never repurposed or removed, without bumping the version.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::domain::batch::BatchStrategy;
10use crate::domain::events::ActionEvent;
11use crate::domain::mode::PermissionMode;
12use crate::domain::signals::Usage;
13
14/// Bumped when the JSON shape changes in a way consumers must notice.
15pub const SCHEMA_VERSION: &str = "0.1";
16
17/// How a harness emits its result, which decides how `text` is extracted.
18///
19/// Also accepted as a CLI value (`--output-format`, parsed in the `oneharness`
20/// binary) and a config-file value (`output_format`, via `Deserialize`). The
21/// CLI parsing lives in the binary so this core crate stays free of `clap`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "kebab-case")]
24pub enum OutputFormat {
25    /// Plain text on stdout; `text` is the trimmed stdout.
26    Text,
27    /// A single JSON document on stdout.
28    Json,
29    /// Line-delimited JSON events on stdout.
30    StreamJson,
31}
32
33/// The outcome of attempting to run one harness.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "kebab-case")]
36pub enum Status {
37    /// Spawned and exited 0.
38    Ok,
39    /// Spawned and exited non-zero.
40    Nonzero,
41    /// Killed after exceeding the per-harness timeout.
42    Timeout,
43    /// The binary was resolved but could not be executed.
44    SpawnError,
45    /// The binary was not found, so the harness was not run.
46    Skipped,
47    /// `--print-command`: the command was built but not executed.
48    Planned,
49}
50
51/// The raw capture from running a subprocess — produced by the io layer,
52/// consumed (with extraction) by the command layer. Carries no extraction so
53/// the spawn path and the parse path stay independently testable.
54#[derive(Debug, Clone)]
55pub struct Capture {
56    pub status: Status,
57    pub exit_code: Option<i32>,
58    pub duration_ms: Option<u128>,
59    pub stdout: String,
60    pub stderr: String,
61    pub error: Option<String>,
62}
63
64/// One harness's entry in the report.
65#[derive(Debug, Clone, Serialize)]
66pub struct RunResult {
67    /// Canonical harness id (e.g. `claude-code`).
68    pub harness: String,
69    /// The binary name or path oneharness resolved and would invoke.
70    pub bin: String,
71    /// Whether that binary was found.
72    pub available: bool,
73    pub status: Status,
74    /// The prompt this result ran, set only on a **batch** run (one harness
75    /// fanned over N prompts), where each result has its own prompt. `null` on an
76    /// ordinary run, where the single top-level `prompt` applies to every result.
77    pub prompt: Option<String>,
78    /// Process exit code; `null` when not run, timed out, or signalled.
79    pub exit_code: Option<i32>,
80    /// Wall-clock duration of the run; `null` when not executed.
81    pub duration_ms: Option<u128>,
82    /// The exact argv oneharness built (argv[0] is the binary).
83    pub command: Vec<String>,
84    pub output_format: OutputFormat,
85    /// Best-effort final assistant text; `null` when extraction is impossible.
86    pub text: Option<String>,
87    /// How `text` was extracted (e.g. `json:result`, `raw`); `null` when absent.
88    pub text_source: Option<String>,
89    /// Best-effort token/cost accounting; every field is `null` when the harness
90    /// does not report it. Always present so consumers can read a stable shape.
91    pub usage: Usage,
92    /// How `usage` was read (e.g. `json`); `null` when nothing was found.
93    pub usage_source: Option<String>,
94    /// Best-effort harness session id for continuation; `null` when none is
95    /// exposed. (Surfaced only; oneharness does not yet consume it.)
96    pub session_id: Option<String>,
97    /// Best-effort normalized tool-call / action events the harness took (shell
98    /// commands, file edits, tool uses), in order — so consumers can assert on
99    /// *behavior*, not just the final `text`. `null` when the harness's output
100    /// exposes no machine-readable trace (a plain-text harness, or Claude Code's
101    /// single-document `json` result), distinct from `[]` — an empty array is not
102    /// currently emitted; absence is signalled by `null` + a null `events_source`.
103    /// Never fabricated. See [`crate::domain::events`].
104    pub events: Option<Vec<ActionEvent>>,
105    /// How `events` was recovered (e.g. `json:opencode-parts`,
106    /// `stream-json:content-blocks`), parallel to `text_source`; `null` when no
107    /// events were found. Lets a consumer tell "harness doesn't support it" from
108    /// "no tools were used."
109    pub events_source: Option<String>,
110    /// Structured-output run only: the JSON value extracted from the final
111    /// answer and validated against the requested schema. `null` when no schema
112    /// was requested, or when no JSON value could be extracted. Carries the
113    /// last-attempted value even when it failed validation, so a consumer can
114    /// see what the harness produced.
115    pub structured: Option<Value>,
116    /// Structured-output run only: whether `structured` conformed to the schema
117    /// on the final attempt. `null` when no schema was requested (or the harness
118    /// did not run); `false` when a schema was requested but the result never
119    /// conformed (including "no JSON found").
120    pub schema_valid: Option<bool>,
121    /// Structured-output run only: how many times this harness was invoked under
122    /// the validate/retry loop (1 + retries). `null` when no schema was
123    /// requested or the harness did not run.
124    pub schema_attempts: Option<u32>,
125    /// Structured-output run only: the validation errors from the final attempt,
126    /// joined for display; `null` when valid or no schema was requested.
127    pub schema_error: Option<String>,
128    /// Best-effort failure reason (`auth`, `rate_limit`, `model_not_found`,
129    /// `quota`) for a non-zero run; `null` when unclassified. Distinct from
130    /// `status`, which records oneharness's relationship to the process.
131    pub failure_kind: Option<String>,
132    /// Where `failure_kind` was read (`stderr`/`stdout`); `null` when absent.
133    pub failure_kind_source: Option<String>,
134    /// Raw captured stdout (empty for skipped/planned).
135    pub stdout: String,
136    /// Raw captured stderr (empty for skipped/planned).
137    pub stderr: String,
138    /// Human-readable problem + suggested action; `null` on success.
139    pub error: Option<String>,
140}
141
142/// The top-level `run` report written to stdout.
143#[derive(Debug, Clone, Serialize)]
144pub struct RunReport {
145    pub schema_version: &'static str,
146    pub oneharness_version: &'static str,
147    /// The prompt sent. On an ordinary run this is *the* prompt every result
148    /// shares; on a **batch** run (see `batch`) it repeats the first prompt for
149    /// back-compat, and each result's own `prompt` field is authoritative.
150    pub prompt: String,
151    pub model: Option<String>,
152    /// The session id being continued, when `--resume` was passed; else `null`.
153    pub resume: Option<String>,
154    /// Whether the resumed session was forked (`--fork`) rather than appended to.
155    /// `false` unless `--resume` was given with `--fork`.
156    pub fork: bool,
157    /// The normalized approval mode requested for this run (see the README
158    /// support matrix). Each harness maps it to its own mechanism.
159    pub permission_mode: PermissionMode,
160    /// Back-compat convenience: `true` exactly when `permission_mode` is
161    /// `bypass`. Retained so existing consumers keep working; new consumers
162    /// should read `permission_mode`.
163    pub bypass_permissions: bool,
164    pub dry_run: bool,
165    /// The JSON Schema applied to this run (structured output), or `null` when
166    /// none was requested. Echoed so a consumer sees the exact constraint each
167    /// result was validated against.
168    pub schema: Option<Value>,
169    /// Maximum retries allowed per harness under the validate/retry loop; `null`
170    /// when no schema was requested.
171    pub schema_max_retries: Option<u32>,
172    /// Same-prefix batch metadata when this run fanned **one** harness over more
173    /// than one prompt; `null` on an ordinary run. Its presence is the signal a
174    /// consumer keys on to read each result's own `prompt`.
175    pub batch: Option<BatchReport>,
176    /// The parsed `--mock-rules` ruleset this run was intercepted with; `null`
177    /// when no mocking was requested. Present so a consumer can tell a mocked
178    /// run's report from a clean one without out-of-band state.
179    pub mock_rules: Option<Value>,
180    /// The spy-log path the mock hook appended tool-call records to (absolute);
181    /// `null` when none was requested.
182    pub spy_file: Option<String>,
183    /// The history session file this run streamed normalized records to
184    /// (absolute); `null` when history was not enabled (or under `--print-command`,
185    /// where nothing runs). The programmatic handle a consumer captures to read the
186    /// session back later with `oneharness history show`.
187    pub history_file: Option<String>,
188    /// Config files that shaped this run, in layering order (user first,
189    /// project last); empty under `--no-config` or when none exist.
190    pub config_files: Vec<String>,
191    pub results: Vec<RunResult>,
192}
193
194/// Metadata for a same-prefix batch run (one harness, N prompts sharing a
195/// cacheable prefix). Present on [`RunReport::batch`] only in that mode.
196#[derive(Debug, Clone, Serialize)]
197pub struct BatchReport {
198    /// How the prompts were scheduled across the parallel runner.
199    pub strategy: BatchStrategy,
200    /// How many prompts were run (equals `results.len()`).
201    pub prompt_count: usize,
202    /// Whether the fan-out actually **forked** the warm-up's session to reuse its
203    /// cached prefix (`min-tokens` on a fork-capable harness whose warm-up exposed
204    /// a session id). `false` for `speed`, for `min-tokens` on a harness that
205    /// cannot fork, or when the warm-up exposed no session to fork. When `true`,
206    /// the fan-out results' `command` carries the resume/fork flags and their
207    /// `usage.cache_read_tokens` reflect the reused prefix.
208    pub forked: bool,
209}