SubX-CLI ships a stable, versioned JSON output mode designed for shell scripts, CI pipelines, and third-party tools. When you opt in, every supported subcommand emits a single JSON envelope on stdout in place of the human-oriented status symbols, progress bars, and result tables. This document is the authoritative reference for that contract.
The default text mode is unchanged. JSON mode is strictly opt-in and
the existing exit-code contract (1–6, mapped through
SubXErrorExt::exit_code from subx-cli/src/cli/error_ext.rs) is preserved across
both modes.
- Overview
- Activation
- Envelope Schema
- Schema-Version Policy
- Error Envelope
- Per-Command Payload Schemas
- CLI Parsing Flow
generate-completionRejection- Stdout/Stderr Discipline
cache status --jsonLegacy Alias- Scripting Recipes
- Stability Guarantees
- References
JSON mode replaces SubX-CLI's interactive UX with a single JSON document per invocation:
- Default (text mode) — colored status symbols (
✓/✗/⚠),indicatifprogress bars, the AI match results table, and free-form prose. Output strings are not part of any contract and may evolve. - Opt-in (JSON mode) — exactly one UTF-8 JSON document terminated
by a single
\nis written to stdout. The shape of that document is fixed byschema_versionand is the contract scripts rely on.
Use JSON mode when you need to:
- Parse match candidates, sync offsets, conversion plans, encoding detections, or cache state programmatically.
- React to specific error categories from a script without scraping English error messages.
- Pipe SubX-CLI output through
jqor another JSON tool without a sanitization step.
Stay in text mode for interactive use; the default UX is unaffected.
JSON mode is selected by either:
-
The top-level
--output jsonflag, before the subcommand token:subx-cli --output json match ./media subx-cli --output json convert --format srt ./subs/
-
The
SUBX_OUTPUTenvironment variable:export SUBX_OUTPUT=json subx-cli match ./media
--output and SUBX_OUTPUT accept text or json (case-insensitive).
An explicit top-level --output argument always wins over
SUBX_OUTPUT. So SUBX_OUTPUT=json subx-cli --output text match …
runs in text mode.
The top-level --output flag is only accepted before the subcommand
token. This is intentional, because convert, sync, and
translate already define their own subcommand-local
--output <PATH> argument that designates an output file. The
positional rule keeps both meanings unambiguous:
# UNAMBIGUOUS:
# first --output -> output MODE (parsed by the root Cli)
# second --output -> output FILE (parsed by ConvertArgs)
subx-cli --output json convert --output a.ass --format ass ./a.srt
# DOES NOT switch to JSON mode:
# --output here is ConvertArgs.output and receives the literal "json"
subx-cli convert --output json --format ass ./a.srtThe same constraint applies to --quiet. Both flags must precede the
subcommand; placing them after the subcommand causes clap to reject the
invocation as an unknown argument (no subcommand currently defines its
own --quiet).
--quiet is orthogonal to the output mode:
- In text mode,
--quietsuppressesprint_success/print_warninghelpers, progress bars, and the match result table. - In JSON mode, stdout is implicitly quiet by construction (only the
envelope is ever written) AND stderr is tightened to suppress all
free-form
eprintln!/println!chatter (matcher analysis blocks, conflict-resolution warnings, AI candidate listings).--quietadditionally silences any remaining structuredtracing/logrecords that JSON mode would otherwise allow on stderr.
--quiet never suppresses the JSON envelope on stdout and never
changes the process exit code.
Every JSON-mode invocation writes exactly one UTF-8 JSON object on
stdout, terminated by a single \n, with the following top-level keys:
| Key | Type | When present | Description |
|---|---|---|---|
schema_version |
string | Always | Semver-style version, currently "1.0". |
command |
string | Always | Subcommand name: "match", "sync", "convert", "detect-encoding", "translate", "cache", "config", or "generate-completion". May be empty for synthetic argument-parsing envelopes when the subcommand could not be identified. |
status |
string | Always | "ok" or "error". |
data |
object | status == "ok" |
Command-specific payload. Omitted entirely (never null) when status == "error". |
error |
object | status == "error" |
Defined by the Error Envelope section. Omitted entirely on success. |
warnings |
array|null | Optional | Reserved for non-fatal warnings. Currently always omitted; consumers SHALL tolerate its presence as an array of {code, message} objects. |
{
"schema_version": "1.0",
"command": "convert",
"status": "ok",
"data": {
"conversions": [
{
"input": "/media/movie.srt",
"output": "/media/movie.ass",
"source_format": "srt",
"target_format": "ass",
"encoding": "UTF-8",
"applied": true,
"entry_count": 412,
"status": "ok"
}
]
}
}{
"schema_version": "1.0",
"command": "convert",
"status": "error",
"error": {
"category": "subtitle_format",
"code": "E_SUBTITLE_FORMAT",
"exit_code": 4,
"message": "Subtitle format error (SRT): unexpected end of file"
}
}schema_version follows semantic versioning.
- Patch / minor bumps (
1.0→1.1,1.1→1.2, …) are backward-compatible. New optional fields MAY be added todata,error.details, or as new top-level optional keys (e.g. a future top-levelerrors[]array mirroring per-item failures, which is reserved bydesign.mdbut not emitted in1.0). Every key documented for an earlier minor version SHALL keep the same type and semantics. - Major bumps (
1.x→2.0) are reserved for renaming or removing documented keys, changing types, or restructuring the envelope. Any major bump goes through a dedicated OpenSpec change proposal.
Scripts SHOULD test the major version (startswith("1.")) and accept
any minor / patch number.
When status == "error", the envelope contains an error object with
these keys:
| Key | Type | When present | Description |
|---|---|---|---|
category |
string | Always | Stable snake_case identifier (see table below). |
code |
string | Always | Stable upper-snake-case machine code, prefixed E_ (e.g. E_AI_SERVICE). |
exit_code |
integer | Always | The numeric process exit code, equal to SubXErrorExt::exit_code for that variant (or to the underlying handler's exit code for synthetic envelopes). The process exit status equals this value. |
message |
string | Always | Human-readable English text from SubXErrorExt::user_friendly_message (or the rendered clap error with ANSI removed for synthetic envelopes). |
hint |
string | Optional | Short remediation hint surfaced from SubXError::hint. |
details |
object | Optional | Free-form structured context (e.g. path, format, partial_results). |
The numeric process exit code SHALL equal error.exit_code.
The category set is closed for envelopes derived from SubXError. The
synthetic argument_parsing category is added by the CLI parsing flow
(see CLI Parsing Flow) and has no SubXError
backing variant.
SubXError variant |
category |
code |
exit_code |
|---|---|---|---|
Io |
io |
E_IO |
1 |
Config |
config |
E_CONFIG |
2 |
SubtitleFormat |
subtitle_format |
E_SUBTITLE_FORMAT |
4 |
AiService |
ai_service |
E_AI_SERVICE |
3 |
Api |
api |
E_API |
3 |
AudioProcessing |
audio_processing |
E_AUDIO_PROCESSING |
5 |
FileMatching |
file_matching |
E_FILE_MATCHING |
6 |
FileAlreadyExists |
file_already_exists |
E_FILE_ALREADY_EXISTS |
1 |
FileNotFound |
file_not_found |
E_FILE_NOT_FOUND |
1 |
InvalidFileName |
invalid_file_name |
E_INVALID_FILE_NAME |
1 |
FileOperationFailed |
file_operation_failed |
E_FILE_OPERATION_FAILED |
1 |
CommandExecution |
command_execution |
E_COMMAND_EXECUTION |
1 |
NoInputSpecified |
no_input_specified |
E_NO_INPUT_SPECIFIED |
1 |
InvalidPath |
invalid_path |
E_INVALID_PATH |
1 |
PathNotFound |
path_not_found |
E_PATH_NOT_FOUND |
1 |
DirectoryReadError |
directory_read_error |
E_DIRECTORY_READ_ERROR |
1 |
InvalidSyncConfiguration |
invalid_sync_configuration |
E_INVALID_SYNC_CONFIGURATION |
1 |
UnsupportedFileType |
unsupported_file_type |
E_UNSUPPORTED_FILE_TYPE |
1 |
OutputModeUnsupported |
command_execution |
E_OUTPUT_MODE_UNSUPPORTED |
1 |
Other |
other |
E_OTHER |
1 |
| (synthetic — clap parse failure) | argument_parsing |
E_ARGUMENT_PARSING |
clap exit code (typically 2) |
OutputModeUnsupported deliberately reuses the command_execution
category because that is its SubXErrorExt::exit_code group; the more
specific signal is in code (E_OUTPUT_MODE_UNSUPPORTED). See
generate-completion Rejection.
For commands that may fail mid-run after applying some operations, the
top-level error.details.partial_results object MAY carry the set of
already-applied changes so a script can reconcile state without
re-scanning the filesystem. This is an optional enrichment; scripts
MUST tolerate its absence.
The following sections document each covered command's data shape.
Field names are committed contract; types are JSON types.
Several commands process multiple files in a single invocation
(match, sync, convert, detect-encoding, cache apply,
translate). They follow a uniform per-item / top-level rule:
statusis"ok"at the top level whenever the command made forward progress on at least one item. Per-item failures appear in the success payload with their ownstatus: "error"plus anerror { code, category, message }object (the per-item error has noexit_code).statusis"error"at the top level only when the entire command failed before any per-item progress — config invalid, no inputs specified, fatal I/O, or a single-input invocation whose only input failed. In that casedatais omitted and the top-levelerrorcarries the failure.
The process exit code follows the top-level decision: 0 when
top-level status == "ok" (even if some per-item entries failed), and
error.exit_code otherwise.
{
"schema_version": "1.0",
"command": "match",
"status": "ok",
"data": {
"dry_run": false,
"confidence_threshold": 80,
"candidates": [
{
"video": "/media/Movie.mkv",
"subtitle": "/media/sub.srt",
"confidence": 92,
"accepted": true
},
{
"video": "/media/Other.mkv",
"subtitle": "/media/maybe.srt",
"confidence": 41,
"accepted": false,
"reason": "below_threshold"
}
],
"operations": [
{
"kind": "rename",
"source": "/media/sub.srt",
"target": "/media/Movie.srt",
"applied": true,
"status": "ok"
}
],
"summary": {
"total_candidates": 2,
"accepted": 1,
"applied": 1,
"skipped": 1,
"failed": 0
}
}
}data.candidates[].reason is one of "below_threshold" or
"id_not_found" and is only present when accepted == false.
data.operations[].kind is one of "rename", "copy", or "move".
Per-operation status is "ok" or "error"; the latter carries an
error { code, category, message }. summary.failed counts operations
whose per-item status == "error".
sync always emits a uniform payload shape exposing a top-level
method, an inputs array describing the per-subtitle analysis stage,
and an operations array describing the per-subtitle write stage.
Single-pair invocations produce 1-element arrays; batch invocations
produce N parallel entries (one input ↔ one operation per processed
subtitle).
Single-pair invocation — VAD-driven sync against a single subtitle/video pair:
{
"schema_version": "1.0",
"command": "sync",
"status": "ok",
"data": {
"method": "vad",
"inputs": [
{
"subtitle_path": "/media/movie.srt",
"audio_path": "/media/movie.mkv",
"detected_offset_ms": -1280,
"confidence": 0.91,
"vad": {
"sensitivity": 0.5,
"padding_ms": 300,
"segments": [{"start": 1.2, "end": 4.8, "duration": 3.6}]
},
"status": "ok"
}
],
"operations": [
{
"subtitle_path": "/media/movie.srt",
"output_path": "/media/movie.srt",
"applied": true,
"dry_run": false,
"status": "ok"
}
]
}
}method is "vad", "manual", or "auto" and reflects the active
dispatch decision. Inside an input entry, confidence and vad are
absent for manual offsets; audio_path is absent when sync ran without
a video. For manual sync, detected_offset_ms equals the user-supplied
offset converted to milliseconds. Inside an operation entry,
output_path is absent on dry runs that produced no concrete target.
Batch invocation — same shape, one entry per processed subtitle.
Per-file failures surface as entries with status == "error" plus an
error object (with code, category, message); the top-level
envelope stays status == "ok" as long as at least one input
succeeded or at least one operation was applied:
{
"data": {
"method": "manual",
"inputs": [
{
"subtitle_path": "/media/a.srt",
"audio_path": "/media/a.mp4",
"detected_offset_ms": 500,
"status": "ok"
},
{
"subtitle_path": "/media/b.srt",
"audio_path": "/media/b.mp4",
"detected_offset_ms": 0,
"status": "error",
"error": {
"code": "E_SUBTITLE_FORMAT",
"category": "subtitle_format",
"message": "..."
}
}
],
"operations": [
{ "subtitle_path": "/media/a.srt", "output_path": "/media/a.srt",
"applied": true, "dry_run": false, "status": "ok" },
{ "subtitle_path": "/media/b.srt",
"applied": false, "dry_run": false, "status": "error",
"error": {
"code": "E_SUBTITLE_FORMAT",
"category": "subtitle_format",
"message": "..."
}
}
]
}
}A batch with zero successful items (no inputs[i].status == "ok"
and no operations[i].applied == true) does NOT emit a success
envelope wrapping all-error items. Instead, the command exits with a
top-level error envelope (typically error.category == "file_matching"
when no subtitle/video pairing succeeded). Whole-command failures such
as InvalidSyncConfiguration likewise produce a top-level error
envelope.
{
"schema_version": "1.0",
"command": "convert",
"status": "ok",
"data": {
"conversions": [
{
"input": "/media/a.srt",
"output": "/media/a.ass",
"source_format": "srt",
"target_format": "ass",
"encoding": "UTF-8",
"applied": true,
"entry_count": 412,
"status": "ok"
},
{
"input": "/media/corrupt.srt",
"output": "/media/corrupt.ass",
"target_format": "ass",
"encoding": "UTF-8",
"applied": false,
"status": "error",
"error": {
"code": "E_SUBTITLE_FORMAT",
"category": "subtitle_format",
"message": "Subtitle format error (SRT): unexpected end of file"
}
}
]
}
}source_format and entry_count are omitted on entries that failed
before parsing succeeded. A batch with at least one successful
conversion keeps top-level status == "ok" and exit code 0; a
single-input invocation that fails produces the top-level error
envelope with error.category == "subtitle_format" and
error.exit_code == 4.
{
"schema_version": "1.0",
"command": "detect-encoding",
"status": "ok",
"data": {
"files": [
{
"path": "/media/a.srt",
"status": "ok",
"encoding": "UTF-8",
"confidence": 1.0,
"has_bom": true,
"bytes_sampled": 8192
},
{
"path": "/media/missing.srt",
"status": "error",
"error": {
"code": "E_FILE_NOT_FOUND",
"category": "file_not_found",
"message": "File not found: /media/missing.srt"
}
}
]
}
}encoding, confidence, has_bom, and bytes_sampled are omitted on
failed entries. A single-input invocation against a missing file
produces the top-level error envelope (E_FILE_NOT_FOUND /
E_PATH_NOT_FOUND).
cache's data shape varies by subcommand. The top-level command
field is always "cache".
{
"command": "cache",
"status": "ok",
"data": {
"path": "/home/user/.local/share/subx/cache.json",
"exists": true,
"journal_present": true,
"total": 12,
"pending": 3,
"applied": 9,
"size_bytes": 4096,
"created_at": 1700000000,
"age_seconds": 3600,
"cache_version": "1",
"ai_model": "gpt-4.1-mini",
"operation_count": 12,
"config_hash": "abcdef…",
"current_config_hash": "abcdef…",
"config_hash_match": true,
"snapshot_status": "valid"
}
}The required fields per the cache-management spec are total,
pending, and applied (non-negative integers). Every other field is
an additive enrichment and MAY be absent.
{ "command": "cache", "status": "ok",
"data": { "removed": 2, "kind": "all",
"cache_path": "…", "cache_removed": true,
"journal_path": "…", "journal_removed": true } }removed is 0 when the cache was already empty.
{ "command": "cache", "status": "ok",
"data": { "rolled_back": 5 } }{
"command": "cache",
"status": "ok",
"data": {
"applied": 4,
"failed": 1,
"items": [
{ "id": "/path/to/sub-a.srt", "status": "ok" },
{ "id": "/path/to/sub-b.srt", "status": "error",
"error": {
"code": "E_FILE_NOT_FOUND",
"category": "file_not_found",
"message": "File not found: /path/to/sub-b.srt"
} }
]
}
}applied + failed == items.len(). Per-item failures keep the top-level
status == "ok"; only fatal whole-command errors (e.g., missing
journal) produce a top-level error envelope.
cache list is reserved by the spec for a future iteration; the
current implementation does not expose a list subcommand. When
added, it SHALL emit
{ "entries": [{ "id", "kind", "created_at", "summary" }] }.
{
"schema_version": "1.0",
"command": "translate",
"status": "ok",
"data": {
"translated_files": [
{ "input": "/media/a.srt", "output": "/media/a.zh-TW.srt",
"applied": true },
{ "input": "/media/b.srt", "output": "/media/b.zh-TW.srt",
"applied": false }
]
}
}The translate payload is intentionally minimal in 1.0. Per-item
errors are surfaced through applied: false plus a top-level
CommandExecution error envelope when one or more files failed; a
fully successful batch always returns status == "ok".
{ "command": "config", "status": "ok",
"data": { "config": { /* resolved configuration object */ } } }config getandconfig listemitdata.config(with sensitive values likeai.api_keymasked).config setemitsdata: { "key": "<key>", "value": "<masked-value>" }.config resetemits the samedata.configshape aslistafter resetting to defaults.
The process boundary in subx-cli/src/main.rs guarantees that every JSON-mode
invocation produces exactly one JSON document on stdout, including the
case where clap rejects argv before any subcommand runs:
- Early argv/env sniff. Before invoking clap,
main.rsscansenv::args_os()for--output <value>/--output=<value>and readsSUBX_OUTPUTto compute a tentativeOutputMode. The sniff is permissive and defaults totextwhen ambiguous. Cli::try_parse(), neverparse(). OnErr(clap::Error):- In tentative
textmode, the clap error is rendered as today (preserving help/usage formatting on stderr) and the process exits withclap::Error::exit_code(). - In tentative
jsonmode, a synthetic JSON error envelope is written to stdout witherror.category == "argument_parsing",error.code == "E_ARGUMENT_PARSING",error.exit_codeequal to the clap exit code, anderror.messageequal to the rendered clap error with ANSI styling stripped. The process exits with the clap exit code.
- In tentative
--helpand--versionare exempt. clap surfaces them throughErr(clap::Error)withErrorKind::DisplayHelp,ErrorKind::DisplayVersion, orErrorKind::DisplayHelpOnMissingArgumentOrSubcommandand exit code0. Even in JSON mode, those three kinds short-circuit to clap's own text rendering, exit0, and do not emit a JSON envelope.- Successful parse.
cli::run_with_configreturns a structuredRunOutcome { output_mode, command, result };main.rsuses it to render the final envelope without re-parsing argv.
$ SUBX_OUTPUT=json subx-cli --bogus-flag match ./media{
"schema_version": "1.0",
"command": "",
"status": "error",
"error": {
"category": "argument_parsing",
"code": "E_ARGUMENT_PARSING",
"exit_code": 2,
"message": "error: unexpected argument '--bogus-flag' found\n\nUsage: subx-cli [OPTIONS] <COMMAND>\n\nFor more information, try '--help'."
}
}generate-completion's stdout is, by design, a shell-completion
script and is incompatible with the JSON envelope contract. When
invoked under JSON mode (via --output json or SUBX_OUTPUT=json),
the command refuses to run and emits a top-level error envelope:
$ subx-cli --output json generate-completion bash{
"schema_version": "1.0",
"command": "generate-completion",
"status": "error",
"error": {
"category": "command_execution",
"code": "E_OUTPUT_MODE_UNSUPPORTED",
"exit_code": 1,
"message": "JSON output mode is not supported for 'generate-completion'. Use --output text or omit --output to write the shell-completion script."
}
}No shell-completion bytes are written to stdout in this case. The exit
code is SubXErrorExt::exit_code for SubXError::CommandExecution(_) (currently 1);
the contract pins the helper, not the literal number. In text mode the
existing behavior is unchanged: the completion script is written to
stdout and the process exits 0.
When the active output mode is json:
- Stdout contains exactly one JSON document followed by a single
trailing
\nand nothing else. No ANSI escape sequences, no✓/✗/⚠status symbols, noindicatifprogress-bar frames, no match table, no free-form prose. - Stderr MAY contain structured diagnostic logs emitted via the
tracing/logfacade (gated byRUST_LOG), but SHALL NOT contain free-formeprintln!/println!chatter, JSON envelopes, status symbols, ANSI styling emitted byprint_success/print_warning/print_error, orindicatifprogress-bar frames. In particular, the matcher's🔍 AI Analysis Results:block,Total matches:/Preview:summaries, per-candidate- file_<id>lines, andWarning: Skipping relocation/Warning: Conflict resolution prompt not implementedwarnings are suppressed in JSON mode. ANSI styling on stderr is stripped in JSON mode. - Progress bars are force-hidden via
ProgressBar::set_draw_target(ProgressDrawTarget::hidden()), regardless ofgeneral.enable_progress_bar. --quietadditionally suppresses stderr chatter (tracing, diagnostics) while leaving the stdout envelope intact.
In text mode (default), all of the above behave exactly as in prior releases.
The legacy cache status --json flag (defined on StatusArgs in
subx-cli/src/cli/cache_args.rs) predates the global output mode. It is
preserved as a thin, backward-compatible alias that forwards to the
global JSON renderer:
# These two invocations emit byte-identical JSON envelopes on stdout:
subx-cli --output json cache status
subx-cli cache status --jsonThe alias is limited to cache status; no other cache subcommand
exposes a --json flag, and none will be added. New scripts should
prefer the global --output json form for uniformity across
subcommands.
The recipes below assume jq is on $PATH.
subx-cli --output json match --dry-run ./media \
| jq -r '.data.candidates[0].confidence'jq -r (raw output) prints the integer with no surrounding quotes.
subx-cli --output json convert --format vtt ./subs/ \
| jq -r '.data.conversions[]
| select(.status == "error")
| "FAILED: \(.input) — \(.error.code) \(.error.message)"'The top-level envelope stays status == "ok" on partial success, so
the script keeps running; per-item failures are surfaced via
select(.status == "error").
#!/usr/bin/env bash
set -euo pipefail
out=$(subx-cli --output json sync ./video.mp4 ./sub.srt) || rc=$?
rc=${rc:-0}
status=$(printf '%s' "$out" | jq -r '.status')
if [[ "$status" == "error" ]]; then
code=$(printf '%s' "$out" | jq -r '.error.code')
message=$(printf '%s' "$out" | jq -r '.error.message')
printf 'subx-cli failed: %s — %s\n' "$code" "$message" >&2
exit "$rc"
fiNote that subx-cli always exits with the documented exit code
(1–6 from SubXErrorExt::exit_code), so set -e plus inspecting $?
remains a valid simpler approach when you only care about
success/failure; parsing error.code is for scripts that want to
react to specific categories.
subx-cli --output json cache status \
| jq '.data.pending'Within a major schema version, SubX-CLI guarantees:
- Every documented top-level key is present with the same JSON type and the same semantics.
- Every documented field inside a covered command's
datapayload is present (or marked optional in this document) with the same JSON type and semantics. - Every error
categoryandcodelisted in Category and machine-code table is stable. New categories may be added in minor bumps. error.exit_codecontinues to equalSubXErrorExt::exit_codefor the underlying variant. The numeric exit-code contract (1–6) is preserved across patch releases.OutputModeUnsupportedcontinues to map to categorycommand_executionand codeE_OUTPUT_MODE_UNSUPPORTED.
What MAY change in a minor bump (1.x) without breaking consumers:
- New optional fields in
datapayloads. - New optional fields in
error.details. - New optional top-level keys (e.g., a future top-level
errors[]array mirroring per-item failures). - New error categories or codes for previously-unmapped error paths.
- Richer payload schemas for
translateandconfig(currently minimal in1.0).
What requires a major bump:
- Renaming or removing any documented field.
- Changing the JSON type of any documented field.
- Restructuring the envelope (e.g., dropping
dataorerror).
- OpenSpec capability:
openspec/changes/archive/2026-04-26-add-machine-readable-output/specs/machine-readable-output/spec.md - Error-handling additions:
openspec/changes/archive/2026-04-26-add-machine-readable-output/specs/error-handling/spec.md - Per-command additions: the
cache-management,encoding-detection,format-conversion,progress-reporting,subtitle-matching, andtimeline-syncspec files under the same change directory. Of the capabilities named in this section,error-handling,cache-management,encoding-detection,format-conversion,subtitle-matching, andtimeline-syncare now split capabilities: each exists in two halves along the crate boundary, with the core half insubx-core/openspec/specs/and the CLI half inopenspec/specs/(record inopenspec/split-capabilities.txt).progress-reportingandmachine-readable-outputstay wholly insubx-cli. - Source of truth for the envelope and error mapping:
subx-cli/src/cli/output.rs(envelope, renderer, schema-version constant)subx-core/src/error.rs(SubXError::category,machine_code,hint— the library-half machine contracts)subx-cli/src/cli/error_ext.rs(SubXErrorExt::exit_code,SubXErrorExt::user_friendly_message— the binary-half presentation contract; callers import the trait)
- Command reference:
command-reference.md - Configuration reference:
configuration-guide.md