From 8abbf62859f4250ac02fb3fe35bf7ee8bceca74c Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 13 Sep 2026 15:11:23 -0400 Subject: [PATCH] easel: the memo on whether this should be an application Written this session, never committed. Nothing in it is implemented; it is the research, with what could not be verified labelled as such. The finding worth keeping out of a transcript: a sandboxed Mac App Store Easel could only ever ship the `ac` bridge, because ac-electron's entitlements carry allow-unsigned-executable-memory and disable-library-validation and the sandbox rejects both. So the App Store and iOS are one decision, not two, and Developer ID direct is the only home the `claude` and `codex` bridges can have. Co-Authored-By: Claude Opus 5 (1M context) --- easel/docs/native-shell.md | 492 +++++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 easel/docs/native-shell.md diff --git a/easel/docs/native-shell.md b/easel/docs/native-shell.md new file mode 100644 index 0000000000..53687c15a4 --- /dev/null +++ b/easel/docs/native-shell.md @@ -0,0 +1,492 @@ +# A native Easel + +Written 2026-09-13. Nothing here is implemented. Line counts, flags and file +paths were read out of the tree that morning; anything I could not run is +labelled as such, and there is a list of those at the end. + +The question is whether Easel should become an application — a Mac app, a +Windows app, possibly an iOS app — instead of a program you run inside somebody +else's terminal. The prompt for this memo framed it as packaging the TUI +together with the Slab overlays that today only work beside it. That framing is +right, and the answer turns out to hinge on one line in `backends.mjs` and one +rule in Apple's sandbox. + +@jeffrey's constraint holds throughout: the runtime stays a webview on every +platform. Nothing below proposes rendering an Aesthetic Computer piece natively. +The native part is the shell — a terminal, the overlays, and the window they +sit in. + +## What is actually here, measured + +Worth getting the numbers right before arguing from them, because the brief for +this memo overstated one of them and the correction changes the argument. + +`easel/src` is **5,274 lines** of Node ESM with zero npm dependencies, and +`test/` holds **89 test cases** across seventeen files. But the *interface* — +the thing that would have to be replaced if we dropped ANSI — is not 4,400 +lines. It is `tui.mjs` at 1,318 and `render.mjs` at 505: **1,823 lines.** The +other 3,451 are session logic that has nothing to do with terminals: the three +bridges, the live channel, publishing, sign-in, the Slab marker, the mascot, the +updater. + +That split is not accidental and it is the most useful fact in this file. +`render.mjs` exports + +```js +renderFrame(state, columns, rows, useColor) +``` + +and it is pure. `state` is a plain object of about twenty fields declared in one +place at `tui.mjs:55` — `input`, `cursor`, `entries`, `approval`, `piece`, +`model`, `audience`, `qr`, `busy`. Nothing in it knows what a terminal is. A +model/view seam already exists, and the view side is five hundred lines. + +The product ships as an 85 KB tarball behind `curl -fsSL +https://prompt.ac/easel.sh | sh`, which checks for Node 18 and unpacks to +`~/.local/share/easel`. `bin/easel` is bash, resolves its own symlink chain, and +refuses to start unless `[[ -t 0 && -t 1 ]]` — an interactive terminal is a hard +precondition, not a preference. + +## The subprocess line + +Two of the three bridges spawn a vendor CLI. The third does not. This is the +constraint everything else follows from, so here is what is actually in the +files rather than a summary of it. + +`claude-server.mjs:268` spawns `claude` with `--print --input-format +stream-json --output-format stream-json`, `stdio: ["pipe","pipe","pipe"]`, and +reads newline-delimited JSON off stdout with `readline`. `app-server.mjs:37` +does the same shape with `codex app-server --stdio`. Both close with +`child.kill("SIGTERM")`. + +`ac-server.mjs` spawns nothing. Its whole import list is: + +```js +import { EventEmitter } from "node:events"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { randomUUID } from "node:crypto"; +``` + +No `child_process`, no `http`, no `net`. It POSTs to +`/api/easel-inference`, parses the `data:` stream by hand, and runs its own +agent loop — bounded at twelve rounds, because a looping model spends someone's +daily budget on a loop. It touches the filesystem exactly twice: it reads four +bundled guides out of `easel/context/`, and it writes the piece. The tool set is +one tool, `write_piece`, which takes the entire file and replaces it. + +So the claim holds, and more strongly than I expected. **The `ac` bridge needs +no subprocess and no filesystem beyond the piece file and four read-only +documents that would ship inside any app bundle.** There is no diffing, no +multi-file workspace, no `Edit` tool with line offsets. One file, whole, every +time. + +What it does need is the rest of the session, and that is where the platform +assumptions live rather than in the bridge. + +## Every POSIX assumption, listed + +I went looking for these rather than assuming them. The result is less alarming +than the `/bin/ps` call suggests. + +**The stdio protocol assumes nothing POSIX.** Pipes and newline-delimited JSON. +No pty, no ioctl, no signal semantics beyond kill-on-close. This is genuinely +portable. + +The assumptions are all in the shell around it: + +1. `slab-session.mjs:21` — `execFileSync("/bin/ps", ["-o","tty=","-p",pid])`, an + absolute path with no Windows equivalent. It is already wrapped to return `""` + on failure, so it degrades rather than crashes. The whole Slab marker is + macOS-shaped anyway. +2. `slab-session.mjs:41` and `render.mjs:59` default to + `~/.local/share/slab`. On Windows the directory simply never exists and the + marker disables itself. Harmless. +3. `ac-session.mjs:38` picks `"start"` as the Windows browser opener and hands it + to `spawn`. `start` is a `cmd.exe` builtin, not an executable, so `/login`'s + browser hand-off throws `ENOENT` on Windows. `tui.mjs:994` gets this right with + `"explorer"`. One of the two is a real bug, and it is three characters. +4. `updates.mjs:154` — `execFile("tar", ["-xzf", …])`. Windows 10 and later ship + bsdtar as `tar.exe`, so this probably works. Unverified. +5. `bin/easel`, `install.sh` and `system/public/easel.sh` are shell scripts, and + `install.sh` uses BSD `/usr/bin/sed -i ''`, which is macOS-only even among + POSIX systems. An app bundle makes these moot for the GUI; they still have no + Windows story for the `ac` command line. +6. `child.kill("SIGTERM")` on Windows terminates the process without giving it a + signal to flush on. The vendor CLI dies rather than closing. Minor. + +None of that is structural. The deepest assumption fails soft and the rest are +edits, not ports. + +The two things I could not check are the ones that matter most for Windows, and +I want to be plain about them. First, whether `spawn("claude", …)` works there +at all: on this machine both binaries are standalone Mach-O executables +(`~/.local/share/claude/versions/2.1.270`, `~/.codex/packages/standalone/…`), +so the npm-shim problem does not arise. If a Windows user has a `.cmd` shim +install instead, Node has refused to spawn `.cmd` without `shell: true` since +18.20.2. Second, and worse: `claude`'s own Bash tool needs a shell, and on +Windows that has historically meant Git Bash. A Windows Easel could plausibly +start the bridge and then watch every command the model runs fail. I have no +Windows machine and there is no Windows evidence anywhere in this repo. + +## 1. Is the terminal emulator the right frame? + +Two shapes are available. Host a real terminal and run the TUI unchanged, or +drop ANSI and drive the same state through native views. + +The seam described above makes the second option look cheaper than it is. Yes, +`state` is a plain object and `renderFrame` is pure — but everything that *moves* +the state lives in `tui.mjs`: the key handler at line 1150, `commandLogin`, +`commandPublish`, `commandBackend`, `commandModel`, and the wiring that binds +`LivePiece`, `ACSession`, `SlabSession` and the bridge together. That code is not +terminal-specific in spirit, but it is not extracted either, and extracting it is +the actual work. You would end up with a shared core, one ANSI view, one native +view, and `render.test.mjs` covering half of them. + +The stronger argument against the rewrite is not cost. It is that +`docs/local-contract.md` says, as a boundary rather than a description: *"The +terminal interface is always Easel. Engines are internal bridges, not alternate +client interfaces."* A native view layer would be a second client interface. It +might be a good one, but it has to be argued for as a product, not slipped in as +a port. Nothing in the brief for this memo argues for it. + +So: **host a real terminal.** Concretely, xterm.js driven by node-pty, with the +existing `bin/easel` running inside it unchanged. The TUI already asks only for +things a competent emulator provides — alternate screen (`\x1b[?1049h`), hidden +cursor, bracketed paste (`\x1b[?2004h`), `setRawMode(true)`, +`stdout.columns/rows`, and a `resize` event. xterm.js does all of it. + +Two costs, stated honestly. + +**node-pty is a native module**, and it is the thing that will break releases. +It must be rebuilt against each Electron ABI, and a universal macOS build needs +both arch slices. ac-electron already carries this — `@electron/rebuild` is a +dependency and `native:rebuild` is a script — but its `prebuild` step ends in +`|| echo 'Native addon build skipped'`, which is a build that is allowed to +silently produce a broken app. + +**A terminal in a window is still a terminal.** You do not get native menus over +the transcript, or drag an image onto the prompt, without inventing a protocol +for it. That is fine, and it points at the right division of effort: put the +native work into the surfaces a terminal genuinely cannot draw — the piece +preview, the scan code, window management — and leave the conversation in ANSI. +Which is, not coincidentally, exactly what Slab already does. + +### What ac-electron actually gives us here + +I assumed going in that ac-electron was either a foundation or a distraction. It +is neither cleanly, and the specifics matter. + +The dependency tree is already right: **Electron 39.2.7, `@xterm/xterm` 5.5.0 +with the fit and webgl addons, and `node-pty` 1.1.0**. The main process already +has PTY plumbing — `connect-pty`, `pty-input`, `pty-resize`, and a second +`connect-flip-pty` channel — and `preload.js` exposes +`connectPty/sendToPty/resizePty/onPtyData` to renderers. + +And none of it is wired to anything that ships. `renderer/shell.html`, +`development.html` and `terminal-offscreen.html` all use xterm, and `main.js` +loads none of them. `flip-view.html`'s back face, where a terminal is supposed to +be, is a literal empty `
`. `RIO-TERMINAL.md` proposes replacing xterm with +Rio's WASM build and references a `createShellWindow()` that does not exist in +`main.js`. Its own status block reads: research completed, integration not +started. + +So the honest description is: **the parts are on the shelf and nothing is +assembled.** That is still worth a great deal — the dependency choices are made, +the native rebuild path exists, the IPC shape exists — but nobody should plan on +the basis that ac-electron has a working terminal, because it does not. + +What ac-electron *does* ship is a macOS menu-bar daemon that wraps +aesthetic.computer in a nested `` inside a local `flip-view.html`, with +an animated tray icon, a silo-backed updater, deep links, and a lot of Docker and +devcontainer orchestration. It spawns subprocesses constantly — docker, +devcontainer, emacs, `ac-login.mjs`, a native notepat binary. It has no +cross-reference to `easel/` anywhere. + +My read: **build inside ac-electron, not beside it.** A second Electron app with +a second signing config, a second updater and a second release pipeline is the +distraction. The daemon it already is — menu bar, tray, updater, deep links, +`~/.ac-token` — is most of the chrome an Easel window needs around it. + +## 2. What the overlays become + +The rocks are worth reading before deciding what happens to them. +`PromptSigilOverlay.swift` is 3,235 lines and says what it is for in its first +paragraph: a rock's *shape* is identity, grown from the session id plus the +current prompt, so the stone re-forms when the session moves on; its *motion* is +the status channel; it carries a pet name; pointing wakes a percussion voice; +clicking reveals the inferred summary. `PromptScanCode.swift` adds the QR, +deliberately unstyled — *"a QR stops being a QR the moment it is lit, tinted, +softened, or tumbled"* — because seventeen rows of half-blocks in the transcript +was too expensive a way to get a piece onto a phone. + +The overlay machinery underneath that exists for exactly one reason, stated at +line 17: *"AppKit cannot make our window a child of another process's Terminal +window."* Everything follows from that — the dedicated companion window level, +`reposition`, occlusion gating by sampling the CGWindowList, the private +`_AXUIElementGetWindow` bridge, an osascript pass to bind each tty to a +CGWindowID, spring-follow so the badge trails its terminal with inertia. Several +hundred lines simulating a parent-child relationship the OS refuses to grant +across processes. + +Inside an app that owns its own window, **all of that evaporates.** A rock +becomes a view in a layout. No AX permission, no CGWindowList polling, no +osascript, no occlusion test, no tty binding. This is strictly simpler, and it is +the single clearest win of the native shape. + +What is lost is more interesting than what is saved. + +**The rock's identity function only survives if the app is multi-session.** The +sigil exists so you can tell nine sessions apart at a glance across a wall of +tiled panes. One window with one session has nothing to disambiguate, and the +rock degrades into decoration. So this is a design constraint, not a detail: a +native Easel should have panes or tabs, and the rock should be what marks them. +If it is a single-session window, drop the rock and keep the scan code. + +**Cross-application reach is gone.** Today the rocks decorate Terminal.app and +iTerm2 — someone else's windows. A native app cannot decorate a terminal it does +not own. If @jeffrey keeps working in Terminal.app under Slab, the native app +does not replace that; it becomes a second place Easel lives. Two shells to keep +in agreement is a real recurring cost and I do not think it goes away. + +**The scan code gets better, not worse.** `PromptScanCode` is 99 lines of +CIQRCodeGenerator plus integer-module nearest-neighbour scaling with a baked-in +quiet zone. The whole discipline — every module a whole number of device pixels, +no resampling, no crossfade on swap — transfers to a canvas or an `` +unchanged. In a native window there is a real pixel surface beside the pane +instead of a 56-point stone, so the minimum module size stops being a constraint. + +**`PromptPreview.swift` is the piece worth keeping wholesale.** It is already a +WKWebView showing the session's `scan_url` next to its pane, capped at low fps +until you point at it, with a badge whose only job is to say whether what you are +looking at is live, one save behind, or a throttled frame. That file is the proof +that the AC-runtime-as-webview constraint works as an inner pane and not just as +a whole window. It ports directly. + +**Keep writing the Slab marker either way.** `SlabSession` writes one JSON file +per session into `~/.local/share/slab/state`. If a native Easel keeps doing that, +it appears in the existing menu bar and prox ledger for free, and the fleet does +not need to learn a new thing. The marker costs nothing and buys continuity. + +## 3. One codebase or three + +Four options, and the fourth is the null one. + +**(a) Electron only — Mac, Windows, Linux.** Shares 100% of `easel/src`, running +unchanged in a PTY. Reuses ac-electron's Developer ID, its two-stage notarize, +and its silo updater. No iOS, ever. Windows is 95% there in code and 0% there in +evidence. + +**(b) SwiftUI + WKWebView — Mac and iOS.** No Windows. And the fatal detail: +**there is no Node.js on iOS.** `easel/src` cannot run there at all. So this +option does not share session logic between its two platforms; it reimplements +it, and then the Mac app and the iOS app share Swift chrome and nothing else. On +macOS it could embed SwiftTerm and spawn `claude` happily — but it would be a +third implementation of the same session, maintained against 89 tests it does not +run. + +**(c) Electron for desktop, a separate small SwiftUI app for iOS.** What is +genuinely shared is not code — it is the protocol and the endpoints: +`/api/easel-inference` with the Anthropic Messages shape, `POST /run` with +`{piece, source, codeChannel}`, the presigned publish grant, `~/.ac-token`'s +Auth0 client id and PKCE flow, the `prompt~channel%20~!autorun` URL +grammar with its load-bearing encoded space, and the fact that a piece is one +file. What is duplicated is the agent loop (about 200 lines of `ac-server.mjs`), +the SSE parser, the publish call, sign-in, and the entire interface. + +**(d) Ship no native app.** Worth naming, because Easel's actual barriers today +are `curl | sh`, "you need Node 18", and "you need a Claude or Codex +subscription", and a window fixes only the first of those. The third is what +`ac-server.mjs` was built to fix, and it was built without any native work at +all. + +I recommend **(c)**, with the emphasis on the comma: it is really *(a) now, and +iOS as a separate product decision later*. The desktop app is a packaging job +over code that already works. The iOS app is a new product that happens to speak +the same protocol. Pretending they are two targets of one codebase would produce +a shared layer that is wrong for both. + +## 4. What iOS actually gets + +No subprocess means the `ac` bridge only, and that is a sharper limit than +"hosted." `easel-inference.mjs` allowlists exactly three models — +`z-ai/glm-4.6`, `qwen/qwen3-coder`, `deepseek/deepseek-chat-v3.1` — with a +comment saying why: *"Adding a frontier model here multiplies the cost of the +free tier by about thirty."* It requires a token, resolves it to an `@handle`, +and meters against the same daily budget as `/api/ask`. So an iOS Easel is +categorically a weaker agent than the desktop one **by server policy, not by +porting effort**. No Opus, no Fable, no Codex, and a day's allowance shared with +everything else AC buys for that handle. + +The piece lives in the app's Documents container, and this is genuinely fine. +`write_piece` sends the whole file every time; there is no patch, no second file, +no directory to walk. The `ac` bridge already treats the workspace as exactly one +file, so the thing iOS cannot give it is a thing it never asked for. + +Sign-in needs rewriting. `ac-session.mjs` runs a loopback HTTP server on port +44233 and shells out to a browser — the desktop PKCE shape. iOS wants +`ASWebAuthenticationSession` against a custom scheme. Auth0 supports it; it is +about ninety lines and a callback URL registration, and it is standard work. + +So, plainly: **is an iOS Easel a real editor or a viewer with a prompt?** + +It is a real single-file editor with a deliberately weak model, and the +interesting thing about it is not the editing. It is that the piece is live and +already published while you are making it. You type a sentence, the file changes, +`prompt.ac/@handle/slug` is already serving it, and the preview is in the same +window instead of on a second device across the desk. That is a coherent product +and it is arguably a *better* demonstration of what Aesthetic Computer is than +the desktop app, because the whole live-piece apparatus stops needing a second +screen to explain itself. + +It is not Easel. It is the hosted half of Easel with a touch interface, and it +should be named and scoped as that rather than shipped as "Easel for iPhone" and +then apologised for. + +One more consequence: it cannot be a terminal. Nobody drives slash commands and +raw-mode keys on a phone. So iOS forces the native-view rewrite regardless of +what desktop does — which is, independently, the strongest argument that it is a +separate app and not a target. + +## 5. Distribution and signing + +### Reuse, which is more than I expected + +**Developer ID is solved.** `scripts/setup-host-mac-signing.sh:21` names +`Developer ID Application: Jeffrey Scudder (FB5948YR3S)`. `package.json` sets +`hardenedRuntime: true` and `notarize: true`, and +`scripts/notarize-dmg.js` adds a second pass because — in its own words — +electron-builder notarizes the `.app` but not the DMG wrapper, so *"the DMG ships +unsigned, so `spctl -t install` reports 'no usable signature'."* That is a solved +problem someone already paid for. + +**Release plumbing is solved.** Not GitHub Releases, despite what `BUILD.md` +says: `scripts/publish-release.mjs` uploads to DigitalOcean Spaces and registers +with silo, and `electron-updater` points at +`https://releases.aesthetic.computer/desktop` as a generic provider. + +**The iOS credentials exist.** App Store Connect app id 6450940883, live since +2023-07-04. Team FB5948YR3S. ASC API key `S4TQKG6U99` with cloud signing via +`-allowProvisioningUpdates` — no match, no sigh, no certificate wrangling. Six +fastlane lanes at `apple/fastlane/`. + +**The WKWebView bridge exists.** `apple/aesthetic.computer/ContentView.swift` is +466 lines and already has the pattern an iOS Easel needs: two message handlers +(`iOSApp`, `iOSAppLog`), a `console.log` monkey-patch at document start, and +native→JS calls into named globals (`window.iOSReceivePushToken`, +`iOSAppSwitchPiece`) with a retry loop for the case where the page has not +defined them yet. + +Two cautions on the Apple side. `apple/aesthetic.computer.xcodeproj` is **iOS +only** — `SDKROOT = iphoneos`, no `SUPPORTED_PLATFORMS` override, no Catalyst. +There is no macOS target in it and never has been. And `fastlane ios build` is +currently *broken*: `PROGRESS.md` records it failing on 2026-09-11 because Xcode +26.6 kept the iPhoneOS SDK but has no simulator runtimes, and version 1.1 has +been sitting in `PREPARE_FOR_SUBMISSION` since. An iOS Easel would inherit a +pipeline that does not presently produce an IPA. + +An iOS Easel would be a **new target or a new project**, not a target of +`aesthetic.computer.xcodeproj`. The precedent in this repo is clear: oskiewar, +trackdrum and tvos-tapes are each their own project, two of them generated by +XcodeGen from a `project.yml`. Follow that. + +### Genuinely new work + +**Windows, entirely.** `BUILD.md`'s matrix claims "Windows exe ✅ Authenticode" +and describes a `.github/workflows/electron-release.yml` that does not exist — +`.github/workflows/` holds two xbox workflows and nothing else. `dist/` contains +only macOS universal artifacts. `build-all-platforms.fish` hardcodes a +devcontainer path and builds Windows in an `electronuserland/builder:wine` +container. `install.fish` exits 1 on anything but Darwin. There is no evidence a +Windows build has ever succeeded, and Authenticode now effectively requires an +OV or EV certificate on hardware or a cloud signing service — a purchase and a +new pipeline, not a reuse. + +**A CI that exists.** Every build today happens by SSHing into a Mac +(`scripts/build-publish-host-mac.sh`, key `jeffrey-macbook`). + +**node-pty per ABI and per arch**, as above. + +### The Mac App Store, which is the significant finding + +**ac-electron cannot go to the Mac App Store as configured.** There is no `mas` +target, no provisioning profile, and no sandbox entitlement anywhere in it. Worse +than absent: `build/entitlements.mac.plist` grants +`com.apple.security.cs.allow-unsigned-executable-memory` and +`com.apple.security.cs.disable-library-validation`, both of which MAS review +rejects outright. This is a Developer ID direct-distribution app by construction. + +If we wanted a MAS build regardless, the repo contains its own case study. +`slab/menuband/MenuBand-AppStore.entitlements` is the one thing here that has +actually shipped to the Mac App Store, and its comments spell out what the +sandbox cost: + +> Mac App Store distribution REQUIRES the App Sandbox. This file is used ONLY +> for the MAC_APP_STORE build; the direct-download Developer ID build keeps +> MenuBand.entitlements (no sandbox). + +and then, in the list of what was removed to pass review: no global keystroke +capture (*"the sandbox forbids it; gated out behind `#if MAC_APP_STORE`"*), no +Apple Events (*"App Review routinely rejects it for a menubar instrument"*), no +MultipeerConnectivity, and *"no broad Downloads, Desktop, Documents, or +home-directory access."* + +Applied to Easel, the equivalent gate is the `claude` and `codex` bridges. The +workspace itself is solvable — MenuBand holds +`com.apple.security.files.user-selected.read-write`, and a folder the user picks +through an open panel plus a security-scoped bookmark is a normal sandboxed +pattern. What is not obviously solvable is two other things: executing a binary +at `~/.local/bin/claude` from inside a container that has no read-execute access +to that path, and the fact that a spawned child **inherits the sandbox** — which +is what ac-electron's own `entitlements.mac.inherit.plist` is for. An inherited +sandbox means the vendor CLI's own Write and Bash tools would be confined to the +same container and the same user-picked grants. An agent that can only write +where you last pointed a file picker is a different tool from the one this repo +describes. + +I did not test any of that, and I want to be exact about the difference between +what the repo proves and what I am inferring. The repo proves: MAS requires the +sandbox; MenuBand had to gate out three features to pass; ac-electron's current +entitlements are the opposite of sandbox-compatible; and nothing in this tree has +ever shipped a sandboxed app that spawns anything. The inheritance rule and the +execute-outside-the-container rule are documented Apple behaviour, not something +I ran. + +The practical conclusion is cleaner than the policy question, and it is the +useful part: **a sandboxed Mac App Store Easel would be the `ac` bridge only — +which is the same product as the iOS app.** So MAS and iOS are one decision, not +two, and Developer ID direct distribution is the only home the CLI bridges can +have. That is a tidy line, and it means nobody has to litigate App Review to +decide what the desktop app is. + +## What I would build, in order + +1. A pane in ac-electron: xterm.js on node-pty, running `bin/easel` unchanged. + Nothing else. This is the whole of the interface question and it either works + in a day or it reveals something this memo missed. +2. `PromptPreview` and `PromptScanCode` redrawn as ordinary views beside that + pane — a throttled webview on `scan_url` and a hard-edged QR. Both already + exist in Swift and both are small. +3. Panes, and the rock as what marks them. If step 3 does not happen, skip the + rock entirely; a single-session rock is decoration. +4. Keep writing the Slab marker, so the existing menu bar and ledger see the + native sessions without being changed. +5. Windows only after somebody has watched `claude` run one command there. +6. iOS as its own project, its own name, and the agent loop rewritten in Swift + against `/api/easel-inference`. + +## What is not verified + +- Whether `claude` and `codex` run on Windows at all, whether Node can spawn the + Windows install shape, and whether the model's own shell commands work without + Git Bash. No Windows machine, no Windows evidence in the repo. This is the + load-bearing unknown for option (a). +- Whether an Electron or Linux build has ever succeeded here. The tooling is + written; there are no artifacts and no logs. +- Whether the App Sandbox would actually refuse a bundled or user-selected vendor + CLI. Argued from Apple's documented behaviour and MenuBand's shipped + entitlements, not from an attempted submission. +- Whether `updates.mjs`'s `tar` call works against Windows' bundled bsdtar. +- Whether the iOS pipeline can currently produce an IPA. `PROGRESS.md` says it + could not on 2026-09-11 and nothing since says otherwise. -- 2.51.2