@bongos/core 1.19.1081 → 1.20.2

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 (253) hide show
  1. package/.bongos-core.json +528 -193
  2. package/.claude/skills/planning-session/SKILL.md +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +32 -0
  5. package/clients/bongos-client/index.cjs +32 -0
  6. package/clients/bongos-client/index.d.ts +51 -6
  7. package/clients/bongos-client/index.mjs +32 -0
  8. package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
  9. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  10. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
  11. package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
  12. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  13. package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
  14. package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
  15. package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +851 -19
  18. package/docs/api-reference.md +33 -9
  19. package/docs/architecture.md +6 -2
  20. package/docs/copy-inventory.md +620 -553
  21. package/docs/copy-registry.json +1467 -820
  22. package/docs/file-map.md +2 -0
  23. package/docs/module-api-changelog.md +8 -0
  24. package/docs/modules-contract.md +1 -0
  25. package/docs/page-inventory.json +38 -4
  26. package/docs/page-readings.json +1666 -1558
  27. package/migrations/core_260_goals_working_area.sql +63 -0
  28. package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
  29. package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
  30. package/modules/agents/lib/validate.js +43 -0
  31. package/modules/autonomy/gauge.js +38 -2
  32. package/modules/grading/grader-subagent.js +37 -2
  33. package/modules/grading/grader-workers/reader.js +65 -5
  34. package/modules/hall-ui/public/approval-queue.css +33 -7
  35. package/modules/hall-ui/public/approval-queue.js +9 -2
  36. package/modules/hall-ui/public/atlas.html +1 -1
  37. package/modules/hall-ui/public/blockers.html +1 -1
  38. package/modules/hall-ui/public/board-room.html +1 -1
  39. package/modules/hall-ui/public/brand-holes.js +121 -0
  40. package/modules/hall-ui/public/collab.html +1 -1
  41. package/modules/hall-ui/public/collab.js +1 -1
  42. package/modules/hall-ui/public/copy-desk.html +1 -1
  43. package/modules/hall-ui/public/deploy.html +6 -1
  44. package/modules/hall-ui/public/deploy.js +22 -8
  45. package/modules/hall-ui/public/diagrams.html +1 -1
  46. package/modules/hall-ui/public/dom-utils.js +20 -0
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.html +1 -1
  50. package/modules/hall-ui/public/goal-inbox.js +1 -1
  51. package/modules/hall-ui/public/goals.html +1 -1
  52. package/modules/hall-ui/public/government.html +1 -1
  53. package/modules/hall-ui/public/idea.html +1 -1
  54. package/modules/hall-ui/public/idea.js +28 -0
  55. package/modules/hall-ui/public/ideas.html +1 -1
  56. package/modules/hall-ui/public/ideas.js +19 -3
  57. package/modules/hall-ui/public/index.html +5 -1
  58. package/modules/hall-ui/public/modules.html +1 -1
  59. package/modules/hall-ui/public/primer.html +1 -1
  60. package/modules/hall-ui/public/profile-nudge.js +21 -4
  61. package/modules/hall-ui/public/profile.html +1 -1
  62. package/modules/hall-ui/public/profile.js +1 -1
  63. package/modules/hall-ui/public/project-settings.html +1 -1
  64. package/modules/hall-ui/public/ranks.html +1 -1
  65. package/modules/hall-ui/public/roadmap.html +1 -1
  66. package/modules/hall-ui/public/roster.html +1 -1
  67. package/modules/hall-ui/public/sessions.html +1 -1
  68. package/modules/hall-ui/public/settings.html +19 -30
  69. package/modules/hall-ui/public/settings.js +24 -177
  70. package/modules/hall-ui/public/settings.states.json +1 -1
  71. package/modules/hall-ui/public/shell.js +4 -2
  72. package/modules/hall-ui/public/studio.css +14 -8
  73. package/modules/hall-ui/public/studio.html +5 -4
  74. package/modules/hall-ui/public/task.html +1 -1
  75. package/modules/hall-ui/public/thinking.css +6 -3
  76. package/modules/hall-ui/public/thinking.html +1 -1
  77. package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
  78. package/modules/hall-ui/public/tweak-editor.css +142 -11
  79. package/modules/hall-ui/public/tweak-editor.html +25 -9
  80. package/modules/hall-ui/public/tweak-editor.js +243 -10
  81. package/modules/hall-ui/public/watch.html +1 -1
  82. package/modules/hall-ui/public/work.html +1 -1
  83. package/modules/hall-ui/records/approval-queue.md +6 -0
  84. package/modules/hall-ui/records/tweak-editor.md +6 -0
  85. package/modules/ideas/ratify.js +36 -8
  86. package/modules/ideas/routes/ratify-goal.js +6 -0
  87. package/modules/lifecycle/db-analytics.js +79 -10
  88. package/modules/lifecycle/db-claims.js +48 -1
  89. package/modules/lifecycle/db-deps-criteria.js +94 -40
  90. package/modules/lifecycle/db-goals.js +4 -1
  91. package/modules/lifecycle/db-grade.js +9 -1
  92. package/modules/lifecycle/db-relevance-flags.js +183 -0
  93. package/modules/lifecycle/db-versions.js +4 -3
  94. package/modules/lifecycle/db.js +19 -0
  95. package/modules/lifecycle/done-when.js +5 -3
  96. package/modules/lifecycle/est-advisory.js +31 -4
  97. package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
  98. package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
  99. package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
  100. package/modules/lifecycle/module.json +2 -1
  101. package/modules/lifecycle/routes/claims.js +8 -1
  102. package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
  103. package/modules/lifecycle/routes/goals.js +1 -1
  104. package/modules/lifecycle/routes/tasks.js +27 -24
  105. package/modules/lifecycle/routes/visuals.js +9 -0
  106. package/modules/lifecycle/ship-preflight.js +7 -11
  107. package/modules/lifecycle/task-visuals.js +109 -5
  108. package/modules/npm-release/module.json +2 -1
  109. package/modules/npm-release/preview/commands.js +81 -0
  110. package/modules/npm-release/preview/divert.js +95 -0
  111. package/modules/npm-release/preview/env.js +78 -0
  112. package/modules/npm-release/preview/proxy.js +107 -0
  113. package/modules/npm-release/preview/runtime.js +83 -0
  114. package/modules/npm-release/preview/supervisor.js +175 -0
  115. package/modules/npm-release/public/work.js +65 -1
  116. package/modules/npm-release/routes/preview.js +133 -0
  117. package/modules/provisioning/app-status.js +83 -0
  118. package/modules/provisioning/module.json +2 -1
  119. package/modules/provisioning/pollers/app-liveness.js +110 -0
  120. package/modules/provisioning/render-lookup.js +68 -0
  121. package/modules/provisioning/render-standup.js +44 -1
  122. package/modules/provisioning/routes/render-standup.js +81 -6
  123. package/modules/provisioning/starter-bundles.js +4 -1
  124. package/modules/public-landing/public/account-private.states.json +14 -0
  125. package/modules/public-landing/public/account.html +513 -0
  126. package/modules/public-landing/public/account.probes.json +37 -0
  127. package/modules/public-landing/public/account.states.json +16 -0
  128. package/modules/public-landing/public/index.html +5 -2
  129. package/modules/public-landing/public/projects.html +661 -18
  130. package/modules/public-landing/public/projects.states.json +8 -1
  131. package/modules/render-deploy/deploys.js +203 -0
  132. package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
  133. package/modules/render-deploy/module.json +25 -0
  134. package/modules/render-deploy/public/deploy.css +10 -0
  135. package/modules/render-deploy/public/deploy.js +249 -0
  136. package/modules/render-deploy/render.js +56 -0
  137. package/modules/render-deploy/routes/act.js +88 -0
  138. package/modules/render-deploy/routes/app.js +65 -0
  139. package/modules/render-deploy/routes/history.js +38 -0
  140. package/modules/specialities/db.js +27 -8
  141. package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
  142. package/modules/specialities/routes/specialities.js +51 -3
  143. package/modules/specialities/skills.js +94 -0
  144. package/modules/specialities/specialities.js +56 -9
  145. package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
  146. package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
  147. package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
  148. package/modules/ui-design/kit/lib.js +3 -1
  149. package/modules/ui-design/kit/serve.js +96 -0
  150. package/package-lock.json +2 -2
  151. package/package.json +1 -1
  152. package/release-notes.json +169 -0
  153. package/scripts/gds/agents-sync.js +13 -3
  154. package/scripts/gds/autobongos-run.js +80 -2
  155. package/scripts/gds/autobongos-service.cmd +12 -0
  156. package/scripts/gds/copy-apply.js +14 -0
  157. package/scripts/gds/dev-box-guard.js +3 -2
  158. package/scripts/gds/fitness.js +9 -0
  159. package/scripts/gds/grade-correlation-audit.js +42 -12
  160. package/scripts/gds/grade-replay.js +1 -1
  161. package/scripts/gds/provision-core-upgrade.js +38 -7
  162. package/scripts/gds/provision-pin-key.js +176 -0
  163. package/scripts/gds/provision-render.js +62 -31
  164. package/scripts/gds/provision-teardown.js +7 -2
  165. package/scripts/gds/provision-units.js +31 -0
  166. package/scripts/gds/provision.js +10 -10
  167. package/scripts/gds/publish-manifest.js +1 -0
  168. package/scripts/gds/render-api.js +19 -0
  169. package/scripts/gds/render-payload.js +39 -65
  170. package/scripts/gds/run-unit-tests.js +10 -0
  171. package/scripts/gds/ship-finish.js +19 -20
  172. package/scripts/gds/smoke-dependencies.sh +37 -8
  173. package/scripts/gds/status.js +12 -4
  174. package/scripts/gds/upgrade.js +2 -2
  175. package/scripts/public-mirror-export.js +7 -1
  176. package/src/bongos/module-scope-map.js +13 -0
  177. package/src/bongos/serve-internal.js +3 -1
  178. package/src/module-api.js +10 -1
  179. package/src/platform-server.js +39 -0
  180. package/tests/account_privacy_flags.mjs +10 -6
  181. package/tests/agents_authoring.mjs +15 -1
  182. package/tests/agents_sync.mjs +106 -5
  183. package/tests/agents_validate.mjs +45 -0
  184. package/tests/api_path_404.mjs +6 -0
  185. package/tests/autobongos_cadence.mjs +7 -0
  186. package/tests/autobongos_loop.mjs +136 -1
  187. package/tests/autonomy_gauge.mjs +62 -0
  188. package/tests/blocker_hall_live_proof.mjs +275 -0
  189. package/tests/claim_gate_ci_unblock.mjs +116 -0
  190. package/tests/claim_gate_rebase.mjs +15 -3
  191. package/tests/collab_page.mjs +10 -0
  192. package/tests/conductor_main_e2e.mjs +97 -0
  193. package/tests/core_262_drop_box_blocked_db.mjs +143 -0
  194. package/tests/core_upgrade_runner.mjs +1 -0
  195. package/tests/dependency_writes_atomic.mjs +191 -0
  196. package/tests/deploy_page_projects.mjs +2 -1
  197. package/tests/effective_visibility_predicate.mjs +15 -0
  198. package/tests/est_advisory.mjs +27 -0
  199. package/tests/goal_map_page.mjs +26 -0
  200. package/tests/goal_task_relevance.mjs +518 -0
  201. package/tests/goal_working_area.mjs +146 -0
  202. package/tests/grade_attempts.mjs +61 -0
  203. package/tests/grade_attribution.mjs +23 -0
  204. package/tests/grade_correlation_audit.mjs +54 -1
  205. package/tests/grader_reader_lens.mjs +55 -2
  206. package/tests/grader_root_outage.mjs +6 -1
  207. package/tests/grader_subagent_tools_arg.mjs +102 -0
  208. package/tests/hall_approval_queue.mjs +54 -2
  209. package/tests/hall_settings_world.mjs +3 -1
  210. package/tests/hall_tweak_editor.mjs +316 -11
  211. package/tests/hub_account_page.mjs +394 -0
  212. package/tests/idea_detail_page.mjs +64 -0
  213. package/tests/idea_goal_ratification.mjs +64 -0
  214. package/tests/idea_spark_hall.mjs +60 -9
  215. package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
  216. package/tests/landing_page.mjs +6 -3
  217. package/tests/module-scope-map.mjs +31 -8
  218. package/tests/module_api.mjs +1 -0
  219. package/tests/module_loader.mjs +1 -1
  220. package/tests/nav_permission_atoms.mjs +1 -1
  221. package/tests/no_phantom_mirror_workflow.mjs +24 -0
  222. package/tests/npm_release_preview_commands.mjs +89 -0
  223. package/tests/npm_release_preview_divert.mjs +164 -0
  224. package/tests/npm_release_preview_env.mjs +100 -0
  225. package/tests/npm_release_preview_proxy.mjs +162 -0
  226. package/tests/npm_release_preview_routes.mjs +208 -0
  227. package/tests/npm_release_preview_supervisor.mjs +203 -0
  228. package/tests/pin_write_key.mjs +383 -0
  229. package/tests/planning_session_skill.mjs +27 -0
  230. package/tests/platform_boot.mjs +71 -0
  231. package/tests/profile_nudge_links.mjs +86 -0
  232. package/tests/profile_ui_cross_project.mjs +1 -1
  233. package/tests/projects_hub.mjs +2 -2
  234. package/tests/projects_hub_app_status.mjs +249 -0
  235. package/tests/projects_hub_app_step.mjs +282 -52
  236. package/tests/projects_hub_render_connect.mjs +295 -0
  237. package/tests/provision_render.mjs +121 -28
  238. package/tests/provision_settings_apply.mjs +62 -0
  239. package/tests/provisioning_app_status.mjs +204 -0
  240. package/tests/provisioning_render_route.mjs +126 -6
  241. package/tests/render_check.mjs +2 -1
  242. package/tests/render_deploy.mjs +495 -0
  243. package/tests/rot_inbox.mjs +8 -0
  244. package/tests/ship_preflight.mjs +61 -8
  245. package/tests/smoke_dependencies_witness.mjs +46 -0
  246. package/tests/speciality_skills.mjs +214 -0
  247. package/tests/studio_room_height.mjs +70 -0
  248. package/tests/task_detail_includes.mjs +10 -0
  249. package/tests/task_visuals_instance_root.mjs +150 -0
  250. package/tests/tweak_batch_apply.mjs +21 -0
  251. package/tests/ui_design_kit.mjs +36 -4
  252. package/tests/upgrade.mjs +1 -1
  253. package/tests/upgrade_persist_pin.mjs +24 -0
@@ -0,0 +1,121 @@
1
+ // modules/hall-ui/public/brand-holes.js — a page's BRAND HOLES, shown as the
2
+ // names they stand for (task 1004381). window.OTBBrandHoles, node-require-safe.
3
+ //
4
+ // WHY. A page's source says {{productName}} where the server writes the
5
+ // instance's name at serve time (src/bongos/serve-internal.js applyBrandTokens),
6
+ // and the page readings keep the token by name (scripts/gds/page-reader.js). So
7
+ // the Approval queue and the tweak editor were showing an artist the raw
8
+ // "{{productName}}", and the editor let them type over it or lose the spaces
9
+ // around it (the first real round came back as "This project,{{productName}}has
10
+ // two environments").
11
+ //
12
+ // THE RULE. The STORED text always keeps the {{token}}: it is what copy-apply
13
+ // writes back into the source, and the source stays brand-free. Only the
14
+ // DISPLAY resolves it, from window.__BRANDING__ (the same pack the server fills
15
+ // from), drawn as a small fixed chip the artist reads but cannot rewrite:
16
+ //
17
+ // splitHoles(text) [{ text } | { hole: 'productName' }], in order
18
+ // holeNames(text) the token names, in order
19
+ // sameHoles(a, b) the same tokens, the same number of times
20
+ // holeLabel(name, brand) 'Cloud Bongos' for productName; the name itself
21
+ // when the pack carries no value for it
22
+ // holesHtml(text, opts) escaped text with each token as a chip span
23
+ // serializeHoles(nodes) a chip box's DOM children back to the stored text
24
+ (() => {
25
+ 'use strict';
26
+
27
+ const HOLE_RE = /\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g;
28
+
29
+ function esc(s) {
30
+ return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
31
+ }
32
+
33
+ function splitHoles(text) {
34
+ const s = String(text == null ? '' : text);
35
+ const out = [];
36
+ let at = 0;
37
+ for (const m of s.matchAll(HOLE_RE)) {
38
+ if (m.index > at) out.push({ text: s.slice(at, m.index) });
39
+ out.push({ hole: m[1] });
40
+ at = m.index + m[0].length;
41
+ }
42
+ if (at < s.length) out.push({ text: s.slice(at) });
43
+ return out;
44
+ }
45
+
46
+ function holeNames(text) {
47
+ return splitHoles(text).filter((p) => p.hole).map((p) => p.hole);
48
+ }
49
+
50
+ function hasHoles(text) {
51
+ return holeNames(text).length > 0;
52
+ }
53
+
54
+ function sameHoles(a, b) {
55
+ const x = holeNames(a).sort();
56
+ const y = holeNames(b).sort();
57
+ return x.length === y.length && x.every((n, i) => n === y[i]);
58
+ }
59
+
60
+ // The tokens serve-internal.js fills from the CLIENT pack (the origins come
61
+ // from the full pack only, so a chip names them instead).
62
+ function holeLabel(name, brand) {
63
+ const b = brand || (typeof window !== 'undefined' && window.__BRANDING__) || {};
64
+ const id = b.identity || {};
65
+ const co = id.company || {};
66
+ const cur = (b.currency && b.currency.label) || '';
67
+ const values = {
68
+ productName: id.productName,
69
+ worldName: id.worldName,
70
+ tagline: b.copy && b.copy.tagline,
71
+ currency: cur,
72
+ Currency: cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : '',
73
+ companyName: co.name,
74
+ companySite: co.site,
75
+ companyTagline: co.tagline,
76
+ };
77
+ const v = values[name];
78
+ return typeof v === 'string' && v.trim() ? v : name;
79
+ }
80
+
81
+ // cls: the chip's class; inBox: the chip sits in a contenteditable box, so it
82
+ // is marked contenteditable="false" (typed around, never into).
83
+ // In a box, a chip with no words on one side gets a zero-width caret stop
84
+ // there (CARET): a browser cannot put the caret after a non-editable chip
85
+ // that ends the box, so the artist could not type after it. serializeHoles
86
+ // drops every CARET, so it never reaches the stored text.
87
+ const CARET = String.fromCharCode(0x200b);
88
+ function holesHtml(text, { cls = 'brand-hole', brand, inBox = false } = {}) {
89
+ const parts = splitHoles(text);
90
+ return parts.map((p, i) => {
91
+ if (!p.hole) return esc(p.text);
92
+ const label = holeLabel(p.hole, brand);
93
+ const chipHtml = `<span class="${esc(cls)}" data-hole="${esc(p.hole)}"${inBox ? ' contenteditable="false"' : ''} title="${esc(`A fixed name the page fills in: {{${p.hole}}}`)}">${esc(label)}</span>`;
94
+ if (!inBox) return chipHtml;
95
+ const before = i === 0 ? CARET : '';
96
+ const after = !parts[i + 1] || parts[i + 1].hole ? CARET : '';
97
+ return before + chipHtml + after;
98
+ }).join('');
99
+ }
100
+
101
+ // A chip box's children back to the stored text: a text node is its words, a
102
+ // chip is its {{token}}, a <br> the browser inserts is a space, and any other
103
+ // element is its own children (a browser may wrap typed words in a <div>).
104
+ function serializeHoles(nodes) {
105
+ let out = '';
106
+ for (const n of Array.from(nodes || [])) {
107
+ if (!n) continue;
108
+ if (n.nodeType === 3) { out += String(n.nodeValue == null ? '' : n.nodeValue).split(CARET).join(''); continue; }
109
+ if (n.nodeType !== 1) continue;
110
+ const hole = n.getAttribute ? n.getAttribute('data-hole') : null;
111
+ if (hole) { out += `{{${hole}}}`; continue; }
112
+ if (String(n.nodeName || n.tagName || '').toUpperCase() === 'BR') { out += ' '; continue; }
113
+ out += serializeHoles(n.childNodes);
114
+ }
115
+ return out;
116
+ }
117
+
118
+ const api = { splitHoles, holeNames, hasHoles, sameHoles, holeLabel, holesHtml, serializeHoles };
119
+ if (typeof window !== 'undefined') window.OTBBrandHoles = api;
120
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
121
+ })();
@@ -148,7 +148,7 @@
148
148
 
149
149
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
150
150
 
151
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
151
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
152
152
  <script src="/builders/palette.js" defer></script>
153
153
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
154
154
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
@@ -169,7 +169,7 @@
169
169
  <button class="cb-ask__door" type="button" data-reply-open="${fid}">${thread.length ? 'Add a reply' : 'Reply'}</button>
170
170
  </div>
171
171
  <form class="cb-reply-form" id="${fid}" data-reply-form="${id}" hidden>
172
- <textarea class="cb-reply-input" name="body" rows="3" maxlength="${L.MAX_REPLY}" placeholder="Moot — the metrics stack was removed on the 15th, so there is nothing left to rotate."></textarea>
172
+ <textarea class="cb-reply-input" name="body" rows="3" maxlength="${L.MAX_REPLY}" placeholder="Write a reply…"></textarea>
173
173
  <div class="cb-reply-actions">
174
174
  <button class="btn-accent" type="submit" data-reply-send="${id}">Post reply</button>
175
175
  <button class="btn-ghost" type="button" data-reply-cancel="${fid}">Cancel</button>
@@ -177,7 +177,7 @@
177
177
 
178
178
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
179
179
 
180
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
180
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
181
181
  <script src="/builders/palette.js" defer></script>
182
182
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
183
183
  <script src="/builders/hall-kit.js"></script>
@@ -38,6 +38,11 @@
38
38
  "it's not showing anything that's been done." -->
39
39
  <section class="scroll" id="nr-work" aria-labelledby="nr-work-h" hidden></section>
40
40
 
41
+ <!-- The render-deploy module's section (task 1004270): the deploy page of a project whose
42
+ app deploys through Render. Empty and hidden here; the module's own script fills it,
43
+ and only in a hall that turned the module on. -->
44
+ <section class="scroll" id="rd-panel" aria-labelledby="rd-h" hidden></section>
45
+
41
46
  <section class="scroll" id="dp-body" aria-labelledby="dp-body-h" hidden>
42
47
  <h2 class="scroll__h" id="dp-body-h">What users can see</h2>
43
48
  <!-- The honest framing, and the one thing a reader must not misread:
@@ -69,7 +74,7 @@
69
74
 
70
75
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
71
76
 
72
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
77
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
73
78
  <script src="/builders/palette.js" defer></script>
74
79
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
75
80
  <script src="/builders/hall-kit.js"></script>
@@ -233,10 +233,10 @@
233
233
  }
234
234
 
235
235
  // The one fix a page can hand the owner for a repo that was not updated: sign in again
236
- // with repository access, which stores the token the runner pushes with. Offered only
236
+ // with repository access, which lets the runner set up its own lasting write key for the
237
+ // repository (task 1004291, ADR 0350). Offered only
237
238
  // for the two causes it can cure — a sign-in does nothing for GitHub being ahead of the
238
- // box, or for a project with no repo. It says the access is short-lived (an hour, per
239
- // GITHUB_TOKEN_TTL_MS) rather than promising the repo will simply catch up.
239
+ // box, for a project with no repo, or for one that needs its own account first.
240
240
  const RECONNECT_CURES = new Set(['no_access', 'refused']);
241
241
  function reconnectHtml(p, code) {
242
242
  if (!RECONNECT_CURES.has(code)) return '';
@@ -249,7 +249,7 @@
249
249
  const href = `${API}/auth/web/start?repos=1&return=${encodeURIComponent(back)}`;
250
250
  return `
251
251
  <p class="ov-row__sub">If you own this project, <a href="${escapeHtml(href)}">reconnect GitHub with repository access</a>.
252
- The hub keeps that access for about an hour, so the next upgrade run within the hour saves this change to GitHub along with its own.</p>`;
252
+ Reconnecting sets up <strong>lasting</strong> access: the hub keeps a key of its own for this repository, so this change and every later upgrade are saved to GitHub without asking you again.</p>`;
253
253
  }
254
254
 
255
255
  function stepOneHtml(p) {
@@ -592,8 +592,17 @@
592
592
  return rows.find((i) => i && String(i.slug) === slug) || null;
593
593
  }
594
594
 
595
+ // A Render project's hall has no provisioning door (task 1004270): its /deploy page is the
596
+ // render-deploy module's, so the door's own reading is skipped rather than failing on routes
597
+ // that hall does not have.
598
+ function doorIsHere() {
599
+ const mods = window.__MODULES__;
600
+ return !(mods && mods.enabled && mods.enabled.provisioning === false);
601
+ }
602
+
595
603
  async function load() {
596
604
  const slug = scopeSlug();
605
+ if (!slug && !doorIsHere()) { $('dp-sub').textContent = 'Render'; return; }
597
606
  try {
598
607
  if (!slug) {
599
608
  // THE HALL'S OWN PROJECT. On the control plane that is the self row, which is why
@@ -650,10 +659,15 @@
650
659
  // module is on, so a hall without it never asks for a script that would 404.
651
660
  function loadModuleSections() {
652
661
  const mods = window.__MODULES__;
653
- if (scopeSlug() || !(mods && mods.enabled && mods.enabled['npm-release'])) return;
654
- const s = document.createElement('script');
655
- s.src = '/modules/npm-release/public/work.js';
656
- document.body.appendChild(s);
662
+ if (scopeSlug() || !(mods && mods.enabled)) return;
663
+ // Each module lends the page its own script, and only where it is on: a hall without a
664
+ // module never asks for a script that would 404.
665
+ for (const [key, src] of [['npm-release', '/modules/npm-release/public/work.js'], ['render-deploy', '/modules/render-deploy/public/deploy.js']]) {
666
+ if (!mods.enabled[key]) continue;
667
+ const s = document.createElement('script');
668
+ s.src = src;
669
+ document.body.appendChild(s);
670
+ }
657
671
  }
658
672
 
659
673
  document.addEventListener('DOMContentLoaded', () => { load(); loadModuleSections(); });
@@ -97,7 +97,7 @@
97
97
 
98
98
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
99
99
  time so the chrome exists before the deferred page scripts execute. -->
100
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
100
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
101
101
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
102
102
  <script src="/builders/palette.js" defer></script>
103
103
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -441,6 +441,25 @@
441
441
  + `${escapeHtml(label)}${inherited ? '<i class="gr-cat__from" aria-hidden="true">via goal</i>' : ''}</span>`;
442
442
  }
443
443
 
444
+ // The hub's origin, for the links a hall sends to the person's account page
445
+ // (task 1004259 / 1003485, ADR 0337 D4.3). Stamped server-side into
446
+ // <meta name="hub-origin" content="{{hubOrigin}}"> — the hub itself, or the
447
+ // hub a federated project signs in through — and empty on a vanilla instance.
448
+ // A bare https origin only: an empty or pathy value must never become a
449
+ // same-host link claiming to be the hub (the profile-canonical.js rule).
450
+ // null means "no hub", and every caller then renders no link.
451
+ function hubOrigin() {
452
+ const meta = document.querySelector('meta[name="hub-origin"]');
453
+ const raw = ((meta && meta.getAttribute('content')) || '').trim().replace(/\/+$/, '');
454
+ if (!raw) return null;
455
+ try {
456
+ const url = new URL(raw);
457
+ if (url.protocol !== 'https:' || url.username || url.password) return null;
458
+ if (url.pathname !== '/' || url.search || url.hash) return null;
459
+ return url.origin;
460
+ } catch (err) { return null; }
461
+ }
462
+
444
463
  window.OTB = {
445
464
  escapeHtml, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
446
465
  // task 1003936 — ONE word for the role axis, all six surfaces
@@ -451,5 +470,6 @@
451
470
  contributeHallWidget, hallWidgets, hallWidgetIds,
452
471
  splitCriterion,
453
472
  categoryChip, // task 1003276 — shared by the Goals page, the Tasks board and the task page
473
+ hubOrigin, // task 1003485 — the hub account page's origin, for Settings and the profile nudge
454
474
  };
455
475
  })();
@@ -98,7 +98,7 @@
98
98
 
99
99
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
100
100
 
101
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
101
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
102
102
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
103
103
  <script src="/builders/palette.js" defer></script>
104
104
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -54,7 +54,7 @@
54
54
 
55
55
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
56
56
 
57
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
57
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
58
58
  <script src="/builders/palette.js" defer></script>
59
59
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
60
60
  <script src="/builders/hall-kit.js"></script>
@@ -76,7 +76,7 @@
76
76
  <div id="gate-modal-root"></div>
77
77
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
78
78
 
79
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
79
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
80
80
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
81
81
  <script src="/builders/palette.js" defer></script>
82
82
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -455,7 +455,7 @@
455
455
  }).join('');
456
456
  return `<li class="ginbox__item" data-rot-tier="${escapeHtml(String(r.tier))}">
457
457
  <div class="ginbox__body">
458
- <a class="ginbox__goal" href="${href}">${escapeHtml(r.title || `${r.tier} #${r.id}`)}</a>
458
+ <a class="ginbox__goal" href="${escapeHtml(href)}">${escapeHtml(r.title || `${r.tier} #${r.id}`)}</a>
459
459
  <span class="ginbox__sub">${escapeHtml(String(r.status || ''))} · ${where} · ${escapeHtml(untouchedFor(r.last_touched_at))}${again}</span>
460
460
  </div>
461
461
  <div class="ginbox__actions">${buttons}</div>
@@ -235,7 +235,7 @@
235
235
 
236
236
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
237
237
 
238
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
238
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
239
239
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
240
240
  <script src="/builders/palette.js" defer></script>
241
241
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -134,7 +134,7 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
137
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
138
138
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
54
54
 
55
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
55
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
@@ -141,6 +141,33 @@
141
141
  }).join('')}</div>`;
142
142
  }
143
143
 
144
+ // The author's scope recommendation (task 1004134; the field is BV2.R13).
145
+ // What the author thinks their Full Idea turned out to be, a goal of its own
146
+ // or a criterion inside an existing goal: the line written for the board, who
147
+ // read this page while deciding. The question and the option labels are the
148
+ // SERVER's (the template's `recommendation` spec), never re-typed here. Three
149
+ // honest outcomes: a served option is shown by its label; a value the served
150
+ // options do not name is printed as stored rather than dropped; NULL (every
151
+ // Full Idea filed before the question existed, and any terse CLI filing) says
152
+ // the author did not say, and invents nothing. It is the author's reading and
153
+ // never the board's decision, so the line says whose it is. The two sentences
154
+ // are the Board Room's own (board-room.js recommendationHtml), so one field
155
+ // reads in one vocabulary on every page that shows it (docs/voice.md V1).
156
+ function recommendationRow(idea, template) {
157
+ const spec = template && template.recommendation;
158
+ const q = spec && spec.label ? spec.label : 'Recommended scope';
159
+ const v = idea.recommendation;
160
+ let a;
161
+ if (v === null || v === undefined || v === '') {
162
+ a = '<p class="ig-answer__a" data-empty="1">Its author gave no scope recommendation.</p>';
163
+ } else {
164
+ const opt = spec && Array.isArray(spec.options) ? spec.options.find((o) => o && o.value === v) : null;
165
+ const label = opt && opt.label ? opt.label.charAt(0).toLowerCase() + opt.label.slice(1) : String(v);
166
+ a = `<p class="ig-answer__a">Its author recommends this becomes <strong>${escapeHtml(label)}</strong>. The board decides.</p>`;
167
+ }
168
+ return `<div class="ig-answers"><div class="ig-answer"><p class="ig-answer__q">${escapeHtml(q)}</p>${a}</div></div>`;
169
+ }
170
+
144
171
  // ---- the R154 mark control -------------------------------------------------
145
172
 
146
173
  // Rendered only when the viewer IS the author. The server re-checks; this is
@@ -433,6 +460,7 @@
433
460
  `<div class="ig-answer"><p class="ig-answer__q">${escapeHtml(k)}</p><p class="ig-answer__a">${escapeHtml(String(idea.field_values[k] || ''))}</p></div>`
434
461
  ).join('')}</div>`);
435
462
  }
463
+ parts.push(recommendationRow(idea, template));
436
464
  // A Full Idea's body_md is the original quick-idea text it grew out of, when
437
465
  // it had one. Shown after the answers, subordinate to them.
438
466
  if (String(idea.body_md || '').trim()) {
@@ -225,7 +225,7 @@
225
225
 
226
226
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
227
227
 
228
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
228
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
229
229
  <script src="/builders/palette.js" defer></script>
230
230
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
231
231
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -86,6 +86,14 @@
86
86
  let allOpenGoals = []; // the picker's "browse everything" half
87
87
  let allGoalsById = new Map(); // for naming the goal a routed capture landed in
88
88
  let goalTouchedByUser = false; // once the builder picks, stop clobbering it with fresh suggestions
89
+ // The suggester's out-of-order guard — the exact sibling of `gravitySeq`
90
+ // below (task 1004187). Both reads are fired from the same debounce, so both
91
+ // invite the same race: a slow answer for an earlier title landing after a
92
+ // fast one for the current title would repaint the picker with options for
93
+ // words the builder has already replaced. `goalTouchedByUser` answers a
94
+ // different question — has the builder picked manually? — and says nothing
95
+ // about ordering.
96
+ let goalSuggestSeq = 0;
89
97
  let quickSuggestTimer = null;
90
98
  let quickFiling = false;
91
99
  // GRAVITY (task 1004083 / spec R02). The landing verdict for the draft as it
@@ -584,10 +592,12 @@
584
592
  // degrades to "no suggestions"; the homeless option and the full goal list
585
593
  // are still there either way (goal-suggest.js's own fail-open contract).
586
594
  async function refreshGoalSuggestions() {
595
+ const seq = ++goalSuggestSeq;
587
596
  if (goalTouchedByUser) return; // never yank a manual pick out from under someone
588
597
  const title = fqTitleEl().value.trim();
589
598
  if (!title || !buildingVersionIds.length) { renderGoalOptions([]); return; }
590
599
  const kind = fqKindEl().value;
600
+ let merged = [];
591
601
  try {
592
602
  const results = await Promise.all(buildingVersionIds.map((versionId) => api.goals.getGoalsSuggestions({
593
603
  query: { version: versionId, title, kind, limit: 5 },
@@ -600,11 +610,17 @@
600
610
  if (!prev || g.score > prev.score) byId.set(String(g.id), g);
601
611
  }
602
612
  }
603
- const merged = Array.from(byId.values()).sort((a, b) => b.score - a.score).slice(0, 5);
604
- renderGoalOptions(merged);
613
+ merged = Array.from(byId.values()).sort((a, b) => b.score - a.score).slice(0, 5);
605
614
  } catch (_) {
606
- renderGoalOptions([]);
615
+ merged = [];
607
616
  }
617
+ // A stale answer never paints. See goalSuggestSeq above. BOTH async exits
618
+ // share the one guard, exactly as refreshGravity's do: a read that fails
619
+ // late would otherwise clear a newer read's options just as surely as a
620
+ // read that succeeds late would replace them. The no-title return above is
621
+ // synchronous, so nothing can have superseded it.
622
+ if (seq !== goalSuggestSeq) return;
623
+ renderGoalOptions(merged);
608
624
  }
609
625
 
610
626
  // ---------------------------------------------------------------------------
@@ -11,6 +11,10 @@
11
11
  that on the #hall-h1 marker below, so the marker must stay index-only. -->
12
12
  <title>Builders' Hall</title>
13
13
  <meta name="description" content="The hall of the builders who shape this world. Standing, achievements, and work waiting to be claimed.">
14
+ <!-- the hub's origin, stamped server-side (task 1003485): the profile nudge
15
+ sends "add a short bio" to the hub's account page, where the bio is edited.
16
+ Empty on an instance with no hub, and the bio half is then not asked for. -->
17
+ <meta name="hub-origin" content="{{hubOrigin}}">
14
18
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
15
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
16
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -102,7 +106,7 @@
102
106
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
103
107
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
104
108
  BEFORE builders.js so the chrome ids exist when events are wired. -->
105
- <script src="/builders/shell.js?v=2026-09-28-archive" defer></script>
109
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy" defer></script>
106
110
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
107
111
  <script src="/builders/palette.js" defer></script>
108
112
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -80,7 +80,7 @@
80
80
 
81
81
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
82
82
 
83
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
83
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
84
84
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
85
85
  <script src="/builders/palette.js" defer></script>
86
86
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
118
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
119
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
120
120
  <script src="/builders/palette.js" defer></script>
121
121
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -54,18 +54,35 @@
54
54
  }
55
55
  // Name what is actually missing: "add a bio" is actionable, "complete your
56
56
  // profile" is a chore of unknown size.
57
+ //
58
+ // Each half links to where it is actually EDITED (task 1003485): the bio is
59
+ // a platform-account field, edited on the hub's account page (task 1004259,
60
+ // ADR 0337 D4.3); the kinds of work are this project's own, in Settings. The
61
+ // line used to send both to /settings, which has no bio field — the "goes to
62
+ // an empty spot" bug. A half with nowhere to go (no hub origin) is not asked
63
+ // for at all, rather than pointed at a page that cannot answer it.
57
64
  const missing = Array.isArray(state.missing) ? state.missing : [];
65
+ const hub = window.OTB.hubOrigin();
58
66
  const parts = [];
59
- if (missing.includes('bio')) parts.push('a short bio');
60
- if (missing.includes('disciplines')) parts.push('the kinds of work you want');
61
- const what = parts.length === 2 ? `${parts[0]} and ${parts[1]}` : (parts[0] || 'a detail or two');
67
+ if (missing.includes('bio') && hub) parts.push({ href: `${hub}/account`, text: 'a short bio' });
68
+ if (missing.includes('disciplines')) parts.push({ href: '/settings#disciplines', text: 'the kinds of work you want' });
69
+ if (!parts.length) {
70
+ box.innerHTML = '';
71
+ window.OTBAttention?.refresh();
72
+ return;
73
+ }
74
+ const link = (part, lead) => `<a class="ginbox__goal" href="${escapeHtml(part.href)}">${escapeHtml(lead + part.text)}</a>`;
75
+ // One flex item, so the two links read as one sentence rather than stacking.
76
+ const what = parts.length === 2
77
+ ? `<span>${link(parts[0], 'Add ')} and ${link(parts[1], '')}</span>`
78
+ : link(parts[0], 'Add ');
62
79
  box.innerHTML = '<h3 class="ginbox__sec-h">Complete your profile</h3>'
63
80
  + '<ul class="ginbox">'
64
81
  + '<li class="ginbox__item">'
65
82
  // .ginbox__body / .ginbox__actions are the classes style.css actually
66
83
  // defines (the body is the flex column that stacks label over sub-line).
67
84
  + '<div class="ginbox__body">'
68
- + `<a class="ginbox__goal" href="/settings">Add ${escapeHtml(what)}</a>`
85
+ + what
69
86
  + '<span class="ginbox__sub">Shown on your public profile — it helps others know what you work on.</span>'
70
87
  + '</div>'
71
88
  + '<div class="ginbox__actions">'
@@ -187,7 +187,7 @@
187
187
 
188
188
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
189
189
  chrome exists before the page script runs. -->
190
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
190
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
191
191
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
192
192
  <script src="/builders/palette.js" defer></script>
193
193
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -287,7 +287,7 @@
287
287
  // ids may serialize as strings (pg bigint) — compare canonicalized.
288
288
  const isSelf = selfId != null && data.builder && String(data.builder.id) === String(selfId);
289
289
  const manage = isSelf
290
- ? `<p class="scroll__lede">This is you — <a href="/settings#cross-project">manage its visibility in Settings → Cross-project record</a>.</p>`
290
+ ? `<p class="scroll__lede">This is you — <a href="/settings#cross-project">manage its visibility in Settings → Profile and privacy</a>.</p>`
291
291
  : '';
292
292
  const cp = data.cross_project;
293
293
  if (!cp) {
@@ -58,7 +58,7 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
61
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
62
62
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
63
63
  <script src="/builders/palette.js" defer></script>
64
64
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
70
70
  chrome exists before the page script runs. -->
71
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
71
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
72
72
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
73
  <script src="/builders/palette.js" defer></script>
74
74
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,7 +41,7 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
44
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
53
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -99,7 +99,7 @@
99
99
 
100
100
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
101
101
  chrome exists before the page script runs. -->
102
- <script src="/builders/shell.js?v=2026-09-28-archive"></script>
102
+ <script src="/builders/shell.js?v=2026-09-29-renderdeploy"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->