From 9c3d61820e66b8c4e21d4643439872e8478a19de Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Thu, 20 Aug 2026 15:49:44 -0500 Subject: [PATCH 01/12] docs: mattstack.app installer/onboarding/updater design (ratified 2026-08-20) + research records Co-Authored-By: Claude Fable 5 --- ...26-08-20-mattstack-app-installer-design.md | 534 ++++++++++++++++++ .../research/research-dependency-inventory.md | 40 ++ .../specs/research/research-linear-rulings.md | 41 ++ .../research/research-local-inventory.md | 48 ++ .../research-onboarding-permissions-ux.md | 52 ++ .../research-sparkle-install-launchd.md | 37 ++ 6 files changed, 752 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-20-mattstack-app-installer-design.md create mode 100644 docs/superpowers/specs/research/research-dependency-inventory.md create mode 100644 docs/superpowers/specs/research/research-linear-rulings.md create mode 100644 docs/superpowers/specs/research/research-local-inventory.md create mode 100644 docs/superpowers/specs/research/research-onboarding-permissions-ux.md create mode 100644 docs/superpowers/specs/research/research-sparkle-install-launchd.md diff --git a/docs/superpowers/specs/2026-08-20-mattstack-app-installer-design.md b/docs/superpowers/specs/2026-08-20-mattstack-app-installer-design.md new file mode 100644 index 00000000..45599197 --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-mattstack-app-installer-design.md @@ -0,0 +1,534 @@ +# mattstack.app: suite installer, onboarding, permissions, updater + +Status: design, ratified decision-by-decision with Matt on 2026-08-20 (sixteen +forms; answers recorded in §2). Supersedes the board wizard (MAT-382 r.5 face) +and the curl front door (MAT-360). Extends MAT-383 (phase 1 rebrand shipped +2026-08-20) into phase 2. Research records that ground this design live in +`research/2026-08-20-mattstack-app/` next to this file: Linear rulings, local +inventory, dependency inventory, macOS onboarding/permissions UX, Sparkle/ +install/launchd conventions. + +## 1. Purpose and scope + +mattstack.app (the rebranded rt-tray, `com.mattstack.app`) becomes the one way +the suite is installed, set up, kept permitted, and updated. A person downloads +a DMG, opens the app, and a setup window walks them to a working machine: rt on +PATH and its daemon running, deck/board/gitq running as services, the mattstack +skills pack and their team's pack installed into Claude Code, the editor +extension installed, macOS permissions granted, accounts connected, team joined +or created, and `rt verify` green. From then on the app keeps itself and every +bundled tool current via Sparkle. + +Audience, from day one: (a) Matt's teammates, invited by Matt, who never see a +git remote; (b) cold users with no team who create a team of one; (c) Matt in +dev mode. **One build target.** Team dogfood is a release gate, not a phase. +Public release follows dogfood without a rewrite. + +In scope: the setup window and its five screens, the readiness checklist model +and its rt verbs, team create/join and the invite system, the dependency +bundling policy, launchd service registration, the permission rows, secrets via +RT-32, distribution (DMG + Sparkle), identity freeze, dev mode, clean-room +testing, uninstall, and the work lanes. Out of scope: redesigning the tray's +day-to-day UI (process panel, menu), the board/gitq/deck products themselves +beyond giving them releasable artifacts, and the peering relay's encryption +(recorded as a constraint on that lane). + +## 2. Rulings (locked 2026-08-20) + +| # | Decision | Ruling | +|---|---|---| +| 1 | Milestones | One build target, public-ready; dogfood = release gate. | +| 2 | Install shape | Suite-only (MAT-379 r.1 stands). Team settings decide what is *live*; no component checkboxes; unused services show as idle, not absent. | +| 3a | Invites | Shared mattstack switchboard (Railway) hosts **opaque** invites: client-encrypted team pointer, relay stores ciphertext + timestamps only; code = id + key; one-time, 7-day expiry, replace = revoke; static landing page `mattstack.dev/join#`. | +| 3b | Git remotes | **Required up front** for both user and team repos, explained in the wizard: mattstack keeps your settings in git for safety and for a paper trail (skill edits and every change visible in history). `gh` one-click private repo when authenticated, else paste a URL. | +| 4 | Checklist | The readiness checklist is the wizard's spine: rt computes it (`rt setup plan --json`); every row has a real validator; Install is disabled until required rows are green; bare `rt setup` and `rt verify` run the same validators. | +| 5 | Architecture | Thin native shell over rt verbs. Swift owns menu bar, windows, Sparkle, SMAppService, tray-socket API; every mechanic is an rt verb; permission status flows back to rt over the socket. The relay is something rt's invite verbs talk to, never an app runtime dependency. | +| 6 | Build tooling | Xcode project generated from a committed `project.yml` (xcodegen); `build.sh` wraps `xcodebuild`; `check-bundle.sh` keeps asserting the bundle contract. Matt installs Xcode 26. | +| 7 | Distribution | The app IS the release (rt + helpers + suite binaries + extension inside). Notarized, stapled DMG for first install; zip enclosure + CI-signed appcast for Sparkle 2.9; `~/.local/bin/rt` is a symlink into the bundle; app re-registers + kickstarts agents when its version changes; `rt update` asks the app. **arm64-only** for now. | +| 8 | Dependencies | Four-way policy (§7): BUNDLE for internal use by absolute path; PATH exposure per tool opt-in, tagged, yields to the user's copy; PROVISION herdr + Claude Code via brew-if-present else vendor installer, version floors only; SYSTEM git/python3 via Apple CLT; TEAM-DECLARED tools from the pack manifest. Deck's proxy via **one admin prompt**. tmux/zellij/terminal-notifier dropped. | +| 9 | Services | SMAppService registers rt daemon + deck (one plist each in the bundle, one Login Items switch, FDA inherits); deck supervises board + gitq as managed deck apps (MAT-384); app monitors deck like the daemon; plist PATH set explicitly to the bundle's helper dir; portless proxy is a root LaunchDaemon owned by the privileged step. | +| 10 | Permissions | Full Disk Access + Login Items required; Notifications optional. No Accessibility/Screen Recording/Automation. | +| 11 | Secrets | RT-32 as ruled: sops/age-encrypted in the user repo, age key generated at setup into the macOS Keychain, plaintext never in git; `secrets.json` is a one-time migration source; invitee's age public key is added as a recipient for team-scoped secrets. | +| 12 | Dev + testing | Dev identity stays (mattstack-dev.app). XCTest + XCUITest against a stub rt; rt verbs under bun test. Clean room = (a) GitHub Actions macOS runner headless install + `rt verify --ci`; (b) local Apple-Silicon VM (Tart/VirtualBuddy) golden image, restored per run, scripted walkthrough; (c) second macOS user account as daily smoke. Release gate = (a) + (b) green. | +| 13 | Identity | Frozen before the first teammate installs: `com.mattstack.app` / `.app.dev`; `com.mattstack.daemon` / `.dev`; `com.mattstack.deck`; embedded binary renamed `rt-daemon` → `rt` now; product name mattstack; TLD `.mattstack`; `~/.mattstack`; **macOS 14 floor**. | +| 14 | Uninstall | Settings → "Uninstall mattstack…" and `rt uninstall`, same rt code path; shows the list, reverses everything the installer did, asks about `~/.mattstack` (default keep), trashes the app. | +| 15 | UI | Five screens + Settings with four panes (§4). Stock macOS 26 controls, `Form(.grouped)` rows, Ice/AltTab shape. GPL sources are design reference only; Rectangle/inket (MIT) borrowable. | +| 16 | Lanes | Seven lanes (§13); start L7 + L3 + L1/L2 now; L4/L5/L6 next. | + +### Invariants (binding on every lane) + +- **No user or employer data ever lands on mattstack-hosted infrastructure.** + Team data lives on the team's own forge. The shared relay sees ciphertext and + timestamps. Peering envelopes (which carry MR URLs) must be end-to-end + encrypted before they may traverse a shared relay; until then teams run their + own relay as today. +- rt owns mechanics, the app owns ceremony; every ceremony has a CLI equivalent + (MAT-374 r.6). The wizard never does something `rt setup` cannot. +- Honesty over magic: every checklist row reports what was actually checked; + nothing is marked ready on a guess. +- Settings are read and written only through the RT-47 stores via `rt settings`; + state only through each app's state.db. The app has no config files of its + own beyond UserDefaults for window/UI state. +- The installer never copies a user's `~/.claude/settings.json` or hooks; it + adds marketplaces and plugins only. +- Pure canonical, no compat: `rt-tray.app`, `com.rt.daemon`, `~/.rt`, brew paths + are swept, never honored. + +## 3. The flow + +``` +DMG → drag to Applications → open + └─ app launches; menu-bar "m" appears; no daemon.json → Setup window opens + 1 Welcome ──► 2 Your team ──► 3 Readiness checklist ──► 4 Install ──► 5 Done + (create or join) (rt setup plan) (rt setup apply) (rt verify) +``` + +Re-entry: menu "Setup status…" reopens screen 3 as a health view; Settings (⌘,) +exposes Permissions, Team (Invite…), General, Uninstall. A `mattstack://join/` +link opens the app on screen 2 with the code filled in; if setup is already +complete it opens Settings → Team → Join. + +## 4. The screens + +Window: one dedicated `NSWindow` hosting SwiftUI (AppKit lifecycle stays), +~560 pt wide, fixed, no close/minimize buttons while setup is incomplete (Quit +remains available from the menu), Back/Continue bottom-right, Continue bound to +Return, `.controlSize(.large)`, custom `enum Step` page model with push +transitions (not `NavigationStack`), a subtle "Step n of 5" indicator. Rows use +`Form(.grouped)` / `LabeledContent`. Stock macOS 26 controls; no custom design +system. Status glyphs: `checkmark.circle.fill` green ready · `xmark.circle` red +failed · `exclamationmark.triangle` yellow needs you · `circle.dotted` grey not +checked/optional-skipped · `ProgressView` small while checking. Buttons that +open System Settings end with an ellipsis. + +### 4.1 Welcome + +Brand mark, one sentence, then a plain list of what setup will do to this Mac +(install the `rt` command into `~/.local/bin` and add one PATH line to the +shell rc; run background services: rt daemon, deck, board, gitq; install the +mattstack skills into Claude Code; install the editor extension; ask for Full +Disk Access and background-item approval). One line that everything is +reversible from Settings → Uninstall. Continue. + +### 4.2 Your team + +Two cards. + +**Create a team.** Fields: team name (slug preview shown), "Others will join +later" checkbox (default on). Remote: if `gh auth status` succeeds, a checked +box "Create a private GitHub repo `/mattstack-team-`" with the +owner selectable (user or an org the token can see); otherwise a URL field +("paste an empty repo's URL; GitHub, GitLab, anything git can push to"). A short +explainer under the fields: *mattstack keeps your team settings in git. That +keeps them safe and gives you a paper trail: skill edits and every change are +visible in history.* The same explainer covers the user repo, created by the +same step (`~/.mattstack/user/`, remote `/mattstack-prefs` by default). +Continue validates both remotes with `git ls-remote` using the user's +credentials and moves on; nothing is pushed until Install. + +**Join a team.** One field: invite code (prefilled from a deep link). Continue +redeems it through rt (`rt team join --dry-run `): decodes, fetches the +ciphertext from the relay, decrypts, and checks `git ls-remote` on the team +remote with the user's credentials. Success shows "Joining ** (owner +**)". Failure states are specific: *invite not recognized or expired: +ask for a new one*; *you don't have access yet: ask to grant you +access to * (no git output, the remote URL is not shown); *code is for a +different forge account than you're signed into*. The user repo is created here +too, remote required, same explainer. + +### 4.3 Readiness checklist ("Before we begin") + +Rendered from `rt setup plan --json` (§5). Groups in order: + +- **Your Mac** — macOS version ≥ 14; Apple command line tools (git, python3) + → *Install…* runs `xcode-select --install` (Apple's dialog); `~/.local/bin` + will be on PATH (informational, fixed by Install); **Full Disk Access** → + *Open Full Disk Access Settings…*; **Background services** → *Open Login + Items…* when `.requiresApproval`; **Notifications** (optional) → *Allow* + (system prompt; if denied → *Open Notification Settings…*). +- **Accounts** — only what the team declares (team-of-one default: the forge + of the team remote). GitHub: *Use gh* when `gh auth status` is good, else + token field; GitLab token; Linear key; Slack → *Connect* (OAuth in browser). + "Ready" means the validator called the API and confirmed the token can see + the team's resources (group/org membership, the team repo). +- **Access** — team repo reachable; each repo the team lists is visible with + those credentials; forge host reachable; switchboard (if the team has one) + answers. +- **Tools** — herdr ≥ floor (present → version shown; missing → *Install*: + brew if present else vendor installer); Claude Code (same); an editor we can + install the extension into (optional); Google Chrome (optional unless the + team's pack requires it); **team-declared tools** from the pack manifest + (doppler, sdm, ldcli, pnpm, Postgres, MCPs…) each with *Install* / *Connect* + and the pack's one-line why. + +Row anatomy: symbol · title · one sentence why · status badge · one button. +Required vs optional is visible (optional rows say "works without this"). +Permission rows re-check on a 1 s timer while the window is visible and on +`didBecomeActive`; other rows re-validate on change and on *Re-check*. Continue +(labelled **Install**) enables when every required row is ready. A *Continue +in limited mode* path exists only when all required rows are green and optional +ones are not. Permission status for the three permission rows is computed by +the app (§9) and merged into the plan by rt via `GET /permissions` on tray.sock, +so the terminal `rt setup` shows identical rows. + +### 4.4 Install + +A live list driven by `rt setup apply --json` (NDJSON stream): each step has +title, state (pending/running/done/failed/skipped), detail, and a log handle. +Steps (order is rt's; shown here for the reader): create user repo + push; +create team repo + push / clone team repo; generate age key into Keychain +(RT-32) and write collected secrets; link `rt` into `~/.local/bin` (+ PATH line ++ shell integration block); write machine settings (`rt.repoRoots` seeded, +detected roots offered); register rt daemon + deck (app, via socket request +from rt); privileged step: install proxy daemon + CA trust + sudoers (one admin +prompt, raised by the app); register board + gitq as managed deck apps; install +marketplaces + plugins (mattstack, fast-browser, team pack) per Claude config +dir; install editor extension; start services; `rt verify`. A failed step stops +the list on that row with remediation text and *Show log*; *Retry* resumes from +the failed step (steps are idempotent). Nothing runs that was not listed. + +### 4.5 Done + +"Everything's working" with the verify summary (N checks), where things live +(the `m` in the menu bar; `rt` in a new terminal; `https://board.mattstack`), +buttons *Open the board*, *Invite teammates…* (owners), *Finish*. Closing the +window is now allowed. + +### 4.6 Settings (⌘,) + +- **General**: start at login (SMAppService.mainApp), automatic updates + (Sparkle), check now, dev mode (prod/dev handoff as today), version. +- **Permissions**: the three rows from §4.3 plus *Reset & re-request* (runs + `tccutil reset` for our bundle id and reprompts; for the stale-grant case). +- **Team**: name, remote (masked to host + repo, copy), backup status (last + push), members with access (from the forge, when the token can see it), + *Invite…* (§6), *Join another team…*. +- **Uninstall…** (§12). + +## 5. Architecture and the rt contract + +### 5.1 Components + +- `mattstack.app` (Swift, Xcode project): `AppDelegate` (menu bar, health + polling, notifications as today), `SetupWindowController` + SwiftUI + `SetupView` (screens), `ReadinessModel` (renders plan JSON, owns timers), + `PermissionsService` (§9), `ServicesRegistrar` (SMAppService for N plists, + version-change restart), `PrivilegedInstaller` (AuthorizationServices admin + prompt for the proxy step), `UpdaterController` (Sparkle), `TrayServer` + (routes below), `RtClient` (spawns the bundled `rt` by absolute path with + `--json`, streams NDJSON). +- `rt` (TypeScript): `commands/setup/*` verbs, `lib/setup/plan.ts` + (requirements + validators), `lib/setup/apply.ts` (steps), `lib/team/*` + (create/join/publish/invite), `lib/secrets/*` (RT-32), `lib/deps/*` + (bundled-tool resolution, tagged PATH links), `commands/uninstall.ts`. +- The shared switchboard (mr-board repo `switchboard/`): opaque invite + endpoints (§6.3). + +### 5.2 rt verbs (the contract the app drives) + +All JSON-emitting verbs accept `--json`; streaming verbs emit NDJSON lines of +`{event, ...}`. Exit codes: 0 ok, 2 user-actionable failure (payload says +what), 1 bug. + +| Verb | Purpose | +|---|---| +| `rt setup plan [--team ] --json` | Compute the readiness plan for the current (or given) team. | +| `rt setup status --json` | Same rows, post-install, as health (used by "Setup status…" and `rt verify` reuses validators). | +| `rt setup connect [--token-stdin] --json` | Validate + store a credential (github/gitlab/linear/slack/switchboard…); `--use-gh` borrows gh auth. | +| `rt setup apply [--from ] --json` | Run the install steps (NDJSON). | +| `rt setup` (TTY) | Interactive walk of the same plan; the headless/catastrophe path. | +| `rt team create --remote [--others] --json` | Init team repo with starter `mattstack/settings.jsonc`, set remote; push happens in apply. | +| `rt team join [--dry-run] --json` | Redeem invite (relay fetch + decrypt), clone under `~/.mattstack/teams//`. | +| `rt team invite [--handle ] --json` | Mint an invite: encrypt pointer, POST to relay, optionally grant forge access + add age recipient; prints code + paste block. | +| `rt team publish --remote --json` | Push a team repo to a remote (also used when a remote must change). | +| `rt user init --remote --json` | Init user repo (`~/.mattstack/user/`), set remote. | +| `rt secrets init/set/get/rotate` | RT-32 surface; `setup connect` uses it. | +| `rt deps resolve --json` | Absolute path of the bundled tool + whether a user copy exists; used by rt internally and by the Tools rows. | +| `rt deps link / unlink` | Opt-in PATH exposure (tagged symlink), auto-unlink when a user copy appears. | +| `rt services list/register/restart --json` | Facade over tray.sock for the app-registered agents + deck-managed apps. | +| `rt update` | `POST /update/check` on tray.sock; if the app is not running, prints where updates come from. | +| `rt uninstall [--keep-data|--delete-data] --json` | §12. | +| `rt verify [--json]` | Existing command; its checks become the plan's validators (one implementation). | + +Plan JSON (abridged): + +```json +{ "team": {"name":"assured","mode":"join"}, + "groups": [{ "id":"mac", "title":"Your Mac", "rows":[ + { "id":"perm.fda", "kind":"permission", "title":"Full Disk Access", + "why":"Reads your repositories' git state so the daemon can show branch and MR status.", + "required":true, "status":"needs-you", "detail":"Not granted", + "action":{"type":"open-settings","target":"fda"} }, + { "id":"tool.clt", "kind":"tool", "title":"Apple command line tools", "required":true, + "status":"ready", "detail":"git 2.50.1", "action":null } ]}, + { "id":"accounts", "title":"Accounts", "rows":[ + { "id":"account.gitlab", "kind":"account", "title":"GitLab", + "why":"The team's merge requests live on gitlab.assured.com.", + "required":true, "status":"missing", "action":{"type":"connect","integration":"gitlab","fields":[{"name":"token","secret":true,"hint":"read_api, read_user"}]} } ]} ], + "canInstall": false, "requiredMissing": ["perm.fda","account.gitlab"] } +``` + +`status` ∈ `ready | missing | invalid | needs-you | checking | skipped | error`; +`action.type` ∈ `open-settings | request-permission | connect | install | +use-gh | open-url | none`. The app never invents rows; it renders what rt sends +and calls the action's verb. Permission rows are the exception in *evaluation* +only: rt asks the app (`GET /permissions`) and folds the answer in, so one +plan serves wizard and terminal. + +### 5.3 tray.sock routes (additions) + +`GET /permissions` → `{fda, notifications, loginItems}` each +`{status, detail}`; `POST /permissions/request` `{which}`; `GET /services` → +registered agents + status; `POST /services/register` `{plist}` / +`/restart` `{label}`; `POST /privileged/proxy-install` (app raises the admin +prompt, runs the bundled installer helper, returns result); `POST /update/check`; +`GET /version`. Existing routes unchanged. + +### 5.4 Where logic lives + +Validators, step logic, team/invite crypto, secrets, dependency resolution: +TypeScript, unit-tested with bun, no Swift. Swift holds view state, timers, +permission probes, SMAppService calls, the admin prompt, Sparkle, and the +socket server. The stub rt used by XCUITest is a tiny Bun script emitting +canned plan/apply JSON for named scenarios (`RT_STUB_SCENARIO=join-happy`), +selected by an env var the app honours only in DEBUG builds. + +## 6. Team and invite model + +### 6.1 Everyone has a team + +`~/.mattstack/teams//` is a git clone with `mattstack/settings.jsonc` +(team scope of the RT-47 resolver) and the team's pack(s). A team of one is the +same shape. The user repo `~/.mattstack/user/` holds user-scope settings and +the RT-32 encrypted secrets. Both repos have remotes from creation (ruling 3b); +the wizard explains why in plain words and never asks the user to run git. + +### 6.2 Create + +`rt team create` writes the starter settings (forge host inferred from the +remote, integrations empty), sets the remote; `rt user init` likewise; Install +pushes both. If `gh` is authenticated, the app offers to create the private +repos in one click (owner = user or an org); otherwise URL fields. The team's +"declared integrations" start as what the remote implies (GitHub → github; +GitLab host → gitlab) and grow when the owner connects more in Settings → Team +or via the pack. + +### 6.3 Invites (opaque, shared relay) + +Owner side (`rt team invite`): + +1. Pointer = `{v:1, team:, name, remote, owner:, forge:, createdAt}`. +2. Generate 32-byte key; encrypt pointer (XChaCha20-Poly1305 or AES-256-GCM); + `POST /v1/invites` to the relay with `{ciphertext, expiresAt}` → `{id}`. + Relay stores `id`, ciphertext, created/expires/redeemed timestamps only. +3. Code = base32(id ‖ key) chunked for reading (~40 chars). Paste block: "Install + mattstack from , then open `mattstack://join/` or paste + the code into Setup → Join a team." +4. Optional in the same action, when the owner gives a forge handle: grant read + access to the team repo (`gh api` / GitLab API with the owner's token), and + record the invitee handle in the team's `members` list so the redeem can + report back. Age recipient for team-scoped secrets is added when the invitee + reports their public key (redeem posts it to the relay as a second opaque + blob keyed by the same id, readable once by the owner's next + `rt team members sync`). +5. Replace-on-mint: a new invite for the same handle replaces the old id (the + relay deletes it). Expiry 7 days. Relay prunes hourly. + +Teammate side (`rt team join`): decode → `GET /v1/invites/` → decrypt → `git +ls-remote ` with the user's credentials → clone → mark redeemed (`POST +/v1/invites//redeem`, one-time) → materialize (pack marketplace add + +install; settings now resolve at team scope). Failure messages in §4.2. + +Relay (mr-board `switchboard/`, Railway, `mattstack` shared instance): +`POST /v1/invites` (any client; rate-limited), `GET /v1/invites/:id` (returns +ciphertext if unexpired and unredeemed), `POST /v1/invites/:id/redeem` (CAS), +`POST /v1/invites/:id/reply` (second opaque blob, once), `DELETE` by the +creator (holds a creator secret returned on create). No admin token needed for +team invites; board-peering invites keep today's admin-minted flow on a team's +own relay until peering envelopes are E2E encrypted. The invariant: no field on +the relay is ever plaintext employer data. + +Landing page: `https://mattstack.dev/join#` — static; reads the fragment +client-side, shows Download + "Open in mattstack" (`mattstack://join/`). +The fragment never leaves the browser. + +## 7. Dependencies + +Policy (ruling 8), derived from the full inventory (research/dependency +inventory): + +| Class | Tools | How | +|---|---|---| +| **Bundle** (Contents/Helpers, signed, pinned via `deps.lock` in CI with sha256) | rt (Contents/MacOS/rt), fzf, jq, bun, gh, glab, private node (for fast-browser + portless only), fast-browser, deck, board, gitq | Used by the suite **by absolute path**. PATH exposure per tool is opt-in from the Tools row ("Use mattstack's"), implemented as a tagged symlink in `~/.local/bin` that rt removes automatically when a non-ours copy appears on PATH (`rt deps` reconciles on `rt verify`, daemon start, app launch). Version floors apply to internal use only; the user's copies are never judged. | +| **Provision** | herdr (≥ floor), Claude Code | Present → floor check. Missing → *Install*: `brew install` if brew is on the machine, else the vendor installer (`herdr.dev/install.sh`, `claude.ai/install.sh`) into `~/.local/bin`. Their own updaters own them thereafter. Never bundled (self-updaters, proprietary). | +| **System** | git, python3 (stdlib only), shells, launchctl/osascript/security/etc. | Apple CLT; the row's *Install…* triggers `xcode-select --install`. | +| **Team-declared** | doppler, sdm, ldcli, pnpm, Postgres, Linear/local-db MCPs, Chrome-for-evidence… | The pack manifest declares `tools: [{name, floor, why, install:{brew, url}, connect?}]`; rows render from it; nothing of the team's is bundled. | +| **Dropped** | tmux, zellij (no callers), terminal-notifier (osascript fallback) | Removed from README/verify. | +| **Privileged** | portless proxy: root LaunchDaemon on :443 + local CA trust + `/etc/sudoers.d/mattstack-proxy-restart` | One admin prompt (AuthorizationServices) raised by the app at the Install step; installs the bundled node+portless privately under `~/.mattstack/deck/`, the daemon plist, CA trust. A deck ticket evaluates absorbing the proxy later. | + +Accounts-bearing tools (gh, glab, doppler) keep their auth in the user's +`~/.config`, so whether the bundled or the user's binary runs, login happens +once. Services' plists set `PATH=/Contents/Helpers:/usr/bin:/bin` plus +`~/.local/bin` for herdr/claude; nothing is captured from the user's shell. + +Consequences for other repos (lane L5): board and gitq ship as `bun --compile` +binaries with published rt-client (no `file:` deps); deck publishes releases; +fast-browser publishes its artifact; the mattstack marketplace becomes a +published repo (MAT-360) instead of a directory of symlinks; the claimview +pack fixes the already-broken `dev-ports.state.json` paths and declares its +tools. rt's two latent `bun`-on-PATH branches (`lib/enrich.ts`, `bunx +pino-pretty`) switch to the bundled bun. + +## 8. Services and launchd + +- Bundle ships `Contents/Library/LaunchAgents/com.mattstack.daemon.plist` and + `com.mattstack.deck.plist` (dev flavor: `.dev` labels), `BundleProgram` under + `Contents/MacOS`/`Contents/Helpers`, `KeepAlive {SuccessfulExit:false}`, + `ThrottleInterval 10`, explicit `EnvironmentVariables.PATH`, no stdout/err + paths (macOS 26 `$(HOME)` breakage; logs go to `~/.mattstack/rt/logs` and + `~/.mattstack/deck/logs` by the programs themselves). `AssociatedBundleIdentifiers` + kept (harmless; needed only for legacy plists). +- `ServicesRegistrar` registers both on first run and on every launch + (idempotent), surfaces `.requiresApproval` as the Background services row, + and on a version change (UserDefaults `lastLaunchedVersion` ≠ bundle version) + re-registers then `launchctl kickstart -k gui/$UID/