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}