Releasing wisp
wisp ships as an arm64 binary, with MLX’s Metal library beside it, through a Homebrew tap. This page is the procedure; the decision is ADR 0012.
What a release is
- A git tag
vX.Y.Zonmainwhose version equalsWispVersion.current(semver, from 0.1.0). - A GitHub release for that tag with
wisp-X.Y.Z-arm64.tar.gz(the strippedwispandwisp-tui,mlx.metallib, and the LICENSE) andwisp-X.Y.Z-arm64.tar.gz.sha256, whose notes are the## X.Y.Zsection ofCHANGELOG.mdfollowed by the install line. - A formula update in
pidster/homebrew-tap(Formula/wisp.rb) pointing at that tarball with its checksum. Users runbrew install pidster/tap/wisp, which installswispandmlx.metallibin the Cellar’slibexecand links/opt/homebrew/bin/wispto it, already onPATH; MLX finds its library beside the binary’s real path (ADR 0047). The formula also installs the shell completions for bash, zsh, and fish into Homebrew’s completion directories, so a Homebrew user has them with no further step.
The binary is unsigned for now; Homebrew does not quarantine what it downloads, so Gatekeeper does not
intervene. A signed and notarised .pkg is a possible later channel.
Step 0: the docs sweep, before running the script
Do this first, by hand or by an agent, and commit it on its own before scripts/release (AGENTS.md,
“Docs sweep before every release”). Each change keeps its own page right; this catches what a change
made stale elsewhere. Read each against the code as it stands:
| Page | Check |
|---|---|
README.md | What wisp can do, the quick start commands, the MCP tool list, the doc map, developer setup, the layout table |
AGENTS.md, CLAUDE.md | The layout table, the architecture paragraph, the model’s tool list, the MCP tool names, the smoke-test commands |
docs/README.md | Every page, ADR, and proposal has a row with a current description (the preflight checks presence, not accuracy) |
docs/introduction.md, docs/guide/*.md | The site’s landing page and guides restate the reference: the commands, flags, defaults, messages, and wisp doctor output they quote, and the tool lists, against the binary’s --help and the pages they link to |
docs/SUMMARY.md and the site | Every new page in its part, the ADRs in number order; scripts/check docs-site builds the site with no warning (ADR 0065) |
site/home/index.html | The homepage restates the docs: the install line, the requirements, the gate’s steps, the MCP tools, the runtimes, and wisp-tui’s text in its terminal picture, against the pages and source they come from |
docs/wisp.md | Every subcommand, flag, chat command, and config field the binary has; wisp --help and /help are the source |
docs/mcp.md | Every MCP tool, argument, result field, and resource ToolCatalog declares |
docs/tools/README.md, docs/trust.md | The model’s tools, and what each can change on the Mac |
docs/design.md | Components added or moved since the last release |
docs/backlog.md | Shipped items marked done with the date; nothing listed as next that has shipped |
CHANGELOG.md | The ## Unreleased section names every user-visible change since the last tag (git log vPREV..) |
Commit it as “Bring the docs up to date for X.Y.Z”, or “Docs sweep for X.Y.Z: nothing stale”.
Procedure
scripts/release X.Y.Z does all of it and refuses to continue at the first problem. --dry-run performs
every local step and prints the remote ones instead of executing them.
- Preflight: the version is exactly three numbers,
X.Y.Z; everydocs/*.md, decision, and proposal is linked fromdocs/README.md(butdocs/SUMMARY.md, the site’s table of contents, whichscripts/check docs-linkschecks instead); clean tree onmain, the Metal toolchain present (xcrun -f metal),WispVersion.currentequalsX.Y.Z, no existing tag here or onorigin(git ls-remote),origin/mainisHEAD(pushed, and nothing on origin left out),ghis authenticated,CHANGELOG.mdhas a non-empty## X.Y.Zsection,scripts/checkpasses (the full test run, and the evaluations compiled),scripts/check mlx-buildpasses (the harness and its tests with the MLX trait, warnings as errors, before the release build is the first to compile that code),scripts/check coverage-gatepasses, andscripts/check evalpasses, judged byswift test’s own exit status (until 0.21.1 it wasgrep’s, so a missed floor passed): every suite but the context eval, which is a measurement for design decisions (ADR 0045) and runs on purpose withscripts/check eval context.--skip-evalleaves the eval out, for a release whose eval already passed on the same code (a dry run just before, with only docs changed since); 0.15.0 was the first. The release’s eval only asserts the floors; it does not rewriteharness/Sources/WispCore/Resources/measurements.json, because it runs only the subsets and every run re-rolls the numbers. Record deliberately withscripts/check eval recordand commit the file; the binary embeds it, so a release carries the numbers that were committed. If the eval nonetheless dirtied the tree the release stops. The eval is the slow step: since 0.23.0 it runs a fixed subset of each large case set, about 52 minutes foredit_file,system_info, anddraft_changeat the configured model’s 2026-10-12 times (measurements.md, “Run time, and the release’s subsets”), plus the other suites, and much longer when the Mac is busy. Change nothing in the checkout while a release runs. The binary is built from the working tree, so before building and again before publishing the script checks thatHEADand the tree are as preflight found them, and stops if not. 0.13.0 shipped an edit made while its evals ran; 0.13.1 is the build that matches its source. - Build:
swift build -c release --traits MLXandcargo build --release -p wisp-tui, each run withWISP_RELEASE=1(only the release script sets it; without it a build printsX.Y.Z-dev+<commit>); copy the build’smlx-swift_Cmlx.bundle/Contents/Resources/default.metallibinto the stage asmlx.metallib,stripboth binaries, verifywisp --versionandwisp-tui --versionprintX.Y.Z(the crate version intools/wisp-tui/Cargo.tomlis bumped withWispVersion.current) andwisp doctorpasses on the build machine, with itsMLXfinding loading the stagedmlx.metallib, away from the build’s bundle, and that the stagedwisp completions zsh|bash|fishprints the committed scripts inharness/Sources/WispCore/Resources/completions/, which the formula installs. MLX adds about 18 MB to the stripped binary and 3.8 MB of library (about 6 MB to the download; ADR 0047). A dry run, often made before the bump is pushed, reports the two remote checks and goes on. - Package: tarball with
wisp,wisp-tui,mlx.metallib, andLICENSE; SHA-256 file; and the formula,wisp.rb, written from the template with the new URL and checksum before anything is published. - Publish:
git tag -a vX.Y.Z, push the tag,gh release createwith both assets and generated notes. A release that fails from here cannot simply be rerun, since preflight refuses an existing tag, so the script prints, on its way out, the exact commands for the steps still to do (the tag push, the release create or an upload with--clobberif it exists, the tap push), with the artifacts and the formula left inharness/.build/release-artifacts. - Tap: clone
pidster/homebrew-tap, copy inFormula/wisp.rb, commit, push. The formula has noversionline: Homebrew scans the version from the URL’sreleases/download/vX.Y.Z/, andbrew auditreports aversionequal to the scanned one as redundant (resource_auditor.rb,audit_version, read on 2026-10-09). The formula putswispandmlx.metallibinlibexecwith a link inbin, and runsgenerate_completions_from_executable(bin/"wisp", "completions"), which writes the output ofwisp completions bash,zsh, andfish(the scripts embedded in the binary) tobash_completion/"wisp",zsh_completion/"_wisp", andfish_completion/"wisp.fish". Its test checks both versions, that the library and the three scripts are installed, and thatwisp completions zshprints a zsh script. - Verify from a clean shell:
brew update && brew install pidster/tap/wisp && wisp doctor.
Until a macOS 27 CI runner exists this runs on a developer’s Mac with Xcode 27.
Coverage gate
harness/coverage-baseline records the total line coverage of the last release. scripts/check coverage-gate measures the current figure and refuses a release when it is lower. When it is higher,
the gate also refuses until scripts/check coverage-baseline has recorded the new figure and it is
committed, so the baseline only ever moves up through a commit that says so. Lowering it is possible
by editing the file, and the commit must say why. A tenth of a point either way counts as unchanged:
timing-dependent branches (sandbox nesting, timeouts) move the measured figure by a few hundredths
between identical runs, observed on 2026-09-20 (93.05% against a 93.08% baseline).
Release notes
CHANGELOG.md is written for people who run wisp, not from commit subjects: what they can now do,
what changed under them, what was broken and is fixed. Add a line under ## Unreleased in the commit
that makes a user-visible change; the version-bump commit renames that section to the version. Anything
a user can notice as a removal or an incompatible change (a URI that moved, old data that no longer
loads) goes first, in a Breaking: list ahead of Added: and Changed:. The release script publishes
the whole section as it stands, so the list needs no other handling.
Bumping the version
WispVersion.current in harness/Sources/WispCore/Audit/AuditEvent.swift is the single source. Bump
it in its own commit (“Bump version to X.Y.Z”) that also renames ## Unreleased in CHANGELOG.md, the
crate version in tools/wisp-tui/Cargo.toml and tools/Cargo.lock, and retrains the risk classifier the
release ships with scripts/check classifier-default, which writes risk@X.Y.Z-default into
harness/Sources/WispCore/Resources/risk-default.json; then run the release. The committed file is the shipped model, and the eval measures it. The tag check and
the shipped classifier’s version in preflight make a mismatch impossible to ship.
First-run support
wisp doctor checks what a new install needs: the on-device model is available and enabled, macOS
is 27 or later, sandbox-exec exists, MLX’s Metal library loads, ~/.wisp/config.json parses, and the home
directory is writable.
It is the first thing to ask for when someone reports a problem.