JSON Contract
Envelope structure
Every --json response is a single JSON object with these top-level fields:
| Field | Type | Description |
|---|---|---|
ok |
boolean | true if errors is empty, false otherwise. |
tool_version |
string | The spoolctl version that produced this envelope. |
data |
object or null | The verb’s result payload. null on failure. |
meta |
object | Request metadata: request_id, ts_iso, elapsed_ms, contract_version, data_hash. May include pagination or wait sub-objects. |
warnings |
array | Warning objects. May be non-empty even when ok is true. |
commands |
array | Reserved for future use. Always [] in the current contract. |
errors |
array | Error objects. Non-empty implies ok: false. |
On failure, the same envelope structure is used with data: null and populated errors. No verb ever produces a raw error string outside the envelope.
Output modes
spoolctl has four output modes. The mode is determined by flags and context:
| Mode | Trigger | Output |
|---|---|---|
envelope |
--json |
A single JSON envelope on stdout. |
frames |
events --follow --json |
NDJSON: one JSON object per line. Data frames interleaved with control frames. |
raw |
output --raw |
Raw bytes of captured stdout/stderr, written directly to stdout. |
text |
No --json flag (default) |
Human-readable text. Format may change between versions. |
envelope is the stable machine interface. text is for humans and is not part of the contract.
frames mode skips envelope emission entirely. The stream consists of data frames (event objects) and control frames ({"control": {"type": ..., "reason": ...}}). See Events.
raw mode writes captured output bytes directly to stdout with no framing. It is mutually exclusive with --json.
Totality guarantees
spoolctl’s contract requires total, structured responses for all inputs:
- No traceback. Every unexpected exception is caught and converted to a structured
INTERNALerror with exit code 3. Python tracebacks never reach stdout or stderr. - No hang. Every code path that blocks has a timeout.
- Structured errors for malformed input. Invalid types, out-of-range values, bad durations, bad timestamps, bad env var syntax, and bad enum values all produce
INVALID_INPUTerrors with exit code 1. - Flag abbreviation disabled. All parsers are constructed with
allow_abbrev=False.--jsdoes not expand to--json. - Inert flags rejected. Unrecognized flags produce
UNKNOWN_FLAG(exit 1) with adid_you_meansuggestion, not a silent pass-through.
These guarantees mean a consumer can always json.loads() the stdout of a --json command and get a well-formed envelope, even for inputs the consumer did not anticipate.
Error and warning objects
Error objects in errors[]:
{
"code": "INVALID_INPUT",
"message": "...",
"exit_code": 1,
"detail": {}
}
Warning objects in warnings[]:
{
"code": "IDEMPOTENCY_METADATA_DIFFERS",
"message": "..."
}
See Error Reference for the full code registry.
CONTRACT_VERSION policy
CONTRACT_VERSION (currently "2") is a semantic marker for the contract surface. It changes when the contract makes a breaking change. The tool version changes on every release; the contract version changes rarely.
The current policy: contract version 2 is the pre-release hardening contract. It intentionally breaks v1 quirks (refusing unsafe operations, rejecting inert flags, enforcing structured malformed-input errors, declaring all four output modes, adding config and doctor). No v1 compatibility shim is provided before public release.
Consumers that depend on specific contract behavior should check meta.contract_version and fail explicitly if it changes.
Consuming schema --json
schema --json provides the structural schema for envelopes and per-verb data payloads. Use it to validate responses programmatically or to generate client bindings. See Agent Guide.