e542a342
docs: the Darwin arm, its measurements and the Mac e2e verdict
a73x 2026-09-04 10:16
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -1,12 +1,17 @@ | |||
| 1 | # mux | 1 | # mux |
| 2 | 2 | ||
| 3 | Terminal multiplexer: ghostty-vt engine runs authoritatively in `mux d`, replicated | 3 | Terminal multiplexer: ghostty-vt engine runs authoritatively in `mux d`, replicated |
| 4 | in the client. Attach = one snapshot, then row deltas. Zig, Linux only, prototype. | 4 | in the client. Attach = one snapshot, then row deltas. Zig; Linux and |
| 5 | Apple-silicon macOS; prototype. | ||
| 5 | 6 | ||
| 6 | ## Toolchain (pinned — system zig will NOT build this) | 7 | ## Toolchain (pinned — system zig will NOT build this) |
| 7 | 8 | ||
| 8 | ```sh | 9 | ```sh |
| 9 | ZIG=deps/zig/zig # vendored 0.15.2 (ghostty pin); copy the toolchain here, gitignored | 10 | ZIG=deps/zig/zig # vendored 0.15.2 (ghostty pin); copy the toolchain here, gitignored |
| 11 | # on a Mac that is the aarch64 0.15.2 tarball, in the same place | ||
| 12 | make mac-sdk # Darwin only, and `make build` runs it: a shadow SDK | ||
| 13 | # under deps/mac-sdk, because zig 0.15.2 cannot link | ||
| 14 | # the stub Xcode 26.4+ ships. No-op elsewhere. | ||
| 10 | make build test e2e # Makefile already points at it | 15 | make build test e2e # Makefile already points at it |
| 11 | make check # fmt + unit tests + shell syntax + comment-claim refs — pre-commit gate | 16 | make check # fmt + unit tests + shell syntax + comment-claim refs — pre-commit gate |
| 12 | make ci # check + e2e + agent + throughput — the delivery gate | 17 | make ci # check + e2e + agent + throughput — the delivery gate |
| @@ -14,6 +19,8 @@ make agent soak bench throughput | |||
| 14 | make vm # real user journeys against the mux-e2e VM (test/vm.sh) | 19 | make vm # real user journeys against the mux-e2e VM (test/vm.sh) |
| 15 | make mac xos # macOS journeys (test/mac.sh); the cross-OS gate (test/xos.sh) | 20 | make mac xos # macOS journeys (test/mac.sh); the cross-OS gate (test/xos.sh) |
| 16 | make install # the one user binary -> ~/.local/bin, ReleaseSafe | 21 | make install # the one user binary -> ~/.local/bin, ReleaseSafe |
| 22 | # MUX_TARGET follows the HOST: a Mac installs a Mac | ||
| 23 | # binary, because that is the only one it can link | ||
| 17 | make xversion-build xversion # cross-version gate; XVER_OLD_WORKTREE points at the old checkout (default ..) | 24 | make xversion-build xversion # cross-version gate; XVER_OLD_WORKTREE points at the old checkout (default ..) |
| 18 | ``` | 25 | ``` |
| 19 | 26 | ||
| @@ -22,23 +29,23 @@ First build after a clean checkout fetches ~30MB of QUIC deps (minutes, once). | |||
| 22 | 29 | ||
| 23 | ## Reading this repo without burning context | 30 | ## Reading this repo without burning context |
| 24 | 31 | ||
| 25 | Files are large and comment-dense (~36% of Zig bytes are whole `//` lines). Reading the repo | 32 | Files are large and comment-dense (~37% of Zig bytes are whole `//` lines). Reading the repo |
| 26 | costs ~800k tokens; every token stays in context and is re-billed each turn. | 33 | costs ~800k tokens; every token stays in context and is re-billed each turn. |
| 27 | 34 | ||
| 28 | - **Never `cat` these:** `src/server/server.zig` (3.2k lines, ~31k tok; its tests are | 35 | - **Never `cat` these:** `src/server/server.zig` (3.3k lines, ~31k tok; its tests are |
| 29 | siblings in `src/server/`, none over 1.7k; the agent relay and the | 36 | siblings in `src/server/`, none over 1.7k; the agent relay and the |
| 30 | session table are `src/server/server_agent.zig` and `src/server/server_sessions.zig`), | 37 | session table are `src/server/server_agent.zig` and `src/server/server_sessions.zig`), |
| 31 | `src/tui/interact.zig` (4.5k), | 38 | `src/tui/interact.zig` (4.8k), |
| 32 | `docs/decisions.md` (7.8k). `test/e2e.sh` is a 182-line runner now; the | 39 | `docs/decisions.md` (8.7k). `test/e2e.sh` is a 188-line runner now; the |
| 33 | bodies are `test/e2e_NN_*.sh` (`E2E_ONLY=<group>` runs one). | 40 | bodies are `test/e2e_NN_*.sh` (`E2E_ONLY=<group>` runs one). |
| 34 | Use `grep -n` for the symbol, then `sed -n 'A,Bp'` for a window. | 41 | Use `grep -n` for the symbol, then `sed -n 'A,Bp'` for a window. |
| 35 | `src/client/client.zig` is 3.1k now — cheap enough to read, and so is | 42 | `src/client/client.zig` is 3.3k now — cheap enough to read, and so is |
| 36 | every piece of the wall: `wallview.zig` is 2.4k (tiles, focus, births, | 43 | every piece of the wall: `wallview.zig` is 2.6k (tiles, focus, births, |
| 37 | endings, the keyboard loop) with `wall_pump.zig` (0.9k, one tile's | 44 | endings, the keyboard loop) with `wall_pump.zig` (0.9k, one tile's |
| 38 | thread), `wall_host.zig` (0.5k), `wall_picker.zig` (0.6k), | 45 | thread), `wall_host.zig` (0.5k), `wall_picker.zig` (0.9k), |
| 39 | `wall_layout.zig` (0.4k) and its tests in `src/tui/wall_test_*.zig` | 46 | `wall_layout.zig` (0.5k) and its tests in `src/tui/wall_test_*.zig` |
| 40 | (none over 1.4k). | 47 | (none over 1.4k). |
| 41 | Every figure here is `wc -l` on 2026-09-02; re-measure before trusting one. | 48 | Every figure here is `wc -l` on 2026-09-04; re-measure before trusting one. |
| 42 | - Every module has a `//!` header stating its contract. `head -12` on it | 49 | - Every module has a `//!` header stating its contract. `head -12` on it |
| 43 | answers most "what is this" questions for ~200 tokens. | 50 | answers most "what is this" questions for ~200 tokens. |
| 44 | - Pipe Bash output: `| tail -30`, `2>/dev/null`, `grep -c`. `make test` full | 51 | - Pipe Bash output: `| tail -30`, `2>/dev/null`, `grep -c`. `make test` full |
| @@ -378,6 +385,17 @@ own. Test fixtures in `test/`: | |||
| 378 | is private and `mux d` is `tooLong`'s only caller: it refuses at parse, | 385 | is private and `mux d` is `tooLong`'s only caller: it refuses at parse, |
| 379 | before any fork, on the stderr the asking client lent it. Everyone else | 386 | before any fork, on the stderr the asking client lent it. Everyone else |
| 380 | dials and reads the kernel's `NameTooLong`, which truncates nothing. | 387 | dials and reads the kernel's `NameTooLong`, which truncates nothing. |
| 388 | `sockpath.runtimeDir` is the one place that names the DIRECTORY those | ||
| 389 | paths go in, so the daemon, the client and the askpass listener agree by | ||
| 390 | construction. `$XDG_RUNTIME_DIR` wins on every OS, because that is how | ||
| 391 | every isolated rig keeps its sockets away from the user's. Linux has NO | ||
| 392 | fallback — a guess cannot make two binaries agree on one daemon, so the | ||
| 393 | caller names it with `--sock` — and Darwin falls back to `/tmp/mux-<uid>`, | ||
| 394 | created 0700 and re-checked on every ask for owner, exact mode and no | ||
| 395 | symlink in the last step, as tmux checks `/tmp/tmux-UID`. Not `$TMPDIR` | ||
| 396 | and not `~/Library/Caches`: the longest name mux creates is the directory | ||
| 397 | plus 68 bytes plus the pid's digits, and neither of those directories | ||
| 398 | leaves room for it in 103 (measured 2026-09-03, docs/decisions.md). | ||
| 381 | - No socket stealing: `mux d start` refuses a path another daemon owns. | 399 | - No socket stealing: `mux d start` refuses a path another daemon owns. |
| 382 | 400 | ||
| 383 | ## Working rules | 401 | ## Working rules |
| @@ -393,6 +411,21 @@ own. Test fixtures in `test/`: | |||
| 393 | names aliased their neighbour's because every test held ONE session; a tile | 411 | names aliased their neighbour's because every test held ONE session; a tile |
| 394 | painted over the rail because `col_off` was 0 in every test body; the | 412 | painted over the rail because `col_off` was 0 in every test body; the |
| 395 | page's own rules went unpinned because no check executed the page. | 413 | page's own rules went unpinned because no check executed the page. |
| 414 | - **A harness assumption is Linux-only until it has run on both OSes.** The | ||
| 415 | shell scripts are the part of this repo with no compiler behind them, and | ||
| 416 | the e2e suite's first run on macOS was sixteen groups and ten of these. | ||
| 417 | Ask questions through `test/os_oracle.sh` and add an arm rather than a | ||
| 418 | branch in a group file. Three shapes bit hardest and are worth knowing by | ||
| 419 | name. A backtick inside an UNQUOTED heredoc is a command substitution | ||
| 420 | wherever it stands, comments included, and macOS ships programs a Linux | ||
| 421 | box does not — a comment that quoted `expect` started `/usr/bin/expect`, | ||
| 422 | which read the rest of the heredoc as its own script and hung the group | ||
| 423 | for its whole budget. `$TMPDIR` on macOS is a per-user directory that | ||
| 424 | ends in `/` and is nearly fifty characters long, so a path spelled from | ||
| 425 | it fails a string compare against the same path normalised, and does not | ||
| 426 | fit in a label bar. And BSD's `ps` and `wc` answer in a different shape | ||
| 427 | from GNU's — a full path where Linux gives a basename, a padded count | ||
| 428 | where Linux gives a bare number. | ||
| 396 | - **Ask the OS about the OS, not the daemon.** A claim about a pid, an fd, a | 429 | - **Ask the OS about the OS, not the daemon.** A claim about a pid, an fd, a |
| 397 | socket or a process tree is asserted against `/proc`, `ps`, or the shell's | 430 | socket or a process tree is asserted against `/proc`, `ps`, or the shell's |
| 398 | own `$$`. A daemon reporting on itself cannot catch itself being wrong — | 431 | own `$$`. A daemon reporting on itself cannot catch itself being wrong — |
| @@ -448,7 +481,7 @@ own. Test fixtures in `test/`: | |||
| 448 | ## Where the answers live | 481 | ## Where the answers live |
| 449 | 482 | ||
| 450 | `docs/roadmap.md` (~660 ln) the ranked queue, but stale past 2026-08-16 — | 483 | `docs/roadmap.md` (~660 ln) the ranked queue, but stale past 2026-08-16 — |
| 451 | `git-collab issue list` is the live order · `docs/decisions.md` (7.8k ln, grep | 484 | `git-collab issue list` is the live order · `docs/decisions.md` (8.7k ln, grep |
| 452 | only) every decision + measurement · `README.md` user-facing usage. Design | 485 | only) every decision + measurement · `README.md` user-facing usage. Design |
| 453 | history (specs, plans, the original handoff) lives in git: `git log -- docs/superpowers`. | 486 | history (specs, plans, the original handoff) lives in git: `git log -- docs/superpowers`. |
| 454 | 487 | ||
README.md
| Old | New | ||
|---|---|---|---|
| @@ -3,8 +3,8 @@ | |||
| 3 | Terminal multiplexer where detach/reattach is **state sync, not replay**: | 3 | Terminal multiplexer where detach/reattach is **state sync, not replay**: |
| 4 | the terminal engine (ghostty-vt) runs authoritatively in a daemon (`mux d`) | 4 | the terminal engine (ghostty-vt) runs authoritatively in a daemon (`mux d`) |
| 5 | and replicated in the client (`mux`). Attach costs one snapshot; everything | 5 | and replicated in the client (`mux`). Attach costs one snapshot; everything |
| 6 | after is row deltas; a dropped connection is a non-event. Linux only, | 6 | after is row deltas; a dropped connection is a non-event. Linux and |
| 7 | prototype (see [Status](#status)). | 7 | Apple-silicon macOS, prototype (see [Status](#status)). |
| 8 | 8 | ||
| 9 | ## Build | 9 | ## Build |
| 10 | 10 | ||
| @@ -35,6 +35,10 @@ make release | |||
| 35 | ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < dist/mux-vVERSION-x86_64-linux-musl.tar.gz | 35 | ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < dist/mux-vVERSION-x86_64-linux-musl.tar.gz |
| 36 | ``` | 36 | ``` |
| 37 | 37 | ||
| 38 | On an Apple-silicon Mac the same build and install commands work, with two | ||
| 39 | things to install first and one binary you cannot cross-compile — see | ||
| 40 | [macOS](#macos). | ||
| 41 | |||
| 38 | ## Quick start, local | 42 | ## Quick start, local |
| 39 | 43 | ||
| 40 | ```sh | 44 | ```sh |
| @@ -707,7 +711,48 @@ whatever you last copied. | |||
| 707 | 711 | ||
| 708 | ## macOS | 712 | ## macOS |
| 709 | 713 | ||
| 710 | mux builds and runs on an Apple-silicon Mac. The gate for it is `make mac`, | 714 | mux builds and runs on an Apple-silicon Mac — `aarch64` only. There is no |
| 715 | Intel build, no universal binary, and no way to cross-compile the Mac | ||
| 716 | binary from Linux, so a Mac binary is made on a Mac. | ||
| 717 | |||
| 718 | You need Xcode's Command Line Tools for the SDK and `cmake` for the QUIC | ||
| 719 | dependencies: | ||
| 720 | |||
| 721 | ```sh | ||
| 722 | xcode-select --install | ||
| 723 | brew install cmake | ||
| 724 | make build # first build fetches + compiles the QUIC deps, as on Linux | ||
| 725 | make install # an aarch64 binary in ~/.local/bin | ||
| 726 | ``` | ||
| 727 | |||
| 728 | `make build` runs `deps/mac-sdk.sh` first, which may build a shadow SDK | ||
| 729 | under `deps/mac-sdk/`. Xcode 26.4 and later ship a `libSystem` stub that | ||
| 730 | lists only the `arm64e` slice, and the pinned Zig 0.15.2 matches the bare | ||
| 731 | `arm64` one, so against that SDK every system symbol comes out undefined | ||
| 732 | and the build cannot start. The shadow is the real SDK by symlink | ||
| 733 | everywhere except that one stub, where Zig's own copy stands in, plus an | ||
| 734 | `xcrun` on PATH that points at it. It costs a few seconds once, retires | ||
| 735 | itself when a future Xcode lists `arm64` again, and needs nothing of you. | ||
| 736 | |||
| 737 | One runtime difference worth knowing. The daemon's default socket lives in | ||
| 738 | `$XDG_RUNTIME_DIR` when that is set, and macOS does not set it, so mux | ||
| 739 | falls back to `/tmp/mux-<uid>` — created 0700 and re-checked for owner and | ||
| 740 | mode on every use, the way tmux uses `/tmp/tmux-UID`. The macOS `$TMPDIR` | ||
| 741 | is not used for this: the longest socket name mux creates would not fit in | ||
| 742 | the 103 bytes a unix socket path gets. | ||
| 743 | |||
| 744 | `make test` passes on a Mac. `make e2e` has two groups that do not, both | ||
| 745 | deliberately and both for the fallback above. A leg in the select group | ||
| 746 | requires `mux d stats` and a bare `mux` to REFUSE when | ||
| 747 | `$XDG_RUNTIME_DIR` is unset, and a leg in the askpass group requires a | ||
| 748 | wall in that state to start no prompt socket at all. On macOS there is | ||
| 749 | always a runtime directory, so mux answers instead of refusing and the | ||
| 750 | wall arms its socket, which is the fallback working. The suite is linear, | ||
| 751 | so a `make e2e` on a Mac stops at the first of the two. The other gates — | ||
| 752 | `make agent`, `make soak`, `make throughput`, `make xversion` — have not | ||
| 753 | been run on a Mac at all. | ||
| 754 | |||
| 755 | The gate for macOS is `make mac`, | ||
| 711 | which is the macOS half of what `make vm` is for Linux: it pushes this | 756 | which is the macOS half of what `make vm` is for Linux: it pushes this |
| 712 | commit to the Mac named by `MUX_MAC` (default `squirtle`), builds it there, | 757 | commit to the Mac named by `MUX_MAC` (default `squirtle`), builds it there, |
| 713 | and then runs the user journeys against a *pristine* macOS virtual machine | 758 | and then runs the user journeys against a *pristine* macOS virtual machine |
| @@ -755,7 +800,8 @@ issue list` (`docs/roadmap.md` is the older ranked queue). | |||
| 755 | 800 | ||
| 756 | ## Status | 801 | ## Status |
| 757 | 802 | ||
| 758 | Prototype, eighteen milestones in, current cut tagged `v0.0.1-15`. All | 803 | Prototype, eighteen milestones in, current cut tagged `v0.0.1-15`. Linux |
| 804 | and Apple-silicon macOS; every gate below was measured on Linux. All | ||
| 759 | founding kill criteria cleared, measured on real networks rather than | 805 | founding kill criteria cleared, measured on real networks rather than |
| 760 | loopback: reattach into a live full-screen `nvim` in ~5ms from one | 806 | loopback: reattach into a live full-screen `nvim` in ~5ms from one |
| 761 | snapshot; steady-state delta traffic ~1% of snapshot-equivalent cost; the | 807 | snapshot; steady-state delta traffic ~1% of snapshot-equivalent cost; the |
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -191,10 +191,11 @@ const mod_table = [_]ModSpec{ | |||
| 191 | // and askpass's prompt socket all take their listener from here, so the | 191 | // and askpass's prompt socket all take their listener from here, so the |
| 192 | // guarded unlink is written once instead of three times. | 192 | // guarded unlink is written once instead of three times. |
| 193 | .{ .name = "serve", .path = "src/serve.zig", .imports = &.{"sockpath"}, .test_imports = &.{"testtmp"} }, | 193 | .{ .name = "serve", .path = "src/serve.zig", .imports = &.{"sockpath"}, .test_imports = &.{"testtmp"} }, |
| 194 | // No imports that teach it anything, deliberately: the proxy is a byte | 194 | // Nothing that teaches it the protocol, deliberately: the proxy is a |
| 195 | // pump that knows nothing about the protocol it carries. `testtmp` is | 195 | // byte pump that knows nothing about what it carries. Its two imports |
| 196 | // the one exception and does not weaken that — it hands its tests a | 196 | // are both about the PATH and never the bytes — `sockpath` for the |
| 197 | // short directory to put a socket in and knows nothing about the bytes. | 197 | // socket name it dials, `testtmp` for a short directory its tests can |
| 198 | // put one in. | ||
| 198 | .{ .name = "proxy", .path = "src/proxy.zig", .link_libc = true, .imports = &.{"sockpath"}, .test_imports = &.{"testtmp"} }, | 199 | .{ .name = "proxy", .path = "src/proxy.zig", .link_libc = true, .imports = &.{"sockpath"}, .test_imports = &.{"testtmp"} }, |
| 199 | // Reflection over a caller's options struct, so it imports nothing: the | 200 | // Reflection over a caller's options struct, so it imports nothing: the |
| 200 | // struct is the flag table and the parser learns it at comptime. | 201 | // struct is the flag table and the parser learns it at comptime. |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -8319,10 +8319,13 @@ Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`. | |||
| 8319 | 8319 | ||
| 8320 | ## 2026-09-03 — the daemon's Darwin arm (macOS port, step 3) | 8320 | ## 2026-09-03 — the daemon's Darwin arm (macOS port, step 3) |
| 8321 | 8321 | ||
| 8322 | `server_os_macos.zig` fills the server root's `.macos` arm. Eleven of the | 8322 | `server_os_macos.zig` fills the server root's `.macos` arm. Most of the |
| 8323 | twelve operations are a spelling change and nothing else. Three are a | 8323 | operations are a spelling change and nothing else. FOUR are a different |
| 8324 | different mechanism, because the Linux one does not exist on Darwin, and | 8324 | mechanism, because the Linux one does not exist on Darwin, and one of the |
| 8325 | one of the three is a trap that a straight translation walks into. | 8325 | four is a trap that a straight translation walks into. They are the four |
| 8326 | below: `sendNoSigNoWait`, `closeFrom`, `anonFd` and `peerCred`. The | ||
| 8327 | `forkDetached` pin at the end is a fifth difference of the same kind, in | ||
| 8328 | the test rather than in the arm. | ||
| 8326 | 8329 | ||
| 8327 | **`sendNoSigNoWait` sets `SO_NOSIGPIPE`, and a REFUSED set is the answer.** | 8330 | **`sendNoSigNoWait` sets `SO_NOSIGPIPE`, and a REFUSED set is the answer.** |
| 8328 | Darwin has no `MSG_NOSIGNAL`: "do not raise SIGPIPE" is a property of the | 8331 | Darwin has no `MSG_NOSIGNAL`: "do not raise SIGPIPE" is a property of the |
| @@ -8342,7 +8345,11 @@ name, value and length are all fixed in the source, so nothing else about | |||
| 8342 | the arguments can be invalid) and returned as `error.BrokenPipe`, which is | 8345 | the arguments can be invalid) and returned as `error.BrokenPipe`, which is |
| 8343 | what `send` would have answered had it not signalled first. Every other | 8346 | what `send` would have answered had it not signalled first. Every other |
| 8344 | setsockopt failure describes a socket that cannot raise SIGPIPE either, so | 8347 | setsockopt failure describes a socket that cannot raise SIGPIPE either, so |
| 8345 | those fall through and let `send` name them. | 8348 | those fall through and let `send` name them. The more robust design is to |
| 8349 | arm `SO_NOSIGPIPE` once at fd BIRTH, which would need a new root operation | ||
| 8350 | at every socket and accept site; it is deferred as a follow-on, and the | ||
| 8351 | cost of not doing it is one misread errno if Darwin ever grows another | ||
| 8352 | EINVAL path on this call. | ||
| 8346 | 8353 | ||
| 8347 | That test now has two legs, because a socket that never carried a byte and | 8354 | That test now has two legs, because a socket that never carried a byte and |
| 8348 | one that lost its peer mid-stream are different states to the kernel and | 8355 | one that lost its peer mid-stream are different states to the kernel and |
| @@ -8354,16 +8361,19 @@ only works after a successful send. | |||
| 8354 | one close per slot from the floor to `getdtablesize()`, which is the soft | 8361 | one close per slot from the floor to `getdtablesize()`, which is the soft |
| 8355 | `RLIMIT_NOFILE` and therefore also the ceiling on any fd this process could | 8362 | `RLIMIT_NOFILE` and therefore also the ceiling on any fd this process could |
| 8356 | be holding. A few hundred cheap EBADFs once per session start, between fork | 8363 | be holding. A few hundred cheap EBADFs once per session start, between fork |
| 8357 | and exec, so nothing is opening fds underneath the walk. | 8364 | and exec, so nothing is opening fds underneath the walk. The cost scales |
| 8365 | with that soft limit rather than with the fds actually held, so a box that | ||
| 8366 | raises `ulimit -n` to a large number pays for the raise here. | ||
| 8358 | 8367 | ||
| 8359 | **`anonFd` is an unlinked `mkstemp` file.** No `memfd_create`. A 0600 file | 8368 | **`anonFd` is an unlinked `mkstemp` file.** No `memfd_create`. A 0600 file |
| 8360 | this uid creates and unlinks before anyone could open it by name is private | 8369 | this uid creates and unlinks before anyone could open it by name is private |
| 8361 | by mode where memfd is private by having no name; the window is those two | 8370 | by mode where memfd is private by having no name; the window is those two |
| 8362 | calls, on an empty file. `/tmp` rather than the runtime directory because | 8371 | calls, on an empty file. `/tmp` rather than the runtime directory because |
| 8363 | `src/os/` imports nothing of ours and must not learn the socket directory. | 8372 | `src/os/` imports nothing of ours and must not learn the socket directory. |
| 8364 | `mkstemp` opens CLOEXEC on modern Darwin and the carrier must survive | 8373 | `mkstemp` is asserted to open CLOEXEC on modern Darwin and the carrier must |
| 8365 | `mux d upgrade`'s exec, so the flag comes back off before the fd is | 8374 | survive `mux d upgrade`'s exec, so the flag is cleared before the fd is |
| 8366 | returned. The root's existing pin — nlink 0, not CLOEXEC, readable and | 8375 | returned. That assertion was never measured and does not need to be: the |
| 8376 | clear is harmless on an fd that never had the flag. The root's existing pin — nlink 0, not CLOEXEC, readable and | ||
| 8367 | writable — passes on the box unchanged. | 8377 | writable — passes on the box unchanged. |
| 8368 | 8378 | ||
| 8369 | **`peerCred` needs two calls where Linux needs one.** Darwin's | 8379 | **`peerCred` needs two calls where Linux needs one.** Darwin's |
| @@ -8413,3 +8423,310 @@ still cannot link there until the client arm exists; `make build` on the box | |||
| 8413 | stops at the client root's `@compileError` and nothing else. | 8423 | stops at the client root's `@compileError` and nothing else. |
| 8414 | 8424 | ||
| 8415 | Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`. | 8425 | Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`. |
| 8426 | |||
| 8427 | ## 2026-09-03 — macOS port step 3: the Darwin arm | ||
| 8428 | |||
| 8429 | The client arm, the toolchain, the two gates and the verdict. Written on | ||
| 8430 | 2026-09-04, over work that ran 2026-09-03 and 2026-09-04. The daemon's own | ||
| 8431 | arm is the entry above; this one is everything around it and the grade. | ||
| 8432 | |||
| 8433 | **The build host cannot be Linux, and the Mac needs a shadow SDK.** Xcode | ||
| 8434 | 26.4 and later ship a `libSystem.B.tbd` whose `targets:` line lists | ||
| 8435 | `arm64e-macos` and no `arm64-macos`. zig 0.15.2's Mach-O linker matches the | ||
| 8436 | bare `arm64` slice only, so against that SDK every libSystem symbol is | ||
| 8437 | undefined — for the build runner too, which is a native link, so `zig build` | ||
| 8438 | cannot even start (ziglang/zig#31658; fixed in 0.16, which ghostty's pin | ||
| 8439 | cannot use). `deps/mac-sdk.sh` builds a shadow SDK that is the real one by | ||
| 8440 | symlink in every path except `usr/lib/libSystem{,.B}.tbd`, where zig's own | ||
| 8441 | bundled stub stands in, plus an `xcrun` shim on PATH that answers | ||
| 8442 | `--show-sdk-path` with it — zig finds the SDK by running `xcrun` | ||
| 8443 | (`std/zig/system/darwin.zig`) and so does ghostty's `apple_sdk` helper. It | ||
| 8444 | is self-retiring: when the real stub lists `arm64-macos` again the script | ||
| 8445 | builds nothing and a stale shim passes straight through. With it the whole | ||
| 8446 | `zig build` compiles ghostty and its three C++ dependencies natively, and | ||
| 8447 | the QUIC stack builds in about a minute once the dep script's `native` word | ||
| 8448 | stopped assuming an x86 host. Cross-compiling the Mac binary from Linux is | ||
| 8449 | still not possible: the QUIC prefix cross-builds, but the link needs the | ||
| 8450 | Mac SDK, and there is no shadow of it on a Linux box. | ||
| 8451 | |||
| 8452 | **The pty master answers the Linux shape.** `tcgetattr`, `TIOCGPGRP`, | ||
| 8453 | `TIOCSWINSZ` and `TIOCGWINSZ` on the master all returned 0 against a forked | ||
| 8454 | child holding the slave as its controlling tty, so the Linux arm's shape is | ||
| 8455 | reused unchanged: nothing reopens the slave by name and `forkPty` records | ||
| 8456 | only the master and the pid. | ||
| 8457 | |||
| 8458 | **The default socket directory is `/tmp/mux-<uid>`, and the measurement is | ||
| 8459 | why.** The longest path mux creates is | ||
| 8460 | `<dir>/mux-agent-<pid>-<12 hex>/agent-<session>.sock`, which is the | ||
| 8461 | directory plus 68 bytes plus the pid's digits at `session_name_max` 32, | ||
| 8462 | against 103 usable bytes of `sun_path`. On the test Mac `$TMPDIR` is 48 | ||
| 8463 | bytes (121 needed), `~/Library/Caches/mux` 34 (107) and | ||
| 8464 | `~/.local/state/mux/run` 36 (109): none of the three spec candidates fits. | ||
| 8465 | `/tmp/mux-501` is 12 (85). So `sockpath.runtimeDir` takes | ||
| 8466 | `$XDG_RUNTIME_DIR` when it is set — every isolated rig sets it, on both | ||
| 8467 | OSes — and otherwise `/tmp/mux-<uid>`, created 0700 and re-checked on every | ||
| 8468 | ask for owner, exact mode and no symlink in the last step. `/tmp` is sticky | ||
| 8469 | and world-writable, so the per-uid directory is what carries the privacy, | ||
| 8470 | as tmux's `/tmp/tmux-UID` does; the check here is the stricter of the two, | ||
| 8471 | because tmux asks only that no other-user bit is set and this asks for | ||
| 8472 | exactly 0700. Linux keeps NO fallback: a guess cannot make two binaries | ||
| 8473 | agree on one daemon, so the caller names it with `--sock`. | ||
| 8474 | |||
| 8475 | **Four operations are a different mechanism, not a different spelling.** | ||
| 8476 | `closeFrom` walks the fd table one close at a time from the floor to | ||
| 8477 | `getdtablesize()`, because Darwin has no `close_range`; that is a few | ||
| 8478 | hundred cheap EBADFs once per session start, between fork and exec, so | ||
| 8479 | nothing is opening fds underneath the walk. `anonFd` is an unlinked | ||
| 8480 | `mkstemp` file in `/tmp` at mode 0600, because there is no `memfd_create`: | ||
| 8481 | private by mode where memfd is private by having no name, for the width of | ||
| 8482 | two syscalls on an empty file. That is a real change of medium — the | ||
| 8483 | upgrade manifest's key bytes touch a disk that memfd never did — and it is | ||
| 8484 | accepted because the file is 0600, unlinked before anyone could open it by | ||
| 8485 | name, empty until written, and on a FileVault volume by default; `shm_open` | ||
| 8486 | was the alternative and Darwin's does not support read/write, so the | ||
| 8487 | manifest writer would have had to mmap. `sendNoSig` sets `SO_NOSIGPIPE` per | ||
| 8488 | call because Darwin has no `MSG_NOSIGNAL` and "do not raise SIGPIPE" is a | ||
| 8489 | property of the socket rather than the send, and a REFUSED set is the | ||
| 8490 | answer: Darwin's `sosetopt` rejects every option with EINVAL once a socket | ||
| 8491 | is shut down both ways, which is exactly the hung-up state, so the arm | ||
| 8492 | reads that EINVAL as the peer being gone and returns `error.BrokenPipe`. | ||
| 8493 | Fourth, the `forkDetached` test asks `getsid(2)` rather than `ps`: macOS's | ||
| 8494 | `ps` has no `sid` column and its `sess` column is a kernel address that | ||
| 8495 | reads 0 for anyone but root, so the natural port of the Linux pin compared | ||
| 8496 | 0 against 0 and passed whatever the child did. | ||
| 8497 | |||
| 8498 | **A Darwin panic the gate found, and the shape of its fix.** Attaching a | ||
| 8499 | Mac client to a Linux daemon crashed with `attempt to unwrap error: | ||
| 8500 | SocketNotConnected` in `client_os_macos.sendNoSig`. Darwin answers ENOTCONN | ||
| 8501 | on a send to a peer that is mid-close, and `std.posix.send` lists | ||
| 8502 | `SocketNotConnected` among the errors it maps to `unreachable`. Both Darwin | ||
| 8503 | arms call `sendto` instead — which returns that error rather than panicking | ||
| 8504 | on it — and map it to `BrokenPipe`, which is what both callers already | ||
| 8505 | handle and what `send` would have answered had it not panicked first. There | ||
| 8506 | is no test. The window is a race, and a deterministic pin would need the | ||
| 8507 | Linux arms to map ENOTCONN too, which this branch does not change: "the | ||
| 8508 | Linux outcome does not change" was a constraint on the whole port. A | ||
| 8509 | root-owned error map, written once above both arms, is the shape if that is | ||
| 8510 | ever done; today the switch is duplicated verbatim in the two arms, because | ||
| 8511 | a shared file would need a third module row and would break the leaf | ||
| 8512 | property the platform layer's rows have. | ||
| 8513 | |||
| 8514 | **Two fixes in the daemon's own suite, made when it first ran on a Mac.** | ||
| 8515 | The hangup-upgrade refusal test raced the stubborn shell's `trap`: on macOS | ||
| 8516 | `/bin/sh` execs bash 3.2, which takes 12 to 21 ms to arm a trap where dash | ||
| 8517 | takes 2 or less, so a fixed sleep that held on Linux did not hold there. The | ||
| 8518 | FIXTURE owns readiness now — `TestDaemon.startStubborn` and | ||
| 8519 | `attachStubborn` block on a per-pid marker the shell writes once its trap is | ||
| 8520 | armed — and five call sites each became one line. Separately the | ||
| 8521 | repeated-end test grew a duration pin: it times the accepted end to the | ||
| 8522 | shell's death on `std.time.Timer` and requires at least `Pty.term_grace_ms` | ||
| 8523 | and less than three times it, measured at 661 ms on Linux and 668 to 682 ms | ||
| 8524 | on macOS against a 500 ms grace. Both were graded by mutation on Linux | ||
| 8525 | before the Mac run. | ||
| 8526 | |||
| 8527 | **The shell harness asks a bare Mac, so its Darwin arm is perl.** | ||
| 8528 | `test/os_oracle.sh` gained a `Darwin` case beside the existing one: one | ||
| 8529 | spelling per question a pin asks, so a group file names the question and | ||
| 8530 | never the OS. Three of those spellings are perl rather than a Homebrew | ||
| 8531 | binary, and the reason is what the `make mac` gate runs on — a PRISTINE | ||
| 8532 | macOS guest with no developer tooling, where nothing `g`-prefixed exists. | ||
| 8533 | `real_path` is `Cwd::realpath` with a hand-written fallback, because | ||
| 8534 | `Cwd::realpath` is not the same function on every perl: on 5.42 with Cwd | ||
| 8535 | 3.94 it hands back the spelling of a directory that is not there, where | ||
| 8536 | `readlink -f` answers nothing with rc 1, so the arm resolves the parent | ||
| 8537 | itself and `oracle_selftest` pins both branches. `now_ms` is | ||
| 8538 | `Time::HiRes`, not `/bin/date`: macOS 26's date answers `%N` and older ones | ||
| 8539 | do not, so spelling the system date would make the suite's clock depend on | ||
| 8540 | how new the OS is, and the failure on an older one is a literal `N` inside | ||
| 8541 | an arithmetic expansion partway through a run. It costs 4.5 ms a call | ||
| 8542 | against gdate's 2.1 and python3's 21.6 (measured 2026-09-03), which is | ||
| 8543 | inside the noise of every bracket in the e2e, whose tightest budget is | ||
| 8544 | 1500 ms, and NOT inside the throughput gate's 10 ms ceiling around a 6 ms | ||
| 8545 | leg — a `make throughput` on a Mac will need `SOLO_MAX_MS` raised. | ||
| 8546 | |||
| 8547 | `timeout` is the third. GNU's binary is used when it is on PATH, | ||
| 8548 | Homebrew's `gtimeout` next, and a perl `alarm` wrapper last, so a group | ||
| 8549 | file keeps spelling `timeout` through all three. The perl arm answers | ||
| 8550 | GNU's exit contract for the five cases this harness asks — 0, 124 for the | ||
| 8551 | budget, the child's own status, 127 for a missing command, 128+N for a | ||
| 8552 | signal — and `oracle_selftest` pins all five. It differs from GNU in three | ||
| 8553 | measured ways, none of which a caller here can reach: a command that | ||
| 8554 | exists but is not executable answers 127 where GNU answers 126; a signal | ||
| 8555 | sent to the WRAPPER is not relayed to the child; and the child is not put | ||
| 8556 | in a process group, so GNU's kill-the-group on expiry becomes | ||
| 8557 | kill-the-child and a grandchild outlives the budget. | ||
| 8558 | |||
| 8559 | **The two gates.** `make mac` (`test/mac.sh`) drives eight legs on a tart | ||
| 8560 | clone of the pristine `mux-mac-base` guest, boots it in under twenty | ||
| 8561 | seconds (17.6 s on the run the ledger records) and | ||
| 8562 | has run green twice unaided from a Linux box. `make xos` (`test/xos.sh`) is | ||
| 8563 | the cross-OS gate: ten legs proving a Mac client against a Linux daemon and | ||
| 8564 | a Linux client against a Mac daemon, over both the ssh handoff and QUIC, | ||
| 8565 | plus the cross-arch upgrade refusal in both directions. It has run green | ||
| 8566 | three times from Linux — 21.6 s, 20.8 s, and 1:12 with a Mac recompile in | ||
| 8567 | it. The Linux side is the VM `mux-lan`, an x86_64 Ubuntu box; macOS | ||
| 8568 | delivered inbound UDP on 4433 to a daemon started from a non-GUI ssh | ||
| 8569 | session, so QUIC INTO a Mac works and needed no firewall exception. | ||
| 8570 | |||
| 8571 | **Two bugs `make xos` surfaced that predate this branch.** First, | ||
| 8572 | `mux d endpoint` announces the key it resolved from the DEFAULT path and | ||
| 8573 | never the listener's `--key`, so a daemon started as | ||
| 8574 | `mux d start -d --quic ADDR --key ELSEWHERE` loses every ssh handoff. It is | ||
| 8575 | not Darwin's and it is not fixed here; the gate uses the default key on the | ||
| 8576 | Linux box and the bug is filed. Second, `parsePreflight` compared `uname -m` | ||
| 8577 | verbatim against the zig target tag, so a Mac reporting `arm64` refused to | ||
| 8578 | upgrade itself against an `aarch64` image. That one IS fixed on this branch, | ||
| 8579 | because a port whose Mac cannot upgrade itself is not a port; the map is | ||
| 8580 | exact and a unit test is the only pin, since one Mac cannot host a | ||
| 8581 | Mac-to-Mac run. | ||
| 8582 | |||
| 8583 | **A lesson about the fixture itself.** Two agents sharing the Mac's single | ||
| 8584 | checkout raced each other's `git checkout` in the middle of a gate run, and | ||
| 8585 | the run graded a tree that was not the one it was asked about. The gate | ||
| 8586 | re-reads the tip after every run now. One box with one working copy is a | ||
| 8587 | shared mutable resource and has to be treated as one. | ||
| 8588 | |||
| 8589 | **`make e2e` on the Mac: the verdict.** Sixteen groups, run one at a time | ||
| 8590 | with `E2E_ONLY` and once as the whole suite, on macOS 26.6.2 (Apple M1) with | ||
| 8591 | Homebrew's coreutils present. | ||
| 8592 | |||
| 8593 | | group | alone | seconds | in the whole suite | | ||
| 8594 | |---|---|---|---| | ||
| 8595 | | 01_boot | rc 0 | 34 | pass | | ||
| 8596 | | 02_predict | rc 0 | 53 | pass | | ||
| 8597 | | 03_side | cannot run alone | — | pass | | ||
| 8598 | | 04_handoff | rc 0 | 17 | pass | | ||
| 8599 | | 05_session | rc 0 | 8 | pass | | ||
| 8600 | | 06_web | rc 0 | 21 | pass | | ||
| 8601 | | 07_wallcli | rc 0 | 30 | pass | | ||
| 8602 | | 08_mouse | rc 0 | 22 | pass | | ||
| 8603 | | 09_hosts | rc 0 | 87 | pass | | ||
| 8604 | | 10_agent | rc 0 | 15 | pass | | ||
| 8605 | | 11_select | cannot run alone | — | RED, by design | | ||
| 8606 | | 12_panes | rc 0 | 59 | not reached | | ||
| 8607 | | 13_birth | rc 0 | 37 | not reached | | ||
| 8608 | | 14_upgrade | rc 0 | 10 | not reached | | ||
| 8609 | | 15_askpass | rc 2 | 31 | RED, by design | | ||
| 8610 | | 16_push | rc 0 | 4 | not reached | | ||
| 8611 | |||
| 8612 | The whole-suite column stops at `11_select` because the suite is linear | ||
| 8613 | and `set -e`: the groups after it are graded by their standalone runs | ||
| 8614 | only. The suite reached 78 of its 113 scenarios before that leg. | ||
| 8615 | |||
| 8616 | **Not one of the failures was a defect in mux.** Ten Linux-only | ||
| 8617 | assumptions in the HARNESS accounted for all of them, and every fix was | ||
| 8618 | made and re-run green on both OSes: | ||
| 8619 | |||
| 8620 | - `proxy_pid` matched a `comm` column. BSD ps prints comm as the | ||
| 8621 | executable's full path where Linux prints the basename, and truncates it | ||
| 8622 | to the column width in a multi-column format, so no proxy was ever found | ||
| 8623 | and every scenario that tears a transport failed. It reads `args` now. | ||
| 8624 | - The handoff shim compared a shebang script's process name to `ssh`. | ||
| 8625 | Linux names such a process after the SCRIPT and Darwin after the | ||
| 8626 | INTERPRETER, so `ssh` and `sh` are both correct and the word says nothing | ||
| 8627 | about the product. Only the parent word, `mux`, is pinned now. | ||
| 8628 | - Three counter checks compared `wc -l` to a string. BSD wc pads its count | ||
| 8629 | to a column width, so " 1" was not "1". | ||
| 8630 | - The scenario log stamped `date +%s.%N`, and BSD date has no `%N`. It | ||
| 8631 | takes the oracle's `now_ms`. This one had no witness: the log is opt-in. | ||
| 8632 | - Every temporary path came out with `//` in it, because macOS sets | ||
| 8633 | `$TMPDIR` to a per-user directory ending in a slash, and two groups | ||
| 8634 | compare such a path as a string. The slash is stripped once, before the | ||
| 8635 | first of the 191 call sites that spell it. | ||
| 8636 | - `pid_args` answered a different SHAPE on each arm: Linux reads a | ||
| 8637 | NUL-terminated `/proc` entry and left a trailing space that | ||
| 8638 | `ps -o args=` does not. The one caller comparing a whole argv with `=` | ||
| 8639 | had to spell a space it could only have learned from Linux. | ||
| 8640 | `oracle_selftest` pins the shape now, which is the right place for it: a | ||
| 8641 | helper the two OSes answer differently makes every pin that reads it | ||
| 8642 | agree with anything. | ||
| 8643 | - Seven assertions across two groups grep a socket path out of a tile's | ||
| 8644 | LABEL BAR. A bar is as wide as its tile and `labelText` keeps the state | ||
| 8645 | word and cuts the label's TAIL, which is the half carrying the session | ||
| 8646 | name — and the macOS temporary directory's name alone was 48 characters | ||
| 8647 | on the test Mac, so an 80-column bar had none of it left. Those daemons' | ||
| 8648 | sockets are spelled under `/tmp` now, which is where a Linux run with no | ||
| 8649 | `$TMPDIR` has always put them, and the leak sweep reads both directories | ||
| 8650 | when they differ so the paths that moved are still swept. | ||
| 8651 | - The pager leg spelled the row numbers `less` leaves on screen. Attaching | ||
| 8652 | makes less repaint, and where the repaint puts the top of an at-the-end | ||
| 8653 | view depends on how many of the 24 rows the build spends on its status | ||
| 8654 | line: less 704 leaves it at 178 and less 668 at 179, three runs each. So | ||
| 8655 | the leg was off by one row on the Mac while the wheel worked perfectly. | ||
| 8656 | The leg READS the anchor now. A short attach waits for the paint and | ||
| 8657 | detaches, which leaves the daemon's grid holding the repainted view; the | ||
| 8658 | dump after it is the top row the wheel is about to move, taken while | ||
| 8659 | nothing is attached and nothing can change it, and the assertion is row | ||
| 8660 | anchor-24 present and row anchor gone. That is exactly 24 rows on both | ||
| 8661 | builds and it can still fail on 23, which matters: a leg that accepted | ||
| 8662 | 154 OR 155 — which is what was delivered first — could no longer fail on | ||
| 8663 | a wheel that moved 23 rows, and 23 is what one lost notch looks like. | ||
| 8664 | - One symptom on this leg looked like a lost input byte and is not mux's. | ||
| 8665 | An earlier attempt anchored the view by TYPING `178g` into the pager | ||
| 8666 | after the attach; on macOS the leading `1` never arrived and less went to | ||
| 8667 | line 78, three runs out of three, while Linux went to 178 three out of | ||
| 8668 | three. Two probes settled where it goes. A session running `cat`, which | ||
| 8669 | echoes exactly what reaches its pty, received a first send of `abcdef` | ||
| 8670 | intact on both OSes, three runs each — so mux delivers the byte. And a | ||
| 8671 | `\x0c` sent first, with `178g` as the SECOND send, reached less whole on | ||
| 8672 | the Mac, three out of three. So whatever discards it is on the far side | ||
| 8673 | of the pty and not an input byte mux drops — by elimination, since less's | ||
| 8674 | own source was not read; a `TCSAFLUSH` on re-entering raw mode is what | ||
| 8675 | behaves this way and is the guess. Nothing in the product changes and the | ||
| 8676 | leg no longer types into the pager at all. | ||
| 8677 | Recorded because the shape — one byte, first write, one OS — is exactly | ||
| 8678 | what a real input bug would look like, and the next person to see it | ||
| 8679 | should know these two probes exist. | ||
| 8680 | - The agent-directory reap was read straight off a bound socket. The reap | ||
| 8681 | is not a boot step — it happens inside the successor daemon's first | ||
| 8682 | `makeDir`, when its first session is born — so the check raced the thing | ||
| 8683 | it asserted, and lost on a Mac where Linux had always won. | ||
| 8684 | - A backtick in an UNQUOTED heredoc is a command substitution wherever it | ||
| 8685 | appears, comments included, and three ptyclient scripts quoted words | ||
| 8686 | that way in their prose. On Linux the words are "command not found" and | ||
| 8687 | the run carries on none the wiser. On macOS `/usr/bin/expect` exists, so | ||
| 8688 | the shell started it, it read the rest of the heredoc as its own script, | ||
| 8689 | and the group hung for its entire 600 s budget. This one cost four gate | ||
| 8690 | runs, not one. The first sweep found two sites and MISSED the third, | ||
| 8691 | because its detector required the heredoc's delimiter to end the line and | ||
| 8692 | that opener has a trailing space after it; the replacement comment | ||
| 8693 | written for the third site had a backtick in it too. The rule is written | ||
| 8694 | above each of the three heredocs, which is all that gates it. A lint | ||
| 8695 | would be better and it is small — match the opener anywhere on the line, | ||
| 8696 | skip a quoted delimiter, and refuse a backtick in the body. | ||
| 8697 | |||
| 8698 | **Two groups stay RED, and both are red BY DESIGN — the same design.** | ||
| 8699 | `sockpath.runtimeDir` has no fallback on Linux and falls back to | ||
| 8700 | `/tmp/mux-<uid>` on Darwin, so every leg that pins what mux does with | ||
| 8701 | `$XDG_RUNTIME_DIR` unset is pinning a state macOS never reaches. | ||
| 8702 | |||
| 8703 | `11_select` has a leg that unsets it and requires `mux d stats` and bare | ||
| 8704 | `mux` to REFUSE, naming the variable. On Darwin they resolve the fallback | ||
| 8705 | and report what is or is not listening on `/tmp/mux-501/muxd.sock`, which | ||
| 8706 | is the arm working. `15_askpass`'s last leg unsets it and requires a wall | ||
| 8707 | to start no prompt listener, so ssh keeps its own prompts. On Darwin the | ||
| 8708 | wall arms the socket — the dial log shows it being handed over — the popup | ||
| 8709 | opens, it eats the detach key and the leg times out. | ||
| 8710 | |||
| 8711 | Neither was patched, because a leg that asserts the Linux rule is not | ||
| 8712 | wrong; it needs a Darwin half that asserts the fallback, and writing those | ||
| 8713 | is not this branch's work. One consequence is worth stating plainly: | ||
| 8714 | `11_select` stops at that leg, so the scenarios AFTER it in that group have | ||
| 8715 | never run on macOS at all. Everything in every other group has, both alone | ||
| 8716 | and in sequence. | ||
| 8717 | |||
| 8718 | **Not done, and not claimed.** There is no x86_64-macos build: the arm is | ||
| 8719 | written for `aarch64-macos` and nothing has compiled or run on an Intel Mac. | ||
| 8720 | There is no universal binary. The Mac binary cannot be cross-compiled from | ||
| 8721 | Linux, for the SDK reason above, so `make install` on a Mac names the native | ||
| 8722 | target — the only one that builds there — and a Mac release is cut on a Mac. | ||
| 8723 | |||
| 8724 | And only `make test` and `make e2e` have run there. `make agent`, `make | ||
| 8725 | soak`, `make throughput` and `make xversion` have never been run on a Mac, | ||
| 8726 | so nothing is known about them beyond that they pass on Linux. | ||
| 8727 | `test/agent.sh` in particular still spells `${TMPDIR:-/tmp}` the way the | ||
| 8728 | e2e suite did before this branch and defines its own `now_ms` through | ||
| 8729 | `python3` instead of taking the oracle's, so the first Mac run of it should | ||
| 8730 | be expected to find the same class of thing the e2e suite just did. | ||
| 8731 | |||
| 8732 | Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`. | ||