@omega.js/desktop 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/css/tokens/_index.scss +1 -1
  3. package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +4 -1
  4. package/dist/assets/themes/base/_includes/frontend/sections/footer.html +13 -6
  5. package/dist/assets/themes/base/_includes/frontend/sections/nav.html +10 -8
  6. package/dist/assets/themes/base/_includes/global/sections/account.html +3 -1
  7. package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +12 -8
  8. package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +10 -6
  9. package/dist/assets/themes/base/_includes/global/sections/page-header.html +8 -4
  10. package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +24 -24
  11. package/dist/assets/themes/base/_layouts/frontend/pages/about.html +10 -10
  12. package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +35 -35
  13. package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +4 -4
  14. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +1 -1
  15. package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +4 -4
  16. package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +3 -3
  17. package/dist/assets/themes/base/_layouts/frontend/pages/download.html +34 -33
  18. package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +11 -11
  19. package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +1 -1
  20. package/dist/assets/themes/base/_layouts/frontend/pages/status.html +1 -1
  21. package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +9 -7
  22. package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +5 -3
  23. package/dist/assets/themes/base/_sections/about/letter/section.html +1 -1
  24. package/dist/assets/themes/base/_sections/about/letter/section.json5 +4 -4
  25. package/dist/assets/themes/base/_sections/marketing/bento/section.html +1 -1
  26. package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +15 -15
  27. package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +1 -1
  28. package/dist/assets/themes/base/_sections/marketing/hero/section.html +6 -6
  29. package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +10 -10
  30. package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +1 -1
  31. package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +2 -2
  32. package/dist/assets/themes/base/_sections/marketing/stats/section.html +1 -1
  33. package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +5 -5
  34. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +1 -1
  35. package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +2 -2
  36. package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +10 -10
  37. package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +10 -10
  38. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +2 -2
  39. package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +4 -4
  40. package/dist/build.js +69 -28
  41. package/dist/cli-run.js +20 -13
  42. package/dist/cli.js +12 -7
  43. package/dist/commands/build.js +0 -2
  44. package/dist/commands/deploy.js +116 -44
  45. package/dist/commands/finalize-release.js +5 -4
  46. package/dist/commands/launch.js +12 -7
  47. package/dist/commands/lib/deploy-precheck.js +66 -34
  48. package/dist/commands/lib/ensure-target.js +71 -11
  49. package/dist/commands/package.js +0 -1
  50. package/dist/commands/publish.js +11 -4
  51. package/dist/commands/release.js +99 -252
  52. package/dist/commands/runner.js +42 -5
  53. package/dist/commands/sign-windows.js +46 -15
  54. package/dist/commands/test.js +4 -3
  55. package/dist/commands/validate-certs.js +291 -115
  56. package/dist/config/page-template.html +4 -0
  57. package/dist/defaults/.github/workflows/build.yml +144 -64
  58. package/dist/defaults/AGENTS.md +3 -2
  59. package/dist/defaults/_.gitignore +4 -2
  60. package/dist/defaults/config/certs/README.md +25 -45
  61. package/dist/defaults/config/omega.json5 +66 -32
  62. package/dist/defaults/hooks/deploy/pre.js +10 -0
  63. package/dist/defaults/src/assets/scss/main.scss +12 -2
  64. package/dist/defaults/src/integrations/tray/index.js +1 -1
  65. package/dist/gulp/main.js +13 -25
  66. package/dist/gulp/tasks/audit.js +18 -6
  67. package/dist/gulp/tasks/build-config.js +181 -60
  68. package/dist/gulp/tasks/bundle.js +108 -36
  69. package/dist/gulp/tasks/release.js +86 -4
  70. package/dist/gulp/tasks/sass.js +11 -0
  71. package/dist/hooks/lib/notarize-tools.js +137 -0
  72. package/dist/hooks/notarize-artifacts.js +57 -0
  73. package/dist/hooks/notarize.js +59 -8
  74. package/dist/lib/auth-persistence.js +25 -0
  75. package/dist/lib/client-bridge.js +11 -8
  76. package/dist/lib/deep-link.js +15 -8
  77. package/dist/lib/protocol.js +7 -1
  78. package/dist/lib/restart-manager/install.js +6 -3
  79. package/dist/lib/sign-helpers/auto-unlock.js +65 -27
  80. package/dist/lib/sign-helpers/console-lock.js +34 -0
  81. package/dist/lib/sign-helpers/exec-with-limit.js +68 -0
  82. package/dist/lib/sign-helpers/resolve-icons.js +6 -4
  83. package/dist/lib/tray.js +7 -6
  84. package/dist/main.js +16 -0
  85. package/dist/preload.js +8 -0
  86. package/dist/renderer.js +16 -4
  87. package/dist/runner/job-started.js +104 -0
  88. package/dist/test/fixtures/consumer-app/config/omega.json5 +7 -0
  89. package/dist/test/fixtures/consumer-app/package.json +1 -1
  90. package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +7 -1
  91. package/dist/test/harness/main-entry.js +2 -1
  92. package/dist/test/harness/renderer-preload.js +12 -3
  93. package/dist/test/runners/boot.js +101 -11
  94. package/dist/test/runners/electron.js +1 -1
  95. package/dist/test/suites/boot/consumer-app-boots.test.js +54 -0
  96. package/dist/test/suites/build/audit.test.js +45 -6
  97. package/dist/test/suites/build/auth-persistence-resolve.test.js +119 -0
  98. package/dist/test/suites/build/auto-unlock.test.js +105 -0
  99. package/dist/test/suites/build/boot-runner-timeout.test.js +253 -0
  100. package/dist/test/suites/build/brand-scss.test.js +106 -0
  101. package/dist/test/suites/build/build-config.test.js +146 -36
  102. package/dist/test/suites/build/build-json-bake.test.js +245 -0
  103. package/dist/test/suites/build/build-verbs.test.js +48 -15
  104. package/dist/test/suites/build/build-workflow-jobs.test.js +190 -0
  105. package/dist/test/suites/build/cli.test.js +32 -10
  106. package/dist/test/suites/build/config-schema.test.js +4 -4
  107. package/dist/test/suites/build/console-lock.test.js +51 -0
  108. package/dist/test/suites/build/defaults-scaffold.test.js +71 -2
  109. package/dist/test/suites/build/deploy-direct.test.js +241 -0
  110. package/dist/test/suites/build/deploy-dispatch.test.js +287 -0
  111. package/dist/test/suites/build/deploy-hook.test.js +169 -0
  112. package/dist/test/suites/build/ensure-target.test.js +14 -2
  113. package/dist/test/suites/build/env-delivery.test.js +19 -10
  114. package/dist/test/suites/build/env-watch.test.js +18 -7
  115. package/dist/test/suites/build/esm-only-dependency.test.js +127 -0
  116. package/dist/test/suites/build/exec-with-limit.test.js +53 -0
  117. package/dist/test/suites/build/finalize-release.test.js +2 -2
  118. package/dist/test/suites/build/get-config.test.js +120 -8
  119. package/dist/test/suites/build/github-utils.test.js +12 -6
  120. package/dist/test/suites/build/license-stamp.test.js +6 -4
  121. package/dist/test/suites/build/manager.test.js +88 -68
  122. package/dist/test/suites/build/manifest-deps.test.js +116 -0
  123. package/dist/test/suites/build/merge-line-files.test.js +27 -4
  124. package/dist/test/suites/build/notarize-artifacts.test.js +135 -0
  125. package/dist/test/suites/build/notarize-tools.test.js +38 -0
  126. package/dist/test/suites/build/notarize.test.js +207 -0
  127. package/dist/test/suites/build/release-pipeline.test.js +38 -60
  128. package/dist/test/suites/build/release-skipped-upload.test.js +107 -0
  129. package/dist/test/suites/build/resolve-icons.test.js +35 -35
  130. package/dist/test/suites/build/runner-job-guard.test.js +182 -0
  131. package/dist/test/suites/build/runner.test.js +25 -2
  132. package/dist/test/suites/build/sentry.test.js +8 -3
  133. package/dist/test/suites/build/setup-scripts.test.js +3 -0
  134. package/dist/test/suites/build/sign-windows.test.js +92 -8
  135. package/dist/test/suites/build/test-stealth.test.js +17 -11
  136. package/dist/test/suites/build/url-helpers.test.js +37 -17
  137. package/dist/test/suites/build/validate-certs.test.js +428 -55
  138. package/dist/test/suites/build/validate-config.test.js +3 -3
  139. package/dist/test/suites/main/auth-flow.test.js +12 -0
  140. package/dist/test/suites/main/auth-persistence.test.js +14 -17
  141. package/dist/test/suites/main/auto-updater.test.js +2 -2
  142. package/dist/test/suites/main/boot-sequence.test.js +1 -1
  143. package/dist/test/suites/main/client-bridge.integration.test.js +5 -82
  144. package/dist/test/suites/main/client-bridge.test.js +19 -6
  145. package/dist/test/suites/main/deep-link.test.js +54 -0
  146. package/dist/test/suites/main/startup-paths-and-ua.test.js +1 -1
  147. package/dist/test/suites/main/url-helpers.test.js +81 -72
  148. package/dist/test/suites/renderer/cross-context-helpers.test.js +19 -16
  149. package/dist/utils/build-pipeline.js +10 -10
  150. package/dist/utils/github.js +12 -51
  151. package/dist/utils/load-env.js +66 -0
  152. package/dist/utils/mode-helpers.js +43 -111
  153. package/dist/utils/platform.js +37 -0
  154. package/dist/utils/runner-env.js +2 -1
  155. package/dist/utils/runner-job-guard.js +149 -0
  156. package/dist/utils/ship-keys.js +52 -0
  157. package/dist/utils/test-stealth.js +5 -3
  158. package/dist/utils/url-helpers.js +33 -17
  159. package/dist/vendor/config/bundle-id.js +53 -0
  160. package/dist/vendor/config/client-config.js +141 -0
  161. package/dist/vendor/config/company.js +334 -15
  162. package/dist/vendor/config/dev-facts.js +48 -0
  163. package/dist/vendor/config/env-delivery.js +231 -9
  164. package/dist/vendor/config/env-retired.js +137 -0
  165. package/dist/vendor/config/env-rules.js +22 -3
  166. package/dist/vendor/config/env-schema.js +234 -117
  167. package/dist/vendor/config/env.js +55 -26
  168. package/dist/vendor/config/environment.js +189 -0
  169. package/dist/vendor/config/hooks.js +13 -11
  170. package/dist/vendor/config/index.js +121 -44
  171. package/dist/vendor/config/load.js +366 -115
  172. package/dist/vendor/config/merge.js +2 -2
  173. package/dist/vendor/config/order.js +3 -3
  174. package/dist/vendor/config/platforms.js +276 -0
  175. package/dist/vendor/config/repo.js +226 -104
  176. package/dist/vendor/config/retired-keys.js +232 -27
  177. package/dist/vendor/config/schema.js +525 -99
  178. package/dist/vendor/config/site-global.js +63 -53
  179. package/dist/vendor/config/targets.js +187 -0
  180. package/dist/vendor/config/validate.js +119 -59
  181. package/dist/vendor/devkit/argv.js +118 -0
  182. package/dist/vendor/devkit/attach-log-file.js +21 -13
  183. package/dist/vendor/devkit/brand-tokens.js +278 -0
  184. package/dist/vendor/devkit/brand-version.js +264 -0
  185. package/dist/vendor/devkit/build-json.js +91 -0
  186. package/dist/vendor/devkit/bundle.js +48 -0
  187. package/dist/vendor/devkit/certificate-expiry.js +108 -0
  188. package/dist/vendor/devkit/certs.js +16 -194
  189. package/dist/vendor/devkit/ci-workflows.js +124 -8
  190. package/dist/vendor/devkit/cli-router.js +3 -3
  191. package/dist/vendor/devkit/defaults-engine.js +69 -7
  192. package/dist/vendor/devkit/deploy-follow.js +297 -0
  193. package/dist/vendor/devkit/deploy-precheck.js +23 -8
  194. package/dist/vendor/devkit/deploy-record.js +11 -27
  195. package/dist/vendor/devkit/deploy-snapshot.js +661 -0
  196. package/dist/vendor/devkit/deploy.js +445 -75
  197. package/dist/vendor/devkit/git-auth.js +73 -0
  198. package/dist/vendor/devkit/git-remote.js +95 -0
  199. package/dist/vendor/devkit/github-repo.js +290 -0
  200. package/dist/vendor/devkit/local.js +47 -0
  201. package/dist/vendor/devkit/merge-line-files.js +23 -16
  202. package/dist/vendor/devkit/omega-bin.js +18 -3
  203. package/dist/vendor/devkit/pack-local.js +391 -0
  204. package/dist/vendor/devkit/preludes/index.js +120 -0
  205. package/dist/vendor/devkit/preludes/origin-heal.js +156 -0
  206. package/dist/vendor/devkit/service-account.js +43 -0
  207. package/dist/vendor/devkit/ship-plan.js +112 -0
  208. package/dist/vendor/devkit/signing-env.js +180 -0
  209. package/dist/vendor/devkit/signing-tree.js +92 -0
  210. package/dist/vendor/devkit/target-seams.js +142 -0
  211. package/dist/vendor/devkit/target-secrets.js +235 -50
  212. package/dist/vendor/devkit/test/esm-only-fixture.js +48 -0
  213. package/dist/vendor/devkit/test/fixtures/esm-only-package/browser.js +19 -0
  214. package/dist/vendor/devkit/test/fixtures/esm-only-package/index.js +20 -0
  215. package/dist/vendor/devkit/test/fixtures/esm-only-package/package.json +13 -0
  216. package/dist/vendor/monitoring/env.js +20 -10
  217. package/dist/vendor/monitoring/main.js +1 -1
  218. package/dist/vendor/monitoring/preload.js +1 -1
  219. package/dist/vendor/monitoring/renderer.js +1 -1
  220. package/docs/analytics.md +1 -1
  221. package/docs/auto-updater.md +5 -5
  222. package/docs/boot-sequence.md +1 -1
  223. package/docs/build-system.md +15 -7
  224. package/docs/client-bridge.md +9 -7
  225. package/docs/config-schema.md +4 -4
  226. package/docs/css.md +8 -2
  227. package/docs/deep-link.md +12 -4
  228. package/docs/environment-detection.md +32 -24
  229. package/docs/hooks.md +3 -1
  230. package/docs/icons.md +7 -7
  231. package/docs/index.md +61 -23
  232. package/docs/installer-options.md +24 -21
  233. package/docs/logging.md +5 -5
  234. package/docs/releasing.md +25 -17
  235. package/docs/runner.md +40 -5
  236. package/docs/shared/brands.md +12 -6
  237. package/docs/shared/breaking-changes.md +375 -21
  238. package/docs/shared/config.md +757 -199
  239. package/docs/shared/deploys.md +194 -91
  240. package/docs/shared/icons.md +18 -0
  241. package/docs/shared/local-dev.md +24 -6
  242. package/docs/shared/logging.md +9 -6
  243. package/docs/shared/monitoring.md +27 -13
  244. package/docs/shared/publishing.md +3 -3
  245. package/docs/shared/rulings.md +2 -2
  246. package/docs/shared/testing.md +1 -1
  247. package/docs/shared/theming.md +26 -1
  248. package/docs/shared/translation.md +49 -7
  249. package/docs/shared/updates.md +1 -1
  250. package/docs/signing.md +59 -33
  251. package/docs/test-framework.md +10 -5
  252. package/docs/themes.md +15 -1
  253. package/package.json +18 -12
  254. package/bin/omega-desktop +0 -2
  255. package/dist/commands/push-secrets.js +0 -141
  256. package/dist/test/suites/build/deliver-certs.test.js +0 -95
  257. package/dist/test/suites/build/derive-signing-env.test.js +0 -122
  258. package/dist/test/suites/build/push-secrets.test.js +0 -226
  259. package/dist/test/suites/build/resolve-signing-cert.test.js +0 -342
  260. package/dist/utils/deliver-certs.js +0 -69
  261. package/dist/utils/derive-signing-env.js +0 -56
  262. package/dist/utils/resolve-signing-cert.js +0 -175
  263. package/dist/vendor/config/desktop-artifacts.js +0 -110
  264. package/dist/vendor/config/instances.js +0 -208
  265. /package/dist/defaults/config/icons/{macos → mac}/dmg.png +0 -0
  266. /package/dist/defaults/config/icons/{macos → mac}/icon.png +0 -0
  267. /package/dist/defaults/config/icons/{macos → mac}/tray.png +0 -0
@@ -22,6 +22,24 @@ overdraw their viewBox).
22
22
 
23
23
  **Emojis are text**, not icons: type the character. Nothing to build.
24
24
 
25
+ **A DATA key that carries an icon carries the same string**: every chrome
26
+ `icon` (nav, sidebar, topbar, page header, account dropdown, account section
27
+ header, footer) is the full class string (`icon: 'fa-brands fa-github'`), never
28
+ a bare name a template wraps
29
+ ([#903](https://github.com/Omega-JS-Stack/omega/issues/903)). One shape to
30
+ author, and every family the brand's set carries is reachable from data.
31
+
32
+ The same string is what a SECTION arg, a page-layout arg and the feature
33
+ catalog's `icon` carry
34
+ ([#929](https://github.com/Omega-JS-Stack/omega/issues/929)): `{% section
35
+ "marketing/stats" %}` takes `icon: 'fa-brands fa-figma'`, and every template
36
+ and runtime builder emits the value verbatim, adding only its own size and
37
+ spacing classes. One key, one shape, wherever it lives. The two keys that name
38
+ a PLATFORM rather than an icon (the footer's `socials` block and a team
39
+ member's link `id`) stay platform keys: a social profile is always a brand
40
+ mark, so the family is derived at the one site that knows it and a brand never
41
+ types a class for a platform it only named.
42
+
25
43
  ## The two halves
26
44
 
27
45
  | Half | When | What happens |
@@ -35,12 +35,12 @@ omega dev --full # boot on the WHOLE manage walk, not just the boo
35
35
  - **The boot cycle never waits on a human** ([#228](https://github.com/Omega-JS-Stack/omega/issues/228)): it runs non-interactive, so any step that needs a person — a console confirm, a consent flow, a secret paste — steps aside into the run summary's ⚑ pending list, each item naming its `npm run manage -- --service=<name>` rerun. The dev legs boot regardless of what is pending; only real errors (broken config, an unloadable brand) stop the boot. A never-setup brand and a fully setup one behave identically here — the difference shows up as a longer pending list, not as a stalled terminal. A dead Google grant is pending too, not an error: the service warns into the same list and the walk carries on. One consequence on a `--full` boot: its web build uses the committed translation cache only (`--cached-only`, the same determinism law the pipeline and deploys follow), so new strings translate on the next interactive manage or build.
36
36
  - **Default set = `web` + `backend`** — the local web loop. GUI/watcher targets (desktop opens an Electron window; extension runs a build watcher) never boot unless named via `--target=`/`--all` ([#780](https://github.com/Omega-JS-Stack/omega/issues/780)). The default also adapts: a web-only brand boots just web, no warning.
37
37
  - **Legs**: web → the target's `npm start` (`omega dev`, :4000); backend → `npm run emulator` (auth/firestore/functions/database/hosting + seeded personas). Backend boots first so its port map is published before web reads it, and the order is optional either way ([#346](https://github.com/Omega-JS-Stack/omega/issues/346)): the web dev server rewrites each page's port map into the response as it serves it, so a backend that publishes later reaches the browser on the next request instead of never.
38
- - **Multi-instance web ports offset deterministically**: each instance wants the classic base + its position in the `targets.web` instances array (targets/website → :4000, targets/website-admin → :4001), so instances dev side-by-side from their own target dirs; the N7 allocator still bumps if the offset port happens to be taken, and pins (`--port` / config `ports.website`) win verbatim. Single-object brands stay on :4000 exactly as before.
38
+ - **Sibling web ports offset deterministically** ([#886](https://github.com/Omega-JS-Stack/omega/issues/886)): each web target wants the classic base + its position among the brand's `web`-type targets (targets/web → :4000, targets/admin → :4001), so they dev side-by-side from their own target dirs; the N7 allocator still bumps if the offset port happens to be taken, and pins (`--port` / config `ports.website`) win verbatim. A brand with one web target stays on :4000 exactly as before.
39
39
  - **Per-target Node**: each leg spawns under its target's own `.nvmrc` major (web and backend pin different ones).
40
40
  - **A leg targets the `https` port, never `hosting`** ([#795](https://github.com/Omega-JS-Stack/omega/issues/795)): the ports file publishes both, and only `https` is the public origin under the local certificate; `hosting` is the internal plain-http port the mkcert proxy forwards to, so a consumer process aimed there is talking to the proxy's back door instead of the address every other surface uses. Every leg `omega dev` spawns carries `NODE_EXTRA_CA_CERTS=<mkcert -CAROOT>/rootCA.pem` (a shell-set value wins verbatim; a host without mkcert gets no key at all), so a brand's own Node process VERIFIES that certificate rather than dying on it. A leg that never speaks TLS ignores the variable, and since this is verification and not a bypass, Node prints no warning. The emulator's and `omega serve`'s own children take the same variable in place of the old `NODE_TLS_REJECT_UNAUTHORIZED=0`, whose boot warning is gone with it; that bypass survives in exactly one place, a host whose mkcert root vanished under certificates already on disk.
41
- - **One terminal covers FRAMEWORK edits too** ([#587](https://github.com/Omega-JS-Stack/omega/issues/587)): when the brand's `@omega.js/*` deps RESOLVE into a monorepo checkout, the boot starts that monorepo's watch as a session child — no flag, no second terminal, so editing a framework's `src/` rebuilds its `dist/` while the stack runs. Resolution answers the question, never the manifest (a `file:` spec can be stale, and workspace hoisting puts the copy at the brand root): `resolveLinkedMonorepo(brandRoot)`. Lock-aware — a watch already running (root `npm start`, or another brand's session) is REUSED, never doubled — session-scoped, so it dies with the stack, and its output rides the `[watch]` tag into `logs/dev.log`. A registry-installed brand has nothing to watch and says so in one line. It starts AFTER the freshness sweep on purpose: the sweep's verdict depends on whether a watch holds the lock ([#281](https://github.com/Omega-JS-Stack/omega/issues/281)/[#398](https://github.com/Omega-JS-Stack/omega/issues/398)), so starting one first would change the answer it just gave. A FRESH watch's initial prepare rewrites every package's `dist/` as it runs, so the boot WAITS for that pass to land ([#670](https://github.com/Omega-JS-Stack/omega/issues/670)) — the helper's `ready` promise resolves once every watched package has printed its `Ready for changes!` line (or the moment the watch child exits without reporting, or after 120s with one warning naming the stragglers), and only then do the legs boot, so nothing loads a CLI out from under the rewrite. **An ALREADY-RUNNING watch gates the same way** ([#622](https://github.com/Omega-JS-Stack/omega/issues/622)) — the branch a linked brand normally takes, since the root `npm start` is usually already up: with no child stdout to read, the boot tails that watch's own `.temp/logs/watch-all.log` (believed only when its pid header IS the lock owner's) through the same tracker, and also waits out a vendor-propagation pass holding `.omega/vendor-propagation.lock`. A settled watch answers off the file with no wait at all; a watch that dies mid-wait settles it at once. Same helper as the website target's `--local` prelude — one implementation, two callers.
41
+ - **One terminal covers FRAMEWORK edits too** ([#587](https://github.com/Omega-JS-Stack/omega/issues/587)): when the brand's `@omega.js/*` deps RESOLVE into a monorepo checkout, the boot starts that monorepo's watch as a session child: no flag, no second terminal, so editing a framework's `src/` rebuilds its `dist/` while the stack runs. Resolution answers the question, never the manifest (a `file:` spec can be stale, and workspace hoisting puts the copy at the brand root): `resolveLinkedMonorepo(brandRoot)`. Lock-aware: a watch already running (root `npm start`, or another brand's session) is REUSED, never doubled, session-scoped, so it dies with the stack, and its output rides the `[watch]` tag into `logs/dev.log`. A registry-installed brand has nothing to watch and says so in one line. It starts AFTER the freshness sweep on purpose: the sweep's verdict depends on whether a watch holds the lock ([#281](https://github.com/Omega-JS-Stack/omega/issues/281)/[#398](https://github.com/Omega-JS-Stack/omega/issues/398)), so starting one first would change the answer it just gave. A FRESH watch's initial prepare rewrites every package's `dist/` as it runs, so the boot WAITS for that pass to land ([#670](https://github.com/Omega-JS-Stack/omega/issues/670)): the helper's `ready` promise resolves once every watched package has printed its `Ready for changes!` line (or the moment the watch child exits without reporting, or after 120s with one warning naming the stragglers), and only then do the legs boot, so nothing loads a CLI out from under the rewrite. **An ALREADY-RUNNING watch gates the same way** ([#622](https://github.com/Omega-JS-Stack/omega/issues/622)): the branch a linked brand normally takes, since the root `npm start` is usually already up: with no child stdout to read, the boot tails that watch's own `.temp/logs/watch-all.log` (believed only when its pid header IS the lock owner's) through the same tracker, and also waits out a vendor-propagation pass holding `.omega/vendor-propagation.lock`. A settled watch answers off the file with no wait at all; a watch that dies mid-wait settles it at once. Same helper as the web target's `--local` prelude: one implementation, two callers.
42
42
  - **Every seeded persona is a FULL account** ([#327](https://github.com/Omega-JS-Stack/omega/issues/327)): identical in shape AND in substance to a real user — a name, a place, a phone, a company, a birthday, and the device and IP it signed up from — so a persona renders like a customer on every surface that shows a person. The profile is DERIVED from the persona key (`packages/backend/src/test/test-accounts.js` `seededProfile`), so it is the same on every boot and a new persona is born complete; anything a persona names for itself wins. Paid personas carry the provider record a real purchase leaves (provider, resource, order, term start, the event that last wrote it), which is what payment-gated surfaces — the billing card's save offer — read before they show anything. Established personas also carry the two lists an account accumulates ([#343](https://github.com/Omega-JS-Stack/omega/issues/343)): the REFERRALS an affiliate link earned (`{ uid, timestamp }`, exactly as a signup appends them, and naming other personas — so whether a referral converted is the referred account's own subscription state) — carried by the DEDICATED `referrer` persona and nothing else ([#363](https://github.com/Omega-JS-Stack/omega/issues/363)), because a persona demonstrates exactly its own scenario — and the ACTIVE SESSIONS of the two or three devices they are signed in on (Realtime Database records under `sessions/app`, seeded on boot and restored by "Reset to seed", because they live outside the user doc). Demo-safe by construction: documentation-range IPs, fictional 555 numbers, invented companies.
43
- - **The dev palette** (the DEV pull-tab the web framework renders in development): one-click sign-in as the seeded personas, including the four billing-journey accounts ([#215](https://github.com/Omega-JS-Stack/omega/issues/215)), and a "Reset to seed" button on any signed-in test account — a development-only backend route rebuilds the account from its canonical seed shape. The switcher's roster is the SEED's own ([#400](https://github.com/Omega-JS-Stack/omega/issues/400)): the backend labels each human-facing persona (`palette: '<Label>'` in `packages/backend/src/test/test-accounts.js`) and serves that list, in declaration order, at `GET /omega/test/roster`; the palette fetches it on build and keeps no copy, so a persona seeded today is switchable today and the machinery an automated suite drives never appears. The WHOLE seed is read, both halves ([#712](https://github.com/Omega-JS-Stack/omega/issues/712)): a project's own personas from its `test/_init.js` carry the same label and are offered after the framework's, so a consumer persona is switchable the moment it is seeded. The address the palette signs in on is the brand's HOST, and so is the one the seeder creates ([#708](https://github.com/Omega-JS-Stack/omega/issues/708)) — ONE derivation, `@omega.js/config`'s `resolvedBrandHost` (an instance's `url`, else `brand.url`), because a brand answering support mail on the apex while the site sits on a subdomain used to seed accounts the palette could never sign in as. There is no fallback list: with no emulator up the dropdown holds its placeholder alone and the "Signed in as" line carries the reason on a second line, under whoever you are signed in as (that readout is composed, so an auth state settling after the failure lands beside it rather than on top of it). A page opened while the emulator is still booting does not stay stuck there ([#402](https://github.com/Omega-JS-Stack/omega/issues/402)): a spinner and a "Backend starting…" line sit at the top of the panel and the roster is fetched again every two seconds, forever, until it answers; the attempt that lands fills the dropdown, preselects the signed-in persona, and clears both the indicator and the explanation, so no reload is needed. A built-in **Storage** section ([#390](https://github.com/Omega-JS-Stack/omega/issues/390)) covers the client blob: a target dropdown ("All" first, then every top-level key, re-derived from the LIVE blob each time the panel opens) with Log and Clear buttons acting on the selected target — Log prints the parsed value into the console, Clear removes it, and nothing asks you to confirm. Source: `packages/web/core/js/core/dev-palette.js`. Pages contribute their own controls via `registerDevSection` (`core/js/core/dev-sections.js`, [#234](https://github.com/Omega-JS-Stack/omega/issues/234)): sections render on open and merge by id — the checkout page's controls (product, frequency, pre-delay, trial, card provider, reCAPTCHA, the decline toggle) live in the palette's Checkout section, and every one of them applies the same way — Apply & reload navigates with its param set, the decline arm included (`_dev_decline`, which stays armed until you apply it away); the page's old gear dropdown is gone.
43
+ - **The dev palette** (the DEV pull-tab the web framework renders in development): one-click sign-in as the seeded personas, including the four billing-journey accounts ([#215](https://github.com/Omega-JS-Stack/omega/issues/215)), and a "Reset to seed" button on any signed-in test account, a development-only backend route rebuilds the account from its canonical seed shape. The switcher's roster is the SEED's own ([#400](https://github.com/Omega-JS-Stack/omega/issues/400)): the backend labels each human-facing persona (`palette: '<Label>'` in `packages/backend/src/test/test-accounts.js`) and serves that list, in declaration order, at `GET /omega/test/roster`; the palette fetches it on build and keeps no copy, so a persona seeded today is switchable today and the machinery an automated suite drives never appears. The WHOLE seed is read, both halves ([#712](https://github.com/Omega-JS-Stack/omega/issues/712)): a project's own personas from its `test/_init.js` carry the same label and are offered after the framework's, so a consumer persona is switchable the moment it is seeded. The address the palette signs in on is the brand's HOST, and so is the one the seeder creates ([#708](https://github.com/Omega-JS-Stack/omega/issues/708)): ONE derivation, `@omega.js/config`'s `resolvedBrandHost` (a target's own `url`, else `brand.url`), because a brand answering support mail on the apex while the site sits on a subdomain used to seed accounts the palette could never sign in as. There is no fallback list: with no emulator up the dropdown holds its placeholder alone and the "Signed in as" line carries the reason on a second line, under whoever you are signed in as (that readout is composed, so an auth state settling after the failure lands beside it rather than on top of it). A page opened while the emulator is still booting does not stay stuck there ([#402](https://github.com/Omega-JS-Stack/omega/issues/402)): a spinner and a "Backend starting…" line sit at the top of the panel and the roster is fetched again every two seconds, forever, until it answers; the attempt that lands fills the dropdown, preselects the signed-in persona, and clears both the indicator and the explanation, so no reload is needed. A built-in **Storage** section ([#390](https://github.com/Omega-JS-Stack/omega/issues/390)) covers the client blob: a target dropdown ("All" first, then every top-level key, re-derived from the LIVE blob each time the panel opens) with Log and Clear buttons acting on the selected target: Log prints the parsed value into the console, Clear removes it, and nothing asks you to confirm. Source: `packages/web/core/js/core/dev-palette.js`. Pages contribute their own controls via `registerDevSection` (`core/js/core/dev-sections.js`, [#234](https://github.com/Omega-JS-Stack/omega/issues/234)): sections render on open and merge by id: the checkout page's controls (product, frequency, pre-delay, trial, card provider, reCAPTCHA, the decline toggle) live in the palette's Checkout section, and every one of them applies the same way: Apply & reload navigates with its param set, the decline arm included (`_dev_decline`, which stays armed until you apply it away); the page's old gear dropdown is gone.
44
44
  - **The palette is the ONLY dev-testing surface** (Ian's ruling, [#342](https://github.com/Omega-JS-Stack/omega/issues/342), extending [#329](https://github.com/Omega-JS-Stack/omega/issues/329)): no client-side test personas as code objects, no helper hung on `window`, no dev switch you can only reach by already knowing its name — hidden functions get forgotten in six months. Alongside Checkout the panel now carries **Tools** (log opening tags, toggle theme), **Icons** (re-run the missing-icon audit), **Download** (open the onboarding walkthrough for a platform), **Extension** (fire a browser's install click) and **OAuth** (rehearse a returning provider redirect: returning user, new user, credential conflict). The account page carries NO control of its own: its referrals and sessions lists were the last client-side fixtures (`?_dev_prefill`), and the personas carry both now ([#343](https://github.com/Omega-JS-Stack/omega/issues/343)) — switching persona IS the affordance. Every one of them is a `@dev-only` block, so a production build ships neither the control nor what it unlocks. The rule outlives the sweep as a guard: `packages/web/test/dev-hooks-guard.test.js` fails on a new `_dev_*` hook or `window.<helper>` in client code, on a listed hook no palette section actually offers, and on any hook read outside a `@dev-only` block.
45
45
  - **Google/OAuth signin uses the redirect flow here, same as production** ([#156](https://github.com/Omega-JS-Stack/omega/issues/156)): the auth emulator returns its credential through storage on the origin it is served from, so `omega dev` proxies the emulator under the SITE origin and the return leg survives the browser's storage partitioning. Details in [docs/web/index.md](../web/index.md).
46
46
  - Output is line-prefixed per target (`[backend] …`, `[web] …`); one Ctrl-C stops everything; a leg dying alone is announced and its siblings stay up.
@@ -86,7 +86,11 @@ Unchanged contract for consumers, now monorepo-backed: `mgr i local` (web, deskt
86
86
 
87
87
  **The brand root itself is part of the tree (cp195):** onboard scaffolds `@omega.js/manager` into the brand root's devDependencies — the omega-bin dispatcher resolves brand-level verbs (`omega dev`, manage, the scaffolded `start`/`manage` scripts) FROM the brand root, and without the declaration nothing installs the manager outside the monorepo (inside it, workspace hoisting masked the gap). `linkLocalPackages()` links it like any target dep (`discoverTargets` already includes the brand root).
88
88
 
89
- **Outside brands may COMMIT relative `file:` specs — the real brand does (cp229):** `../omega-brand` (folder renamed from omegajs.dev, cp235) declares every `@omega.js/*` dep as `file:../../../omega/packages/<name>` (brand root: `file:../omega/packages/manager`), the same pattern the in-repo brands already use at their own depth. Clone the two repos side by side and a plain `npm install` links the whole tree with zero linker involvement; `linkLocalPackages()` all-skips because the specs already resolve into the monorepo. At first publish the specs flip to exact `0.1.0` registry pins (versions re-reset to 0.1.0 at cp238 — 0.x until live publishes are proven — so the flip stays seamless).
89
+ **Outside brands may COMMIT relative `file:` specs — the real brand did in the local era (cp229):** `../omega-omega` declared every `@omega.js/*` dep as `file:../../../omega/packages/<name>` (brand root: `file:../omega/packages/manager`), the same pattern the in-repo brands still use at their own depth. Clone the two repos side by side and a plain `npm install` links the whole tree with zero linker involvement; `linkLocalPackages()` all-skips because the specs already resolve into the monorepo. Since the first publish (0.50.0) the real brand pins the exact registry version instead; the in-repo brands stay on `file:` specs by ruling (2026-09-10).
90
+
91
+ **A linked brand reaches CI as TARBALLS, not as a checkout** ([#872](https://github.com/Omega-JS-Stack/omega/issues/872)): a runner has nothing at the path `file:` specs name, so `omega deploy` packs every linked package into the brand's `omega_modules/` and pushes that snapshot to the brand repo, and the runner installs it with a plain `npm install`. Nothing is asked of the consumer or of the CI tool, and nothing framework-aware runs on the box. The lane, the pack step and the two hops: [deploys.md](deploys.md).
92
+
93
+ **`npx omega` is only ever run inside an installed target** ([#881](https://github.com/Omega-JS-Stack/omega/issues/881)): with no local bin npx fetches a stranger's package named `omega` from the registry, and the plugin's npx hook refuses that in agent shells.
90
94
 
91
95
  ## Vendoring vs runtime deps (what ships where)
92
96
 
@@ -124,9 +128,14 @@ Every framework CLI boot (web/backend/desktop/extension/manager — `freshnessBo
124
128
 
125
129
  **With the watch DOWN, the boot heals the link itself** ([#398](https://github.com/Omega-JS-Stack/omega/issues/398), Ian's call 2026-08-20). The stop is right only while something else owns the rebuild; with no watch running it is pure friction — the guard already knows the watch is down (the `.omega/dev-watch.lock` pid probe it warns from), so a stale linked dist takes the ordinary heal: one `npm run prepare` in place under the package's heal lock, `rebuilt` by `self`, then the boot's re-exec. The hazard #281 named is narrowed, not dismissed: the heal lock still serializes the CLIs booting on that package, and the largest concurrent writer is by definition not running. The once-per-process watch-down warning STAYS on this path — a stale linked dist is healed once at boot, and a framework edit made during the session still needs `npm start` for live rebuilds. **And a heal that FAILS is a loud stop, not a shrug**: the outcome is `heal-failed`, and the boot exits 1 naming the package, the staleness, the prepare's exit status and the `npm run prepare` to run by hand in that checkout. A plain checkout keeps the old warn-and-continue (its dist is the developer's own, and nothing shared hangs on this boot), but a monorepo link has no watch coming to land the build, so continuing would serve code nobody built — the silent fallback #281 forbids. `omega dev`'s hoisted sweep returns those entries as `healFailed` and stops the fan-out before any leg spawns, exactly as it does for `staleLinked`.
126
130
 
127
- **The INSTALL path is guarded at the prepare script** ([#350](https://github.com/Omega-JS-Stack/omega/issues/350)). npm runs a `file:`-linked dependency's `prepare` INSIDE the linked checkout, so `omega i local`, `omega dev --local` and any plain `npm install` in a linked brand rebuilt a monorepo package in place through the one door a CLI guard cannot see — npm invokes prepare, devkit never gets a say. Every dist-building package's prepare now opens with devkit's gate: `node -e "require('@omega.js/devkit/prepare-guard')() && require('prepare-package')()"` (`packages/devkit/tools/prepare-guard.js`). When npm's install root (`npm_config_local_prefix`, `INIT_CWD` as the fallback) lies OUTSIDE the monorepo that contains the package, the build is skipped — a false return short-circuits the `&&`, exit 0, so the consumer's install completes normally — with one stderr line naming the package, the install that reached in, and the watch that owns the dist. It fires for every TREE-TOUCHING command from outside — `install`, `ci`, `update`, `rebuild`, `dedupe`, the five npm reports in `npm_command` that re-run a linked package's prepare (npm 11.12.1) — and for nothing else: the monorepo's own root install, workspace installs (`-w packages/web`), an install typed inside a package or an in-repo app, `npm run prepare`, and the `npm pack`/`npm publish` prepare lane all build exactly as before, so a published tarball still carries a freshly built dist. npm buffers lifecycle output, so the notice surfaces under `--foreground-scripts`.
131
+ **The INSTALL path is guarded at the prepare script** ([#350](https://github.com/Omega-JS-Stack/omega/issues/350)). The gate is required by a RELATIVE path (`../devkit/tools/prepare-guard`) and borrows nothing from the rest of devkit, because a linked brand's install reifies its `file:` links, and so runs this prepare, before there is any `node_modules/@omega.js/devkit` link to resolve through; and a checkout with no root `node_modules` at all skips its prepare outright, there being nothing to build with ([#868](https://github.com/Omega-JS-Stack/omega/issues/868)). npm runs a `file:`-linked dependency's `prepare` INSIDE the linked checkout, so `omega i local`, `omega dev --local` and any plain `npm install` in a linked brand rebuilt a monorepo package in place through the one door a CLI guard cannot see: npm invokes prepare, devkit never gets a say. Every dist-building package's prepare IS devkit's gate: `node -e "require('../devkit/tools/prepare-guard').run()"` (`packages/devkit/tools/prepare-guard.js`), which runs the decision and then prepare-package itself. When npm's install root (`npm_config_local_prefix`, `INIT_CWD` as the fallback) lies OUTSIDE the monorepo that contains the package, the build is skipped (prepare-package is never even loaded, exit 0, so the consumer's install completes normally) with one stderr line naming the package, the install that reached in, and the watch that owns the dist. It fires for every TREE-TOUCHING command from outside: `install`, `ci`, `update`, `rebuild`, `dedupe`, the five npm reports in `npm_command` that re-run a linked package's prepare (npm 11.12.1), and for nothing else: the monorepo's own root install, workspace installs (`-w packages/web`), an install typed inside a package or an in-repo app, `npm run prepare`, and the `npm pack`/`npm publish` prepare lane all build exactly as before, so a published tarball still carries a freshly built dist. npm buffers lifecycle output, so the notice surfaces under `--foreground-scripts`.
132
+
133
+ **The gate keeps itself wired, even when the prepare fails** ([#870](https://github.com/Omega-JS-Stack/omega/issues/870)). prepare-package OWNS `scripts.prepare`: every full build rewrites the manifest with its own canonical one-liner, which would silently drop the gate on the monorepo's next prepare. Two things put it back, both through the one `ensureGuarded()`/`rewire()` pair in `prepare-guard.js`, so no lane carries a copy of the gated string:
134
+
135
+ 1. **`rewire()` is the FIRST `preparePackage.hooks.after` command of every package**, ahead of the vendor hook, because the manifest write is the last thing prepare-package does before its hooks. It restores the gated line in prepare-package's own formatting (idempotent and narrow: it writes only when the manifest holds prepare-package's exact string, and never touches a prepare it does not own), and running first is what covers the `prepare:watch` lane, which never goes through `scripts.prepare` at all. Ordering used to be the other way, and a vendor hook that failed (twice: a devkit source string that looked like a package-internal require) stopped the chain before the rewire entry and left the manifest UNGUARDED.
136
+ 2. **`run()` calls `ensureGuarded()` in a `finally`**, so the `prepare` lane ends gated whether the build succeeded, a hook exited nonzero or prepare-package itself threw, whatever the hook order says. The failure stays loud: the rejection is returned, so the script still exits nonzero.
128
137
 
129
- **The gate keeps itself wired.** prepare-package OWNS `scripts.prepare`: every full build rewrites the manifest with its own canonical one-liner, which would silently drop the gate on the monorepo's next prepare. So each package's `preparePackage.hooks.after` is now a two-command chain — the vendor hook, then `require('@omega.js/devkit/prepare-guard').rewire()` — which runs after that manifest write in BOTH the `prepare` and `prepare:watch` lanes and puts the gated line back, in prepare-package's own formatting so a settled `package.json` never churns. It is idempotent and narrow: it writes only when the manifest holds prepare-package's exact string, and never touches a prepare it does not own. Pinned by devkit `test/prepare-guard.test.js` (22 tests: the decision table, the notice, and real-npm/real-prepare-package fixture runs — including the red baselines, an unguarded consumer install rebuilding the linked package and a real build overwriting the gate).
138
+ **And the freshness heal refuses to leave a manifest its prepare stripped**: after its `npm run prepare` the heal calls that same `ensureGuarded()` on the package it prepared (the guard loaded from the devkit beside it), which covers the one case a `finally` cannot reach, a prepare killed outright. Pinned by devkit `test/prepare-guard.test.js` (27 tests: the decision table, the notice, real-npm/real-prepare-package fixture runs including the red baselines, a failing after hook that still ends gated, the heal on a stripped manifest, and a sweep asserting every dist-building `package.json` carries the gate with `rewire()` first).
130
139
 
131
140
  The consequence is deliberate: with that watch running, a linked dist that is missing, unbuilt or stale FAILS the consumer build loudly, before the verb runs, naming the package, the evidence, the checkout it will not touch, and the watch already on the job:
132
141
 
@@ -145,3 +154,12 @@ There is no silent fallback and no staging copy: the build runs on a dist the wa
145
154
  **A live watch buys a bounded grace, not blind trust.** Stale with the root-watch lock held → recheck for ~2s so an in-flight watcher copy can land (dim note, `rebuilt` by `watch` if it does — still a heal, so the boot re-execs onto it), then the verdict: a monorepo link stops the invocation, any other local checkout heals here. **A dead watcher is loud**: a monorepo-linked boot with no live lock prints one stderr line naming the fix (`npm start` in the monorepo) — the boot's own heal ([#398](https://github.com/Omega-JS-Stack/omega/issues/398)) repairs a STALE dist once, at boot, and nothing propagates a src edit made afterwards, so the warning still carries the whole story (it fires on a fresh dist too, so it states that standing deal rather than claiming a heal that may not have happened). Both surfaced from the freshness path, so all five framework CLIs get them for free.
146
155
 
147
156
  **Heals are locked per package** (a local checkout outside this monorepo, a monorepo link with the watch down, and the watch's own prepares): a mkdir-as-mutex at `<package>/.omega/heal.lock` with the owner pid inside (the `withStateLock` idiom from `deploy-record.js`; pid liveness replaces its mtime age because a real prepare holds the lock for minutes). The loser waits, then RE-CHECKS freshness — the winner's build makes it a no-op, so two or ten CLIs booting on the same stale link produce one build. Abandoned locks (owner gone — an EPERM pid is LIVE, never stolen) are stolen; a wait past the deadline proceeds unlocked rather than failing a boot, and lock bookkeeping that cannot be written (read-only fs, full disk) degrades to unlocked instead of throwing out of a heal. **The vendor-propagation watcher takes the same lock** around its own `npm run prepare` spawn (`startVendorPropagation`, held for the child's whole life, awaited async so the watcher keeps serving its other watches) — that spawn is devkit's own, so it locks; only prepare-package's INTERNAL src→dist copying stays unlocked third-party territory, and it converges on the watcher's next change event.
157
+
158
+ ## Boot order: freshness, preludes, verb ([#890](https://github.com/Omega-JS-Stack/omega/issues/890))
159
+
160
+ Every framework CLI boot (web/backend/desktop/extension/manager) runs exactly two steps before the verb, in this order, with the identical call on all five:
161
+
162
+ 1. **the freshness guard** above: a stale locally-linked `dist/` heals (or stops the boot loudly) and the invocation re-execs, so no verb ever runs stale framework code;
163
+ 2. **the boot preludes** (`@omega.js/devkit/preludes`, `runPreludes({ verb: process.argv[2], targetDir: process.cwd() })`): the cheap, non-interactive checks the whole CLI surface has to settle first, each declaring the verbs it targets (`'all'`, one, or a list) and each a no-op on the ordinary. The list, the contract and the preludes that ship are in [devkit/index.md](../devkit/index.md#boot-preludes-890).
164
+
165
+ The order is the point: the preludes run in the process that will run the verb, which only the freshness step can guarantee is the current framework code. First shipped prelude: the `origin` heal, so a brand whose repo moved is pointed at the address GitHub's redirect answers on the next verb, whichever verb that is.
@@ -106,7 +106,9 @@ surface attaches it at its entry point.
106
106
  rotation (the ruled retention, see below).
107
107
  - **Stackable.** `createTee()` returns an independent tee; an attach captures the
108
108
  CURRENT writers, so tees nest and each detach restores exactly what it found (LIFO).
109
- The default export is the process-wide singleton, which is what a CLI verb wants.
109
+ The default export is the process-wide singleton, which is what a CLI verb wants; two
110
+ attaches of different paths on the singleton stack the same way (a verb run inside
111
+ another verb's process), and its `detach()` pops the newest.
110
112
  - **`createChildLog()` for spawned children.** A child's stdout/stderr never pass
111
113
  through this process' writers; the caller mirrors each buffer to the terminal —
112
114
  which the verb's own tee then catches, making the verb log a SUPERSET of the child
@@ -120,7 +122,7 @@ surface attaches it at its entry point.
120
122
 
121
123
  ## Where every log lives
122
124
 
123
- `<targetRoot>` is a target dir in a brand (`targets/website`, `targets/backend`, …);
125
+ `<targetRoot>` is a target dir in a brand (`targets/web`, `targets/backend`, …);
124
126
  `<brandRoot>` is the brand monorepo root.
125
127
 
126
128
  | Surface | File | What's in it |
@@ -129,23 +131,24 @@ surface attaches it at its entry point.
129
131
  | `omega dev` (web) · `omega serve` / `omega emulator` (backend) · `npm start` (desktop, extension) | `<targetRoot>/logs/dev.log` | the whole dev run: boot, ports, watcher rebuilds, the crash — plus every child chunk the verb mirrored (see below) |
130
132
  | `omega build` (web, backend) · production gulp build (desktop, extension) | `<targetRoot>/logs/build.log` | the whole production build |
131
133
  | `omega test` | `<targetRoot>/logs/test.log` | suite names, pass/fail, harness boot lines |
134
+ | `omega deploy` (web, backend, extension, desktop) | `<targetRoot>/logs/deploy.log` | the whole deploy: the scaffold, the precheck and its refusals, the dispatch, then the followed run's job logs and its verdict ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)) |
132
135
  | **Backend children** — firebase's own processes, beside firebase-tools' debug logs. The verb mirrors every child chunk to its own terminal, so the `logs/<verb>.log` above is a SUPERSET of these; a child file is the child-ONLY view (and the one that `roll()`s mid-run) | | |
133
136
  | the firebase emulator child | `<targetRoot>/dist/emulator.log` | emulator traffic: function invocations, Firestore/auth calls |
134
137
  | the `firebase serve` child | `<targetRoot>/dist/dev.log` | serve output; rolls on each reload |
135
138
  | the test runner child | `<targetRoot>/dist/test.log` | the runner's own output under `omega test` |
136
- | `omega deploy` | `<targetRoot>/dist/deploy.log` | the deploy transcript |
139
+ | `omega deploy --direct` (the firebase child) | `<targetRoot>/dist/deploy.log` | the `firebase deploy` transcript (the verb's own record is `logs/deploy.log`, one row up) |
137
140
  | `omega logs` | `<targetRoot>/dist/production.log` | the Cloud Logging tail |
138
141
  | firebase-tools itself | `<targetRoot>/*-debug.log` | `firestore-debug.log`, `firebase-debug.log`, `ui-debug.log`, … — theirs, never swept by us |
139
142
  | **Desktop extras** | | |
140
143
  | the running app itself (main + preload + renderer converge) | `<targetRoot>/logs/runtime.log` (dev) · the OS log dir (packaged) | lifecycle, window and updater lines; kept across boots, rotating at 10 MB — `packages/desktop/docs/logging.md` |
141
144
  | `npx omega logs [runtime\|dev\|build\|test]` (desktop's own verb — read, not write) | tails whichever of the four `<targetRoot>/logs/` files was named, `runtime` by default | the print/follow/open surface for all of the above; backend's `omega logs` is a different verb (the Cloud Logging tail, one row up) |
142
- | `npm run release` | `<targetRoot>/logs/ci.log` | the GH Actions release run, streamed locally |
145
+ | `npm run release` (the same dispatch `omega deploy` delegates to) | `<targetRoot>/logs/deploy.log` | the GH Actions release run, streamed locally: one name for every target's deploy ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)), where this was `logs/ci.log` |
143
146
  | Windows code-signing | `<targetRoot>/logs/signing.log` | JSONL signing events (local fallback; on CI it lands in the runner home) |
144
147
  | **Brand root** — every verb tees to its OWN `logs/<verb>.log` ([#623](https://github.com/Omega-JS-Stack/omega/issues/623)), so one verb never truncates another's record. A FAN-OUT log holds the walk's own verdict — its header, its loud skips, its summary — because `runCommand` spawns each target with stdio inherit, so a target's output goes past the tee into that target's own log above | | |
145
148
  | `omega manage` (the service walk) | `<brandRoot>/logs/manage.log` | the whole service walk |
146
149
  | `omega dev` (the fan-out) | `<brandRoot>/logs/dev.log` | the boot walk, then every dev leg's prefixed output (consecutive duplicate lines collapse to one ` (repeated N×)` note) |
147
150
  | `omega build` / `omega clean` (the fan-outs) | `<brandRoot>/logs/build.log` · `<brandRoot>/logs/clean.log` | the walk order, every loud skip, the per-target summary |
148
- | `omega deploy` (the fan-out) | `<brandRoot>/logs/deploy.log` | the delivery lane, then which target published in which order and the summary — the backend's own transcript additionally lands in `targets/backend/dist/deploy.log` (the other targets keep no per-target deploy log) |
151
+ | `omega deploy` (the fan-out) | `<brandRoot>/logs/deploy.log` | the delivery lane, then which target published in which order and the summary. Every target additionally keeps its own `<targetRoot>/logs/deploy.log` ([#873](https://github.com/Omega-JS-Stack/omega/issues/873)), and the backend its firebase transcript `targets/backend/dist/deploy.log` |
149
152
  | `omega update` (the fan-out) | `<brandRoot>/logs/update.log` | which target was checked and what it reported/applied |
150
153
  | `omega test` (the fan-out) | `<brandRoot>/logs/test.log` | which target ran which scope, and the aggregate verdict |
151
154
  | `omega pipeline` (the live full-cycle test) | `<brandRoot>/logs/pipeline.log` | the child invocation, the deploy/verify legs, the scorecard and the PASS/FAIL verdict |
@@ -195,7 +198,7 @@ emulator or watcher belongs to the user: never restart one, and never re-run a s
195
198
  just to see output.
196
199
 
197
200
  ```bash
198
- tail -50 targets/website/logs/dev.log # is the dev server up, what did it last build
201
+ tail -50 targets/web/logs/dev.log # is the dev server up, what did it last build
199
202
  grep -i error targets/backend/dist/emulator.log # what the emulator actually served
200
203
  grep '^FAIL' .temp/*/steps.log # which e2e step broke
201
204
  tail -100 .temp/logs/test-packages.log # what the last lane printed
@@ -31,7 +31,7 @@ One block, `monitoring`, in omega.json5, with the monitor named as a KEY under `
31
31
  presence IS the enable signal at runtime — there is no separate runtime `enabled` flag (the same
32
32
  convention every other role section follows: a block's credentials are its switch); the role-level
33
33
  `enabled: false` is the manager's skip switch for the provisioning service. Per-surface DSNs are
34
- `targets.<type>.monitoring.providers.sentry.dsn` overrides.
34
+ `targets.<name>.monitoring.providers.sentry.dsn` overrides.
35
35
 
36
36
  ```jsonc
37
37
  monitoring: {
@@ -79,12 +79,19 @@ In the order they win:
79
79
  | `OMEGA_SENTRY_ENABLED=false` | Kill switch. Nothing reports, ever. |
80
80
  | `OMEGA_TEST_RUNNER` | A test run never pollutes a live project. |
81
81
  | `OMEGA_SENTRY_FORCE=true` | Report from a non-production run (local proving). |
82
- | `OMEGA_BUILD_MODE=true` | The default production signal, for a host with no runtime one. |
83
-
84
- The production signal is the host's to supply. @omega.js/desktop has no runtime answer — "should we
85
- ship telemetry" is a property of its BUILD — so it falls through to `OMEGA_BUILD_MODE`.
86
- @omega.js/backend has one (`Manager.isProduction()`, env-derived and stable for the life of the
87
- process) and passes it in, alongside its own `reportErrorsInDev` option.
82
+ | the ONE environment | The default production signal, for a host that passes no runtime one. |
83
+
84
+ The production signal is the host's to supply, and the DEFAULT is the one environment every OMEGA
85
+ target answers from ([#817](https://github.com/Omega-JS-Stack/omega/issues/817)):
86
+ `@omega.js/config/environment`'s `isProduction()`, read off the host the gates were asked for
87
+ (`process.env.OMEGA_ENVIRONMENT` on Node, the host's baked `config.environment` in a browser-ish
88
+ context such as a renderer bundle). `OMEGA_BUILD_MODE` used to be that default, which made it a
89
+ FIFTH production signal with an opinion of its own: a build-mode run of a development artifact
90
+ reported as production, and a packaged production app whose lane did not carry the flag reported as
91
+ development. @omega.js/backend still passes its own answer in (`Manager.isProduction()`, env-derived
92
+ and stable for the life of the process), alongside its own `reportErrorsInDev` option, and a boolean
93
+ a host supplies always wins. A context that names no environment at all throws by name, the same way
94
+ every other read of the one environment does.
88
95
 
89
96
  ## Release tags
90
97
 
@@ -101,7 +108,7 @@ Each host supplies its own version identity:
101
108
  | `@omega.js/backend` | the functions package version. The id is `brand.id`, falling back to the project id on a config missing one (which already warns at boot). |
102
109
  | `@omega.js/desktop` (main + renderer) | `app.getVersion()` — the packaged app version, with `brand.id` read off the resolved config |
103
110
  | `@omega.js/client` in `@omega.js/extension` | the extension target's package version, baked into the build blob as `config.version` |
104
- | `@omega.js/client` in `@omega.js/web` | the website target's package version, read off the target root's package.json and emitted in the page `Configuration` block as `version` |
111
+ | `@omega.js/client` in `@omega.js/web` | the website target's package version, read off the target root's package.json and baked into the page's `OMEGA_BUILD_JSON.config` as `version` |
105
112
  | `@omega.js/client` in the `@omega.js/desktop` renderer | the desktop target's package version, folded into `OMEGA_BUILD_JSON.config` at bake time (the renderer only ever sees `buildJson.config`) |
106
113
 
107
114
  The client reads `config.version` and falls back to `config.buildTime`. Every host above bakes a
@@ -140,13 +147,20 @@ config: a page never opts its own credentials back into an event ([#661](https:/
140
147
 
141
148
  ## How the config reaches a browser
142
149
 
143
- The client reads a `sentry: { enabled, config }` namespace on its init blob, and every framework
144
- maps it from `monitoring.providers.sentry`:
150
+ The client reads a `sentry: { enabled, config }` namespace on its init blob, and it maps that
151
+ namespace from `monitoring.providers.sentry` ITSELF, once, for every framework
152
+ ([#894](https://github.com/Omega-JS-Stack/omega/issues/894)): each browser surface bakes the
153
+ canonical section into `OMEGA_BUILD_JSON.config` and `_canonicalConfiguration()` in
154
+ `packages/client/src/index.js` turns a DSN's presence into the switch. That home outranks a
155
+ stale `client.sentry` blob, which keeps reporting until a brand migrates
156
+ ([#485](https://github.com/Omega-JS-Stack/omega/issues/485)); with no canonical DSN the off
157
+ state rides first, so the legacy blob still decides.
145
158
 
146
- | Framework | Where |
159
+ | Framework | Where the section is baked |
147
160
  |---|---|
148
- | `@omega.js/web` | the `Configuration` block in `core/_includes/core/foot.html` — `resolved.monitoring.providers.sentry` → `sentry`. A real DSN is emitted AFTER the `resolved.client` loop, so the canonical home outranks a stale `client.sentry` ([#485](https://github.com/Omega-JS-Stack/omega/issues/485)); with no DSN there, the off state rides before the loop, so a brand not yet migrated off `client.sentry` keeps reporting |
149
- | `@omega.js/extension` | `src/gulp/tasks/bundle.js` (`composeBuildConfig`, baked into every bundle) |
161
+ | `@omega.js/web` | the engine's per-build snapshot, written as `<outDir>/build.js` (#743) |
162
+ | `@omega.js/extension` | `src/gulp/tasks/bundle.js` (`composeBuildJson`, written as `dist/build.js`) |
163
+ | `@omega.js/desktop` | `src/gulp/tasks/bundle.js` (`composeClientBuildJson`, written as `dist/build.js` for the renderer) |
150
164
 
151
165
  The client's `sentry.config` is the PROVIDER block, flat — nothing role-level ever rides into
152
166
  `Sentry.init`. Node/Electron hosts pass the whole `monitoring` section instead and core's
@@ -121,8 +121,8 @@ phone home).
121
121
  |---|---|---|---|
122
122
  | web | `omega build` (the production build — on the runner for a deploy, locally for a local one) | `site.license`, a build fact beside `site.pricing`/`site.brandTokens` | the footer's "Powered by omegajs.dev" block renders only while `site.license.attribution == 'shown'` (themes/base `_includes/frontend/sections/footer.html`) |
123
123
  | backend | `omega deploy`, before the stage — the CLI reads the key from the .env cascade in its own process | `OMEGA_LICENSE_STATUS` in the composed `dist/.env` (the one COMPUTED key there; the KEY itself never rides the upload) | `libraries/payment/license.js` refuses Stripe/PayPal/Chargebee `init()` on `keyless`. The `test` provider is never gated, and an ABSENT status — every local lane, the emulator, a test — behaves exactly as before |
124
- | desktop | the bundle task, production builds only | `OMEGA_BUILD_JSON.license` (outside `config`, the blob the renderer hands @omega.js/client) | nothing at runtime: the artifact records what it was packaged as. Neither target has an attribution surface today, and their payments ride the backend's gate |
125
- | extension | the bundle task, production builds only (once per build — the one snapshot every browser target then copies) | `OMEGA_BUILD_JSON.license`, baked into every bundle, likewise outside `config` | as desktop |
124
+ | desktop | the bundle task, production builds only | `OMEGA_BUILD_JSON.license` (outside `config`, in the bundles' define and in the renderer's `dist/build.js`) | nothing at runtime: the artifact records what it was packaged as. Neither target has an attribution surface today, and their payments ride the backend's gate |
125
+ | extension | the bundle task, production builds only (once per build: the one snapshot every browser target then copies) | `OMEGA_BUILD_JSON.license` in the artifact's `build.js`, likewise outside `config` | as desktop |
126
126
 
127
127
  **Honesty system** (spec call 6): plain readable checks, no obfuscation and no artifact
128
128
  signing. The legal backing is the Elastic License 2.0 below, whose terms forbid
@@ -163,7 +163,7 @@ circumventing license-key functionality and removing notices.
163
163
  3. **Verify from the outside**: in an empty temp dir, `npm install @omega.js/web`
164
164
  (and one more, e.g. manager) — install + `require.resolve` must succeed with no
165
165
  overrides. That is the moment the untested-lane risk is retired.
166
- 4. **Flip omega-brand to registry specs**: from any TARGET root (`targets/website`;
166
+ 4. **Flip the real brand (omega-omega) to registry specs**: from any TARGET root (`targets/web`;
167
167
  the manager has no `i` verb), `npx omega i live` — tree-wide `file:` → the EXACT
168
168
  family pin + one registry install (`restoreRegistrySpecs` writes the linked copy's version with no
169
169
  caret, because the family is lockstep; `omega i local` is the way back for
@@ -25,10 +25,10 @@ Ian's durable rulings, migrated verbatim from PROGRESS.md's Rulings lane when th
25
25
  - Standing: checkpoint discipline — survey → design → implement → tests → sandbox/fixture proof → docs → commit; live checks never touch real ITW resources outside sanctioned paths
26
26
  - Ian 2026-07-30: uniformity — commands/surfaces of the same TYPE act the SAME; no split defaults within one family (the CLI read/write emulator split was the offense: every backend CLI subcommand now defaults to the emulator, `--production` the only path to live)
27
27
  - Ian 2026-07-30: NO legacy accommodations in the new system — no code path accepting a superseded form; breaking changes get DOCUMENTED (register: #148) and migrated once, manually (playbook: #149); the config-convert input lane is the one sanctioned legacy-reading exception
28
- - Ian 2026-07-30: company membership is the `.omega/company.json` stamp POINTER — brands never physically nest inside a company folder; anything resolving the company must follow the stamp, never the directory tree
28
+ - Ian 2026-07-30: company membership is a POINTER, never the directory tree: brands never physically nest inside a company folder, and anything resolving the company follows the pointer (SUPERSEDED in mechanism by #677, 2026-09-12: the pointer is the config key `company: { id }`, resolved through the machine registry, and the `.omega/company.json` stamp is retired; the rule itself stands)
29
29
  - Ian 2026-08-06: harmonize at BUILD time, never in a later pass — when a mechanism lands in one framework, its shared home (devkit) and the mirroring evaluation happen in the same work item; "wait for the harmonization pass" is not an accepted answer (first application: the #200 captured-read helper lifted to devkit pre-ship)
30
30
  - Ian 2026-08-20: migrations converge by SHAPE, not by version steps — each fix detects its legacy pattern in the doc itself, converged docs are proven no-ops, still-invalid docs surface loudly in the audit; every future doc reshape adds its convergent fix to the migrations pipeline in the SAME work item (register: docs/shared/breaking-changes.md)
31
31
  - Ian 2026-08-20: writing real data is OPT-IN for one-off scripts/processes — any standalone script that mutates live data (Firestore docs, mailing lists, provider accounts) previews by default and writes only under an explicit `--execute`; the manage/reconciliation services (own `--dry-run` + convergence) and scripts that only write tracked files (git diff is the review) are out of scope; template: the migrations service (#394)
32
32
 
33
- - Ian 2026-09-03: `--target=<a,b>` is the ONE target picker on every brand-root fan-out verb (test, deploy, build, clean, dev, update); `--only`/`--except` are retired, not aliased — `--only` collided with Firebase's own `firebase deploy --only hosting` (register: https://github.com/Omega-JS-Stack/omega/issues/780)
33
+ - Ian 2026-09-03: `--target=<name>[,<name>]` is the ONE target picker on every brand-root fan-out verb (test, deploy, build, clean, dev, update); `--only`/`--except` are retired, not aliased: `--only` collided with Firebase's own `firebase deploy --only hosting` (register: https://github.com/Omega-JS-Stack/omega/issues/780)
34
34
  - Ian 2026-09-03: an ORPHANED process of the emulator family (parent gone, this user's uid, a strict command-shape match) is NOBODY's and every emulator boot reaps it, whatever project it names, with no config and no warning-only mode. The family's shapes are named once, where the reap is described: [backend/index.md](../backend/index.md). Supersedes the #293 line that another brand's orphans stay; the #293 lesson survives as the strict family match replacing the loose name regex (register: https://github.com/Omega-JS-Stack/omega/issues/781)
@@ -35,7 +35,7 @@ Layer names inside a suite are platform-native on purpose: desktop's `main`/`ren
35
35
  | 2d — Desktop auth e2e | The **desktop ↔ backend auth boundary in a REAL Electron app** ([scripts/e2e-desktop-auth.js](../../scripts/e2e-desktop-auth.js)): a consumer app staged from @omega.js/desktop's bundled fixture is built and booted by the real boot runner, then a SECOND Electron instance launches carrying `<brand.id>://auth/token?authToken=…` — the OS single-instance lock forwards that argv to the running app, whose real `second-instance` → deep-link → client-bridge chain signs it in. Both sides of the process boundary are asserted: main's client-bridge on the emulator user, and the renderer's own @omega.js/client Firebase on the same uid (it learned only through the real `desktop:auth:sign-in-with-token` broadcast). Offline by construction — a testing run connects main's auth to the emulator and the staged config points the renderer's client at the same ports | `npm run test:e2e-desktop` at the root | Every checkpoint touching desktop auth, the deep-link routes, or the `/user/token` wire; `OMEGA_SKIP_E2E=1` to skip, and no electron binary (or no built @omega.js/desktop `dist/`) → SKIPPED (exit 0, reason printed) |
36
36
  | 2e — User flows e2e (real browser) | The **CRUCIAL user flows through the actual UI** ([scripts/e2e-flows.js](../../scripts/e2e-flows.js)): headless Chromium against the playground's full emulator suite (seeded personas) plus the REAL `omega dev` — the auth-emulator proxy the provider redirect leg needs lives only there. Five areas — four per spec item of [#155](https://github.com/Omega-JS-Stack/omega/issues/155), plus the billing journeys of [#209](https://github.com/Omega-JS-Stack/omega/issues/209): **auth** (the Google picker's real redirect leg on /signup and again with `authReturnUrl`, `?authSignout=true` + the account page's kick-out, the empty-return loud failure, and the password FORMS — /signin, /signup, /reset), **checkout** (bound state, armed payment buttons, a `_dev_cardProvider=test` payment landing on /payment/confirmation with its order), **verts** (an unfilled slot laddering no-fill → promo, the card content-sized under its slot ceiling, the UTM set on the promo link and the click forwarded out of the frame), **account** (a signed-in policy page rendering the persona's account state from the emulator), **billing journeys** (one paid lifecycle per DEDICATED seeded persona — upgrade, cancel, a declined renewal, a trial converting — each verdict read off the RENDERED account page; the two end states no UI can reach post a hand-built test webhook at the backend exactly as a provider would). **Hard precondition**: `OMEGA_WEBHOOK_KEY` must be in the playground backend's `.env` cascade — the webhook route authenticates on it, and the lane throws `OMEGA_WEBHOOK_KEY is missing from the playground backend's .env cascade` at startup rather than running a crippled pass. Owns its stack: it HOLDS every classic port so both children bump onto fresh ones, so it never disturbs a live dev boot | `npm run test:flows` at the root | Every checkpoint touching auth pages, checkout, the vert ladder, the account page, or the billing lifecycle; `OMEGA_SKIP_E2E=1` to skip, and no puppeteer Chrome → SKIPPED (exit 0, reason printed) |
37
37
  | 2.5 — Wizard journey (outside-monorepo consumer) | The FULL consumer story in a temp brand born OUTSIDE the monorepo: real onboard wizard (flags) → `i local` tree link → `omega dev` boot + branded-homepage probe → headless creds-scrubbed manage (update must build every target) | `npm run test:journey` at the root (also the tail of root `npm test`) | The full sequence, and any change to onboard/linking/boot plumbing |
38
- | 3 — Playground (live rehearsal) | The 24-service manage pipeline against REAL cloud (Firebase, Cloudflare, SendGrid, …) | `npm run pipeline` in `brands/omega-playground` | SPARINGLY — Ian-authorized (real infra, real cost) |
38
+ | 3 — Playground (live rehearsal) | The 24-service manage pipeline against REAL cloud (Firebase, Cloudflare, SendGrid, …) | `npm run pipeline` in `brands/playground-omega` | SPARINGLY — Ian-authorized (real infra, real cost) |
39
39
  | C — Consumer brand e2e | Every OMEGA brand's OWN browser lane, `<brandRoot>/test/e2e/run.js` on [@omega.js/devkit/test/e2e-harness](../../packages/devkit/src/test/e2e-harness.js): the brand's real pages against its real local stack (the backend emulator with its seeded personas plus the real `omega dev`), on allocator ports beside a live dev boot. The brand-root walk, the `--target` picker and the fanned-out flags: [../manager/brand.md](../manager/brand.md) ([#775](https://github.com/Omega-JS-Stack/omega/issues/775)) | `npx omega test` at the brand root, after every target | Every brand checkpoint. Tier 2a IS this lane, run by the sandbox brand |
40
40
 
41
41
  **Opt-in lanes are not a tier and never join a run by accident.** A lane is a set of suites that exist only for the real external thing — credentials, the network, a vendor CLI — plus a GATE deciding whether they may run at all. It is reached exactly one way, `npx omega test --lane=<name>`, and a gate that cannot be satisfied prints ONE skip line and exits clean; the lane's own `test/<lane>/` directory is excluded from discovery unless its gate opened it, so no default run, no CI tier and no path filter can pull it in. Today there is one: backend's `--lane=stripe-live`, which forwards REAL Stripe test-mode webhooks into the local emulator and refuses to open on anything that is not an `sk_test_` secret ([packages/backend/docs/test-framework.md](../../packages/backend/docs/test-framework.md#opt-in-lanes---lane)). Distinct from `--extended`, which is a *mode* letting the same suites make real calls.
@@ -77,7 +77,7 @@ platform rail).
77
77
  `omega.json5 → brand.color` is an optional hex string (`#RGB` / `#RRGGBB`,
78
78
  schema-validated since [#272](https://github.com/Omega-JS-Stack/omega/issues/272);
79
79
  unset leaves the token sheet's neutral placeholder standing) and it drives
80
- `composeBrandTokens()` (`packages/web/src/brand-tokens.js`): a light ramp plus a **dark-mode variant**
80
+ `composeBrandTokens()` (`packages/devkit/src/brand-tokens.js`): a light ramp plus a **dark-mode variant**
81
81
  (darker brands lift into a legible lightness band for the charcoal ground;
82
82
  already-light brands pass through). `core/_includes/core/head.html` emits both
83
83
  as inline `:root` blocks AFTER the css bundles — same three-stamp plumbing as
@@ -86,6 +86,23 @@ the token sheet — so the ramp wins the cascade everywhere, including
86
86
  `.progress-bar`, and `.text-primary`/`.bg-primary` (classy re-points those at
87
87
  the tokens).
88
88
 
89
+ ONE hex, three surfaces ([#912](https://github.com/Omega-JS-Stack/omega/issues/912)).
90
+ Desktop and the extension have no `<head>` of their own to inline into, so their
91
+ sass tasks write the SAME ramp to a generated partial before every compile:
92
+ `<dist>/assets/scss/_brand.scss` on desktop, `<dist>/assets/css/_brand.scss` on
93
+ the extension, both rendered by `renderBrandScss()` beside the ramp math. The
94
+ partial carries `$primary` (the compile-time accent Bootstrap's color ramp
95
+ derives from) plus a `ramp` mixin holding the runtime `--omega-accent` family,
96
+ and the scaffold's `main.scss` reads both: `@use 'brand'` above the framework
97
+ import for the variable, `@include brand.ramp;` below it for the css. The mixin
98
+ is why the include sits below: a used module's css is emitted at its LOAD
99
+ position, which is ahead of the token sheet and theme it has to beat, so a
100
+ root-level rule in the partial would lose to the very placeholders it replaces.
101
+ A brand that WANTS a different accent from `brand.color` puts a literal back in
102
+ the `with (...)` block, in place of `brand.$primary`. No `brand.color` at all,
103
+ and the renderer's own fallback (`#2563EB`, the hex the classy theme declares
104
+ with `!default`) is what compiles.
105
+
89
106
  ## Consumer customization (tier 1 — main.scss)
90
107
 
91
108
  - **Discovery**: `omega customize --list` prints the layered override map —
@@ -594,6 +611,14 @@ the page no longer carries,
594
611
  [#740](https://github.com/Omega-JS-Stack/omega/issues/740)), and
595
612
  `theme.topbar.enabled: false` drops the topbar.
596
613
 
614
+ A link's `icon` carries the FULL Font Awesome class string
615
+ ([#903](https://github.com/Omega-JS-Stack/omega/issues/903)): `icon: 'fa-brands
616
+ fa-github'`, rendered exactly as authored, so the whole chrome spells one key one
617
+ way, the nav, the sidebar, the topbar (its notifications bell included), the page
618
+ header (its title's `theme.header.title.icon` included), the account dropdown,
619
+ the account section header and the footer, and any family the set carries is
620
+ reachable. The mechanism behind the markup: [icons.md](icons.md).
621
+
597
622
  ## Stable-API line (don't churn once consumers exist)
598
623
 
599
624
  Token NAMES · `_config.scss` variable names · `.omega-shell` markup contract
@@ -16,7 +16,7 @@ translation: {
16
16
  languages: ['es', 'fr'],// target codes — EMPTY/ABSENT = translation off
17
17
  providers: { claude: {} },// the engine is a KEY (#425): claude | chatgpt. Absent block = claude
18
18
  model: null, // optional override for the chosen engine (claude → 'sonnet' alias, chatgpt → 'gpt-5.4-nano')
19
- exclude: [], // web only: BRAND page routes/folders to skip (the framework's own default pages are already excluded — #605)
19
+ include: ['**', '!blog/**'], // web only: globs over BRAND page routes, `!` negates; this is the framework default, a brand list replaces it (#858; the framework's own default pages are excluded on their own, #605)
20
20
  }
21
21
  ```
22
22
 
@@ -112,7 +112,13 @@ string. Committed to git — that's the whole point:
112
112
  re-translation. Everything else is a cache hit (zero provider calls).
113
113
  - **Survives clones/CI**: a warm cache builds a fully-translated site with NO
114
114
  AI credentials (kills the BXM parked finding where fresh clones burned live
115
- Claude calls).
115
+ Claude calls). No AI key is delivered to CI by default
116
+ ([#819](https://github.com/Omega-JS-Stack/omega/issues/819), Ian 2026-09-13):
117
+ translation runs on the developer's machine and a runner reads the committed
118
+ cache, which is why `OPENAI_API_KEY` is an `env` delivery on every target that
119
+ names it and no workflow carries a line for it
120
+ ([#905](https://github.com/Omega-JS-Stack/omega/issues/905) owns the one
121
+ system, cloud translation included).
116
122
  - **Human-overridable**: hand-edit a translation VALUE in the cache file and
117
123
  it sticks for as long as the source is unchanged (the value is the
118
124
  translation; the key only changes when the SOURCE changes).
@@ -172,10 +178,44 @@ The list is DERIVED from the packaged defaults tree
172
178
  never typed out, so a default page that moves or arrives cannot drift out of it,
173
179
  and each excluded route guards its subtree too. On top of that: socials
174
180
  redirects (config `socials` keys), the `admin`/`test`/`team`/`updates` folders,
175
- every known language-code folder, plus config `translation.exclude` — which is
176
- for the BRAND's own pages, and only those. A brand that lists `signin` or
177
- `account` is listing something the framework already skips.
178
- Element opt-out: `data-omega-no-translate`. Collector fixes vs UJM:
181
+ every known language-code folder.
182
+
183
+ **Which of the BRAND's own pages are translated is `translation.include`**
184
+ ([#858](https://github.com/Omega-JS-Stack/omega/issues/858), Ian 2026-09-13,
185
+ the same-name ruling): a list of route GLOBS read like a `.gitignore`, where
186
+ `!` negates and the LAST pattern that matches a route decides it. A route no
187
+ pattern matches is not translated, so an empty list translates nothing. A
188
+ folder pattern covers the folder itself as well as its contents, so
189
+ `!blog/**` takes `/blog` out along with every post under it. The framework
190
+ default lives in the DEFAULTS layer of the merge chain (the schema's own
191
+ `default:`, resolution-only so no brand file carries a copy of it):
192
+
193
+ ```json5
194
+ translation: { include: ['**', '!blog/**'] } // the default: the whole site except the blog
195
+ ```
196
+
197
+ A brand list **REPLACES** it outright rather than adding to it (arrays replace
198
+ at every level of the merge chain), so a brand that writes `['docs/**']` gets
199
+ docs and nothing else. It is for the brand's own pages only: the framework's
200
+ derived exclusions above are not in its hands, and a brand that names `signin`
201
+ or `account` is naming something already skipped.
202
+
203
+ **A page overrides the list for itself**, under the same key name one level
204
+ down: `translation: { include: true }` in its frontmatter translates a page the
205
+ list left out, `false` takes one out that the list would have covered. The
206
+ build stamps that answer on `<html data-omega-translate>` (the seam #355's base
207
+ path already uses), because the pass runs post-build over `dist/` and
208
+ `omega translate` runs with no build in reach. `include` is the only key a page
209
+ may write under a bare `translation:`; anything else is a config section
210
+ restated bare and fails the build.
211
+
212
+ **`translation.exclude` is RETIRED** with it. There is no dual-read: a config
213
+ still carrying it fails validation naming its replacement, and
214
+ `omega migrate` at the brand root CONVERTS the list (`exclude: ['docs']`
215
+ becomes `include: ['**', '!docs']`, which keeps translating exactly what the
216
+ brand was translating before) and deletes the old key in the same run.
217
+
218
+ Element opt-out: `data-omega-no-translate`, unchanged. Collector fixes vs UJM:
179
219
  `aria-describedby`/`aria-labelledby` are NOT collected (ID refs), `value`
180
220
  only on button-type inputs (hidden-input tokens stay intact).
181
221
 
@@ -213,7 +253,9 @@ alike, since it never touches a provider:
213
253
  `terms`/`privacy`/`cookies` are deliberately not generated.
214
254
  - A configured language the package does not ship is one log line for the whole
215
255
  run, not an error — those routes stay in the source language.
216
- - A route the brand named in `translation.exclude` is left alone entirely.
256
+ - A brand's `translation.include` list never touches the framework's default
257
+ pages: it scopes the brand's OWN pages, and the framework's chrome is
258
+ translated once, on the framework side.
217
259
 
218
260
  **Shipped set: `es`, `fa`.** Regenerating, or extending the set, is one command
219
261
  in `packages/web` (the ONLY place the framework's own pages ever reach a
@@ -54,7 +54,7 @@ the same exact pin ([local-dev.md](local-dev.md) § API surface).
54
54
 
55
55
  ## Brand root
56
56
 
57
- `omega update` at a brand root (manager) fans out over the brand's targets — cp251's deploy fan-out shape: same target discovery, same `--target=<target|dir>` picker ([#780](https://github.com/Omega-JS-Stack/omega/issues/780)), every other flag forwarded verbatim, each target answering through its own framework's `update` verb. Unlike deploy, targets are **independent**: one failing target never blocks the rest (any failure still exits 1). The brand-root shell `package.json` rides the walk as its LAST leg ([#794](https://github.com/Omega-JS-Stack/omega/issues/794)), scoped to the one `@omega.js/*` dependency it carries — `@omega.js/manager` — and run in-process through the same devkit implementation (there is no framework bin at the root to spawn; it would dispatch straight back into this command). A brand's own root tooling is never touched, and a picked run (`--target=`) skips the root, because the picker names targets. Without that leg a manager-behind brand could never heal itself: the boot check would refuse every verb, and the fix it names would move every target except the one that was wrong.
57
+ `omega update` at a brand root (manager) fans out over the brand's targets, cp251's deploy fan-out shape: same target discovery, same `--target=<name>[,<name>]` picker ([#780](https://github.com/Omega-JS-Stack/omega/issues/780)), every other flag forwarded verbatim, each target answering through its own framework's `update` verb. Unlike deploy, targets are **independent**: one failing target never blocks the rest (any failure still exits 1). The brand-root shell `package.json` rides the walk as its LAST leg ([#794](https://github.com/Omega-JS-Stack/omega/issues/794)), scoped to the one `@omega.js/*` dependency it carries (`@omega.js/manager`) and run in-process through the same devkit implementation (there is no framework bin at the root to spawn; it would dispatch straight back into this command). A brand's own root tooling is never touched, and a picked run (`--target=`) skips the root, because the picker names targets. Without that leg a manager-behind brand could never heal itself: the boot check would refuse every verb, and the fix it names would move every target except the one that was wrong.
58
58
 
59
59
  ## Testing
60
60