Releases¶
GWZ releases are distributed through GitHub Releases for the gwz-cli
repository:
https://github.com/owebeeone/gwz-cli/releases
The hosted documentation is built from
the tag of the most recently published release, so its command model matches
the released CLI rather than unreleased work on main.
1.0.17: gwz fetch, and lanes that dispose clean¶
1.0.17 (2026-09-18) adds one read-only network verb and finishes the lane disposal clean-up that 1.0.14 and 1.0.16 began.
gwz fetch¶
gwz fetch contacts every selected repository's configured remote, updates
that repository's remote-tracking refs, and prints one row each: the tracking
ref before and after, how far the current branch is ahead of and behind it, or
new <after> for a tracking ref that did not exist before, or updated,
no change, no upstream, failed. Plain gwz fetch covers @root plus the
configured members, and the selectors are gwz push's. It answers "what moved
upstream while I was working?" across the whole workspace in one pass, without
changing a file.
$ gwz fetch
status: Partial
@root . no change (origin/main, +2 -0)
mem_core gwz-core 90ef552..330a174 (origin/main, +0 -3)
mem_cli gwz-cli no change (origin/main, +0 -0)
mem_local local no upstream
mem_priv private failed RemoteRejected: member 'mem_priv' at 'private': failed to connect to 127.0.0.1: Connection refused
What it never does:
- It never integrates. No merge, no rebase, no fast-forward, no reset: no
branch, no
HEAD, no index, no working-tree file. Usegwz pullto integrate what a fetch showed you. - It never writes workspace artifacts. No lock, no manifest, no boundary
sync. Because of that it still runs while a coordinated merge is open,
unlike
pullandpush. - It never prunes.
- It never skips the network, with one exception:
--dry-run. There is no--check-remotesand no "unchanged since the last fetch" short-circuit as there is ongwz push: a fetch that does not connect has answered nothing.--dry-runis not git's.git fetch --dry-runcontacts the remote and declines to write;gwz --dry-run fetchcontacts no remote at all, resolving the selection and printing the planned rows.
A planned row carries a token of its own, so it can never be read as an answer:
$ gwz --dry-run fetch
status: Noop
@root . would contact origin
mem_core gwz-core would contact origin
mem_cli gwz-cli would contact origin
mem_local local no upstream
mem_priv private would contact origin
would contact <remote> is "result": "Planned" in the machine output, and no
live fetch prints either. no change and Unchanged keep their one meaning:
the repository was contacted and its tracking ref did not move. A repository
with no fetch remote is still no upstream under --dry-run, because that
answer needs no network.
Exit codes follow gwz push, with one difference worth knowing: no change
means contacted-and-answered rather than skipped, so a run in which one remote
failed and every other repository read cleanly exits 1, because the report is
incomplete, even though nothing moved.
| Exit | Meaning |
|---|---|
0 |
every selected repository answered |
1 |
some answered and some failed; the report is incomplete |
2 |
every selected repository was refused before the network, so nothing was contacted |
Exit 2 is the whole-batch refusal, for example --remote <name> naming a
remote no selected repository has; a refusal before there is a selection (no
workspace, an unknown member id) is a typed error and exits 1 as on every
verb. A dry run shares these codes with the live run it rehearses, and refuses
a --remote name a repository lacks just as the live run does.
The global --remote <name> selects the remote each selected repository
contacts, and --json carries the rows under fetch_repos.
Not offered yet. This release is the verb and nothing more. --prune,
--tags, per-remote selection and multi-remote fetch for a member that has
more than one remote are all planned and none is implemented; this note does
not say when. See gwz fetch.
Lane disposal: an integrated lane needs no waiver¶
gwz local dispose <name> now succeeds, with no --force and no operator
comparison, on a lane whose work the surviving family already holds, even one
that was built in. Disposal compares the lane against the family, and against
the record the clone wrote of what it copied, so the caches, ignored user data
and stash and reflog entries a verbatim copy inherits are reported without
refusing. What refuses is what only the lane holds.
A refusal sorts what it found into four categories, printing each with its count and with a description of what it holds, the empty ones included. Most entries are paths; a protected root is described by its object id and the head or ref that reaches it:
- regenerable: a tool made it and the same tool remakes it. Never refuses.
- unchanged copy: the clone copied it, the lane has not touched it, and a surviving member still holds it. Never refuses.
- changed copy: the clone copied it and it is not the family's any more. Refuses.
- unique to the lane: the lane alone holds it, including any protected root no single surviving family repository preserves whole. Refuses.
The refusal then prints the exact --force <hazard,...> command that waives
exactly what it found, and names nothing more: a lane whose only refusing entry
is dirt is offered --force dirty even when the same report lists regenerable
entries and unchanged copies beside it.
Regenerable is recognised by marker and by shape, never by a directory's
name. A directory holding a CACHEDIR.TAG that begins with the Cache
Directory Tagging Specification's signature line; a __pycache__/ holding
nothing but .pyc and .pyo files; an *.egg-info/ holding a PKG-INFO; a
bazel-* or razel-* symlink whose target lies outside the workspace; a file
ending .so, .pyd or .dylib inside a worktree; and an untagged build
directory proved by its tool's own markers (cargo's .rustc_info.json, a
debug/.fingerprint beside a debug/deps, a pyvenv.cfg). A target with no
cargo marker in it is not regenerable, and neither is anything a probe cannot
read. Recognition does not consult the copy record, so a cache the lane rebuilt
or created from nothing is still a cache.
A forced deletion now reports what it was actually forced past, not the
names you typed. A waiver that covered a refusing entry is listed after
forced past:; a waiver you named that covered nothing is listed after
unused waiver:, so an over-broad --force says so in its own report.
The four categories and the exact waiver command shipped in 1.0.16 with
regenerable always empty. 1.0.17 fills it. See
Local Clones and gwz local.
Catching up: 1.0.14 and 1.0.16¶
No release notes were written for 1.0.14 (2026-09-18) or 1.0.16 (2026-09-18). What they carried:
- Claude Code worktree hooks.
gwz hook claude-code worktree-creategives a Claude Code session started with--worktreea lane, a local clone of the whole workspace, instead of thegit worktreeClaude Code would have made, which in a GWZ workspace has no members.gwz hook claude-code worktree-removedisposes that lane throughgwz local dispose, with GWZ's own refusals intact and never--forceor--keep.gwz hook claude-code setupprints or writes the settings block (--project,--project --localor--user, with--write) and--removetakes it back out, changing no byte outside the block. Outside a GWZ workspace the same hooks make and remove the plain worktree Claude Code would have made, so the block can live in user-level settings without changing other projects. See Claude Code andgwz hook. --owner <token>and--wait <secs>on the family verbs.gwz local clone --owner <token>records an opaque caller token, up to 128 bytes of[A-Za-z0-9._:-], on the new member row, in the same index write that reserves the row. It never changes afterwards,gwz local listreports it, and GWZ never interprets it: it is the caller's identity for the caller's own reuse decisions.--wait <secs>makes a busy family lock retry until the deadline instead of refusingBusyat once, on every family verb and ongwz merge --remote <name>; a wait that wins rereads the index before acting.- Family index schema v2. Recording an owner needs a place to put it, so
the index carries
gwz.local-family/v2: v1 plus the optional per-rowowner. A 1.0.14 or later gwz reads a v1 index unchanged and writes v2 on its first write of any kind, a create, a dispose, a--keepor a family merge. Going the other way is a refusal rather than a downgrade: an older gwz refuses a v2 index as a whole, and its refusal names no gwz version at all. On 1.0.13 it readsgwz: ManifestInvalid: ... .gwz/local-family.yml is malformed: `schema: gwz.local-family/v2` is not `gwz.local-family/v1` (format version 1); this file is not in a format this store reads. That line is derived from the 1.0.13 source (v1.0.13:crates/family-store/src/format.rs:37-42), not from a run on this machine. It calls the file malformed, which it is not, and offers no remedy; the remedy is to upgrade. Once a 1.0.14 or later gwz has written a workspace's family index, every gwz used on that workspace must be 1.0.14 or later. - Lane disposal clean-up, Phase 1.
gwz local clonerecords what it copied per repository, andgwz local disposeuses that record so an unchanged copy the family still holds is reported instead of refusing. Where no record exists, because the lane came from an older gwz or was copied outside gwz, dispose makes the comparison against the family itself. The identical-copy witness settledunpreserved-historyfor a merged lane: a copy the surviving family holds object for object is not a loss. 1.0.16 completed the phase with the four-category refusal and the exact waiver command described above. gwz lssays why a listed member is not on disk. A member the lock records but that is absent is still listed, rather than quietly hidden, withmaterialized: falseand a human note:(private, skipped)for the quiet-clone case,recorded in the lock but absent on diskfor any other. Members the lock never materialized are unchanged, omitted unless--unmaterializedand carrying no note. Readmaterialized, not the note, to decide anything. Seegwz ls.
1.0.4: the 1.0 line ships¶
1.0.4 (2026-09-08) is the first published release of the 1.0 series — the 1.0.0 release candidates and 1.0.2/1.0.3 were pre-publication and packaging iterations from the same morning. If you are upgrading from 0.14 or earlier, read this section together with the lane-recovery notes below.
The 1.0 series delivers the local clone family (gwz local): a verbatim,
isolated second copy of the whole workspace — its own repositories, its own
runtime state, its own merges — created with gwz local clone NAME, integrated
back by name with gwz merge --remote NAME, and disposable only when its
history provably survives elsewhere. See Local Clones for the
lane workflow and measured disk/speed data
(on reflink filesystems, ten whole-workspace lanes cost ~20.5 MiB; on ext4 the
same ten cost ~537 MiB as ordinary copies).
Release verification for 1.0.4 (2026-09-08) covered install, workspace clone, local clone, detach, and delete on Linux ARM64 and Windows x86_64, alongside the release pipeline's own gates.
1.0.0: lane recovery¶
This release makes the ordinary lane cycle work with one default merge:
gwz local clone A
# Work and use gwz add / gwz commit in A.
gwz merge --remote A
gwz local dispose A
- Merge selection changes: ordinary and family merges include the workspace
root and active members by default. To merge members only, use
gwz --target @all --no-target @root merge. An explicit partial merge can leave root history unpreserved and prevent lane disposal. - Untouched clones can be disposed despite a regenerated configuration marker. Only freshly verified generated bytes are discounted; actual edits and unique history remain protected. The marker can still appear modified in status.
- Network publication captures source refs before transfers and withholds the root if selected member publication fails or locked dependencies cannot be proved available. Earlier member pushes may already have succeeded; repair the reported failure and retry.
- SSH file selection is explicit through
--identity PATHor--remote-identity NAME=PATH, without agent fallback for the selected file. Exact selection of an encrypted key inside an agent remains unsupported. - Only verbatim local clones are supported.
--clean,--bare,--from, and their branch option remain refused, as do family pull/push. Use the receiving workspace'smerge --remote NAMEto integrate a lane.
Windows publication and fresh-lock verification, Windows/Linux lane lifecycles, and Linux ARM64 ext4 operation have focused acceptance evidence. XFS supports space-efficient CoW copies; ext4 uses independent ordinary copies. See Local Clones for measured scope.
The older open-merge upgrade restriction below still applies.
Upgrading To 0.14.0¶
Close any open coordinated merge before you upgrade. GWZ 0.14 has one merge
implementation, and it cannot act on a merge record written by 0.13.x or
earlier. On 0.13.x, run gwz merge --status, then finish the merge with
gwz merge --continue or undo it with gwz merge --abort. Only then install
0.14.
If a pre-0.14 merge is still open after the upgrade, GWZ says so and stops. Every merge verb, and every command a merge blocks, refuses with one sentence:
gwz: OpenOperation: this is a pre-0.14 merge; use gwz 0.13.0 (the last release before 0.14) to continue or abort
--status, --continue and --abort all refuse alike, so the usual
open-merge advice does not apply and is deliberately not printed. Nothing is
wrong with the record and nothing needs repairing: reinstall a 0.13.x build,
close the merge with it, and return to 0.14. There is no conversion step and no
in-place upgrade for an open record.
Merges that were already closed are unaffected. Archived records still project
read-only through gwz merge --status <merge-id>, and gwz merge --gc never
deletes an archive it cannot read.
Starting a merge is otherwise unchanged. Ordinary, --ff-only,
custom-message and --no-ff starts now all write the same coordinated merge
record, so status, continue, abort and recovery behave identically whichever
way a merge was started.
Compatibility Notes¶
Behaviour a consumer of GWZ's output or metadata has to account for. Every
note here describes released behaviour: the log, merge, anchor-directory
and gwz.conf/ notes below all shipped across the 0.10 to 1.0 releases.
Unreleased work is described in the version section it will ship in, not here.
gwz logadds one newest-first history across the workspace root and selected member repositories, with coordinated-marker and conservative heuristic coalescing, revision/snapshot/lock ranges, six filters, compact and full human rendering, and the dedicatedgwz.log/v0JSON/JSONL record schema. The Python API andgwz-py logexpose the same records and rendering contract.- Structural workspace commands refuse uncommitted hand edits to
gwz.conf/(the machine-managed manifest and lock). The refusal names the sanctionedgwz repoverbs and the acceptance path (gwz init --update --force); states produced by git itself — clone, pull, branch switch — reconcile silently, and read-only/list commands never gate. Workspace bootstrap also emits a machine-managed banner ongwz.yml, records digests ingwz.conf/markers/conf-integrity.yml, and writes or merges a.claude/settings.jsondeny rule (Edit(/gwz.conf/**)) for agent sessions started at the workspace root. - A checked-artifact private anchor directory holding a foreign,
non-canonical retired-anchor rendering (for example
.ca1-anchor-retired-007) now refuses operations on that family until the foreign file is removed; such names were previously adopted silently. The canonical rendering is unpadded (.ca1-anchor-retired-7). - First-class merge JSON and JSONL include the complete current merge-response
key set, including finalization progress. Structured errors include
target_kindand retain member id/path context even for whole-operation preflight failures. Durable record compatibility errors also include typedrecord_contextrather than encoding merge id, schema/version, required wave, or legacy mode only in prose. Consumers must tolerate additive keys within these versioned envelopes. - Merge status rows expose durable pending-action reconciliation as
NotStarted,ExpectedConflict,CompletedExactly, orAmbiguous. Ambiguity is also reported as dedicated structured drift and remains mutation-blocking. - Merge and
pull --sync mergereject source and target commits with unrelated histories, matching Git porcelain. GWZ does not implicitly allow unrelated histories. - While a coordinated merge is open, the accepted workspace lock remains the exact pre-merge baseline. Clean, conflicted, failed, and unattempted outcomes are retained in the local durable operation record rather than published as a partial composition.
- Merge commits use the quoted default message
Merge '<source>' into '<target-branch>', or the body supplied with-m, with mandatoryGWZ-Merge-IDandGWZ-Operation-IDidentity lines. The exact final message is frozen before mutation and survives restart and conflict resolution. A request that creates a commit supplies its author/committer identity when present; otherwise the target repository identity is used. - Coordinated merge start, dry-run, status, continue, and safe abort are
available together. Successful changed merges publish a checked root
composition commit; interrupted finalization resumes idempotently.
Recovery must not substitute raw
git merge --abortfor the coordinated operation.
Install Latest¶
macOS or Linux:
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/owebeeone/gwz-cli/releases/latest/download/gwz-installer.sh | sh
Windows PowerShell:
powershell -NoProfile -ExecutionPolicy Bypass -Command "iex (irm https://github.com/owebeeone/gwz-cli/releases/latest/download/gwz-installer.ps1)"
Install A Pinned Version¶
Replace latest with a concrete tag:
https://github.com/owebeeone/gwz-cli/releases/download/v0.9.0/gwz-installer.sh
https://github.com/owebeeone/gwz-cli/releases/download/v0.9.0/gwz-installer.ps1
Verify Assets¶
Release assets are checksummed and have GitHub artifact attestations. For stronger verification:
- Download the release archive and checksum file.
- Compare the archive SHA-256 with the checksum.
- Verify the GitHub artifact attestation.
- Run
gwz --version. - Run
gwz --help.
Smoke Test A Unix Installer¶
tmp="$(mktemp -d)"
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/owebeeone/gwz-cli/releases/latest/download/gwz-installer.sh \
-o "${tmp}/gwz-installer.sh"
GWZ_UNMANAGED_INSTALL="${tmp}/bin" \
GWZ_NO_MODIFY_PATH=1 \
sh "${tmp}/gwz-installer.sh"
"${tmp}/bin/gwz" --version
"${tmp}/bin/gwz" --help
Smoke Test A Windows Installer¶
$ErrorActionPreference = "Stop"
$tmp = Join-Path $env:TEMP "gwz-test-$([guid]::NewGuid())"
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$installer = Join-Path $tmp "gwz-installer.ps1"
Invoke-WebRequest `
"https://github.com/owebeeone/gwz-cli/releases/latest/download/gwz-installer.ps1" `
-OutFile $installer
$env:GWZ_UNMANAGED_INSTALL = Join-Path $tmp "bin"
$env:GWZ_NO_MODIFY_PATH = "1"
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
& $installer
$exe = Join-Path $env:GWZ_UNMANAGED_INSTALL "gwz.exe"
& $exe --version
& $exe --help
Local Release Checks¶
From the development workspace:
cargo fmt --check
cargo test
cargo run -q -p gwz -- --help
python scripts/generate_cli_reference.py --check
When changing command docs, inspect the generated reference and spot-check command help for the affected commands:
scripts/release.py runs the generated CLI reference check by default before it
commits the release worktree. If the release must proceed while docs are being
reconciled separately, pass --no-doc-check; otherwise update the reference
with: