Machine Output¶
GWZ has three script-oriented output modes:
--jsonrenders one structured JSON response.--jsonlrenders newline-delimited JSON records for streaming operation consumers.gwz status --porcelainrenders stable path-oriented status text.
--json and --jsonl are mutually exclusive. status --porcelain cannot be
combined with either machine output flag.
JSON Response¶
Most commands render a response object:
{
"kind": "response",
"meta": {
"request_id": "req-...",
"schema_version": "gwz.protocol/v0",
"action": "Status",
"aggregate_status": "Ok",
"operation_id": "op-...",
"message": null
},
"members": [],
"errors": [],
"workspace_git_status": null
}
Member entries include:
{
"member_id": "gwz-cli",
"member_path": "gwz-cli",
"source_kind": "Git",
"status": "Ok",
"error": null,
"planned": null,
"state": null,
"git_status": null,
"lock_match": null
}
Member entries from clone and materialize also carry url_resolution,
described below.
Errors use:
{
"code": "MemberNotFound",
"message": "unknown member",
"member_id": null,
"member_path": null,
"target_kind": null,
"detail": null,
"record_context": null
}
Top-level CLI errors in --json or --jsonl mode keep the same response shape,
with meta: null, no members, and one error entry. Per-member failures retain
member_id, member_path, and target_kind: "Member" even when preflight
rejects the whole operation before a normal response exists.
url_resolution¶
Every member entry that clone or materialize cloned carries
url_resolution:
{
"url_resolution": {
"manifest_url": "git@github.com:owner/repo.git",
"effective_url": "https://github.com/owner/repo.git",
"scheme": "https",
"source": "request",
"derived": true,
"host_known": true
}
}
scheme:manifest,ssh, orhttps.source:requestfor--url-schemeorGWZ_URL_SCHEME,workspacefor the workspace's remembered preference in.gwz/url-scheme.yml,defaultotherwise.derived: true only wheneffective_urldiffers frommanifest_url.host_known: true for github.com, gitlab.com, and bitbucket.org.
Members that were only checked out, not cloned, have url_resolution: null.
Human output prints one summary line when the scheme is not manifest:
The parenthesis reports from --url-scheme, from GWZ_URL_SCHEME, or
from .gwz/url-scheme.yml. With --verbose, each converted member also gets
one manifest-url -> effective-url line.
meta.transport¶
meta.transport is an optional list of rows, one per remote authentication
attempt. Each row has:
repository_path: the repository that made the attempt.remote: the remote name used.operation:clone,fetch,push, orread_advertisement.credential_method:unknown,file,agent, orhelper.selection_source:ambient,invocation_remote,invocation_default, orlocal_configuration.credential_offered: boolean.authenticated: true, false, or null when no credential decision was observed.public_key_fingerprint: string or null.
--verbose prints these rows as human lines.
Commit Log Output¶
gwz log uses a dedicated finite record stream rather than the generic
operation-response envelope. JSON wraps the records with schema
gwz.log/v0:
{
"records": [
{
"author": {
"email": "ada@example.test",
"name": "Ada",
"time": { "offset_min": 600, "time": 1788134400 }
},
"committer": {
"email": "ada@example.test",
"name": "Ada",
"time": { "offset_min": 600, "time": 1788134400 }
},
"members": [
{
"hash": "0123456789abcdef0123456789abcdef01234567",
"member_id": "mem_api",
"member_path": "services/api",
"parents": ["89abcdef0123456789abcdef0123456789abcdef"]
}
],
"provenance": "marker:<uuid-v7>",
"record": "entry",
"subject": "Update the shared API"
},
{
"member_id": "mem_web",
"member_path": "services/web",
"message": "revision was not found",
"operand": "topic..HEAD",
"reason": "revision_unresolved",
"record": "degradation"
}
],
"schema": "gwz.log/v0"
}
The literal provenance vocabulary is:
none— one uncoalesced commit;heuristic— compatible unmarked commits coalesced by the conservative message, author, and time rule;marker:<uuid-v7>— commits coalesced by a valid coordinated-commit marker;marker-invalid— a marker-like claim was present but invalid or malformed; the entry is a singleton and never joins a heuristic group.
body is present only when --body is requested. lossy is present only as
true, when invalid Git bytes required U+FFFD replacement. Member ordering and
parent hashes are stable and complete.
Degradation reason is one of repository_unreadable, repository_missing,
unborn, revision_unresolved, snapshot_entry_missing,
lock_entry_missing, or unsupported_source_kind. The optional operand and
message fields may be null.
JSONL begins with a schema header and then emits the same entry and degradation objects one per line:
{"record":"header","schema":"gwz.log/v0"}
{"provenance":"heuristic","record":"entry","subject":"Update API and web"}
The second line above is abbreviated for readability; real entry records retain
the complete author, committer, member, and timestamp fields shown in the JSON
example. See gwz log for limits, ranges, filters, and exit
status behavior.
Durable merge-record compatibility failures populate record_context instead
of requiring message parsing:
{
"merge_id": "merge_example",
"schema": "gwz.merge-operation/v1",
"record_schema_version": 1,
"required_wave": "A1",
"legacy_mode": null
}
required_wave is null for a genuinely unknown schema/version pair. It is
A1, A2, A3, or A4 for an allocated pair that requires a newer semantic
wave. When the envelope itself is malformed and its header cannot be read,
record_context is null.
Merge JSON¶
Merge responses use the normal response envelope and populate its merge
field. JSON and JSONL expose the complete merge protocol shape, including the
current finalization step:
{
"merge": {
"merge_id": "merge_example",
"state": "Finalizing",
"open": true,
"participant_counts": {
"total": 1,
"planned": 0,
"up_to_date": 0,
"fast_forwarded": 1,
"merged": 0,
"conflicted": 0,
"failed": 0,
"unattempted": 0,
"continued": 0,
"aborted": 0,
"rolled_back": 0
},
"repos": [],
"operation_drift": [],
"preservation": null,
"publication_step": "PublishingCandidate",
"record": {
"source_version": "V1",
"archived": false,
"terminal_outcome": null,
"acceptance": {
"kind": "SupportedPersisted",
"supported_persisted": {
"kind": "V1",
"v1": {
"operation_baseline_lock_sha256": "sha256...",
"metadata_base": {
"source": "OperationBaseline",
"source_commit": null,
"manifest_yaml": "...",
"manifest_sha256": "sha256...",
"lock_yaml": "...",
"lock_sha256": "sha256..."
},
"lock_yaml": "...",
"lock_sha256": "sha256...",
"members": [],
"root": {
"kind": "BornAttached",
"commit": "abc123",
"symbolic_branch": "main",
"publication_branch": "main",
"lock_worktree_sha256": "sha256...",
"manifest_worktree_sha256": "sha256...",
"lock_commit_sha256": null,
"manifest_commit_sha256": null
}
}
},
"legacy_complete": null,
"legacy_source": null,
"legacy_evidence": null,
"missing_gaps": []
},
"recovery": null
}
}
}
record is present on every successful response tied to a durable merge
record: start after creation, open or archived status, continue,
preserve-abort, abort, and id-qualified GC. It is null for dry-run, idle
status, pre-record responses, and unqualified GC. A command failure uses the
top-level error envelope and therefore carries record_context, not a partial
record projection.
source_version is V0 or V1. This build writes V1 for every merge, so
V0 appears only on an archived record written before 0.14; a pre-0.14 record
that is still open is refused rather than projected, and no response carries
it. archived is true only when status was read from immutable archive bytes;
only an archived projection has terminal_outcome (Completed or Aborted).
An archived projection always carries acceptance and always has null
recovery — an archive is history, not a resumable state. An archived V0
record's acceptance uses the legacy shapes below (LegacyComplete,
LegacyUnavailable or NotAccepted) because its bytes predate the persisted
acceptance record; nothing converts them into one. An open projection is the
mirror image: null terminal_outcome, null acceptance until the merge has
accepted a workspace, and null recovery until a recovery context is
recorded.
Archive-only status does not inspect repositories. Its repos and
operation_drift arrays are empty, and the immutable terminal and acceptance
history is reported through record.
Acceptance has one of four exclusive shapes:
SupportedPersistedusessupported_persisted.kind: "V1"and its completev1payload.LegacyCompleteuseslegacy_completeandlegacy_source(CandidateorBaselineNoPublication).LegacyUnavailableuseslegacy_evidenceplus a sorted, nonemptymissing_gapslist. Gap values areExactLockBytes,CompleteMemberAudit,AcceptedRootInput, andPublicationEvidence.NotAcceptedhas no payload or gaps and applies only to an aborted archive that never accepted a workspace.
Accepted member rows use kind Selected, UnselectedPresent, or Absent.
A selected row contains integration, final_checkout, and lock_member; an
unselected-present row contains only lock_member; an absent row contains no
payload. Root kind is BornAttached, BornDetached, or UnbornAttached.
Optional fields remain explicit JSON nulls and repeated fields remain arrays,
including when empty.
When a durable record is in RecoveryRequired, record.recovery reports its
literal origin_state, record-derived base_phase, the current
next_action: "ReportRecoveryRequired", and the action a resume would take in
resume_action. This projection is diagnostic: status does not reconcile or
rewrite the record.
Repository rows include their target, source, branch, before/resulting/live
commits, lifecycle state, prediction, conflicts, eligibility flags, structured
participant drift, an optional structured error, and an optional
pending_action. A pending action contains its kind, reconciliation state
(NotStarted, ExpectedConflict, CompletedExactly, or Ambiguous), and a
guidance message. Merge errors use the same seven-field shape as envelope
errors, including target_kind and record_context. Operation drift entries
contain kind and message.
Preservation entries contain target_id, path, backup_ref,
backup_commit, stash_id, and stash_object_id.
Preservation remains null until that later feature is available. Publication
steps are populated while finalization is open and end at Complete. Consumers of these versioned envelopes must tolerate additive keys while continuing to
validate the keys they understand.
Participant drift distinguishes advanced, rewound, and diverged heads, missing
recorded objects or repositories, exact native-merge mismatches, and foreign
integration/sequencer state. Status carries member context and expected/live
evidence for these cases rather than returning a memberless backend error.
An ambiguous pending action also emits the dedicated
PendingActionAmbiguous drift kind and blocks both continue and abort until a
fresh exact classification succeeds.
MergeOperationState includes the append-only Idle value used by the
read-only merge-status lifecycle when no coordinated merge is open. An idle
response has no merge id, participants, or drift and does not fabricate a
completed operation. Its record field is null.
crash_recovery¶
merge.crash_recovery reports the start-time crash-recovery decision — whether
the workspace volume can prove the durable filesystem identity GWZ needs to
reconstruct an interrupted start. It is the machine truth for that decision;
consumers never need to parse the human warning off stderr.
{
"merge": {
"crash_recovery": {
"supported": false,
"filesystem": "btrfs",
"gap": "NoDurableIdentity",
"handles_ok": true
}
}
}
supported—truewhen the volume proved identity and the merge records its artifacts as before;falsewhen the merge proceeds without that recording. Afalsedecision is not a failure: the response is still a success, and the merge ran.filesystem— the name of the filesystem behind the decision.nullwhensupportedis true, andnullbelow the bar when the filesystem cannot be named.gap— why identity could not be proved, andnullwhensupportedis true. One ofNoDurableIdentity,RemoteFilesystem,VolatileFilesystem.handles_ok— whether the volume proved persistent file handles.nullwhensupportedis true, because a handle failure above the bar is an anomaly rather than a capability the merge plans around.trueon a below-bar volume whose handles work.falseon a below-bar volume whose handles do not — there the merge record is written raw and the merge still runs, but a selected-root or--preserveabort may refuse until the workspace is on a handle-capable volume. Plain participant-only abort is unaffected.
filesystem, gap and handles_ok are always present as keys and carry
an explicit null when the decision has no value for them. Only the
crash_recovery object as a whole is omitted, on any response that made no
such decision: --abort, --status, --gc and dry run. Present means a
decision was made, not that crash recovery is available; read supported for
that.
The Rust and Python driver tests compare semantic JSON values with the single
canonical fixture at
gwz-core/protocol/fixtures/cli_parity/merge_response.json. Driver development
checkouts therefore retain the usual sibling gwz-core layout; both drivers
already require that checkout through their development path dependency. The
fixture is test-only and is not read by an installed driver at runtime.
It includes both an envelope error and an error-bearing failed repository row,
so cross-driver parity covers the complete structured error sub-shape.
JSONL Stream¶
--jsonl streams event records as an operation runs, then the final render path
prints the response object. Event records have this shape:
{
"kind": "event",
"operation_id": "op-...",
"request_id": "req-...",
"sequence": 1,
"timestamp_ms": 0,
"event_kind": "MemberProgress",
"severity": "Info",
"member_id": "gwz-cli",
"member_path": "gwz-cli",
"message": null,
"member": null,
"error": null,
"attribution": null,
"target_kind": "Member",
"merge_state": null,
"merge_member": null,
"artifact_path": null,
"progress": {
"phase": "Receiving",
"received_objects": 10,
"total_objects": 20,
"received_bytes": 1024,
"indexed_deltas": null,
"total_deltas": null
}
}
Merge JSONL uses the same event envelope. Each invocation emits operation
start/finish events. Actionable participants emit member start/finish events;
MemberFinished carries the durable merge participant outcome in
merge_member. Verified operation-record and evidence writes emit
ArtifactWritten with artifact_path. Lifecycle transitions carry
merge_state. Participant outcome and state-change events are emitted only
after their corresponding durable write succeeds. A merge start on a volume
that cannot support crash recovery also emits one Diagnostic event with
severity: "Warn", no member, and the warning text in message; the same
decision is in the response's crash_recovery object, which is the field to
read rather than the event.
After successful finalization verification, the stream reports the composition evidence in this order:
git:@root/<commit>for the checked root evidence commit;gwz.conf/markers/<id>.yamlfor the merge marker;gwz.conf/gwz.lock.ymlfor the accepted lock; and.git/info/excludefor the local workspace boundary;gwz.conf/markers/conf-integrity.ymlwhen the composition includes the configuration integrity marker. Older saved candidates retain the original four-artifact sequence.
These events describe verified publication. Recovery may report them again when it re-verifies a publication whose prior process stopped before terminal completion.
Both drivers emit merge events as they occur rather than buffering them until
the operation finishes. After OperationFinished, the stream contains exactly
one final kind: "response" object. A failed invocation retains any events
already emitted and ends with one structured error response. Event-stream
completion is not published until that final response, or its structured
failure, is available to the driver.
The Rust and Python event serializers compare against the shared
gwz-core/protocol/fixtures/cli_parity/merge_event.json fixture. This pins the
merge-member outcome and artifact fields to the same JSONL shape in both
drivers.
Progress event frequency is controlled by:
Use --progress-interval 0 to emit every update.
Listings¶
Read-only listing commands render listing objects with --json or --jsonl.
gwz --json ls:
{
"kind": "members",
"entries": [
{
"id": "gwz-cli",
"path": "gwz-cli",
"abspath": "/work/gwz-dev/gwz-cli",
"materialized": true,
"note": null
},
{
"id": "mem_secret",
"path": "repos/secret",
"abspath": "/work/gwz-dev/repos/secret",
"materialized": false,
"note": "private, skipped"
}
]
}
materialized is observed, not the lock's claim: the lock records the member
and its directory exists. note is an explicit null on every ordinary row,
and human text on a row where the two disagree — private, skipped for a
private member gwz clone was refused access to and quietly skipped, otherwise
recorded in the lock but absent on disk. Switch on materialized; the note is
for a person to read. See gwz ls.
gwz --json tag --list:
gwz --json snapshot --list:
{
"kind": "snapshots",
"entries": [
{
"name": "before-refactor",
"created_at": "2026-06-25T00:00:00Z",
"created_by": "user",
"members": 3
}
]
}
gwz --json local list renders the local clone family under
local_family_members, with the family root's path beside the rows under
local_family_root_path; see the
gwz local command page.
Each row carries name, kind, recorded_state, observed_state, path,
last_error and owner. owner is the opaque token a
gwz local clone --owner <token> recorded on that row, reported verbatim and
never interpreted by gwz; it is null for the root, for a row created without
--owner, and for every row of a family index still in format 1. It is
written once, by the index write that reserves the row, and no command
changes it afterwards. path stays relative to local_family_root_path.
Push JSON¶
gwz --json push renders one member entry per selected repository, the root
included. A repository with nothing to push has status Noop, planned.action
Noop and its reason in planned.message; a dry run reports the repositories
it would not contact the same way. Abbreviated to those fields:
{
"member_id": "@root",
"member_path": ".",
"status": "Noop",
"planned": {
"action": "Noop",
"from_ref": "abc123",
"to_ref": "refs/heads/main:refs/heads/main",
"message": "up to date with origin/main as of the last fetch or push"
}
}
The reason names the remote and branch:
already on <remote>: this push read the remote, and its branch was already at the local commit.up to date with <remote>/<branch> as of the last fetch or push: the branch equals its remote-tracking ref; the remote was not contacted.behind <remote>/<branch> as of the last fetch or push: the branch is behind its remote-tracking ref; the remote was not contacted.
The aggregate is Noop when nothing was pushed and nothing was refused, and a
Noop aggregate exits 0. A push that contacted no remote has no
meta.transport. JSON tells a checked Noop from an assumed one only by the
reason text: only already on <remote> was read in this operation. A script
that needs every result checked against its remote runs
gwz --json push --check-remotes, which gives no as of the last fetch or push
reasons.
This changed after gwz 1.0.12, which reported every row of a push with nothing
to publish, and its aggregate, as Ok. A script that treats only Ok as
success must also accept Noop.
Fetch JSON¶
gwz --json fetch renders the ordinary member entries plus a fetch_repos
array, one object per selected repository in the same order. The key appears on
a fetch response and on no other response.
{
"kind": "response",
"meta": { "action": "Fetch", "aggregate_status": "Ok" },
"members": [ { "member_id": "mem_core", "status": "Ok" } ],
"fetch_repos": [
{
"member_id": "mem_core",
"member_path": "gwz-core",
"source_kind": "Git",
"result": "Updated",
"remote": "origin",
"branch": "main",
"before": "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678",
"after": "9f8e7d6c5b4a39281706f5e4d3c2b1a098765432",
"upstream": "refs/remotes/origin/main",
"ahead": 0,
"behind": 3
}
]
}
result is the field to switch on:
Updated: the remote-tracking ref moved.beforeandafterare its object ids either side of the fetch;beforeis absent when the ref did not exist yet.Unchanged: the repository was contacted and its tracking ref did not move. This is a successful read, not a skip. Outside--dry-run,gwz fetchnever skips the network, andUnchangedalways means contacted.NoUpstream: the repository has no fetch remote, or no attached branch whose tracking ref could move.remote,beforeandaftermay all be absent. It is a reported row, not a failure.Failed: the remote refused, or the fetch errored. The reason is on the matchingmembersentry'serror, where every other verb puts it.Planned:--dry-runonly. The repository would have been contacted and was not, so the row carries no result from any remote. A live fetch never produces it.
Telling a dry run apart¶
gwz --json --dry-run fetch resolves the selection and returns before the
network. Every repository it would have contacted carries "result":
"Planned", its members[] entry carries "status": "Planned", and upstream,
before, after, ahead and behind are all null:
{
"member_id": "@root",
"member_path": ".",
"source_kind": "Git",
"result": "Planned",
"remote": "origin",
"branch": "main",
"before": null,
"after": null,
"upstream": null,
"ahead": null,
"behind": null
}
A repository with no fetch remote is still NoUpstream under --dry-run,
because that answer needs no network. So Planned is the one value that tells a
consumer the report is a plan; a consumer that treats Unchanged as
contacted-and-answered stays correct, because a dry run never returns it.
ahead and behind count the repository's current branch against after,
after the fetch. They are absent when there is nothing to count against.
Aggregates follow gwz push's exit codes with one difference worth knowing:
because Unchanged means contacted-and-answered rather than skipped, a batch
in which one remote failed and every other repository read cleanly is Partial
(exit 1, the report is incomplete) even though no row is Ok. Rejected
(exit 2) means every row was refused before the network and nothing was
contacted; a --remote <name> no selected repository has is the ordinary way
to reach it. Under --dry-run a Planned row counts as contacted for the
aggregate, so a dry run and the live run of one selection aggregate alike.
gwz fetch never integrates and never writes a workspace artifact, so a
fetch_repos row never implies a branch, HEAD, index or lock change.
Status JSON¶
gwz --json status includes workspace_git_status:
{
"clean": false,
"root_status": {
"branch": "main",
"detached": false,
"head": "abc123",
"staged": 0,
"unstaged": 1,
"untracked": 0,
"dirty": true,
"unborn": false
},
"root_file_changes": [],
"file_changes": [],
"branches": [],
"branch_groups": [],
"branch_differences": []
}
File change entries use repo_path, workspace_path, index_status,
worktree_status, and original_repo_path.
Status Porcelain¶
Use porcelain for stable path-oriented status text:
Output is similar to Git status porcelain, with workspace-relative paths:
If no file changes are available but members have non-OK status, porcelain
prints !! <member-path> lines.
Forall¶
gwz forall rejects --json and --jsonl. It inherits child process stdio and
streams child output directly, so GWZ does not wrap that output in machine
records.
For machine-readable member selection, combine gwz --json ls with external
tooling rather than forall --json.
Exit Codes¶
GWZ maps aggregate status to process exit codes:
| Aggregate status | Exit code |
|---|---|
Accepted, Ok, Noop, Dirty |
0 |
Partial, Failed, Conflicted |
1 |
Rejected |
2 |
Argument parsing and top-level CLI construction errors also exit non-zero.