@bongos/core 1.24.25 → 1.24.27

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 (114) hide show
  1. package/.bongos-core.json +173 -108
  2. package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +12 -0
  3. package/docs/copy-inventory.md +280 -256
  4. package/docs/copy-registry.json +572 -306
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +10 -4
  7. package/docs/page-readings.json +4337 -4313
  8. package/docs/recipes/ui-look-before-you-ship.md +1 -0
  9. package/modules/autonomy/fence.js +16 -1
  10. package/modules/government/protected-surfaces.json +2 -0
  11. package/modules/hall-ui/public/ai-studio.html +2 -2
  12. package/modules/hall-ui/public/atlas.html +1 -1
  13. package/modules/hall-ui/public/blockers.html +2 -2
  14. package/modules/hall-ui/public/blockers.js +3 -1
  15. package/modules/hall-ui/public/board-room.html +3 -1
  16. package/modules/hall-ui/public/city.html +1 -1
  17. package/modules/hall-ui/public/collab.html +4 -4
  18. package/modules/hall-ui/public/collab.js +4 -0
  19. package/modules/hall-ui/public/commands.html +3 -3
  20. package/modules/hall-ui/public/copy-desk.html +3 -3
  21. package/modules/hall-ui/public/core-move-notice.js +9 -6
  22. package/modules/hall-ui/public/criterion-uat-ui.js +1 -1
  23. package/modules/hall-ui/public/deploy-handoff.js +203 -0
  24. package/modules/hall-ui/public/deploy.html +6 -2
  25. package/modules/hall-ui/public/deploy.js +7 -1
  26. package/modules/hall-ui/public/diagrams.html +1 -1
  27. package/modules/hall-ui/public/drachmae.html +2 -2
  28. package/modules/hall-ui/public/fleet.html +3 -1
  29. package/modules/hall-ui/public/gate.html +3 -1
  30. package/modules/hall-ui/public/goal-map.html +1 -1
  31. package/modules/hall-ui/public/goals-page.js +1 -1
  32. package/modules/hall-ui/public/goals.css +3 -2
  33. package/modules/hall-ui/public/goals.html +3 -3
  34. package/modules/hall-ui/public/goals.js +1 -1
  35. package/modules/hall-ui/public/government.html +3 -1
  36. package/modules/hall-ui/public/hall-widgets.js +9 -9
  37. package/modules/hall-ui/public/idea.html +1 -1
  38. package/modules/hall-ui/public/ideas.html +3 -3
  39. package/modules/hall-ui/public/index.html +3 -1
  40. package/modules/hall-ui/public/lightbulb.js +1 -1
  41. package/modules/hall-ui/public/market-watch-item.html +2 -2
  42. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  43. package/modules/hall-ui/public/market-watch-settings.html +3 -1
  44. package/modules/hall-ui/public/market-watch.html +2 -2
  45. package/modules/hall-ui/public/modules.html +2 -2
  46. package/modules/hall-ui/public/my-agents.html +3 -3
  47. package/modules/hall-ui/public/primer.html +2 -2
  48. package/modules/hall-ui/public/profile.html +6 -6
  49. package/modules/hall-ui/public/project-settings.html +3 -1
  50. package/modules/hall-ui/public/ranks.html +4 -4
  51. package/modules/hall-ui/public/roadmap.html +2 -2
  52. package/modules/hall-ui/public/roster.html +2 -2
  53. package/modules/hall-ui/public/sessions.html +3 -1
  54. package/modules/hall-ui/public/settings-software-update.js +23 -9
  55. package/modules/hall-ui/public/settings-specialities.js +1 -1
  56. package/modules/hall-ui/public/settings.html +10 -6
  57. package/modules/hall-ui/public/settings.js +3 -0
  58. package/modules/hall-ui/public/shell.js +25 -73
  59. package/modules/hall-ui/public/street.html +1 -1
  60. package/modules/hall-ui/public/studio.html +1 -1
  61. package/modules/hall-ui/public/studio.js +3 -3
  62. package/modules/hall-ui/public/style.css +209 -40
  63. package/modules/hall-ui/public/task-detail.js +2 -2
  64. package/modules/hall-ui/public/task.html +1 -1
  65. package/modules/hall-ui/public/thinking.html +1 -1
  66. package/modules/hall-ui/public/tweak-editor.html +2 -2
  67. package/modules/hall-ui/public/update-pill.js +623 -0
  68. package/modules/hall-ui/public/watch.html +3 -1
  69. package/modules/hall-ui/public/watch.js +1 -1
  70. package/modules/hall-ui/public/work.html +4 -4
  71. package/modules/hall-ui/records/loader.md +35 -0
  72. package/modules/hall-ui/records/update-pill.md +12 -0
  73. package/modules/lifecycle/github-push.js +1 -0
  74. package/modules/lifecycle/routes/tasks.js +2 -2
  75. package/modules/lifecycle/runner-self-guard.js +122 -0
  76. package/modules/lifecycle/runner-self.js +154 -0
  77. package/modules/public-landing/public/account.html +3 -3
  78. package/modules/public-landing/public/assets/cosmos.css +45 -0
  79. package/modules/public-landing/public/community.html +4 -4
  80. package/modules/public-landing/public/connections.html +1 -1
  81. package/modules/public-landing/public/guild.html +1 -1
  82. package/modules/public-landing/public/invitations.html +4 -4
  83. package/modules/public-landing/public/projects.html +26 -26
  84. package/modules/status-ui/public/index.html +2 -2
  85. package/modules/status-ui/public/index.states.json +12 -0
  86. package/modules/status-ui/public/status.js +10 -2
  87. package/modules/status-ui/public/style.css +57 -0
  88. package/modules/ui-design/kit/README.md +1 -1
  89. package/modules/ui-design/kit/serve.js +8 -1
  90. package/package-lock.json +2 -2
  91. package/package.json +1 -1
  92. package/release-notes.json +24 -0
  93. package/scripts/bongos/autobongos-loop.js +4 -0
  94. package/scripts/bongos/autobongos-run.js +21 -4
  95. package/scripts/bongos/autobongos-tweak-lane.js +5 -3
  96. package/scripts/bongos/ship-flow.js +20 -0
  97. package/scripts/hall-preview/README.md +1 -0
  98. package/scripts/hall-preview/server.js +9 -0
  99. package/src/bongos/apex-hall-redirects.js +53 -0
  100. package/src/bongos/serve-internal.js +3 -18
  101. package/src/module-api.js +1 -1
  102. package/tests/autobongos_host_access.mjs +5 -3
  103. package/tests/autobongos_self_modification.mjs +420 -0
  104. package/tests/core_update_banner.mjs +6 -92
  105. package/tests/core_upgrade_owner_door.mjs +4 -56
  106. package/tests/deploy_handoff.mjs +206 -0
  107. package/tests/government_protected_surfaces.mjs +15 -0
  108. package/tests/hall_loading.mjs +153 -0
  109. package/tests/hub_connections_page.mjs +1 -1
  110. package/tests/hub_invitations_page.mjs +1 -1
  111. package/tests/loading_mark.mjs +203 -0
  112. package/tests/nav_permission_atoms.mjs +3 -2
  113. package/tests/software_update.mjs +39 -2
  114. package/tests/update_pill.mjs +431 -0
@@ -7,11 +7,12 @@
7
7
  // update channel lets it take; what each version in between brings — and, for the people
8
8
  // the upgrade door lets through, an Update button.
9
9
  //
10
- // THE BUTTON LEADS TO THE DOOR; IT IS NOT THE DOOR. Taking an update is the deploy door's
11
- // two steps (see exactly what moves, then confirm that version) with its health check and
12
- // auto-rollback (ADR 0293), and on a hosted project that door is on the hub (ADR 0339 D3).
13
- // A second copy of that flow here would be a second place for its safety rules to drift, so
14
- // the server names where this project's door is (`door`) and the button goes there.
10
+ // THE BUTTON RUNS THE PILL'S FLOW; IT IS NOT A SECOND DOOR. Taking an update is the deploy
11
+ // door's act, with its health check and auto-rollback (ADR 0293), and on a hosted project that
12
+ // door is on the hub (ADR 0339 D3). Since task 1005041 the button hands this panel's reading to
13
+ // update-pill.js, which takes the update in place (this page names the version; the door moves
14
+ // it), so there is one copy of that flow, not two. The server still names the door (`door`),
15
+ // and the button's href is that door, for a page whose pill script did not load.
15
16
  //
16
17
  // THE PAGE KNOWS NOTHING (deploy.js's rule, kept). Every version here is the server's reading
17
18
  // with the time it was taken. "Up to date" is drawn only from an ok reading with nothing
@@ -125,11 +126,14 @@
125
126
  if (!sameOrigin && !/^https?:\/\/[^\s"'<>]+$/i.test(url)) return '';
126
127
  let where = '';
127
128
  if (!sameOrigin) {
128
- try { where = ` on ${new URL(url).host}`; } catch (e) { return ''; }
129
+ try { where = new URL(url).host; } catch (e) { return ''; }
129
130
  }
131
+ // IN PLACE (task 1005041): the button runs the Update pill's flow (update-pill.js) and the
132
+ // page stays put; the pill in the top bar shows the progress. The href stays the door's,
133
+ // so a page whose pill script did not load still reaches the update the old way.
130
134
  return `<div class="row-end su-door">
131
- <a class="pbtn pbtn--save" id="su-update" href="${escapeHtml(url)}">Update to ${escapeHtml(offered)}${sameOrigin ? '' : ' ↗'}</a>
132
- <span class="panel-hint">Opens this project's upgrade${escapeHtml(where)}: you see what would move, confirm it, and it installs, checks itself, and rolls back if the project does not come back healthy.</span>
135
+ <a class="pbtn pbtn--save" id="su-update" href="${escapeHtml(url)}">Update to ${escapeHtml(offered)}</a>
136
+ <span class="panel-hint">Takes a few minutes. The project restarts once, checks itself, and goes back to the version it runs now if it does not come back healthy.${where ? ` A small window from ${escapeHtml(where)} opens for a moment to start it.` : ''}</span>
133
137
  </div>${door.kind === 'hub' && ask.available ? askHtml(ask, offered, { lead: false }) : ''}`;
134
138
  }
135
139
 
@@ -236,6 +240,16 @@
236
240
  const askBtn = byId('su-ask');
237
241
  if (askBtn) askBtn.addEventListener('click', () => askForUpgrade(data, askBtn));
238
242
 
243
+ // The same press as the pill's Update now; only when it took the update in place is the
244
+ // link's own navigation stopped.
245
+ const updateBtn = byId('su-update');
246
+ if (updateBtn) {
247
+ updateBtn.addEventListener('click', (e) => {
248
+ const pill = window.OTBUpdatePill;
249
+ if (pill && pill.updateNow(data, { api })) e.preventDefault();
250
+ });
251
+ }
252
+
239
253
  const more = byId('su-more');
240
254
  if (more) {
241
255
  more.addEventListener('click', () => {
@@ -275,7 +289,7 @@
275
289
  if (asked) return;
276
290
  asked = true;
277
291
  const body = byId('su-body');
278
- if (body) body.innerHTML = '<p class="panel-hint">Checking for a newer version…</p>';
292
+ if (body) body.innerHTML = '<p class="hall-loading hall-loading--inline" role="status">Checking for a newer version…</p>';
279
293
  try {
280
294
  const res = await api.request('GET', `${API}/software-update`);
281
295
  if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
@@ -276,7 +276,7 @@
276
276
  return `
277
277
  <form class="speciality-offer" data-id="${escapeHtml(String(s.id))}">
278
278
  <h4 class="speciality-create__h">What ${escapeHtml(s.name)} offers</h4>
279
- <div class="speciality-offer__choices">${state.offerChoices || '<p class="prow__hint">Loading this project’s skills…</p>'}</div>
279
+ <div class="speciality-offer__choices">${state.offerChoices || '<p class="hall-loading hall-loading--inline" role="status">Loading this project’s skills…</p>'}</div>
280
280
  <div class="speciality-actions">
281
281
  <button type="submit" class="btn" ${state.offerChoices ? '' : 'disabled'}>Save what it offers</button>
282
282
  </div>
@@ -54,6 +54,8 @@
54
54
  <nav class="section-rail" id="settings-rail" aria-label="Settings sections"></nav>
55
55
 
56
56
  <div class="settings-groups" id="settings-groups" data-active="account">
57
+ <!-- task 1005043: the panel loader, until settings.js has read who is viewing (hideLayout). -->
58
+ <p class="hall-loading hall-loading--page" id="settings-loading" role="status">Loading…</p>
57
59
 
58
60
  <!-- ============ group: account ============ -->
59
61
 
@@ -344,7 +346,7 @@
344
346
  </label>
345
347
  <details id="speciality-create-offer" class="speciality-offer-box">
346
348
  <summary>Skills it offers <small>(optional)</small></summary>
347
- <div id="speciality-create-skills"><p class="prow__hint">Loading this project’s skills…</p></div>
349
+ <div id="speciality-create-skills"><p class="hall-loading hall-loading--inline" role="status">Loading this project’s skills…</p></div>
348
350
  </details>
349
351
  <div class="speciality-actions">
350
352
  <button type="submit" class="btn">Create speciality</button>
@@ -473,9 +475,9 @@
473
475
  <!-- SOFTWARE UPDATE — this project's version, the newest release its update
474
476
  channel allows, and what the versions between them carry (task 1004296).
475
477
  Like a phone's Software Update screen: every builder reads it; the Update
476
- button is drawn only for those the upgrade door lets through, and it LEADS
477
- to that door (the two-step confirm and auto-rollback of ADR 0293) — the
478
- act never happens on this page. Rendered by settings-software-update.js
478
+ button is drawn only for those the upgrade door lets through, and it runs
479
+ the Update pill's in-place flow (update-pill.js, task 1005041): the act is
480
+ still the door's, with its auto-rollback (ADR 0293). Rendered by settings-software-update.js
479
481
  from GET /api/bongos/software-update, read only when this group opens:
480
482
  an update's notes come out of a package download. -->
481
483
  <section class="scroll" id="software-update-scroll" aria-labelledby="software-update-h" data-group="update" hidden>
@@ -512,7 +514,7 @@
512
514
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
513
515
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
514
516
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
515
- <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
517
+ <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
516
518
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
517
519
  <script src="/builders/palette.js" defer></script>
518
520
  <!-- shared component kit (window.OTBKit) — section rail (task 2166). -->
@@ -538,7 +540,9 @@
538
540
  settings.js is against the 1500-line ratchet. -->
539
541
  <script src="/builders/settings-render.js?v=2026-09-16-render-levels"></script>
540
542
  <!-- task 1004296: Settings → Software update. Own file on the same precedent. -->
541
- <script src="/builders/settings-software-update.js?v=2026-10-18-history"></script>
543
+ <!-- the Update pill's flow, which the panel's Update button runs in place (task 1005041) -->
544
+ <script src="/builders/update-pill.js?v=2026-11-03-pill"></script>
545
+ <script src="/builders/settings-software-update.js?v=2026-11-03-pill"></script>
542
546
  <!-- task 1004501: Settings → Your Autobongos runner. Own file on the same precedent. -->
543
547
  <script src="/builders/settings-autobongos.js?v=2026-10-01-autobongos"></script>
544
548
  <!-- task 1003793: Settings → Module install reports. Own file on the same precedent. -->
@@ -1186,6 +1186,9 @@
1186
1186
  function hideLayout(hide) {
1187
1187
  const layout = document.getElementById('settings-layout');
1188
1188
  if (layout) layout.hidden = !!hide;
1189
+ // Every path past /me decides the layout here, so the page loader stops here too (task 1005043).
1190
+ const loading = document.getElementById('settings-loading');
1191
+ if (loading) loading.hidden = true;
1189
1192
  }
1190
1193
 
1191
1194
  // ---- bootstrap -----------------------------------------------------------
@@ -751,8 +751,11 @@
751
751
  history.replaceState(null, '', window.location.pathname + rest + window.location.hash);
752
752
  }
753
753
 
754
- // task 1004045 — the core-update banner. The owner's ask: "a popup banner at the
755
- // top of every owners builders hall that a core update has been released".
754
+ // task 1004045 → task 1005041 — the Update pill. The owner's first ask was a banner at the top
755
+ // of every owner's hall when a core update is released; the second (2026-11-03) was that it
756
+ // was "way too obstructing and not obvious what to do", and should feel like the Claude
757
+ // app's "restart to update". So the news is a quiet pill in the top bar now, and the
758
+ // update is taken in place from it: update-pill.js owns all of it, this only asks.
756
759
  //
757
760
  // WHY THE SERVER IS ASKED RATHER THAN TOLD. The version this box runs is known
758
761
  // here, but what is PUBLISHED is not, and the fact lives behind an outbound
@@ -764,6 +767,11 @@
764
767
  // ASKED ONCE PER PAGE LOAD. applyAccess runs on every settle (the /me probe, a
765
768
  // page's own report), and re-firing a network call on each would be a request per
766
769
  // nav interaction for a fact with a six-hour life on the server.
770
+ //
771
+ // SAID FROM AN ANSWER, OR FROM AN UPDATE ALREADY UNDER WAY. The pill script loads only
772
+ // when the answer has something to say (behind, held, or just moved), or when this
773
+ // browser remembers an update it started: mid-restart the read itself may fail, and the
774
+ // pill must still follow the owner from page to page.
767
775
  var coreUpdateAsked = false;
768
776
  function maybeRenderCoreUpdate() {
769
777
  if (coreUpdateAsked) return;
@@ -771,10 +779,20 @@
771
779
  if (!perms || perms['core.pin.move'] !== true) return;
772
780
  coreUpdateAsked = true;
773
781
  api.request('GET', '/api/bongos/core-update').then(function (res) {
774
- if (!res || res.status !== 200 || !res.data) return;
775
- if (res.data.update) renderCoreUpdateBanner(res.data.update, res.data.held);
776
- if (res.data.moved) renderCoreMoveNotice(res.data.moved); // task 1004731: loads core-move-notice.js, which draws it
777
- }, function () { /* fail-soft: no banner is the correct answer to no answer */ });
782
+ mountUpdatePill(res && res.status === 200 && res.data ? res.data : null);
783
+ }, function () { mountUpdatePill(null); });
784
+ }
785
+ function updateUnderWay() {
786
+ try { return !!(window.localStorage && window.localStorage.getItem('bongos.update.run')); } catch (e) { return false; }
787
+ }
788
+ function mountUpdatePill(data) {
789
+ var u = data && data.update;
790
+ var news = !!(u && u.reading === 'ok' && u.behind) || !!(data && data.held);
791
+ if (news || updateUnderWay()) {
792
+ var go = function () { if (window.OTBUpdatePill) window.OTBUpdatePill.mount(data, { api: api }); };
793
+ if (window.OTBUpdatePill) go(); else loadHallScript('update-pill.js', go);
794
+ }
795
+ if (data && data.moved) renderCoreMoveNotice(data.moved); // task 1004731: loads core-move-notice.js, which draws it
778
796
  }
779
797
  // One hall script on demand, stamped with the shell's own ?v= (task 1004996 made it one helper).
780
798
  function loadHallScript(file, onload, onerror) {
@@ -785,7 +803,7 @@
785
803
  head.appendChild(tag);
786
804
  }
787
805
  function renderCoreMoveNotice(moved) {
788
- loadHallScript('core-move-notice.js', function () { if (window.CoreMoveNotice) window.CoreMoveNotice.render(moved, { api: api, href: SOFTWARE_UPDATE_HREF }); });
806
+ loadHallScript('core-move-notice.js', function () { if (window.CoreMoveNotice) window.CoreMoveNotice.render(moved, { api: api, href: '/settings#software-update' }); });
789
807
  }
790
808
  var githubChipAsked = false; // task 1004996: the Link GitHub chip (github-chip.js), asked once a page
791
809
  function loadGithubChip(builder) {
@@ -795,72 +813,6 @@
795
813
  loadHallScript('github-chip.js', function () { if (window.OTBGithubChip) window.OTBGithubChip.mount({ api: api, builder: builder, hub: hub }); });
796
814
  }
797
815
 
798
- // Where the banner's Upgrade goes (task 1004296). The platform's hall runs the deploy
799
- // door itself, so there it is /deploy. Every other hall — a hosted project, a
800
- // self-hosted instance — takes its update from Settings → Software update, which says
801
- // what the update carries and, on a hosted project, hands the owner to the hub's door
802
- // for this project (ADR 0339 D3). The banner no longer links off-site itself.
803
- var SOFTWARE_UPDATE_HREF = '/settings#software-update';
804
-
805
- // SAY NOTHING WHEN THERE IS NOTHING TO SAY. `behind` is false both when the box
806
- // is current AND when the registry could not be read; `reading` separates them,
807
- // and neither renders. A banner is a claim about being out of date, and a wrong
808
- // one on a project that is actually current is worse than silence.
809
- function renderCoreUpdateBanner(update, held) {
810
- if (!update || update.reading !== 'ok' || !update.behind || !update.latest) return;
811
- // A dismissal is remembered PER VERSION, not forever: the next release is new
812
- // news and must re-announce itself, or the surface is dead after one click.
813
- // localStorage throws in a private window and is absent in the unit tests' bare
814
- // vm context, so every touch is guarded and its loss only costs the memory.
815
- try {
816
- var seen = window.localStorage && window.localStorage.getItem('bongos.coreUpdate.dismissed');
817
- if (seen && seen === update.latest) return;
818
- } catch (e) { /* no storage: always show */ }
819
- var topbar = document.getElementById('app-topbar');
820
- if (!topbar) return;
821
- var el = document.createElement('div');
822
- el.className = 'core-update-notice';
823
- el.id = 'core-update-notice';
824
- el.setAttribute('role', 'status');
825
- // The action goes where the update can actually be taken. The deploy door is the
826
- // provisioning module's, and a hosted project does NOT run that module (probed:
827
- // /api/bongos/provisioning/health is 404 on a tenant, 200 on the platform) — so on
828
- // those it goes to Settings → Software update (SOFTWARE_UPDATE_HREF above).
829
- // NOT A LINK TO THE PAGE YOU ARE ALREADY ON (task 1004145). This reads as the action
830
- // button — it is labelled with the verb and styled like one — but it is a plain
831
- // navigation to /deploy. On /deploy itself that is a no-op, and the owner who
832
- // pressed it there reasonably reported "I clicked update, but I didn't see anything
833
- // happen": nothing had, and nothing had been asked for. The control is on that page
834
- // already, so on it the banner informs and points, and does not pretend to act.
835
- // The same rule on Settings → Software update: that panel IS the control there.
836
- var here = window.location ? String(window.location.pathname || '').replace(/\/+$/, '') : '';
837
- var onDeploy = here === '/deploy';
838
- var onUpdatePanel = here === '/settings' && String(window.location.hash || '') === '#software-update';
839
- var action = moduleOn('provisioning')
840
- ? (onDeploy
841
- ? '<span class="core-update-notice__hint">Use the control below to take it.</span>'
842
- : '<a class="core-update-notice__action" href="/deploy">Upgrade</a>')
843
- : (onUpdatePanel
844
- ? '<span class="core-update-notice__hint">Its details are below.</span>'
845
- : '<a class="core-update-notice__action" href="' + SOFTWARE_UPDATE_HREF + '">Upgrade</a>');
846
- // task 1004730: the newest version already failed here and was rolled back. Say so, never
847
- // "available" with a button to take it again; Software update carries the full sentence.
848
- var hold = held && held.version === update.latest ? held : null;
849
- el.innerHTML = (hold
850
- ? '<span>Core <strong>' + esc(hold.version) + '</strong> was tried on this project and rolled back. It is still on ' +
851
- esc(hold.still_on) + ' and working.</span><a class="core-update-notice__action" href="' + SOFTWARE_UPDATE_HREF + '">Details</a>'
852
- : '<span>Core <strong>' + esc(update.latest) + '</strong> is available — this project runs ' +
853
- esc(update.running || 'an unknown version') + '.</span>' + action) +
854
- '<button type="button" class="core-update-notice__close" aria-label="Dismiss">×</button>';
855
- el.querySelector('.core-update-notice__close').addEventListener('click', function () {
856
- try {
857
- if (window.localStorage) window.localStorage.setItem('bongos.coreUpdate.dismissed', update.latest);
858
- } catch (e) { /* dismissal is best-effort */ }
859
- el.remove();
860
- });
861
- topbar.insertAdjacentElement('afterend', el);
862
- }
863
-
864
816
  // task 1003303: the skip link, rendered once here rather than pasted into 27
865
817
  // heads. The hall had none, which is a platform floor (DESIGN.md
866
818
  // §Accessibility Floors) and, with the sidebar first in the DOM, meant a
@@ -231,7 +231,7 @@
231
231
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
232
232
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
233
233
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
234
- <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
234
+ <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
235
235
  <script src="/builders/palette.js" defer></script>
236
236
  <script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
237
237
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3"></script>
@@ -219,7 +219,7 @@
219
219
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
220
220
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
221
221
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
222
- <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
222
+ <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
223
223
  <script src="/builders/palette.js" defer></script>
224
224
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
225
225
  (task 1003829), which the older reviews still show. -->
@@ -755,7 +755,7 @@
755
755
 
756
756
  function statusHtml() {
757
757
  if (!home.pages) {
758
- return `<p class="st-panel__line">${home.pagesFailed ? 'The pages could not be read just now. Try again in a moment.' : 'Reading the pages…'}</p>`;
758
+ return home.pagesFailed ? '<p class="st-panel__line">The pages could not be read just now. Try again in a moment.</p>' : '<p class="hall-loading" role="status">Reading the pages…</p>';
759
759
  }
760
760
  const rows = lib.surfaceRows(home.pages, lib.landingHost(byId('hall-main') && byId('hall-main').getAttribute('data-apex'), window.location && window.location.host)).map((s) => `
761
761
  <li class="st-surface">
@@ -804,7 +804,7 @@
804
804
 
805
805
  function siteHtml() {
806
806
  if (!home.site) {
807
- return `<p class="st-panel__line">${home.siteFailed ? SITE_FAILED : 'Reading the reviews…'}</p>`;
807
+ return home.siteFailed ? `<p class="st-panel__line">${SITE_FAILED}</p>` : '<p class="hall-loading" role="status">Reading the reviews…</p>';
808
808
  }
809
809
  if (!home.site.length) return `<p class="st-panel__line">${SITE_NONE}</p>`;
810
810
  const rows = home.site.map((r) => `
@@ -1178,7 +1178,7 @@
1178
1178
  }
1179
1179
 
1180
1180
  function siteListPage() {
1181
- if (!home.site) return `<p class="st-panel__line">${home.siteFailed ? SITE_FAILED : 'Reading the reviews…'}</p>`;
1181
+ if (!home.site) return home.siteFailed ? `<p class="st-panel__line">${SITE_FAILED}</p>` : '<p class="hall-loading" role="status">Reading the reviews…</p>';
1182
1182
  if (!home.site.length) return `<p class="st-panel__line">${SITE_NONE}</p>`;
1183
1183
  return `<ul class="st-doc-list">${home.site.map((r) => `<li><a class="st-doc-list__row" href="${escapeHtml(siteHref(r.link))}">
1184
1184
  <span class="st-doc-list__page">${escapeHtml(r.page_title || r.page_path || r.page_id)}</span>
@@ -1038,54 +1038,134 @@ html[data-theme="dark"] .gplate__light { display: none; }
1038
1038
  line-height: 1;
1039
1039
  cursor: pointer;
1040
1040
  }
1041
- /* Core-update banner (task 1004045) — rendered by shell.js when this instance is
1042
- behind the newest published core. Deliberately the accent, not the warning
1043
- colour the denial notice uses: being a release behind is news, not a fault. */
1044
- .core-update-notice {
1045
- display: flex;
1046
- align-items: baseline;
1047
- gap: 12px;
1048
- padding: 10px 22px;
1049
- font-size: 13px;
1050
- color: var(--accent);
1051
- background: var(--accent-weak, var(--warn-weak));
1052
- border-bottom: 1px solid var(--rule-soft);
1041
+ /* ---------- the Update pill and its card (task 1005041, update-pill.js) ----------
1042
+ The owner: the full-width update banners were "way too obstructing"; it should feel
1043
+ like the Claude app's "restart to update". So the news is a quiet pill in the top bar
1044
+ (the Link GitHub chip's shape, one step louder: the accent dot), and everything else
1045
+ lives in a small card under it. The same card carries the one-time "Bongos is updated"
1046
+ note (core-move-notice.js), pinned to the same corner. */
1047
+ .upd-pill {
1048
+ display: inline-flex;
1049
+ align-items: center;
1050
+ gap: 6px;
1051
+ min-height: 28px;
1052
+ min-width: 28px;
1053
+ padding: 0 11px 0 9px;
1054
+ border: 1px solid var(--rule);
1055
+ border-radius: var(--radius-pill);
1056
+ background: var(--bg-card);
1057
+ color: var(--ink);
1058
+ font: inherit;
1059
+ font-size: 12px;
1060
+ font-weight: 500;
1061
+ line-height: 1;
1062
+ white-space: nowrap;
1063
+ flex-shrink: 0;
1064
+ cursor: pointer;
1053
1065
  }
1054
- .core-update-notice__action {
1055
- margin-left: auto;
1056
- font-weight: 600;
1057
- color: inherit;
1066
+ .upd-pill:hover { border-color: var(--ink-faint); }
1067
+ .upd-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1068
+ .upd-pill__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
1069
+ .upd-pill[data-face="held"] .upd-pill__dot { background: var(--ink-faint); }
1070
+ .upd-pill[data-face="done"] { background: var(--accent); border-color: var(--accent); color: var(--accent-on); }
1071
+ .upd-pill[data-face="done"] .upd-pill__dot { background: var(--accent-on); }
1072
+ .upd-pill[data-face="done"]:hover { background: var(--accent-lift); border-color: var(--accent-lift); }
1073
+ /* The running face turns a ring. The Kill Switch stops it, and the label still says
1074
+ "Updating…", so the rest frame is the whole message. */
1075
+ .upd-pill__spin {
1076
+ width: 10px; height: 10px;
1077
+ border: 1.5px solid var(--rule);
1078
+ border-top-color: var(--accent);
1079
+ border-radius: 50%;
1080
+ flex-shrink: 0;
1081
+ animation: upd-turn 0.9s linear infinite;
1058
1082
  }
1059
- /* The banner on /deploy itself: it still announces the release, but points at the
1060
- control instead of linking to the page the reader is already on (task 1004145). */
1061
- .core-update-notice__hint {
1062
- margin-left: auto;
1063
- color: var(--ink-soft);
1083
+ @keyframes upd-turn { to { transform: rotate(360deg); } }
1084
+
1085
+ .upd-card {
1086
+ position: fixed;
1087
+ top: 64px;
1088
+ right: 16px;
1089
+ z-index: calc(var(--z-sidebar) + 5);
1090
+ width: min(360px, calc(100vw - 32px));
1091
+ max-height: calc(100vh - 96px);
1092
+ overflow-y: auto;
1093
+ padding: 16px 18px 18px;
1094
+ border: 1px solid var(--rule-soft);
1095
+ border-radius: var(--radius);
1096
+ background: var(--bg-card);
1097
+ color: var(--ink);
1098
+ box-shadow: var(--shadow-pop);
1099
+ font-size: 13px;
1100
+ line-height: 1.5;
1064
1101
  }
1065
- .core-update-notice__close {
1066
- min-width: 24px;
1067
- min-height: 24px;
1102
+ .upd-card[hidden] { display: none; }
1103
+ .upd-card p { margin: 0 0 8px; }
1104
+ .upd-card__h { margin: 0 32px 8px 0; font-size: 15px; font-weight: 600; line-height: 1.3; }
1105
+ .upd-card__close {
1106
+ position: absolute;
1107
+ top: 10px; right: 10px;
1108
+ width: 28px; height: 28px;
1068
1109
  border: 0;
1110
+ border-radius: var(--radius-pill);
1069
1111
  background: none;
1070
- color: inherit;
1071
- font-size: 16px;
1112
+ color: var(--ink-soft);
1113
+ font-size: 18px;
1072
1114
  line-height: 1;
1073
1115
  cursor: pointer;
1074
1116
  }
1075
- /* With no action link the close button still sits hard right. */
1076
- .core-update-notice__action + .core-update-notice__close { margin-left: 0; }
1077
- .core-update-notice > span + .core-update-notice__close { margin-left: auto; }
1078
- /* "Upgraded to core X on <date>" and what changed (task 1004731): the same strip, with the
1079
- release-note lines under the sentence in the body ink; it wraps on a phone. */
1080
- .core-move-notice { flex-wrap: wrap; }
1081
- .core-move-notice__body { flex: 1 1 20rem; min-width: 0; }
1082
- .core-move-notice__list {
1083
- margin: 4px 0 0;
1084
- padding-left: 1.15rem;
1085
- max-width: var(--measure, 68ch);
1086
- color: var(--ink-soft);
1117
+ .upd-card__close:hover { background: var(--glass-strong); color: var(--ink); }
1118
+ .upd-card__list { margin: 0 0 10px; padding-left: 1.1rem; color: var(--ink-soft); }
1119
+ .upd-card__list li + li { margin-top: 4px; }
1120
+ .upd-card__note { color: var(--ink-soft); }
1121
+ .upd-card__ver { color: var(--ink-faint); font-size: 12px; }
1122
+ .upd-card__err { color: var(--danger); }
1123
+ .upd-ver { font-family: var(--font-mono); font-size: 12px; }
1124
+ .upd-card__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
1125
+ .upd-btn {
1126
+ display: inline-flex;
1127
+ align-items: center;
1128
+ justify-content: center;
1129
+ min-height: 36px;
1130
+ padding: 0 14px;
1131
+ border: 1px solid var(--glass-edge);
1132
+ border-radius: var(--radius-pill);
1133
+ background: transparent;
1134
+ color: var(--ink);
1135
+ font: inherit;
1136
+ font-size: 13px;
1137
+ text-decoration: none;
1138
+ cursor: pointer;
1087
1139
  }
1088
- .core-move-notice__list li + li { margin-top: 2px; }
1140
+ .upd-btn:hover { background: var(--glass-strong); text-decoration: none; }
1141
+ .upd-btn:focus-visible, .upd-card__close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1142
+ .upd-btn--go { background: var(--accent); border-color: var(--accent); color: var(--accent-on); font-weight: 500; }
1143
+ .upd-btn--go:hover { background: var(--accent-lift); border-color: var(--accent-lift); }
1144
+ .upd-btn:disabled { opacity: 1; color: var(--ink-faint); background: var(--glass); border-color: var(--glass-edge); cursor: default; }
1145
+ /* An indeterminate bar: how long an upgrade takes is not known here, so it never fills. */
1146
+ .upd-track { position: relative; height: 4px; margin: 4px 0 10px; border-radius: 2px; background: var(--glass-strong); overflow: hidden; }
1147
+ .upd-track span {
1148
+ position: absolute; top: 0; bottom: 0; left: 0;
1149
+ width: 40%;
1150
+ border-radius: 2px;
1151
+ background: var(--accent);
1152
+ animation: upd-slide 1.6s ease-in-out infinite;
1153
+ }
1154
+ @keyframes upd-slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(250%); } }
1155
+ /* The one-time note sits under the top bar's right end too, on every page that loads it. */
1156
+ .upd-card--note { top: 64px; }
1157
+ @media (max-width: 640px) {
1158
+ .upd-card { right: 16px; left: 16px; width: auto; }
1159
+ }
1160
+ /* The hub's handoff window (deploy-handoff.js): a small popup with nothing in it but the card.
1161
+ The shell still runs (it signs the viewer in), so its chrome is hidden, not removed. */
1162
+ html[data-handoff="on"] .app-sidebar,
1163
+ html[data-handoff="on"] .app-topbar,
1164
+ html[data-handoff="on"] .app-foot,
1165
+ html[data-handoff="on"] .upd-pill,
1166
+ html[data-handoff="on"] .core-move-notice { display: none; }
1167
+ html[data-handoff="on"] .app-content { padding: 0; }
1168
+ .upd-card--handoff { position: static; width: auto; max-width: 420px; max-height: none; margin: 24px auto; box-shadow: none; }
1089
1169
  .topbar-menu {
1090
1170
  display: none;
1091
1171
  align-items: center;
@@ -4317,7 +4397,8 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
4317
4397
  .empty-state__action { display: inline-block; margin-top: 6px; font-size: 13px; }
4318
4398
 
4319
4399
  /* skeleton shimmer — standard-height placeholder rows while data loads, so
4320
- cards don't jump; derives from the ink/bg tokens like everything else */
4400
+ cards don't jump; derives from the ink/bg tokens like everything else. It
4401
+ breathes in the loader's beat (task 1005043; the keyframes sit with it). */
4321
4402
  .kit-skel {
4322
4403
  display: inline-block;
4323
4404
  width: 60%;
@@ -4325,9 +4406,97 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
4325
4406
  height: 12px;
4326
4407
  border-radius: 2px;
4327
4408
  background: var(--skeleton);
4409
+ animation: hall-loading-breathe .8s ease-in-out infinite alternate-reverse;
4328
4410
  }
4329
4411
  .kit-skel-row td { border-bottom: 1px solid var(--rule-soft); }
4330
4412
 
4413
+ /* ---- the loader (task 1005043) ----------------------------------------------
4414
+ One loader for everything a hall page waits on, drawn from the Bongos mark
4415
+ (shell.js's brand-mark SVG: the ring, the core and the accent dot). A page
4416
+ opts in by giving its existing placeholder the class; its own words stay as
4417
+ the caption, so no page gains markup. The ring and the dot turn together round
4418
+ the still core and the ink breathes, after the Vibe skeleton the owner pointed
4419
+ at (vibe.monday.com: 0.4 to 1 and back, 0.8s). Three sizes: the default for a
4420
+ panel, --page for a page's main area, --inline for a line in a list or card.
4421
+
4422
+ DESIGN.md holds it: CSS only, transform and filter only (the breath and the
4423
+ arrival are filter: opacity(), not opacity), every colour a var() or a mix of
4424
+ one; the black in the two masks is alpha, not paint. The 20s floor is for the
4425
+ ambient plates: a loader is the live thing on the page, so it keeps a loader's
4426
+ tempo and the dot spends the accent. Each turn eases out of and back into the
4427
+ logo's own pose, so the mark lands on the logo once per beat. It arrives after
4428
+ 240ms, so a read that answers faster never shows it. Under the Kill Switch
4429
+ nothing moves and the rest frame is the logo itself. ---- */
4430
+ .hall-loading {
4431
+ --hl-size: 40px;
4432
+ --hl-pad: 28px;
4433
+ --hl-beat: 1.6s;
4434
+ position: relative;
4435
+ display: flex;
4436
+ flex-direction: column;
4437
+ align-items: center;
4438
+ gap: 12px;
4439
+ margin: 0;
4440
+ padding: var(--hl-pad) 0;
4441
+ font-size: 13.5px;
4442
+ color: var(--ink-faint);
4443
+ text-align: center;
4444
+ animation: hall-loading-arrive 500ms cubic-bezier(.16, 1, .3, 1) 240ms both;
4445
+ }
4446
+ .hall-loading::before,
4447
+ .hall-loading::after {
4448
+ content: "";
4449
+ display: block;
4450
+ flex: none;
4451
+ width: var(--hl-size);
4452
+ height: var(--hl-size);
4453
+ -webkit-mask: var(--hl-shape) center / contain no-repeat;
4454
+ mask: var(--hl-shape) center / contain no-repeat;
4455
+ animation: hall-loading-turn var(--hl-beat) cubic-bezier(.45, .05, .55, .95) infinite;
4456
+ }
4457
+ /* the ring and the core: one ink, breathing */
4458
+ .hall-loading::before {
4459
+ --hl-shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cpath d='M137 31 A78 78 0 1 0 170 66' fill='none' stroke='black' stroke-width='15' stroke-linecap='round'/%3E%3Ccircle cx='100' cy='100' r='55'/%3E%3C/svg%3E");
4460
+ background: color-mix(in srgb, var(--ink) 30%, var(--bg-card));
4461
+ animation:
4462
+ hall-loading-turn var(--hl-beat) cubic-bezier(.45, .05, .55, .95) infinite,
4463
+ hall-loading-breathe calc(var(--hl-beat) / 2) ease-in-out infinite alternate-reverse;
4464
+ }
4465
+ /* the dot: the accent, laid exactly over the mark and turning with it */
4466
+ .hall-loading::after {
4467
+ --hl-shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Ccircle cx='156' cy='46' r='12'/%3E%3C/svg%3E");
4468
+ position: absolute;
4469
+ top: var(--hl-pad);
4470
+ left: calc(50% - var(--hl-size) / 2);
4471
+ background: var(--accent);
4472
+ }
4473
+ .hall-loading--page { --hl-size: 64px; --hl-pad: 72px; gap: 16px; }
4474
+ .hall-loading--inline {
4475
+ --hl-size: 16px;
4476
+ --hl-pad: 0px;
4477
+ display: inline-flex;
4478
+ flex-direction: row;
4479
+ gap: 8px;
4480
+ text-align: left;
4481
+ }
4482
+ .hall-loading--inline::after { top: calc(50% - var(--hl-size) / 2); left: 0; }
4483
+ /* --wait: a page whose every section stays [hidden] until it knows who is
4484
+ reading (the sealed/body pairs) puts one loader FIRST in its <main>. It shows
4485
+ only while nothing after it is showing, and steps aside the moment the page
4486
+ reveals anything (the body, the sealed notice, an error), so no page script
4487
+ has to remember to hide it. Without :has() it never shows: today's page. */
4488
+ /* --zones: the same, for a view made of always-present, empty zones that cards
4489
+ mount into (Home, Watch's panes): it shows while no showing zone after it
4490
+ holds a showing child. */
4491
+ .hall-loading--wait, .hall-loading--zones { display: none; }
4492
+ @supports selector(:has(a)) {
4493
+ .hall-loading--wait:not(:has(~ :not([hidden], script, template, style))) { display: flex; }
4494
+ .hall-loading--zones:not(:has(~ :not([hidden]) > :not([hidden], script, template, style))) { display: flex; }
4495
+ }
4496
+ @keyframes hall-loading-arrive { from { filter: opacity(0); transform: translateY(6px); } }
4497
+ @keyframes hall-loading-turn { to { transform: rotate(1turn); } }
4498
+ @keyframes hall-loading-breathe { from { filter: opacity(.45); } }
4499
+
4331
4500
  /* collapsible card body — the Work-backlog <details> idiom, generalized */
4332
4501
  .kit-collapse__summary {
4333
4502
  cursor: pointer;
@@ -413,7 +413,7 @@
413
413
  }
414
414
 
415
415
  async function renderPromote() {
416
- box.innerHTML = '<p class="deeplink-card__note">Loading versions…</p>';
416
+ box.innerHTML = '<p class="hall-loading hall-loading--inline" role="status">Loading versions…</p>';
417
417
  // Both dead ends offer the way back through the same delegated [data-go]
418
418
  // handler the forms use — a builder must never be stranded mid-panel.
419
419
  function bail(message) {
@@ -658,7 +658,7 @@
658
658
  function bodyFor(state) {
659
659
  var nodes = [];
660
660
  var label = '#' + state.id;
661
- if (state.loading) { nodes.push(h('p', 'deeplink-card__title', 'Loading ' + label + '…')); return nodes; }
661
+ if (state.loading) { nodes.push(h('p', 'hall-loading', 'Loading ' + label + '…')); return nodes; }
662
662
  if (state.needsAuth) {
663
663
  nodes.push(h('p', 'deeplink-card__title', label));
664
664
  nodes.push(h('p', 'deeplink-card__note', 'Sign in to the Builders’ Hall to view this record.'));
@@ -59,7 +59,7 @@
59
59
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
60
60
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
61
61
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
62
- <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
62
+ <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
63
63
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
64
64
  <script src="/builders/palette.js" defer></script>
65
65
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N deep-link overlay