Conversation
Two prerequisites for packaging a macOS .app, both of which make a release build meaningfully different from a developer build. Version stamping. `make build` passed no ldflags, so every non-Docker build (make bundle, install-server.sh --mode native, and now the .app) shipped a cix-server that could not report what it was — only server/Dockerfile stamped -X main.version. Mirror the Dockerfile's flags exactly: -trimpath, -w, and no -s, since govulncheck -mode=binary needs the Go symbol table. The version source needed fixing too. `git describe --tags` has no --match filter, and this repo has three tag streams; on develop it resolved to a *CLI* tag, so a locally built *server* image was labelled with a CLI version. CI never hit this because release-server.yml passes VERSION= explicitly, which is why it went unnoticed. Filter to server/v* and strip the prefix. fetch-llama strict mode. The script records a missing checksum and continues, which is right when a contributor is bootstrapping a LLAMA_VERSION bump and wrong in a build that ships: the recorded value is computed from the download it is meant to verify, so it verifies nothing. LLAMA_STRICT=1 fails instead, and does so before spending the ~50 MB download. Default stays 0. The header comment claimed downstream CI already failed hard on this; nothing did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a third, independent tag stream. `mac/vX.Y.Z` builds a single Apple
Silicon .app carrying cix-server, the cix CLI and a Metal-accelerated
llama-server, published as a drag-to-Applications DMG. The versions of the two
bundled binaries are stamped separately and recorded in Info.plist, because the
app is versioned by what it does, not by what it happens to contain.
This release is the pipeline, not the product. The menu-bar interface —
start/stop, provider status, dashboard link, password reset, autostart and
self-update — lands in later mac/v* releases. What ships now is a bundle that
assembles, signs and verifies, plus enough of a front end (`cix-launcher
-report`) to prove each component is the one the build intended.
Layout is constrained, not chosen. Every executable sits in Contents/MacOS,
including llama/: codesign --verify --strict rejects executable code under
Resources/, and cix-server resolves llama-server at
filepath.Dir(os.Executable())/llama, so keeping them siblings means
CIX_LLAMA_BIN_DIR never has to be set.
Signing is ad-hoc and bottom-up. There is no paid Apple Developer membership,
so there is nothing to notarize — but ad-hoc signing is still mandatory, since
Apple Silicon refuses to run unsigned code at all. `xattr -cr` runs on every
build, not once: server/Makefile already records that macOS 26 amfid SIGKILLs
an ad-hoc-signed binary whose dylibs carry a stale signature or a
com.apple.provenance xattr, with empty stderr, and every cp into the staging
tree recreates those conditions. --deep is avoided (deprecated, and unreliable
for a bundle with four executables and ~35 dylibs directly in MacOS/).
Consequences that are documented rather than worked around: Gatekeeper blocks
the first launch and macOS 15+ removed the right-click→Open shortcut, so the
System Settings route ships in the DMG, the release body and doc/MACOS_APP.md;
Homebrew Cask is not an option, as support for casks failing Gatekeeper ends
2026-09-01; and there is no Intel build, because upstream llama.cpp publishes
no macOS x86_64 asset — build-app.sh refuses rather than producing a bundle
that dies at the first embedding.
The launcher lives at cli/launcher/ inside the cli module so it can import
cli/internal/{client,config} directly — Go's internal rule is directory-scoped,
and a separate module would force duplicating both packages. A non-darwin stub
keeps `go build ./...` green on ubuntu in ci-cli.yml.
Icons are placeholders, replaceable by dropping in three files with no code
change. make-placeholder-icons.py regenerates them so they are reproducible
rather than mystery binaries; it is stdlib-only and no build ever runs it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the placeholder artwork with the delivered CIX icon set and puts the DMG window together the way the design assumes. The set is split by role. Cream magnifier = the app, red magnifier with a "+" = the installer; that inversion is the only thing telling the two files apart in a Downloads folder, so the red one becomes the DMG's volume icon and never the app's. The menu-bar glyph is a pure black-plus-alpha template image, which is a requirement rather than a style: macOS recolours template images for dark mode and for the pressed state and reads nothing but the alpha channel. The .icns files are built from the iconsets at package time rather than committed, so the PNGs stay the single source of truth. The export pipeline could not write "@" into filenames, so the retina files arrived as -2x.png and were renamed to @2x.png, which is what iconutil requires. make-dmg.sh now builds a read-write image, applies the designed window layout through Finder, then compresses — the layout lives in the volume's .DS_Store and cannot be written into a read-only image after the fact. Finder needs a GUI session that a CI runner may not have, and an automation-consent prompt would hang the build rather than fail it, so the step runs under a watchdog and DMG_LAYOUT decides what failure means: auto (warn, ship unstyled), require (abort), off (skip). Default is auto until a real release proves the runner can do it. The image ships two visible items and no third. A READ ME FIRST.txt with the Gatekeeper instructions was tried and dropped: the block happens after the user has dragged the app to Applications and ejected the image, so the file is on screen exactly when it is not needed and gone when it is. Those instructions stay in the release body, next to the download button, and in doc/MACOS_APP.md. Two behaviours cost real debugging and are recorded where they bite: - The volume icon has to be installed AFTER the Finder pass. Staging it up front looks like it works — hdiutil copies the file, SetFile sets the flag — and then Finder removes both while laying out the window, shipping a generic disk icon. Found by bisecting the steps. - Finder's item `position` is the top-left of the cell, not its centre, and a y below roughly 68 is not clamped for that one item: Finder translates *every* item down by the difference, sliding the app and the symlink off the arrow. Verified by reading the finished image back: content 640x420, icon size 104, cix.app at (175,250), Applications at (465,250), volume icon flag set, and the bundle still passing `codesign --verify --strict` after the round trip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`display alert` is drawn with the icon of the process that ran the script, which is osascript — so every dialog the app showed came up wearing a generic folder icon, immediately after packaging a real icon set. `display dialog` takes an explicit icon, so point it at the bundle's cix.icns. The trade is that the title becomes a window title rather than bold body text, which is worth it. It is also the primitive the later phases need regardless: only `display dialog` supports `default answer`, required for the reset-password prompt. Falls back to `display alert` when running outside a bundle, where there is no icon to point at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turns cix.app from a package into something you can use: a menu bar item that shows whether the local server is running and what its embedding provider is doing, with start/stop and a dashboard link. No Dock icon, no windows. First-run setup is load-bearing, not polish. cix-server refuses to start against an empty database unless both CIX_BOOTSTRAP_ADMIN_* env vars are set (bootstrap.go, `case count == 0`) — it will not invent an admin silently — so a drag-installed app with no configuration could never start its own server. The wizard asks for an email, generates a password and an API key in the server's own format, writes ~/.cix/server.env at 0600, starts the server and registers it with the CLI. It deliberately writes ~/.cix/server.env rather than the repo .env install-server.sh uses, so a checkout and an installed app can coexist. Server state comes from two sources because neither is sufficient. launchd knows whether the job is loaded and has a pid but not whether it is serving — a cold start spends 30s to several minutes loading an embedding model, in silence. /health knows whether it is serving but cannot tell "stopped" from "starting", which is the distinction that stops people killing a server that was nearly ready. Together they give three honest states. Start is bootstrap-then-kickstart and Stop is `launchctl kill SIGTERM`, which needs KeepAlive=false — a deliberate divergence from install-server.sh. With KeepAlive=true, Stop would have to be a bootout, dragging launchd's drain race onto the hot path of a menu click. The trade is that a crashed server is not resurrected; the menu shows it stopped within five seconds. install-server.sh owns the same launchd label and points it at a repo checkout. Rather than clobber it, the app detects a wrapper without its own provenance marker and goes observe-only: status, provider and dashboard still work over HTTP, Start/Stop are disabled and labelled "managed externally". Two display decisions that are correctness, not taste: - The provider kind "ollama" is rendered as "llama.cpp (bundled)". In this codebase that kind IS the bundled llama-server supervisor — provider/ollama spawns llama-server and hardcodes ManagesProcess: true — and no Ollama is involved or possible. Printing the raw kind tells the user they are running software they never installed. The dashboard keeps showing the raw kind; it is a diagnostic surface and this is not. - model_loaded is debounced over two consecutive polls. The server computes it under a 500 ms deadline it can lose under load, so one false is not evidence, and for HTTP providers (openai, voyage) it is skipped entirely — hence EmbeddingsHealthy, which never derives a red state from a provider that owns no local process. Also extends Client.Status, which was dead code decoding 4 of 10 always-present fields, and pins the whole response with twin golden fixtures on both sides of the module boundary — the repo's convention for a contract two modules cannot share a type for. Verified end to end on a clean machine state: wizard writes the env at 0600 and bootstraps the agent, server answers /health, menu renders all three rows, Stop leaves the job loaded with no pid, Start brings it back, Open Dashboard reaches the login page, and the CLI's existing default_server is left untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes to the menu bar, plus the server config the last one needs.
Width. An NSMenu is exactly as wide as its widest row, so an untruncated model
id — `ollama:awhiteside/CodeRankEmbed-Q8_0-GGUF`, 41 characters — stretched the
whole menu to fit one line nobody reads in full. Every row is now capped at the
same width, so the menu is a predictable size rather than a function of
whichever model happens to be configured. Truncation is from the middle, since
these are qualified names where both ends carry information and tail truncation
would render every Hugging Face model as its owner.
Status. Coloured dots — green running, amber starting, red stopped, grey
unknown — instead of a "— ready" suffix, which is how macOS status apps say
this and which costs no width. Amber for starting is deliberate: a cold start
loading a model is working, and red is what makes people kill it. The dots are
generated rather than shipped as files, and are NOT template images: a template
is recoloured from its alpha channel alone, which would make all four the same
colour and erase the only thing they say. The model row gets a transparent dot
of the same size, because AppKit indents a title by its image width and without
one that row starts left of its neighbours.
Details submenu, replacing per-item tooltips. Native NSMenuItem tooltips have
two behaviours that make them unusable here, neither with an API: once any
tooltip in the app has appeared, every subsequent one shows with no delay at
all, and they are positioned against the item rather than the pointer. Fixing
either means giving every row a custom NSView with its own tracking area — that
is, writing the menu in Objective-C instead of using systray. A submenu gets
native timing and placement for free, and is where the full model id, the pid,
the port, the exposure and the reason Start/Stop is disabled now live.
Local/network toggle. This needed a server change: the HTTP server hardcoded
`fmt.Sprintf(":%d", cfg.Port)`, so it always bound every interface with no way
to say otherwise. Adds CIX_BIND_ADDR, defaulting to empty — every interface,
which is what containers need and what every existing deployment already has,
so nothing changes for them. Config.ListenAddr composes it with net.JoinHostPort
so an IPv6 literal is bracketed rather than concatenated into a different
address, and validation rejects a URL or a host:port pair, both of which bind as
a hostname on some systems and then fail to resolve — a server that is silently
unreachable instead of one that refused to start.
The app writes 127.0.0.1 at first run, deliberately unlike the server default:
exposing a code index to the local network is a choice to make on purpose. The
menu toggle asks for confirmation when widening access and not when narrowing
it, and restarts the server, because the bind address is read once at process
start and saying "saved" without restarting would be a lie discovered later.
Existing installs keep whatever they had; the app does not silently narrow a
server someone already relies on.
Verified at the socket, not the config file: toggling produced
`TCP 127.0.0.1:21847` and a LAN request refused, then `TCP *:21847` and the
same request served, with localhost working throughout and a new pid each time.
Cancelling the confirmation left both the file and the binding untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops the last ones — the status-item tooltip and the two still passed as the second argument to AddMenuItem. The app now shows none at all. AppKit's tooltips have two behaviours here that no API can change: once any one of them has appeared, every subsequent one shows with no delay whatsoever, and they are positioned against the element rather than the pointer. Changing either means giving every row a custom NSView with its own tracking area — writing the menu in Objective-C instead of using systray, which is not a trade worth making for hover text. Everything they carried already moved to the details submenu, where the timing and placement are the system's own. One tooltip was load-bearing and could not simply be deleted: Quit closes the menu bar app and leaves the launchd agent running, the opposite of what Quit means in most menu bar apps. That now reads "Quit (server keeps running)" — a surprise is worth 22 characters of title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…keover Phase 3 of the menu bar app. Start at Login writes RunAtLoad and reloads the agent. The reload is not avoidable: launchd reads a plist exactly once, when the job is bootstrapped, so rewriting it with the job loaded changes nothing until the next login — which is the setting being changed, so the user would find out months later. Reloading stops a running server, so the toggle puts it back rather than pretending the cost is zero. The checkbox reads RunAtLoad back out of the file instead of remembering it, so it cannot drift from what launchd will actually do. Reset Password drives `cix-server -reset-password`, which opens the database directly and does not need the server stopped. Its stdin is /dev/null, the branch that makes it generate a password rather than read one, and the "Temporary password: " line is matched by prefix — the surrounding lines are conditional. That command answers an unknown address by listing every account in the database. Reasonable for an operator at a terminal who already holds the DB file; an account enumeration anyone behind the user can read once it is in a GUI alert. The dialog gets one sentence, the full output goes to ~/.cix/logs/launcher.log at mode 0600 — which is also why the launcher now has a log at all, being an LSUIElement app with no terminal to write to. Takeover. install-server.sh owns the same launchd label and points it at a repo checkout, so on a machine already running cix from a clone this app finds an agent it did not create, holding the port it wants. It now asks once, shows the paths it found so the installation is recognisable, and remembers the answer. Leaving it alone is observe-only, as before. Taking it over migrates the port, API key and database paths out of the .env the old wrapper sourced — and only those four, since the rest of that file is the other installation's business — backs up the plist and wrapper first, and refuses the whole operation if the backup fails, that being the one step that overwrites something unrecoverable. The first-run wizard is now behind that check, not beside it: with a foreign agent present it would have set up a second server that cannot bind the port, against a second, empty database. Fixes a real bug found while testing the takeover prompt. Dialog dismissal was detected by matching osascript's "User canceled" — but the message is localised, and this project's own development Mac runs a British locale where it says "cancelled" with two Ls. So no cancel anywhere in the app was recognised; the network toggle only appeared to work because both its branches do the same thing. Now matched on OSStatus -128, which is a number in every language. Verified end to end against a simulated install-server.sh agent: the prompt named both its .env and its binary, Leave It Alone recorded the choice and disabled Start/Stop while leaving reset password working, a relaunch did not ask again, and Take Over migrated the API key back into a server.env it had been removed from, backed up both files, and left the config byte-identical to the snapshot taken before the simulation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app replaces itself whole — bundle and all — rather than swapping individual binaries. Two reasons, and the first is not negotiable: the bundle's ad-hoc signature seals every executable inside it, so replacing one of them breaks `codesign --verify --strict` and Apple Silicon then refuses to run it. The second is that cix-server, the cix CLI and llama-server are built and tested as one artefact, and mixing release streams would produce a combination nobody has run. It is also what makes "update the CLI" mean anything: the cix on PATH is a symlink into the bundle. Checks are cheap by construction. The releases listing is fetched with the previous ETag, so an unchanged check is a 304 — which does not count against GitHub's 60-per-hour unauthenticated limit — and the ETag is persisted so that holds across restarts too. On top of that the automatic check is throttled to 30 minutes and says nothing when there is nothing to offer. The version comparison is ported from server/internal/versioncheck rather than imported, since that package is under the server module's internal/. One rule is deliberately inverted: versioncheck treats an unparseable current version as "anything is newer", which is right for a dashboard banner and wrong for something that overwrites the running application — a development build is usually newer than the last release. Here an unidentifiable build is never replaced, and dev builds skip the check entirely. Every step before the swap is reversible, and the swap itself moves rather than deletes: live → .old, staged → live, then remove .old. If the second move fails there is still a complete application on disk to restore, which "delete then copy" would not leave. The swap runs from an embedded script in its own session because the application being replaced is the one running the update, and a process cannot outlive the deletion of its own bundle to reopen it. Not asking for admin is a decision, not an omission. If the folder holding the app is not writable the update stops before downloading anything and says to install by hand. An unsigned app requesting an administrator password to overwrite itself is indistinguishable from malware, and teaching users to approve that is worse than an update that asks them to drag an icon. The SHA-256 check against checksums.txt proves the download arrived intact and is explicitly not a trust anchor — the checksums travel the same channel as the image. Without a Developer ID there is nothing stronger available; a detached signature over the checksums is the obvious next step and is noted in the code. Verified end to end twice against a local release API serving a real, locally built DMG — the update path replaces the running application, so the only way to know it works is to run it. First pass: 0.3.0 → 0.4.0, signature still valid after the swap, no .new or .old left behind, app reopened by itself. That run exposed a real defect, since the server happened to be down: the "was it running" test used the health check, so a server still loading its embedding model — pid alive, no /health yet — read as stopped, and the update would have replaced the bundle under a live cix-server. Now it tests for a process. Second pass with a running server: 0.4.0 → 0.5.0 and the server came back on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cix-server persists its embedding provider config on first boot and treats the stored blob as authoritative from then on. Two of the ollama fields in it are not choices anyone made — they are derived from the running process: bin_dir filepath.Dir(os.Executable())/llama socket_path <TMPDIR>/cix-llama-<pid>.sock Frozen at first boot, bin_dir names the directory the server was installed in when the row was written. Move or upgrade the installation and every later boot keeps launching the *old* llama-server, until that directory is deleted — at which point embeddings stop working with no configuration having changed. A frozen socket_path defeats the uniqueness it exists for: a new server can find an orphaned llama-server already bound to that name and talk to it instead of spawning its own. Both fields are already documented as deployment-level and are deliberately absent from the dashboard's edit schema (the ollama factory's SchemaJSON). This makes that true on the second boot as well as the first. Everything a person can choose — the model, the context size, the GPU layers — still comes from the database untouched. The refreshed blob is not written back: the stored row stays the record of what was chosen. Found while splitting the macOS app's runtime into versioned directories, where it is fatal — the app would keep running the previous release's sidecar and break outright once that version was pruned. It is not macOS-specific: the same applies to a container whose image layout changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app carried everything: launcher, cix-server, the cix CLI and a Metal
llama-server, 102 MB in one bundle. Every server update therefore replaced
the whole application through a trampoline that had to quit the launcher,
move the bundle aside, move the new one in, and reopen. That machinery
existed only because the server lived inside the app.
The Mac now gets two things, released separately because they are separate
things:
cix-<ver>-arm64.dmg mac/v* the app 3.9 MB (was 41)
cix-runtime-<ver>-darwin-arm64.tar.gz server/v* the server 35 MB
The runtime IS the server, so it carries the server's version and ships
from the server's tag — the same server/vX.Y.Z and the same workflow run
that publishes the Docker images. A Mac install on 0.12.8 and a container
on 0.12.8 are the same server, and a llama bump remains what it has always
been: a server release. release-server.yml grows a macos-runtime job on
macos-latest and attaches the tarball plus a checksums.txt to the release;
release-mac.yml now builds only the app, and no longer needs a server/v* or
cli/v* tag reachable because nothing in it is stamped from one.
The runtime installs into ~/.cix/runtime/<server version>/ with a `current`
symlink. Updating it is a download and a rename: the app stays open, the
launchd wrapper needs no rewrite because it execs the stable `current`
path, and the version just replaced is kept. If the new server exits
instead of starting, the symlink goes back and the old one is restarted,
with no download and no prompt. "Exits" is the test, not "did not answer
/health" — a cold start loads an embedding model and can take minutes.
The app and the server update independently, on their own schedules, and
neither waits for the other: a server release reaches a Mac the day it
reaches Docker Hub. The launcher watches both streams, with an ETag each.
The CLI travels with the server because it speaks a specific server's API,
so /usr/local/bin/cix becomes a symlink into ~/.cix/runtime/current and
follows updates without /usr/local being touched. llama travels with the
server for the same reason it always has: cix-server resolves its sidecar
next to its own executable, so shipping them apart would mean carrying
CIX_LLAMA_BIN_DIR forever.
Two things fell out of the split rather than being designed in:
- An app update no longer stops the server. Nothing a running server
touches is inside the bundle any more, so updating cix no longer
interrupts indexing — and the prefs flag that carried "restart it
afterwards" across the swap is gone.
- Info.plist no longer records CIXServerVersion/CIXCLIVersion/
CIXLlamaVersion. With the runtime outside, those would be claims about
somebody else's files, wrong the first time either half updates alone.
build-runtime.sh round-trips its own tarball before returning: extract,
codesign --verify --strict every Mach-O, check llama-server's @rpath
dependencies, and actually exec the server. That check is in the script so
local builds get it too, and because the failure it catches is silent —
a signature the kernel rejects is SIGKILL with empty stderr. It also
refuses to ship a payload whose server reports a different version from
its label, which is what a stale server/dist produces.
Verified on an M3 Max: fresh install, launchd wiring, a live 0.5.0 → 0.6.0
→ 0.7.0 runtime update with the server restarting on the new one each time,
and a deliberate rollback from a runtime that installs cleanly and exits on
exec. Unit tests cover the tar containment checks, the execute bit, the
symlink swap, pruning, and the full download → checksum → unpack → verify
path against a served release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(mac): install the cix server outside the app, and ship it from the server release
Three tests in internal/watcher wait for BeginIndex and then assert on FinishIndex. Those are two sequential HTTP calls from the indexer goroutine, so the window between them is small but real, and on a loaded CI runner the assertion lands inside it. It surfaces as "expected FinishIndex to be called" on a test that took 0.02s — not a timeout, an assertion made too early. Adds waitForFinish for the tests that actually care about completion, and keeps waitForCalls for those that only assert BeginIndex. TestDebounce_MultipleEventsOnce had a different problem with the same shape: its deadline was ten times the 80ms debounce interval, which reads generous and is 800ms. A cold macOS runner loses that, and because the flush goroutine then ran after t.TempDir() cleanup, the failure printed as "cannot read a.go: no such file or directory" rather than as the assertion that failed. The property under test is that five events collapse into one flush, not how quickly, so it now waits on patience rather than on the interval. The shared deadline goes to 15s. It exits the instant the condition holds, so a long limit costs nothing when things work. Both failures appeared on macos-latest during the mac-runtime PRs (#229, #230) and passed on rerun; neither is caused by those changes, but the extra parallel load of a new test package is enough to make the races fire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test(cli): wait for FinishIndex instead of racing it
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotion for
server/v0.12.9.What ships
fix(server): re-derive the llama sidecar paths on every boot. A real bug,independent of macOS.
cix-serverpersists its embedding provider config onfirst boot and treats the stored blob as authoritative afterwards — including
bin_dir(fromos.Executable()) andsocket_path(fromos.Getpid()), whichdescribe the running process rather than anything a person chose. Frozen at
first boot,
bin_dirkeeps a moved or upgraded install launching the oldllama-serveruntil that directory is deleted, at which point embeddings stopwith no configuration having changed; a frozen
socket_pathlets a new serveradopt an orphaned
llama-serveralready bound to that name instead of spawningits own. Both fields are already documented as deployment-level and are absent
from the dashboard's edit schema — this makes that true on the second boot too.
Everything a person can choose still comes from the database untouched.
build(server): stamp the binary version.make bundlepassed no ldflags,so every non-Docker build reported the zero version. It also fixes an unfiltered
git describethat labelled server images with CLI versions.A new release asset.
release-server.ymlgrows amacos-runtimejob onmacos-latestthat packagescix-server, thecixCLI and the Metalllama-serverascix-runtime-<version>-darwin-arm64.tar.gz, and attaches itwith a
checksums.txtto the GitHub release. It is the same server as theDocker images, from the same tag and the same run.
The macOS app, which ships nothing yet — no
mac/v*tag is being cut here.Its code lands on main so the launcher can be tagged separately once this
release is confirmed working.
Ordering
Deliberately staged: cut
server/v0.12.9first, verify the runtime tarballinstalls and runs, then tag
mac/v*for the app.Note for the release run
releasenow depends onmacos-runtime. If the macOS job fails the Dockerimages still push — those jobs are independent — but the GitHub release is not
created until it is re-run. That is intentional: a release without the runtime
attached is one no Mac can install from.
🤖 Generated with Claude Code