458595a20b
34 of the 46 `NO_DESKTOP_SURFACE` rows in desktop-slash-commands.ts were a byte-for-byte copy of `desktop=` on the matching CommandDef in hermes_cli/commands.py (7 more were aliases of those rows). The live `commands.catalog` already carries that metadata; the static list was the offline fallback and would silently drift on the next registry edit. Now `hermes_cli/commands.py::desktop_surface_registry()` is the one author of `/name -> desktop` (aliases included). `scripts/dump_desktop_slash_registry.py` writes it to apps/desktop/src/lib/desktop-slash-registry.json, which the desktop imports as its offline fallback (`registryUnavailableSpecs`). Five names the Python registry has never heard of stay in an explicit `TS_ONLY_NO_DESKTOP_SURFACE` with the reason WHY: `/density /details /logs /mouse` are Ink-process-local display toggles (handled in ui-tui/src/app/slash/commands/core.ts; advertised via `_TUI_EXTRA`), and `/pets` is the plural typo of the desktop's own `/pet` action. `/switch` was never a block-list row (it is a `/resume` alias) — not a finding. Cross-language contract: tests/hermes_cli/test_desktop_slash_registry.py asserts the committed JSON == desktop_surface_registry() and that every alias carries its canonical value; desktop-slash-commands.test.ts asserts every dumped row is unavailable/unsuggested offline with the dumped reason and that the TS-only set is disjoint from the dump. Both sides fail on drift (sabotage: flipping one `desktop=` in commands.py -> Python test "stale"; adding `/clear` to the TS-only list -> vitest disjointness fails; dropping `registryUnavailableSpecs()` -> 4 existing vitest cases fail). Behavior change: none for users. `/model` (`desktop="hidden"`) keeps its local picker spec; `hidden` is a popover flag read from the live catalog. Sites: apps/desktop/src/lib/desktop-slash-commands.ts::NO_DESKTOP_SURFACE (46 rows) -> hermes_cli/commands.py::desktop_surface_registry (41 rows via the dump) + TS_ONLY_NO_DESKTOP_SURFACE (5 rows). apps/desktop/src/AGENTS.md updated.