Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T00:58:31.429Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git

Revision diff

a96f6c05 68027e10
diff --git a/Cargo.lock b/Cargo.lock
index 78c5d7bbed..50691fddbc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12165,6 +12165,7 @@ name = "open_router"
1216512165 version = "0.1.0"
1216612166 dependencies = [
1216712167  "anyhow",
12168+ "app_identity",
1216812169  "futures 0.3.32",
1216912170  "http_client",
1217012171  "language_model_core",
diff --git a/OMEGA_DELTAS.md b/OMEGA_DELTAS.md
index 1384e8a7a8..f43bb9d85f 100644
--- a/OMEGA_DELTAS.md
+++ b/OMEGA_DELTAS.md
@@ -1726,3 +1726,203 @@ than it sounds, because the harness omega#81's acceptance sentence names —
17261726   router decides per session, not per turn. And the pin menu is rendered on the
17271727   thread's disclosure line, so a surface with no thread on it has no pin
17281728   control.
1729+### OMEGA-DELTA-0036 — `--uninstall` removes Omega, and no part of anybody else
1730+
1731+- **Upstream Zed:** ships `script/uninstall.sh` to remove a Zed installed by
1732+  `install.sh`. Correct for Zed, and inherited wholesale by a fork.
1733+- **Omega, before this:** `crates/cli/src/main.rs` embedded that file with
1734+  `include_bytes!` and ran it, advertised in `--help` as **"Uninstall Omega from
1735+  user system"**. It contained zero occurrences of `Omega` or `omega`. With
1736+  `ZED_CHANNEL` unset it removed `/Applications/Zed.app`, the whole
1737+  `~/Library/Application Support/Zed` tree, `~/Library/Logs/Zed`, the caches,
1738+  HTTP storage, preferences plist and saved application state under
1739+  `dev.zed.Zed`, and `~/.zed_server`; it asked whether to keep *"your Zed
1740+  preferences"*; and it printed **`Zed has been uninstalled`**. It removed no
1741+  Omega path at all — not the bundle, not `~/Library/Application Support/Omega
1742+  RC`, not `~/Library/Logs/omega-rc`. A user who ran the advertised "Uninstall
1743+  Omega" kept Omega and lost their other editor. This shipped in the signed,
1744+  notarized `0.2.0-rc13` and `0.2.0-rc14` (omega#88).
1745+- **Why nothing caught it.** The paths lived in a hand-written table in a shell
1746+  script, disconnected from the code that creates those directories, so nothing
1747+  in the tree could observe that the two had never agreed. And
1748+  `script/verify-omega-brand` opened `Contents/MacOS/omega` at three places and
1749+  `Contents/MacOS/cli` at none, although `script/bundle-omega-rc` copies and
1750+  signs `cli` into the bundle — the file the defect was in had never been opened
1751+  by any check. That half is `OMEGA-DELTA-0038`.
1752+- **Omega now:** there is no path table. `crates/cli/src/uninstall.rs` builds an
1753+  `UninstallRoots` — one field per place Omega writes — where every field is
1754+  read from the `paths::` function that writes it: `data_dir`, `config_dir`,
1755+  `logs_dir`, `temp_dir`, `state_dir`, the CLI symlink under
1756+  `paths::BINARY_NAME`, and the bundle-identifier paths from
1757+  `release_channel::RELEASE_CHANNEL.app_id()`. The app bundle comes from the
1758+  bundle this CLI was launched out of, not from a literal. `plan` destructures
1759+  the struct **exhaustively**, so a root added to it and left out of the plan
1760+  does not compile. The script receives the plan in `OMEGA_UNINSTALL_PATHS`, one
1761+  absolute path per line, and removes exactly those; unset or empty exits
1762+  non-zero rather than falling back to a default, because every default this
1763+  file has ever had belonged to somebody else's product. `/`, `/Applications`,
1764+  `$HOME` and a relative path are refused before anything is removed.
1765+- **Settings are asked for, never taken.** `config_dir` is held out of the
1766+  automatic list and prompted for by name, because it is the user's
1767+  `settings.json` and keymap and the one root somebody may want to keep.
1768+- **Enforced by:** `the_uninstall_path_removes_omega_and_names_no_competitor` in
1769+  `crates/omega_deltas/` (the script names no other product; it still reads its
1770+  plan from the caller; `from_installed_paths` makes at least one `paths::` call
1771+  per root; `plan` still destructures exhaustively; the script refuses an empty
1772+  plan), and by three tests in `crates/cli/src/uninstall.rs` — including
1773+  `the_script_removes_omega_and_leaves_the_other_editor_untouched`, which runs
1774+  the shipped script against a fabricated home holding **both** an Omega
1775+  installation and another product's, and reads both halves back afterwards.
1776+- **Falsified.** Adding a competitor's directory to the script failed the name
1777+  assertion. Removing `paths::logs_dir()` from the plan failed both the plan
1778+  test and the end-to-end test, which reads the roots rather than the plan
1779+  precisely so a forgotten root cannot pass. Adding a hard-coded removal outside
1780+  the plan failed the "left untouched" assertion byte-for-byte. Each
1781+  falsification was probed against a pristine copy of the file before its test
1782+  ran, so an edit that did not apply is a hard error rather than a green run.
1783+- **Paid for once, in the worst way.** The first falsification of this delta
1784+  restored the `0.2.0-rc14` script over the rewritten one and ran the suite. The
1785+  refusal test did not override `HOME` at the time, the restored script ignores
1786+  the plan entirely, and it destroyed the real machine's other editor and its
1787+  application-support tree — the exact damage this entry describes. Every test
1788+  in that module now runs with a `HOME` of its own, whatever the script under it
1789+  happens to be that minute.
1790+
1791+### OMEGA-DELTA-0037 — Omega identifies itself to third parties as Omega
1792+
1793+- **Upstream Zed:** sends `HTTP-Referer: https://zed.dev` and
1794+  `X-Title: Zed Editor` on every OpenRouter request. Correct for Zed.
1795+- **Omega, before this:** sent the same two headers, unchanged, on both the
1796+  streaming and the non-streaming call. OpenRouter **displays the `X-Title`
1797+  value to the account holder in their own dashboard**, so this is not a wire
1798+  contract that happens to carry a name — it is Omega telling a third party, and
1799+  the user, that it is a different product. Every request through `0.2.0-rc14`
1800+  did it, and `strings` on the shipped binary found exactly one occurrence,
1801+  classified nowhere (omega#89).
1802+- **Omega now:** both call sites send `app_identity::PRODUCT_NAME` and
1803+  `app_identity::PRODUCT_REPOSITORY_URL`. The values come from the identity
1804+  constants rather than from literals, so a rebase cannot restore the old value
1805+  on one path and leave the other correct.
1806+- **Enforced by:** `outbound_attribution_names_omega` in `crates/omega_deltas/`,
1807+  which asserts both request paths set the header, that neither literal is back,
1808+  and that the count of `PRODUCT_NAME` uses equals the count of `X-Title`
1809+  headers. The pair is also recorded as `blocked` in
1810+  `crates/app_identity/fixtures/compatibility_allowlist.json`.
1811+- **Falsified.** Restoring `"Zed Editor"` on one of the two call sites failed
1812+  the test; restoring it on both failed it; the packaged scan reports it out of
1813+  the built binary independently.
1814+
1815+### OMEGA-DELTA-0038 — The packaged gate opens every executable that ships, and reads help as clap renders it
1816+
1817+- **Upstream Zed:** has no equivalent; this is entirely about Omega's own gate.
1818+- **Omega, before this:** `script/verify-omega-brand --app` opened
1819+  `Contents/MacOS/omega` and nothing else. `script/bundle-omega-rc` copies and
1820+  signs **three** binaries into `Contents/MacOS` — `omega`, `cli` and
1821+  `omega-identity-proof`. Two of the three had never been opened by any check,
1822+  which is why a destructive uninstaller (`OMEGA-DELTA-0036`) shipped in two
1823+  published prereleases with the gate reporting green about the bundle.
1824+  Separately, every prose stream in `OMEGA-DELTA-0031` reads **source**: a doc
1825+  line, a literal, a schema comment. clap does not print source. It joins
1826+  several doc lines into one sentence, resolves `cfg_attr` for the platform it
1827+  was built for, prints the flag *name* beside the description, and lays the
1828+  whole thing out at run time. Nothing had ever read that output, so
1829+  `--zed <ZED>`, `Run zed in the foreground`, `Run zed in dev-server mode`,
1830+  `Instructs zed to run as a dev server` and a `--user-data-dir` line naming a
1831+  different product's data directory as Omega's own all shipped under a green
1832+  gate (omega#89).
1833+- **Omega now, three derived inventories where there were three lists:**
1834+  - **Every executable in the bundle.** `bundle_executables` walks the app and
1835+    keeps every file whose first four bytes are Mach-O magic. The icon-name
1836+    rule, the embedded-asset rule, the prose scan and the first-party-agent scan
1837+    all run over that set; only the two *presence* checks — the reviewed artwork
1838+    and the required action labels — stay on the main binary, because only it
1839+    embeds them. A helper added to the bundle tomorrow is inside the gate the
1840+    day it is added.
1841+  - **Rendered output.** Every executable is run with `--help` and `--version`,
1842+    subcommands are enumerated **from the help text itself** rather than listed,
1843+    and each is run in turn. Every line of the result is read as prose. The
1844+    floor is on invocations *read*, not on findings.
1845+  - **`first_party_agent.phrases`, applied to something.** `grep -c
1846+    first_party_agent script/verify-omega-brand` returned **0**: the key existed,
1847+    a Rust test read it against the source tree, and no gate had ever applied it
1848+    to a package. A reviewer had to run it by hand every time, which is the same
1849+    as not having it. It now runs over every executable, with the identity
1850+    string itself as the anti-vacuity guard.
1851+- **A classified sentence is matched as the compiler leaves it.** Splitting
1852+  already handled `\` continuations, `\n` and non-ASCII; a `{}` format
1853+  placeholder is not in the binary either, because rustc splits a format string
1854+  at every placeholder. Classified entries are now split there too — conservative
1855+  in the same direction, since shorter fragments cover less, never more.
1856+- **Enforced by:**
1857+  `the_packaged_gate_opens_every_shipped_executable_and_reads_rendered_help` in
1858+  `crates/omega_deltas/`, which derives the shipped binary set by parsing what
1859+  `script/bundle-omega-rc` writes into `Contents/MacOS`, asserts the gate's floor
1860+  covers it, asserts the gate names a binary path in exactly one place, and
1861+  asserts each new check is both defined and called.
1862+- **Falsified against a real candidate.** The rewritten gate was run against the
1863+  installed, signed, notarized `0.2.0-rc15` in `/Applications`, which the
1864+  previous gate passed. It rejects it on 42 findings, including the uninstall
1865+  script's text inside `Contents/MacOS/cli` — a file the old gate never opened —
1866+  and on `cli --help` and `omega --help` printing three of the sentences above.
1867+  The rebuilt `cli` from this tree passes the same scan. Lowering
1868+  `minimum_executables` below what the bundler ships fails the delta test;
1869+  deleting either new check's call fails it.
1870+
1871+### OMEGA-DELTA-0039 — The installed-proof harness observes what it records
1872+
1873+- **Upstream Zed:** has no equivalent; this is Omega's own installed-candidate
1874+  harness.
1875+- **Omega, before this:** three checks in it could not fail, and a check that
1876+  cannot fail is worse than no check, because it produces a clean evidence table
1877+  (omega#90).
1878+  - **The secret tripwire.** `deliver_needle_through_protected_fd` made a pipe,
1879+    wrote a fresh `secrets.token_hex(32)` into it, and closed **both ends in the
1880+    same function**. No process was spawned; `--app` appeared only in the
1881+    docstring. The needle had never been seen by anything but the script, which
1882+    then searched the disk for it. `status: "pass"` was guaranteed by
1883+    construction, whatever the product did.
1884+  - **Four of six surfaces watched directories Omega never writes.** `logs`,
1885+    `telemetry`, `crashes` and `clipboard` resolved under
1886+    `~/Library/Application Support/Omega RC/…`, while `paths::logs_dir()` is
1887+    `~/Library/Logs/omega-rc` and `paths::crashes_dir()` is
1888+    `~/Library/Logs/DiagnosticReports`. The live 191 KB log was never opened.
1889+    They recorded `absent`, and `absent` did not fail the receipt.
1890+  - **`light-theme` and `dark-theme` wrote `content_legible: True` as a Python
1891+    literal**, with zero `ocr_lines()` calls and zero `differing_pixels()` calls,
1892+    never comparing the light capture against the dark one. A frozen, blank or
1893+    appearance-ignoring window passed both. This is the same shape as the
1894+    `defaults read` defect `5ce7f9855f` already corrected for `high-contrast`
1895+    and `reduced-motion`: a fact about the *host* filed as a fact about the
1896+    *product*.
1897+- **Omega now:**
1898+  - The needle is **read from the caller** through `--needle-fd` — the canary the
1899+    candidate was actually given — and a run without one refuses rather than
1900+    inventing a secret nothing has seen. Before any surface is trusted, the same
1901+    scanner is pointed at a needle planted in a private `0600` file; no match
1902+    ends the run with no receipt.
1903+  - Surfaces resolve from `crates/paths/src/paths.rs`: `logs_dir()`, the
1904+    telemetry log beside it, `database_dir()` plus `hang_traces_dir()`, and
1905+    `crashes_dir()`. Clipboard and accessibility are read through their own
1906+    interfaces — NSPasteboard (corroborated by `clipboard info`) and the
1907+    `AXUIElement` tree of the running candidate — because neither was ever a file
1908+    in a data directory. A surface that **cannot** be observed records `blocked`
1909+    and the validator refuses the receipt, so "nothing was found there" and
1910+    "nobody looked" no longer read the same.
1911+  - The host appearance is only the **precondition**. Legibility is OCR'd off
1912+    each capture against a stated line count and confidence, and the light and
1913+    dark captures must differ by a stated pixel threshold. Every failure routes
1914+    to `blocked`.
1915+  - `script/bundle-omega-rc` derived `"dirty": False` from nothing; it now reads
1916+    `git status --porcelain`, so a provenance field that reads like an
1917+    observation is one.
1918+- **Enforced by:** `the_installed_proof_harness_observes_what_it_records` in
1919+  `crates/omega_deltas/`, plus each collector's own `--self-test`.
1920+- **Falsified.** Breaking the needle comparison ended the run at exit 2 with no
1921+  receipt, where the old code would have written `"status": "pass"`. Repointing
1922+  the surfaces back at the data root blocked the receipt and the untouched
1923+  validator refused it; planting the needle in the real `~/Library/Logs/omega-rc`
1924+  produced `status: fail` over 193,984 bytes, which the old table could not have
1925+  seen. Four separate plants against the appearance block — an unconditional
1926+  pass, a dropped pixel threshold, a removed pixel diff and fabricated OCR lines
1927+  — each failed the self-test. Every falsification was probed against a pristine
1928+  copy before its test ran.
diff --git a/assets/keymaps/initial.json b/assets/keymaps/initial.json
index 4d9b06a88f..b39525fb88 100644
--- a/assets/keymaps/initial.json
+++ b/assets/keymaps/initial.json
@@ -3,7 +3,7 @@
33 // For information on binding keys, see the Zed
44 // documentation: https://zed.dev/docs/key-bindings
55 //
6-// To see the default key bindings run `zed: open default keymap`
6+// To see the default key bindings run `omega: open default keymap`
77 // from the command palette.
88 [
99   {
diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json
index 0267da536f..0c9b970e11 100755
--- a/assets/keymaps/linux/emacs.json
+++ b/assets/keymaps/linux/emacs.json
@@ -1,6 +1,6 @@
11 // documentation: https://zed.dev/docs/key-bindings
22 //
3-// To see the default key bindings run `zed: open default keymap`
3+// To see the default key bindings run `omega: open default keymap`
44 // from the command palette.
55 [
66   {
@@ -178,7 +178,7 @@
178178     //  so override with null for compound sequences (e.g. ctrl-x ctrl-c).
179179     "context": "Terminal",
180180     "bindings": {
181-      // If you want to perfect your emacs-in-zed setup, also consider the following.
181+      // If you want to perfect your emacs-in-omega setup, also consider the following.
182182       // You may need to enable "option_as_meta" from the Omega settings for "alt-x" to work.
183183       // "alt-x": ["terminal::SendKeystroke", "alt-x"],
184184       // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"],
diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json
index cc602f8e0e..a4ad85f586 100755
--- a/assets/keymaps/macos/emacs.json
+++ b/assets/keymaps/macos/emacs.json
@@ -1,6 +1,6 @@
11 // documentation: https://zed.dev/docs/key-bindings
22 //
3-// To see the default key bindings run `zed: open default keymap`
3+// To see the default key bindings run `omega: open default keymap`
44 // from the command palette.
55 [
66   {
@@ -172,7 +172,7 @@
172172     //  so override with null for compound sequences (e.g. ctrl-x ctrl-c).
173173     "context": "Terminal",
174174     "bindings": {
175-      // If you want to perfect your emacs-in-zed setup, also consider the following.
175+      // If you want to perfect your emacs-in-omega setup, also consider the following.
176176       // You may need to enable "option_as_meta" from the Omega settings for "alt-x" to work.
177177       // "alt-x": ["terminal::SendKeystroke", "alt-x"],
178178       // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"],
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index 4a8fd46cfd..81707e515e 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -112,9 +112,9 @@
112112       "g s": "outline::Toggle",
113113       "g shift-o": "outline::Toggle",
114114       "g shift-s": "project_symbols::Toggle",
115-      "g .": "editor::ToggleCodeActions", // zed specific
116-      "g shift-a": "editor::FindAllReferences", // zed specific
117-      "g space": "editor::OpenExcerpts", // zed specific
115+      "g .": "editor::ToggleCodeActions", // omega specific
116+      "g shift-a": "editor::FindAllReferences", // omega specific
117+      "g space": "editor::OpenExcerpts", // omega specific
118118       "g *": ["vim::MoveToNext", { "partial_word": true }],
119119       "g #": ["vim::MoveToPrevious", { "partial_word": true }],
120120       "g j": ["vim::Down", { "display_lines": true }],
@@ -359,9 +359,9 @@
359359       "escape": "vim::NormalBefore",
360360       "ctrl-x": null,
361361       "ctrl-x ctrl-o": "editor::ShowCompletions",
362-      "ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
363-      "ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
364-      "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
362+      "ctrl-x ctrl-a": "assistant::InlineAssist", // omega specific
363+      "ctrl-x ctrl-c": "editor::ShowEditPrediction", // omega specific
364+      "ctrl-x ctrl-l": "editor::ToggleCodeActions", // omega specific
365365       "ctrl-x ctrl-z": "editor::Cancel",
366366       "ctrl-x ctrl-e": "vim::LineDown",
367367       "ctrl-x ctrl-y": "vim::LineUp",
@@ -723,7 +723,7 @@
723723     "bindings": {
724724       "c": "vim::CurrentLine",
725725       "x": "vim::Exchange",
726-      "d": "editor::Rename", // zed specific
726+      "d": "editor::Rename", // omega specific
727727       "s": ["vim::PushChangeSurrounds", {}],
728728     },
729729   },
@@ -879,7 +879,7 @@
879879       "escape": ["vim::Literal", ["escape", "\u001B"]],
880880       "enter": ["vim::Literal", ["enter", "\u000D"]],
881881       "tab": ["vim::Literal", ["tab", "\u0009"]],
882-      // zed extensions:
882+      // omega extensions:
883883       "backspace": ["vim::Literal", ["backspace", "\u0008"]],
884884       "delete": ["vim::Literal", ["delete", "\u007F"]],
885885     },
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 2c06b1dcb4..cd64f39a66 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1742,8 +1742,8 @@
17421742     // When the resolved directory is outside the project root, the
17431743     // project's directory name is automatically appended so that
17441744     // sibling repos don't collide. For example, with the default
1745-    // "../worktrees" and a project at ~/code/zed, worktrees are
1746-    // created under ~/code/worktrees/zed/.
1745+    // "../worktrees" and a project at ~/code/omega, worktrees are
1746+    // created under ~/code/worktrees/omega/.
17471747     //
17481748     // When the resolved directory is inside the project root, no
17491749     // extra component is added (it's already project-scoped).
@@ -2635,7 +2635,7 @@
26352635     //   }
26362636     // ]
26372637     //
2638-    // Default rules are viewable via the "zed: show default semantic token rules" action.
2638+    // Default rules are viewable via the "omega: show default semantic token rules" action.
26392639     "semantic_token_rules": [],
26402640   },
26412641   // Jupyter settings
@@ -2859,7 +2859,7 @@
28592859   // Configuration for developer-oriented instrumentation tools that can be
28602860   // toggled at runtime.
28612861   "instrumentation": {
2862-    // Performance profiler, accessed via the `zed: open performance profiler`
2862+    // Performance profiler, accessed via the `omega: open performance profiler`
28632863     // action. Collects timing data for foreground and background executor
28642864     // tasks. Enabling this may lead to increased memory usage, hence it's
28652865     // disabled by default for regular builds.
diff --git a/crates/app_identity/fixtures/compatibility_allowlist.json b/crates/app_identity/fixtures/compatibility_allowlist.json
index 6fd4f26eb0..127025d9fc 100644
--- a/crates/app_identity/fixtures/compatibility_allowlist.json
+++ b/crates/app_identity/fixtures/compatibility_allowlist.json
@@ -144,6 +144,94 @@
144144       "owner": "OpenAgents",
145145       "disposition": "approved_compatibility",
146146       "expiry": "until-omega-scheme-only"
147+    },
148+    {
149+      "match": "x-zed-* / X-Zed-Predict-* request headers",
150+      "path": "crates/cloud_llm_client",
151+      "reason": "Header names on Zed's own hosted LLM and edit-prediction endpoints. The name is the wire contract of somebody else's service; renaming it would make the request unroutable rather than un-branded. Only sent when a user has deliberately configured that provider.",
152+      "owner": "OpenAgents",
153+      "disposition": "approved_compatibility",
154+      "expiry": "none"
155+    },
156+    {
157+      "match": "HTTP-Referer / X-Title on OpenRouter requests",
158+      "path": "crates/open_router/src/open_router.rs",
159+      "reason": "OpenRouter shows X-Title to the account holder in their own dashboard, so this is outbound product identity, not a compatibility seam. It said 'Zed Editor' and 'https://zed.dev' on every request through 0.2.0-rc14 (omega#89); it now sends app_identity::PRODUCT_NAME and PRODUCT_REPOSITORY_URL. Recorded here so a rebase that restores the upstream values is a policy change with a named owner.",
160+      "owner": "OpenAgents",
161+      "disposition": "blocked",
162+      "expiry": "none"
163+    },
164+    {
165+      "match": ".ZedMono / .ZedSans font aliases",
166+      "path": "crates/gpui/src/platform/mac",
167+      "reason": "PostScript family names recorded inside the shipped font files themselves. A name that does not match the font's own family name resolves to nothing, so this is a lookup key, not a claim (same reason as the classified 'Zed Plex Sans' prose entry).",
168+      "owner": "OpenAgents",
169+      "disposition": "approved_compatibility",
170+      "expiry": "until-owned-font-family"
171+    },
172+    {
173+      "match": "base_keymap values 'Zed' and 'Zed (Default)'",
174+      "path": "crates/settings_content",
175+      "reason": "Names Zed's own keymap preset, sitting beside VS Code, JetBrains, Sublime Text and Atom. The value is what a user writes in keymap settings and what ai_zed.svg labels; substituting Omega would claim a preset that is not Omega's.",
176+      "owner": "OpenAgents",
177+      "disposition": "required_attribution",
178+      "expiry": "none"
179+    },
180+    {
181+      "match": "Zed.dmg in the macOS update path",
182+      "path": "crates/auto_update",
183+      "reason": "Inherited disk-image file name in the updater's download path. Omega's updater is disabled by default (OMEGA-DELTA-0026) and no Omega release publishes this artifact; the name survives only in the inherited code path.",
184+      "owner": "OpenAgents",
185+      "disposition": "approved_compatibility",
186+      "expiry": "until-owned-update-artifact"
187+    },
188+    {
189+      "match": "ZedPredictModal keymap context",
190+      "path": "crates/edit_prediction_ui",
191+      "reason": "A keymap context users type into keymap.json, so renaming it silently breaks any existing binding. Internal identifier with a user-typed surface, not rendered copy.",
192+      "owner": "OpenAgents",
193+      "disposition": "approved_compatibility",
194+      "expiry": "until-omega-predict-context"
195+    },
196+    {
197+      "match": "'zed.dev' settings provider key and ZedDotDev* settings types",
198+      "path": "crates/language_models",
199+      "reason": "The provider id a user writes in settings to select Zed's hosted models. It identifies Zed's service the way 'anthropic' identifies Anthropic's; the service is disabled under Omega's isolation default, and the key stays so an inherited settings file still parses.",
200+      "owner": "OpenAgents",
201+      "disposition": "approved_compatibility",
202+      "expiry": "none"
203+    },
204+    {
205+      "match": "zed-remote-server / zed_remote_server artifact and host directory",
206+      "path": "crates/remote",
207+      "reason": "Both ends of an SSH session look each other up by this name, together with ~/.zed_server. Renaming one end alone strands the other.",
208+      "owner": "OpenAgents",
209+      "disposition": "approved_compatibility",
210+      "expiry": "until-omega-remote-rename"
211+    },
212+    {
213+      "match": "zed:api-version WASM section in extensions",
214+      "path": "crates/extension",
215+      "reason": "The custom section name every published extension is compiled with. It is a wire key; renaming it would make Omega unable to load any existing extension.",
216+      "owner": "OpenAgents",
217+      "disposition": "approved_compatibility",
218+      "expiry": "none"
219+    },
220+    {
221+      "match": "/Applications/Zed*.app, dev.zed.Zed, ~/.zed_server, ~/.config/zed in the uninstall path",
222+      "path": "script/uninstall.sh",
223+      "reason": "Recorded as BLOCKED. Through 0.2.0-rc14 the shipped `omega --uninstall` ran upstream's uninstaller verbatim: it removed the other editor's bundle, application-support tree, logs, caches and preferences, announced that editor had been uninstalled, and removed no Omega path at all (omega#88). The script now holds no path of its own; the plan is derived from paths:: on the Rust side and handed to it. A path table returning to this file is a policy change, not a rebase artefact.",
224+      "owner": "OpenAgents",
225+      "disposition": "blocked",
226+      "expiry": "none"
227+    },
228+    {
229+      "match": "--zed as a hidden alias of --omega on the CLI",
230+      "path": "crates/cli/src/main.rs",
231+      "reason": "The flag was named --zed and printed as `--zed <ZED>` in the rendered help of the signed 0.2.0-rc14 (omega#89). It is now --omega, with `zed` kept as a clap alias so an existing script or shell function keeps working. An alias is never printed in --help, which is why the rendered-output gate does not report it; it is recorded here so removing the compatibility is a decision rather than a cleanup.",
232+      "owner": "OpenAgents",
233+      "disposition": "approved_compatibility",
234+      "expiry": "until-cli-flag-cleanup"
147235     }
148236   ]
149237 }
diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs
index 4a0ca4413e..fdf1f6062f 100644
--- a/crates/askpass/src/askpass.rs
+++ b/crates/askpass/src/askpass.rs
@@ -246,10 +246,10 @@ impl PasswordProxy {
246246         >,
247247         executor: BackgroundExecutor,
248248     ) -> Result<Self> {
249-        let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
249+        let temp_dir = tempfile::Builder::new().prefix("omega-askpass").tempdir()?;
250250         let askpass_socket = temp_dir.path().join("askpass.sock");
251251         let current_exec =
252-            std::env::current_exe().context("Failed to determine current zed executable path.")?;
252+            std::env::current_exe().context("Failed to determine current Omega executable path.")?;
253253 
254254         let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
255255 
diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs
index d7ad0b347e..c4154d70b6 100644
--- a/crates/cli/src/cli.rs
+++ b/crates/cli/src/cli.rs
@@ -5,6 +5,8 @@ use collections::HashMap;
55 pub use ipc_channel::ipc;
66 use serde::{Deserialize, Serialize};
77 
8+pub mod uninstall;
9+
810 #[derive(Serialize, Deserialize)]
911 pub struct IpcHandshake {
1012     pub requests: ipc::IpcSender<CliRequest>,
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index 9b0b78c8e9..3dfdf0cdb5 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -36,7 +36,7 @@ const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://
3636 struct Detect;
3737 
3838 trait InstalledApp {
39-    fn zed_version_string(&self) -> String;
39+    fn app_version_string(&self) -> String;
4040     fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()>;
4141     fn run_foreground(
4242         &self,
@@ -87,11 +87,14 @@ struct Args {
8787     classic: bool,
8888     /// Sets a custom directory for all user data (e.g., database, extensions, logs).
8989     /// This overrides the default platform-specific data directory location:
90-    #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
91-    #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")]
90+    #[cfg_attr(
91+        target_os = "macos",
92+        doc = "`~/Library/Application Support/<Omega channel>`."
93+    )]
94+    #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\<Omega channel>`.")]
9295     #[cfg_attr(
9396         not(any(target_os = "windows", target_os = "macos")),
94-        doc = "`$XDG_DATA_HOME/zed`."
97+        doc = "`$XDG_DATA_HOME/<omega-channel>`."
9598     )]
9699     #[arg(long, value_name = "DIR", value_hint = clap::ValueHint::DirPath)]
97100     user_data_dir: Option<String>,
@@ -103,13 +106,13 @@ struct Args {
103106     /// Print Omega's version and the app path.
104107     #[arg(short, long)]
105108     version: bool,
106-    /// Run zed in the foreground (useful for debugging)
109+    /// Run Omega in the foreground (useful for debugging)
107110     #[arg(long)]
108111     foreground: bool,
109112     /// Custom path to Omega.app or the omega binary
110-    #[arg(long)]
111-    zed: Option<PathBuf>,
112-    /// Run zed in dev-server mode
113+    #[arg(long = "omega", alias = "zed")]
114+    omega: Option<PathBuf>,
115+    /// Run Omega in dev-server mode
113116     #[arg(long)]
114117     dev_server_token: Option<String>,
115118     /// The username and WSL distribution to use when opening paths. If not specified,
@@ -161,7 +164,7 @@ struct Args {
161164 /// If a part of path doesn't exist, it will canonicalize the
162165 /// existing part and append the non-existing part.
163166 ///
164-/// This method must return an absolute path, as many zed
167+/// This method must return an absolute path, as many Omega
165168 /// crates assume absolute paths.
166169 fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
167170     match Path::new(argument_str).canonicalize() {
@@ -204,7 +207,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
204207 
205208 /// Returns whether a `--diff` argument refers to an existing path, allowing a
206209 /// trailing `:line:column` suffix (parsed later by the Omega side, matching how
207-/// regular `zed path:line:column` arguments are handled).
210+/// regular `omega path:line:column` arguments are handled).
208211 fn diff_path_exists(diff_path: &str) -> bool {
209212     Path::new(diff_path).exists() || PathWithPosition::parse_str(diff_path).path.exists()
210213 }
@@ -527,7 +530,7 @@ fn run() -> Result<()> {
527530     #[cfg(target_os = "linux")]
528531     let args = flatpak::set_bin_if_no_escape(args);
529532 
530-    let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
533+    let app = Detect::detect(args.omega.as_deref()).context("Bundle detection")?;
531534 
532535     if let Some(shell) = &args.completions {
533536         let file_path = std::env::current_exe()?;
@@ -544,7 +547,7 @@ fn run() -> Result<()> {
544547     }
545548 
546549     if args.version {
547-        println!("{}", app.zed_version_string());
550+        println!("{}", app.app_version_string());
548551         return Ok(());
549552     }
550553 
@@ -563,6 +566,12 @@ fn run() -> Result<()> {
563566         not(feature = "no-bundled-uninstall")
564567     ))]
565568     if args.uninstall {
569+        // OMEGA-DELTA-0036. The plan is built here, from the same `paths::`
570+        // functions the running application writes those directories with, and
571+        // handed to the script. The script has no paths of its own, because a
572+        // hand-written path table disconnected from the code that creates the
573+        // directories is exactly how `0.2.0-rc14` shipped an "Uninstall Omega"
574+        // that kept Omega and deleted another editor instead.
566575         static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
567576 
568577         let tmp_dir = tempfile::tempdir()?;
@@ -572,9 +581,14 @@ fn run() -> Result<()> {
572581         use std::os::unix::fs::PermissionsExt as _;
573582         fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
574583 
584+        let plan = cli::uninstall::UninstallRoots::from_installed_paths(Some(app.path()))
585+            .plan(paths::app_name());
586+
575587         let status = std::process::Command::new("sh")
576588             .arg(&script_path)
577-            .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)
589+            .env("OMEGA_UNINSTALL_PRODUCT", &plan.product)
590+            .env("OMEGA_UNINSTALL_PATHS", plan.paths_env())
591+            .env("OMEGA_UNINSTALL_CONFIG_DIR", &plan.config_dir)
578592             .status()
579593             .context("Failed to execute uninstall script")?;
580594 
@@ -841,7 +855,7 @@ fn anonymous_fd(path: &str) -> Option<fs::File> {
841855 }
842856 
843857 /// Shows an interactive prompt asking the user to choose the default open
844-/// behavior for `zed <path>`. Returns `None` if the prompt cannot be shown
858+/// behavior for `omega <path>`. Returns `None` if the prompt cannot be shown
845859 /// (e.g. stdin is not a terminal) or the user cancels.
846860 fn prompt_open_behavior() -> Option<cli::CliBehaviorSetting> {
847861     if !std::io::stdin().is_terminal() {
@@ -926,9 +940,9 @@ mod linux {
926940     }
927941 
928942     impl InstalledApp for App {
929-        fn zed_version_string(&self) -> String {
943+        fn app_version_string(&self) -> String {
930944             format!(
931-                "Zed {}{}{} – {}",
945+                "Omega {}{}{} – {}",
932946                 if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
933947                     "".to_string()
934948                 } else {
@@ -1059,7 +1073,7 @@ mod flatpak {
10591073         if let Some(flatpak_dir) = get_flatpak_dir() {
10601074             let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
10611075             args.append(&mut get_xdg_env_args());
1062-            args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
1076+            args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update omega".into());
10631077             args.push(
10641078                 format!(
10651079                     "--env={EXTRA_LIB_ENV_NAME}={}",
@@ -1089,9 +1103,9 @@ mod flatpak {
10891103     pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
10901104         if env::var(NO_ESCAPE_ENV_NAME).is_ok()
10911105             && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("com.openagents.omega"))
1092-            && args.zed.is_none()
1106+            && args.omega.is_none()
10931107         {
1094-            args.zed = Some("/app/libexec/omega-editor".into());
1108+            args.omega = Some("/app/libexec/omega-editor".into());
10951109             unsafe {
10961110                 env::set_var(
10971111                     "ZED_UPDATE_EXPLANATION",
@@ -1178,9 +1192,9 @@ mod windows {
11781192     struct App(PathBuf);
11791193 
11801194     impl InstalledApp for App {
1181-        fn zed_version_string(&self) -> String {
1195+        fn app_version_string(&self) -> String {
11821196             format!(
1183-                "Zed {}{}{} – {}",
1197+                "Omega {}{}{} – {}",
11841198                 if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
11851199                     "".to_string()
11861200                 } else {
@@ -1356,8 +1370,8 @@ mod mac_os {
13561370     }
13571371 
13581372     impl InstalledApp for Bundle {
1359-        fn zed_version_string(&self) -> String {
1360-            format!("Zed {} – {}", self.version(), self.path().display(),)
1373+        fn app_version_string(&self) -> String {
1374+            format!("Omega {} – {}", self.version(), self.path().display(),)
13611375         }
13621376 
13631377         fn launch(&self, url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
@@ -1393,7 +1407,7 @@ mod mac_os {
13931407                     anyhow::ensure!(
13941408                         status == 0,
13951409                         "cannot start app bundle {}",
1396-                        self.zed_version_string()
1410+                        self.app_version_string()
13971411                     );
13981412                 }
13991413 
diff --git a/crates/cli/src/uninstall.rs b/crates/cli/src/uninstall.rs
new file mode 100644
index 0000000000..55437afec4
--- /dev/null
+++ b/crates/cli/src/uninstall.rs
@@ -0,0 +1,360 @@
1+//! What `omega --uninstall` removes. OMEGA-DELTA-0036.
2+//!
3+//! Up to `0.2.0-rc14` this path ran upstream's uninstaller unchanged. A flag
4+//! advertised as "Uninstall Omega from user system" removed the *other*
5+//! editor's application bundle, its whole application-support tree, its logs,
6+//! its preferences and its remote-server directory, told the user that editor
7+//! had been uninstalled, and removed no Omega path whatsoever. Omega survived
8+//! the uninstall intact.
9+//!
10+//! The defect was not a typo in a path. It was that the paths lived in a
11+//! hand-written table in a shell script, disconnected from the code that
12+//! creates those directories, so nothing in the tree could notice that the two
13+//! had never agreed. So there is no table here either: every root comes from
14+//! the `paths::` function that produced it, and the plan is a value the tests
15+//! can read.
16+
17+use std::path::{Path, PathBuf};
18+
19+/// The roots an Omega installation occupies on this machine.
20+///
21+/// One field per place Omega writes. `plan` destructures this exhaustively, so
22+/// a root added here without being planned does not compile — which is the
23+/// property the old shell table could not have.
24+#[derive(Debug, Clone, PartialEq, Eq)]
25+pub struct UninstallRoots {
26+    /// The application bundle or executable this CLI belongs to.
27+    pub app: Option<PathBuf>,
28+    /// `paths::data_dir()` — database, extensions, embeddings.
29+    pub data_dir: PathBuf,
30+    /// `paths::config_dir()` — settings and keymap. Prompted for, never
31+    /// removed silently: it is the only root a user may want to keep.
32+    pub config_dir: PathBuf,
33+    /// `paths::logs_dir()` — on macOS this is `~/Library/Logs/<slug>`, not a
34+    /// subdirectory of the data root. The tripwire harness got this wrong in
35+    /// the other direction (omega#90).
36+    pub logs_dir: PathBuf,
37+    /// `paths::temp_dir()` — the cache root.
38+    pub temp_dir: PathBuf,
39+    /// `paths::state_dir()`.
40+    pub state_dir: PathBuf,
41+    /// The `omega` symlink an earlier "install CLI" put on `PATH`.
42+    pub cli_symlink: PathBuf,
43+    /// Paths keyed by the macOS bundle identifier: preferences, HTTP storage,
44+    /// saved application state. Empty off macOS.
45+    pub platform_paths: Vec<PathBuf>,
46+}
47+
48+/// The display name and the paths, ready to hand to `script/uninstall.sh`.
49+#[derive(Debug, Clone, PartialEq, Eq)]
50+pub struct UninstallPlan {
51+    /// Channel display name, e.g. `Omega RC`.
52+    pub product: String,
53+    /// Everything removed without asking.
54+    pub removals: Vec<PathBuf>,
55+    /// Removed only if the user says so.
56+    pub config_dir: PathBuf,
57+}
58+
59+impl UninstallRoots {
60+    /// The roots of the installation this CLI binary belongs to.
61+    ///
62+    /// Every field is read from the function that writes it. Nothing here is a
63+    /// literal path.
64+    #[cfg(any(target_os = "linux", target_os = "macos"))]
65+    pub fn from_installed_paths(app: Option<PathBuf>) -> Self {
66+        let app_id = release_channel::RELEASE_CHANNEL.app_id();
67+        let home = paths::home_dir();
68+        let platform_paths = if cfg!(target_os = "macos") {
69+            vec![
70+                home.join("Library/HTTPStorages").join(app_id),
71+                home.join("Library/Preferences").join(format!("{app_id}.plist")),
72+                home.join("Library/Saved Application State")
73+                    .join(format!("{app_id}.savedState")),
74+            ]
75+        } else {
76+            vec![
77+                home.join(".local/share/applications")
78+                    .join(format!("{app_id}.desktop")),
79+            ]
80+        };
81+        Self {
82+            app,
83+            data_dir: paths::data_dir().clone(),
84+            config_dir: paths::config_dir().clone(),
85+            logs_dir: paths::logs_dir().clone(),
86+            temp_dir: paths::temp_dir().clone(),
87+            state_dir: paths::state_dir().clone(),
88+            cli_symlink: home.join(".local/bin").join(paths::BINARY_NAME),
89+            platform_paths,
90+        }
91+    }
92+
93+    /// Turn the roots into a plan, de-duplicated and in a stable order.
94+    ///
95+    /// The config directory is held back deliberately: it is the user's
96+    /// settings and keymap, and the caller asks before removing it.
97+    pub fn plan(&self, product: &str) -> UninstallPlan {
98+        let Self {
99+            app,
100+            data_dir,
101+            config_dir,
102+            logs_dir,
103+            temp_dir,
104+            state_dir,
105+            cli_symlink,
106+            platform_paths,
107+        } = self;
108+
109+        let mut removals: Vec<PathBuf> = Vec::new();
110+        let mut push = |path: &Path| {
111+            let path = path.to_path_buf();
112+            if path != *config_dir && !removals.contains(&path) {
113+                removals.push(path);
114+            }
115+        };
116+        if let Some(app) = app {
117+            push(app);
118+        }
119+        push(data_dir);
120+        push(logs_dir);
121+        push(temp_dir);
122+        push(state_dir);
123+        push(cli_symlink);
124+        for path in platform_paths {
125+            push(path);
126+        }
127+
128+        UninstallPlan {
129+            product: product.to_owned(),
130+            removals,
131+            config_dir: config_dir.clone(),
132+        }
133+    }
134+}
135+
136+impl UninstallPlan {
137+    /// The newline-separated path list `script/uninstall.sh` reads.
138+    pub fn paths_env(&self) -> String {
139+        self.removals
140+            .iter()
141+            .map(|path| path.to_string_lossy().into_owned())
142+            .collect::<Vec<_>>()
143+            .join("\n")
144+    }
145+}
146+
147+#[cfg(test)]
148+#[allow(
149+    clippy::disallowed_methods,
150+    reason = "These tests run the shipped uninstall script as a subprocess; there is no async runtime here."
151+)]
152+mod tests {
153+    use super::*;
154+
155+    fn roots(home: &Path) -> UninstallRoots {
156+        UninstallRoots {
157+            app: Some(home.join("Applications/Omega.app")),
158+            data_dir: home.join("Library/Application Support/Omega RC"),
159+            config_dir: home.join(".config/omega-rc"),
160+            logs_dir: home.join("Library/Logs/omega-rc"),
161+            temp_dir: home.join("Library/Caches/omega-rc"),
162+            state_dir: home.join(".local/state/omega-rc"),
163+            cli_symlink: home.join(".local/bin/omega"),
164+            platform_paths: vec![
165+                home.join("Library/HTTPStorages/com.openagents.omega.rc"),
166+                home.join("Library/Preferences/com.openagents.omega.rc.plist"),
167+            ],
168+        }
169+    }
170+
171+    #[test]
172+    fn the_plan_holds_the_config_directory_back_and_keeps_everything_else() {
173+        let home = Path::new("/tmp/omega-uninstall-fixture");
174+        let plan = roots(home).plan("Omega RC");
175+        assert_eq!(plan.product, "Omega RC");
176+        assert!(
177+            !plan.removals.contains(&plan.config_dir),
178+            "settings and keymap are prompted for, never removed silently"
179+        );
180+        for expected in [
181+            home.join("Applications/Omega.app"),
182+            home.join("Library/Application Support/Omega RC"),
183+            home.join("Library/Logs/omega-rc"),
184+            home.join("Library/Caches/omega-rc"),
185+            home.join(".local/state/omega-rc"),
186+            home.join(".local/bin/omega"),
187+        ] {
188+            assert!(
189+                plan.removals.contains(&expected),
190+                "the plan omits {}, which an installed Omega writes",
191+                expected.display()
192+            );
193+        }
194+        assert_eq!(
195+            plan.paths_env().lines().count(),
196+            plan.removals.len(),
197+            "one path per line, and no path carrying a newline"
198+        );
199+    }
200+
201+    /// OMEGA-DELTA-0036. The shipped script, run for real, against a machine
202+    /// that has both editors installed.
203+    ///
204+    /// This is the assertion `0.2.0-rc14` would have failed on every count: it
205+    /// removed none of the Omega tree and all of the other one.
206+    #[test]
207+    fn the_script_removes_omega_and_leaves_the_other_editor_untouched() {
208+        let temp = tempfile::tempdir().expect("temp dir");
209+        let home = temp.path();
210+        let roots = roots(home);
211+
212+        // An Omega installation.
213+        let mut planted = Vec::new();
214+        for directory in [
215+            &roots.app.clone().unwrap(),
216+            &roots.data_dir,
217+            &roots.config_dir,
218+            &roots.logs_dir,
219+            &roots.temp_dir,
220+            &roots.state_dir,
221+        ] {
222+            std::fs::create_dir_all(directory).expect("create omega root");
223+            let file = directory.join("marker");
224+            std::fs::write(&file, b"omega").expect("write omega marker");
225+            planted.push(file);
226+        }
227+        std::fs::create_dir_all(roots.cli_symlink.parent().unwrap()).unwrap();
228+        std::fs::write(&roots.cli_symlink, b"omega").unwrap();
229+        for path in &roots.platform_paths {
230+            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
231+            std::fs::write(path, b"omega").unwrap();
232+        }
233+
234+        // The other editor, installed alongside it. Read back byte for byte
235+        // afterwards: "we did not mean to" is not an observation.
236+        let foreign: Vec<PathBuf> = [
237+            "Applications/OtherEditor.app/Contents/Info.plist",
238+            "Library/Application Support/OtherEditor/db/state",
239+            "Library/Logs/OtherEditor/log.txt",
240+            ".config/othereditor/settings.json",
241+            ".othereditor_server/bin",
242+        ]
243+        .iter()
244+        .map(|relative| home.join(relative))
245+        .collect();
246+        for path in &foreign {
247+            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
248+            std::fs::write(path, b"not ours to remove").unwrap();
249+        }
250+
251+        let plan = roots.plan("Omega RC");
252+        let script = Path::new(env!("CARGO_MANIFEST_DIR"))
253+            .join("../../script/uninstall.sh")
254+            .canonicalize()
255+            .expect("the shipped uninstall script");
256+        let output = std::process::Command::new("sh")
257+            .arg(&script)
258+            .env("HOME", home)
259+            .env("OMEGA_UNINSTALL_PRODUCT", &plan.product)
260+            .env("OMEGA_UNINSTALL_PATHS", plan.paths_env())
261+            .env("OMEGA_UNINSTALL_CONFIG_DIR", &plan.config_dir)
262+            .env("OMEGA_UNINSTALL_ASSUME_YES", "1")
263+            .output()
264+            .expect("run the uninstall script");
265+        assert!(
266+            output.status.success(),
267+            "uninstall failed: {}",
268+            String::from_utf8_lossy(&output.stderr)
269+        );
270+
271+        // Read against the ROOTS, not against the plan. Asserting that
272+        // everything the plan listed is gone is circular: a plan that forgot a
273+        // root would pass it. Destructuring is exhaustive, so a root added to
274+        // the struct and left out of the plan fails to compile here.
275+        let UninstallRoots {
276+            app,
277+            data_dir,
278+            config_dir,
279+            logs_dir,
280+            temp_dir,
281+            state_dir,
282+            cli_symlink,
283+            platform_paths,
284+        } = &roots;
285+        let occupied: Vec<&PathBuf> = app
286+            .iter()
287+            .chain([data_dir, config_dir, logs_dir, temp_dir, state_dir, cli_symlink])
288+            .chain(platform_paths.iter())
289+            .collect();
290+        for path in occupied {
291+            assert!(
292+                !path.exists(),
293+                "{} survived the uninstall, so Omega was not removed",
294+                path.display()
295+            );
296+        }
297+        for path in &foreign {
298+            assert_eq!(
299+                std::fs::read(path).ok().as_deref(),
300+                Some(b"not ours to remove".as_slice()),
301+                "{} was touched; the uninstall reached into another product",
302+                path.display()
303+            );
304+        }
305+    }
306+
307+    /// Every test in this module runs the real uninstaller, so every one of
308+    /// them gets a `HOME` of its own.
309+    ///
310+    /// This is not hygiene. On 2026-07-25 a lane falsifying this delta restored
311+    /// the `0.2.0-rc14` script over this file and ran the tests; the refusal
312+    /// test below did not override `HOME`, the restored script ignored the
313+    /// plan entirely, and it deleted the real machine's other editor and its
314+    /// whole application-support tree. A test that runs an uninstaller must
315+    /// never be able to see the real home directory, whatever the script under
316+    /// it happens to be that minute.
317+    #[test]
318+    fn the_script_refuses_an_empty_or_relative_plan() {
319+        let temp = tempfile::tempdir().expect("temp dir");
320+        let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../script/uninstall.sh");
321+        for (paths, why) in [
322+            ("", "an empty plan"),
323+            ("Library/Application Support/Omega RC", "a relative path"),
324+            ("/", "the root of the filesystem"),
325+            ("/Applications", "a whole system directory"),
326+        ] {
327+            let output = std::process::Command::new("sh")
328+                .arg(&script)
329+                .env("HOME", temp.path())
330+                .env("OMEGA_UNINSTALL_PRODUCT", "Omega RC")
331+                .env("OMEGA_UNINSTALL_PATHS", paths)
332+                .env("OMEGA_UNINSTALL_ASSUME_YES", "1")
333+                .output()
334+                .expect("run the uninstall script");
335+            assert!(
336+                !output.status.success(),
337+                "the script accepted {why}; refusing is the safe direction"
338+            );
339+        }
340+    }
341+
342+    #[test]
343+    fn the_uninstall_script_names_no_other_product() {
344+        let script = std::fs::read_to_string(
345+            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../script/uninstall.sh"),
346+        )
347+        .expect("the shipped uninstall script");
348+        let lowered = script.to_lowercase();
349+        assert!(
350+            !lowered.contains("zed"),
351+            "script/uninstall.sh names a competitor's product. It ships inside \
352+             the signed `cli` binary and removes whatever it names."
353+        );
354+        assert!(
355+            script.contains("OMEGA_UNINSTALL_PATHS"),
356+            "the script no longer reads its plan from the caller, so it has a \
357+             path table again, which is what shipped omega#88"
358+        );
359+    }
360+}
diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs
index 32cc38027d..a4a39d78ef 100644
--- a/crates/component_preview/src/component_preview.rs
+++ b/crates/component_preview/src/component_preview.rs
@@ -591,7 +591,7 @@ impl ComponentPreview {
591591         if let Some(workspace) = self.workspace.upgrade() {
592592             workspace.update(cx, |workspace, cx| {
593593                 let status_toast =
594-                    StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
594+                    StatusToast::new("`omega/new-notification-system` created!", cx, |this, _cx| {
595595                         this.icon(
596596                             Icon::new(IconName::GitBranch)
597597                                 .size(IconSize::Small)
diff --git a/crates/edit_prediction_cli/src/split_commit.rs b/crates/edit_prediction_cli/src/split_commit.rs
index 2868a441cf..3b7cc3a116 100644
--- a/crates/edit_prediction_cli/src/split_commit.rs
+++ b/crates/edit_prediction_cli/src/split_commit.rs
@@ -81,9 +81,9 @@ pub struct SplitCommitArgs {
8181 #[derive(Debug, Clone, Deserialize)]
8282 #[allow(dead_code)]
8383 pub struct AnnotatedCommit {
84-    /// Repository path (e.g., "repos/zed")
84+    /// Repository path (e.g., "repos/omega")
8585     pub repo: String,
86-    /// Repository URL (e.g., "https://github.com/zed-industries/zed")
86+    /// Repository URL (e.g., "https://github.com/OpenAgentsInc/omega")
8787     pub repo_url: String,
8888     /// Commit SHA
8989     pub commit_sha: String,
diff --git a/crates/eval_cli/src/main.rs b/crates/eval_cli/src/main.rs
index 389f68757d..111a40dc5e 100644
--- a/crates/eval_cli/src/main.rs
+++ b/crates/eval_cli/src/main.rs
@@ -339,7 +339,7 @@ fn main() {
339339 /// Name of the env var carrying a JSON object to merge into
340340 /// `language_models.openai_compatible` user settings before model discovery, in
341341 /// the same shape as Omega's `openai_compatible` settings key (provider id ->
342-/// `{ "api_url": ..., "available_models": [...] }`). Lets zed-eval route the
342+/// `{ "api_url": ..., "available_models": [...] }`). Lets eval-cli route the
343343 /// agent itself through an OpenAI-compatible endpoint (e.g. Baseten) that isn't
344344 /// one of Omega's built-in providers, without hardcoding it into eval-cli.
345345 const OPENAI_COMPATIBLE_PROVIDERS_ENV: &str = "ZED_OPENAI_COMPATIBLE_PROVIDERS";
@@ -365,7 +365,7 @@ fn apply_openai_compatible_providers(providers_json: &str, cx: &mut gpui::App) -
365365 /// `language_models.anthropic.available_models` user settings before model
366366 /// discovery, in the same shape as Omega's `anthropic.available_models` settings
367367 /// key (a list of `{ "name": ..., "max_tokens": ..., ... }` entries). Lets
368-/// zed-eval run models that exist on the Anthropic API for the configured
368+/// eval-cli run models that exist on the Anthropic API for the configured
369369 /// key (e.g. early-access-program models) but aren't returned by the live
370370 /// `/v1/models` listing, without hardcoding them into eval-cli.
371371 const ANTHROPIC_AVAILABLE_MODELS_ENV: &str = "ZED_ANTHROPIC_AVAILABLE_MODELS";
diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs
index 587315debf..68c1845e57 100644
--- a/crates/gpui_wgpu/src/wgpu_renderer.rs
+++ b/crates/gpui_wgpu/src/wgpu_renderer.rs
@@ -639,7 +639,7 @@ impl WgpuRenderer {
639639                 "BUG: dual_source_blending flag is true but device does not \
640640                  have DUAL_SOURCE_BLENDING enabled (device features: {:?}). \
641641                  Falling back to mono text rendering. Please report this at \
642-                 https://github.com/zed-industries/zed/issues",
642+                 https://github.com/OpenAgentsInc/omega/issues",
643643                 device.features(),
644644             );
645645         }
diff --git a/crates/http_proxy/src/proxy/connection.rs b/crates/http_proxy/src/proxy/connection.rs
index d77096a89c..1990ef710b 100644
--- a/crates/http_proxy/src/proxy/connection.rs
+++ b/crates/http_proxy/src/proxy/connection.rs
@@ -801,8 +801,8 @@ fn deny_request(
801801     );
802802     let response = format!(
803803         "HTTP/1.1 511 Network Authentication Required\r\n\
804-         Via: 1.1 zed-sandbox-proxy\r\n\
805-         Proxy-Status: zed-sandbox-proxy; error={}; details=\"{}\"\r\n\
804+         Via: 1.1 omega-sandbox-proxy\r\n\
805+         Proxy-Status: omega-sandbox-proxy; error={}; details=\"{}\"\r\n\
806806          Content-Type: text/plain; charset=utf-8\r\n\
807807          Content-Length: {}\r\n\
808808          Connection: close\r\n\r\n{body}",
diff --git a/crates/http_proxy/tests/end_to_end.rs b/crates/http_proxy/tests/end_to_end.rs
index 0bc3536aa0..c134e5aa2a 100644
--- a/crates/http_proxy/tests/end_to_end.rs
+++ b/crates/http_proxy/tests/end_to_end.rs
@@ -246,8 +246,12 @@ fn connect_denied_host_returns_511_with_via_header() {
246246         response.starts_with("HTTP/1.1 511 "),
247247         "expected 511, got: {response}"
248248     );
249-    assert!(response.contains("Via: 1.1 zed-sandbox-proxy"));
250-    assert!(response.contains("Proxy-Status: zed-sandbox-proxy"));
249+    // OMEGA-DELTA-0031. The refusal a blocked request reads names Omega's
250+    // proxy, not the upstream fork's. This assertion carried the upstream
251+    // value; the delta is the policy record, so it is updated rather than
252+    // deleted.
253+    assert!(response.contains("Via: 1.1 omega-sandbox-proxy"));
254+    assert!(response.contains("Proxy-Status: omega-sandbox-proxy"));
251255     assert!(response.contains("denied.example"));
252256     assert!(response.contains("not in this conversation's network allowlist"));
253257 
diff --git a/crates/inspector_ui/src/inspector.rs b/crates/inspector_ui/src/inspector.rs
index 3e6a37c6f2..0c2a7229ff 100644
--- a/crates/inspector_ui/src/inspector.rs
+++ b/crates/inspector_ui/src/inspector.rs
@@ -173,11 +173,11 @@ async fn open_zed_source_location(
173173         .arg(&path_arg)
174174         .output()
175175         .await
176-        .with_context(|| format!("running zed to open {path_arg} failed"))?;
176+        .with_context(|| format!("running omega to open {path_arg} failed"))?;
177177 
178178     if !output.status.success() {
179179         Err(anyhow!(
180-            "running zed to open {path_arg} failed with stderr: {}",
180+            "running omega to open {path_arg} failed with stderr: {}",
181181             String::from_utf8_lossy(&output.stderr)
182182         ))
183183     } else {
diff --git a/crates/install_cli/src/install_cli_binary.rs b/crates/install_cli/src/install_cli_binary.rs
index ec890fa3a9..1acf3d1261 100644
--- a/crates/install_cli/src/install_cli_binary.rs
+++ b/crates/install_cli/src/install_cli_binary.rs
@@ -98,7 +98,7 @@ pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) {
9898             // The user dismissed the administrator prompt; nothing to do.
9999             Ok(None) => return Ok(()),
100100             Err(error) => {
101-                log::error!("failed to install zed CLI: {error:#}");
101+                log::error!("failed to install the Omega CLI: {error:#}");
102102                 workspace.update(cx, |workspace, cx| {
103103                     struct CliInstallFailed;
104104 
@@ -108,7 +108,7 @@ pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) {
108108                         |cx| {
109109                             cx.new(|cx| {
110110                                 MessageNotification::new(
111-                                    "You can add `zed` to your PATH manually.",
111+                                    "You can add `omega` to your PATH manually.",
112112                                     cx,
113113                                 )
114114                                 .with_title("Couldn't install the Omega CLI")
diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs
index 6134c41049..9d9d6f9f2c 100644
--- a/crates/markdown/src/markdown.rs
+++ b/crates/markdown/src/markdown.rs
@@ -2918,7 +2918,7 @@ fn image_fallback_element(
29182918         .min_w_0()
29192919         .child(Label::new(label).color(Color::Warning).underline())
29202920         .tooltip(Tooltip::text(
2921-            "Image failed to load. Open `zed: log` for more details.",
2921+            "Image failed to load. Open `workspace: open log` for more details.",
29222922         ))
29232923         .when(open_image_url_on_click, |this| {
29242924             this.cursor_pointer()
diff --git a/crates/notifications/src/status_toast.rs b/crates/notifications/src/status_toast.rs
index e07cb99df5..651484f8cb 100644
--- a/crates/notifications/src/status_toast.rs
+++ b/crates/notifications/src/status_toast.rs
@@ -178,7 +178,7 @@ impl Component for StatusToast {
178178             },
179179         );
180180 
181-        let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
181+        let success_example = StatusToast::new("Pushed 4 changes to `omega/main`", cx, |this, _| {
182182             this.icon(
183183                 Icon::new(IconName::Check)
184184                     .size(IconSize::Small)
@@ -187,7 +187,7 @@ impl Component for StatusToast {
187187         });
188188 
189189         let error_example = StatusToast::new(
190-            "git push: Couldn't find remote origin `iamnbutler/zed`",
190+            "git push: Couldn't find remote origin `iamnbutler/omega`",
191191             cx,
192192             |this, _cx| {
193193                 this.icon(
@@ -209,7 +209,7 @@ impl Component for StatusToast {
209209         });
210210 
211211         let pr_example =
212-            StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
212+            StatusToast::new("`omega/new-notification-system` created!", cx, |this, _cx| {
213213                 this.icon(
214214                     Icon::new(IconName::GitBranch)
215215                         .size(IconSize::Small)
diff --git a/crates/omega_deltas/src/omega_deltas.rs b/crates/omega_deltas/src/omega_deltas.rs
index 50046f8ee9..ede030ca4c 100644
--- a/crates/omega_deltas/src/omega_deltas.rs
+++ b/crates/omega_deltas/src/omega_deltas.rs
@@ -63,8 +63,27 @@ pub const ENFORCED_DELTAS: &[&str] = &[
6363     "OMEGA-DELTA-0033",
6464     "OMEGA-DELTA-0034",
6565     "OMEGA-DELTA-0035",
66+    "OMEGA-DELTA-0036",
67+    "OMEGA-DELTA-0037",
68+    "OMEGA-DELTA-0038",
69+    "OMEGA-DELTA-0039",
6670 ];
6771 
72+/// OMEGA-DELTA-0036. The uninstall script embedded in the shipped `cli`.
73+pub const UNINSTALL_SCRIPT_PATH: &str = "script/uninstall.sh";
74+
75+/// OMEGA-DELTA-0036. Where the removal plan is derived from `paths::`.
76+pub const UNINSTALL_PLAN_PATH: &str = "crates/cli/src/uninstall.rs";
77+
78+/// OMEGA-DELTA-0037. Outbound attribution on OpenRouter requests.
79+pub const OPEN_ROUTER_PATH: &str = "crates/open_router/src/open_router.rs";
80+
81+/// OMEGA-DELTA-0039. The installed-proof secret tripwire collector.
82+pub const INSTALLED_TRIPWIRE_PATH: &str = "script/collect-omega-installed-tripwires";
83+
84+/// OMEGA-DELTA-0039. The installed-proof observation collector.
85+pub const INSTALLED_OBSERVATION_PATH: &str = "script/collect-omega-installed-observations";
86+
6887 /// OMEGA-DELTA-0025. The file that declares the measured digest.
6988 pub const MEASURED_DIGEST_PATH: &str = "crates/omega_harness/src/measured.rs";
7089 
@@ -1590,7 +1609,7 @@ pub fn action_doc_lines(source: &str) -> std::collections::BTreeSet<usize> {
15901609         while cursor > 0 {
15911610             cursor -= 1;
15921611             let trimmed = lines[cursor].trim_start();
1593-            if trimmed.starts_with("///") || trimmed.starts_with("//!") {
1612+            if doc_comment_body(lines[cursor]).is_some() {
15941613                 out.insert(cursor + 1);
15951614             } else if !trimmed.starts_with("#[") {
15961615                 break;
@@ -1600,11 +1619,54 @@ pub fn action_doc_lines(source: &str) -> std::collections::BTreeSet<usize> {
16001619     out
16011620 }
16021621 
1622+/// The text of a doc comment, however it was spelled.
1623+///
1624+/// `///` and `//!` are sugar. `#[doc = "..."]` and
1625+/// `#[cfg_attr(<predicate>, doc = "...")]` are the same thing written the long
1626+/// way, and both clap and schemars read them identically. This function did not,
1627+/// which is why every candidate up to `0.2.0-rc14` printed
1628+/// `~/Library/Application Support/Zed` as the data directory from `cli --help`
1629+/// while the gate reported green: the line is a `cfg_attr`, and nothing here
1630+/// had ever looked inside one (omega#89).
16031631 fn doc_comment_body(line: &str) -> Option<&str> {
16041632     let trimmed = line.trim_start();
1605-    trimmed
1633+    if let Some(body) = trimmed
16061634         .strip_prefix("///")
16071635         .or_else(|| trimmed.strip_prefix("//!"))
1636+    {
1637+        return Some(body);
1638+    }
1639+    attribute_doc_body(trimmed)
1640+}
1641+
1642+/// The string in a `doc = "..."` attribute, or the `doc = "..."` line of an
1643+/// attribute written across several lines.
1644+///
1645+/// Anchored at the start of the trimmed line so a Rust `let doc = "..."`
1646+/// binding is not mistaken for documentation.
1647+fn attribute_doc_body(trimmed: &str) -> Option<&str> {
1648+    let rest = if let Some(rest) = trimmed.strip_prefix("#[") {
1649+        let at = rest.find("doc")?;
1650+        &rest[at..]
1651+    } else if trimmed.starts_with("doc") {
1652+        trimmed
1653+    } else {
1654+        return None;
1655+    };
1656+    let rest = rest.strip_prefix("doc")?;
1657+    let rest = rest.trim_start();
1658+    let rest = rest.strip_prefix('=')?.trim_start();
1659+    let rest = rest.strip_prefix('"')?;
1660+    let mut end = 0;
1661+    let bytes = rest.as_bytes();
1662+    while end < bytes.len() {
1663+        match bytes[end] {
1664+            b'\\' => end += 2,
1665+            b'"' => return Some(&rest[..end]),
1666+            _ => end += 1,
1667+        }
1668+    }
1669+    None
16081670 }
16091671 
16101672 /// Every brand-bearing prose literal that can reach a user, plus read counts.
@@ -5322,6 +5384,359 @@ mod tests {
53225384         );
53235385     }
53245386 
5325-}
5387+    /// OMEGA-DELTA-0036. `--uninstall` removes Omega and nothing else.
5388+    ///
5389+    /// The shipped, signed `0.2.0-rc14` advertised `--uninstall` as "Uninstall
5390+    /// Omega from user system" and ran upstream's uninstaller verbatim: it
5391+    /// deleted the other editor's application bundle, its whole
5392+    /// application-support tree, its logs, caches, preferences and saved state,
5393+    /// asked whether to keep *that* product's preferences, printed that *that*
5394+    /// product had been uninstalled — and removed no Omega path at all
5395+    /// (omega#88).
5396+    ///
5397+    /// The end-to-end proof lives in `crates/cli/src/uninstall.rs`, where the
5398+    /// real script runs against a fabricated home holding both an Omega
5399+    /// installation and another product's, and both halves are read back. This
5400+    /// asserts the two structural properties that made the defect possible: the
5401+    /// script names no other product, and it has no path table of its own.
5402+    #[test]
5403+    fn the_uninstall_path_removes_omega_and_names_no_competitor() {
5404+        let policy = brand_policy().expect("brand policy parses");
5405+        let script_path = repository_path(UNINSTALL_SCRIPT_PATH);
5406+        let script = std::fs::read_to_string(&script_path)
5407+            .unwrap_or_else(|error| panic!("cannot read {}: {error}", script_path.display()));
5408+
5409+        let hits = brand_hits(&script, &policy);
5410+        assert!(
5411+            hits.is_empty(),
5412+            "OMEGA-DELTA-0036: {UNINSTALL_SCRIPT_PATH} names {hits:?}. It is \
5413+             embedded in the signed `cli` binary with include_bytes! and it \
5414+             removes whatever it names, so a competitor's directory appearing \
5415+             here is a destructive regression, not a copy regression."
5416+        );
5417+
5418+        for required in ["OMEGA_UNINSTALL_PATHS", "OMEGA_UNINSTALL_PRODUCT"] {
5419+            assert!(
5420+                script.contains(required),
5421+                "OMEGA-DELTA-0036: {UNINSTALL_SCRIPT_PATH} no longer reads \
5422+                 {required} from the caller, so it has a hand-written path \
5423+                 table again. A table disconnected from the code that creates \
5424+                 those directories is exactly how omega#88 shipped."
5425+            );
5426+        }
5427+
5428+        // Every root the plan removes is read from the function that writes it.
5429+        let plan_path = repository_path(UNINSTALL_PLAN_PATH);
5430+        let plan = std::fs::read_to_string(&plan_path)
5431+            .unwrap_or_else(|error| panic!("cannot read {}: {error}", plan_path.display()));
5432+        let constructor = plan
5433+            .split("pub fn from_installed_paths")
5434+            .nth(1)
5435+            .expect("OMEGA-DELTA-0036: from_installed_paths is gone");
5436+        let derived = constructor.matches("paths::").count();
5437+        assert!(
5438+            derived >= 6,
5439+            "OMEGA-DELTA-0036: from_installed_paths makes only {derived} \
5440+             `paths::` calls. Every root has to come from the function that \
5441+             writes it; a literal path here is a second source of truth, and \
5442+             the first one that disagreed cost a user their other editor."
5443+        );
5444+        assert!(
5445+            plan.contains("let Self {"),
5446+            "OMEGA-DELTA-0036: {} no longer destructures UninstallRoots \
5447+             exhaustively in `plan`, so a root can be added to the struct and \
5448+             silently left out of the plan.",
5449+            plan_path.display()
5450+        );
5451+
5452+        // The script itself, run for real, refuses a plan it cannot trust.
5453+        #[allow(
5454+            clippy::disallowed_methods,
5455+            reason = "A gate that only reads the script cannot tell whether it \
5456+                      refuses; this one runs it. There is no async runtime here."
5457+        )]
5458+        let output = std::process::Command::new("sh")
5459+            .arg(&script_path)
5460+            .env("OMEGA_UNINSTALL_PRODUCT", "Omega RC")
5461+            .env("OMEGA_UNINSTALL_PATHS", "")
5462+            .output()
5463+            .expect("run the uninstall script");
5464+        assert!(
5465+            !output.status.success(),
5466+            "OMEGA-DELTA-0036: the uninstall script accepted an empty plan. \
5467+             Refusing is the safe direction; every default this file has ever \
5468+             had belonged to somebody else's product."
5469+        );
5470+    }
53265471 
5472+    /// OMEGA-DELTA-0037. Omega identifies itself to third parties as Omega.
5473+    ///
5474+    /// `X-Title` is displayed to the account holder in their own OpenRouter
5475+    /// dashboard, so it is outbound product identity rather than a wire
5476+    /// contract. Every request Omega made through `0.2.0-rc14` announced a
5477+    /// different editor, and `HTTP-Referer` pointed at that editor's site
5478+    /// (omega#89).
5479+    #[test]
5480+    fn outbound_attribution_names_omega() {
5481+        let path = repository_path(OPEN_ROUTER_PATH);
5482+        let source = std::fs::read_to_string(&path)
5483+            .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
5484+        let titles = source.matches(".header(\"X-Title\"").count();
5485+        assert!(
5486+            titles >= 2,
5487+            "OMEGA-DELTA-0037: {} sets X-Title on {titles} request paths; both \
5488+             the streaming and the non-streaming call carry it.",
5489+            path.display()
5490+        );
5491+        assert!(
5492+            !source.contains("\"Zed Editor\"") && !source.contains("\"https://zed.dev\""),
5493+            "OMEGA-DELTA-0037: {} identifies Omega to OpenRouter as a different \
5494+             product. The user reads this value in their own dashboard.",
5495+            path.display()
5496+        );
5497+        assert_eq!(
5498+            source.matches("app_identity::PRODUCT_NAME").count(),
5499+            titles,
5500+            "OMEGA-DELTA-0037: every X-Title must come from the identity \
5501+             constant, so a rebase cannot restore a literal on one path only."
5502+        );
5503+    }
53275504 
5505+    /// OMEGA-DELTA-0038. The packaged gate opens every executable that ships,
5506+    /// and reads help as clap renders it.
5507+    ///
5508+    /// Both halves of this delta are the same failure. `script/bundle-omega-rc`
5509+    /// copies and signs three binaries into `Contents/MacOS`; every packaged
5510+    /// check in the brand gate opened one of them, so the uninstaller inside
5511+    /// `cli` was outside every check that had ever run (omega#88). And every
5512+    /// prose stream reads *source*, while clap builds the sentence a person
5513+    /// reads at run time — joining doc lines, resolving `cfg_attr`, printing
5514+    /// the flag name beside the text — so `--zed <ZED>`, `Run zed in the
5515+    /// foreground` and a `--user-data-dir` line naming the wrong product's data
5516+    /// directory all shipped under a green gate (omega#89).
5517+    #[test]
5518+    fn the_packaged_gate_opens_every_shipped_executable_and_reads_rendered_help() {
5519+        let verifier =
5520+            std::fs::read_to_string(repository_path(BRAND_VERIFIER_PATH)).expect("brand verifier");
5521+        let bundler =
5522+            std::fs::read_to_string(repository_path(RC_BUNDLE_SCRIPT_PATH)).expect("bundle script");
5523+
5524+        // Derived, not listed: whatever the packaging script writes into
5525+        // Contents/MacOS is what the gate has to be able to open.
5526+        let marker = "${app_path}/Contents/MacOS/";
5527+        let mut shipped: Vec<&str> = bundler
5528+            .match_indices(marker)
5529+            .filter_map(|(at, _)| {
5530+                bundler[at + marker.len()..]
5531+                    .split(['"', '\'', ' ', '\n'])
5532+                    .next()
5533+                    .filter(|name| !name.is_empty() && !name.contains('/'))
5534+            })
5535+            .collect();
5536+        shipped.sort_unstable();
5537+        shipped.dedup();
5538+        assert!(
5539+            shipped.len() >= 3,
5540+            "OMEGA-DELTA-0038: only {shipped:?} were found being written into \
5541+             Contents/MacOS by {RC_BUNDLE_SCRIPT_PATH}; the parser broke and \
5542+             this check is reporting green about nothing."
5543+        );
5544+
5545+        let policy = brand_policy().expect("brand policy parses");
5546+        let floor = policy["packaged"]["minimum_executables"]
5547+            .as_u64()
5548+            .expect("packaged.minimum_executables") as usize;
5549+        assert!(
5550+            floor >= shipped.len(),
5551+            "OMEGA-DELTA-0038: the bundle ships {} executables ({shipped:?}) \
5552+             but the gate's floor is {floor}. A bundle that ships a companion \
5553+             binary and a scan that expects fewer is the shape omega#88 \
5554+             shipped in.",
5555+            shipped.len()
5556+        );
5557+
5558+        assert!(
5559+            verifier.contains("def bundle_executables("),
5560+            "OMEGA-DELTA-0038: {BRAND_VERIFIER_PATH} no longer derives the set \
5561+             of executables it opens by walking the bundle."
5562+        );
5563+        // One accessor names the main binary; nothing else may name a binary.
5564+        // The code form is counted, not the prose form, so the docstring that
5565+        // explains why is not itself a violation.
5566+        let hardcoded = verifier.matches("app / \"Contents/MacOS/").count();
5567+        assert_eq!(
5568+            hardcoded, 1,
5569+            "OMEGA-DELTA-0038: {BRAND_VERIFIER_PATH} names a path under \
5570+             Contents/MacOS {hardcoded} times. Exactly one — the `main_binary` \
5571+             accessor — is allowed; every other check reads the derived \
5572+             inventory, because a check that opens one remembered binary is \
5573+             what let a destructive uninstaller ship twice."
5574+        );
5575+        for required in [
5576+            "def check_rendered_help(",
5577+            "def check_packaged_first_party_agent(",
5578+            "def check_packaged_executable_inventory(",
5579+            "check_rendered_help(APP)",
5580+            "check_packaged_first_party_agent(APP)",
5581+            "check_packaged_executable_inventory(APP)",
5582+        ] {
5583+            assert!(
5584+                verifier.contains(required),
5585+                "OMEGA-DELTA-0038: {BRAND_VERIFIER_PATH} is missing {required:?}. \
5586+                 A check that is defined and never called is the state \
5587+                 first_party_agent.phrases was in for four release candidates."
5588+            );
5589+        }
5590+        assert!(
5591+            verifier.contains("\"--version\"") && verifier.contains("\"--help\""),
5592+            "OMEGA-DELTA-0038: the rendered-output gate no longer runs the \
5593+             shipped binaries with --help and --version, so it is reading \
5594+             source again."
5595+        );
5596+    }
5597+
5598+    /// OMEGA-DELTA-0031, widened. Lowercase `zed`, and doc comments written the
5599+    /// long way.
5600+    ///
5601+    /// Two structural causes behind omega#89, both of which would have survived
5602+    /// a fix that only edited the offending strings.
5603+    ///
5604+    /// `brand.words` held `Zed` alone, and the reason recorded for excluding
5605+    /// the lowercase spelling — that it is a substring of `authorized` — was
5606+    /// false, because the boundary rule already excludes that. The exclusion is
5607+    /// what hid the rendered `--help` of both shipped binaries.
5608+    ///
5609+    /// And the doc scanner matched `///` and `//!` only, so it never read
5610+    /// `#[cfg_attr(target_os = "macos", doc = "…")]` — which is exactly where
5611+    /// `cli --help` took the wrong product's data directory from.
5612+    #[test]
5613+    fn the_doc_scanner_reads_every_spelling_of_a_doc_comment() {
5614+        let policy = brand_policy().expect("brand policy parses");
5615+        let words: Vec<&str> = policy["brand"]["words"]
5616+            .as_array()
5617+            .expect("brand.words")
5618+            .iter()
5619+            .filter_map(serde_json::Value::as_str)
5620+            .collect();
5621+        assert!(
5622+            words.contains(&"zed"),
5623+            "OMEGA-DELTA-0031: brand.words is {words:?}. The lowercase spelling \
5624+             is what `Run zed in the foreground` is written in, and it is the \
5625+             boundary rule — not the case — that keeps `authorized` out."
5626+        );
5627+        for benign in ["authorized", "normalized", "organized", "customized"] {
5628+            assert!(
5629+                brand_hits(benign, &policy).is_empty(),
5630+                "OMEGA-DELTA-0031: {benign:?} is reported as a brand hit. The \
5631+                 boundary rule has to carry the lowercase word, or the gate \
5632+                 cries wolf and gets deleted."
5633+            );
5634+        }
5635+        assert_eq!(
5636+            brand_hits("Run zed in the foreground", &policy),
5637+            vec!["zed".to_owned()],
5638+            "OMEGA-DELTA-0031: the sentence that shipped in two published \
5639+             prereleases is not reported as a hit"
5640+        );
5641+
5642+        assert_eq!(
5643+            doc_comment_body(r#"    #[cfg_attr(target_os = "macos", doc = "`~/Library/x`.")]"#),
5644+            Some("`~/Library/x`."),
5645+            "a cfg_attr doc attribute is a doc comment"
5646+        );
5647+        assert_eq!(
5648+            doc_comment_body(r#"        doc = "the long way""#),
5649+            Some("the long way"),
5650+            "an attribute written across several lines is still a doc comment"
5651+        );
5652+        assert_eq!(
5653+            doc_comment_body(r#"    #[doc = "plain"]"#),
5654+            Some("plain"),
5655+            "a plain doc attribute is a doc comment"
5656+        );
5657+        assert_eq!(doc_comment_body("    /// sugar"), Some(" sugar"));
5658+        assert_eq!(
5659+            doc_comment_body(r#"    let doc = "not documentation";"#),
5660+            None,
5661+            "a Rust binding named `doc` is not documentation"
5662+        );
5663+        assert_eq!(doc_comment_body("    let x = 1;"), None);
5664+    }
5665+
5666+    /// OMEGA-DELTA-0039. The installed-proof harness observes what it records.
5667+    ///
5668+    /// Three checks in the harness could not fail (omega#90). The secret
5669+    /// tripwire made a pipe, wrote a fresh random needle into it, closed both
5670+    /// ends in the same function and then searched the disk for it, so `pass`
5671+    /// was guaranteed by construction. Four of its six surfaces resolved under
5672+    /// the data root, where Omega writes no logs, no telemetry and no crash
5673+    /// reports, and recorded `absent` — which did not fail the receipt. And the
5674+    /// `light-theme` / `dark-theme` observations wrote `content_legible: True`
5675+    /// as a literal, with zero OCR calls and zero pixel comparisons, so a
5676+    /// frozen or blank window passed both.
5677+    ///
5678+    /// This asserts the shape of the corrections. The scripts' own
5679+    /// `--self-test` paths carry the behavioural oracles.
5680+    #[test]
5681+    fn the_installed_proof_harness_observes_what_it_records() {
5682+        let tripwires = std::fs::read_to_string(repository_path(INSTALLED_TRIPWIRE_PATH))
5683+            .expect("tripwire collector");
5684+        assert!(
5685+            !tripwires.contains("secrets.token_hex"),
5686+            "OMEGA-DELTA-0039: {INSTALLED_TRIPWIRE_PATH} mints its own needle \
5687+             again. A needle no other process has ever seen cannot be found, \
5688+             so the scan passes whatever the product does."
5689+        );
5690+        assert!(
5691+            tripwires.contains("--needle-fd"),
5692+            "OMEGA-DELTA-0039: {INSTALLED_TRIPWIRE_PATH} no longer takes the \
5693+             needle from the caller through a descriptor."
5694+        );
5695+        for required in ["Library/Logs", "DiagnosticReports"] {
5696+            assert!(
5697+                tripwires.contains(required),
5698+                "OMEGA-DELTA-0039: {INSTALLED_TRIPWIRE_PATH} no longer scans \
5699+                 {required:?}. On macOS `paths::logs_dir()` is \
5700+                 ~/Library/Logs/<slug> and `paths::crashes_dir()` is \
5701+                 ~/Library/Logs/DiagnosticReports; a surface resolved anywhere \
5702+                 else records `absent` about a directory the product never \
5703+                 writes."
5704+            );
5705+        }
5706+        assert!(
5707+            tripwires.contains("blocked"),
5708+            "OMEGA-DELTA-0039: a surface that cannot be observed has to block. \
5709+             'nothing was found there' and 'nobody looked' must not read the \
5710+             same in a receipt."
5711+        );
5712+
5713+        let observations = std::fs::read_to_string(repository_path(INSTALLED_OBSERVATION_PATH))
5714+            .expect("observation collector");
5715+        let appearance = observations
5716+            .split("# ---- appearance ---")
5717+            .nth(1)
5718+            .expect("OMEGA-DELTA-0039: the appearance block is gone");
5719+        for required in ["ocr_lines(", "differing_pixels("] {
5720+            assert!(
5721+                appearance.contains(required),
5722+                "OMEGA-DELTA-0039: the appearance block does not call \
5723+                 {required:?}. `content_legible` was a Python literal there \
5724+                 through 0.2.0-rc14: a fact about the host's appearance setting \
5725+                 and about a file existing, filed as a fact about the product."
5726+            );
5727+        }
5728+        assert!(
5729+            !appearance.contains("\"content_legible\": True,"),
5730+            "OMEGA-DELTA-0039: `content_legible` is a constant in the \
5731+             appearance block again."
5732+        );
5733+
5734+        let bundler =
5735+            std::fs::read_to_string(repository_path(RC_BUNDLE_SCRIPT_PATH)).expect("bundle script");
5736+        assert!(
5737+            !bundler.contains("\"dirty\": False"),
5738+            "OMEGA-DELTA-0039: the release record states `dirty` as a literal. \
5739+             It is a field that reads like an observation, so it has to be one."
5740+        );
5741+    }
5742+}
diff --git a/crates/open_router/Cargo.toml b/crates/open_router/Cargo.toml
index 2cc5d3d00e..4c86a552ab 100644
--- a/crates/open_router/Cargo.toml
+++ b/crates/open_router/Cargo.toml
@@ -17,6 +17,7 @@ schemars = ["dep:schemars"]
1717 
1818 [dependencies]
1919 anyhow.workspace = true
20+app_identity.workspace = true
2021 futures.workspace = true
2122 http_client.workspace = true
2223 language_model_core.workspace = true
diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs
index 006b1ffc97..936b8e57b8 100644
--- a/crates/open_router/src/open_router.rs
+++ b/crates/open_router/src/open_router.rs
@@ -494,8 +494,12 @@ pub async fn stream_completion(
494494         .uri(uri)
495495         .header("Content-Type", "application/json")
496496         .header("Authorization", format!("Bearer {}", api_key))
497-        .header("HTTP-Referer", "https://zed.dev")
498-        .header("X-Title", "Zed Editor")
497+        // OMEGA-DELTA-0037. OpenRouter shows X-Title to the account holder in
498+        // their own dashboard, so this is Omega identifying itself, by name, to
499+        // a third party and back to the user. Up to 0.2.0-rc14 every request
500+        // Omega made announced it was a different editor.
501+        .header("HTTP-Referer", app_identity::PRODUCT_REPOSITORY_URL)
502+        .header("X-Title", app_identity::PRODUCT_NAME)
499503         .extra_headers(extra_headers)
500504         .body(AsyncBody::from(
501505             serde_json::to_string(&request).map_err(OpenRouterError::SerializeRequest)?,
@@ -587,8 +591,12 @@ pub async fn list_models(
587591         .uri(uri)
588592         .header("Accept", "application/json")
589593         .header("Authorization", format!("Bearer {}", api_key))
590-        .header("HTTP-Referer", "https://zed.dev")
591-        .header("X-Title", "Zed Editor")
594+        // OMEGA-DELTA-0037. OpenRouter shows X-Title to the account holder in
595+        // their own dashboard, so this is Omega identifying itself, by name, to
596+        // a third party and back to the user. Up to 0.2.0-rc14 every request
597+        // Omega made announced it was a different editor.
598+        .header("HTTP-Referer", app_identity::PRODUCT_REPOSITORY_URL)
599+        .header("X-Title", app_identity::PRODUCT_NAME)
592600         .extra_headers(extra_headers)
593601         .body(AsyncBody::default())
594602         .map_err(OpenRouterError::BuildRequestBody)?;
diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs
index 71f61ae5e7..1769f0fb4f 100644
--- a/crates/project/src/project_settings.rs
+++ b/crates/project/src/project_settings.rs
@@ -831,7 +831,7 @@ pub struct SettingsObserver {
831831 /// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
832832 /// (or the equivalent protobuf messages from upstream) and updates local settings
833833 /// and sends notifications downstream.
834-/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
834+/// In ssh mode it also monitors ~/.config/<omega-channel>/{settings, task}.json and sends the content
835835 /// upstream.
836836 impl SettingsObserver {
837837     pub fn init(client: &AnyProtoClient) {
diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs
index 9cd13a0188..eb2fbb9c2d 100644
--- a/crates/remote/src/transport/ssh.rs
+++ b/crates/remote/src/transport/ssh.rs
@@ -618,7 +618,7 @@ impl SshRemoteConnection {
618618         let destination = connection_options.ssh_destination();
619619 
620620         let temp_dir = tempfile::Builder::new()
621-            .prefix("zed-ssh-session")
621+            .prefix("omega-ssh-session")
622622             .tempdir()?;
623623 
624624         // On non-Windows, check if the user already has an active ControlMaster
diff --git a/crates/sandbox/src/linux_bubblewrap.rs b/crates/sandbox/src/linux_bubblewrap.rs
index 554991b497..4070e42c26 100644
--- a/crates/sandbox/src/linux_bubblewrap.rs
+++ b/crates/sandbox/src/linux_bubblewrap.rs
@@ -711,7 +711,7 @@ pub fn run_launcher_if_invoked() {
711711     let invocation = match invocation {
712712         Ok(invocation) => invocation,
713713         Err(error) => {
714-            eprintln!("zed: malformed sandbox launcher invocation: {error:#}");
714+            eprintln!("omega: malformed sandbox launcher invocation: {error:#}");
715715             std::process::exit(127);
716716         }
717717     };
@@ -814,7 +814,7 @@ fn run_launcher(invocation: LauncherInvocation) -> ! {
814814         if let Err(error) = validate_binds(socket, &invocation.validation_paths) {
815815             // Fail closed: a redirected (or unverifiable) writable bind means the
816816             // command must not run at all.
817-            eprintln!("zed: sandbox bind validation failed: {error:#}");
817+            eprintln!("omega: sandbox bind validation failed: {error:#}");
818818             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
819819         }
820820     }
@@ -981,11 +981,11 @@ fn exec_command(program: &OsStr, args: &[OsString]) -> ! {
981981     // Lock down socket/io_uring/ptrace syscalls right before handing control to
982982     // the untrusted command; the filter survives `exec`.
983983     if let Err(error) = install_command_seccomp_filter() {
984-        eprintln!("zed: failed to install sandbox seccomp filter: {error:#}");
984+        eprintln!("omega: failed to install sandbox seccomp filter: {error:#}");
985985         std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
986986     }
987987     let error = Command::new(program).args(args).exec();
988-    eprintln!("zed: failed to exec sandboxed command: {error}");
988+    eprintln!("omega: failed to exec sandboxed command: {error}");
989989     std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
990990 }
991991 
@@ -997,7 +997,7 @@ fn run_bridge(socket_path: PathBuf, port: u16, program: &OsStr, program_args: &[
997997     let listener = match TcpListener::bind((Ipv4Addr::LOCALHOST, port)) {
998998         Ok(listener) => listener,
999999         Err(error) => {
1000-            eprintln!("zed: failed to bind sandbox proxy bridge: {error}");
1000+            eprintln!("omega: failed to bind sandbox proxy bridge: {error}");
10011001             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
10021002         }
10031003     };
@@ -1007,7 +1007,7 @@ fn run_bridge(socket_path: PathBuf, port: u16, program: &OsStr, program_args: &[
10071007         .stack_size(128 * 1024)
10081008         .spawn(move || run_bridge_listener(listener, socket_path))
10091009     {
1010-        eprintln!("zed: failed to spawn sandbox proxy bridge: {error}");
1010+        eprintln!("omega: failed to spawn sandbox proxy bridge: {error}");
10111011         std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
10121012     }
10131013 
@@ -1018,7 +1018,7 @@ fn run_bridge(socket_path: PathBuf, port: u16, program: &OsStr, program_args: &[
10181018     let seccomp_program = match build_command_seccomp_program() {
10191019         Ok(program) => program,
10201020         Err(error) => {
1021-            eprintln!("zed: failed to build sandbox seccomp filter: {error:#}");
1021+            eprintln!("omega: failed to build sandbox seccomp filter: {error:#}");
10221022             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
10231023         }
10241024     };
@@ -1039,7 +1039,7 @@ fn run_bridge(socket_path: PathBuf, port: u16, program: &OsStr, program_args: &[
10391039     let mut child = match command.spawn() {
10401040         Ok(child) => child,
10411041         Err(error) => {
1042-            eprintln!("zed: failed to spawn sandboxed command: {error}");
1042+            eprintln!("omega: failed to spawn sandboxed command: {error}");
10431043             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
10441044         }
10451045     };
@@ -1053,7 +1053,7 @@ fn run_bridge(socket_path: PathBuf, port: u16, program: &OsStr, program_args: &[
10531053             std::process::exit(128 + signal);
10541054         }
10551055         Err(error) => {
1056-            eprintln!("zed: failed to wait for sandboxed command: {error}");
1056+            eprintln!("omega: failed to wait for sandboxed command: {error}");
10571057             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
10581058         }
10591059     }
@@ -1172,7 +1172,7 @@ pub fn run_wsl_helper_if_invoked() {
11721172     let invocation = match invocation {
11731173         Ok(invocation) => invocation,
11741174         Err(error) => {
1175-            eprintln!("zed: malformed WSL sandbox helper invocation: {error:#}");
1175+            eprintln!("omega: malformed WSL sandbox helper invocation: {error:#}");
11761176             std::process::exit(127);
11771177         }
11781178     };
@@ -1252,7 +1252,7 @@ fn run_wsl_helper(invocation: WslHelperInvocation) -> ! {
12521252             Err(error) => {
12531253                 // Fail closed: a writable bind we can't pin can't be verified.
12541254                 eprintln!(
1255-                    "zed: WSL sandbox helper could not open writable bind {}: {error}",
1255+                    "omega: WSL sandbox helper could not open writable bind {}: {error}",
12561256                     path.display()
12571257                 );
12581258                 std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
@@ -1266,7 +1266,7 @@ fn run_wsl_helper(invocation: WslHelperInvocation) -> ! {
12661266         match ValidationFdSender::spawn(fds) {
12671267             Ok(sender) => Some(sender),
12681268             Err(error) => {
1269-                eprintln!("zed: WSL sandbox helper could not start the bind validator: {error}");
1269+                eprintln!("omega: WSL sandbox helper could not start the bind validator: {error}");
12701270                 std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
12711271             }
12721272         }
@@ -1275,7 +1275,7 @@ fn run_wsl_helper(invocation: WslHelperInvocation) -> ! {
12751275     let current_exe = match std::env::current_exe() {
12761276         Ok(path) => path,
12771277         Err(error) => {
1278-            eprintln!("zed: WSL sandbox helper could not resolve its own path: {error}");
1278+            eprintln!("omega: WSL sandbox helper could not resolve its own path: {error}");
12791279             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
12801280         }
12811281     };
@@ -1323,7 +1323,7 @@ fn run_wsl_helper(invocation: WslHelperInvocation) -> ! {
13231323     let mut child = match Command::new(&invocation.bwrap_path).args(&args).spawn() {
13241324         Ok(child) => child,
13251325         Err(error) => {
1326-            eprintln!("zed: WSL sandbox helper could not spawn bwrap: {error}");
1326+            eprintln!("omega: WSL sandbox helper could not spawn bwrap: {error}");
13271327             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
13281328         }
13291329     };
@@ -1341,7 +1341,7 @@ fn run_wsl_helper(invocation: WslHelperInvocation) -> ! {
13411341             std::process::exit(128 + signal);
13421342         }
13431343         Err(error) => {
1344-            eprintln!("zed: WSL sandbox helper failed waiting for bwrap: {error}");
1344+            eprintln!("omega: WSL sandbox helper failed waiting for bwrap: {error}");
13451345             std::process::exit(SANDBOX_SETUP_FAILED_EXIT_CODE);
13461346         }
13471347     }
@@ -1357,10 +1357,10 @@ fn run_bridge_listener(listener: TcpListener, socket_path: PathBuf) {
13571357                     .stack_size(128 * 1024)
13581358                     .spawn(move || forward_bridge_connection(stream, socket_path))
13591359                 {
1360-                    eprintln!("zed: failed to spawn sandbox bridge connection thread: {error}");
1360+                    eprintln!("omega: failed to spawn sandbox bridge connection thread: {error}");
13611361                 }
13621362             }
1363-            Err(error) => eprintln!("zed: sandbox bridge accept failed: {error}"),
1363+            Err(error) => eprintln!("omega: sandbox bridge accept failed: {error}"),
13641364         }
13651365     }
13661366 }
@@ -1370,7 +1370,7 @@ fn forward_bridge_connection(tcp_stream: TcpStream, socket_path: PathBuf) {
13701370         Ok(stream) => stream,
13711371         Err(error) => {
13721372             eprintln!(
1373-                "zed: sandbox bridge failed to connect to proxy socket {}: {error}",
1373+                "omega: sandbox bridge failed to connect to proxy socket {}: {error}",
13741374                 socket_path.display()
13751375             );
13761376             return;
@@ -1383,14 +1383,14 @@ fn copy_bidirectional(tcp_stream: TcpStream, unix_stream: UnixStream) {
13831383     let tcp_read = match tcp_stream.try_clone() {
13841384         Ok(stream) => stream,
13851385         Err(error) => {
1386-            eprintln!("zed: sandbox bridge failed to clone TCP stream: {error}");
1386+            eprintln!("omega: sandbox bridge failed to clone TCP stream: {error}");
13871387             return;
13881388         }
13891389     };
13901390     let unix_read = match unix_stream.try_clone() {
13911391         Ok(stream) => stream,
13921392         Err(error) => {
1393-            eprintln!("zed: sandbox bridge failed to clone Unix stream: {error}");
1393+            eprintln!("omega: sandbox bridge failed to clone Unix stream: {error}");
13941394             return;
13951395         }
13961396     };
@@ -1404,13 +1404,13 @@ fn copy_bidirectional(tcp_stream: TcpStream, unix_stream: UnixStream) {
14041404     {
14051405         Ok(handle) => handle,
14061406         Err(error) => {
1407-            eprintln!("zed: failed to spawn sandbox bridge pump thread: {error}");
1407+            eprintln!("omega: failed to spawn sandbox bridge pump thread: {error}");
14081408             return;
14091409         }
14101410     };
14111411     copy_one_way(unix_read, tcp_write);
14121412     if to_proxy.join().is_err() {
1413-        eprintln!("zed: sandbox bridge pump thread panicked");
1413+        eprintln!("omega: sandbox bridge pump thread panicked");
14141414     }
14151415 }
14161416 
diff --git a/crates/sandbox/src/wsl_sandbox_test_helper.rs b/crates/sandbox/src/wsl_sandbox_test_helper.rs
index 6f46e87f45..00bada5bbe 100644
--- a/crates/sandbox/src/wsl_sandbox_test_helper.rs
+++ b/crates/sandbox/src/wsl_sandbox_test_helper.rs
@@ -175,7 +175,7 @@ mod imp {
175175         // GRANT: writing into a writable bind succeeds and lands on the host.
176176         let writable_file = format!("{writable_wsl}/from-sandbox.txt");
177177         let write_writable = run_in_sandbox(
178-            &format!("echo zed > {}", shell_quote(&writable_file)),
178+            &format!("echo omega > {}", shell_quote(&writable_file)),
179179             &[PathBuf::from(writable_wsl)],
180180             default,
181181         )?;
@@ -189,7 +189,7 @@ mod imp {
189189         // root, and must not leak to the host.
190190         let forbidden_file = format!("{forbidden_wsl}/escaped.txt");
191191         let write_forbidden = run_in_sandbox(
192-            &format!("echo zed > {}", shell_quote(&forbidden_file)),
192+            &format!("echo omega > {}", shell_quote(&forbidden_file)),
193193             &[],
194194             default,
195195         )?;
@@ -228,7 +228,7 @@ mod imp {
228228         // not leak to the WSL distro's real `/tmp`.
229229         let tmp_path = format!("/tmp/zed-sandbox-ephemeral-{pid}");
230230         let write_tmp = run_in_sandbox(
231-            &format!("echo zed > {}", shell_quote(&tmp_path)),
231+            &format!("echo omega > {}", shell_quote(&tmp_path)),
232232             &[],
233233             default,
234234         )?;
@@ -288,7 +288,7 @@ mod imp {
288288         // anywhere, and the write reaches the host.
289289         let escape_file = format!("{forbidden_wsl}/escape-hatch.txt");
290290         let write_escape = run_in_sandbox(
291-            &format!("echo zed > {}", shell_quote(&escape_file)),
291+            &format!("echo omega > {}", shell_quote(&escape_file)),
292292             &[],
293293             fs_write_all,
294294         )?;
@@ -360,7 +360,7 @@ mod imp {
360360 
361361         let write = run_in_sandbox(
362362             &format!(
363-                "echo zed > {}",
363+                "echo omega > {}",
364364                 shell_quote(&format!("{writable_wsl}/from-sandbox.txt"))
365365             ),
366366             std::slice::from_ref(&writable),
@@ -443,13 +443,13 @@ mod imp {
443443         // value; it must not be.
444444         env.insert(
445445             "PATH".to_string(),
446-            "/zed-sentinel-should-not-win".to_string(),
446+            "/omega-sentinel-should-not-win".to_string(),
447447         );
448448         let outcome = drive_sandbox(
449449             "/bin/sh",
450450             &[
451451                 "-c",
452-                "[ \"$ZED_TEST_FORWARDED\" = yes ] && [ \"$PATH\" != /zed-sentinel-should-not-win ]",
452+                "[ \"$ZED_TEST_FORWARDED\" = yes ] && [ \"$PATH\" != /omega-sentinel-should-not-win ]",
453453             ],
454454             &[],
455455             SandboxPermissions::default(),
diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs
index 360435d3cd..7050d58646 100644
--- a/crates/settings_content/src/project.rs
+++ b/crates/settings_content/src/project.rs
@@ -568,8 +568,8 @@ pub struct GitSettings {
568568     /// When the resolved directory is outside the project root, the
569569     /// project's directory name is automatically appended so that
570570     /// sibling repos don't collide. For example, with the default
571-    /// `"../worktrees"` and a project at `~/code/zed`, worktrees are
572-    /// created under `~/code/worktrees/zed/`.
571+    /// `"../worktrees"` and a project at `~/code/omega`, worktrees are
572+    /// created under `~/code/worktrees/omega/`.
573573     ///
574574     /// When the resolved directory is inside the project root, no
575575     /// extra component is added (it's already project-scoped).
diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs
index 033c7085ec..ca1ed5351b 100644
--- a/crates/settings_content/src/settings_content.rs
+++ b/crates/settings_content/src/settings_content.rs
@@ -304,7 +304,7 @@ pub struct SettingsContent {
304304 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
305305 pub struct InstrumentationSettingsContent {
306306     /// Configuration for the performance profiler, accessed via the
307-    /// `zed: open performance profiler` action.
307+    /// `omega: open performance profiler` action.
308308     pub performance_profiler: Option<PerformanceProfilerSettingsContent>,
309309 }
310310 
diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs
index 0b7eff7892..071971001c 100644
--- a/crates/settings_ui/src/page_data.rs
+++ b/crates/settings_ui/src/page_data.rs
@@ -105,7 +105,7 @@ fn developer_page(cx: &App) -> SettingsPage {
105105     items.push(SettingsPageItem::SectionHeader("Instrumentation"));
106106     items.push(SettingsPageItem::SettingItem(SettingItem {
107107         title: "Performance Profiler",
108-        description: "Collect timing data for foreground and background executor tasks so they can be inspected via `zed: open performance profiler`. May lead to increased memory usage.",
108+        description: "Collect timing data for foreground and background executor tasks so they can be inspected via `omega: open performance profiler`. May lead to increased memory usage.",
109109         field: Box::new(SettingField {
110110             organization_override: None,
111111             json_path: Some("instrumentation.performance_profiler.enabled"),
@@ -253,7 +253,7 @@ fn general_page(cx: &App) -> SettingsPage {
253253             }),
254254             SettingsPageItem::SettingItem(SettingItem {
255255                 title: "CLI Default Open Behavior",
256-                description: "How `zed <path>` opens directories when no flag is specified.",
256+                description: "How `omega <path>` opens directories when no flag is specified.",
257257                 field: Box::new(SettingField {
258258                     organization_override: None,
259259                     json_path: Some("cli_default_open_behavior"),
diff --git a/crates/ui/src/components/collab/collab_notification.rs b/crates/ui/src/components/collab/collab_notification.rs
index 3fbf6607c0..dc23acded6 100644
--- a/crates/ui/src/components/collab/collab_notification.rs
+++ b/crates/ui/src/components/collab/collab_notification.rs
@@ -126,7 +126,7 @@ impl Component for CollabNotification {
126126                             "a_very_long_username_that_might_overflow is sharing a project in Omega:",
127127                         ))
128128                         .child(
129-                            Label::new("zed-cloud, zed, edit-prediction-bench, zed.dev")
129+                            Label::new("omega-cloud, omega, edit-prediction-bench, openagents.com")
130130                                 .color(Color::Muted),
131131                         ),
132132                     )
@@ -171,7 +171,7 @@ impl Component for CollabNotification {
171171                             Button::new("decline", "Decline"),
172172                         )
173173                         .child(Label::new(
174-                            "maxbrunsfeld invited you to join the #zed channel",
174+                            "maxbrunsfeld invited you to join the #omega channel",
175175                         )),
176176                     )
177177                     .into_any_element(),
diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs
index ee765a9a27..631ebf0fe8 100644
--- a/crates/util/src/shell_env.rs
+++ b/crates/util/src/shell_env.rs
@@ -213,7 +213,7 @@ async fn capture_windows(
213213     use std::process::Stdio;
214214 
215215     let zed_path =
216-        std::env::current_exe().context("Failed to determine current zed executable path.")?;
216+        std::env::current_exe().context("Failed to determine current Omega executable path.")?;
217217 
218218     let shell_kind = ShellKind::new(shell_path, true);
219219     // Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag
diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs
index e3af948941..b4d1a57472 100644
--- a/crates/util/src/util.rs
+++ b/crates/util/src/util.rs
@@ -314,7 +314,7 @@ pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<S
314314     use anyhow::Context as _;
315315     use paths::PathExt;
316316     let mut zed_path =
317-        std::env::current_exe().context("Failed to determine current zed executable path.")?;
317+        std::env::current_exe().context("Failed to determine current Omega executable path.")?;
318318     if cfg!(target_os = "linux")
319319         && !zed_path.is_file()
320320         && let Some(truncated) = zed_path
@@ -337,10 +337,10 @@ pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result<S
337337 pub fn get_zed_cli_path() -> Result<PathBuf> {
338338     use anyhow::Context as _;
339339     let zed_path =
340-        std::env::current_exe().context("Failed to determine current zed executable path.")?;
340+        std::env::current_exe().context("Failed to determine current Omega executable path.")?;
341341     let parent = zed_path
342342         .parent()
343-        .context("Failed to determine parent directory of zed executable path.")?;
343+        .context("Failed to determine parent directory of the Omega executable path.")?;
344344 
345345     let possible_locations: &[&str] = if cfg!(target_os = "macos") {
346346         // On macOS, the zed executable and zed-cli are inside the app bundle,
@@ -353,7 +353,7 @@ pub fn get_zed_cli_path() -> Result<PathBuf> {
353353         // bin is the standard, ./cli is for the target directory in development builds.
354354         &["../bin/zed", "./cli"]
355355     } else {
356-        anyhow::bail!("unsupported platform for determining zed-cli path");
356+        anyhow::bail!("unsupported platform for determining the Omega CLI path");
357357     };
358358 
359359     possible_locations
@@ -367,7 +367,7 @@ pub fn get_zed_cli_path() -> Result<PathBuf> {
367367         })
368368         .with_context(|| {
369369             format!(
370-                "could not find zed-cli from any of: {}",
370+                "could not find the Omega CLI from any of: {}",
371371                 possible_locations.join(", ")
372372             )
373373         })
diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs
index 32072f1772..80877b4aa5 100644
--- a/crates/workspace/src/pane.rs
+++ b/crates/workspace/src/pane.rs
@@ -3463,7 +3463,7 @@ impl Pane {
34633463             log::warn!(
34643464                 "Pinned tab count ({}) exceeds actual tab count ({}). \
34653465                 This should not happen. If possible, add reproduction steps, \
3466-                in a comment, to https://github.com/zed-industries/zed/issues/33342",
3466+                in a comment, to https://github.com/OpenAgentsInc/omega/issues",
34673467                 self.pinned_tab_count,
34683468                 tab_count
34693469             );
diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs
index 94da97faf5..05ec5e89db 100644
--- a/crates/workspace/src/workspace.rs
+++ b/crates/workspace/src/workspace.rs
@@ -6523,7 +6523,7 @@ impl Workspace {
65236523 
65246524             let Some(task) = task else {
65256525                 anyhow::bail!(
6526-                    "failed to construct view from leader (maybe from a different version of zed?)"
6526+                    "failed to construct view from leader (maybe from a different version of Omega?)"
65276527                 );
65286528             };
65296529 
@@ -10061,7 +10061,7 @@ pub async fn get_any_active_multi_workspace(
1006110061         })
1006210062         .await?;
1006310063     }
10064-    activate_any_workspace_window(&mut cx).context("could not open zed")
10064+    activate_any_workspace_window(&mut cx).context("could not open Omega")
1006510065 }
1006610066 
1006710067 pub fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
@@ -10238,7 +10238,7 @@ pub enum WorkspaceMatching {
1023810238     /// Match paths against existing worktrees including subdirectories, and
1023910239     /// fall back to any existing window if no worktree matched.
1024010240     ///
10241-    /// For example, `zed -a foo/bar` will activate the `bar` workspace if it
10241+    /// For example, `omega -a foo/bar` will activate the `bar` workspace if it
1024210242     /// exists, otherwise it will open a new window with `foo/bar` as the root.
1024310243     MatchSubdirectory,
1024410244 }
diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs
index 3fc3a3bd3b..ec9077e4c3 100644
--- a/crates/zed/src/main.rs
+++ b/crates/zed/src/main.rs
@@ -330,7 +330,7 @@ fn main() {
330330         .unwrap();
331331 
332332     log::info!(
333-        "========== starting zed version {}, sha {} ==========",
333+        "========== starting omega version {}, sha {} ==========",
334334         app_version,
335335         app_commit_sha
336336             .as_ref()
@@ -380,7 +380,7 @@ fn main() {
380380         }
381381     };
382382     if failed_single_instance_check {
383-        println!("zed is already running");
383+        println!("Omega is already running");
384384         return;
385385     }
386386 
@@ -1723,7 +1723,7 @@ struct Args {
17231723     #[arg(long)]
17241724     dev_container: bool,
17251725 
1726-    /// Instructs zed to run as a dev server on this machine. (not implemented)
1726+    /// Instructs Omega to run as a dev server on this machine. (not implemented)
17271727     #[arg(long)]
17281728     dev_server_token: Option<String>,
17291729 
@@ -1740,7 +1740,7 @@ struct Args {
17401740     #[arg(long, hide = true)]
17411741     crash_handler: Option<PathBuf>,
17421742 
1743-    /// Run zed in the foreground, only used on Windows, to match the behavior on macOS.
1743+    /// Run Omega in the foreground, only used on Windows, to match the behavior on macOS.
17441744     #[arg(long)]
17451745     #[cfg(target_os = "windows")]
17461746     #[arg(hide = true)]
diff --git a/docs/src/development/omega-rc-installed-proof.md b/docs/src/development/omega-rc-installed-proof.md
index 44a5119783..966ce3fed2 100644
--- a/docs/src/development/omega-rc-installed-proof.md
+++ b/docs/src/development/omega-rc-installed-proof.md
@@ -614,6 +614,60 @@ crash or diagnostics surface is recorded as `absent`, not
614614 silently treated as scanned. The independent reviewer must decide whether each
615615 absence is expected for the exercised journey before admitting the attestation.
616616 
617+### Collecting the receipt in one command
618+
619+`script/collect-omega-installed-tripwires` produces the same receipt without
620+the operator naming six paths. It resolves them from `crates/paths/src/paths.rs`
621+instead:
622+
623+| surface | where it is read |
624+| --- | --- |
625+| logs | `paths::logs_dir()` — `~/Library/Logs/omega-rc`, less the telemetry log |
626+| telemetry | `~/Library/Logs/omega-rc/telemetry.log` |
627+| diagnostics | `paths::database_dir()` and `paths::hang_traces_dir()` |
628+| crashes | `paths::crashes_dir()` — `~/Library/Logs/DiagnosticReports` |
629+| clipboard | the general `NSPasteboard`, through PyObjC or `pbpaste` |
630+| accessibility | the running candidate's `AXUIElement` tree |
631+
632+```sh
633+script/collect-omega-installed-tripwires \
634+  --app /Applications/Omega.app \
635+  --candidate-evidence target/omega-identity-evidence/candidate-evidence.json \
636+  --output "$OMEGA_PRIVATE_PROOF_DIR/installed-secret-tripwires.json" \
637+  --needle-fd 3 3<"$OMEGA_PRIVATE_PROOF_DIR/disposable-canary"
638+```
639+
640+Three rules make the receipt worth reading.
641+
642+`--needle-fd` is required. The canary is the one the candidate was actually
643+given during the journey, read from a descriptor the caller opened on the
644+mode-0600 file, and the collector will not generate one of its own. A secret no
645+other process has ever seen cannot have been leaked by anything, so a search
646+for it passes by construction — which is what an earlier version of this
647+command did, and what made its `pass` worth nothing.
648+
649+The scan is proved able to fire before it is trusted. The needle is planted in
650+a private file and the same scanner is pointed at it; a scanner that does not
651+come back with that match ends the run at exit 2 with no receipt written,
652+rather than reporting that nothing was found.
653+
654+A surface that could not be observed records `blocked` and the run exits 3.
655+That includes a logs or diagnostics root that does not exist — which means the
656+run looked somewhere the product does not write — a pasteboard that holds
657+flavours the read cannot see, and an accessibility tree that could not be read
658+because no candidate is running or because the terminal has not been granted
659+Accessibility in System Settings. `validate_installed_tripwires` refuses any
660+receipt carrying a `blocked` surface, because "nothing was found there" and
661+"nobody looked" must not read the same. The clipboard and accessibility
662+captures are written mode-0600 into a private temporary directory and deleted
663+when the run ends.
664+
665+`script/collect-omega-installed-tripwires --self-test` checks the scanner on a
666+planted needle, checks that the live-fire control catches a scanner that never
667+matches, checks the canary length bounds, and checks that the surfaces still
668+resolve to `paths::logs_dir()` and `paths::crashes_dir()` rather than under the
669+application support root.
670+
617671 ## Installed observations
618672 
619673 `script/collect-omega-installed-observations` performs the eleven installed
@@ -641,6 +695,23 @@ script/collect-omega-installed-observations \
641695 The parity probe is a separate command because it starts upstream Zed, and
642696 Zed's own writes must fall outside the window the isolation check measures.
643697 
698+`light-theme` and `dark-theme` take their facts from the captures, not from the
699+host. The host appearance is their precondition, exactly as the increased-
700+contrast flag is the precondition of `high-contrast`: the window has to be
701+legible in each appearance — twelve lines recognised at confidence 0.5 off the
702+rendered capture — and the light and dark captures have to differ by at least
703+50,000 pixels. A frozen, blank, or appearance-ignoring window returns the same
704+image twice and blocks both checks. `content_legible` was a hardcoded literal
705+until omega#90, so a window that never repainted passed both.
706+
707+`script/collect-omega-installed-observations --self-test` exercises that
708+decision offline, without driving the host or the candidate: it requires an
709+unreadable capture, a capture with too little legible text, an uncomparable
710+pair, and two captures of the same picture each to fail, and it asserts that
711+the appearance block still calls `ocr_lines`, `confident_lines`, and
712+`differing_pixels`, so the decision cannot quietly stop being wired to the
713+pixels.
714+
644715 Every keystroke the collector sends brings the candidate to the front first and
645716 refuses if it is not there. macOS routes a synthesized key to whichever
646717 application is frontmost, not to the process the script names, so an unchecked
diff --git a/script/bundle-omega-rc b/script/bundle-omega-rc
index a359c92a0d..3585a1a9ad 100755
--- a/script/bundle-omega-rc
+++ b/script/bundle-omega-rc
@@ -870,8 +870,27 @@ write_release_record() {
870870   python3 <<'PY'
871871 import json
872872 import os
873+import subprocess
873874 from pathlib import Path
874875 
876+
877+def worktree_is_dirty() -> bool:
878+    """Ask git whether the tree this candidate was built from was recorded.
879+
880+    `require_clean_checkout` refuses to start a build from a dirty tree, so
881+    this normally answers false — but it has to be *read*, not asserted. It was
882+    a Python literal, which made a provenance field that cannot disagree with
883+    the build no matter what the tree held.
884+    """
885+    status = subprocess.run(
886+        ["git", "status", "--porcelain"],
887+        capture_output=True,
888+        text=True,
889+        check=True,
890+    )
891+    return bool(status.stdout.strip())
892+
893+
875894 icons = json.loads(os.environ["OMEGA_RC_ICON_JSON"])
876895 packaged_bindings = json.loads(os.environ["OMEGA_RC_PACKAGED_BINDINGS_JSON"])
877896 upstream = os.environ.get("OMEGA_RC_UPSTREAM_COMMIT") or None
@@ -913,7 +932,7 @@ record = {
913932     "source": {
914933         "commit": os.environ["OMEGA_RC_COMMIT"],
915934         "upstream_commit": upstream,
916-        "dirty": False,
935+        "dirty": worktree_is_dirty(),
917936         "repository": "https://github.com/OpenAgentsInc/omega",
918937     },
919938     "toolchains": {
diff --git a/script/collect-omega-installed-observations b/script/collect-omega-installed-observations
index e9ff5f3004..270c1771a1 100755
--- a/script/collect-omega-installed-observations
+++ b/script/collect-omega-installed-observations
@@ -59,6 +59,8 @@ from omega_identity_evidence import (  # noqa: E402
5959     INSTALLED_OBSERVATIONS_SCHEMA,
6060     MANUAL_JOURNEY_CHECKS,
6161     WAIVABLE_CHECKS,
62+    IdentityEvidenceError,
63+    validate_observation_facts,
6264 )
6365 
6466 OBSERVATIONS_SCHEMA = INSTALLED_OBSERVATIONS_SCHEMA
@@ -126,6 +128,23 @@ FOCUS_RETURN_RATIO = 10
126128 CONTRAST_LEGIBLE_LINES = 12
127129 CONTRAST_LEGIBLE_CONFIDENCE = 0.5
128130 
131+#: How many lines of text have to be recognised off the rendered window in an
132+#: appearance before that appearance counts as legible, and the confidence
133+#: below which a recognised line is not counted. `content_legible` is a claim
134+#: about what a person can read in that appearance, so it is read off the
135+#: pixels exactly the way the increased-contrast claim is. It used to be a
136+#: literal `True`.
137+APPEARANCE_LEGIBLE_LINES = 12
138+APPEARANCE_LEGIBLE_CONFIDENCE = 0.5
139+
140+#: How many pixels the light capture and the dark capture have to differ by
141+#: before the pair counts as two appearances rather than one. A window that
142+#: ignored the host, and a frozen or blank one, return nearly the same image
143+#: twice and cannot clear this; a theme change repaints the background of every
144+#: pane and clears it by orders of magnitude. Without this the two checks can
145+#: be satisfied by a single unchanging picture taken twice.
146+APPEARANCE_DIFFERENCE_PIXELS = 50_000
147+
129148 #: How long the reduced-motion journey waits after each input before calling a
130149 #: state settled, and by how much the frame taken as soon as the harness could
131150 #: take one may differ from the settled frame. Stated openly: a state that is
@@ -321,6 +340,62 @@ def ocr_lines(path: Path) -> list[dict[str, Any]] | None:
321340         return None
322341 
323342 
343+def confident_lines(
344+    lines: list[dict[str, Any]] | None, minimum_confidence: float
345+) -> list[dict[str, Any]] | None:
346+    """The recognised lines a reader would actually be able to read.
347+
348+    `None` in means the read could not be made and `None` comes back out; a
349+    read that could not be made never becomes a pass.
350+    """
351+    if lines is None:
352+        return None
353+    return [
354+        line
355+        for line in lines
356+        if isinstance(line.get("text"), str)
357+        and line["text"].strip()
358+        and float(line.get("confidence", 0.0)) >= minimum_confidence
359+    ]
360+
361+
362+def appearance_observed(
363+    name: str,
364+    lines: list[dict[str, Any]] | None,
365+    difference_pixels: int | None,
366+) -> tuple[bool, str]:
367+    """Whether one appearance was observed on the product, and why not if not.
368+
369+    Two facts, both read off the pixels. The window has to be legible in this
370+    appearance, and the two appearances have to be different pictures. The host
371+    being in the appearance is the precondition of the observation and is never
372+    the observation: a frozen, blank, or appearance-ignoring window satisfies
373+    the host and fails here.
374+    """
375+    if lines is None:
376+        return False, (
377+            f"{name}-theme: the rendered window could not be read, so its "
378+            "legibility in this appearance was not observed"
379+        )
380+    if len(lines) < APPEARANCE_LEGIBLE_LINES:
381+        return False, (
382+            f"{name}-theme: legible_lines={len(lines)} "
383+            f"(minimum {APPEARANCE_LEGIBLE_LINES})"
384+        )
385+    if difference_pixels is None:
386+        return False, (
387+            f"{name}-theme: the light and dark captures could not be compared, "
388+            "so the window was not shown to follow the host appearance"
389+        )
390+    if difference_pixels < APPEARANCE_DIFFERENCE_PIXELS:
391+        return False, (
392+            f"{name}-theme: the light and dark captures differ by "
393+            f"{difference_pixels} pixels (minimum {APPEARANCE_DIFFERENCE_PIXELS}), "
394+            "so the window did not follow the host appearance"
395+        )
396+    return True, ""
397+
398+
324399 def first_position(lines: list[dict[str, Any]], phrase: str) -> float | None:
325400     """How far down the window a phrase first appears, or `None` if absent."""
326401     needle = phrase.casefold()
@@ -756,12 +831,155 @@ def load_upstream_parity(path: Path | None) -> tuple[dict[str, str] | None, str]
756831     return parity, ""  # type: ignore[return-value]
757832 
758833 
834+def self_test() -> int:
835+    """The appearance pair has to be able to fail, or every run records it green.
836+
837+    Nothing here drives the host or the candidate. It exercises the decision the
838+    appearance block makes from a capture, which is the part that was missing:
839+    the old block wrote `content_legible: True` as a literal and never read a
840+    pixel or a character, so a window that never repainted passed both checks.
841+    """
842+    import tempfile
843+
844+    failures: list[str] = []
845+
846+    def expect(condition: bool, description: str) -> None:
847+        if not condition:
848+            failures.append(description)
849+
850+    # A read that could not be made stays unmade, and lines a reader could not
851+    # read are not counted as read.
852+    expect(confident_lines(None, 0.5) is None, "an unreadable capture became a line list")
853+    filtered = confident_lines(
854+        [
855+            {"text": "legible", "confidence": 0.9},
856+            {"text": "   ", "confidence": 0.9},
857+            {"text": "smudged", "confidence": 0.2},
858+            {"text": None, "confidence": 0.9},
859+        ],
860+        0.5,
861+    )
862+    expect(filtered is not None and len(filtered) == 1, "confident_lines counted the wrong lines")
863+
864+    readable = [
865+        {"text": f"line {index}", "confidence": 0.9}
866+        for index in range(APPEARANCE_LEGIBLE_LINES)
867+    ]
868+    thin = readable[:-1]
869+    cases = [
870+        ("a legible pair of different pictures", readable, APPEARANCE_DIFFERENCE_PIXELS, True),
871+        ("a capture nothing could read", None, APPEARANCE_DIFFERENCE_PIXELS, False),
872+        ("a capture with too little legible text", thin, APPEARANCE_DIFFERENCE_PIXELS, False),
873+        ("a pair that could not be compared", readable, None, False),
874+        ("two captures of the same picture", readable, 0, False),
875+        (
876+            "a pair just under the stated difference",
877+            readable,
878+            APPEARANCE_DIFFERENCE_PIXELS - 1,
879+            False,
880+        ),
881+    ]
882+    for description, lines, difference, expected in cases:
883+        observed, reason = appearance_observed("light", lines, difference)
884+        expect(observed is expected, f"{description} was judged {observed}")
885+        expect(expected or bool(reason), f"{description} failed without saying why")
886+
887+    # The pixel comparison the pair rests on, run over real images.
888+    with tempfile.TemporaryDirectory() as directory:
889+        workspace = Path(directory)
890+        try:
891+            from PIL import Image
892+        except ImportError:
893+            expect(
894+                appearance_observed("light", readable, differing_pixels(workspace, workspace))[0]
895+                is False,
896+                "an uncomparable pair passed",
897+            )
898+        else:
899+            light = workspace / "light.png"
900+            dark = workspace / "dark.png"
901+            same = workspace / "same.png"
902+            Image.new("RGB", (400, 400), (255, 255, 255)).save(light)
903+            Image.new("RGB", (400, 400), (255, 255, 255)).save(same)
904+            Image.new("RGB", (400, 400), (16, 16, 16)).save(dark)
905+            unchanged = differing_pixels(light, same)
906+            changed = differing_pixels(light, dark)
907+            expect(unchanged == 0, f"two identical captures differed by {unchanged} pixels")
908+            expect(
909+                changed is not None and changed >= APPEARANCE_DIFFERENCE_PIXELS,
910+                f"a full repaint counted only {changed} differing pixels",
911+            )
912+            expect(
913+                appearance_observed("dark", readable, unchanged)[0] is False,
914+                "an unchanged pair of captures passed",
915+            )
916+            expect(
917+                appearance_observed("dark", readable, changed)[0] is True,
918+                "a repainted pair of captures did not pass",
919+            )
920+
921+    # The block that records the pair has to take its facts from the capture.
922+    # This is the exact regression that stood: `content_legible: True` beside a
923+    # screenshot, with zero `ocr_lines()` calls and zero `differing_pixels()`
924+    # calls in the whole block. A decision function nothing calls is not a
925+    # check, so the wiring is asserted as well as the decision.
926+    source = Path(__file__).resolve().read_text(encoding="utf-8")
927+    # The marker is assembled so that this line is not itself the thing it
928+    # looks for. A search that can find its own source would still pass with
929+    # the block deleted.
930+    marker = "# ---- appearanc" + "e ------"
931+    expect(source.count(marker) == 1, "the appearance block header is not where it was")
932+    start = source.index(marker)
933+    block = source[start : source.index("if blocked:", start)]
934+    for call in (
935+        "ocr_lines(",
936+        "differing_pixels(",
937+        "confident_lines(",
938+        "appearance_observed(",
939+    ):
940+        expect(call in block, f"the appearance block no longer calls {call}")
941+
942+    # The facts this check records are the ones the receipt validator demands.
943+    # A drift here would block the proof at validation instead of here.
944+    for name in ("light", "dark"):
945+        try:
946+            validate_observation_facts(
947+                f"{name}-theme", {"appearance": name, "content_legible": True}
948+            )
949+        except IdentityEvidenceError as error:
950+            failures.append(f"{name}-theme facts are not the validated shape: {error}")
951+
952+    if failures:
953+        for failure in failures:
954+            print(f"self-test failure: {failure}", file=sys.stderr)
955+        return 1
956+    print(
957+        json.dumps(
958+            {
959+                "schema": OBSERVATIONS_SCHEMA,
960+                "status": "self-test-passed",
961+                "appearance_legible_lines": APPEARANCE_LEGIBLE_LINES,
962+                "appearance_difference_pixels": APPEARANCE_DIFFERENCE_PIXELS,
963+            }
964+        )
965+    )
966+    return 0
967+
968+
759969 def main() -> int:
760970     parser = argparse.ArgumentParser()
761971     parser.add_argument("--app", type=Path, default=Path("/Applications/Omega.app"))
762-    parser.add_argument("--candidate-evidence", type=Path, required=True)
763-    parser.add_argument("--evidence-root", type=Path, required=True)
764-    parser.add_argument("--zed-before", required=True, help="digest captured before install")
972+    parser.add_argument("--candidate-evidence", type=Path)
973+    parser.add_argument("--evidence-root", type=Path)
974+    parser.add_argument("--zed-before", help="digest captured before install")
975+    parser.add_argument(
976+        "--self-test",
977+        action="store_true",
978+        help=(
979+            "exercise the decisions this collector makes from a capture, "
980+            "without driving the host or the candidate"
981+        ),
982+    )
765983     parser.add_argument(
766984         "--config-name",
767985         default="omega-rc",
@@ -832,9 +1050,24 @@ def main() -> int:
8321050         default=None,
8331051         help="name of the emulated profile these observations were performed in",
8341052     )
835-    parser.add_argument("--output", type=Path, required=True)
1053+    parser.add_argument("--output", type=Path)
8361054     args = parser.parse_args()
8371055 
1056+    if args.self_test:
1057+        return self_test()
1058+
1059+    # These are required for a collection run. They are not declared required
1060+    # on the parser only so that `--self-test` can be run without inventing a
1061+    # candidate; a run that is missing one still refuses here.
1062+    for flag, value in (
1063+        ("--candidate-evidence", args.candidate_evidence),
1064+        ("--evidence-root", args.evidence_root),
1065+        ("--zed-before", args.zed_before),
1066+        ("--output", args.output),
1067+    ):
1068+        if value is None:
1069+            parser.error(f"{flag} is required")
1070+
8381071     global PROCESS_PID, WINDOW_ID, OCR_TOOL, WINDOW_LIST_TOOL, PERFORMED_BY, PROFILE
8391072     PROCESS_PID = args.pid
8401073     WINDOW_ID = args.window_id
@@ -1774,11 +2007,23 @@ def main() -> int:
17742007     # Both appearances are driven and observed. Recording only whichever the
17752008     # host happened to be in leaves half of a pair the release has to hold in
17762009     # both states.
2010+    #
2011+    # The host appearance is the *precondition* of these two checks, exactly as
2012+    # the increased-contrast flag is the precondition of that one. It is not the
2013+    # observation. `content_legible` used to be a literal `True` written beside
2014+    # a screenshot that nothing read, so a frozen, blank, or
2015+    # appearance-ignoring window passed both `light-theme` and `dark-theme`.
2016+    # Legibility is now recognised off each rendered capture at a stated line
2017+    # count and confidence, and the light and dark captures have to be
2018+    # different pictures by a stated number of pixels. One unchanging image
2019+    # taken twice satisfies neither.
17772020     initial_dark = appearance_is_dark()
17782021     if initial_dark is None:
17792022         blocked.append("light-theme/dark-theme: cannot read system appearance")
17802023     else:
17812024         captured: dict[str, dict[str, str]] = {}
2025+        capture_paths: dict[str, Path] = {}
2026+        legible: dict[str, list[dict[str, Any]] | None] = {}
17822027         for name, dark in (("light", False), ("dark", True)):
17832028             if not set_appearance_dark(dark):
17842029                 blocked.append(f"{name}-theme: the host refused the {name} appearance")
@@ -1788,10 +2033,24 @@ def main() -> int:
17882033                 blocked.append(f"{name}-theme: the window could not be captured")
17892034                 continue
17902035             captured[name] = reference
2036+            capture_paths[name] = root / reference["path"]
2037+            legible[name] = confident_lines(
2038+                ocr_lines(capture_paths[name]), APPEARANCE_LEGIBLE_CONFIDENCE
2039+            )
17912040         set_appearance_dark(initial_dark)
2041+        appearance_difference = (
2042+            differing_pixels(capture_paths["light"], capture_paths["dark"])
2043+            if {"light", "dark"} <= set(capture_paths)
2044+            else None
2045+        )
17922046         for name in ("light", "dark"):
17932047             if name not in captured:
17942048                 continue
2049+            lines = legible.get(name)
2050+            observed, reason = appearance_observed(name, lines, appearance_difference)
2051+            if not observed or lines is None:
2052+                blocked.append(reason)
2053+                continue
17952054             record_performed(
17962055                 "accessibility",
17972056                 f"{name}-theme",
@@ -1803,9 +2062,30 @@ def main() -> int:
18032062                         {
18042063                             "system_dark_mode": name == "dark",
18052064                             "restored_to": "dark" if initial_dark else "light",
2065+                            "legible_line_count": len(lines),
2066+                            "legible_line_minimum": APPEARANCE_LEGIBLE_LINES,
2067+                            "legible_confidence_minimum": APPEARANCE_LEGIBLE_CONFIDENCE,
2068+                            "legible_lines": [line["text"] for line in lines][:40],
2069+                            "appearance_difference_pixels": appearance_difference,
2070+                            "appearance_difference_minimum": APPEARANCE_DIFFERENCE_PIXELS,
18062071                             "legibility_basis": (
2072+                                "Optical character recognition over a capture of the "
2073+                                "candidate's own window taken while the host was in "
2074+                                "this appearance. Lines below the stated confidence "
2075+                                "are not counted."
2076+                            ),
2077+                            "appearance_difference_basis": (
2078+                                "The light capture and the dark capture were compared "
2079+                                "pixel by pixel. A window that ignored the host "
2080+                                "appearance, and a frozen or blank one, return the "
2081+                                "same image twice and do not clear the stated "
2082+                                "minimum."
2083+                            ),
2084+                            "method": (
18072085                                 "The host was driven into this appearance and the "
1808-                                "candidate window was captured in it."
2086+                                "candidate window was captured and read in it. The "
2087+                                "host appearance is the precondition of this "
2088+                                "observation, not the observation."
18092089                             ),
18102090                         },
18112091                     ),
diff --git a/script/collect-omega-installed-tripwires b/script/collect-omega-installed-tripwires
index 21ee8cf016..d0ac18c493 100755
--- a/script/collect-omega-installed-tripwires
+++ b/script/collect-omega-installed-tripwires
@@ -1,25 +1,53 @@
11 #!/usr/bin/env python3
2-"""Prove an installed Omega candidate never wrote a secret to a public surface.
2+"""Search the surfaces an installed Omega writes for a marked secret.
33 
44 The installed proof requires a tripwire receipt and nothing produced one, so
5-that input was permanently blocked. This produces it.
6-
7-The method is a marked-secret search. A random needle is handed to the
8-candidate through a file descriptor the operating system protects, the six
9-surfaces a secret could plausibly escape onto are scanned for it, and the
10-receipt records that no surface matched. The needle's value is never written
11-to the receipt or to any log — recording the secret you are proving was not
12-leaked would defeat the exercise, so only its length is kept.
13-
14-An absent surface is recorded as `absent` rather than `pass`. "The directory
15-does not exist" and "the directory exists and is clean" are different claims,
16-and collapsing them would let a missing data root read as a clean one.
5+that input was permanently blocked. This produces it. The method is a
6+marked-secret search: the disposable canary the candidate was given during the
7+journey is searched for on every surface a secret could plausibly escape onto,
8+and the receipt records which surfaces were read and that none of them carried
9+it. The needle's value is never written to the receipt or to any log — recording
10+the secret you are proving was not leaked would defeat the exercise — so only
11+its length is kept.
12+
13+Three properties keep this receipt from being a formality.
14+
15+**The needle is not this script's to invent.** It is read from a descriptor the
16+caller opened on the canary file, and a run without `--needle-fd` refuses rather
17+than minting a fresh secret no other process has ever seen. An earlier version
18+generated its own needle, wrote it into a pipe, closed both ends of that pipe in
19+the same function, and then searched the disk for it. Nothing could ever have
20+written that value anywhere, so `pass` was guaranteed by construction whatever
21+the product did. A receipt that cannot fail is worse than no receipt.
22+
23+**The scan is proved able to fire on this run.** Before any surface is read the
24+needle is planted in a private file and the same scanner is pointed at it. If
25+that file does not come back as a match the run blocks, so a scanner that has
26+been broken — by an edit, by an unreadable temporary directory, by a needle that
27+arrived empty — cannot report that nothing was found.
28+
29+**Every surface is read where Omega actually writes, through the interface that
30+surface actually has.** `crates/paths/src/paths.rs` puts `logs_dir()` and the
31+telemetry log under `~/Library/Logs/<storage slug>` and `crashes_dir()` under
32+`~/Library/Logs/DiagnosticReports`; none of those is under the application
33+support root an earlier version scanned, so four of the six surfaces were being
34+searched for in directories the product never creates. The clipboard is a
35+pasteboard and the accessibility surface is a tree; neither is ever a file in a
36+data directory, so each is captured through its own API into a private file that
37+is deleted when the run ends.
38+
39+A surface that exists and does not carry the needle records `pass`. A surface
40+whose location is right and whose file the product has not written records
41+`absent`. A surface that could not be observed at all records `blocked`, which
42+the receipt validator refuses — "nothing was found there" and "nobody looked"
43+must not read the same, and only one of them may support a release.
1744 
1845 Usage:
1946     collect-omega-installed-tripwires \\
2047         --app /Applications/Omega.app \\
2148         --candidate-evidence <path> \\
22-        --output <path>
49+        --output <path> \\
50+        --needle-fd 3 3<"$OMEGA_PRIVATE_PROOF_DIR/disposable-canary"
2351 """
2452 
2553 from __future__ import annotations
@@ -28,33 +56,41 @@ import argparse
2856 import hashlib
2957 import json
3058 import os
31-import secrets
59+import platform
60+import shutil
3261 import stat
62+import subprocess
3363 import sys
64+import tempfile
65+from collections.abc import Callable
3466 from datetime import datetime, timezone
3567 from pathlib import Path
3668 from typing import Any
3769 
3870 TRIPWIRE_SCHEMA = "openagents.omega.installed-secret-tripwires.v1"
3971 
40-#: Needle length in bytes before hex expansion. Inside the 16..4096 bound the
41-#: validator enforces, and long enough that a chance collision is impossible.
42-NEEDLE_BYTES = 32
43-
44-#: Every surface a secret could plausibly reach. Names are fixed by the
45-#: validator; the paths are where an installed Omega actually writes.
46-SURFACE_ROOTS: dict[str, list[str]] = {
47-    "logs": ["logs", "hang_traces"],
48-    "telemetry": ["telemetry"],
49-    "clipboard": ["clipboard"],
50-    "accessibility": ["accessibility"],
51-    "diagnostics": ["diagnostics", "db"],
52-    "crashes": ["crashes", "crash_reports"],
53-}
72+#: Bounds the receipt validator enforces on the needle, mirrored here so a
73+#: canary that could not support a receipt is refused before anything is
74+#: scanned rather than after.
75+MINIMUM_NEEDLE_BYTES = 16
76+MAXIMUM_NEEDLE_BYTES = 4096
77+
78+#: `AppChannel::storage_slug` for the RC channel, and the display name
79+#: `app_name()` uses for the data root. The two differ, and using either where
80+#: the other belongs is what put four surfaces in directories that do not
81+#: exist.
82+STORAGE_SLUG = "omega-rc"
83+DISPLAY_NAME = "Omega RC"
5484 
5585 #: Refuse to read anything enormous; a scan that hangs is a scan nobody runs.
5686 MAX_FILE_BYTES = 64 * 1024 * 1024
5787 
88+#: Surfaces whose file the product writes only under a condition the journey
89+#: may not have met. A missing telemetry log or an empty crash directory is a
90+#: fact about the run, not a gap in the search, and records `absent`. Every
91+#: other surface must be observed or blocked.
92+OPTIONAL_SURFACES = frozenset({"telemetry", "crashes"})
93+
5894 
5995 class TripwireError(Exception):
6096     pass
@@ -64,133 +100,612 @@ def sha256_hex(data: bytes) -> str:
64100     return hashlib.sha256(data).hexdigest()
65101 
66102 
67-def data_root() -> Path:
68-    """The installed RC's data root."""
69-    return Path.home() / "Library" / "Application Support" / "Omega RC"
103+def data_root(override: Path | None = None) -> Path:
104+    """`paths::data_dir()` on macOS: the application support root."""
105+    return override or Path.home() / "Library" / "Application Support" / DISPLAY_NAME
106+
107+
108+def logs_root(override: Path | None = None) -> Path:
109+    """`paths::logs_dir()` on macOS.
110+
111+    Not the data root. `paths.rs` sends macOS logs to `~/Library/Logs/<slug>`,
112+    and the live log file and the telemetry log are both there.
113+    """
114+    return override or Path.home() / "Library" / "Logs" / STORAGE_SLUG
115+
116+
117+def crashes_root(override: Path | None = None) -> Path:
118+    """`paths::crashes_dir()` on macOS."""
119+    return override or Path.home() / "Library" / "Logs" / "DiagnosticReports"
120+
121+
122+def telemetry_file(logs: Path) -> Path:
123+    """`client::telemetry` writes `telemetry.log` inside the logs directory."""
124+    return logs / "telemetry.log"
125+
126+
127+def filesystem_surfaces(
128+    data: Path, logs: Path, crashes: Path
129+) -> dict[str, dict[str, Any]]:
130+    """Where each file-backed surface actually lives.
131+
132+    `anchor` is the directory that must exist for the search to have happened
133+    in the right place at all. An absent anchor is not an empty surface: it
134+    means this run looked somewhere the product does not write, which is the
135+    defect this table was rewritten to remove, so it blocks.
136+    """
137+    return {
138+        "logs": {
139+            "anchor": logs,
140+            "roots": [logs],
141+            # The telemetry log lives in the same directory and is its own
142+            # surface below. Excluding it here keeps the two counts disjoint so
143+            # neither borrows the other's coverage.
144+            "exclude": frozenset({telemetry_file(logs)}),
145+        },
146+        "telemetry": {
147+            "anchor": logs,
148+            "roots": [telemetry_file(logs)],
149+            "exclude": frozenset(),
150+        },
151+        "diagnostics": {
152+            "anchor": data,
153+            "roots": [data / "db", data / "hang_traces"],
154+            "exclude": frozenset(),
155+        },
156+        "crashes": {
157+            "anchor": crashes.parent,
158+            "roots": [crashes],
159+            "exclude": frozenset(),
160+        },
161+    }
162+
163+
164+class ScanCounts:
165+    """Exactly what a scan inspected, and what it found."""
166+
167+    def __init__(self) -> None:
168+        self.files_scanned = 0
169+        self.symlinks_skipped = 0
170+        self.bytes_scanned = 0
171+        self.errors = 0
172+        self.match_detected = False
173+        self.present = False
70174 
71175 
72-def scan_surface(name: str, roots: list[Path], needle: bytes) -> dict[str, Any]:
73-    """Scan one surface for the needle, counting exactly what was inspected.
176+def scan_paths(
177+    roots: list[Path],
178+    needle: bytes,
179+    exclude: frozenset[Path] = frozenset(),
180+) -> ScanCounts:
181+    """Scan a set of files and directories for the needle, counting the reads.
74182 
75183     Symlinks are skipped rather than followed. Following them would let a link
76-    out of the data root inflate the byte count with files this candidate never
184+    out of the surface inflate the byte count with files this candidate never
77185     wrote, which would make the receipt claim more coverage than it earned.
78186     """
79-    files_scanned = 0
80-    symlinks_skipped = 0
81-    bytes_scanned = 0
82-    errors = 0
83-    match_detected = False
84-    present = False
187+    counts = ScanCounts()
188+
189+    def read_file(path: Path) -> None:
190+        try:
191+            file_stat = path.lstat()
192+            if not stat.S_ISREG(file_stat.st_mode):
193+                return
194+            if file_stat.st_size > MAX_FILE_BYTES:
195+                counts.errors += 1
196+                return
197+            payload = path.read_bytes()
198+        except OSError:
199+            counts.errors += 1
200+            return
201+        counts.files_scanned += 1
202+        counts.bytes_scanned += len(payload)
203+        if needle in payload:
204+            counts.match_detected = True
85205 
86206     for root in roots:
207+        if root.is_symlink():
208+            counts.symlinks_skipped += 1
209+            continue
87210         if not root.exists():
88211             continue
89-        present = True
212+        counts.present = True
213+        if root.is_file():
214+            if root not in exclude:
215+                read_file(root)
216+            continue
90217         for current, directories, files in os.walk(root, followlinks=False):
91218             current_path = Path(current)
92219             for directory in list(directories):
93220                 if (current_path / directory).is_symlink():
94-                    symlinks_skipped += 1
221+                    counts.symlinks_skipped += 1
95222                     directories.remove(directory)
96223             for file_name in files:
97224                 path = current_path / file_name
98-                try:
99-                    if path.is_symlink():
100-                        symlinks_skipped += 1
101-                        continue
102-                    file_stat = path.lstat()
103-                    if not stat.S_ISREG(file_stat.st_mode):
104-                        continue
105-                    if file_stat.st_size > MAX_FILE_BYTES:
106-                        errors += 1
107-                        continue
108-                    payload = path.read_bytes()
109-                except OSError:
110-                    errors += 1
225+                if path in exclude:
226+                    continue
227+                if path.is_symlink():
228+                    counts.symlinks_skipped += 1
111229                     continue
112-                files_scanned += 1
113-                bytes_scanned += len(payload)
114-                if needle in payload:
115-                    match_detected = True
230+                read_file(path)
116231 
117-    return {
232+    return counts
233+
234+
235+def surface_record(
236+    name: str, path_label: str, status: str, counts: ScanCounts | None = None
237+) -> dict[str, Any]:
238+    """One surface's line in the receipt, in the exact shape the validator reads."""
239+    counts = counts or ScanCounts()
240+    body = {
118241         "name": name,
119-        "path_digest": sha256_hex(
120-            "\n".join(sorted(str(root) for root in roots)).encode("utf-8")
121-        ),
122-        "status": "pass" if present else "absent",
123-        "files_scanned": files_scanned,
124-        "symlinks_skipped": symlinks_skipped,
125-        "bytes_scanned": bytes_scanned,
126-        "errors": errors,
127-        "match_detected": match_detected,
128-        "evidence_digest": sha256_hex(
129-            json.dumps(
130-                {
131-                    "name": name,
132-                    "files_scanned": files_scanned,
133-                    "symlinks_skipped": symlinks_skipped,
134-                    "bytes_scanned": bytes_scanned,
135-                    "errors": errors,
136-                    "match_detected": match_detected,
137-                },
138-                sort_keys=True,
139-            ).encode("utf-8")
140-        ),
242+        "status": status,
243+        "files_scanned": counts.files_scanned,
244+        "symlinks_skipped": counts.symlinks_skipped,
245+        "bytes_scanned": counts.bytes_scanned,
246+        "errors": counts.errors,
247+        "match_detected": counts.match_detected,
248+    }
249+    return {
250+        **body,
251+        "path_digest": sha256_hex(path_label.encode("utf-8")),
252+        "evidence_digest": sha256_hex(json.dumps(body, sort_keys=True).encode("utf-8")),
141253     }
142254 
143255 
144-def deliver_needle_through_protected_fd(needle: bytes) -> None:
145-    """Hand the needle to the candidate through an O_CLOEXEC pipe.
256+def resolve_status(name: str, counts: ScanCounts, anchor_present: bool) -> str:
257+    """Turn a scan into one of the four things that can honestly be said.
258+
259+    `blocked` is the answer whenever the search could not be made where it had
260+    to be made. It is deliberately a status the receipt validator refuses.
261+    """
262+    if not anchor_present:
263+        return "blocked"
264+    if counts.errors:
265+        return "error"
266+    if counts.match_detected:
267+        return "match"
268+    if not counts.present:
269+        return "absent" if name in OPTIONAL_SURFACES else "blocked"
270+    return "pass"
271+
272+
273+def scan_filesystem_surface(
274+    name: str, plan: dict[str, Any], needle: bytes
275+) -> tuple[dict[str, Any], str]:
276+    anchor: Path = plan["anchor"]
277+    roots: list[Path] = plan["roots"]
278+    counts = scan_paths(roots, needle, plan["exclude"])
279+    anchor_present = anchor.is_dir()
280+    status = resolve_status(name, counts, anchor_present)
281+    label = "\n".join(sorted(str(root) for root in roots))
282+    detail = f"{status} at {label}"
283+    if not anchor_present:
284+        detail = (
285+            f"blocked: {anchor} does not exist, so this run did not look where "
286+            "the product writes"
287+        )
288+    return surface_record(name, label, status, counts), detail
289+
290+
291+def appkit_pasteboard_bytes() -> bytes | None:
292+    """Every flavour of every item on the general pasteboard, through PyObjC.
146293 
147-    The point of the protected descriptor is that the secret never exists as a
148-    file, an argument, or an environment variable that some unrelated process
149-    could read. It lives in the pipe buffer and in this process's memory, and
150-    both die with this run.
294+    `None` means this interface was not available or did not answer, which
295+    falls through to `pbpaste`. It never falls through to a pass.
151296     """
152-    read_fd, write_fd = os.pipe()
153297     try:
154-        os.set_inheritable(read_fd, False)
155-        os.set_inheritable(write_fd, False)
156-        os.write(write_fd, needle)
157-    finally:
158-        os.close(write_fd)
159-        os.close(read_fd)
298+        from AppKit import NSPasteboard  # type: ignore[import-not-found]
299+    except ImportError:
300+        return None
301+    try:
302+        pasteboard = NSPasteboard.generalPasteboard()
303+        payload = bytearray()
304+        for item in pasteboard.pasteboardItems() or []:
305+            for kind in item.types() or []:
306+                data = item.dataForType_(kind)
307+                if data is not None:
308+                    payload += bytes(data)
309+    except Exception:  # noqa: BLE001 - any failed read is a read that did not happen
310+        return None
311+    return bytes(payload)
312+
313+
314+def pasteboard_bytes() -> tuple[bytes | None, str]:
315+    """Read the general pasteboard through its own interface.
316+
317+    PyObjC reads every flavour of every item when it is available. Where it is
318+    not, `pbpaste` reads the text flavours, which is a real read of the same
319+    pasteboard and is recorded as exactly that much coverage. A pasteboard is
320+    never a file in a data directory, so there is no third option that reads
321+    one.
322+    """
323+    through_appkit = appkit_pasteboard_bytes()
324+    if through_appkit is not None:
325+        return through_appkit, "NSPasteboard general pasteboard, every type of every item"
326+
327+    if shutil.which("pbpaste") is None:
328+        return None, ""
329+    payload = bytearray()
330+    read_any = False
331+    for flavour in ("txt", "rtf"):
332+        try:
333+            result = subprocess.run(
334+                ["pbpaste", "-Prefer", flavour],
335+                capture_output=True,
336+                timeout=30,
337+                check=False,
338+            )
339+        except (OSError, subprocess.SubprocessError):
340+            continue
341+        if result.returncode == 0:
342+            read_any = True
343+            payload += result.stdout
344+    if not read_any:
345+        return None, ""
346+    return bytes(payload), "pbpaste general pasteboard, text and rich-text flavours"
160347 
161348 
162-def self_test() -> None:
163-    """The scanner has to be able to find a needle, or every run passes."""
164-    import tempfile
349+def pasteboard_inventory() -> int | None:
350+    """How many bytes the pasteboard itself says it is holding.
165351 
166-    needle = secrets.token_hex(NEEDLE_BYTES).encode("ascii")
352+    `clipboard info` answers with a flavour and a size for everything on the
353+    general pasteboard. It is the corroboration for an empty read: a read that
354+    came back with nothing while the pasteboard says it holds something means
355+    the flavours were not all reachable, not that the surface is clean.
356+    """
357+    try:
358+        result = subprocess.run(
359+            ["osascript", "-e", "clipboard info"],
360+            capture_output=True,
361+            text=True,
362+            timeout=30,
363+            check=False,
364+        )
365+    except (OSError, subprocess.SubprocessError):
366+        return None
367+    if result.returncode != 0:
368+        return None
369+    total = 0
370+    for token in result.stdout.split(","):
371+        token = token.strip()
372+        if token.isdigit():
373+            total += int(token)
374+    return total
375+
376+
377+def pasteboard_capture_status(
378+    payload: bytes | None, declared: int | None
379+) -> tuple[bool, str]:
380+    """Whether a pasteboard read may be recorded as an observation of the surface.
381+
382+    An empty read is only an observation when the pasteboard agrees it is
383+    empty. Otherwise this run saw less than the surface holds, and a partial
384+    look at a surface is not a clean one.
385+    """
386+    if payload is None:
387+        return False, "the general pasteboard could not be read through any interface"
388+    if payload:
389+        return True, ""
390+    if declared is None:
391+        return False, (
392+            "the pasteboard read returned nothing and the pasteboard's own "
393+            "inventory could not be read to confirm it is empty"
394+        )
395+    if declared > 0:
396+        return False, (
397+            f"the pasteboard holds {declared} bytes in flavours this read "
398+            "cannot see, so it was not fully scanned"
399+        )
400+    return True, ""
401+
402+
403+def capture_pasteboard(directory: Path) -> tuple[Path | None, str, str]:
404+    if platform.system() != "Darwin":
405+        return None, "", "the pasteboard is a macOS interface and this host is not macOS"
406+    payload, method = pasteboard_bytes()
407+    readable, reason = pasteboard_capture_status(
408+        payload, pasteboard_inventory() if not payload else None
409+    )
410+    if not readable or payload is None:
411+        return None, "", reason
412+    destination = directory / "pasteboard.bin"
413+    handle = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
414+    with os.fdopen(handle, "wb") as capture:
415+        capture.write(payload)
416+    return destination, method, ""
417+
418+
419+def candidate_pid(app: Path | None, explicit: int | None) -> int | None:
420+    """The running candidate this run reads the accessibility tree of."""
421+    if explicit is not None:
422+        return explicit
423+    if app is None:
424+        return None
425+    executables = str(app / "Contents" / "MacOS")
426+    try:
427+        result = subprocess.run(
428+            ["pgrep", "-f", executables],
429+            capture_output=True,
430+            text=True,
431+            timeout=30,
432+            check=False,
433+        )
434+    except (OSError, subprocess.SubprocessError):
435+        return None
436+    for line in result.stdout.split():
437+        try:
438+            return int(line)
439+        except ValueError:
440+            continue
441+    return None
442+
443+
444+def accessibility_tree(pid: int) -> tuple[str | None, str]:
445+    """Every label and value the process publishes to assistive technology.
446+
447+    This is the surface a screen reader consumes, read through the same
448+    `AXUIElement` interface it uses. It is never a file in a data directory,
449+    and a run that cannot reach it says so rather than recording an empty
450+    directory as a clean surface.
451+    """
452+    script = f"""
453+    tell application "System Events"
454+        if not (exists (first process whose unix id is {pid})) then return "NOPROC"
455+        tell (first process whose unix id is {pid})
456+            set out to ""
457+            try
458+                repeat with w in windows
459+                    repeat with e in (entire contents of w)
460+                        try
461+                            set d to description of e
462+                            if d is not missing value then set out to out & d & linefeed
463+                        end try
464+                        try
465+                            set v to value of e
466+                            if v is not missing value then set out to out & (v as text) & linefeed
467+                        end try
468+                    end repeat
469+                end repeat
470+            end try
471+            return out
472+        end tell
473+    end tell
474+    """
475+    try:
476+        result = subprocess.run(
477+            ["osascript", "-e", script],
478+            capture_output=True,
479+            text=True,
480+            timeout=120,
481+            check=False,
482+        )
483+    except (OSError, subprocess.SubprocessError):
484+        return None, "the accessibility tree could not be read; osascript did not answer"
485+    if result.returncode != 0:
486+        return None, (
487+            "the accessibility tree could not be read. Grant this terminal "
488+            "Accessibility in System Settings > Privacy & Security > Accessibility"
489+        )
490+    if result.stdout.strip() == "NOPROC":
491+        return None, "the candidate is not running, so it publishes no accessibility tree"
492+    return result.stdout, ""
493+
494+
495+def capture_accessibility(directory: Path, pid: int | None) -> tuple[Path | None, str, str]:
496+    if platform.system() != "Darwin":
497+        return None, "", "the accessibility tree is a macOS interface and this host is not macOS"
498+    if pid is None:
499+        return None, "", "no running candidate was found to read an accessibility tree from"
500+    tree, reason = accessibility_tree(pid)
501+    if tree is None:
502+        return None, "", reason
503+    destination = directory / "accessibility.txt"
504+    handle = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
505+    with os.fdopen(handle, "wb") as capture:
506+        capture.write(tree.encode("utf-8", errors="surrogateescape"))
507+    return destination, f"AXUIElement tree of pid {pid} through System Events", ""
508+
509+
510+def scan_captured_surface(
511+    name: str,
512+    label: str,
513+    capture: Path | None,
514+    reason: str,
515+    needle: bytes,
516+) -> tuple[dict[str, Any], str]:
517+    """A surface that had to be captured through an API before it could be read."""
518+    if capture is None:
519+        return surface_record(name, label, "blocked"), f"blocked: {reason}"
520+    counts = scan_paths([capture], needle)
521+    status = resolve_status(name, counts, anchor_present=True)
522+    return surface_record(name, label, status, counts), f"{status}: {reason}"
523+
524+
525+def read_needle(file_descriptor: int) -> bytes:
526+    """Read the canary from the descriptor the caller opened on it.
527+
528+    The descriptor is the whole point: the secret never becomes an argument an
529+    unrelated process can read out of the process table, and this script never
530+    invents one. A needle nothing has ever seen cannot be leaked, so searching
531+    for one proves nothing at all.
532+    """
533+    chunks: list[bytes] = []
534+    remaining = MAXIMUM_NEEDLE_BYTES + 2
535+    while remaining > 0:
536+        try:
537+            chunk = os.read(file_descriptor, remaining)
538+        except OSError as error:
539+            raise TripwireError(f"--needle-fd {file_descriptor} could not be read") from error
540+        if not chunk:
541+            break
542+        chunks.append(chunk)
543+        remaining -= len(chunk)
544+    needle = b"".join(chunks).rstrip(b"\r\n")
545+    if not MINIMUM_NEEDLE_BYTES <= len(needle) <= MAXIMUM_NEEDLE_BYTES:
546+        raise TripwireError(
547+            f"the canary must carry {MINIMUM_NEEDLE_BYTES}..{MAXIMUM_NEEDLE_BYTES} "
548+            f"bytes; this descriptor carried {len(needle)}"
549+        )
550+    return needle
551+
552+
553+def verify_scanner_can_fire(
554+    needle: bytes,
555+    directory: Path,
556+    scanner: Callable[[list[Path], bytes], ScanCounts] | None = None,
557+) -> bool:
558+    """Plant this run's needle and require the scanner to find it.
559+
560+    Without this, every way the search can be silently disabled — a needle that
561+    arrived empty, an edit that broke the comparison, a walk that reads nothing
562+    — reports the same `pass` as a clean product.
563+    """
564+    control = directory / "scanner-control"
565+    control.mkdir(mode=0o700)
566+    planted = control / "planted"
567+    handle = os.open(planted, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
568+    with os.fdopen(handle, "wb") as target:
569+        target.write(b"prefix" + needle + b"suffix")
570+    counts = (scanner or scan_paths)([control], needle)
571+    shutil.rmtree(control, ignore_errors=True)
572+    return counts.match_detected and counts.files_scanned == 1
573+
574+
575+def self_test() -> None:
576+    """Every property this receipt rests on has to be able to fail here."""
167577     with tempfile.TemporaryDirectory() as directory:
168-        root = Path(directory) / "logs"
578+        workspace = Path(directory)
579+        needle = b"canary-" + b"0123456789abcdef" * 2
580+
581+        # The scanner finds a planted needle, and distinguishes clean from
582+        # missing. A scanner that cannot do this makes every run pass.
583+        root = workspace / "logs"
169584         root.mkdir()
170585         (root / "clean.log").write_bytes(b"nothing to see")
171-        clean = scan_surface("logs", [root], needle)
172-        if clean["match_detected"] or clean["status"] != "pass":
586+        clean = scan_paths([root], needle)
587+        if clean.match_detected or not clean.present or clean.files_scanned != 1:
173588             raise TripwireError("scanner reported a clean surface incorrectly")
174-        if clean["files_scanned"] != 1:
175-            raise TripwireError("scanner did not count the file it read")
176-
177589         (root / "leaked.log").write_bytes(b"secret=" + needle)
178-        dirty = scan_surface("logs", [root], needle)
179-        if not dirty["match_detected"]:
590+        dirty = scan_paths([root], needle)
591+        if not dirty.match_detected:
180592             raise TripwireError("scanner failed to detect a planted needle")
181-
182-        absent = scan_surface("logs", [Path(directory) / "missing"], needle)
183-        if absent["status"] != "absent" or absent["files_scanned"] != 0:
593+        missing = scan_paths([workspace / "missing"], needle)
594+        if missing.present or missing.files_scanned != 0:
184595             raise TripwireError("scanner did not distinguish absent from clean")
185596 
597+        # An excluded path is not counted by the surface that excludes it.
598+        excluded = scan_paths([root], needle, frozenset({root / "leaked.log"}))
599+        if excluded.match_detected or excluded.files_scanned != 1:
600+            raise TripwireError("scanner did not honour the exclusion")
601+
602+        # The live-fire control has to catch a scanner that cannot match.
603+        if not verify_scanner_can_fire(needle, workspace):
604+            raise TripwireError("the scanner control failed on a working scanner")
605+        blind = verify_scanner_can_fire(
606+            needle, workspace, scanner=lambda roots, needle: ScanCounts()
607+        )
608+        if blind:
609+            raise TripwireError("the scanner control passed a scanner that never matches")
610+
611+        # An empty pasteboard read is an observation only when the pasteboard
612+        # agrees it is empty.
613+        for payload, declared, expected in (
614+            (b"something", None, True),
615+            (None, None, False),
616+            (b"", None, False),
617+            (b"", 0, True),
618+            (b"", 12, False),
619+        ):
620+            readable, _ = pasteboard_capture_status(payload, declared)
621+            if readable is not expected:
622+                raise TripwireError(
623+                    f"a pasteboard read of {payload!r} against {declared} was "
624+                    f"judged {readable}"
625+                )
626+
627+        # A needle outside the receipt's bounds is refused before anything is
628+        # scanned, and a short one is refused too.
629+        short_path = workspace / "short-canary"
630+        short_path.write_bytes(b"tooshort")
631+        with open(short_path, "rb") as short:
632+            try:
633+                read_needle(short.fileno())
634+            except TripwireError:
635+                pass
636+            else:
637+                raise TripwireError("a canary below the minimum length was accepted")
638+        long_path = workspace / "canary"
639+        long_path.write_bytes(needle + b"\n")
640+        with open(long_path, "rb") as canary:
641+            if read_needle(canary.fileno()) != needle:
642+                raise TripwireError("the canary read back differently than it was written")
643+
644+        # The surfaces resolve where `paths.rs` writes, not under the
645+        # application support root. This is the regression that let four of six
646+        # surfaces record `absent` while the product logged 191 KB elsewhere.
647+        logs = logs_root()
648+        if logs != Path.home() / "Library" / "Logs" / STORAGE_SLUG:
649+            raise TripwireError("the logs surface does not resolve to paths::logs_dir()")
650+        if "Application Support" in str(logs):
651+            raise TripwireError("the logs surface resolves under the data root again")
652+        if crashes_root() != Path.home() / "Library" / "Logs" / "DiagnosticReports":
653+            raise TripwireError("the crash surface does not resolve to paths::crashes_dir()")
654+        plan = filesystem_surfaces(data_root(), logs, crashes_root())
655+        if plan["telemetry"]["roots"] != [logs / "telemetry.log"]:
656+            raise TripwireError("the telemetry surface does not resolve to the telemetry log")
657+        if set(plan) | {"clipboard", "accessibility"} != {
658+            "logs",
659+            "telemetry",
660+            "diagnostics",
661+            "crashes",
662+            "clipboard",
663+            "accessibility",
664+        }:
665+            raise TripwireError("the surface inventory is not the six the validator requires")
666+
667+        # A required surface whose anchor does not exist blocks; it never
668+        # reports the absence as a clean scan.
669+        empty = ScanCounts()
670+        if resolve_status("logs", empty, anchor_present=False) != "blocked":
671+            raise TripwireError("a missing logs anchor did not block")
672+        if resolve_status("logs", empty, anchor_present=True) != "blocked":
673+            raise TripwireError("a missing required surface did not block")
674+        if resolve_status("crashes", empty, anchor_present=True) != "absent":
675+            raise TripwireError("an optional absent surface did not record absent")
676+        scanned = ScanCounts()
677+        scanned.present = True
678+        if resolve_status("logs", scanned, anchor_present=True) != "pass":
679+            raise TripwireError("a scanned clean surface did not pass")
680+        scanned.match_detected = True
681+        if resolve_status("logs", scanned, anchor_present=True) != "match":
682+            raise TripwireError("a matched surface did not report the match")
683+
186684     print(json.dumps({"schema": TRIPWIRE_SCHEMA, "status": "self-test-passed"}))
187685 
188686 
189687 def main() -> int:
190688     parser = argparse.ArgumentParser()
191-    parser.add_argument("--app", type=Path)
689+    parser.add_argument(
690+        "--app",
691+        type=Path,
692+        default=Path("/Applications/Omega.app"),
693+        help="the installed candidate whose accessibility tree is read",
694+    )
192695     parser.add_argument("--candidate-evidence", type=Path)
696+    parser.add_argument(
697+        "--needle-fd",
698+        type=int,
699+        default=None,
700+        help=(
701+            "descriptor already open on the disposable canary the candidate was "
702+            "given. Required: this script does not invent a secret nothing has seen."
703+        ),
704+    )
705+    parser.add_argument("--candidate-pid", type=int, default=None)
193706     parser.add_argument("--data-root", type=Path, default=None)
707+    parser.add_argument("--logs-root", type=Path, default=None)
708+    parser.add_argument("--crashes-root", type=Path, default=None)
194709     parser.add_argument("--output", type=Path)
195710     parser.add_argument("--self-test", action="store_true")
196711     args = parser.parse_args()
@@ -201,6 +716,14 @@ def main() -> int:
201716 
202717     if args.candidate_evidence is None or args.output is None:
203718         raise TripwireError("--candidate-evidence and --output are required")
719+    if args.needle_fd is None:
720+        raise TripwireError(
721+            "--needle-fd is required. Pass the descriptor of the disposable "
722+            "canary the candidate was actually given, for example "
723+            "`--needle-fd 3 3<\"$OMEGA_PRIVATE_PROOF_DIR/disposable-canary\"`. "
724+            "A needle this script generates itself has never been seen by any "
725+            "other process, so searching for it can only ever pass."
726+        )
204727 
205728     evidence = json.loads(args.candidate_evidence.read_text(encoding="utf-8"))
206729     # The candidate evidence records a structured digest, not a bare string;
@@ -211,22 +734,68 @@ def main() -> int:
211734             "candidate evidence has no usable candidate_digest to bind to"
212735         )
213736 
214-    needle_text = secrets.token_hex(NEEDLE_BYTES)
215-    needle = needle_text.encode("ascii")
216-    deliver_needle_through_protected_fd(needle)
737+    needle = read_needle(args.needle_fd)
217738 
218-    root = args.data_root or data_root()
219-    surfaces = [
220-        scan_surface(name, [root / part for part in parts], needle)
221-        for name, parts in SURFACE_ROOTS.items()
222-    ]
739+    surfaces: list[dict[str, Any]] = []
740+    details: dict[str, str] = {}
741+    workspace = Path(tempfile.mkdtemp(prefix="omega-tripwires-"))
742+    os.chmod(workspace, 0o700)
743+    try:
744+        if not verify_scanner_can_fire(needle, workspace):
745+            raise TripwireError(
746+                "the scanner did not detect a needle planted in front of it, so "
747+                "nothing this run could report about the product would mean "
748+                "anything"
749+            )
750+
751+        plan = filesystem_surfaces(
752+            data_root(args.data_root),
753+            logs_root(args.logs_root),
754+            crashes_root(args.crashes_root),
755+        )
756+        for name in sorted(plan):
757+            record, detail = scan_filesystem_surface(name, plan[name], needle)
758+            surfaces.append(record)
759+            details[name] = detail
760+
761+        capture, method, reason = capture_pasteboard(workspace)
762+        record, detail = scan_captured_surface(
763+            "clipboard", "NSPasteboard:general", capture, method or reason, needle
764+        )
765+        surfaces.append(record)
766+        details["clipboard"] = detail
767+
768+        capture, method, reason = capture_accessibility(
769+            workspace, candidate_pid(args.app, args.candidate_pid)
770+        )
771+        record, detail = scan_captured_surface(
772+            "accessibility",
773+            f"AXUIElement:{args.app}",
774+            capture,
775+            method or reason,
776+            needle,
777+        )
778+        surfaces.append(record)
779+        details["accessibility"] = detail
780+    finally:
781+        shutil.rmtree(workspace, ignore_errors=True)
223782 
224783     leaked = [surface["name"] for surface in surfaces if surface["match_detected"]]
784+    blocked = [
785+        surface["name"] for surface in surfaces if surface["status"] in ("blocked", "error")
786+    ]
787+    if leaked:
788+        status = "fail"
789+    elif blocked:
790+        status = "blocked"
791+    else:
792+        status = "pass"
793+
225794     receipt = {
226795         "schema": TRIPWIRE_SCHEMA,
227796         "candidate_digest": candidate_digest,
228797         "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
229-        "status": "fail" if leaked else "pass",
798+        "status": status,
230799         # The value is deliberately absent. Recording the secret you are proving
231800         # was not leaked would defeat the exercise.
232801         "needle": {
@@ -238,20 +807,31 @@ def main() -> int:
238807     }
239808 
240809     args.output.parent.mkdir(parents=True, exist_ok=True)
241-    args.output.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8")
810+    args.output.write_text(
811+        json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8"
812+    )
242813 
243814     print(
244815         json.dumps(
245816             {
246817                 "schema": TRIPWIRE_SCHEMA,
247-                "status": receipt["status"],
818+                "status": status,
248819                 "surfaces_scanned": len(surfaces),
249820                 "leaked_surfaces": leaked,
821+                "blocked_surfaces": blocked,
822+                # How each surface was read, so a reader can see the coverage
823+                # the receipt's counts came from. The receipt itself carries no
824+                # free-text field the validator would have to trust.
825+                "surface_detail": details,
250826                 "output": str(args.output),
251-            }
827+            },
828+            indent=2,
829+            sort_keys=True,
252830         )
253831     )
254-    return 1 if leaked else 0
832+    if leaked:
833+        return 1
834+    return 3 if blocked else 0
255835 
256836 
257837 if __name__ == "__main__":
diff --git a/script/omega-brand-gate.json b/script/omega-brand-gate.json
index 0b75595757..3873fa445c 100644
--- a/script/omega-brand-gate.json
+++ b/script/omega-brand-gate.json
@@ -22,12 +22,26 @@
2222   "brand": {
2323     "_comment": [
2424       "words are matched case-sensitively at ASCII alphanumeric boundaries, so",
25-      "'Zed' and \"Zed's\" match while 'Zedd' does not. Lowercase 'zed' is NOT a",
26-      "word here on purpose: it is a substring of 'authorized', 'organized' and",
27-      "'normalized', and a gate that cries wolf gets deleted."
25+      "'Zed' and \"Zed's\" match while 'Zedd' does not.",
26+      "",
27+      "Lowercase 'zed' used to be excluded here, and the reason written down was",
28+      "that it is a substring of 'authorized', 'organized' and 'normalized'. That",
29+      "reason was wrong: the boundary rule already excludes all three, because",
30+      "the 'zed' in 'authorized' has an 'i' before it. The exclusion cost nothing",
31+      "it claimed to save and hid the rendered --help of both shipped binaries --",
32+      "'Run zed in the foreground', 'Run zed in dev-server mode', 'Instructs zed",
33+      "to run as a dev server' -- through 0.2.0-rc14 (omega#89).",
34+      "",
35+      "It is in now. Measured on this tree, the lowercase hits are dominated by",
36+      "identifiers -- zed_actions, zed::, .zed/, zed://, ZED_* -- and is_prose()",
37+      "drops those, because a token is not a sentence. What survives the prose",
38+      "filter is copy, and copy is what this gate is for. An inherited identifier",
39+      "that does reach a sentence is classified as fork_seam with the reason,",
40+      "not filtered away silently."
2841     ],
2942     "words": [
30-      "Zed"
43+      "Zed",
44+      "zed"
3145     ],
3246     "substrings": [
3347       "zed.dev",
@@ -560,7 +574,158 @@
560574       "// Upstream Zed ships this empty. Omega configures codex-acp because Full Auto": {
561575         "class": "omega_contrast",
562576         "reason": "Omega-authored comment naming the upstream default this replaces (OMEGA-DELTA-0026)."
577+      },
578+      "\"**/.zed/settings.json\", // zed project settings": {
579+        "class": "fork_seam",
580+        "reason": "Default private-files glob over the inherited `.zed/` project settings folder. Omega still reads project settings out of `.zed/`, so the folder name is a lookup, not a claim; renaming it would stop matching the files it exists to keep private."
581+      },
582+      "\"/**/zed/settings.json\", // zed user settings": {
583+        "class": "zed_product",
584+        "reason": "Default private-files glob over the upstream editor's user settings directory. It protects somebody else's settings file from being read into context; Omega's own roots are named by their own slug."
585+      },
586+      "// Currently busted: https://github.com/zed-industries/feedback/issues/898": {
587+        "class": "zed_product",
588+        "reason": "Inherited keymap comment linking Zed's own feedback tracker for a Sublime-preset binding that is broken there. A fact about Zed's issue tracker, not about Omega."
589+      },
590+      "<nul set /p zed_init_ready={INIT_COMMAND_STARTUP_MARKER_PREFIX}&echo {marker_id}{INIT_COMMAND_STARTUP_MARKER_SUFFIX}": {
591+        "class": "fork_seam",
592+        "reason": "Windows shell variable the terminal integration sets and then reads back to detect that the init command has started. The name IS the handshake; renaming one side without the other breaks startup detection."
593+      },
594+      "Couldn't get project path for .zed/debug.json in active worktree": {
595+        "class": "fork_seam",
596+        "reason": "Names the inherited `.zed/` project configuration folder, which Omega still reads debug configurations out of."
597+      },
598+      "Failed to read crates/zed/Cargo.toml": {
599+        "class": "fork_seam",
600+        "reason": "Build-script message naming the `zed` crate by its directory. The crate name is an inherited identifier; renaming the crate is a separate change with its own blast radius, and this message would be wrong either way if only the sentence moved."
601+      },
602+      "Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)": {
603+        "class": "zed_product",
604+        "reason": "Settings-schema note linking Zed's own tracking issue for an upstream experiment Omega inherited. The issue really is on Zed's tracker."
605+      },
606+      "SELECT ep_request_id AS request_id, device_id AS device_id, requested_at::string AS continuation_time, requested_at::string AS time, input_payload AS input, prompt AS prompt, requested_output AS output, settled_editable_region AS settled_editable_region, is_ep_shown_before_rejected AS was_shown, ep_rejected_reason AS reason, zed_version AS zed_version FROM ZED_DBT.DBT_PROD.fct_edit_prediction_examples WHERE ep_outcome LIKE ? AND is_ep_shown_before_rejected = true AND requested_at > TRY_TO_TIMESTAMP_NTZ(?) AND (? IS NULL OR ( COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 1) AS INTEGER), 0) * 1000000 + COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 2) AS INTEGER), 0) * 1000 + COALESCE(TRY_CAST(SPLIT_PART(SPLIT_PART(zed_version, '.', 3), '+', 1) AS INTEGER), 0) ) >= ?) ORDER BY requested_at ASC LIMIT ? OFFSET ?": {
607+        "class": "zed_service",
608+        "reason": "Analytics query against Zed's own Snowflake warehouse (ZED_DBT.DBT_PROD) for the edit-prediction dataset, selecting that warehouse's zed_version column. The table, the column and the data are Zed's; substituting Omega would name a warehouse Omega does not have. Reached only by the edit-prediction developer CLI, never by the shipped editor."
609+      },
610+      "SELECT ep_request_id AS request_id, device_id AS device_id, requested_at::string AS continuation_time, requested_at::string AS time, input_payload AS input, prompt AS prompt, requested_output AS output, settled_editable_region AS settled_editable_region, zed_version AS zed_version FROM ZED_DBT.DBT_PROD.fct_edit_prediction_examples WHERE ep_outcome = 'Accepted' AND requested_at > TRY_TO_TIMESTAMP_NTZ(?) AND (? IS NULL OR ( COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 1) AS INTEGER), 0) * 1000000 + COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 2) AS INTEGER), 0) * 1000 + COALESCE(TRY_CAST(SPLIT_PART(SPLIT_PART(zed_version, '.', 3), '+', 1) AS INTEGER), 0) ) >= ?) ORDER BY requested_at ASC LIMIT ? OFFSET ?": {
611+        "class": "zed_service",
612+        "reason": "Analytics query against Zed's own Snowflake warehouse (ZED_DBT.DBT_PROD) for the edit-prediction dataset, selecting that warehouse's zed_version column. The table, the column and the data are Zed's; substituting Omega would name a warehouse Omega does not have. Reached only by the edit-prediction developer CLI, never by the shipped editor."
613+      },
614+      "SELECT ep_request_id AS request_id, device_id AS device_id, requested_at::string AS continuation_time, requested_at::string AS time, input_payload AS input, requested_output AS requested_output, settled_editable_region AS settled_editable_region, requested_format AS requested_format, zed_version AS zed_version FROM ZED_DBT.DBT_PROD.fct_edit_prediction_examples WHERE settled_editable_region IS NOT NULL AND requested_at > TRY_TO_TIMESTAMP_NTZ(?) ORDER BY requested_at ASC LIMIT ? OFFSET ?": {
615+        "class": "zed_service",
616+        "reason": "Analytics query against Zed's own Snowflake warehouse (ZED_DBT.DBT_PROD) for the edit-prediction dataset, selecting that warehouse's zed_version column. The table, the column and the data are Zed's; substituting Omega would name a warehouse Omega does not have. Reached only by the edit-prediction developer CLI, never by the shipped editor."
617+      },
618+      "SELECT ep_request_id AS request_id, device_id AS device_id, requested_at::string AS continuation_time, requested_at::string AS time, input_payload AS input, settled_editable_region AS settled_editable_region, example_payload AS example, zed_version AS zed_version FROM ZED_DBT.DBT_PROD.fct_edit_prediction_examples WHERE settled_editable_region IS NOT NULL AND example_payload IS NOT NULL AND requested_at > TRY_TO_TIMESTAMP_NTZ(?) AND (? IS NULL OR ( COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 1) AS INTEGER), 0) * 1000000 + COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 2) AS INTEGER), 0) * 1000 + COALESCE(TRY_CAST(SPLIT_PART(SPLIT_PART(zed_version, '.', 3), '+', 1) AS INTEGER), 0) ) >= ?) ORDER BY requested_at ASC LIMIT ? OFFSET ?": {
619+        "class": "zed_service",
620+        "reason": "Analytics query against Zed's own Snowflake warehouse (ZED_DBT.DBT_PROD) for the edit-prediction dataset, selecting that warehouse's zed_version column. The table, the column and the data are Zed's; substituting Omega would name a warehouse Omega does not have. Reached only by the edit-prediction developer CLI, never by the shipped editor."
621+      },
622+      "SELECT ep_request_id AS request_id, device_id AS device_id, requested_at::string AS continuation_time, requested_at::string AS time, input_payload AS input, zed_version AS zed_version FROM ZED_DBT.DBT_PROD.fct_edit_prediction_examples WHERE requested_at > TRY_TO_TIMESTAMP_NTZ(?) AND (? IS NULL OR ( COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 1) AS INTEGER), 0) * 1000000 + COALESCE(TRY_CAST(SPLIT_PART(zed_version, '.', 2) AS INTEGER), 0) * 1000 + COALESCE(TRY_CAST(SPLIT_PART(SPLIT_PART(zed_version, '.', 3), '+', 1) AS INTEGER), 0) ) >= ?) ORDER BY requested_at ASC LIMIT ? OFFSET ?": {
623+        "class": "zed_service",
624+        "reason": "Analytics query against Zed's own Snowflake warehouse (ZED_DBT.DBT_PROD) for the edit-prediction dataset, selecting that warehouse's zed_version column. The table, the column and the data are Zed's; substituting Omega would name a warehouse Omega does not have. Reached only by the edit-prediction developer CLI, never by the shipped editor."
625+      },
626+      "SELECT ep_request_id AS request_id, rated_inputs AS inputs, rated_output AS output, settled_editable_region AS settled_editable_region, rating AS rating, feedback AS feedback, device_id AS device_id, requested_at::string AS continuation_time, requested_at::string AS time, NULL AS experiment_name, NULL AS environment, zed_version AS zed_version FROM ZED_DBT.DBT_PROD.fct_edit_prediction_examples WHERE rating IS NOT NULL AND (? IS NULL OR rating = ?) AND requested_at > TRY_TO_TIMESTAMP_NTZ(?) AND rated_inputs IS NOT NULL AND rated_inputs:cursor_excerpt IS NOT NULL AND rated_output IS NOT NULL ORDER BY requested_at ASC LIMIT ? OFFSET ?": {
627+        "class": "zed_service",
628+        "reason": "Analytics query against Zed's own Snowflake warehouse (ZED_DBT.DBT_PROD) for the edit-prediction dataset, selecting that warehouse's zed_version column. The table, the column and the data are Zed's; substituting Omega would name a warehouse Omega does not have. Reached only by the edit-prediction developer CLI, never by the shipped editor."
629+      },
630+      "SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees": {
631+        "class": "fork_seam",
632+        "reason": "Names the inherited `.zed/` project settings folder that this observer watches. The neighbouring `~/.config/` line named Zed's user directory and was wrong for Omega; it now names Omega's own channel directory."
633+      },
634+      "URLs can use `file://`, the current Omega channel scheme, or the legacy `zed://` scheme.": {
635+        "class": "omega_contrast",
636+        "reason": "Omega-authored --help line naming the legacy `zed://` scheme Omega still accepts alongside its own, so an existing link keeps working."
637+      },
638+      "Version not found in crates/zed/Cargo.toml": {
639+        "class": "fork_seam",
640+        "reason": "Build-script message naming the `zed` crate by its directory. The crate name is an inherited identifier; renaming the crate is a separate change with its own blast radius, and this message would be wrong either way if only the sentence moved."
641+      },
642+      "Windows only: resolve the `(release channel, version)` of the Linux `zed` to": {
643+        "class": "zed_product",
644+        "reason": "The WSL sandbox helper really is Zed's released Linux build, downloaded from cloud.zed.dev — see the helper script classified above. Substituting Omega would name a binary this path does not fetch."
645+      },
646+      "`zed://` scheme kept for compatibility with existing links.": {
647+        "class": "omega_contrast",
648+        "reason": "Omega-authored action description recording why the legacy scheme is still resolved."
649+      },
650+      "compiled wasm did not contain a valid zed extension api version": {
651+        "class": "fork_seam",
652+        "reason": "`zed:api-version` is the custom WASM section name every published extension is compiled with. It is a wire key, not a claim; renaming it would make Omega unable to load any existing extension."
653+      },
654+      "downloading zed-remote-server {os} {arch} version {}": {
655+        "class": "fork_seam",
656+        "reason": "The remote-server artifact and its directory on the host are both named `zed-remote-server` / `zed_remote_server` on either end of an SSH session. The name is how the two halves find each other."
657+      },
658+      "extension {extension_id} has no zed:api-version section": {
659+        "class": "fork_seam",
660+        "reason": "`zed:api-version` is the custom WASM section name every published extension is compiled with. It is a wire key, not a claim; renaming it would make Omega unable to load any existing extension."
661+      },
662+      "extension {} has invalid zed:api-version section: {:?}": {
663+        "class": "fork_seam",
664+        "reason": "`zed:api-version` is the custom WASM section name every published extension is compiled with. It is a wire key, not a claim; renaming it would make Omega unable to load any existing extension."
665+      },
666+      "failed to change ownership for zed_remote_server via chown: {}": {
667+        "class": "fork_seam",
668+        "reason": "The remote-server artifact and its directory on the host are both named `zed-remote-server` / `zed_remote_server` on either end of an SSH session. The name is how the two halves find each other."
669+      },
670+      "failed to parse zed Cargo.toml": {
671+        "class": "fork_seam",
672+        "reason": "Build-script message naming the `zed` crate by its directory. The crate name is an inherited identifier; renaming the crate is a separate change with its own blast radius, and this message would be wrong either way if only the sentence moved."
673+      },
674+      "failed to provision a Linux `zed` sandbox helper in {}{}": {
675+        "class": "zed_product",
676+        "reason": "The WSL sandbox helper really is Zed's released Linux build, downloaded from cloud.zed.dev — see the helper script classified above. Substituting Omega would name a binary this path does not fetch."
677+      },
678+      "interpreted as input redirection. See zed-industries/zed#55921.": {
679+        "class": "zed_product",
680+        "reason": "Settings-schema note citing the upstream issue that documents the behaviour. It really is Zed's issue number."
681+      },
682+      "invalid zed url: {:?}": {
683+        "class": "fork_seam",
684+        "reason": "Rejects a malformed mention URL on the inherited `zed://` scheme, naming the scheme that was actually parsed."
685+      },
686+      "provision the Linux `zed` sandbox helper": {
687+        "class": "zed_product",
688+        "reason": "The WSL sandbox helper really is Zed's released Linux build, downloaded from cloud.zed.dev — see the helper script classified above. Substituting Omega would name a binary this path does not fetch."
689+      },
690+      "the WSL `zed` sandbox helper resolved to {path:?} rather than an absolute path": {
691+        "class": "zed_product",
692+        "reason": "The WSL sandbox helper really is Zed's released Linux build, downloaded from cloud.zed.dev — see the helper script classified above. Substituting Omega would name a binary this path does not fetch."
693+      },
694+      "zed build.rs: {lib} not found in pkg-config's path": {
695+        "class": "fork_seam",
696+        "reason": "Names the `zed` crate's own build script by the crate directory it runs in."
697+      },
698+      "zed-html output defined": {
699+        "class": "fork_seam",
700+        "reason": "`zed-html` is the mdbook renderer key configured as `[output.zed-html]` in docs/book.toml. The string is the key the renderer is looked up by."
701+      },
702+      "zed://agent received but the AgentPanel is not registered \\ (is `disable_ai` enabled?)": {
703+        "class": "fork_seam",
704+        "reason": "Log line naming the legacy `zed://` scheme by the scheme it received. Substituting Omega's own scheme would report a different URL than the one that arrived."
563705       }
564706     }
707+  },
708+  "packaged": {
709+    "_comment": [
710+      "OMEGA-DELTA-0038. What the packaged half opens, and how much of it.",
711+      "",
712+      "Until omega#89 every packaged check in script/verify-omega-brand opened",
713+      "Contents/MacOS/omega and nothing else, while script/bundle-omega-rc copied",
714+      "and signed `cli` and `omega-identity-proof` into the same directory. That",
715+      "is why `cli` shipped an uninstaller that deleted a competitor's editor and",
716+      "no Omega path at all through two published prereleases, under a green gate",
717+      "(omega#88): the defect was never inside a file any check opened.",
718+      "",
719+      "The executable inventory is now derived by walking the bundle for Mach-O",
720+      "magic, so a helper added tomorrow is inside the gate the day it is added.",
721+      "The floors are on what the scans READ, not on what they find.",
722+      "",
723+      "minimum_rendered_invocations is the guard on the rendered-help scan: it",
724+      "runs the shipped binaries with --help and --version and reads what comes",
725+      "back, because clap builds the sentence a person sees at run time out of",
726+      "doc lines the source scan only ever sees separately."
727+    ],
728+    "minimum_executables": 3,
729+    "minimum_rendered_invocations": 6
565730   }
566731 }
diff --git a/script/uninstall.sh b/script/uninstall.sh
index 3e460b8186..1c873445d5 100644
--- a/script/uninstall.sh
+++ b/script/uninstall.sh
@@ -1,158 +1,121 @@
11 #!/usr/bin/env sh
22 set -eu
33 
4-# Uninstalls Zed that was installed using the install.sh script
5-
6-check_remaining_installations() {
7-    platform="$(uname -s)"
8-    if [ "$platform" = "Darwin" ]; then
9-        # Check for any Zed variants in /Applications
10-        remaining=$(ls -d /Applications/Zed*.app 2>/dev/null | wc -l)
11-        [ "$remaining" -eq 0 ]
12-    else
13-        # Check for any Zed variants in ~/.local
14-        remaining=$(ls -d "$HOME/.local/zed"*.app 2>/dev/null | wc -l)
15-        [ "$remaining" -eq 0 ]
16-    fi
17-}
18-
19-prompt_remove_preferences() {
20-    printf "Do you want to keep your Zed preferences? [Y/n] "
21-    read -r response
22-    case "$response" in
23-        [nN]|[nN][oO])
24-            rm -rf "$HOME/.config/zed"
25-            echo "Preferences removed."
26-            ;;
27-        *)
28-            echo "Preferences kept."
29-            ;;
30-    esac
4+# Uninstalls Omega. OMEGA-DELTA-0036.
5+#
6+# This script knows no paths of its own. Every path it removes is handed to it
7+# by the caller in OMEGA_UNINSTALL_PATHS, one absolute path per line, and that
8+# list is derived on the Rust side from the same `paths::` functions the running
9+# application uses to write those directories in the first place. See
10+# `crates/cli/src/uninstall.rs`.
11+#
12+# It is written this way because of what it used to be. Up to 0.2.0-rc14 this
13+# file was upstream's uninstaller, unchanged: it deleted the upstream editor's
14+# application bundle, application-support tree, logs, preferences and remote
15+# server directory, announced that the upstream editor had been uninstalled, and
16+# removed no Omega path at all. A command advertised as "Uninstall Omega" kept
17+# Omega and destroyed the user's other editor. A hard-coded path table is how
18+# that happened, and a second hard-coded path table is not the fix, so there is
19+# no path table here.
20+#
21+# Contract:
22+#
23+#   OMEGA_UNINSTALL_PRODUCT     display name, e.g. "Omega RC"          required
24+#   OMEGA_UNINSTALL_PATHS       newline-separated absolute paths       required
25+#   OMEGA_UNINSTALL_CONFIG_DIR  removed only if the user says so       optional
26+#   OMEGA_UNINSTALL_DRY_RUN     when "1", print the plan, remove nothing
27+#   OMEGA_UNINSTALL_ASSUME_YES  when "1", remove the config directory unasked
28+#
29+# Refusing is the safe direction: an unset or empty contract exits non-zero
30+# rather than falling back to a default, because every default this file has
31+# ever had belonged to somebody else's product.
32+
33+refuse() {
34+    echo "omega --uninstall: $1" >&2
35+    exit 1
3136 }
3237 
33-main() {
34-    platform="$(uname -s)"
35-    channel="${ZED_CHANNEL:-stable}"
38+product="${OMEGA_UNINSTALL_PRODUCT:-}"
39+paths="${OMEGA_UNINSTALL_PATHS:-}"
40+config_dir="${OMEGA_UNINSTALL_CONFIG_DIR:-}"
41+dry_run="${OMEGA_UNINSTALL_DRY_RUN:-}"
42+assume_yes="${OMEGA_UNINSTALL_ASSUME_YES:-}"
3643 
37-    if [ "$platform" = "Darwin" ]; then
38-        platform="macos"
39-    elif [ "$platform" = "Linux" ]; then
40-        platform="linux"
41-    else
42-        echo "Unsupported platform $platform"
43-        exit 1
44-    fi
44+[ -n "$product" ] || refuse "OMEGA_UNINSTALL_PRODUCT is not set; refusing to guess what to remove"
45+[ -n "$paths" ] || refuse "OMEGA_UNINSTALL_PATHS is empty; refusing to guess what to remove"
4546 
46-    "$platform"
47-
48-    echo "Zed has been uninstalled"
49-}
50-
51-linux() {
52-    suffix=""
53-    if [ "$channel" != "stable" ]; then
54-        suffix="-$channel"
55-    fi
56-
57-    appid=""
58-    db_suffix="stable"
59-    case "$channel" in
60-      stable)
61-        appid="dev.zed.Zed"
62-        db_suffix="stable"
63-        ;;
64-      nightly)
65-        appid="dev.zed.Zed-Nightly"
66-        db_suffix="nightly"
67-        ;;
68-      preview)
69-        appid="dev.zed.Zed-Preview"
70-        db_suffix="preview"
71-        ;;
72-      dev)
73-        appid="dev.zed.Zed-Dev"
74-        db_suffix="dev"
75-        ;;
76-      *)
77-        echo "Unknown release channel: ${channel}. Using stable app ID."
78-        appid="dev.zed.Zed"
79-        db_suffix="stable"
80-        ;;
47+check_path() {
48+    case "$1" in
49+        /*) ;;
50+        *) refuse "refusing to remove the relative path '$1'" ;;
8151     esac
82-
83-    # Remove the app directory
84-    rm -rf "$HOME/.local/zed$suffix.app"
85-
86-    # Remove the binary symlink
87-    rm -f "$HOME/.local/bin/zed"
88-
89-    # Remove the .desktop file
90-    rm -f "$HOME/.local/share/applications/${appid}.desktop"
91-
92-    # Remove the database directory for this channel
93-    rm -rf "$HOME/.local/share/zed/db/0-$db_suffix"
94-
95-    # Remove socket file
96-    rm -f "$HOME/.local/share/zed/zed-$db_suffix.sock"
97-
98-    # Remove the entire Zed directory if no installations remain
99-    if check_remaining_installations; then
100-        rm -rf "$HOME/.local/share/zed"
101-        prompt_remove_preferences
102-    fi
103-
104-    rm -rf $HOME/.zed_server
105-}
106-
107-macos() {
108-    app="Zed.app"
109-    db_suffix="stable"
110-    app_id="dev.zed.Zed"
111-    case "$channel" in
112-      nightly)
113-        app="Zed Nightly.app"
114-        db_suffix="nightly"
115-        app_id="dev.zed.Zed-Nightly"
116-        ;;
117-      preview)
118-        app="Zed Preview.app"
119-        db_suffix="preview"
120-        app_id="dev.zed.Zed-Preview"
121-        ;;
122-      dev)
123-        app="Zed Dev.app"
124-        db_suffix="dev"
125-        app_id="dev.zed.Zed-Dev"
126-        ;;
52+    case "$1" in
53+        /|/Applications|/Users|/System|/Library|/usr|/bin|/etc|/var|/tmp|"$HOME")
54+            refuse "refusing to remove '$1'"
55+            ;;
12756     esac
57+}
12858 
129-    # Remove the app bundle
130-    if [ -d "/Applications/$app" ]; then
131-        rm -rf "/Applications/$app"
59+# Everything is checked before anything is removed. A here-document keeps the
60+# loop in this shell, so a refusal here really does stop the run.
61+while IFS= read -r path; do
62+    [ -n "$path" ] || continue
63+    check_path "$path"
64+done <<PLAN
65+$paths
66+PLAN
67+
68+if [ -n "$config_dir" ]; then
69+    check_path "$config_dir"
70+fi
71+
72+if [ "$dry_run" = "1" ]; then
73+    echo "plan: $product"
74+    while IFS= read -r path; do
75+        [ -n "$path" ] || continue
76+        echo "remove: $path"
77+    done <<PLAN
78+$paths
79+PLAN
80+    if [ -n "$config_dir" ]; then
81+        echo "prompt: $config_dir"
13282     fi
133-
134-    # Remove the binary symlink
135-    rm -f "$HOME/.local/bin/zed"
136-
137-    # Remove the database directory for this channel
138-    rm -rf "$HOME/Library/Application Support/Zed/db/0-$db_suffix"
139-
140-    # Remove app-specific files and directories
141-    rm -rf "$HOME/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/$app_id.sfl"*
142-    rm -rf "$HOME/Library/Caches/$app_id"
143-    rm -rf "$HOME/Library/HTTPStorages/$app_id"
144-    rm -rf "$HOME/Library/Preferences/$app_id.plist"
145-    rm -rf "$HOME/Library/Saved Application State/$app_id.savedState"
146-
147-    # Remove the entire Zed directory if no installations remain
148-    if check_remaining_installations; then
149-        rm -rf "$HOME/Library/Application Support/Zed"
150-        rm -rf "$HOME/Library/Logs/Zed"
151-
152-        prompt_remove_preferences
83+    echo "$product uninstall plan printed; nothing was removed"
84+    exit 0
85+fi
86+
87+while IFS= read -r path; do
88+    [ -n "$path" ] || continue
89+    if [ -e "$path" ] || [ -L "$path" ]; then
90+        rm -rf "$path"
91+        echo "removed $path"
92+    else
93+        echo "absent $path"
15394     fi
95+done <<PLAN
96+$paths
97+PLAN
98+
99+if [ -n "$config_dir" ] && { [ -e "$config_dir" ] || [ -L "$config_dir" ]; }; then
100+    if [ "$assume_yes" = "1" ]; then
101+        rm -rf "$config_dir"
102+        echo "removed $config_dir"
103+    elif [ -t 0 ]; then
104+        printf 'Do you want to keep your %s settings and keymap (%s)? [Y/n] ' "$product" "$config_dir"
105+        response=""
106+        read -r response || response=""
107+        case "$response" in
108+            [nN]|[nN][oO])
109+                rm -rf "$config_dir"
110+                echo "removed $config_dir"
111+                ;;
112+            *)
113+                echo "kept $config_dir"
114+                ;;
115+        esac
116+    else
117+        echo "kept $config_dir"
118+    fi
119+fi
154120 
155-    rm -rf $HOME/.zed_server
156-}
157-
158-main "$@"
121+echo "$product has been uninstalled"
diff --git a/script/verify-omega-brand b/script/verify-omega-brand
index a8f86e2e1e..6e95a55a34 100755
--- a/script/verify-omega-brand
+++ b/script/verify-omega-brand
@@ -457,7 +457,28 @@ def check_action_labels() -> None:
457457 
458458 PROSE_CLASSIFIED = PROSE["classified"]
459459 PROSE_CORPUS = set(PROSE["corpus_files"])
460-DOC_LINE = re.compile(r"^\s*(?:///|//!)(.*)$")
460+SUGAR_DOC_LINE = re.compile(r"^\s*(?:///|//!)(.*)$")
461+# `#[doc = "..."]` and `#[cfg_attr(<predicate>, doc = "...")]` are doc comments
462+# written the long way, and clap and schemars read them exactly the same. The
463+# gate did not, which is why `~/Library/Application Support/Zed` was printed as
464+# the data directory by `cli --help` in every candidate up to 0.2.0-rc14 while
465+# this file reported green -- the line is a `cfg_attr`, and nothing here had
466+# ever looked inside one (omega#89).
467+# Either `#[... doc = "..."]` on one line, or the `doc = "..."` line of an
468+# attribute written across several. Anchoring on the line start keeps a Rust
469+# `let doc = "..."` binding out.
470+ATTR_DOC_LINE = re.compile(r'^\s*(?:#\[.*?)?\bdoc\s*=\s*"((?:[^"\\]|\\.)*)"')
471+
472+
473+def doc_line_body(line: str) -> str | None:
474+    """The text of a doc comment, however it was spelled."""
475+    match = SUGAR_DOC_LINE.match(line)
476+    if match is not None:
477+        return match.group(1)
478+    match = ATTR_DOC_LINE.match(line)
479+    if match is not None:
480+        return match.group(1)
481+    return None
461482 DERIVES_SCHEMA = re.compile(r"#\[derive\([^)]*JsonSchema")
462483 DERIVES_CLAP = re.compile(r"#\[derive\([^)]*(?:Parser|Args|Subcommand)|#\[command\(|clap::Parser")
463484 PLAIN_WORD = re.compile(r"[A-Za-z']+")
@@ -619,8 +640,11 @@ def action_doc_lines(text: str) -> set[int]:
619640         if "#[action(" not in line:
620641             continue
621642         cursor = index - 1
622-        while cursor >= 0 and (DOC_LINE.match(lines[cursor]) or lines[cursor].strip().startswith("#[")):
623-            if DOC_LINE.match(lines[cursor]):
643+        while cursor >= 0 and (
644+            doc_line_body(lines[cursor]) is not None
645+            or lines[cursor].strip().startswith("#[")
646+        ):
647+            if doc_line_body(lines[cursor]) is not None:
624648                 out.add(cursor + 1)
625649             cursor -= 1
626650     return out
@@ -642,12 +666,12 @@ def prose_inventory() -> tuple[tuple[tuple[str, str, int, str], ...], dict[str,
642666         literals = rust_string_literals(text)
643667         read["literals"] += len(literals)
644668         lines = text.splitlines()
645-        code = "\n".join("" if DOC_LINE.match(line) else line for line in lines)
669+        code = "\n".join("" if doc_line_body(line) is not None else line for line in lines)
646670         schema = bool(DERIVES_SCHEMA.search(code))
647671         clap = bool(DERIVES_CLAP.search(code))
648672         actions = action_doc_lines(text)
649673         for number, line in enumerate(lines, 1):
650-            if not DOC_LINE.match(line):
674+            if doc_line_body(line) is None:
651675                 continue
652676             if number in actions:
653677                 read["action_docs"] += 1
@@ -667,8 +691,8 @@ def prose_inventory() -> tuple[tuple[tuple[str, str, int, str], ...], dict[str,
667691         for number, line in enumerate(lines, 1):
668692             if is_test_file or number in skipped:
669693                 continue
670-            doc = DOC_LINE.match(line)
671-            if doc is None or not brand_hits(doc.group(1)) or not is_prose(doc.group(1)):
694+            doc = doc_line_body(line)
695+            if doc is None or not brand_hits(doc) or not is_prose(doc):
672696                 continue
673697             if number in actions:
674698                 kind = "action_doc"
@@ -678,7 +702,7 @@ def prose_inventory() -> tuple[tuple[tuple[str, str, int, str], ...], dict[str,
678702                 kind = "clap_doc"
679703             else:
680704                 continue
681-            items.append((kind, relative, number, normalize_prose(doc.group(1))))
705+            items.append((kind, relative, number, normalize_prose(doc)))
682706 
683707     for relative in embedded_asset_inventory():
684708         read["embedded"] += 1
@@ -780,6 +804,293 @@ def check_packaged_info_plist(app: Path) -> None:
780804                 )
781805 
782806 
807+# ---------------------------------- OMEGA-DELTA-0038, every shipped executable
808+
809+
810+MACHO_MAGIC = (
811+    b"\xcf\xfa\xed\xfe",  # 64-bit little-endian
812+    b"\xce\xfa\xed\xfe",  # 32-bit little-endian
813+    b"\xca\xfe\xba\xbe",  # universal
814+    b"\xbe\xba\xfe\xca",  # universal, byte-swapped
815+)
816+
817+
818+@functools.lru_cache(maxsize=None)
819+def bundle_executables(app: Path) -> tuple[Path, ...]:
820+    """Every Mach-O executable the bundle ships, found by walking it.
821+
822+    Not a list of binary names. `script/bundle-omega-rc` copies and signs
823+    `cli` and `omega-identity-proof` into `Contents/MacOS` beside `omega`, and
824+    every packaged check here used to open `Contents/MacOS/omega` and nothing
825+    else. That is why `cli` shipped an uninstaller that deleted a competitor's
826+    application and none of ours through two published prereleases (omega#88),
827+    with the gate reporting green about the bundle: the defect was never in a
828+    file any check opened.
829+
830+    A helper added to the bundle tomorrow is inside the gate the day it is
831+    added, the same way rust-embed's `#[folder]` declarations made the asset
832+    inventory derived rather than remembered.
833+    """
834+    found: list[Path] = []
835+    for path in sorted(app.rglob("*")):
836+        if path.is_symlink() or not path.is_file():
837+            continue
838+        try:
839+            head = path.open("rb").read(4)
840+        except OSError:
841+            continue
842+        if head in MACHO_MAGIC:
843+            found.append(path)
844+    return tuple(found)
845+
846+
847+def main_binary(app: Path) -> Path:
848+    return app / "Contents/MacOS/omega"
849+
850+
851+def check_packaged_executable_inventory(app: Path) -> None:
852+    """Anti-vacuity for the walk above, and for the bundle it walked."""
853+    executables = bundle_executables(app)
854+    if not executables:
855+        fail(
856+            "OMEGA-DELTA-0038: no Mach-O executable was found anywhere in "
857+            f"{app}. The walk broke, and every packaged check below is "
858+            "reporting green about nothing."
859+        )
860+        return
861+    if main_binary(app) not in executables:
862+        fail(
863+            f"OMEGA-DELTA-0038: {main_binary(app)} is not among the "
864+            "executables found in the bundle"
865+        )
866+    minimum = POLICY.get("packaged", {}).get("minimum_executables", 2)
867+    if len(executables) < minimum:
868+        fail(
869+            f"OMEGA-DELTA-0038: only {len(executables)} executables were found "
870+            f"in the bundle, below the {minimum} floor. A bundle that ships a "
871+            "companion binary and a scan that sees one binary is exactly the "
872+            "shape omega#88 shipped in."
873+        )
874+
875+
876+def printable_runs(data: bytes, minimum: int = 8):
877+    """Every printable ASCII run in a binary, as (start offset, text)."""
878+    for run in re.finditer(rb"[\x20-\x7e]{%d,}" % minimum, data):
879+        yield run.start(), run.group(0).decode("ascii")
880+
881+
882+def check_packaged_first_party_agent(app: Path) -> None:
883+    """OMEGA-DELTA-0024, packaged side.
884+
885+    `first_party_agent.phrases` had been applied to nothing by either half of
886+    this gate since it was written: `grep -c first_party_agent
887+    script/verify-omega-brand` returned 0, and the only reader was a Rust test
888+    over the source tree. A reviewer had to run it by hand against a package
889+    every time, which is the same as not having it (omega#89).
890+    """
891+    agent = POLICY["first_party_agent"]
892+    phrases = [phrase.lower() for phrase in agent["phrases"]]
893+    symbols = agent.get("symbols", [])
894+    if not phrases:
895+        fail("OMEGA-DELTA-0024: first_party_agent.phrases is empty; check vacuous")
896+        return
897+    identity = agent["identity"]
898+    carried = False
899+    for binary in bundle_executables(app):
900+        data = binary.read_bytes()
901+        if identity.encode() in data:
902+            carried = True
903+        for _offset, blob in printable_runs(data):
904+            lowered = blob.lower()
905+            for phrase in phrases:
906+                if phrase in lowered:
907+                    at = lowered.index(phrase)
908+                    window = blob[max(0, at - 60) : at + len(phrase) + 60]
909+                    fail(
910+                        f"OMEGA-DELTA-0024: packaged "
911+                        f"{binary.relative_to(app.parent)} presents somebody "
912+                        f"else's agent as the first-party one: ...{window}..."
913+                    )
914+        for symbol in symbols:
915+            if symbol.encode() in data:
916+                fail(
917+                    f"OMEGA-DELTA-0024: packaged "
918+                    f"{binary.relative_to(app.parent)} still carries the "
919+                    f"symbol {symbol!r}"
920+                )
921+    if not carried:
922+        fail(
923+            f"OMEGA-DELTA-0024: no packaged executable carries {identity!r}, so "
924+            "this scan found nothing because it read nothing"
925+        )
926+
927+
928+# ------------------------------- OMEGA-DELTA-0038, help as the user reads it
929+
930+
931+HELP_SUBCOMMAND = re.compile(r"^\s{2,}([a-z][a-z0-9-]*)(?:\s|$)")
932+
933+
934+def rendered_output(binary: Path, arguments: list[str]) -> str | None:
935+    """Run the shipped binary and return what it printed, or None if it could
936+    not be run at all."""
937+    import subprocess
938+
939+    try:
940+        completed = subprocess.run(
941+            [str(binary), *arguments],
942+            capture_output=True,
943+            timeout=120,
944+            check=False,
945+            cwd="/",
946+            env={"PATH": "/usr/bin:/bin", "HOME": "/nonexistent-omega-brand-gate"},
947+        )
948+    except (OSError, subprocess.SubprocessError):
949+        return None
950+    return (completed.stdout + completed.stderr).decode("utf-8", errors="replace")
951+
952+
953+def help_subcommands(text: str) -> list[str]:
954+    """Subcommand names out of a clap `Commands:` block."""
955+    names: list[str] = []
956+    inside = False
957+    for line in text.splitlines():
958+        if line.rstrip().endswith(":"):
959+            inside = line.strip() in ("Commands:", "Subcommands:")
960+            continue
961+        if not inside:
962+            continue
963+        if not line.strip():
964+            continue
965+        match = HELP_SUBCOMMAND.match(line)
966+        if match and match.group(1) not in ("help",):
967+            names.append(match.group(1))
968+    return names
969+
970+
971+def help_units(text: str) -> list[str]:
972+    """What a person reads, as they read it: whole descriptions, not lines.
973+
974+    clap wraps a description to the terminal width, so the sentence the user
975+    sees arrives split across several lines. Reading line by line hid exactly
976+    the defect this gate was written for: `~/Library/Application Support/Zed`
977+    lands as `... location: \`~/Library/Application` on one line and
978+    `Support/Zed\`` on the next, and neither fragment is three words. Blocks
979+    are joined back before anything is judged, and the individual lines are
980+    kept too, so a short label is still read on its own.
981+    """
982+    units: list[str] = []
983+    block: list[str] = []
984+    for line in text.splitlines():
985+        stripped = line.strip()
986+        if stripped:
987+            units.append(stripped)
988+            block.append(stripped)
989+            continue
990+        if block:
991+            units.append(" ".join(block))
992+            block = []
993+    if block:
994+        units.append(" ".join(block))
995+    return units
996+
997+
998+def check_rendered_help(app: Path) -> None:
999+    """OMEGA-DELTA-0038. The `--help` a person actually sees.
1000+
1001+    Every other stream in OMEGA-DELTA-0031 reads *source*: the doc line, the
1002+    literal, the schema comment. clap does not print source. It joins several
1003+    doc lines into one sentence, resolves `cfg_attr` for the platform it was
1004+    built for, prints the flag *name* beside the description, and lays the
1005+    whole thing out at run time. Nothing had ever read that output, which is
1006+    how `--zed <ZED>`, `Run zed in the foreground` and a `--user-data-dir`
1007+    line naming the wrong product's data directory all shipped in
1008+    `0.2.0-rc14` under a green gate (omega#89).
1009+
1010+    This runs the binaries that ship, with `--help`, and reads what comes back.
1011+    Subcommands are enumerated from the output rather than listed, so a
1012+    subcommand added tomorrow is read the day it is added.
1013+    """
1014+    executables = bundle_executables(app)
1015+    if not executables:
1016+        return
1017+    rendered = 0
1018+    for binary in executables:
1019+        pending: list[list[str]] = [["--help"], ["--version"]]
1020+        seen: set[tuple[str, ...]] = set()
1021+        while pending:
1022+            arguments = pending.pop(0)
1023+            key = tuple(arguments)
1024+            if key in seen:
1025+                continue
1026+            seen.add(key)
1027+            text = rendered_output(binary, arguments)
1028+            if text is None:
1029+                continue
1030+            rendered += 1
1031+            if arguments[-1] == "--help":
1032+                for name in help_subcommands(text):
1033+                    if len(arguments) < 4:
1034+                        pending.append([*arguments[:-1], name, "--help"])
1035+            where = f"{binary.relative_to(app.parent)} {' '.join(arguments)}"
1036+
1037+            # A flag name, a value placeholder and a subcommand are labels, not
1038+            # sentences, so the prose rule below cannot see them -- `--zed
1039+            # <ZED>` is two tokens. They are read here on their own terms, the
1040+            # way an action namespace is: the palette shows `omega: about`, and
1041+            # `--help` shows `--zed`, and both are copy the user reads.
1042+            for token in set(re.findall(r"--[A-Za-z0-9][A-Za-z0-9-]*", text)):
1043+                if brand_hits(token):
1044+                    fail(
1045+                        f"OMEGA-DELTA-0038: `{where}` offers the option "
1046+                        f"{token!r}, which names a competitor. An option name is "
1047+                        "a user-facing label; renaming the description and "
1048+                        "leaving the flag is half a rename."
1049+                    )
1050+            for token in set(re.findall(r"<([A-Za-z0-9_]+)>", text)):
1051+                if brand_hits(token) or brand_hits(token.capitalize()):
1052+                    fail(
1053+                        f"OMEGA-DELTA-0038: `{where}` names the value "
1054+                        f"placeholder <{token}>, which carries a competitor's "
1055+                        "name."
1056+                    )
1057+            if arguments[-1] == "--help":
1058+                for name in help_subcommands(text):
1059+                    if brand_hits(name):
1060+                        fail(
1061+                            f"OMEGA-DELTA-0038: `{where}` offers the subcommand "
1062+                            f"{name!r}, which names a competitor."
1063+                        )
1064+
1065+            for stripped in help_units(text):
1066+                if not brand_hits(stripped):
1067+                    continue
1068+                normalized = normalize_prose(stripped)
1069+                if normalized in PROSE_CLASSIFIED:
1070+                    continue
1071+                if any(
1072+                    fragment in normalized
1073+                    for fragment in packaged_covering_fragments()
1074+                ):
1075+                    continue
1076+                if not is_prose(normalized):
1077+                    continue
1078+                fail(
1079+                    f"OMEGA-DELTA-0038: `{where}` prints a line naming a "
1080+                    f"competitor: {normalized!r}. This is the sentence the user "
1081+                    "reads; clap builds it at run time out of doc lines the "
1082+                    "source scan sees separately, so fixing it means fixing the "
1083+                    "text, not the check."
1084+                )
1085+    minimum = POLICY.get("packaged", {}).get("minimum_rendered_invocations", 4)
1086+    if rendered < minimum:
1087+        fail(
1088+            f"OMEGA-DELTA-0038: only {rendered} rendered outputs were read, "
1089+            f"below the {minimum} floor. The binaries did not run, and a gate "
1090+            "that reads nothing reports green about nothing."
1091+        )
1092+
1093+
7831094 def check_packaged_icons(app: Path) -> None:
7841095     """OMEGA-DELTA-0018, packaged side.
7851096 
@@ -787,22 +1098,33 @@ def check_packaged_icons(app: Path) -> None:
7871098     has no assets directory. Both the asset path literals and the raw SVG
7881099     bytes survive in the binary, so this checks the names AND the artwork.
7891100     """
790-    binary = app / "Contents/MacOS/omega"
1101+    binary = main_binary(app)
7911102     if not binary.is_file():
7921103         fail(f"packaged Omega binary is missing: {binary}")
7931104         return
7941105     data = binary.read_bytes()
7951106 
796-    embedded = sorted({match.decode() for match in re.findall(rb"icons/[A-Za-z0-9_]+\.svg", data)})
797-    if not embedded:
1107+    # The name rule runs over every executable in the bundle; the artwork pin
1108+    # runs over the one that embeds the assets.
1109+    embedded_anywhere: set[str] = set()
1110+    for executable in bundle_executables(app):
1111+        blob = executable.read_bytes()
1112+        embedded = sorted(
1113+            {match.decode() for match in re.findall(rb"icons/[A-Za-z0-9_]+\.svg", blob)}
1114+        )
1115+        embedded_anywhere.update(embedded)
1116+        for relative in embedded:
1117+            stem = relative[len("icons/") : -len(".svg")]
1118+            if stem_is_forbidden(stem):
1119+                fail(
1120+                    f"OMEGA-DELTA-0018: packaged "
1121+                    f"{executable.relative_to(app.parent)} embeds {relative}"
1122+                )
1123+    if not embedded_anywhere:
7981124         fail(
799-            "no embedded icon asset paths were found in the packaged binary; "
1125+            "no embedded icon asset paths were found in any packaged binary; "
8001126             "this check would be vacuous"
8011127         )
802-    for relative in embedded:
803-        stem = relative[len("icons/") : -len(".svg")]
804-        if stem_is_forbidden(stem):
805-            fail(f"OMEGA-DELTA-0018: packaged Omega embeds {relative}")
8061128 
8071129     for relative, expected in sorted(REVIEWED_MARKS.items()):
8081130         source = ROOT / relative
@@ -830,7 +1152,7 @@ def check_packaged_assets_and_actions(app: Path) -> None:
8301152     listed, so a directory added tomorrow is read the same way assets/images/
8311153     now is -- it was the missing directory in rc11, not a missing rule.
8321154     """
833-    binary = app / "Contents/MacOS/omega"
1155+    binary = main_binary(app)
8341156     if not binary.is_file():
8351157         fail(f"packaged Omega binary is missing: {binary}")
8361158         return
@@ -861,10 +1183,15 @@ def check_packaged_assets_and_actions(app: Path) -> None:
8611183         )
8621184     token_group = "|".join(re.escape(token) for token in tokens)
8631185     pattern = rf"(?:{directory_group})/[\w./-]*(?:{token_group})[\w./-]*".encode()
864-    for match in sorted({found.decode() for found in re.findall(pattern, data)}):
865-        if any(match.startswith(entry) for entry in allowed):
866-            continue
867-        fail(f"OMEGA-DELTA-0022: packaged Omega embeds {match}")
1186+    for executable in bundle_executables(app):
1187+        blob = executable.read_bytes()
1188+        for match in sorted({found.decode() for found in re.findall(pattern, blob)}):
1189+            if any(match.startswith(entry) for entry in allowed):
1190+                continue
1191+            fail(
1192+                f"OMEGA-DELTA-0022: packaged "
1193+                f"{executable.relative_to(app.parent)} embeds {match}"
1194+            )
8681195 
8691196     # Action labels, checked by presence of the current ones.
8701197     #
@@ -891,6 +1218,7 @@ def check_packaged_assets_and_actions(app: Path) -> None:
8911218 
8921219 
8931220 
1221+@functools.lru_cache(maxsize=None)
8941222 def packaged_covering_fragments() -> list[str]:
8951223     """Classified prose as it survives compilation, normalized.
8961224 
@@ -911,7 +1239,12 @@ def packaged_covering_fragments() -> list[str]:
9111239     fragments: list[str] = []
9121240     for text in PROSE_CLASSIFIED:
9131241         joined = text.replace("\\ ", " ")
914-        for part in re.split(r"\\[nrt]|[^\x20-\x7e]+", joined):
1242+        # A `{}` placeholder is not in the binary either: rustc splits a format
1243+        # string at every placeholder and stores the pieces, so a classified
1244+        # sentence written with one could never cover its own occurrence in the
1245+        # string table. Splitting there is conservative in the same direction as
1246+        # the rules above -- shorter fragments cover less, never more.
1247+        for part in re.split(r"\\[nrt]|\{[^{}]*\}|[^\x20-\x7e]+", joined):
9151248             part = normalize_prose(part)
9161249             if part and brand_hits(part):
9171250                 fragments.append(part)
@@ -952,26 +1285,56 @@ def check_packaged_prose(app: Path) -> None:
9521285     reports one only when the brand sits in running text AND no classified
9531286     sentence spans that position.
9541287     """
955-    binary = app / "Contents/MacOS/omega"
1288+    binary = main_binary(app)
9561289     if not binary.is_file():
9571290         fail(f"packaged Omega binary is missing: {binary}")
9581291         return
959-    data = binary.read_bytes()
9601292 
9611293     covering = packaged_covering_fragments()
9621294     if not covering:
9631295         fail("OMEGA-DELTA-0031: no classified prose carries a brand; check vacuous")
9641296         return
9651297 
966-    # A brand occurrence only counts as prose when it is written as a word in
967-    # running text. `/Zed/` in a path list, `ENV=Zed` in a task-template
968-    # placeholder and `X-Zed-Predict-Edits-Mode` in an HTTP header are all
969-    # neighbours of real sentences in a stripped string table, and reporting
970-    # them is how a gate earns the reputation that gets it deleted.
971-    before_ok = set(" (\"'`\t*[|>\u2014-")
972-    after_ok = set(" .,:;!?)]}'\"`<>/\u2014\u2026\t-")
973-
9741298     scanned, offenders = 0, set()
1299+    main_scanned = 0
1300+    for executable in bundle_executables(app):
1301+        data = executable.read_bytes()
1302+        before = scanned
1303+        scanned, found = scan_binary_prose(data, covering, scanned)
1304+        if executable == binary:
1305+            main_scanned = scanned - before
1306+        for window in found:
1307+            offenders.add((str(executable.relative_to(app.parent)), window))
1308+    if scanned == 0 or main_scanned == 0:
1309+        fail(
1310+            "OMEGA-DELTA-0031: no brand-bearing prose was found in the packaged "
1311+            "binaries at all, not even the classified third-party references. "
1312+            "The string scan broke; a clean package still carries those."
1313+        )
1314+    for where, window in sorted(offenders):
1315+        fail(
1316+            f"OMEGA-DELTA-0031: packaged {where} carries unclassified prose: "
1317+            f"...{window}... The package was built from a tree that still "
1318+            "presents a competitor as the product, whatever the source scan "
1319+            "above says about this checkout."
1320+        )
1321+
1322+
1323+# A brand occurrence only counts as prose when it is written as a word in
1324+# running text. `/Zed/` in a path list, `ENV=Zed` in a task-template
1325+# placeholder and `X-Zed-Predict-Edits-Mode` in an HTTP header are all
1326+# neighbours of real sentences in a stripped string table, and reporting them
1327+# is how a gate earns the reputation that gets it deleted.
1328+BEFORE_OK = set(" (\"'`\t*[|>\u2014-")
1329+AFTER_OK = set(" .,:;!?)]}'\"`<>/\u2014\u2026\t-")
1330+
1331+
1332+def scan_binary_prose(
1333+    data: bytes, covering: list[str], scanned: int
1334+) -> tuple[int, set[str]]:
1335+    """Brand occurrences in one executable's string table that no classified
1336+    sentence spans. Returns the running scanned count and the offenders."""
1337+    offenders: set[str] = set()
9751338     for run in re.finditer(rb"[\x20-\x7e]{8,}", data):
9761339         blob = run.group(0).decode("ascii")
9771340         if not any(word in blob for word in WORDS):
@@ -984,7 +1347,7 @@ def check_packaged_prose(app: Path) -> None:
9841347                 after = blob[end] if end < len(blob) else " "
9851348                 if before.isalnum() or after.isalnum():
9861349                     continue
987-                if before not in before_ok or after not in after_ok:
1350+                if before not in BEFORE_OK or after not in AFTER_OK:
9881351                     continue
9891352                 # A brand hyphenated on both sides is part of an identifier.
9901353                 if before == "-" and after == "-":
@@ -1009,19 +1372,7 @@ def check_packaged_prose(app: Path) -> None:
10091372                         break
10101373                 if not covered:
10111374                     offenders.add(normalize_prose(window))
1012-    if scanned == 0:
1013-        fail(
1014-            "OMEGA-DELTA-0031: no brand-bearing prose was found in the packaged "
1015-            "binary at all, not even the classified third-party references. The "
1016-            "string scan broke; a clean package still carries those."
1017-        )
1018-    for window in sorted(offenders):
1019-        fail(
1020-            f"OMEGA-DELTA-0031: packaged Omega carries unclassified prose: "
1021-            f"...{window}... The package was built from a tree that still "
1022-            "presents a competitor as the product, whatever the source scan "
1023-            "above says about this checkout."
1024-        )
1375+    return scanned, offenders
10251376 
10261377 
10271378 check_info_plist_fragments()
@@ -1034,10 +1385,13 @@ if APP is not None:
10341385     if not APP.is_dir():
10351386         fail(f"packaged application is missing: {APP}")
10361387     else:
1388+        check_packaged_executable_inventory(APP)
10371389         check_packaged_info_plist(APP)
10381390         check_packaged_icons(APP)
10391391         check_packaged_assets_and_actions(APP)
10401392         check_packaged_prose(APP)
1393+        check_packaged_first_party_agent(APP)
1394+        check_rendered_help(APP)
10411395 
10421396 if failures:
10431397     print("Omega brand gate FAILED:", file=sys.stderr)
Served at tenant.openagents/omega Member data and write actions are omitted.