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 and harn dev --watch --json. Each line still carries schemaVersion; consumers can jq -c over the stream.

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.

CommandNotes
harn check --jsonPer-file static check diagnostics + summary
harn check provider-matrix --jsonProvider/model capability matrix
harn check connector-matrix --jsonConnector package capability matrix
harn fmt --jsonPer-file formatting result for write and check modes
harn lint --jsonPer-file lint diagnostics + autofix availability
harn parse --jsonTagged Harn AST with byte spans
harn tokens --jsonLexer token stream with source lexemes
harn run --jsonStreaming NDJSON event log (stdout/stderr/tool/result)
harn run --emit-summary-jsonOne terminal raw NDJSON summary object on stderr/file/fd
harn run --emit-phase-jsonOne terminal raw NDJSON phase object on stderr/file/fd
harn run --emit-rusage-jsonOne terminal raw NDJSON CPU sample on stderr/file/fd
harn replay --jsonPer-stage replay summary + embedded fixture verdict
harn test conformance --jsonConformance results with xfail accounting
harn graph --jsonStatic module graph: symbols, imports, capabilities
harn routes --jsonTrigger route + budget + capability inventory
harn dev --watch --jsonStreaming NDJSON incremental rebuild events
harn time run --jsonPer-phase wall-clock + per-LLM/tool-call latency
harn fix plan --json / apply --jsonRepair plan or applied edits, plus skipped invalid files
harn pack --json.harnpack bundle build summary (inline schema)
harn doctor --jsonCapability matrix: host, targets, providers, effects
harn explain <CODE> --jsonPer-diagnostic-code explanation
harn explain --catalog --jsonFull diagnostic-code catalog
harn session export --jsonPortable session bundle export
harn provider-catalog --jsonResolved provider/model catalog snapshot
harn connect status --json / setup-plan --jsonConnector readiness reports
harn skills list --json / get --jsonCanonical Harn skill corpus frontmatter
harn version --jsonCLI build metadata (name, version, description)
harn upgrade --jsonSelf-update probe (--check) or install summary

Per-command notes

harn version --json

{
  "schemaVersion": 1,
  "ok": true,
  "data": {
    "name": "harn-cli",
    "version": "0.8.27",
    "description": "CLI for the Harn programming language — run, test, REPL, format, and lint"
  },
  "error": null,
  "warnings": []
}

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.

{
  "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.

  • data.summary.fixable counts diagnostics carrying autofix edits; fixed is the count actually applied (always 0 when --fix is not set).
  • --json is intentionally orthogonal to --fix: agents plan repairs from the report and apply them in a follow-up harn lint --fix or harn fix apply.

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 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": 1,
  "event": "run_summary",
  "wall_time_ms": 1234,
  "exit_code": 0,
  "llm": {
    "call_count": 2,
    "input_tokens": 1024,
    "output_tokens": 256,
    "time_ms": 480,
    "cost_usd": 0.0042
  },
  "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 llm.call_count, token totals, LLM time, and accumulated cost.

harn run --emit-phase-json

The phase sink is also a raw NDJSON line. It preserves the same five-row phase 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": 1,
  "event": "run_phase",
  "phases": [
    { "name": "parse", "duration_ms": 12, "input_bytes": 4096 },
    { "name": "typecheck", "duration_ms": 80 },
    { "name": "bytecode_compile", "duration_ms": 35, "cache": "miss" },
    { "name": "run_setup", "duration_ms": 8 },
    { "name": "run_main", "duration_ms": 1200, "events": 14 }
  ]
}

The phase order is fixed: parse, typecheck, bytecode_compile, run_setup, run_main. Cache hits keep all five rows and switch the bytecode_compile row to "cache": "hit" while leaving parse and typecheck at duration_ms: 0.

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

  • schemaVersion is 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.code is a stable identifier; error.message carries the human sentence; error.details is free-form structured context.
  • Streaming commands keep the same envelope shape per line.
  • --json mode 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.