@bongos/core 1.21.28 → 1.21.30

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 (59) hide show
  1. package/.bongos-core.json +73 -63
  2. package/docs/api/openapi.json +1 -1
  3. package/docs/api-reference.md +1 -1
  4. package/docs/copy-inventory.md +386 -386
  5. package/docs/copy-registry.json +452 -452
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/page-readings.json +462 -462
  8. package/modules/hall-ui/public/atlas.html +3 -3
  9. package/modules/hall-ui/public/blockers.html +3 -3
  10. package/modules/hall-ui/public/board-room.html +3 -3
  11. package/modules/hall-ui/public/city.html +3 -3
  12. package/modules/hall-ui/public/collab.html +3 -3
  13. package/modules/hall-ui/public/commands.html +3 -3
  14. package/modules/hall-ui/public/copy-desk.html +3 -3
  15. package/modules/hall-ui/public/craft-mode.js +57 -3
  16. package/modules/hall-ui/public/deploy.html +3 -3
  17. package/modules/hall-ui/public/diagrams.html +3 -3
  18. package/modules/hall-ui/public/drachmae.html +3 -3
  19. package/modules/hall-ui/public/fleet.html +3 -3
  20. package/modules/hall-ui/public/gate.html +3 -3
  21. package/modules/hall-ui/public/goal-map.html +3 -3
  22. package/modules/hall-ui/public/goals.html +3 -3
  23. package/modules/hall-ui/public/government.html +3 -3
  24. package/modules/hall-ui/public/idea.html +3 -3
  25. package/modules/hall-ui/public/ideas.html +3 -3
  26. package/modules/hall-ui/public/index.html +3 -3
  27. package/modules/hall-ui/public/modules.html +3 -3
  28. package/modules/hall-ui/public/primer.html +3 -3
  29. package/modules/hall-ui/public/profile.html +3 -3
  30. package/modules/hall-ui/public/project-settings.html +3 -3
  31. package/modules/hall-ui/public/ranks.html +3 -3
  32. package/modules/hall-ui/public/roadmap.html +3 -3
  33. package/modules/hall-ui/public/roster.html +3 -3
  34. package/modules/hall-ui/public/sessions.html +3 -3
  35. package/modules/hall-ui/public/settings.html +3 -3
  36. package/modules/hall-ui/public/shell.js +2 -0
  37. package/modules/hall-ui/public/street.html +3 -3
  38. package/modules/hall-ui/public/studio.html +3 -3
  39. package/modules/hall-ui/public/style.css +2 -0
  40. package/modules/hall-ui/public/task.html +3 -3
  41. package/modules/hall-ui/public/thinking.html +3 -3
  42. package/modules/hall-ui/public/tweak-editor.html +3 -3
  43. package/modules/hall-ui/public/watch.html +3 -3
  44. package/modules/hall-ui/public/work.html +3 -3
  45. package/modules/hall-ui/records/craft-mode.md +7 -0
  46. package/modules/provisioning/routes/callback-page.js +28 -65
  47. package/modules/provisioning/routes/provisioning.js +6 -6
  48. package/modules/public-landing/public/projects.html +68 -3
  49. package/package-lock.json +2 -2
  50. package/package.json +1 -1
  51. package/release-notes.json +12 -0
  52. package/src/module-api.js +1 -1
  53. package/tests/craft_mode_nav.mjs +202 -0
  54. package/tests/hall_tweak_editor.mjs +1 -1
  55. package/tests/nav_permission_atoms.mjs +1 -1
  56. package/tests/provisioning_callback_page.mjs +5 -5
  57. package/tests/provisioning_github_app_routes.mjs +24 -25
  58. package/tests/wizard_auto_open.mjs +94 -0
  59. package/tests/wizard_intent_resume.mjs +17 -2
@@ -171,9 +171,9 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell-trail.js?v=2026-10-02-craft"></script>
175
- <script src="/builders/craft-mode.js?v=2026-10-02-craft"></script>
176
- <script src="/builders/shell.js?v=2026-10-02-craft"></script>
174
+ <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
175
+ <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
176
+ <script src="/builders/shell.js?v=2026-10-03-nav"></script>
177
177
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
178
178
  <script src="/builders/palette.js" defer></script>
179
179
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -20,3 +20,10 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
20
20
  ## The carve
21
21
 
22
22
  `shell.js` sat at exactly 1,500 lines, the fitness ratchet, so the Back link + trail section (tasks 1004430/1004476/1004477) moved verbatim into `shell-trail.js` (`window.OTBShellTrail.make({ esc, nav })`). Behaviour is unchanged; `tests/hall_fullscreen.mjs` loads it before the shell and still drives the trail through the real top bar.
23
+
24
+ ## The nav follows the mode (task 1004493)
25
+
26
+ - **Each craft has a named tab list**, `MODE_TABS` in `craft-mode.js`, by NAV id. `ALWAYS_TABS` is the owner's list of tabs no mode may hide (2026-10-01: Builders, NAV id `roster`, first), and every mode's list is `ALWAYS_TABS` plus its own. Legacy and no mode have no entry, so they show every tab, as before.
27
+ - **Government shows only in Governing**, and outside that group Governing is the shortest list (Blockers, Board Room). Building is every tab except Government and the Board Room; Artistry is Studio, Goals, Your thinking; Ideating is Sky, Your thinking, Goals. The prototype names rooms that do not exist yet (Store, Page reviews, Treasury); a list names only real NAV items, and `tests/craft_mode_nav.mjs` requires every NAV item to sit in some mode.
28
+ - **The mode only ever takes away, and never through `hidden`.** The access gate owns `hidden`; `applyNav()` marks what the mode leaves out with `[data-craft-off]`, which `style.css` hides. So a mode can never show a tab the gate hid, and switching mode just moves the attribute. A group head goes too once nothing under it shows.
29
+ - **Two callers.** `apply()` runs it on every mode change (live, no reload), and `shell.js` `applyAccess` runs it right after its gate loop, so a later `/me` answer (a promotion, a permission) brings a head back with its tabs. Hidden tabs still open by their URL; nothing is enforced (ADR 0016).
@@ -15,7 +15,7 @@
15
15
  //
16
16
  // Shape: no DB, no network, no request object — given a plain options bag it returns
17
17
  // a string. `esc` and `callbackPage` are the whole surface; PAGE_STYLE and
18
- // CALLBACK_SCRIPT stay private because they are only ever this page's own body.
18
+ // HANDBACK_SCRIPT stay private because they are only ever this page's own body.
19
19
  'use strict';
20
20
 
21
21
  const api = require('../../../src/module-api');
@@ -53,73 +53,44 @@ h1{font-family:"Baloo 2",system-ui,sans-serif;font-weight:700;font-size:1.5rem;l
53
53
  .sub p{margin:.6rem 0}
54
54
  code{font-family:"Fira Code",ui-monospace,SFMono-Regular,monospace;background:rgba(56,189,248,.14);color:var(--sky600);padding:.12em .45em;border-radius:6px;font-size:.9em;overflow-wrap:anywhere}
55
55
  @media (prefers-color-scheme:dark){code{color:var(--sky)}}
56
- .steps{display:flex;align-items:flex-start;justify-content:center;margin:1.9rem 0 1.1rem}
57
- .step{flex:1 1 0;max-width:9rem;position:relative}
58
- .step::before{content:"";position:absolute;top:16px;left:-50%;width:100%;height:3px;background:var(--line);z-index:0;transition:background .4s ease}
59
- .step:first-child::before{display:none}
60
- .dot{width:34px;height:34px;border-radius:50%;margin:0 auto;display:grid;place-items:center;background:var(--bg3);border:2px solid var(--line);color:var(--inkSoft);font-weight:700;font-size:.95rem;position:relative;z-index:1;transition:all .4s ease}
61
- .lbl{margin-top:.55rem;font-size:.78rem;color:var(--inkSoft);font-weight:600;line-height:1.25;padding:0 .2rem}
62
- .step.done .dot{background:var(--ok);border-color:var(--ok);color:#fff}
63
- .step.done::before{background:var(--ok)}
64
- .step.active .dot{border-color:var(--sun);color:var(--sun600);animation:pulse 1.5s ease-in-out infinite}
65
- .step.active .lbl{color:var(--ink)}
66
- .step.active .dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid transparent;border-top-color:var(--sun);animation:spin .8s linear infinite}
67
- @keyframes spin{to{transform:rotate(360deg)}}
68
- @keyframes pulse{0%,100%{box-shadow:0 0 0 4px rgba(249,115,22,.15)}50%{box-shadow:0 0 0 9px rgba(249,115,22,.04)}}
69
- @media (prefers-reduced-motion:reduce){.step.active .dot,.step.active .dot::after{animation:none}}
70
- .status-line{min-height:1.4em;color:var(--inkSoft);font-size:.94rem;margin:.4rem auto 0;max-width:24rem;line-height:1.45}
71
56
  .cta{margin-top:1.4rem}
72
57
  .btn{display:inline-block;background:var(--sun);color:var(--sunInk);text-decoration:none;font-weight:700;font-family:"Baloo 2",system-ui,sans-serif;padding:.72rem 1.5rem;border-radius:999px;font-size:1.02rem;box-shadow:0 10px 22px -8px rgba(249,115,22,.7);transition:transform .15s ease,box-shadow .15s ease}
73
58
  .btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,.8)}
74
59
  .muted{color:var(--inkSoft);opacity:.9;font-size:.85rem;margin:1.1rem auto 0;max-width:24rem;line-height:1.45}
75
- .hidden{display:none!important}
76
60
  `;
77
61
 
78
- // Client script for the PROGRESS variant only (task 2096): poll the public status
79
- // endpoint, advance the stepper, and auto-open the instance the moment its credentials
80
- // land. Deliberately contains NO backticks and NO ${...} so it survives the outer
81
- // template literal verbatim; the only server data it reads is the JSON config block
82
- // written just above it. Fails soft — a poll hiccup just retries; a long wait reveals a
83
- // manual "open" button rather than ever falsely erroring. The auto-open countdown runs
84
- // ONLY when this page is the wizard's pop-up; a same-tab return (pop-up blocked) keeps
85
- // the founder here with the door and a way back to the checklist (task 1003236).
86
- const CALLBACK_SCRIPT = `
62
+ // Client script for the HAND-BACK variant (task 1004548; replaces the task 2096 progress
63
+ // stepper). Placing the credentials takes minutes, and the founder used to wait for it
64
+ // HERE, in a second tab, away from the checklist that still had steps for them. Now the
65
+ // page hands them straight back: as the wizard's pop-up it closes itself so the checklist
66
+ // tab is in front again (that tab is already polling the same status and shows the wait);
67
+ // if the browser refuses the close, or this was a same-tab return (pop-up blocked), this
68
+ // tab becomes the checklist. The one-time state rides the URL FRAGMENT — never sent to a
69
+ // server — so a checklist opened here can keep watching the same attempt.
70
+ // Deliberately contains NO backticks and NO ${...} so it survives the outer template
71
+ // literal verbatim; the only server data it reads is the JSON config block above it.
72
+ const HANDBACK_SCRIPT = `
87
73
  (function(){
88
74
  var cfg=window.__CB_SETUP__||{};
89
- var state=cfg.state||'',domain=cfg.domain||'';
90
- if(!state||!domain)return;
91
- var target='https://'+domain;
92
- var pollUrl=location.pathname.replace('/callback','/status')+'?state='+encodeURIComponent(state);
93
- var steps=document.getElementById('steps'),statusEl=document.getElementById('status');
94
- var cta=document.getElementById('cta'),go=document.getElementById('go'),foot=document.getElementById('foot'),back=document.getElementById('back');
95
- /* Opened from the wizard as a pop-up (window.opener alive, or the tab still carries the name the wizard gave it):
96
- the checklist is in the other tab, so this one may auto-open the instance. Same-tab fallback (pop-up blocked,
97
- form target=_self): this IS the founder's checklist tab — never navigate it away; offer both doors (task 1003236). */
98
- var popup=!!window.opener||window.name==='ghAppSetup';
99
- var finished=false,tries=0,manual=false;
100
- function dot(n){return steps.querySelector('[data-step="'+n+'"]');}
101
- function setActive(n){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active','done');if(i<n){el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}else if(i===n){el.classList.add('active');}}}
102
- function allDone(){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active');el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}}
103
- function ready(){if(finished)return;finished=true;allDone();statusEl.textContent='Your instance is ready \\u2014 sign in with GitHub to claim it as owner.';cta.classList.remove('hidden');
104
- if(!popup){go.textContent='Open '+domain+' \\u2192';if(back)back.classList.remove('hidden');if(foot)foot.textContent='Your setup checklist is waiting where you left it \\u2014 finish it first, or open your instance now.';return;}
105
- var n=4;(function tick(){if(n<=0){location.href=target;return;}go.textContent='Opening '+domain+' in '+n+'\\u2026';n--;setTimeout(tick,1000);})();}
106
- function failed(){if(finished)return;finished=true;statusEl.textContent='Setup didn\\u2019t finish. Open your instance page and start the GitHub sign-in setup again.';cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';}
107
- function revealManual(){if(manual||finished)return;manual=true;cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';if(foot)foot.textContent='This is taking a little longer than usual \\u2014 you can open your instance now, or keep waiting.';}
108
- function schedule(){if(!finished)setTimeout(poll,3000);}
109
- function poll(){tries++;fetch(pollUrl,{headers:{accept:'application/json'},cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(j){if(!j){schedule();return;}if(j.ready||j.status==='done'){ready();return;}if(j.status==='error'||j.status==='expired'){failed();return;}setActive(2);if(tries===40)revealManual();schedule();}).catch(function(){schedule();});}
110
- setActive(2);poll();
75
+ var back=cfg.back||'/projects?view=new';
76
+ if(cfg.state)back+='#ghstate='+encodeURIComponent(cfg.state);
77
+ var link=document.getElementById('back');if(link)link.href=back;
78
+ var popup=false;
79
+ try{popup=!!window.opener&&!window.opener.closed;}catch(e){}
80
+ if(popup){try{window.opener.focus();}catch(e){}try{window.close();}catch(e){}}
81
+ setTimeout(function(){location.replace(back);},popup?700:300);
111
82
  })();
112
83
  `;
113
84
 
114
85
  // The standalone landing page for the public GitHub redirect (the mothership serves it;
115
86
  // it is NOT the hall shell). Two variants share the Cloud Bongos brand shell (task 2096):
116
- // • progress — ok + state + domain: an animated stepper that polls the status endpoint
117
- // and auto-opens the instance when its credentials are placed.
87
+ // • hand-back — ok + state + domain: a brief "GitHub app created" that returns the
88
+ // founder to their setup checklist, where the wait is shown (task 1004548).
118
89
  // • message — everything else (errors + the no-domain success fallback): a calm badge
119
90
  // + `body` (server-authored, trusted HTML).
120
91
  function callbackPage({ ok, title, body, state, domain }) {
121
- const progress = !!(ok && state && domain);
122
- const cfgJson = JSON.stringify({ state: state || '', domain: domain || '' }).replace(/</g, '\\u003c');
92
+ const handback = !!(ok && state && domain);
93
+ const cfgJson = JSON.stringify({ state: state || '', domain: domain || '', back: '/projects?view=new' }).replace(/</g, '\\u003c');
123
94
  const head = `<!doctype html><html lang="en"><head><meta charset="utf-8">
124
95
  <meta name="viewport" content="width=device-width, initial-scale=1">
125
96
  <meta name="color-scheme" content="light dark">
@@ -127,24 +98,16 @@ function callbackPage({ ok, title, body, state, domain }) {
127
98
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
128
99
  <link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;700;800&family=Quicksand:wght@400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
129
100
  <style>${PAGE_STYLE}</style></head><body>`;
130
- if (progress) {
101
+ if (handback) {
131
102
  return `${head}
132
103
  <main class="card">
104
+ <div class="badge-warn">✓</div>
133
105
  <h1>${esc(title)}</h1>
134
- <p class="sub">Finishing sign-in setup for <code>${esc(domain)}</code>.</p>
135
- <div class="steps" id="steps" aria-label="Setup progress">
136
- <div class="step done" data-step="1"><div class="dot">✓</div><div class="lbl">GitHub app created</div></div>
137
- <div class="step active" data-step="2"><div class="dot">2</div><div class="lbl">Placing credentials</div></div>
138
- <div class="step" data-step="3"><div class="dot">3</div><div class="lbl">Ready to sign in</div></div>
139
- </div>
140
- <p class="status-line" id="status">Hang tight — keep this tab open while we finish.</p>
141
- <div class="cta hidden" id="cta"><a class="btn" id="go" href="https://${esc(domain)}">Open ${esc(domain)} →</a></div>
142
- <p class="muted hidden" id="back"><a href="/projects?view=new">Finish your remaining setup steps →</a></p>
143
- <p class="muted" id="foot">This page opens <code>${esc(domain)}</code> for you the moment it’s ready.</p>
144
- <noscript><p class="muted">When setup finishes, open <a href="https://${esc(domain)}">${esc(domain)}</a> and sign in with GitHub — that first sign-in makes you the owner.</p></noscript>
106
+ <p class="sub">Taking you back to your setup checklist — it finishes setting up <code>${esc(domain)}</code> there.</p>
107
+ <div class="cta"><a class="btn" id="back" href="/projects?view=new">Back to my checklist →</a></div>
145
108
  </main>
146
109
  <script>window.__CB_SETUP__=${cfgJson};</script>
147
- <script>${CALLBACK_SCRIPT}</script>
110
+ <script>${HANDBACK_SCRIPT}</script>
148
111
  </body></html>`;
149
112
  }
150
113
  return `${head}
@@ -1391,12 +1391,12 @@ module.exports = function provisioningRoutes() {
1391
1391
  });
1392
1392
 
1393
1393
  // GET /provisioning/github-app/status?state=<> — the PUBLIC progress read the onboarding
1394
- // callback page polls (task 2096) to know when setup is done + where to send the owner.
1394
+ // setup checklist polls (task 1004548) to know when setup is done + where to send the owner.
1395
1395
  // Same public-by-state model as the callback: the unguessable, single-use `state` token
1396
1396
  // is the only key, and the response reveals only the manifest lifecycle status + the
1397
1397
  // instance's own (already-public) domain — never a secret. Per-IP rate-limited because
1398
1398
  // the page polls it every ~3s. `ready` is true iff the manifest reached 'done' (creds
1399
- // placed + /healthz verified) — the page then auto-redirects to https://<domain>.
1399
+ // placed + /healthz verified) — the setup checklist then opens https://<domain>.
1400
1400
  // rank: public — no session, no mutation, no secret; an unknown state is a 404 no-op.
1401
1401
  router.get('/provisioning/github-app/status', statusRateLimit, async (req, res) => {
1402
1402
  try {
@@ -1461,11 +1461,11 @@ module.exports = function provisioningRoutes() {
1461
1461
  }).catch(() => {});
1462
1462
  const inst = await provisioning.getInstanceById(pool, row.instance_id).catch(() => null);
1463
1463
  const domain = inst && inst.domain ? inst.domain : null;
1464
- // Progress variant (task 2096): the page polls …/github-app/status?state= and
1465
- // auto-opens the instance the moment its credentials are placed — no "come back
1466
- // later" dead-end. Falls back to a static success if we have no domain to poll.
1464
+ // Hand-back variant (task 1004548): the page returns the founder to their setup
1465
+ // checklist, which shows the wait for the credentials and opens the instance when
1466
+ // it's ready. Falls back to a static success if we have no domain to point at.
1467
1467
  return sendPage(200, domain
1468
- ? callbackPage({ ok: true, title: 'Setting up your instance', state, domain })
1468
+ ? callbackPage({ ok: true, title: 'GitHub app created', state, domain })
1469
1469
  : callbackPage({ ok: true, title: 'GitHub app created',
1470
1470
  body: '<p>Nice — GitHub created the app. We’re placing its credentials on your instance now; this finishes on its own within a few minutes.</p><p class="muted">You can close this tab.</p>' }));
1471
1471
  } catch (err) {
@@ -423,6 +423,10 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
423
423
  .samePath{font-size:13px;font-weight:400;color:var(--ink-faint);margin-top:18px;}
424
424
  .samePath a{display:inline-block;padding:3px 0;min-height:24px;line-height:18px;}
425
425
  #liveProgress{margin-top:4px;}
426
+ /* the sign-in wait inside its step + the "open it when ready" tick (task 1004548) */
427
+ .humanSteps .prog{margin-top:14px;}
428
+ .autoOpen{display:flex;align-items:flex-start;gap:10px;margin-top:14px;font-size:14px;line-height:1.45;color:var(--ink-soft);cursor:pointer;min-height:24px;}
429
+ .autoOpen input{width:18px;height:18px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--accent);cursor:pointer;}
426
430
 
427
431
  /* ── the standup strip (task 2083), drawn as the lifecycle rail (DESIGN.md):
428
432
  three hairlines, a 7px dot, one lit step — no plate, no connector, no
@@ -6557,6 +6561,35 @@ summary{min-height:24px;padding:3px 0;}
6557
6561
  '<p>The platform team places the generated credentials; this step ticks itself once they land.</p></details>';
6558
6562
  }
6559
6563
 
6564
+ /* "Open it when it's ready" (task 1004548). The founder now waits for sign-in on
6565
+ this checklist instead of on the callback page, so the auto-open lives here: a
6566
+ tick, on unless they untick it (remembered per browser), that opens the project
6567
+ the moment it answers AND sign-in is wired. Armed only by a wait this page has
6568
+ actually watched, so revisiting a long-finished checklist never jumps away. */
6569
+ var AUTO_OPEN_KEY = 'cb-open-when-ready';
6570
+ var autoOpenArmed = false;
6571
+ var autoOpened = false;
6572
+ function autoOpenPref() {
6573
+ try { return localStorage.getItem(AUTO_OPEN_KEY) !== '0'; } catch (e) { return true; }
6574
+ }
6575
+ function autoOpenTick() {
6576
+ return '<label class="autoOpen"><input type="checkbox" data-action="auto-open"' + (autoOpenPref() ? ' checked' : '') + '>' +
6577
+ '<span>Open my project automatically when it’s ready</span></label>';
6578
+ }
6579
+ function maybeAutoOpen() {
6580
+ var domain = resolvedDomain();
6581
+ if (autoOpened || !autoOpenArmed || !domain || !autoOpenPref()) return;
6582
+ if (live.manifest !== 'done' || live.instStatus !== 'active' || !live.addrReady) return;
6583
+ autoOpened = true;
6584
+ var url = 'https://' + domain;
6585
+ /* a new tab keeps this checklist (two steps follow sign-in); a browser that
6586
+ blocks an unprompted tab gets this one sent instead — the founder asked */
6587
+ var w = null;
6588
+ try { w = window.open(url, '_blank'); } catch (e) {}
6589
+ if (w) { try { w.opener = null; } catch (e) {} }
6590
+ else window.location.assign(url);
6591
+ }
6592
+
6560
6593
  /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
6561
6594
  (gap 3 / idea 1000676); locked until a domain exists (the App needs the real
6562
6595
  callback host — the server refuses domainless with domain_required). */
@@ -6572,7 +6605,12 @@ summary{min-height:24px;padding:3px 0;}
6572
6605
  return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
6573
6606
  }
6574
6607
  if (live.manifest === 'done') {
6575
- return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.</p></li>';
6608
+ /* the wait ends HERE now, not on the callback page (task 1004548): once the
6609
+ project answers too, the step carries the door itself */
6610
+ var open = live.instStatus === 'active' && live.addrReady;
6611
+ return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.' +
6612
+ (open ? '' : ' The door below opens once your project is answering.') + '</p>' +
6613
+ (open ? '<a class="ghbtn" href="https://' + esc(domain) + '" target="_blank" rel="noopener">Open ' + esc(domain) + ' →</a>' : '') + '</li>';
6576
6614
  }
6577
6615
  if (live.manifest === 'error' || live.manifest === 'expired') {
6578
6616
  /* a plain sentence first; the raw route text folds under Technical
@@ -6589,10 +6627,14 @@ summary{min-height:24px;padding:3px 0;}
6589
6627
  if (live.manifest === 'code_received') {
6590
6628
  /* GitHub's half is done — say so, rather than keep telling the founder
6591
6629
  to approve an app they just approved */
6592
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub’s part is done. The platform is placing the credentials on your project now — this step ticks itself.</p></li>';
6630
+ autoOpenArmed = true;
6631
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub’s part is done. The platform is placing the credentials on your project now — this usually takes a few minutes, and this step ticks itself.</p>' +
6632
+ progressMarkup(1, 1, 'Placing credentials and checking sign-in works…', '', 'signin') + autoOpenTick() + '</li>';
6593
6633
  }
6594
6634
  if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
6635
+ autoOpenArmed = true;
6595
6636
  return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p>' +
6637
+ progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
6596
6638
  (manifestStale ? '<button type="button" class="linklike" data-action="manifest-start">Didn’t get to GitHub? Start the setup again</button>' : '') + '</li>';
6597
6639
  }
6598
6640
  return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in — one click</b><p>Approve a pre-filled GitHub App and the platform wires sign-in by itself: no keys to copy, and the secret never leaves the server side.</p>' +
@@ -6678,6 +6720,7 @@ summary{min-height:24px;padding:3px 0;}
6678
6720
  if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
6679
6721
  lastStepsHtml = html;
6680
6722
  $('humanSteps').innerHTML = html;
6723
+ maybeAutoOpen();
6681
6724
  });
6682
6725
  }
6683
6726
 
@@ -6730,6 +6773,12 @@ summary{min-height:24px;padding:3px 0;}
6730
6773
  });
6731
6774
  });
6732
6775
 
6776
+ $('humanSteps').addEventListener('change', function (ev) {
6777
+ var box = ev.target.closest && ev.target.closest('[data-action="auto-open"]');
6778
+ if (!box) return;
6779
+ try { localStorage.setItem(AUTO_OPEN_KEY, box.checked ? '1' : '0'); } catch (e) {}
6780
+ });
6781
+
6733
6782
  /* ── C8: the wizard's polls run only on stage, exactly like the sky.
6734
6783
  Off-view or hidden-tab, a due tick parks itself instead of fetching;
6735
6784
  enter() and the visibility flip resume whatever was parked. ── */
@@ -6754,6 +6803,13 @@ summary{min-height:24px;padding:3px 0;}
6754
6803
  var manifestPending = false;
6755
6804
  var manifestStartedAt = 0; /* when the founder was sent to GitHub */
6756
6805
  var manifestStale = false; /* ~90s with no approval → offer to start again */
6806
+ /* the GitHub hand-back (task 1004548): the callback page returns the founder here
6807
+ with the attempt's one-time state in the fragment (#ghstate=…), so a checklist
6808
+ that page opened — not only the tab that started the setup — keeps watching the
6809
+ same attempt. Read once at load, before the router's replaceState drops it. */
6810
+ var handbackState = (function () {
6811
+ try { var m = /[#&]ghstate=([0-9a-f]{16,128})/i.exec(location.hash || ''); return m ? m[1] : null; } catch (e) { return null; }
6812
+ })();
6757
6813
  /* the manifest leg survives a reload: token + last status ride the CREATED
6758
6814
  record, so enter() re-seeds the step instead of re-offering "Set up
6759
6815
  GitHub sign-in" for an app the founder already approved (task 1003232) */
@@ -6846,7 +6902,9 @@ summary{min-height:24px;padding:3px 0;}
6846
6902
  standup: { names: ['Request filed', 'Standing it up', 'Live'], cls: '' },
6847
6903
  revive: { names: ['Revive queued', 'Standing it up', 'Back live'], cls: '' },
6848
6904
  teardown: { names: ['Teardown queued', 'Winding down', 'Torn down'], cls: 'flowDown' },
6849
- settings: { names: ['Change saved', 'Restarting to apply it', 'Applied'], cls: '' }
6905
+ settings: { names: ['Change saved', 'Restarting to apply it', 'Applied'], cls: '' },
6906
+ /* the GitHub sign-in wait, drawn inside its checklist step (task 1004548) */
6907
+ signin: { names: ['Approve on GitHub', 'Placing credentials', 'Ready to sign in'], cls: '' }
6850
6908
  };
6851
6909
  /* No live-region attributes here: this strip is destroyed and rebuilt on
6852
6910
  every 5s tick, and a live region that is itself replaced never announces.
@@ -7171,6 +7229,13 @@ summary{min-height:24px;padding:3px 0;}
7171
7229
  /* restore the opt-out too — without it resolvedDomain() re-derives the
7172
7230
  assigned address and the done panel promises one that doesn't exist */
7173
7231
  state.noAddress = !!created.noaddr;
7232
+ /* a hand-back from the callback page names the attempt GitHub just approved */
7233
+ if (handbackState) {
7234
+ created.manifest_state = handbackState;
7235
+ if (created.manifest !== 'done') created.manifest = 'code_received';
7236
+ rememberManifest(handbackState, created.manifest);
7237
+ handbackState = null;
7238
+ }
7174
7239
  /* the manifest leg's last known truth, kept across a reload */
7175
7240
  if (created.manifest) { live.manifest = created.manifest; live.appSlug = created.app_slug || null; }
7176
7241
  if (created.manifest_state && created.manifest !== 'done' && created.manifest !== 'error' && created.manifest !== 'expired') {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.28",
3
+ "version": "1.21.30",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.28",
9
+ "version": "1.21.30",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.28",
3
+ "version": "1.21.30",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -8924,5 +8924,17 @@
8924
8924
  "id": "1004492",
8925
8925
  "text": "The Builders Hall now asks what you are doing when you arrive: Building, Artistry, Ideating or Governing, or Legacy mode for the hall exactly as it was. It asks once per session (or never again, if you untick the box), artists"
8926
8926
  }
8927
+ ],
8928
+ "1.21.29": [
8929
+ {
8930
+ "id": "1004548",
8931
+ "text": "After approving GitHub sign-in, founders go straight back to their setup checklist to wait, and can tick a box to have their project open automatically when it's ready."
8932
+ }
8933
+ ],
8934
+ "1.21.30": [
8935
+ {
8936
+ "id": "1004493",
8937
+ "text": "The hall's sidebar now follows the mode you pick. Building shows the everyday tabs, Artistry shows Studio, Goals and Your thinking, Ideating shows the Sky, Your thinking and Goals, and Governing is the shortest list: Blockers,"
8938
+ }
8927
8939
  ]
8928
8940
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.28'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.30'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,202 @@
1
+ // tests/craft_mode_nav.mjs — the hall nav follows the craft mode (task 1004493,
2
+ // goal 1000095 WA6).
3
+ //
4
+ // Each craft mode shows only the tabs its craft needs; Legacy mode and no mode
5
+ // show every tab, exactly as before. The owner's rule (2026-10-01): some tabs
6
+ // stay in EVERY mode, Builders first among them, written down as ONE shared list
7
+ // that each mode's list adds to. These cases boot the REAL shell-trail.js +
8
+ // craft-mode.js + shell.js and read the sidebar the shell actually rendered, so a
9
+ // tab id that drifts from NAV, or a gate pass that runs after the mode and
10
+ // re-shows a tab, reds here.
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import vm from 'node:vm';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+ const HALL = path.join(ROOT, 'modules/hall-ui/public');
20
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
21
+ const SHELL = read('shell.js');
22
+ const TRAIL = read('shell-trail.js');
23
+ const CRAFT = read('craft-mode.js');
24
+
25
+ // Every NAV id, and the Government group's, read from shell.js itself.
26
+ const NAV_IDS = [...SHELL.matchAll(/\{ id: '([a-z-]+)', label:/g)].map((m) => m[1]);
27
+ const GOV_BLOCK = SHELL.slice(SHELL.indexOf("{ group: 'Government'"));
28
+ const GOV_IDS = [...GOV_BLOCK.slice(0, GOV_BLOCK.indexOf('] }')).matchAll(/\{ id: '([a-z-]+)'/g)].map((m) => m[1]);
29
+
30
+ // A small element. Elements parsed from the sidebar's markup are built ONCE, so
31
+ // what the gate loop writes (hidden) is what the mode pass reads back.
32
+ function makeEl(tag) {
33
+ const attrs = {};
34
+ return {
35
+ tagName: tag, id: '', dataset: {}, style: {}, hidden: false, children: [], _html: '',
36
+ classList: { _s: new Set(), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, toggle(c, on) { if (on === undefined) on = !this._s.has(c); if (on) this._s.add(c); else this._s.delete(c); return on; }, contains(c) { return this._s.has(c); } },
37
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
38
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
39
+ addEventListener() {}, appendChild(c) { this.children.push(c); return c; }, removeChild() {}, insertBefore(c) { return c; },
40
+ focus() {}, closest() { return null; }, querySelector() { return null; }, querySelectorAll() { return []; },
41
+ };
42
+ }
43
+ // The rendered sidebar as groups of { head, items }.
44
+ function parseSidebar(html) {
45
+ return html.split('<div class="nav-group').slice(1).map((chunk) => {
46
+ const group = makeEl('DIV');
47
+ const tag = (m, t) => {
48
+ const el = makeEl(t);
49
+ for (const a of m.matchAll(/([a-z-]+)="([^"]*)"/g)) {
50
+ if (a[1] === 'id') el.id = a[2];
51
+ else if (a[1].startsWith('data-')) el.dataset[a[1].slice(5)] = a[2];
52
+ }
53
+ el.hidden = /\shidden[\s>]/.test(m);
54
+ return el;
55
+ };
56
+ const headM = chunk.match(/<button type="button" class="nav-group__head"[^>]*>/);
57
+ const head = headM ? tag(headM[0], 'BUTTON') : null;
58
+ const items = [...chunk.matchAll(/<a class="nav-item"[^>]*>/g)].map((m) => tag(m[0], 'A'));
59
+ group.querySelector = (sel) => (sel === '.nav-group__head' ? head : null);
60
+ group.querySelectorAll = (sel) => (sel === '.nav-item' ? items : []);
61
+ return { el: group, head, items };
62
+ });
63
+ }
64
+
65
+ function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
66
+ const root = makeEl('HTML');
67
+ const topbar = makeEl('DIV');
68
+ const sidebar = makeEl('NAV');
69
+ const body = makeEl('BODY');
70
+ body.dataset.page = 'work';
71
+ let groups = null;
72
+ const parsed = () => (groups ||= parseSidebar(sidebar._html));
73
+ Object.defineProperty(sidebar, 'innerHTML', { set(v) { sidebar._html = String(v); groups = null; }, get() { return sidebar._html; } });
74
+ sidebar.querySelectorAll = (sel) => {
75
+ if (sel === '.nav-item') return parsed().flatMap((g) => g.items);
76
+ if (sel === '.nav-group') return parsed().map((g) => g.el);
77
+ return [];
78
+ };
79
+ const gated = () => parsed().flatMap((g) => (g.head ? [g.head, ...g.items] : g.items));
80
+ const els = { 'app-topbar': topbar, 'app-sidebar': sidebar };
81
+ const documentObj = {
82
+ body, referrer: '', documentElement: root, activeElement: null,
83
+ getElementById: (id) => els[id] || null,
84
+ querySelector: () => null,
85
+ querySelectorAll: (sel) => (sel === '#app-sidebar [data-gate]' ? gated() : []),
86
+ createElement: (t) => makeEl(t.toUpperCase()),
87
+ addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
88
+ get cookie() { return ''; }, set cookie(_v) {},
89
+ };
90
+ const location = { hostname: 'h', origin: 'https://h', href: 'https://h/work', pathname: '/work', hash: '', search: '', assign() {}, replace() {}, reload() {} };
91
+ const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
92
+ const windowObj = {
93
+ __BRANDING__: {}, __MODULES__: { enabled: { provisioning: true } },
94
+ BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
95
+ matchMedia: () => ({ matches: false, addEventListener() {} }),
96
+ localStorage: mk({}), sessionStorage: mk(session), location,
97
+ clearTimeout() {}, setTimeout() { return 0; }, requestAnimationFrame() {}, scrollTo() {},
98
+ addEventListener() {}, dispatchEvent() { return true; },
99
+ };
100
+ const sandbox = {
101
+ window: windowObj, document: documentObj, location, console, URL, URLSearchParams, queueMicrotask,
102
+ setTimeout() { return 0; }, clearTimeout() {},
103
+ Event: class { constructor(t) { this.type = t; } },
104
+ CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } },
105
+ };
106
+ sandbox.globalThis = sandbox;
107
+ vm.createContext(sandbox);
108
+ vm.runInContext(TRAIL, sandbox, { filename: 'shell-trail.js' });
109
+ vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
110
+ vm.runInContext(SHELL, sandbox, { filename: 'shell.js' });
111
+ const shell = windowObj.OTBShell;
112
+ const craft = windowObj.OTBCraftMode;
113
+ const byId = (id) => parsed().flatMap((g) => g.items).find((a) => a.id === 'nav-' + id);
114
+ return {
115
+ craft, root,
116
+ signIn: (rank = 'archon') => shell.applyAccess({ authed: true, rank, permissions: perms, builder: { id: 7, rank } }),
117
+ // The tabs a reader can see: the gate let them through AND the mode kept them.
118
+ showing: () => parsed().flatMap((g) => g.items).filter((a) => !a.hidden && !a.hasAttribute('data-craft-off')).map((a) => a.id.slice(4)),
119
+ headShowing: (group) => {
120
+ const g = parsed().find((x) => x.head && x.head.dataset.group === group);
121
+ return !!g && !g.head.hidden && !g.head.hasAttribute('data-craft-off');
122
+ },
123
+ byId,
124
+ };
125
+ }
126
+
127
+ const CRAFTS = ['building', 'artistry', 'ideating', 'governing'];
128
+
129
+ test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, inside every mode', () => {
130
+ const { craft } = boot();
131
+ assert.equal(craft.ALWAYS_TABS[0], 'roster', 'Builders (NAV id roster) heads the list (owner, 2026-10-01)');
132
+ for (const m of CRAFTS) {
133
+ const tabs = craft.tabsFor(m);
134
+ assert.ok(Array.isArray(tabs), `${m} has a named tab list`);
135
+ for (const id of craft.ALWAYS_TABS) assert.ok(tabs.includes(id), `${m} keeps ${id}`);
136
+ }
137
+ assert.match(CRAFT, /return own \? ALWAYS_TABS\.concat\(own\) : null;/, 'each list is ALWAYS_TABS plus its own, not a copy of it');
138
+ });
139
+
140
+ test('every tab a mode names is a real NAV item, and every NAV item lives in some mode', () => {
141
+ const { craft } = boot();
142
+ assert.ok(NAV_IDS.length >= 25, `read ${NAV_IDS.length} NAV ids from shell.js`);
143
+ const named = new Set(CRAFTS.flatMap((m) => craft.tabsFor(m)));
144
+ for (const id of named) assert.ok(NAV_IDS.includes(id), `${id} is a NAV id`);
145
+ for (const id of NAV_IDS) assert.ok(named.has(id), `${id} is reachable from some mode's nav`);
146
+ });
147
+
148
+ test('Government shows only in Governing, and Governing is the shortest list', () => {
149
+ const { craft } = boot();
150
+ assert.ok(GOV_IDS.length >= 5, `read ${GOV_IDS.length} Government ids`);
151
+ for (const m of CRAFTS) {
152
+ for (const id of GOV_IDS) assert.equal(craft.tabsFor(m).includes(id), m === 'governing', `${id} in ${m}`);
153
+ }
154
+ const lengths = CRAFTS.map((m) => craft.tabsFor(m).filter((id) => !GOV_IDS.includes(id)).length);
155
+ assert.equal(Math.min(...lengths), lengths[CRAFTS.indexOf('governing')], 'outside its own Government group, Governing shows the fewest tabs');
156
+ });
157
+
158
+ test('NO MODE AND LEGACY ARE TODAY\'S NAV: every tab the gate allows, nothing marked', () => {
159
+ const none = boot();
160
+ none.signIn();
161
+ const all = none.showing();
162
+ assert.deepEqual(all, NAV_IDS, 'an archon with no mode sees every tab');
163
+ const legacy = boot({ session: { 'hall-craft': 'legacy' } });
164
+ legacy.signIn();
165
+ assert.deepEqual(legacy.showing(), all, 'Legacy mode shows exactly the same');
166
+ assert.equal(legacy.craft.tabsFor('legacy'), null);
167
+ });
168
+
169
+ test('each mode shows its own tabs, and switching updates the nav live', () => {
170
+ const p = boot();
171
+ p.signIn();
172
+ for (const m of CRAFTS) {
173
+ p.craft.choose(m);
174
+ assert.equal(p.root.dataset.craft, m);
175
+ assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor(m)), `${m} shows exactly its list`);
176
+ assert.ok(p.showing().includes('roster'), `Builders stays in ${m}`);
177
+ assert.equal(p.headShowing('government'), m === 'governing', `the Government head shows only where its tabs do (${m})`);
178
+ }
179
+ p.craft.choose('legacy');
180
+ assert.deepEqual(p.showing(), NAV_IDS, 'back to Legacy, every tab returns without a reload');
181
+ });
182
+
183
+ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mode', () => {
184
+ const p = boot({ session: { 'hall-craft': 'governing' } });
185
+ p.signIn('thetes');
186
+ assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
187
+ assert.ok(!p.headShowing('government'), 'nor its group head');
188
+ assert.deepEqual(p.showing(), ['blockers', 'board-room', 'roster'], 'what a thetes board member in Governing sees');
189
+ p.signIn('archon');
190
+ assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
191
+ assert.ok(p.headShowing('government'), 'and its group head comes back with it, which only a mode pass after the gate can know');
192
+ assert.ok(!p.showing().includes('work'), 'and the gate pass did not bring back a tab the mode left out');
193
+ });
194
+
195
+ test('shell.js applies the mode AFTER its gate loop, and style.css hides what the mode leaves out', () => {
196
+ const body = SHELL.slice(SHELL.indexOf('function applyAccess('));
197
+ const gate = body.indexOf('el.hidden = !ok;');
198
+ const mode = body.indexOf('CRAFT.applyNav()');
199
+ assert.ok(gate > 0 && mode > gate, 'applyNav runs after the gate loop settles `hidden`');
200
+ assert.match(read('style.css'), /#app-sidebar \[data-craft-off\] \{ display: none !important; \}/);
201
+ assert.doesNotMatch(CRAFT, /\.hidden\s*=/, 'the mode never writes `hidden`, which the gate owns');
202
+ });
@@ -635,7 +635,7 @@ try {
635
635
  assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
636
636
  assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
637
637
  assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
638
- assert.match(HTML, /shell\.js\?v=2026-10-02-craft/);
638
+ assert.match(HTML, /shell\.js\?v=2026-10-03-nav/);
639
639
  assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
640
640
  for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
641
641
  assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = '8932abeda77f250a';
90
+ const SHELL_SHA = 'af1c61ca87767b44';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);