# `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](https://github.com/burin-labs/harn/issues/1753).

## Envelope shape

Every `--json` payload is a [`JsonEnvelope<T>`][envelope-impl] with the
same five fields. `schemaVersion` is the per-command discriminator —
agents dispatch on it to handle multiple Harn releases concurrently.

```jsonc
{
  "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
}
```

[envelope-impl]: https://github.com/burin-labs/harn/blob/main/crates/harn-cli/src/json_envelope.rs

### 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:

```bash
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.

| 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 (stdout/stderr/tool/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 with xfail accounting                |
| `harn graph --json`            | Static module graph: symbols, imports, 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 --json`   | Portable session bundle export                           |
| `harn provider-catalog --json` | Resolved provider/model catalog snapshot                 |
| `harn connect status --json` / `setup-plan --json` | Connector readiness reports        |
| `harn skills list --json` / `get --json` | Canonical Harn skill corpus frontmatter        |
| `harn version --json`          | CLI build metadata (`name`, `version`, `description`)    |
| `harn upgrade --json`          | Self-update probe (`--check`) or install summary         |

## Per-command notes

### `harn version --json`

```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.

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

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

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

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