CLAUDE.md@apps/mac · git:20260912.b4ba2d5 · 2026-09-12 · sha256 fd887f96a1a22ca1
CLAUDE.md@apps/mac git:20260912.b4ba2d5A
Immutable. This exact content is served forever at /api/v1/blob/fd887f96a1a22ca1.
# Infinitus — project rules Native macOS menu bar app (`apps/mac` of the Infinitus repo since #823; before that the `native` branch). Split out of `~/death/claude-swap/swift/CswapBar` on 2026-08-29 with history. ## Non-negotiables - **Infinitus is not tied to cswap — forever** (user 2026-09-05: "Infinitus has nothing tight to cswap, not anymore. this is a forever decision"). cswap is one `AccountEngine` adapter among several; no feature, design, data format, CLI or publisher may depend on cswap existing. Anything cross-platform ships from THIS repo (InfinitusCore + InfinitusCLI on Swift for macOS/Linux/Windows), never as a cswap subcommand. - **Everything is Swift; the engine is fully isolated.** Every engine touchpoint is a `swapd … --json` subprocess (InfinitusCore/Engines/Swapd/SwapdCLI.swift). Never read engine internals (`~/.claude-swap-backup/*`). Reading Claude Code's own files is fine: `~/.claude/settings.json`, `~/.claude/sessions/*.json` (+ `.key`), `~/.claude/projects/*/*.jsonl`. - **The resume-nudge mechanism lives HERE, not in the engine** (user 2026-08-30; upstream never merged PR #250's copy). InfinitusCore ClaudeSessions/Transcript/PeerSocket/PtyHosts/PtyNudge/SessionResume + ResumeService. Never rebuild it engine-side. - **Bundle id is `run.infinitus`**, the phone's `run.infinitus.mobile` (+`.widgets`, `.share`), every derived service id under the same prefix (user-approved explicit ask, 2026-09-05, with the paid Apple team `Q783W6B4FA`; before it `com.huuloc.infinitus` from 2026-09-03, `com.huuloc.limitless` from 2026-08-30, before that the CswapBar g2 id). Prefs copy-migrate from the previous id's domain on first launch (AppModel.migrateLegacyDefaults). App Support is `Infinitus/` (copy-migrated from `Limitless/`, which came from `CswapBar/`; legacy dirs left for rollback). The local checkout may still live at `~/death/limitless`. Notification Center and login-item grants key on the id and must be re-granted once under it; keychain items are ACL'd to the old signature, so the proxy key is re-entered and the phone re-paired. Never change the id casually again — the 2026-08-29 casual change cost a day of ControlCenter-ban debugging. - **Improvements ship by default** (user 2026-09-09: "all of these are just experimental. any better improvements are by default valid"): a measured, tested improvement merges without a per-PR go — no holding PRs for approval, no AskUserQuestion for the obvious better option. Still ask before destructive or irreversible actions (a history rewrite, a force-push, deleting user data). - **Push nothing to any remote** unless explicitly asked. Commit locally. - **Layout since #823 (layer 2):** the Mac app lives in `apps/mac` of this repo's `main`, beside the T3 Code fork (TypeScript, never an upstream PR). Before that (#555, 2026-09-10 → 2026-09-12) it was the `native` branch; that branch is frozen and read-only for a fallback window, then deleted. Base Mac work on `origin/main`; a Mac PR is an ordinary main PR. Since #823 layer 3 the Mac app ships inside the one `v<version>` release (`.github/workflows/infinitus-release.yml`, the root `VERSION` file, `apps/mac/docs/RELEASING.md`): both Mac zips, the desktop DMG nesting the Menu Bar bundle, the Linux tray. `releases/latest` and the `nightly` tag are what AboutPane polls; older Mac releases were tagged `mac-v<version>` (AboutPane still strips that prefix). - **`main` takes commits only through pull requests** (GitHub ruleset "main via pull requests"; the native one retires with the branch): 0 required approvals (solo repo); the Mac checks required on a PR head are mac-test, mac-e2e, mac-linux (never mac-windows, 2026-09-06, #206) — path-filtered, so a server-only PR passes them at once. Work on a branch, `gh pr create` (base main), merge with `gh pr merge --squash` (or `--merge` when the branch history matters) once tests pass; `--auto` queues the merge behind the checks. PRs get `size:*` and `vouch:*` labels automatically. - **Every commit carries `Co-Authored-By: Claude Code <noreply@anthropic.com>`** (user 2026-09-04: "some commits still missing Claude in author"). `tools/githooks/prepare-commit-msg` appends it; each clone/worktree owner runs `git config core.hooksPath tools/githooks` once (shared across worktrees of one clone). Subagent briefs still say it explicitly. - Secrets (webhook URLs, bot tokens) travel over stdin, never argv; shown masked only. Usage-cost figures are estimates, never billing truth. - **Release notes: one feature, one line** (user 2026-09-04). A CHANGELOG bullet is a single short sentence — no multi-sentence paragraphs, no wrapped essays; details live in the site/README, not the note. - Surgical changes; match existing style; no speculative abstractions. - **Todos and research notes go to GitHub issues, never to files** (user 2026-09-04: "stop noting TODO file to avoid a PR, just log to issue"). `gh issue create` / `gh issue comment`; docs/TODO.md is the shipped log only. - **Account policy lives in the engines** (user 2026-09-03). Auto-swap, pick-first, ordering come from each engine's own knobs (cswap `autoswitch.*`, the proxy's priority); the app only sets those and never runs a second policy on top (the app-side auto-order writer was removed for this). Missing knob → upstream PR, never a fork. - **Keep performance in check with every feature** (user 2026-09-03): idle CPU with the pop-out open must stay near 0% (`infinitusctl perf` twice, 15s apart; `tools/e2e.sh` gates it in CI). Any continuous motion goes through Core Animation (`LayerEffect`), never a TimelineView / repeatForever `.animation` — see the hard-won fact. ## Hard-won facts - NSPopover measures content once; wholesale content-shape swaps must go through `withAnimation` so it re-measures live. Never close/reopen. - macOS ignores Dynamic Type — popup scaling is `PopupScale` (fixedSize → measure → scaleEffect + matching frame). Measuring without `fixedSize` feeds the scaled width back in and runs away ×scale. - The pop-out window must NOT let NSHostingView size it (crash on unbounded ideal width); PinnedRoot reports its fixedSize geometry and `fitPinned` applies it. - Never `cp` over the RUNNING unbundled binary — overwriting a signed executable in place gets the process killed on its next page-in (the dev instance "mysteriously died" 2026-08-30). pkill first. - macOS 26 ControlCenter can stop adopting new bundled apps' status items after rapid relaunch churn — only a logout clears it; `run-unbundled.sh` is the workaround. Don't run the dev loop's kill/reopen cycle for hours. - Never hide the status item with `NSStatusItem.isVisible` (#876): off then on left the app pulling WindowServer datagrams every frame on the CI runner (`remote_context_notify` → tracking-area + hosting-view layout each display cycle, idle 5–11% with Settings open; a Mac with a real GPU shows nothing). `menu_bar_enabled` off removes the item (`removeStatusItem`) and on installs a fresh one (`installItem()`), which idles at 1%. Tearing the layer effects down while hidden changed nothing — it is the item, not the animations. - NSPopover windows refuse CABackdropLayer at every level (renders a black slab; probed 2026-08-30) — the anchored popup is therefore a borderless non-activating NSPanel. CABackdropLayer + CAFilter gaussianBlur in a plain window is the only tunable-blur glass. - NSGlassEffectView does NOT deactivate when its window resigns key — the "goes solid unfocused" repro was the probe window being occluded. Never reintroduce a focus-swap around it; glass runs in all states. - usernoted refuses dev-cert builds without a provisioning profile; notifications fall back to osascript (working mode, not an error). - SwiftUI Grid: spanning cells span the widest row's real column count; placeholders need `.frame(maxWidth: .infinity)` + `.gridCellUnsizedAxes(.horizontal)`. - macOS 26: a VStack of mixed text+gauge rows under-reports its ideal HEIGHT under two-axis fixedSize (last row clips to slivers) — give every such row its own `.fixedSize()`. - A GridRow can't wear a modifier (collapses to one cell), but a Group INSIDE it distributes the modifier to every cell — that's how the rows intro slides a whole grid row. - Multi-engine (#8): every engine is an `AccountEngine` yielding `EngineFleet`s; the popup stacks one `FleetState` per fleet and `AppModel` is only a FleetModel FACADE over the primary Claude fleet. Gate UI on `capabilities`, never on engine identity. - The CLIProxyAPI key lives in the keychain (`run.infinitus.cliproxy`, account = base URL). Unsigned debug binaries trip an ACL prompt on every rebuild — reads skip UI, and the dev loop codesigns the debug binary with the Apple Development identity so the grant sticks. - A dev instance never pushes Live Activities from the shipped app's APNs key (#845): the `.p8` item's decrypt ACL names `Infinitus.app` only, `Keychain.read` skips UI, so the dev-signed binary reads nil and the pusher stays unconfigured — silently (no last-result line, no event). Verify push changes on the tagged build; never paste the key into the instance's Devices pane (`Keychain.write` deletes the shipped app's item first). - Every SwiftUI-driven frame (TimelineView tick, repeatForever `.animation`) commits a CA transaction: display-list diff, AppKit drag-region + tracking-area rebuild, a WindowServer fence — ~7 ms each, the same whether one leaf or the whole grid changed. Five RPG effects at 20 fps idled the pop-out at 43% CPU (#18, 2026-09-03); as CAAnimations on a LayerEffect host they idle at 0.4%, burn overlays included (CAEmitterLayer sparks; a `.line` emitter's emissionLongitude is a quarter turn off a `.point` one's: 0 = up). A per-second `.contentTransition(.numericText)` grows the CG glyph cache ~2 MB/min for as long as it ticks (macOS 26) — never on a countdown; the e2e gate checks idle heap growth via `perf.heapBytes`. An ordered-out window keeps its SwiftUI content ticking (the wall's 15 fps TimelineView cost ~8% idle after every visit): detach the hosting controller on close, and reuse the NSWindow — a closed borderless one lingers in AppKit's list regardless. - Dev instances: sign the debug binary `--identifier run.infinitus` (tools/e2e.sh does) or the keychain ACL prompt blocks AppModel.init forever (no socket, SecurityAgent spawns). `swift build --target X` may not relink — use `--product`, ONE per invocation: with two `--product` flags SwiftPM builds only the last (CI's e2e ran a stale app binary for a day, 2026-09-03). - Several Claude sessions work this repo: each in its OWN worktree (one `cd` there; separate `apps/mac/.build`). The main checkout (`~/death/limitless`) is merge-only (`git pull --ff-only origin main`) and owned by the coordinating session. Since 2026-09-12 the user's menu bar is the helper nested in Infinitus desktop (`/Applications/Infinitus.app/Contents/Library/LoginItems`), so nothing relaunches a local `Infinitus.app` on the real control socket: Mac changes reach the user through the one `v<version>` release, which nests the Menu Bar bundle of the same sha (#777, #823). Dev instances run on `INFINITUS_CONTROL_SOCKET`. Never edit another session's tree; in any tree stage by explicit path. - Linux corelibs `Process`: one waited on through its `terminationHandler` alone is never freed — its run-loop source retains it back and `CFRunLoopSourceInvalidate` keeps that context on purpose; only `waitUntilExit()` (instant once the child is gone) clears the source. Every leaked child keeps two pipe ends, and at ~1,300 descriptors `Process.run()`'s `/proc/self/fd` walk reads past its readdir buffer (signal 11, #510; upstream fix not in any shipped toolchain). Darwin's Foundation has neither problem. `.github/workflows/linux-sanitize.yml` runs the suite under ASan on demand. ## Release - Every release updates **site/** (infinitus.run) and the **GitHub README** with the new features so app, site and README stay in sync (user 2026-09-03). Do it in the release commit, not after. - **The site deploys by hand** (ruling 2026-09-12, #823: no deploy workflow, none ever existed): after the release merges, from `apps/mac/site` run `npx wrangler deploy` (the Cloudflare login is the user's; the custom domain and the RENDEZVOUS KV bind from `wrangler.jsonc`). Nothing in CI touches infinitus.run. - Any dev/smoke instance of the app (debug binary, `-mock_mode`, a second bundle) MUST run with `INFINITUS_CONTROL_SOCKET=/tmp/<short>.sock` — without it, ControlServer.start() unlinks the real app's control socket and `infinitusctl`/the phone get "connection refused" until the bundle relaunches (bit us 2026-09-03 08:21). Short path: unix sockets cap at ~104 bytes, so never the scratchpad dir. It also needs `INFINITUS_MIRROR_SNAPSHOT=<state-dir>/mirror-snapshot.json`, or MirrorExporter overwrites the real app's mirror snapshot (#474), and `INFINITUS_APP_SUPPORT=<state-dir>/app-support`, or every other file it writes (stats caches, births, the owned ledger, events…) lands in the real Application Support/Infinitus (#506; the stats clobber cost each relaunch a year's re-read, #346). ## Build / run / test `./make-app.sh && open Infinitus.app` · `swift test` · `./dev.sh` (entr) · `./run-unbundled.sh` (menu bar wedge workaround).