Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.Z on main whose version equals WispVersion.current (semver, from 0.1.0).
  • A GitHub release for that tag with wisp-X.Y.Z-arm64.tar.gz (the stripped wisp and wisp-tui, mlx.metallib, and the LICENSE) and wisp-X.Y.Z-arm64.tar.gz.sha256, whose notes are the ## X.Y.Z section of CHANGELOG.md followed by the install line.
  • A formula update in pidster/homebrew-tap (Formula/wisp.rb) pointing at that tarball with its checksum. Users run brew install pidster/tap/wisp, which installs wisp and mlx.metallib in the Cellar’s libexec and links /opt/homebrew/bin/wisp to it, already on PATH; 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:

PageCheck
README.mdWhat wisp can do, the quick start commands, the MCP tool list, the doc map, developer setup, the layout table
AGENTS.md, CLAUDE.mdThe layout table, the architecture paragraph, the model’s tool list, the MCP tool names, the smoke-test commands
docs/README.mdEvery page, ADR, and proposal has a row with a current description (the preflight checks presence, not accuracy)
docs/introduction.md, docs/guide/*.mdThe 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 siteEvery 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.htmlThe 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.mdEvery subcommand, flag, chat command, and config field the binary has; wisp --help and /help are the source
docs/mcp.mdEvery MCP tool, argument, result field, and resource ToolCatalog declares
docs/tools/README.md, docs/trust.mdThe model’s tools, and what each can change on the Mac
docs/design.mdComponents added or moved since the last release
docs/backlog.mdShipped items marked done with the date; nothing listed as next that has shipped
CHANGELOG.mdThe ## 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.

  1. Preflight: the version is exactly three numbers, X.Y.Z; every docs/*.md, decision, and proposal is linked from docs/README.md (but docs/SUMMARY.md, the site’s table of contents, which scripts/check docs-links checks instead); clean tree on main, the Metal toolchain present (xcrun -f metal), WispVersion.current equals X.Y.Z, no existing tag here or on origin (git ls-remote), origin/main is HEAD (pushed, and nothing on origin left out), gh is authenticated, CHANGELOG.md has a non-empty ## X.Y.Z section, scripts/check passes (the full test run, and the evaluations compiled), scripts/check mlx-build passes (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-gate passes, and scripts/check eval passes, judged by swift test’s own exit status (until 0.21.1 it was grep’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 with scripts/check eval context. --skip-eval leaves 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 rewrite harness/Sources/WispCore/Resources/measurements.json, because it runs only the subsets and every run re-rolls the numbers. Record deliberately with scripts/check eval record and 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 for edit_file, system_info, and draft_change at 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 that HEAD and 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.
  2. Build: swift build -c release --traits MLX and cargo build --release -p wisp-tui, each run with WISP_RELEASE=1 (only the release script sets it; without it a build prints X.Y.Z-dev+<commit>); copy the build’s mlx-swift_Cmlx.bundle/Contents/Resources/default.metallib into the stage as mlx.metallib, strip both binaries, verify wisp --version and wisp-tui --version print X.Y.Z (the crate version in tools/wisp-tui/Cargo.toml is bumped with WispVersion.current) and wisp doctor passes on the build machine, with its MLX finding loading the staged mlx.metallib, away from the build’s bundle, and that the staged wisp completions zsh|bash|fish prints the committed scripts in harness/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.
  3. Package: tarball with wisp, wisp-tui, mlx.metallib, and LICENSE; SHA-256 file; and the formula, wisp.rb, written from the template with the new URL and checksum before anything is published.
  4. Publish: git tag -a vX.Y.Z, push the tag, gh release create with 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 --clobber if it exists, the tap push), with the artifacts and the formula left in harness/.build/release-artifacts.
  5. Tap: clone pidster/homebrew-tap, copy in Formula/wisp.rb, commit, push. The formula has no version line: Homebrew scans the version from the URL’s releases/download/vX.Y.Z/, and brew audit reports a version equal to the scanned one as redundant (resource_auditor.rb, audit_version, read on 2026-10-09). The formula puts wisp and mlx.metallib in libexec with a link in bin, and runs generate_completions_from_executable(bin/"wisp", "completions"), which writes the output of wisp completions bash, zsh, and fish (the scripts embedded in the binary) to bash_completion/"wisp", zsh_completion/"_wisp", and fish_completion/"wisp.fish". Its test checks both versions, that the library and the three scripts are installed, and that wisp completions zsh prints a zsh script.
  6. 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.