harn --json contract
Every harn subcommand that exposes a machine-readable mode emits a
versioned JSON envelope to stdout. Logs, progress, and warnings
continue to go to stderr so a --json pipeline stays a single
parseable document.
This page is the agent-facing contract. It cross-links the per-command shapes and explains the envelope discipline so an automated caller can drive Harn end-to-end without parsing prose.
Tracking epic: #1753.
Envelope shape#
Every --json payload is a JsonEnvelope<T> with the
same five fields. schemaVersion is the per-command discriminator —
agents dispatch on it to handle multiple Harn releases concurrently.
{
"schemaVersion": 1, // per-command, monotonically increasing
"ok": true, // false on hard failure; warnings keep ok=true
"data": { "...": "..." }, // command-specific payload, null on error
"error": null, // { "code", "message", "details" } when ok=false
"warnings": [] // [ { "code", "message" } ]; always [] not absent
}
Discovery#
The full catalog of registered commands and their current schema
version is available at runtime via the top-level --json-schemas
flag — itself an envelope:
harn --json-schemas | jq '.data[] | {command, schemaVersion}'
harn --json-schemas --command lint # filter to one entry
Error shape#
error.code is a stable lowercase identifier (e.g. "lint_failed",
"run_record_load_failed"). error.message is a human-readable
sentence. error.details is a free-form JSON object, null when the
command has no structured payload to attach.
Streaming commands#
A small number of commands emit NDJSON (one envelope-shaped event
per line) rather than a single document. Today this set is harn run --json, harn dev --watch --json, and harn connect <provider> --json.
Each line still carries
schemaVersion; consumers can jq -c over the stream.
Connector setup events use data.schema = "harn.connector_setup.event.v1"
and a monotonic data.sequence. They report setup stage, terminal status,
required host interaction, plain recovery copy, and a closed error code. They
never include credential values, OAuth codes, PKCE material, callback payloads,
or authorization URLs. Hosts should render the event stream directly and keep
browser interaction outside model context and transcripts.
harn connect setup-plan --json may list allowlisted environment variable
names for non-secret setup fields such as an OAuth client id. It never includes
the resolved values. The setup adapter consumes those values without putting
them in command arguments or connector status output.
harn run --emit-summary-json, --emit-phase-json, and
--emit-rusage-json are intentionally separate from this envelope
stream. Each emits one raw NDJSON object to stderr by default or to a
dedicated file/fd sink when supplied, so wrappers can read auxiliary
run metadata without changing the harn run --json stdout contract.
Supported commands#
These commands accept --json and emit a stable, schema-versioned
envelope. Run harn --json-schemas for the live list with current
versions.
| Command | Notes |
|---|---|
harn check --json | Per-file static check diagnostics + summary |
harn check provider-matrix --json | Provider/model capability matrix |
harn check connector-matrix --json | Connector package capability matrix |
harn fmt --json | Per-file formatting result for write and check modes |
harn lint --json | Per-file lint diagnostics + autofix availability |
harn parse --json | Tagged Harn AST with byte spans |
harn tokens --json | Lexer token stream with source lexemes |
harn run --json | Streaming NDJSON event log (stdio/tool/evidence/result) |
harn run --emit-summary-json | One terminal raw NDJSON summary object on stderr/file/fd |
harn run --emit-phase-json | One terminal raw NDJSON phase object on stderr/file/fd |
harn run --emit-rusage-json | One terminal raw NDJSON CPU sample on stderr/file/fd |
harn replay --json | Per-stage replay summary + embedded fixture verdict |
harn test conformance --json | Conformance results, xfail accounting, and duration distribution |
harn graph --json | Static module graph: symbols, imports, references, capabilities |
harn routes --json | Trigger route + budget + capability inventory |
harn dev --watch --json | Streaming NDJSON incremental rebuild events |
harn time run --json | Per-phase wall-clock + per-LLM/tool-call latency |
harn fix plan --json / apply --json | Repair plan or applied edits, plus skipped invalid files |
harn pack --json | .harnpack bundle build summary (inline schema) |
harn doctor --json | Capability matrix: host, targets, providers, effects |
harn explain <CODE> --json | Per-diagnostic-code explanation |
harn explain --catalog --json | Full diagnostic-code catalog |
harn session export | Portable session bundle export. Prints JSON to stdout when --out is omitted; there is no --json flag |
harn provider catalog show | Resolved provider/model catalog snapshot. Always prints JSON; there is no --json flag |
harn models batch plan --json | Provider Batch API candidates plus batch.harn_live_adapter support |
harn models batch manifest --json | Durable offline batch manifest summary and request groups |
harn models batch prepare --json | Provider-native batch request files, deterministic prepare receipt, and normalized lifecycle state |
harn models batch submit --json | Batch submission receipt with provider job ids, dry-run operations, and normalized lifecycle state |
harn models batch status --json | Provider batch status receipt with dry-run cached status validation and normalized lifecycle counts |
harn models batch cancel --json | Batch cancellation receipt with redacted cancel operations, skipped-job reasons, and normalized lifecycle counts |
harn models batch download --json | Provider result-file download receipt with artifact paths, hashes, and normalized lifecycle counts |
harn models batch execute init|advance|inspect|cancel --json | Durable execution receipt with stable identities, revision/history, artifact digests, and provider-operation recovery state |
harn models batch rejoin --json | Manifest-ordered normalized result artifact plus exact consumable/quarantine counts, ids, reasons, and source identities |
harn models lora plan --json | LoRA/QLoRA route, trainer, data, eval, promotion evidence, launch contract, training.contract.tool_catalog, and serving.serving_requirements |
harn models lora inspect --json | PEFT adapter compatibility report, launch metadata, and serving.serving_requirements |
harn models lora export --json | Trainer dataset export report with contract id, tool-catalog contract, stats, and promotion evidence contract |
harn models lora manifest --json | LoRA training-run manifest with route, data, artifact, tool-catalog contract, structured serving requirements, and promotion evidence contracts |
harn models lora train --json | LoRA trainer receipt with backend argv/status, backend.argv_required when argv is omitted, input hashes, route/tool-catalog metadata, dataset_audit, and post-training commands |
harn models lora preflight --json | Corpus readiness report before LoRA training, including typed behavior-strata policy/status/counts, missing classes, and unclassified record ids |
harn connect status --json / setup-plan --json | Connector readiness reports |
harn connect <provider> --json | Secret-free connector setup progress and terminal NDJSON events |
harn skill list --json / get --json | Canonical Harn skill corpus frontmatter |
harn version --json | CLI build metadata plus the VM-owned linked-runtime content fingerprint |
harn upgrade --json | Self-update probe (--check) or install summary |
Per-command notes#
harn run --json#
After the VM finishes, Harn writes the execution's run record and emits one
evidence_persisted event before the terminal event:
{
"schemaVersion": 1,
"ok": true,
"data": {
"event_type": "evidence_persisted",
"seq": 4,
"execution_id": "hxe-0199...",
"run_record_path": "/workspace/.harn-runs/hxe-0199....json"
},
"error": null,
"warnings": []
}
When --flight-recorder is active, data.flight_recording contains the same
artifact descriptor stored in the run record. Otherwise the field is omitted.
The path is readable when the event arrives. A terminal result or error
always has a higher seq.
Import failures discovered while launching a source program keep
error.code = "compile_error" and attach a closed detail object:
{
"kind": "import_failure",
"failure_class": "missing_imported_symbol",
"module": "./helpers",
"symbol": "old_name",
"source": "helpers.harn",
"harn_version": "0.10.125-dev",
"harn_revision": null
}
failure_class is unresolved_module, missing_imported_symbol,
private_imported_symbol, or imported_module_compile_failure. symbol is
null when the import does not name one. source is a workspace-relative path,
or a harn:// module URI when the target is outside the workspace.
harn_revision is the immutable build revision when the binary carries one;
local builds still identify the producer through harn_version. Entrypoint
parse failures and dependency materialization failures do not use this detail
kind.
harn version --json#
{
"schemaVersion": 2,
"ok": true,
"data": {
"name": "harn-cli",
"version": "0.8.27",
"description": "CLI for the Harn programming language — run, test, REPL, format, and lint",
"source_revision": "0123456789abcdef0123456789abcdef01234567",
"runtime_content_fingerprint": {
"schema": "harn.runtime_content_fingerprint.v1",
"content_sha256": "f27d...64 lowercase hexadecimal characters...",
"harn_version": "0.8.27",
"embedded_stdlib_sha256": "a61b...64 lowercase hexadecimal characters...",
"compatibility": {
"codegen_fingerprint": "02c9...",
"bytecode_schema_version": 14,
"linked_program_schema_version": 1,
"linker_algorithm_version": 1,
"build_features": {
"default": true,
"full": true,
"content": true,
"compression": true,
"http_compression": true,
"cloud_aws": true,
"native_keyring": true,
"postgres": true,
"sqlite": true,
"otel": false,
"testbench_wasi": false,
"llm_bench_internals": false,
"vm_bench_internals": false
}
},
"source_revision": "0123456789abcdef0123456789abcdef01234567"
}
},
"error": null,
"warnings": []
}
source_revision is an optional additive field under schema version 1. It is
the full immutable object ID supplied when the binary was built, or null when
that build carried no revision attestation. The command never guesses it from
the caller's current directory or a runtime environment variable. Consumers
that credit measurements to an exact revision should require a non-null exact
match; version-only consumers may ignore the field.
runtime_content_fingerprint is computed by the linked VM and cannot be
overridden by the caller's runtime environment. Its content_sha256 covers the
Harn version, every embedded standard-library source, and the code-generation,
bytecode, linker, and enabled Cargo-feature identities. source_revision is
optional provenance and is deliberately excluded from content_sha256. This
required fingerprint field changes the version command's schema from 1 to 2.
harn upgrade --json#
upgrade --json --check is the lowest-risk probe: it resolves the
target release without downloading. Combined with a real install, the
envelope is printed after the install action so callers can read the
final installed flag. An install also reports hook_runtime as
not_enrolled, refreshed, or skipped_unverified. A refreshed record includes
the release version, peeled source revision, and binary SHA-256. The field is
absent in check mode and same-version no-op responses.
{
"schemaVersion": 1,
"ok": true,
"data": {
"current": "0.8.27",
"target": "v0.8.27",
"needs_upgrade": false,
"mode": "check",
"installed": false,
"archive_url": "https://github.com/burin-labs/harn/releases/download/v0.8.27/harn-aarch64-apple-darwin.tar.gz",
"checksums_url": "https://github.com/burin-labs/harn/releases/download/v0.8.27/SHA256SUMS",
"target_triple": "aarch64-apple-darwin"
}
}
harn lint --json#
Mirrors the per-file diagnostic shape of harn check --json so agent
consumers can dispatch on a single CheckDiagnostic layout regardless
of whether they invoked check or lint.
The complete Draft 2020-12 schema is published inline:
harn --json-schemas --command lint | jq '.data[0].schemaJson'
Typed Harn consumers should decode through std/cli/envelope
(decode_lint_json / decode_lint_envelope) rather than re-implementing
envelope validation. The decoder fails closed on malformed JSON,
unsupported schemaVersion values, invalid severities or spans,
inconsistent summary aggregates or per-file status, and disagreement
between process exit status and ok when an exit code is supplied.
Diagnostic span values are UTF-8 half-open byte offsets
[start, end) into the source file. They are byte-accurate across
multiline and non-ASCII text; they are not character or UTF-16 indexes.
Empty spans (start == end) are valid. By contrast, data.changed.files[].added_lines
ranges are inclusive, one-based physical line numbers.
data.summary.fixablecounts diagnostics carrying autofix edits;fixedis the count actually applied (always0when--fixis not set).--jsonis intentionally orthogonal to--fix: agents plan repairs from the report and apply them in a follow-upharn lint --fixorharn fix apply.--changed-from <REV>addsdata.changedwithout changing the ordinary lint payload. It records the requested and resolvedfrom/tocommits plus every evaluated source path, change status, previous rename/copy path when present, and inclusive one-basedadded_linesranges.data.files[].diagnosticscontains matching warning/error diagnostics and all information diagnostics; the file statuses and summary counters are recomputed from that filtered set.- Soft lint failure (
lint_failed) keepsok: falsewhile still populatingdatawith the report. Hard failures such asno_lint_targetsusedata: null.
{
"schemaVersion": 1,
"ok": false,
"data": {
"files": [
{
"path": "src/agent.harn",
"status": "warning",
"diagnostics": [
{
"source": "lint",
"severity": "warning",
"code": "HARN-LNT-032",
"message": "comparison to `false` is redundant",
"span": { "start": 128, "end": 142 }
}
],
"fixable": 1,
"fixed": 0
}
],
"summary": {
"ok": 0,
"warnings": 1,
"errors": 0,
"diagnostics": 1,
"fixable": 1,
"fixed": 0
},
"changed": {
"from": { "requested": "origin/main", "commit": "<full commit id>" },
"to": { "requested": "HEAD", "commit": "<full commit id>" },
"files": [
{
"path": "src/agent.harn",
"status": "modified",
"added_lines": [{ "start": 9, "end": 9 }]
}
]
}
},
"error": {
"code": "lint_failed",
"message": "one or more changed lines failed `harn lint`",
"details": null
},
"warnings": []
}
harn replay --json#
Loads a persisted run record and emits a structured per-stage summary
plus the embedded replay-fixture verdict. harn replay --fixture <path>
accepts either a run record or a harn.orchestration.replay_trace.v1
fixture. harn replay --session-id <id> --events-db <path> reconstructs
the same replayable run-record shape from the SQLite EventLog
observability.agent_events.<id> topic.
The default --runs 1 response keeps the original single ReplayReport
payload. When --runs N is greater than one, the response keeps
schemaVersion, ok, error, and warnings at the top level and also
emits top-level reports, runs, and determinism fields. reports
contains one ReplayReport per replay read, runs contains the
allowlist-normalized event sequences, and determinism reports the
allowlist-stripped replay comparison. ok: false with error.code: "replay_fixture_failed" indicates at least one fixture verdict failed;
error.code: "replay_determinism_failed" indicates the per-run event
material diverged after applying the replay allowlist.
harn replay --session-id <id> --counterfactual <plan.harn> evaluates an
alternate edit plan after the replay source has been rehydrated at the
--at cutoff and attaches the divergence to the single ReplayReport
under data.counterfactual: { plan_path, plan_paths, step_count, result, diverged: [{ path, status, lines_added, lines_removed }], files_touched, lines_added, lines_removed, ops_applied, ops_rejected }. Repeat
--counterfactual to chain plans; the returned edit-op lists are
concatenated into one cumulative edit.dry_run. The field is omitted for
a plain replay. A plan that fails to load or evaluate exits non-zero with
error.code: "replay_counterfactual_failed".
harn test --json-out#
User-test reports are standalone JSON documents rather than envelopes. Schema v3 includes typed timeout and phase records, suite-level timing and aggregate work attribution, and optional captured output:
{
"schemaVersion": 3,
"suite": "user",
"root": "/workspace/tests",
"duration_ms": 31,
"timing": {
"sample_count": 1,
"average_ms": 30,
"p50_ms": 30,
"p90_ms": 30,
"p95_ms": 30,
"p99_ms": 30
},
"aggregate": {
"collection_ms": 1,
"setup_ms": 0,
"compile_ms": 0,
"execute_ms": 30,
"teardown_ms": 0,
"modules": {
"module_compile_ms": 4,
"module_load_ms": 7,
"modules_compiled": 1,
"modules_loaded": 2
}
},
"summary": {
"total": 1,
"passed": 0,
"failed": 0,
"timed_out": 1,
"skipped": 0
},
"cases": [{
"name": "test_timeout",
"file": "test_timeout.harn",
"classname": "test_timeout.harn",
"outcome": "timed_out",
"duration_ms": 30,
"timeout": { "phase": "execute", "limit_ms": 30 },
"phases": {
"setup_ms": 0,
"compile_ms": 0,
"execute_ms": 30,
"teardown_ms": 0,
"modules": {
"module_compile_ms": 4,
"module_load_ms": 7,
"modules_compiled": 1,
"modules_loaded": 2
}
},
"message": "execute phase timed out after 30ms"
}]
}
root is the narrowest common directory containing every requested test
target; a requested file contributes its parent directory. Case file and
classname values are relative to that root, which remains stable when
--affected-from narrows the executed files.
timeout, phases, and message are omitted when unavailable. An empty
distribution has sample_count: 0 and null for every duration statistic.
Discovery and worker-start error rows are not duration samples. Aggregate
phases are cumulative worker-time and can exceed suite wall time under parallel
execution. Nested module values overlap setup/execute and are never additive.
harn test conformance --json#
Conformance schema v3 retains the standard envelope and adds the same typed
duration distribution under data.timing. Sequential and parallel runs use
the same snapshot and result contract; durations can differ because workers
overlap:
A run that executed no tests is reported as ok: false with error.code: "conformance_empty_selection" and data: null, on both the sequential and
parallel paths, unless --allow-empty is passed.
{
"schemaVersion": 3,
"ok": true,
"data": {
"snapshotKey": "<blake3-hex>",
"results": [{
"name": "pass.harn",
"outcome": "pass",
"duration_ms": 12,
"message": null,
"diagnostic_codes": []
}],
"summary": {
"pass": 1,
"fail": 0,
"xfail_expected": 0,
"xfail_unexpected_pass": 0,
"skipped": 0
},
"timing": {
"sample_count": 1,
"average_ms": 12,
"p50_ms": 12,
"p90_ms": 12,
"p95_ms": 12,
"p99_ms": 12
}
},
"error": null,
"warnings": []
}
harn serve test#
The JSON-RPC initialize result advertises
capabilities.test_run.schema_version: 2. Each test/run result uses
snake-case schema_version: 2 and contains worker identity, run/cache counters,
and summary. The summary is the user-runner shape above: results, verdict
counts, wall duration, timing, and cumulative aggregate. Each executed result
carries optional typed timeout and measured phases with nested module
attribution; discovery and worker-start errors omit unavailable phases.
harn run --emit-summary-json#
The post-run summary is a raw NDJSON line, not a JsonEnvelope, because
it is an auxiliary sink rather than the command's primary stdout JSON
mode. --summary-file <path> overwrites the file with the one-line
summary; --summary-fd <fd> writes the same line to an already-open
Unix file descriptor.
{
"schema_version": 4,
"event": "run_summary",
"wall_time_ms": 1234,
"exit_code": 0,
"llm": {
"call_count": 2,
"provider_call_count": 3,
"input_tokens": 1024,
"output_tokens": 256,
"time_ms": 480,
"cost_usd": 0.0042,
"known_cost_usd": 0.0042,
"unpriced_calls": 0,
"usage_unknown_calls": 0
},
"profile": {
"total_wall_ms": 1234,
"by_kind": [],
"residual_ms": 12,
"top_llm_calls": [],
"top_tool_calls": [],
"steps": []
}
}
profile is omitted unless --profile or --profile-json is active.
The LLM counters are enabled by the summary flag itself, so callers do
not need to add --trace to receive logical call count, physical provider
request count, token totals, LLM time, and accumulated cost.
llm.call_count counts completed logical operations;
llm.provider_call_count includes schema, transport, and content retries.
llm.cost_usd is null when any physical request is unpriced, while
llm.known_cost_usd remains the priced-request lower bound.
llm.usage_unknown_calls separately counts physical requests whose token or
cache usage is unavailable; known token totals are then lower bounds. Schema
version 4 adds the physical-call and unknown-usage fields to the nullable-cost
version 3 contract.
harn run --emit-phase-json#
The phase sink is also a raw NDJSON line. It preserves the same
seven-row contract as harn time run --json, but routes it to a
separate sink so a parent wrapper can spawn harn run and recover
parse/typecheck/compile/setup/main timing without parsing stdout.
--phase-file <path> overwrites the file with the one-line phase
object; --phase-fd <fd> writes the same line to an already-open Unix
file descriptor.
{
"schema_version": 2,
"event": "run_phase",
"phases": [
{ "name": "parse", "kind": "top_level", "duration_ms": 12, "input_bytes": 4096 },
{ "name": "typecheck", "kind": "top_level", "duration_ms": 80 },
{ "name": "bytecode_compile", "kind": "top_level", "duration_ms": 35, "cache": "miss" },
{ "name": "run_setup", "kind": "top_level", "duration_ms": 8 },
{ "name": "run_main", "kind": "top_level", "duration_ms": 1200, "events": 14 },
{ "name": "module_compile", "kind": "attribution", "duration_ms": 40, "events": 3 },
{ "name": "module_load", "kind": "attribution", "duration_ms": 75, "events": 8 }
]
}
The phase order is fixed: parse, typecheck, bytecode_compile,
run_setup, run_main, module_compile, module_load. Cache hits keep all
seven rows and switch the
bytecode_compile row to "cache": "hit" while leaving parse and
typecheck at duration_ms: 0. kind is the machine-readable addition rule:
only top_level rows reconcile wall time. The final two attribution rows
overlap top-level phases; their events values count successful compiles and
unique fresh-VM loads.
harn run --emit-rusage-json#
The rusage sink is a raw NDJSON line carrying the process CPU sample
needed by subprocess wrappers that cannot call getrusage in-process.
--rusage-file <path> overwrites the file with the one-line object;
--rusage-fd <fd> writes the same line to an already-open Unix file
descriptor.
{
"schema_version": 1,
"event": "run_rusage",
"cpu_ms": 320
}
Compatibility#
schemaVersionis bumped when the data shape changes in a way agents need to detect. Additive optional fields can land without a bump.- Errors are machine-readable:
error.codeis a stable identifier;error.messagecarries the human sentence;error.detailsis free-form structured context. - Streaming commands keep the same envelope shape per line.
--jsonmode never mixes human chatter into stdout. Anything diagnostic — progress bars, warnings about flags, network logs — goes to stderr.
When in doubt#
Run harn <subcommand> --help to confirm --json is supported, and
harn --json-schemas --command <subcommand> to see the current schema
version. If a subcommand is missing from the catalog, that's a bug
worth filing.