Skip to repository content
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T00:58:50.408Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitRevision diff
e5b422b1 → 1f732301diff --git a/OMEGA_DELTAS.md b/OMEGA_DELTAS.md
index 357ddc9f18..7cb32ca55d 100644
--- a/OMEGA_DELTAS.md
+++ b/OMEGA_DELTAS.md
@@ -2175,3 +2175,141 @@ than it sounds, because the harness omega#81's acceptance sentence names —
21752175 Tier B's to fix. The lane reads Exo's durable event log after a turn and
21762176 currently only logs a failure to do so, because the turn has already run and
21772177 refusing it afterwards would describe a world that did not happen.
2178+
2179+### OMEGA-DELTA-0043 — `--uninstall` removes the installation, not one file inside it
2180+
2181+- **Upstream Zed:** `script/uninstall.sh` removes `/Applications/Zed.app` — the
2182+ bundle — along with the user-level directories. Correct for Zed.
2183+- **Omega, before this:** `OMEGA-DELTA-0036` moved the path table out of the
2184+ shell script and into `UninstallRoots`, built from the `paths::` functions
2185+ that write those directories, and that fix is real: the plan derived by the
2186+ signed `0.2.0-rc16` holds nine Omega paths and zero belonging to anybody else.
2187+ But `crates/cli/src/main.rs` handed it `app.path()`, which on macOS is
2188+ `Omega.app/Contents/MacOS/omega` — **one executable inside the bundle** — into
2189+ a field whose doc comment read "the application bundle **or executable** this
2190+ CLI belongs to". So a full uninstall run against the signed `0.2.0-rc16`
2191+ reported success and left `/Applications/Omega.app` standing with **130.9 MB
2192+ and five executables** in it: `cli`, which still carries `--uninstall`,
2193+ `omega-identity-proof`, `omega-effectd` and its **bundled Node runtime**. Zed
2194+ was safe and Omega was not removed; the user was left to drag a signed,
2195+ non-functional bundle out of `/Applications` by hand (omega#92).
2196+- **Why nothing caught it.** The end-to-end test planted the app root as a
2197+ directory holding a single marker file, and asserted the root was gone.
2198+ "Removed the bundle" and "removed the one file the plan named" are the same
2199+ observation from a root like that. And `InstalledApp` had a single `path()`
2200+ answering two different questions, shadowed on macOS by an inherent `path()`
2201+ that returned the bundle — so the same call spelled two ways returned two
2202+ different paths, and the uninstaller took the wrong one.
2203+- **Omega now:** `InstalledApp` has two methods with two names.
2204+ `path()` is the executable to run; `installation_root()` is the whole
2205+ directory a removal has to remove, and on macOS it is the `.app`. The
2206+ ambiguous inherent `path()` is gone, so there is one answer per question. The
2207+ field is `UninstallRoots::app_root`, and `from_installed_paths` runs whatever
2208+ it is given through `uninstall::installation_root`, which resolves any path
2209+ under a `.app` to the outermost `.app` — a caller holding an executable, which
2210+ every macOS caller does, cannot reintroduce the defect from outside. A loose
2211+ development build with no bundle above it is returned unchanged, because
2212+ inventing a root there would plan its parent directory for removal.
2213+- **Enforced by:** `the_uninstall_plan_names_the_installation_root` in
2214+ `crates/omega_deltas/` (the derivation exists, the constructor normalizes
2215+ through it, the call site asks for the installation, and the trait still
2216+ distinguishes the two), and by two tests in `crates/cli/src/uninstall.rs`:
2217+ `the_plan_names_the_bundle_root_and_never_an_executable_inside_it`, which
2218+ asserts both directions — the bundle is planned, and no file inside it stands
2219+ in for it — and the end-to-end
2220+ `the_script_removes_omega_and_leaves_the_other_editor_untouched`, whose
2221+ fabricated bundle now holds `omega`, `cli`, `omega-identity-proof`, the
2222+ `omega-effectd` bundle and its Node runtime, every one of which is read back
2223+ after the real script has run. The bundle path in that test is named
2224+ independently of the plan, so a plan that names the executable plants its
2225+ files somewhere else and fails rather than moving with the defect.
2226+- **Proved against the product, not only the tests.** The plan was derived by
2227+ running the built `cli --uninstall` with `OMEGA_UNINSTALL_DRY_RUN=1` under an
2228+ isolated `HOME` against a fabricated bundle, exactly as omega#92 recorded the
2229+ defect. Before: `remove: …/Omega.app/Contents/MacOS/omega`. After:
2230+ `remove: …/Omega.app`, with the other eight roots unchanged.
2231+- **Falsified.** Reverting the call site to `app.path()` reproduced the
2232+ executable in the product's own dry-run plan and failed the delta test.
2233+ Making `installation_root` return its argument unchanged failed the plan test
2234+ and left `cli` alive in the end-to-end test. Both edits were probed in the
2235+ file and in the rebuilt binary before their tests ran.
2236+- **Handled with the care this path has earned.** This is the code that
2237+ destroyed the owner's real Zed installation on 2026-07-25, when a
2238+ falsification restored the old script and one test did not override `HOME`.
2239+ Every manual exercise here ran with `OMEGA_UNINSTALL_DRY_RUN=1` and an
2240+ isolated `HOME`, against a fabricated bundle in a scratch directory. Nothing
2241+ was ever run against `/Applications`.
2242+
2243+### OMEGA-DELTA-0044 — A brand-bearing command form is user-facing text
2244+
2245+- **Upstream Zed:** the CLI's interactive open-behavior prompt shows
2246+ `zed --existing`, `zed --classic` and `zed <path>`. Correct for Zed.
2247+- **Omega, before this:** those three literals shipped verbatim in the signed
2248+ `cli` of `0.2.0-rc16`, inside a panel whose surrounding copy reads "Add to
2249+ existing **Omega** window", "You can change this later in **Omega**
2250+ settings" — naming our product twice and somebody else's binary three times.
2251+ `zed --existing` and `zed --classic` are not Omega commands at all
2252+ (omega#93).
2253+- **They are product claims by the gate's own rule.** `OMEGA-DELTA-0031` is
2254+ written around one test: substitute our own name, and if the sentence stays
2255+ true the brand was standing where our product's name belongs. "Add to existing
2256+ Omega window (`omega --existing`)" is true of Omega. All three are rewrites,
2257+ not classifications.
2258+- **Why the gate passed.** `is_prose` requires three tokens, so a two-token
2259+ command form is invisible to it. That blind spot was *known* — the
2260+ `0.2.0-rc16` release notes record "does not classify one- and two-word
2261+ labels" — and this is it biting: the three literals were in neither
2262+ `prose.classified` nor the compatibility allow-list, and
2263+ `verify-omega-brand --app` exited 0.
2264+- **Omega now:** the prompt builds every command form from
2265+ `paths::BINARY_NAME`, so it cannot drift from the binary it describes. And
2266+ the blind spot is **narrowed rather than papered over**: the inventory admits
2267+ prose *or* a command form — a brand word standing in `argv[0]` followed by
2268+ flags, placeholders or paths — in all three streams (Rust literals, doc lines,
2269+ embedded assets), on both the Python and the Rust side. Adding the three
2270+ strings to a denylist instead would have reproduced exactly the defect the
2271+ inverted inventory was built to end.
2272+- **The width was measured, not asserted.** Across the whole tree the
2273+ command-form shape adds **3** items — the three defects, and nothing else.
2274+ Admitting any two-token literal beginning with a brand word instead adds
2275+ **11** (8 more: `Zed Pro`, `Zed Agent`, `Zed AI`, `Zed Repository`,
2276+ `Zed Twitter`, `Zed Default`, and `Zed (Default)` twice), all genuine
2277+ references to somebody else's product that would need classifying. Admitting a
2278+ bare brand token adds **58**. A gate that cries wolf gets deleted, which is
2279+ how a known blind spot stays open; the shape that costs nothing is the one
2280+ that can be carried.
2281+- **Enforced by:** `a_brand_bearing_command_form_is_user_facing_text` in
2282+ `crates/omega_deltas/` (the three shipped literals are invisible to `is_prose`
2283+ and visible to `is_command_form`; the eight two-word labels and a bare brand
2284+ token are not command forms; and `script/verify-omega-brand` carries the same
2285+ rule wired into the same three streams, so the source and packaged sides
2286+ cannot drift), and `the_cli_prompt_names_our_own_binary`, which reads
2287+ `prompt_open_behavior` and fails on any brand hit or on a command form not
2288+ built from `paths::BINARY_NAME`.
2289+- **Reachability, and what was actually observed.** `resolve_open_behavior` in
2290+ `crates/zed/src/zed/open_listener.rs` sends `CliResponse::PromptOpenBehavior`
2291+ when there are existing windows, the paths are not already in a workspace, at
2292+ least one path is a directory, and the settings file has no
2293+ `cli_default_open_behavior`. omega#93 recorded a *shipped-string* result,
2294+ because the reporting lane could not get the installed CLI to render the panel
2295+ with all four conditions held. This lane rendered it: the built `cli` was run
2296+ under a pty against a stand-in for the editor that performs the real IPC
2297+ handshake and sends `PromptOpenBehavior`, and the panel came up reading
2298+
2299+ ```
2300+ Configure default behavior for omega <path>
2301+ You can change this later in Omega settings:
2302+ > Add to existing Omega window (omega --existing)
2303+ Open a new window (omega --classic)
2304+ ```
2305+
2306+ So the CLI half is live code, not dead code, and the copy it renders is ours.
2307+ What that does **not** establish is that a running Omega reaches the four send
2308+ conditions in practice — the stand-in supplied the response. That half is
2309+ still unobserved, and the panel is still worth deleting if nobody can make a
2310+ real Omega send it.
2311+- **Falsified.** Restoring `zed --existing` failed both the prompt test and the
2312+ unclassified-prose test. Restoring the three-token rule alone — leaving the
2313+ literals fixed — failed the command-form test. Removing the rule from
2314+ `script/verify-omega-brand` while keeping it in Rust failed the parity
2315+ assertion. Each edit was probed in the file before its test ran.
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index 3dfdf0cdb5..4bcec5d347 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -43,7 +43,17 @@ trait InstalledApp {
4343 ipc_url: String,
4444 user_data_dir: Option<&str>,
4545 ) -> io::Result<ExitStatus>;
46+ /// The executable to run — one file.
4647 fn path(&self) -> PathBuf;
48+ /// The whole installation — the directory `--uninstall` has to remove.
49+ ///
50+ /// Distinct from `path` because on macOS they are never the same thing:
51+ /// `path` is `Omega.app/Contents/MacOS/omega`, and the installation is
52+ /// `Omega.app`, which also holds `cli`, `omega-identity-proof`, and a
53+ /// bundled Node runtime. `0.2.0-rc16` passed `path` to the uninstaller, so
54+ /// a full uninstall left 130.9 MB and five executables in `/Applications`,
55+ /// among them a `cli` that still carried `--uninstall` (omega#92).
56+ fn installation_root(&self) -> PathBuf;
4757 }
4858
4959 #[derive(Parser, Debug)]
@@ -581,8 +591,12 @@ fn run() -> Result<()> {
581591 use std::os::unix::fs::PermissionsExt as _;
582592 fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
583593
584- let plan = cli::uninstall::UninstallRoots::from_installed_paths(Some(app.path()))
585- .plan(paths::app_name());
594+ // `installation_root()`, never `path()`: on macOS the executable is one
595+ // file inside `Omega.app`, and removing it leaves the bundle, four more
596+ // executables and a bundled Node runtime behind (omega#92).
597+ let plan =
598+ cli::uninstall::UninstallRoots::from_installed_paths(Some(app.installation_root()))
599+ .plan(paths::app_name());
586600
587601 let status = std::process::Command::new("sh")
588602 .arg(&script_path)
@@ -863,17 +877,27 @@ fn prompt_open_behavior() -> Option<cli::CliBehaviorSetting> {
863877 }
864878
865879 let blue = console::Style::new().blue();
880+ // Every command form here is built from `paths::BINARY_NAME`, never
881+ // written out. The literals `zed --existing`, `zed --classic` and
882+ // `zed <path>` shipped in the signed `cli` of `0.2.0-rc16`, in a panel
883+ // whose surrounding copy said "Omega window" and "Omega settings", so it
884+ // named our product twice and somebody else's binary three times
885+ // (omega#93). A command form cannot drift from the binary it describes if
886+ // it is spelled by the binary's own name.
866887 let items = [
867888 format!(
868889 "Add to existing Omega window ({})",
869- blue.apply_to("zed --existing")
890+ blue.apply_to(format!("{} --existing", paths::BINARY_NAME))
891+ ),
892+ format!(
893+ "Open a new window ({})",
894+ blue.apply_to(format!("{} --classic", paths::BINARY_NAME))
870895 ),
871- format!("Open a new window ({})", blue.apply_to("zed --classic")),
872896 ];
873897
874898 let prompt = format!(
875899 "Configure default behavior for {}\n{}",
876- blue.apply_to("zed <path>"),
900+ blue.apply_to(format!("{} <path>", paths::BINARY_NAME)),
877901 console::style("You can change this later in Omega settings"),
878902 );
879903
@@ -991,6 +1015,14 @@ mod linux {
9911015 fn path(&self) -> PathBuf {
9921016 self.0.clone()
9931017 }
1018+
1019+ fn installation_root(&self) -> PathBuf {
1020+ // A Linux install is loose files under a prefix — `libexec`,
1021+ // `bin`, `share` — with no single directory that is only Omega, so
1022+ // the editor binary is the honest answer here. macOS is where the
1023+ // installation really is one directory.
1024+ self.0.clone()
1025+ }
9941026 }
9951027
9961028 impl App {
@@ -1256,6 +1288,11 @@ mod windows {
12561288 fn path(&self) -> PathBuf {
12571289 self.0.clone()
12581290 }
1291+
1292+ fn installation_root(&self) -> PathBuf {
1293+ // Windows has no `--uninstall`; the installer owns removal there.
1294+ self.0.clone()
1295+ }
12591296 }
12601297
12611298 impl Detect {
@@ -1371,7 +1408,15 @@ mod mac_os {
13711408
13721409 impl InstalledApp for Bundle {
13731410 fn app_version_string(&self) -> String {
1374- format!("Omega {} – {}", self.version(), self.path().display(),)
1411+ // The installation, which is what a person can point at in Finder.
1412+ // This used to read `self.path()`, resolved to an inherent method
1413+ // that returned the bundle while the trait method of the same name
1414+ // returned the executable. That ambiguity is what omega#92 was.
1415+ format!(
1416+ "Omega {} – {}",
1417+ self.version(),
1418+ self.installation_root().display(),
1419+ )
13751420 }
13761421
13771422 fn launch(&self, url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
@@ -1466,6 +1511,15 @@ mod mac_os {
14661511 Bundle::LocalPath { executable, .. } => executable.clone(),
14671512 }
14681513 }
1514+
1515+ fn installation_root(&self) -> PathBuf {
1516+ match self {
1517+ // The bundle, not the executable inside it. Everything Omega
1518+ // installs on macOS lives under this directory.
1519+ Bundle::App { app_bundle, .. } => app_bundle.clone(),
1520+ Bundle::LocalPath { executable, .. } => executable.clone(),
1521+ }
1522+ }
14691523 }
14701524
14711525 impl Bundle {
@@ -1475,13 +1529,6 @@ mod mac_os {
14751529 Self::LocalPath { .. } => "<development>".to_string(),
14761530 }
14771531 }
1478-
1479- fn path(&self) -> &Path {
1480- match self {
1481- Self::App { app_bundle, .. } => app_bundle,
1482- Self::LocalPath { executable, .. } => executable,
1483- }
1484- }
14851532 }
14861533
14871534 pub(super) fn spawn_channel_cli(
diff --git a/crates/cli/src/uninstall.rs b/crates/cli/src/uninstall.rs
index 55437afec4..e5772d63d0 100644
--- a/crates/cli/src/uninstall.rs
+++ b/crates/cli/src/uninstall.rs
@@ -16,6 +16,30 @@
1616
1717 use std::path::{Path, PathBuf};
1818
19+/// The installation root containing `path`.
20+///
21+/// On macOS an Omega installation is one directory — `Omega.app` — and every
22+/// path a caller has to hand names something *inside* it: the running
23+/// executable, the `cli` beside it, a resource. Removing what the caller
24+/// happened to be holding is not an uninstall, so any path under a `.app`
25+/// resolves to the outermost `.app` in it. Anything else, including a
26+/// development build sitting loose in `target/debug`, is returned unchanged.
27+#[must_use]
28+pub fn installation_root(path: &Path) -> PathBuf {
29+ let mut root: Option<&Path> = None;
30+ let mut cursor = Some(path);
31+ while let Some(candidate) = cursor {
32+ if candidate
33+ .extension()
34+ .is_some_and(|extension| extension.eq_ignore_ascii_case("app"))
35+ {
36+ root = Some(candidate);
37+ }
38+ cursor = candidate.parent().filter(|parent| !parent.as_os_str().is_empty());
39+ }
40+ root.unwrap_or(path).to_path_buf()
41+}
42+
1943 /// The roots an Omega installation occupies on this machine.
2044 ///
2145 /// One field per place Omega writes. `plan` destructures this exhaustively, so
@@ -23,8 +47,18 @@ use std::path::{Path, PathBuf};
2347 /// property the old shell table could not have.
2448 #[derive(Debug, Clone, PartialEq, Eq)]
2549 pub struct UninstallRoots {
26- /// The application bundle or executable this CLI belongs to.
27- pub app: Option<PathBuf>,
50+ /// The installation root — the whole directory Omega occupies, never one
51+ /// executable inside it.
52+ ///
53+ /// The name and the doc comment used to read "the application bundle or
54+ /// executable", and the caller took the second reading: `0.2.0-rc16`
55+ /// planned `Omega.app/Contents/MacOS/omega`, so a completed uninstall left
56+ /// `Omega.app` in `/Applications` with 130.9 MB, four more executables and
57+ /// a bundled Node runtime in it — including a `cli` that still carried
58+ /// `--uninstall` (omega#92). `installation_root` now resolves any path
59+ /// inside a bundle to the bundle itself, so a caller that hands over an
60+ /// executable cannot reintroduce that.
61+ pub app_root: Option<PathBuf>,
2862 /// `paths::data_dir()` — database, extensions, embeddings.
2963 pub data_dir: PathBuf,
3064 /// `paths::config_dir()` — settings and keymap. Prompted for, never
@@ -62,7 +96,7 @@ impl UninstallRoots {
6296 /// Every field is read from the function that writes it. Nothing here is a
6397 /// literal path.
6498 #[cfg(any(target_os = "linux", target_os = "macos"))]
65- pub fn from_installed_paths(app: Option<PathBuf>) -> Self {
99+ pub fn from_installed_paths(app_root: Option<PathBuf>) -> Self {
66100 let app_id = release_channel::RELEASE_CHANNEL.app_id();
67101 let home = paths::home_dir();
68102 let platform_paths = if cfg!(target_os = "macos") {
@@ -79,7 +113,9 @@ impl UninstallRoots {
79113 ]
80114 };
81115 Self {
82- app,
116+ // Normalized here rather than trusted from the caller: the caller
117+ // holds an executable, and every macOS caller always will.
118+ app_root: app_root.as_deref().map(installation_root),
83119 data_dir: paths::data_dir().clone(),
84120 config_dir: paths::config_dir().clone(),
85121 logs_dir: paths::logs_dir().clone(),
@@ -96,7 +132,7 @@ impl UninstallRoots {
96132 /// settings and keymap, and the caller asks before removing it.
97133 pub fn plan(&self, product: &str) -> UninstallPlan {
98134 let Self {
99- app,
135+ app_root,
100136 data_dir,
101137 config_dir,
102138 logs_dir,
@@ -113,8 +149,8 @@ impl UninstallRoots {
113149 removals.push(path);
114150 }
115151 };
116- if let Some(app) = app {
117- push(app);
152+ if let Some(app_root) = app_root {
153+ push(app_root);
118154 }
119155 push(data_dir);
120156 push(logs_dir);
@@ -152,9 +188,17 @@ impl UninstallPlan {
152188 mod tests {
153189 use super::*;
154190
191+ /// The path the CLI actually holds: the executable it is running beside,
192+ /// inside the bundle. Every root below is derived from it the way the
193+ /// product derives it, so a plan that names the executable fails here
194+ /// rather than in `/Applications`.
195+ fn installed_executable(home: &Path) -> PathBuf {
196+ home.join("Applications/Omega.app/Contents/MacOS/omega")
197+ }
198+
155199 fn roots(home: &Path) -> UninstallRoots {
156200 UninstallRoots {
157- app: Some(home.join("Applications/Omega.app")),
201+ app_root: Some(installation_root(&installed_executable(home))),
158202 data_dir: home.join("Library/Application Support/Omega RC"),
159203 config_dir: home.join(".config/omega-rc"),
160204 logs_dir: home.join("Library/Logs/omega-rc"),
@@ -198,6 +242,58 @@ mod tests {
198242 );
199243 }
200244
245+ /// OMEGA-DELTA-0043. The plan names the installation, not one file in it.
246+ ///
247+ /// `0.2.0-rc16` planned `Omega.app/Contents/MacOS/omega`, so an uninstall
248+ /// that reported success left the bundle, the `cli` that carries
249+ /// `--uninstall`, `omega-identity-proof` and a bundled Node runtime in
250+ /// `/Applications` (omega#92). Both directions are asserted: the bundle is
251+ /// in the plan, and the executable is not standing in for it.
252+ #[test]
253+ fn the_plan_names_the_bundle_root_and_never_an_executable_inside_it() {
254+ let home = Path::new("/tmp/omega-uninstall-fixture");
255+ let bundle = home.join("Applications/Omega.app");
256+ let plan = roots(home).plan("Omega RC");
257+
258+ assert!(
259+ plan.removals.contains(&bundle),
260+ "the plan does not remove {}, so the installation survives an \
261+ uninstall that reports success",
262+ bundle.display()
263+ );
264+ for inside in [
265+ installed_executable(home),
266+ bundle.join("Contents/MacOS/cli"),
267+ bundle.join("Contents/Resources/omega-effectd/runtime/bin/node"),
268+ ] {
269+ assert!(
270+ !plan.removals.contains(&inside),
271+ "the plan names {}, a file inside the bundle. Removing one file \
272+ out of an installation is not an uninstall.",
273+ inside.display()
274+ );
275+ }
276+
277+ // The derivation itself, on the shapes a caller can hand it.
278+ assert_eq!(
279+ installation_root(&installed_executable(home)),
280+ bundle,
281+ "a path inside a bundle resolves to the bundle"
282+ );
283+ assert_eq!(
284+ installation_root(&bundle),
285+ bundle,
286+ "the bundle resolves to itself"
287+ );
288+ let development = Path::new("/tmp/omega/target/debug/omega");
289+ assert_eq!(
290+ installation_root(development),
291+ development,
292+ "a loose development build has no bundle to climb to, and inventing \
293+ one would plan its parent directory for removal"
294+ );
295+ }
296+
201297 /// OMEGA-DELTA-0036. The shipped script, run for real, against a machine
202298 /// that has both editors installed.
203299 ///
@@ -209,10 +305,33 @@ mod tests {
209305 let home = temp.path();
210306 let roots = roots(home);
211307
212- // An Omega installation.
308+ // An Omega installation. The bundle is planted the way one ships:
309+ // several executables and a bundled Node runtime under the same root,
310+ // because "the app is gone" and "one file in the app is gone" look
311+ // identical from a root that holds a single marker (omega#92).
312+ // Named here, not read out of `roots`: reading it back from the plan
313+ // would make this circular, and a plan that named the executable would
314+ // plant its files under the executable and pass.
315+ let bundle = home.join("Applications/Omega.app");
316+ let bundle_contents: Vec<PathBuf> = [
317+ "Contents/Info.plist",
318+ "Contents/MacOS/omega",
319+ "Contents/MacOS/cli",
320+ "Contents/MacOS/omega-identity-proof",
321+ "Contents/Resources/omega-effectd/dist/omega-effectd.mjs",
322+ "Contents/Resources/omega-effectd/runtime/bin/node",
323+ ]
324+ .iter()
325+ .map(|relative| bundle.join(relative))
326+ .collect();
327+ for path in &bundle_contents {
328+ std::fs::create_dir_all(path.parent().unwrap()).expect("create bundle directory");
329+ std::fs::write(path, b"omega").expect("write bundle file");
330+ }
331+
213332 let mut planted = Vec::new();
214333 for directory in [
215- &roots.app.clone().unwrap(),
334+ &bundle,
216335 &roots.data_dir,
217336 &roots.config_dir,
218337 &roots.logs_dir,
@@ -273,7 +392,7 @@ mod tests {
273392 // root would pass it. Destructuring is exhaustive, so a root added to
274393 // the struct and left out of the plan fails to compile here.
275394 let UninstallRoots {
276- app,
395+ app_root,
277396 data_dir,
278397 config_dir,
279398 logs_dir,
@@ -282,7 +401,7 @@ mod tests {
282401 cli_symlink,
283402 platform_paths,
284403 } = &roots;
285- let occupied: Vec<&PathBuf> = app
404+ let occupied: Vec<&PathBuf> = app_root
286405 .iter()
287406 .chain([data_dir, config_dir, logs_dir, temp_dir, state_dir, cli_symlink])
288407 .chain(platform_paths.iter())
@@ -294,6 +413,20 @@ mod tests {
294413 path.display()
295414 );
296415 }
416+
417+ // And nothing inside the bundle either. A plan that names the
418+ // executable removes one file and reports success, which is what
419+ // `0.2.0-rc16` shipped: 130.9 MB and five executables survived
420+ // (omega#92). The root check above cannot see that, because the root it
421+ // was handed would have been the executable.
422+ for path in &bundle_contents {
423+ assert!(
424+ !path.exists(),
425+ "{} survived the uninstall. The plan removed something inside \
426+ the installation instead of the installation.",
427+ path.display()
428+ );
429+ }
297430 for path in &foreign {
298431 assert_eq!(
299432 std::fs::read(path).ok().as_deref(),
diff --git a/crates/omega_deltas/src/omega_deltas.rs b/crates/omega_deltas/src/omega_deltas.rs
index 26f6a4e286..4aab74ace7 100644
--- a/crates/omega_deltas/src/omega_deltas.rs
+++ b/crates/omega_deltas/src/omega_deltas.rs
@@ -70,6 +70,8 @@ pub const ENFORCED_DELTAS: &[&str] = &[
7070 "OMEGA-DELTA-0040",
7171 "OMEGA-DELTA-0041",
7272 "OMEGA-DELTA-0042",
73+ "OMEGA-DELTA-0043",
74+ "OMEGA-DELTA-0044",
7375 ];
7476
7577 /// OMEGA-DELTA-0036. The uninstall script embedded in the shipped `cli`.
@@ -78,6 +80,10 @@ pub const UNINSTALL_SCRIPT_PATH: &str = "script/uninstall.sh";
7880 /// OMEGA-DELTA-0036. Where the removal plan is derived from `paths::`.
7981 pub const UNINSTALL_PLAN_PATH: &str = "crates/cli/src/uninstall.rs";
8082
83+/// OMEGA-DELTA-0043, OMEGA-DELTA-0044. The CLI binary: it derives the
84+/// uninstall plan and renders the open-behavior prompt.
85+pub const CLI_MAIN_PATH: &str = "crates/cli/src/main.rs";
86+
8187 /// OMEGA-DELTA-0037. Outbound attribution on OpenRouter requests.
8288 pub const OPEN_ROUTER_PATH: &str = "crates/open_router/src/open_router.rs";
8389
@@ -1466,6 +1472,65 @@ pub fn is_prose(text: &str) -> bool {
14661472 plain >= 2
14671473 }
14681474
1475+/// Whether `text` is a command line whose program name is a competitor's
1476+/// binary.
1477+///
1478+/// OMEGA-DELTA-0044. `is_prose` needs three tokens, so `zed --existing`,
1479+/// `zed --classic` and `zed <path>` were invisible to it — and all three
1480+/// shipped in the signed `cli` of `0.2.0-rc16`, in an interactive panel whose
1481+/// surrounding copy says "Omega window" and "Omega settings" around them
1482+/// (omega#93). Substitute our own name and each stays true, so by the rule
1483+/// this gate is written around they are product claims, not references.
1484+///
1485+/// This admits the narrow shape only: a brand word standing in `argv[0]`,
1486+/// followed by flags, placeholders or paths. Admitting every two-token literal
1487+/// that starts with a brand word instead adds 8 more across the tree — `Zed
1488+/// Pro`, `Zed (Default)`, `Zed Repository` — which are genuine references to
1489+/// somebody else's product; admitting a bare brand token adds 55. This shape
1490+/// adds exactly the three, which is why the gate can afford to carry it
1491+/// instead of writing the three literals into a denylist.
1492+#[must_use]
1493+pub fn is_command_form(text: &str, policy: &serde_json::Value) -> bool {
1494+ let words: Vec<&str> = policy["brand"]["words"]
1495+ .as_array()
1496+ .map(|values| values.iter().filter_map(serde_json::Value::as_str).collect())
1497+ .unwrap_or_default();
1498+ let tokens: Vec<&str> = text.split_whitespace().collect();
1499+ if tokens.len() < 2 || !words.contains(&tokens[0]) {
1500+ return false;
1501+ }
1502+ tokens[1..].iter().all(|token| is_command_argument(token))
1503+}
1504+
1505+/// A flag (`-n`, `--existing`), a placeholder (`<path>`, `[FILE]`), or a path
1506+/// (`.`, `..`, `~/x`, `./x`, `/x`).
1507+fn is_command_argument(token: &str) -> bool {
1508+ let flag = token.strip_prefix("--").or_else(|| token.strip_prefix('-'));
1509+ if let Some(rest) = flag {
1510+ return rest
1511+ .chars()
1512+ .next()
1513+ .is_some_and(|character| character.is_ascii_alphanumeric())
1514+ && rest
1515+ .chars()
1516+ .all(|character| character.is_ascii_alphanumeric() || "-_.".contains(character));
1517+ }
1518+ let placeholder = matches!(token.chars().next(), Some('<' | '[' | '{'))
1519+ && matches!(token.chars().last(), Some('>' | ']' | '}'))
1520+ && token.len() >= 2
1521+ && !token[1..token.len() - 1].contains(['>', ']', '}']);
1522+ if placeholder {
1523+ return true;
1524+ }
1525+ token == "." || token == ".." || token.starts_with(['~', '/']) || token.starts_with("./")
1526+}
1527+
1528+/// Prose, or a command form. What the prose inventory admits.
1529+#[must_use]
1530+pub fn is_user_facing_text(text: &str, policy: &serde_json::Value) -> bool {
1531+ is_prose(text) || is_command_form(&normalize_prose(text), policy)
1532+}
1533+
14691534 /// Every Rust string literal in `source`, as `(start line, contents)`.
14701535 ///
14711536 /// Raw and multi-line literals included, comments skipped. A regex over single
@@ -1775,7 +1840,7 @@ pub fn prose_inventory(policy: &serde_json::Value) -> (Vec<ProseLiteral>, ProseR
17751840 if is_test_file || skipped.contains(&number) {
17761841 continue;
17771842 }
1778- if !brand_hits(&body, policy).is_empty() && is_prose(&body) {
1843+ if !brand_hits(&body, policy).is_empty() && is_user_facing_text(&body, policy) {
17791844 items.push(ProseLiteral {
17801845 kind: "rust_string",
17811846 file: relative.clone(),
@@ -1792,7 +1857,7 @@ pub fn prose_inventory(policy: &serde_json::Value) -> (Vec<ProseLiteral>, ProseR
17921857 let Some(body) = doc_comment_body(line) else {
17931858 continue;
17941859 };
1795- if brand_hits(body, policy).is_empty() || !is_prose(body) {
1860+ if brand_hits(body, policy).is_empty() || !is_user_facing_text(body, policy) {
17961861 continue;
17971862 }
17981863 let kind = if actions.contains(&number) {
@@ -1822,7 +1887,7 @@ pub fn prose_inventory(policy: &serde_json::Value) -> (Vec<ProseLiteral>, ProseR
18221887 continue;
18231888 }
18241889 for (index, line) in source.lines().enumerate() {
1825- if !brand_hits(line, policy).is_empty() && is_prose(line) {
1890+ if !brand_hits(line, policy).is_empty() && is_user_facing_text(line, policy) {
18261891 items.push(ProseLiteral {
18271892 kind: "asset",
18281893 file: relative.clone(),
@@ -4051,6 +4116,138 @@ mod tests {
40514116 );
40524117 }
40534118
4119+ /// OMEGA-DELTA-0044. A brand-bearing command form is user-facing text.
4120+ ///
4121+ /// `is_prose` needs three tokens, and `0.2.0-rc16` shipped three two-token
4122+ /// command literals in the signed `cli` because of it: `zed --existing`,
4123+ /// `zed --classic` and `zed <path>`, rendered by an interactive panel that
4124+ /// says "Omega window" and "Omega settings" around them (omega#93). They
4125+ /// were in neither `prose.classified` nor the compatibility allow-list, and
4126+ /// `verify-omega-brand --app` exited 0.
4127+ ///
4128+ /// Both directions matter. Widening this to "any two tokens beginning with
4129+ /// a brand word" pulls in eight more literals that really are references to
4130+ /// somebody else's product, and widening it to a bare brand token pulls in
4131+ /// 55; a rule that cries wolf gets deleted, which is how the blind spot
4132+ /// stayed open once it was known about.
4133+ #[test]
4134+ fn a_brand_bearing_command_form_is_user_facing_text() {
4135+ let policy = brand_policy().expect("brand policy parses");
4136+
4137+ for shipped in ["zed --existing", "zed --classic", "zed <path>"] {
4138+ assert!(
4139+ !is_prose(shipped),
4140+ "OMEGA-DELTA-0044: {shipped:?} is being kept out of the \
4141+ inventory by the three-token rule, which is the blind spot \
4142+ this exists to close"
4143+ );
4144+ assert!(
4145+ is_command_form(shipped, &policy),
4146+ "OMEGA-DELTA-0044: {shipped:?} shipped in a signed `cli` and \
4147+ the inventory still cannot see it"
4148+ );
4149+ assert!(is_user_facing_text(shipped, &policy));
4150+ }
4151+ for form in ["zed .", "zed ~/project", "zed -n", "Zed --help"] {
4152+ assert!(
4153+ is_command_form(form, &policy),
4154+ "OMEGA-DELTA-0044: {form:?} is a command form"
4155+ );
4156+ }
4157+
4158+ // The other direction: the shapes that would make this noisy enough to
4159+ // be deleted, and are therefore excluded on purpose.
4160+ for benign in [
4161+ "Zed Pro",
4162+ "Zed (Default)",
4163+ "Zed Repository",
4164+ "zed",
4165+ "crates/zed/src",
4166+ "X-Zed-Predict-Edits-Mode",
4167+ "zed_llm_client",
4168+ ] {
4169+ assert!(
4170+ !is_command_form(benign, &policy),
4171+ "OMEGA-DELTA-0044: {benign:?} is not a command form. This rule \
4172+ covers a brand word standing in argv[0]; a two-word label is \
4173+ either prose already or a reference to be classified."
4174+ );
4175+ }
4176+
4177+ // The shell gate carries the same rule, on the same three streams, or
4178+ // the source and packaged sides disagree about what is inventoried.
4179+ let verifier =
4180+ std::fs::read_to_string(repository_path(BRAND_VERIFIER_PATH)).expect("brand verifier");
4181+ assert!(
4182+ verifier.contains("def is_command_form("),
4183+ "OMEGA-DELTA-0044: {BRAND_VERIFIER_PATH} does not carry the \
4184+ command-form rule, so `verify-omega-brand` and these tests \
4185+ inventory different things."
4186+ );
4187+ let wired = verifier.matches("is_user_facing_text(").count();
4188+ assert!(
4189+ wired >= 4,
4190+ "OMEGA-DELTA-0044: {BRAND_VERIFIER_PATH} calls is_user_facing_text \
4191+ {wired} time(s) — one definition and one call per stream (Rust \
4192+ literals, doc lines, embedded assets) is the minimum. A predicate \
4193+ that is defined and never called is the state \
4194+ first_party_agent.phrases was in for four release candidates."
4195+ );
4196+ assert!(
4197+ !verifier.contains("and is_prose(body)") && !verifier.contains("not is_prose(doc)"),
4198+ "OMEGA-DELTA-0044: a stream in {BRAND_VERIFIER_PATH} still filters \
4199+ on is_prose alone, so command forms are invisible to it."
4200+ );
4201+ }
4202+
4203+ /// OMEGA-DELTA-0044. The prompt spells its command forms with the binary's
4204+ /// own name.
4205+ ///
4206+ /// Fixing the three literals is not the same as making them unable to come
4207+ /// back. Any command form written out by hand can drift from the binary it
4208+ /// describes; one built from `paths::BINARY_NAME` cannot.
4209+ #[test]
4210+ fn the_cli_prompt_names_our_own_binary() {
4211+ let path = repository_path(CLI_MAIN_PATH);
4212+ let source = std::fs::read_to_string(&path)
4213+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
4214+ let prompt = source
4215+ .split("fn prompt_open_behavior()")
4216+ .nth(1)
4217+ .expect("OMEGA-DELTA-0044: prompt_open_behavior is gone");
4218+ let prompt = &prompt[..prompt.find("\n}\n").unwrap_or(prompt.len())];
4219+ // Ordinary comments are dropped, doc comments are not: a `//` line does
4220+ // not reach a user, and the comment recording what shipped here has to
4221+ // be able to quote it. `///` and `//!` stay, because clap and schemars
4222+ // print those.
4223+ let rendered: String = prompt
4224+ .lines()
4225+ .filter(|line| {
4226+ let trimmed = line.trim_start();
4227+ !trimmed.starts_with("//") || trimmed.starts_with("///") || trimmed.starts_with("//!")
4228+ })
4229+ .collect::<Vec<_>>()
4230+ .join("\n");
4231+
4232+ let policy = brand_policy().expect("brand policy parses");
4233+ let hits = brand_hits(&rendered, &policy);
4234+ assert!(
4235+ hits.is_empty(),
4236+ "OMEGA-DELTA-0044: the open-behavior prompt in {} names {hits:?}. \
4237+ This panel already says \"Omega window\" and \"Omega settings\"; \
4238+ the commands beside them have to be ours too (omega#93).",
4239+ path.display()
4240+ );
4241+ let derived = rendered.matches("paths::BINARY_NAME").count();
4242+ assert!(
4243+ derived >= 3,
4244+ "OMEGA-DELTA-0044: the prompt builds only {derived} of its command \
4245+ forms from paths::BINARY_NAME. All three — the two choices and the \
4246+ heading — come from the binary's own name, so a rename cannot \
4247+ leave one behind."
4248+ );
4249+ }
4250+
40544251 #[test]
40554252 fn delta_ids_are_unique() {
40564253 let mut seen = std::collections::BTreeSet::new();
@@ -5810,6 +6007,67 @@ mod tests {
58106007 );
58116008 }
58126009
6010+ /// OMEGA-DELTA-0043. `--uninstall` plans the installation, not one file.
6011+ ///
6012+ /// `0.2.0-rc16` fixed the destructive half of omega#88 and still did not
6013+ /// remove Omega: `main.rs` handed `app.path()` — `Omega.app/Contents/MacOS/
6014+ /// omega` — to a field documented as "the application bundle or executable",
6015+ /// so a completed uninstall left `/Applications/Omega.app` in place with
6016+ /// 130.9 MB, five executables including a `cli` that still carries
6017+ /// `--uninstall`, and a bundled Node runtime (omega#92).
6018+ ///
6019+ /// The behavioural proof is in `crates/cli/src/uninstall.rs`, where the
6020+ /// shipped script runs against a fabricated home whose bundle holds several
6021+ /// executables and every one of them is read back. This asserts the two
6022+ /// structural properties: the call site asks for the installation rather
6023+ /// than the executable, and the constructor normalizes what it is given so
6024+ /// a caller cannot reintroduce the defect from outside.
6025+ #[test]
6026+ fn the_uninstall_plan_names_the_installation_root() {
6027+ let plan_path = repository_path(UNINSTALL_PLAN_PATH);
6028+ let plan = std::fs::read_to_string(&plan_path)
6029+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", plan_path.display()));
6030+ assert!(
6031+ plan.contains("pub fn installation_root("),
6032+ "OMEGA-DELTA-0043: {} no longer derives an installation root, so \
6033+ whatever path a caller happens to be holding is what gets removed.",
6034+ plan_path.display()
6035+ );
6036+ let constructor = plan
6037+ .split("pub fn from_installed_paths")
6038+ .nth(1)
6039+ .and_then(|tail| tail.split("\n }").next())
6040+ .expect("OMEGA-DELTA-0043: from_installed_paths is gone");
6041+ assert!(
6042+ constructor.contains("installation_root"),
6043+ "OMEGA-DELTA-0043: from_installed_paths takes the caller's path as \
6044+ the installation root. Every macOS caller holds an executable \
6045+ inside the bundle, and removing it is not an uninstall."
6046+ );
6047+
6048+ let main_path = repository_path(CLI_MAIN_PATH);
6049+ let main = std::fs::read_to_string(&main_path)
6050+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", main_path.display()));
6051+ let call = main
6052+ .split("from_installed_paths(")
6053+ .nth(1)
6054+ .expect("OMEGA-DELTA-0043: the uninstall call site is gone");
6055+ let call = &call[..call.find(')').unwrap_or(call.len())];
6056+ assert!(
6057+ call.contains("installation_root"),
6058+ "OMEGA-DELTA-0043: {} passes {call:?} to the uninstaller. \
6059+ `path()` is one executable inside `Omega.app`; \
6060+ `installation_root()` is the installation.",
6061+ main_path.display()
6062+ );
6063+ assert!(
6064+ main.contains("fn installation_root(&self) -> PathBuf;"),
6065+ "OMEGA-DELTA-0043: InstalledApp no longer distinguishes the \
6066+ executable from the installation. One method answering both \
6067+ questions is what shipped omega#92."
6068+ );
6069+ }
6070+
58136071 /// OMEGA-DELTA-0037. Omega identifies itself to third parties as Omega.
58146072 ///
58156073 /// `X-Title` is displayed to the account holder in their own OpenRouter
diff --git a/script/verify-omega-brand b/script/verify-omega-brand
index 6e95a55a34..06f4675851 100755
--- a/script/verify-omega-brand
+++ b/script/verify-omega-brand
@@ -513,6 +513,40 @@ def is_prose(text: str) -> bool:
513513 return plain >= 2
514514
515515
516+COMMAND_ARGUMENT = re.compile(
517+ r"^(?:--?[A-Za-z0-9][-A-Za-z0-9_.]*" # a flag: -n, --existing
518+ r"|[<\[{][^\s>\]}]*[>\]}]" # a placeholder: <path>, [FILE]
519+ r"|\.{1,2}|[~./][^\s]*)$" # a path: ., .., ~/x, ./x, /x
520+)
521+
522+
523+def is_command_form(text: str) -> bool:
524+ """A command line whose program name is a competitor's binary.
525+
526+ `is_prose` needs three tokens, so `zed --existing`, `zed --classic` and
527+ `zed <path>` were invisible to it -- and all three shipped in the signed
528+ `cli` of `0.2.0-rc16`, inside a panel that says "Omega window" and "Omega
529+ settings" around them (omega#93). Substituting our own name leaves each one
530+ true, so by the rule this gate is written around they are product claims.
531+
532+ This is the narrow shape only: a brand word standing in argv[0], followed
533+ by flags, placeholders or paths. Counting every two-token literal beginning
534+ with a brand word instead adds 8 more across the tree -- `Zed Pro`, `Zed
535+ (Default)`, `Zed Repository` -- which are genuine references to somebody
536+ else's product and would need classifying; counting a bare brand token adds
537+ 55. This shape adds exactly the three, which is why the gate can carry it.
538+ """
539+ tokens = text.split()
540+ if len(tokens) < 2 or tokens[0] not in WORDS:
541+ return False
542+ return all(COMMAND_ARGUMENT.fullmatch(token) for token in tokens[1:])
543+
544+
545+def is_user_facing_text(text: str) -> bool:
546+ """Prose, or a command form. What the inventory admits."""
547+ return is_prose(text) or is_command_form(normalize_prose(text))
548+
549+
516550 def rust_string_literals(text: str) -> list[tuple[int, str]]:
517551 """(start line, contents) for every Rust string literal in `text`.
518552
@@ -686,13 +720,13 @@ def prose_inventory() -> tuple[tuple[tuple[str, str, int, str], ...], dict[str,
686720 for number, body in literals:
687721 if is_test_file or number in skipped:
688722 continue
689- if brand_hits(body) and is_prose(body):
723+ if brand_hits(body) and is_user_facing_text(body):
690724 items.append(("rust_string", relative, number, normalize_prose(body)))
691725 for number, line in enumerate(lines, 1):
692726 if is_test_file or number in skipped:
693727 continue
694728 doc = doc_line_body(line)
695- if doc is None or not brand_hits(doc) or not is_prose(doc):
729+ if doc is None or not brand_hits(doc) or not is_user_facing_text(doc):
696730 continue
697731 if number in actions:
698732 kind = "action_doc"
@@ -713,7 +747,7 @@ def prose_inventory() -> tuple[tuple[tuple[str, str, int, str], ...], dict[str,
713747 if not brand_hits(text):
714748 continue
715749 for number, line in enumerate(text.splitlines(), 1):
716- if brand_hits(line) and is_prose(line):
750+ if brand_hits(line) and is_user_facing_text(line):
717751 items.append(("asset", relative, number, normalize_prose(line)))
718752 return tuple(items), read