OMI Desktop App for macOS (Swift)
- App log file:
/private/tmp/omi.log(production). Each non-production launch writes to its own owner-only log; ask the running named bundle for its exact path with./scripts/omi-ctl log-pathrather than reading a shared dev log.
Check errors in the latest (or specific) release using the sentry-release skill:
./scripts/sentry-release.sh # new issues in latest version (default)
./scripts/sentry-release.sh --version X # specific version
./scripts/sentry-release.sh --all # include carryover issues
./scripts/sentry-release.sh --quota # billing/quota statusRun the script with --help for the full option list.
When debugging issues for a specific user, check Sentry dashboard for crashes and PostHog for events.
- A desktop chat query starts after local concurrency/quota preflight and must
emit exactly one terminal outcome:
completed,failed, orcancelled. Intentional Stop and supersession are cancellations, never errors. - A physical voice shortcut turn emits one start and one terminal outcome from the coordinator. Full-answer duration ends at playback drain; a later journal failure does not rewrite a delivered response as missing. Intentional and too-short endings are excluded from response-failure rates.
- Query latency ends when the final answer is visible. Persistence, title generation, and other post-answer work have their own reliability signals and must not inflate user-visible query duration.
- Product authority is independent from telemetry. Revoked or timed-out turns cannot apply late callbacks/results or persist a late response even if analytics is disabled or refactored.
- PostHog receives bounded dimensions and shape metadata only. Never send raw prompts, responses, notification/window titles, filesystem paths, or exception messages. Keep diagnostic detail in the private local log and Sentry.
- Production
QueryTraceroutput is shape-only and stored under a0700directory in0600files. Full prompt/response/tool content is a deliberate non-production debugging capability only.
Provider/mode switches and fail-open paths must call DesktopDiagnosticsManager.recordFallback(area:from:to:reason:outcome:) (PostHog desktop_health_event / fallback_triggered) or Rust fallback::record_fallback. Same field contract as root AGENTS.md → Fallback / resilience telemetry. Do not invent new health-event enum cases or product “Recording Error” events for successful heals (outcome=recovered).
- This is the
desktop/macos/subfolder of the OMI monorepo (BasedHardware/omi) - macOS Swift app + Rust backend live here
Beta candidates are cut on every macOS-affecting merge to main (push trigger on the releasable desktop paths, plus a 15-min schedule backstop). Each run tags the newest releasable desktop/macos/**/codemagic.yaml change since the last tag, binding the immutable tag to the Codemagic config that builds it. A candidate reaches beta only after qualification passes:
- GitHub Actions (
desktop_auto_release.yml) — on thatpush(plus the 15-minschedulebackstop and manualworkflow_dispatch) it auto-increments the version and pushes av*-macostag. A ~60s quiet window (AUTO_RELEASE_QUIET_SECONDS) coalesces near-simultaneous merges; the one-active-release fence (Codemagic build status on the latest tag) admits one candidate at a time. Fails closed unlessRelease Eligibility,Desktop Swift Build & Tests, andDesktop Swift Release Compileall succeeded for the exact newest releasable SHA — so a candidate cuts once that SHA's CI is green (the push fires too early; the backstop catches it after CI settles). - Codemagic (
codemagic.yaml, workflowomi-desktop-swift-release) — triggered by the tag, runs on Mac mini M4:- Builds the universal app and dSYM, UUID-checks and uploads symbols to Sentry, and publishes both
- Signs with Developer ID, notarizes with Apple
- Creates DMG + Sparkle ZIP
- Runs
scripts/smoke-signed-desktop-artifact.sh(signed app, Sparkle ZIP, DMG) before publishing, with a mandatory in-app Keychain write/read/delete canary - Publishes an immutable non-live GitHub candidate with smoke evidence
- Qualification (
desktop_qualify_beta.yml) — dispatched by Codemagic after candidate publication. A Codemagic primary lane plus independent per-machine self-hosted fallback lanes, fail-closed with an at-least-one-passes verdict (a single self-hosted machine proved to be a release-blocking single point of failure in the Jul 22 2026 outage — failure classFC-single-machine-release-gate):- Codemagic lane (primary) — the orchestrating job validates the candidate on GitHub-hosted ubuntu, starts the
omi-desktop-qualificationCodemagic workflow (config trusted frommain; candidate source materialized from the immutable tag; hermetic stack runs Typesense natively viaOMI_TYPESENSE_RUNTIME=nativebecause ephemeral Codemagic Macs lack nested virtualization), passes only a short-lived Omi Bot token for read-only gh calls, and publishes the immutable qualification evidence itself only after the build, its tag binding, and itsqualification-result.jsonartifact all verify. Codemagic already builds and signs the candidate, so executing qualification there extends it no new trust. - Per-machine self-hosted fallback lanes —
qualify-m1-studio(labelomi-qual-m1-studio) thenqualify-m4-mini(labelomi-qual-m4-mini), each pinned to its own physical machine so a sick-but-online runner cannot absorb the only fallback attempt. Aplan-fallbacksjob reads runner online-status and skips an offline lane (a job targeting an offline self-hosted runner queues up to 24h); the mini lane runs only when the M1 lane did not publish evidence. Each lane verifies published asset digests against signed-smoke evidence, runs the static release checks, rebuilds the exact tag, runs hermetic T2 plus the fault-injection suite, and writes canonicalqualifiedBeta*evidence metadata. The exact-SHA source clone and its direct SwiftPM.buildpath live together outside the Actions checkout, so checkout cleanup cannot delete them and retries/prewarms use identical absolute source and output paths. Reuse is fail-closed on SHA,Package.swift/Package.resolved, Xcode, Swift, macOS, architecture, symlinks, collisions, incomplete entries, or tracked source changes; only untrusted untracked residue outside.buildis cleaned. SwiftPM still validates/rebuilds every requested product, and this cache is never release evidence. A qualification runner must be an administrator-managed Mac with a healthy Docker daemon (Docker Desktop or Colima) or a nativetypesense-server27.1 on PATH, plus full Xcode, node, a Java runtime,redis-server, rustup, anduv(the qualify script provisions the backend venv, falling back to the pinned minor Python when uv lacks the exact patch); it must never execute pull-request or arbitrary-ref workflows. Registered runners: David's M1 Mac Studio and Nik's M4 Mac mini. Theverdictjob is the only job that can fail the run, so promotion sees success iff at least one lane published evidence.
- Codemagic lane (primary) — the orchestrating job validates the candidate on GitHub-hosted ubuntu, starts the
- Automatic beta promotion (
desktop_promote_beta.yml) — a separateworkflow_runstarts only after successful qualification, derives and validates the immutablev*-macostag against the qualification SHA, then invokes the same internal-only backend admission authority. The backend captures the server-owned Beta admission generation before validating digest-matched evidence, then atomically verifies that the reservation and pause state are unchanged while registering the immutable manifest and advancing the explicit beta pointer. If that handoff fails after qualification, use onlydesktop_recover_beta.ymlwith the exact tag,confirm=recover-beta, and a reason.
The shared Python backend must contain the manifest/pointer endpoints before the first beta promotion. Deploy it separately with gcp_backend.yml; merging desktop code does not deploy the prod backend. Static GCS/CDN feed ownership remains follow-up work and is not the channel source of truth.
Signed artifact smoke scope:
- Always-on release audit covers bundle identity, version/tag alignment, signing/Keychain entitlements, Sparkle metadata, backend URL leakage, helper/runtime packaging, artifact readability, and local storage package surface.
- Codemagic uploads
build/desktop-smoke-result.jsonwith artifact digests and completed checks; promotion tooling should compare this result to the exact release asset before changing channels. - The synthetic
--auth-storage-canaryis mandatory before beta publication and runs inside the exact signed app without real credentials. Optional broader live probes (--launch --network --auth --chat --permissions --storage) require an isolated release runner and explicit canary env vars; production-bundle launch is fail-closed unlessOMI_SIGNED_ARTIFACT_SMOKE_ALLOW_PRODUCTION_LAUNCH=1, and--authrequiresOMI_SIGNED_ARTIFACT_SMOKE_AUTH_PROOF_COMMANDto prove app-level persistence rather than a raw bearer-token curl. - Artifact creation and user visibility are split: create/upload the immutable candidate first, then advance beta/stable visibility only after digest-matched qualification passes.
- Automatic beta is fail-closed: any signed-smoke, reservation, digest, static, T2, fault-suite, manifest, admission-generation, or pointer failure leaves the candidate non-live. Operators pause/resume only through the ADMIN_KEY-protected backend admission control; workflow variables are not pause authority. When a served Beta is concretely broken,
desktop_rollback_beta.ymlcan repoint only macOS Beta to a retained T2-qualified manifest, anddesktop_breakglass_rollout_beta.ymlcan advance only to a higher evidence-bound emergency candidate. Both reuse the existing protectedprodenvironment and Google identity, atomically audit + pause admission, and cannot reach Stable. Rollback does not downgrade already-updated clients; follow it with a higher-build repair.
Stable is manual:
- Automatic qualification never promotes Stable.
desktop_promote_prod.ymlremainsworkflow_dispatchonly and protected by theprodenvironment. - Run it with the current qualified Beta
release_tagandconfirm=promote-stable. It reads and compare-and-swaps the current Stable pointer itself, advances only that pointer, updates the existing legacy/static bridges, and verifies hashes and feed output. - Backend deployments remain independent in their established workflows. Do not manually edit release visibility or pointers outside the promotion workflow.
Codemagic CLI & API:
- Token:
$CODEMAGIC_API_TOKEN(set in~/.zshrc) - App ID:
66c95e6ec76853c447b8bcbb - List builds:
curl -s -H "x-auth-token: $CODEMAGIC_API_TOKEN" "https://api.codemagic.io/builds?appId=66c95e6ec76853c447b8bcbb" | python3 -c "import json,sys; [print(f\"{b.get('status','?'):12} tag={b.get('tag','-'):30} start={(b.get('startedAt') or '-')[:19]}\") for b in json.load(sys.stdin).get('builds',[])[:5]]"
Promotion from beta to stable is handled by desktop_promote_prod.yml, not Codemagic.
Use the /firebase command if your agent provides it.
Quick connect:
cd ../backend && source venv/bin/activate && python3 -c "
import firebase_admin
from firebase_admin import credentials, firestore, auth
cred = credentials.Certificate('google-credentials.json')
try: firebase_admin.initialize_app(cred)
except ValueError: pass
db = firestore.client()
print('Connected to Firebase: based-hardware')
"Desktop/Package.swift is incrementally splitting the monolithic executable into
library targets with enforced dependency edges:
OmiTheme— shared colors, typography, chrome (Sources/Theme/)OmiWAL— write-ahead log model + coordinator (Sources/OmiWAL/)OmiSupport— shared desktop runtime helpers (Sources/OmiSupport/, e.g.DesktopLocalProfileandDictionary(lastWriteWins:))
Rewind/Core/ remains in the executable target for now — it still references main-app
types (TaskActionItem, PowerMonitor, etc.) and needs a shared-models carve-out first.
Do not add new .swift files directly under Desktop/Sources/. Place new
code in a feature directory (Onboarding/, MainWindow/, Chat/, etc.). CI
enforces this via scripts/check-sources-root-layout.py.
When carving out additional leaf modules, prefer bottom-up order (models and
storage before UI) and wire import + public on the extracted target's API.
Swift formatting uses a pinned swift-format binary (release 602.0.0 at commit
62eaad2), bootstrapped from source via scripts/swift-format-wrapper.sh. The
config lives at Desktop/.swift-format (2-space indent, 120-column limit).
Generated sources under Desktop/Sources/Generated/ are excluded from the
formatter scope. Bootstrap once: ./scripts/swift-format-wrapper.sh bootstrap.
Lint the full scope: ./scripts/swift-format-wrapper.sh lint -r $(./scripts/swift-format-wrapper.sh scope).
SwiftLint safety rules run as an explicit macOS manifest check (not a SwiftPM
build-tool plugin) through scripts/swiftlint-wrapper.sh. The wrapper pins the
upstream 0.65.0 universal macOS release artifact by SHA-256 and caches the
verified binary under ~/.cache/omi-swiftlint; use
./scripts/swiftlint-wrapper.sh lint to run the full configured scope.
Generated sources and test fixtures remain excluded and the committed baseline
is down-only. SwiftLint baseline locations are absolute, so the wrapper
materializes a temporary baseline rooted at the current checkout before linting;
do not hand-edit those paths to match a specific machine.
- A reducer transition is atomic through model assignment, effect delivery, UI projection, and snapshot publication. A callback may request another event, but it must not recursively reduce against a half-published transition.
- Coordinators with synchronous effect/snapshot callbacks drain nested events through a FIFO, non-reentrant queue. Do not fix recursion with one-off boolean suppression or by dispatching after an arbitrary delay.
- Tests for callback-driven machines must synchronously enqueue from both an effect callback and an observer/snapshot callback, assert callback depth stays one, and assert the resulting event order.
- Never use
Dictionary(uniqueKeysWithValues:)for API responses, decoded persistence, runtime projections, or any other data whose key uniqueness is not enforced by the Swift type system. A duplicate key traps and terminates the process. - Use
Dictionary(lastWriteWins:)fromOmiSupportwhen the newest record in input order is authoritative. Use another explicit non-trapping merge policy when the domain requires different semantics. - A raw trapping initializer is allowed only for a statically proven uniqueness
contract, with a local reason:
// omi-collection-safety: static-unique-keys -- <why the type guarantees uniqueness>. Runtime validation, backend expectations, and “should be unique” are not static contracts. - Run
python3 scripts/check_desktop_test_quality.pyafter changing Swift collection construction.
- Behavior fixes require tests that call the production API and assert outcomes.
Reading a production
.swiftfile and asserting that it contains a function name or implementation string is not behavioral coverage. - Source inspection is reserved for narrow forbidden-pattern or static wiring
tripwires. New tripwires must carry a local reason:
// omi-test-quality: source-inspection -- static contract: <what cannot be expressed behaviorally>. The tripwire supplements rather than replaces behavioral coverage. - Do not add wall-clock sleeps to unit tests. Inject a
Clock/sleeper, drive a callback/continuation, or await a deterministic state signal. An unavoidable real-scheduler integration wait needs// omi-test-quality: wall-clock-wait -- <why injection cannot test this boundary>. python3 scripts/check_desktop_test_quality.pyratchets both legacy source-inspection sites and wall-clock waits; its baselines may only decrease.
- Firebase Auth with Apple/Google Sign-In
- Desktop apps should use backend OAuth flow:
/v1/auth/authorize - Apple Services ID:
me.omi.web(shared across all apps) - iOS apps use native Sign-In, Desktop uses backend OAuth + custom token
- Session death is owned by
AuthSessionCoordinator(INV-AUTH-1); useinvalidateSessionfor expired/revoked Firebase creds, not nuclearsignOut().
| Failure class | Owner | Action on 401 after forced refresh |
|---|---|---|
Firebase session token (default API Authorization) |
AuthSessionCoordinator |
invalidateSession → Sign-in CTA |
| BYOK provider key on request | CredentialHealthManager |
Suppress/mark provider unhealthy; do not invalidate Firebase session |
| Realtime/voice managed lane | CredentialHealthManager + hub UX |
requiresLogin only when session mint fails after refresh |
Background poll with RequestAuthPolicy.sessionPreserving |
Caller | Throw .unauthorized; no session invalidation |
DesktopLocalProfile harness |
Auth emulator bootstrap | Re-bootstrap emulator session; no prod invalidation side effects |
- Firestore (
based-hardware): User data, conversations, action items - Redis: Caching
- Typesense: Search
users/{uid}/conversations- Hassourcefield (omi, desktop, phone, etc.)users/{uid}/action_items- Tasks (no platform tracking)users/{uid}/fcm_tokens- Token ID prefix = platform (ios_, android_, macos_)users/{uid}/memories- Extracted memories
- FCM tokens: Document ID prefix (e.g.,
macos_abc123) - Conversations:
sourcefield - Action items: No platform tracking
- Firestore has no collection group indexes for
sourcefield - Counting users by platform requires iterating all users (slow)
- Apple Sign-In: Only one Services ID per Firebase project
- Production:
https://api.omi.me - Local:
http://localhost:8080
Connection details come from your local agent configuration; they are deliberately not checked in. Ask the user for anything you are missing rather than guessing an endpoint.
-
No Xcode project — this is a Swift Package Manager project
-
Build command:
xcrun swift build -c debug --package-path Desktop(thexcrunprefix is required to match the SDK version) -
Full dev run:
./run.sh— builds Swift app, starts Rust backend, starts Cloudflare tunnel, launches app -
Fast default dev run: after one successful full named-bundle launch, ordinary Swift-only
./run.shcalls reuse the installed bundle. The fast lane runs incremental SwiftPM, atomically replaces the executable and current desktop API URL, re-signs the app, and relaunches without copying/re-signing static agent/framework assets or resetting LaunchServices/auth. Named local profiles are eligible: their current disposable.envis refreshed on each patch and is never cached in the bundle fingerprint. Package metadata, resources, agent/runtime inputs, entitlements, and persistent launch configuration automatically take the full path. Force that path with./run.sh --fullorOMI_FORCE_FULL_BUNDLE=1.OMI_SCAN_STALE_BUNDLES=1is an explicit stale-LaunchServices recovery scan; do not enable it in the normal loop. -
Focused feedback loop:
./scripts/dev-feedback.py --once|--watch swift '<XCTest filter>'or... rust '<cargo filter>'runs exactly the regression you selected and reports each iteration time. It watches only the matching component inputs, keeps watching after a failure, and never replaces the full component suite. Pre-push deliberately adds onlyxcrun swift build -c debug; never promote it to the full pinned-Xcode suite or release compile, because that push-time budget belongs to CI. -
Swift suite throughput:
scripts/swift-test-suites.shisolates suite processes but now defaults to four workers (matching CI). SetOMI_SWIFT_TEST_SUITE_WORKERS=1only when diagnosing an order/concurrency-sensitive failure. -
Local Rust backend: direct
./run.shdevelopment uses Cargo debug output (target/debug) by default and reuses a healthy backend that this worktree owns when Rust source/config/profile have not changed. A compile failure leaves that healthy process alive. UseOMI_DESKTOP_BACKEND_RELEASE=1only for an explicit optimized local check; release/CI builds remain unchanged. -
Agent runtime preparation cache: local
./run.shcalls reuse validated agent packaging from the worktree-local.harness/agent-runtimecache when source, locks, preparation logic, pinned runtime, mode, OS/architecture, Node/npm versions, and every file copied from the prepared runtime are unchanged. Hits verify the complete agentdist, both packaged dependency trees, their symlinks, and staged Node; workingagent/node_modulesis not hashed. The script logsCache HIT,MISS, orBYPASS; hits preserve output mtimes but spend roughly a second on a warm local filesystem hashing the packaged outputs for integrity (hardware/filesystem dependent). CI and--skip-npmalways bypass the stamp. SetOMI_AGENT_RUNTIME_FORCE_REBUILD=1for an explicit local rebuild. Do not copy this cache between worktrees or treat it as a release artifact. The checksum-verified universal Node archives are separately shared at~/Library/Caches/OmiDesktop/node-archives(override withOMI_AGENT_RUNTIME_ARCHIVE_CACHE_DIR), so fresh linked worktrees reuse the download but still validate it before staging. -
Release builds: Handled entirely by Codemagic CI (no local release script needed)
-
DO NOT use bare
swift build— it will fail with SDK version mismatch -
DO NOT use
xcodebuild— there is no.xcodeproj -
DO NOT launch the app directly from
build/— always use./run.sh. These scripts install to/Applications/Omi Dev.appand launch from there, which is required for macOS "Quit & Reopen" (after granting permissions) to find the correct binary. Launching frombuild/causes stale binaries to run after permission restarts. -
DO NOT manually copy binaries into app bundles and launch them — this bypasses signing,
/Applications/installation, and LaunchServices registration -
DO NOT kill, delete, or interfere with running "Omi", "omi", or "Omi Beta" app bundles — these are production/release installs the user relies on
./run.shbuilds "Omi Dev" → installs to/Applications/Omi Dev.app(bundle ID:com.omi.desktop-dev)- "Omi" stable (bundle ID:
com.omi.computer-macos) and "Omi Beta" (bundle ID:com.omi.computer-macos.beta, isolated "Omi Beta" storage root, runs side-by-side with stable) are built by Codemagic CI only - To check which app is currently running:
ps aux | grep "Omi"
When the user asks to test a feature or bug fix, always create a separate named bundle so it can run side-by-side with the existing dev/prod apps:
OMI_APP_NAME="omi-fix-rewind" ./run.shThis creates /Applications/omi-fix-rewind.app with bundle ID com.omi.omi-fix-rewind, completely independent of "Omi Dev" and "Omi Beta". Name it after the feature/bug being tested. The user can then run multiple test builds simultaneously without interfering with each other or the production app.
Build-lock invariant: ./run.sh locks per worktree (repo-root .dev/run-sh-build.lock.d), through build→install→seed→open, then releases before the long-running wait. Parallel worktrees must not block each other. Two named-bundle builds in the same worktree still serialize (shared Desktop/.build/). Do not reuse the same explicit OMI_APP_NAME across worktrees — /Applications/$APP_NAME.app is machine-global and not cross-locked.
Rules:
- NEVER use the default
./run.sh(which overwrites "Omi Dev") when testing a specific feature — always setOMI_APP_NAME - ALWAYS prefix the name with
omi-(e.g.,omi-fix-rewind,omi-6512-polling,omi-vision-test) so named bundles are visually grouped in/Applications/alongside "Omi Dev" and "Omi Beta" - Keep the name short and descriptive (it becomes both the app name and bundle ID suffix)
- The named bundle gets its own permissions and writable database.
./run.shauto-seeds auth/onboarding, curated settings, and a one-time consistent Rewind snapshot from "Omi Dev"; setOMI_SKIP_REWIND_SEED=1to start with an empty Rewind profile. - To connect agent-swift:
agent-swift connect --bundle-id com.omi.omi-fix-rewind - Skip the web login: sign into "Omi Dev" once; named bundles launched by
./run.shclone that session before launch. - Jump to a screen without clicking: the automation bridge auto-enables on non-prod bundles —
./scripts/omi-ctl navigate <screen>(e.g.rewind,memories,settings rewind). See "Fast-Path for Local Iteration" ine2e/SKILL.md. - Named/dev bundles default to the development Python and Rust backends unless
an explicit launch URL overrides them. Before QA, run
./scripts/omi-ctl health; its unauthenticated identity payload reports the resolved backend environment/URLs plus the agent-runtime handshake state, negotiated protocol version, packaged runtime version, and expected protocol. A protocol-compatible runtime that omits a required capability is rejected at startup; health never reports the expected protocol as if it were negotiated. - Run
./scripts/agent-logic-harness.sh --cross-surface-smokebefore building a QA bundle. This is the compact Swift/Node/Rust contract gate; reserve full component suites and the live continuity gauntlet for PR readiness.
./run.sh --yolo— quick start against the dev backend, no local services.OMI_SKIP_BACKEND=1— app only, remote backend viaOMI_DESKTOP_API_URL.OMI_SKIP_TUNNEL=1— no Cloudflare tunnel.- Parallel worktrees auto-isolate.
scripts/dev-instance.shderives a unique instance from each linked git worktree, sorun.sh(andbackend/scripts/dev-serve.sh) pick per-worktree ports (Rust 10201+, Python 8080+, automation 47777+) and bundle name (omi-<worktree>). Kills are pidfile-scoped (never the globalomi-desktop-backendname), and a taken port fails loud instead of clobbering. The primary checkout is unchanged (Omi Dev, 10201/8080/47777). Override any ofOMI_INSTANCE/PORT/PYTHON_PORT/OMI_AUTOMATION_PORT/OMI_APP_NAMEto opt out. Omi Devis the canonical shared development profile (reusable permissions, auth seed source). Do not passOMI_APP_NAME="Omi Dev"from a linked worktree; that creates a named bundle displayed as Omi Dev with a different bundle id and breaks permission reuse.- Local Python backend (per-worktree port):
cd backend && ./scripts/dev-serve.sh.
Hard rule: you may not ask the user to verify a feature you have not actually exercised yourself. Compiling, "looks correct from the code", or "scroll down to see it" are not verification. If the obvious path is blocked (permission, focus, missing tool), try a long sequence of alternatives before involving the user — extend the bridge with a new action, add a temporary in-process hook, search the web for a workaround, grant the missing permission yourself if you can, write a tiny standalone harness. Roughly: spend ten serious attempts across different approaches before you escalate. Asking the user is the last move, not the first.
Fast path (skips web login and sidebar click-through):
- Build + launch a named bundle (see Testing with Named Bundles above).
./run.shauto-clones Omi Dev auth/onboarding plus common shortcuts/settings before launch. Manual seeding:./scripts/omi-auth-dump.sh # capture the Omi Dev session ./scripts/omi-auth-seed.sh com.omi.omi-<feature> \ tmp/desktop-auth.json "/Applications/omi-<feature>.app" # clears stale Keychain; UD→KC migrate ./scripts/omi-settings-seed.sh com.omi.omi-<feature> # replay shortcuts/settings
- Prefer the local bridge — it never touches the cursor. It calls the app's real code in-process (no synthetic mouse events). Use it before reaching for
agent-swift click/cliclick/computer-use. Auto-enables on non-prod bundles; run several at once via distinctOMI_AUTOMATION_PORT(default 47777)../scripts/omi-ctl state— app-state snapshot (selected tab, auth, onboarding)../scripts/omi-ctl navigate <screen> [settings-section]— jump straight to a screen in ~150ms (omi-ctl screenslists targets)../scripts/omi-ctl actionsthen./scripts/omi-ctl action <name> [k=v …]— semantic actions (e.g.refresh_all_data). Add new ones inDesktopAutomationActionRegistry. Seee2e/SKILL.md§2b.agent-swiftonly for UI the bridge can't reach yet (clickmoves the cursor).
- Read logs to confirm behavior: app + chat bridge in the exact path from
./scripts/omi-ctl log-path(named dev bundles) or/private/tmp/omi.log(production);./run.shprints the isolated local Rust backend log path at launch; per-user issues in Sentry/PostHog. - Verify the actual behavior, not just that the app launched — exercise the feature and check the logs/UI reflect the change.
- Edit or diagnose: run the smallest relevant unit/static harness. For repeated saves, start
./scripts/dev-feedback.py --watch swift '<filter>'or... rust '<filter>'; do not launch the app only to obtain compile evidence. - Swift/UI behavior: reuse the existing named bundle with
OMI_APP_NAME=omi-<feature> ./run.sh --yolo --fast-only; add--no-waitonly with a harness/external backend, then use the local bridge (omi-ctl action,state, or a semantic snapshot) to assert the changed behavior. - Package boundary: use
./run.sh --fullonly for the first named launch, resource/entitlement/package/runtime input changes, or when--fast-onlyreports an expected fingerprint mismatch. - QA, commit, and PR readiness: run
./scripts/omi-macos-dev doctor, exercise the real user-facing path, then run the appropriate full component/PR contract.
omi-macos-dev defaults to bounded JSON summaries so an agent can safely inspect a busy machine. Pass --verbose to the specific command for path-level records (for example, clean plan --verbose); cleanup always requires the exact current plan hash. The normal 14-day retention window can be deliberately bypassed with --older-than 0 only when the operator has explicitly approved immediate cleanup.
Never ask a user to test an unexercised path. A fast named-bundle launch plus a semantic bridge assertion is valid inner-loop evidence; a clean full bundle is release/QA evidence.
xcrun swift buildis for compile checks only — it does NOT start the backend- Voice-path verification means a natural authenticated PTT turn on a named bundle — signed-out, forced-transcript, or reducer-only runs do not count; provider mint or payload changes must also show the deploy-inline provider probe.
- When the user says "test it", use the
test-localskill to build, run, and verify via macOS automation
- The deployment floor is
.macOS("14.0")inDesktop/Package.swift. Every change must work on every supported macOS version from that floor up. - Never call an API newer than the floor unguarded: wrap it in
if #available(macOS XX, *)and give theelsebranch a working fallback (degrade the feature, don't blank it). Example: System Audio capture gates on#available(macOS 14.4, *)and hides cleanly below it. - Version-dependent system facts (renamed apps, moved paths, changed defaults) get an explicit mapping with the old value still handled — stored user data may predate the change (example:
AppIconCache.renamedAppsmaps "System Preferences" → "System Settings"). - Raising the deployment floor or dropping a fallback is a product decision — never do it as a side effect of another change.
- Before starting and before committing,
git fetch origin && git rebase origin/main(or merge) — other contributors land changes continuously; never review your diff against a stale base. - Keep diffs surgical: touch only lines your change needs. No drive-by reformatting, renames, or import reshuffles in files others may have in-flight PRs against.
- After rebasing onto new upstream work, re-run the test suites for every file you touched and every file the rebase brought in that overlaps your change; a clean build alone is not revision.
- If your change modifies shared surfaces (Theme tokens,
SettingsSection, bridge actions, INV-* contract files), grep for all usages — including tests and e2e flows — and update them in the same commit so concurrent contributors inherit a consistent tree.
When touching desktop agent runtime, floating agent pills, realtime hub, PTT, or pi-mono-extension, run the focused harness before broader checks:
cd desktop/macos && ./scripts/agent-logic-harness.shIt is self-driving for agents: it runs the risky Swift lifecycle/state tests, focused agent runtime tests, exact pi-mono-extension package tests, and prints per-step runtime. Use --swift-only, --node-only, or --skip-install only when narrowing a failure.
Invariant: Main Chat, Home chat, and floating/notch chat are one timeline over one
ChatProvider (historyChatProvider). Kernel main_chat turns are the durable
source of truth; journal acceptance publishes the immediate pending projection,
and UI must never append a pre-journal turn.
Rules (fail the PR if any break):
- Single provider + floating viewport — floating presentation is chrome + a
viewport cursor (
FloatingChatViewportmessage ids /clientTurnId) overChatProvider.messages. It must not own a second durable transcript array (chatHistoryofChatMessagecopies is forbidden). - Single
turn_recordedUI apply gate — onlyKernelTurnProjectiononChatProvider.mainInstance(historyChatProvider) may attach the runtime turn handler (one replaceable slot). Speculative warm and other surfaces must reusemainInstance; never construct a secondChatProvider()that callsattachClient/setTurnRecordedHandleron the shared runtime. - One idempotency key per logical turn — call
recordJournalExchange(or the corresponding kernel control RPC) with one opaque continuity key and await acceptance before binding a visible row. Direct-control spawn receipts already materialize their exchange; refresh that journal instead of issuing a second write. Never dedupe by assistant/user text. - Kernel apply is idempotent —
KernelTurnProjectionupserts only by the canonical turn ID published by ordered journal replay. Rejection must leave no visible row, and replay/acknowledgement must replace rather than append. - Cross-surface agent identity is structured —
agentSpawn/agentCompletioncontent blocks (plus tool-blockspawnedAgentID/ sessionId / runId lines) are authoritative. Persist structured blocks through the kernel journal/outbox so they survive reload; kernel apply still materializesagentCompletionfrom bracket text for legacy rows. Legacy[Background agent id=…]bracket text remains dual-read only. Do not invent new free-text formats; extend the schema + tests together. Proactive notifications use continuity keynotification:<uuid>(originproactive_notification) and enter the notification-to-chat cache only after journal acceptance; do not reintroduce local timeline append paths. - Pill cache is derived — open-by-id hydrates from kernel (
listFloatingAgentPills/listAgentSessions/inspectAgentRun) when the in-memory pill is missing; refresh-on-miss is a fast path only. Success = resolvable agent after hydrate. Do not keep a second durable pill store. - Snapshots are aliases —
automationFloatingChatSnapshot==automationChatSnapshot/automationMainChatSnapshotover the same messages; no surface-specific transcript filter. - Resources live on the producing message — artifacts attach to the
ChatMessagethat produced them (stage/promote keepsresourceson that id). UI must not invent a standalone artifact-only turn. Floating/notch resource strips bindmessage.displayResourceson viewport-derived messages only (never flatMap the whole provider timeline). Aggregate strips must filter withChatContinuityInvariants.resourcesBelongingToMessages/FloatingControlBarState.viewportDisplayResources. - Agent card/list preview = prompt/objective — collapsed header / list
subtitle uses
ChatContinuityInvariants.agentPreviewText(prompt:output:)(prompt wins; output is expanded-body only). Do not put raw completion output in the one-line preview. - Forbidden dual-write patterns — never: construct
ChatProvider()for speculative warm (useChatProvider.mainInstance); addaddTurnRecordedHandler/ multi-handler append APIs; introducesuppressNextRecordedTurn; store@Published var chatHistoryofChatMessagecopies onFloatingControlBarState. - Tests — continuity behavior changes require a hermetic behavioral test (call projection/provider APIs, assert message counts/IDs). Source-string greps for function names are not continuity coverage (forbidden-pattern tripwires are the exception). Live gauntlet/stress are gates, not substitutes for hermetic tests.
A PR that touches chat write-path, kernel projection, floating viewport, agent timeline identity/open, or pill projection is incomplete until:
- Contract still true — INV-6 rules above hold after the change (or are updated in the same PR with a matching behavioral test).
- Hermetic behavioral test for the invariant touched (stage/promote, snapshot alias, structured identity, open-by-id hydrate, viewport derive / restore, resources-on-message, agent preview text). Not a source grep (except forbidden-pattern tripwires).
./scripts/agent-logic-harness.shgreen (includesKernelTurnRecordedProjectionTests,ChatTimelineContinuityTests,FloatingControlBarStateTests,RuntimeOwnerIdentityTestsin the Swift focus filter).- Write-path / cross-surface changes: run a named-bundle continuity
gauntlet and note evidence in the PR:
CI only runs gauntlet
cd desktop/macos && OMI_APP_NAME=omi-gauntlet OMI_SKIP_TUNNEL=1 ./run.sh # run.sh seeds auth after install (UD tokens → app Keychain migrate). Manual reseed: # ./scripts/omi-auth-seed.sh com.omi.omi-gauntlet tmp/desktop-auth.json "/Applications/omi-gauntlet.app" ./scripts/agent-continuity-gauntlet.sh --suite continuity --bundle-id com.omi.omi-gauntlet ./scripts/check-gauntlet-evidence-at-head.sh
--self-check(wiring). Live suite is a PR/RC gate, not PR CI. Do not assert exact assistant wording. - Hermetic e2e only if a bridge action/surface contract changed. Do not
expand flow
covers:lists as fake continuity coverage. - No second message store / no new free-text identity format / no
suppressNextRecordedTurn-style dual-write bandage. - Changelog fragment only if user-visible.
- CI:
agent-continuity-gauntlet.sh --self-checkonly (via desktop-core / agent-logic harness). Never require live LLM in PR CI. - Prompt / gateway changes:
--suite promptson a namedomi-*bundle; P4 requires a completed public-web lookup with a source URL and fails on provider tool-choice incompatibilities. Continuity PRs / RC:--suite continuity(typed + PTT + blind recall) after auth seed;--suite allfor RC. Evidence under.harness/agent-continuity-gauntlet/*/manifest.jsonwith matching git SHA. - Anti-flake: clear owner/kernel surface before probes; per-run nonces; hard-fail on blind-recall / structural snapshot only; zero automatic retries on model wrongness.
- Stress: offline JSONL + forbidden terminal reasons remain the default
gate; live bridge probes stay optional until continuity
terminal_reasons exist in the taxonomy.
Do not confuse these gates — a green live suite does not prove write-path contract rules, and hermetic unit tests do not prove bridge/LLM continuity.
| Gate | What it covers | What it does not cover |
|---|---|---|
Hermetic (agent-logic-harness.sh Swift filter: KernelTurnRecordedProjectionTests, ChatTimelineContinuityTests, FloatingControlBarStateTests, RuntimeOwnerIdentityTests) |
stage/promote same key → one message pair; floating snapshot aliases main; structured agent identity; open-by-id hydrate preference; floating viewport derive / SoT; resources on producing message; agent preview = prompt; owner-swap preserves Firebase tokens; forbidden dual-write tripwires | Live bridge auth, LLM tool use, PTT hub, race/busy policy under a real runtime |
Gauntlet --self-check |
Bridge action registration (incl. R3 ask_main_chat_no_wait / main_chat_busy_state), resilience suite wiring, hermetic contract test presence in harness filter |
Any live turn |
Live --suite continuity / agents / owner / prompts |
Typed + PTT + blind recall, spawn/status, owner swap probe, prompt regressions on a named bundle | stage/promote single-writer, snapshot alias, hydrate preference, viewport SoT (those stay hermetic) |
Live --suite resilience (R1–R4) |
Cold bridge launch, warm reuse, bridge busy/race rejection (R3; requires real is_sending/is_streaming once, latch only extends the race window), subagent launch+status (R4) |
INV-6 write-path unit invariants above |
--self-check fails if R3 race actions or the hermetic INV-6 test methods /
harness filter classes drift away.
After editing Swift UI code, verify the change programmatically using agent-swift — a CLI that controls any macOS app via the Accessibility API.
One-time setup: brew install beastoin/tap/agent-swift + grant Accessibility permission to Terminal.app.
# After ./run.sh launches the app:
agent-swift doctor # verify Accessibility permission
agent-swift connect --bundle-id com.omi.desktop-dev # connect to running app
agent-swift snapshot -i # see interactive elements
agent-swift click @e3 # CGEvent click (SwiftUI)
agent-swift press @e3 # AXPress (AppKit buttons)
agent-swift fill @e5 "search text" # type into a text field
agent-swift find role button click # find + chained action
agent-swift is exists @e3 # assert element exists (exit 0/1)
agent-swift wait text "Settings" # wait for text to appear
agent-swift screenshot /tmp/evidence.png # capture app windowKey rules:
- Always use
snapshot -i(interactive only) — full snapshot of a complex SwiftUI app is extremely verbose. - Prefer
clickoverpressfor SwiftUI —clicksends CGEvent clicks (triggers NavigationLink),presssends AXPress (AppKit only). - Refs go stale after
click/press/fill/scroll— re-snapshot before the next interaction. - Argument order:
get <property> <ref>,is <condition> <ref>,wait <condition> [<target>],find <locator> <value>. - 15 commands:
doctor,connect,disconnect,status,snapshot,press,click,fill,get,find,screenshot,is,wait,scroll,schema. - No app-side instrumentation needed — works via macOS Accessibility API on any Cocoa/SwiftUI app.
- Dev bundle ID:
com.omi.desktop-dev. Prod:com.omi.computer-macos(stable) andcom.omi.computer-macos.beta(Omi Beta) — never automate prod.
After completing a desktop task with user-visible impact, add one fragment file under desktop/macos/changelog/unreleased/:
Example desktop/macos/changelog/unreleased/20260628-short-description.json:
{
"change": "Your user-facing change description"
}Guidelines:
- Write from the user's perspective: "Fixed X", "Added Y", "Improved Z"
- One sentence, no period at the end
- Use a unique kebab-case filename so parallel PRs do not conflict
- Skip internal-only changes (refactors, CI config, code cleanup)
- HTML is allowed for links:
<a href='...'>text</a> - Do not edit
CHANGELOG.jsonby hand; release automation regenerates it - Commit the fragment with your other changes (same commit is fine)
When completing a task that was triggered by an app user request (bug report, feature request, support inquiry, etc.) and you have the user's email address, send them an email about the results using the omi-email skill:
node ../omi-analytics/scripts/send-email.js \
--to "<user-email>" \
--subject "<brief result summary>" \
--body "<what was done, what they should expect, any next steps>"- Write as Matt (first person "I", not "we") — the user already has an ongoing email thread with us, so treat this as a casual continuation of that conversation, not a fresh introduction
- Be concise and direct — they know the context, just share what was done and any next steps (e.g. "update the app")
- Only send when there are meaningful results to share (don't email for internal-only changes)