@bongos/core 1.24.91 → 1.24.93

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 (36) hide show
  1. package/.bongos-core.json +54 -39
  2. package/.claude/skills/builder-reauth/SKILL.md +23 -25
  3. package/clients/bongos-client/index.d.ts +2 -1
  4. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +8 -0
  5. package/docs/api/openapi.json +22 -2
  6. package/docs/api-reference.md +1 -1
  7. package/docs/copy-inventory.md +3 -3
  8. package/docs/copy-registry.json +3 -3
  9. package/docs/module-api-changelog.md +4 -2
  10. package/docs/page-inventory.json +4 -2
  11. package/docs/page-readings.json +6 -6
  12. package/modules/hall-ui/public/city.css +6 -2
  13. package/modules/hall-ui/public/city.js +44 -2
  14. package/modules/hall-ui/public/goal-map.html +2 -0
  15. package/modules/hall-ui/public/sky-interaction.js +4 -9
  16. package/modules/hall-ui/public/sky-visor.js +96 -0
  17. package/modules/hall-ui/public/thinking.css +8 -0
  18. package/modules/hall-ui/public/thinking.html +2 -0
  19. package/modules/hall-ui/public/visor.js +20 -4
  20. package/modules/hall-ui/records/visor.md +16 -2
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/release-notes.json +15 -1
  24. package/scripts/bongos/cli-lib.js +7 -6
  25. package/scripts/bongos/login.js +14 -3
  26. package/scripts/bongos/plain-cards.js +1 -1
  27. package/scripts/bongos/reauth.js +100 -50
  28. package/src/bongos/routes/auth-google.js +5 -3
  29. package/src/bongos/routes/auth.js +3 -3
  30. package/src/bongos/sign-in-pages.js +1 -2
  31. package/src/module-api.js +1 -1
  32. package/tests/cli_reauth_browser.mjs +157 -0
  33. package/tests/hall_city.mjs +41 -1
  34. package/tests/hall_city_living.mjs +2 -0
  35. package/tests/hall_sky_visor.mjs +150 -0
  36. package/tests/hall_visor.mjs +28 -2
@@ -283,7 +283,7 @@
283
283
  "surface": "builders",
284
284
  "title": "The city",
285
285
  "states_read": ["city","fullscreen","focus","pins","living","card","card-ask"],
286
- "files_hash": "43c2a3f5444895d6",
286
+ "files_hash": "d9dd949934a5b719",
287
287
  "reading_hash": "277096d903879a57",
288
288
  "counts": {"lines":26,"placed":9,"shared":6,"unplaced":11},
289
289
  "omitted": {"data":7},
@@ -613,7 +613,7 @@
613
613
  "surface": "builders",
614
614
  "title": "The sky",
615
615
  "states_read": ["tour","sky","wells","ledger","full","full-back","out"],
616
- "files_hash": "ca4e9b0498d510b8",
616
+ "files_hash": "f54ebe767edaf0c8",
617
617
  "reading_hash": "4a879397a84631e3",
618
618
  "counts": {"lines":50,"placed":25,"shared":6,"unplaced":19},
619
619
  "omitted": {"data":10},
@@ -954,8 +954,8 @@
954
954
  "surface": "builders",
955
955
  "title": "Builders' Hall",
956
956
  "states_read": ["home","out","founding","genesis","decide","demo"],
957
- "files_hash": "0091ac704ef9996f",
958
- "reading_hash": "67b29854aad1bffe",
957
+ "files_hash": "6fce98a5eabe1c82",
958
+ "reading_hash": "42901560779fc4d1",
959
959
  "counts": {"lines":32,"placed":12,"shared":8,"unplaced":12},
960
960
  "omitted": {"data":5},
961
961
  "notes": ["founding: an action failed (page.focus: Timeout 6000ms exceeded.); read as it stood"],
@@ -977,7 +977,7 @@
977
977
  {"key":"L0015","section":"Needs your attention13","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":320,"string_id":"e181637c5a5e"},
978
978
  {"key":"L0016","section":"Needs your attention13","text":"Requires project {…} fix","placement":"unplaced"},
979
979
  {"key":"L0017","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":514,"string_id":"1883ad7f07fc"},
980
- {"key":"L0018","section":"Needs your attention13","text":"core 1.24.89","placement":"unplaced"},
980
+ {"key":"L0018","section":"Needs your attention13","text":"core 1.24.90","placement":"unplaced"},
981
981
  {"key":"L0019","section":"Needs your attention13","text":"Skip","placement":"unplaced"},
982
982
  {"key":"L0020","section":"Needs your attention13","text":"Built on","placement":"placed","file":"modules/hall-ui/public/welcome-boot.js","line":183,"string_id":"fef59346ffee"},
983
983
  {"key":"L0021","section":"Needs your attention13","text":"cloudbongos.com","placement":"unplaced"},
@@ -3159,7 +3159,7 @@
3159
3159
  "surface": "builders",
3160
3160
  "title": "Your thinking",
3161
3161
  "states_read": ["tour","sky","list","simple","full-idea","full","full-list","full-back","out"],
3162
- "files_hash": "3e3d0ce3f938dfc4",
3162
+ "files_hash": "3cce7079079c7782",
3163
3163
  "reading_hash": "b73f848b46b0f9b8",
3164
3164
  "counts": {"lines":50,"placed":25,"shared":6,"unplaced":19},
3165
3165
  "omitted": {"data":10},
@@ -88,8 +88,12 @@ html:has(body[data-page="city"]) { height: 100%; }
88
88
  the home above and below measures from the stage instead of the window. */
89
89
  .city-main { position: fixed; top: var(--topbar-h, 56px); left: var(--sidebar-w-rail, 64px); right: 0; bottom: 0; max-width: none; margin: 0; contain: layout paint; z-index: 1; }
90
90
  html[data-fullscreen="on"] .city-main { top: 0; left: 0; }
91
- /* tasks 1005148 and 1005195: with the visor up (game mode), the door rises clear of its foot row, whose one button is centred too. */
92
- html.visor-on .city-door { bottom: 150px; }
91
+ /* tasks 1005148, 1005195 and 1005209: with the visor up (game mode), the door (now its line alone) rises clear of the
92
+ foot row, whose one button is centred too, even when the key hints wrap onto a second line. */
93
+ html.visor-on .city-door { bottom: 224px; }
94
+ /* task 1005209 (ADR 0403): with the visor up the door's button and the archived links are the
95
+ visor's (D on the glass, the rest in its Actions sheet), so the scene carries none of its own. */
96
+ html.visor-on .city-start, html.visor-on .city-archived { display: none; }
93
97
  @media (max-width: 768px) { .city-main { left: 0; } }
94
98
  /* The full-screen switch floats top right over the stage, where the sun/moon
95
99
  switch sits; the strip steps in by the button's width so nothing sits under it.
@@ -411,6 +411,7 @@
411
411
  start.setAttribute('href', model.cta.href);
412
412
  start.setAttribute('aria-label', model.cta.aria);
413
413
  }
414
+ if (model.cta) registerScene(model.cta);
414
415
  drawPlaque(model.plaque || null);
415
416
  if (openKey) renderCard(openKey);
416
417
  return model;
@@ -759,6 +760,47 @@
759
760
  window.GovCityPage.ready = boot();
760
761
  // task 1005148: the Governor City as the visor's scene. No camera until R13, so no movement
761
762
  // actions (ADR 0399 §2.2); the card's own keys are listed for the sheet.
762
- window.__visorScene = { name: 'city', place: 'Governor city', keys: [{ label: 'On a card: answer', key: '1–9' }, { label: 'On a card: next', key: '← →' }] };
763
- if (window.OTBVisor) window.OTBVisor.scene(window.__visorScene);
763
+ // task 1005209 (ADR 0403): in game mode the city's door and its two archived links are the
764
+ // visor's. The door is the lead action, on the glass as a key hint with D; the archived
765
+ // links are in the sheet's "This place" list. city.css takes them off the scene under
766
+ // html.visor-on; in classic mode they stay where they were and these do nothing new.
767
+ function doorLabel(cta) { return cta && cta.label ? String(cta.label) : 'start governing'; }
768
+ function sceneFor(cta) {
769
+ var label = doorLabel(cta);
770
+ return {
771
+ name: 'city', place: 'Governor city',
772
+ actions: [
773
+ { label: label, key: 'd', bind: true, lead: true, run: function () {
774
+ if (window.GovCityDecide && window.GovDeck) openDecide(null);
775
+ else if (window.location) window.location.hash = '#decide';
776
+ } },
777
+ ].concat(archivedActions()),
778
+ keys: [{ label: 'On a card: answer', key: '1–9' }, { label: 'On a card: next', key: '← →' }],
779
+ };
780
+ }
781
+ // The archived links, read off the page's own line (city.html), so the sheet says and opens
782
+ // exactly what the line does: "Archived: Street", "Archived: Building Level Views".
783
+ function archivedActions() {
784
+ var line = document.querySelector ? document.querySelector('.city-archived') : null;
785
+ var links = line && line.querySelectorAll ? Array.prototype.slice.call(line.querySelectorAll('a[href]')) : [];
786
+ return links.map(function (a) {
787
+ var href = a.getAttribute('href');
788
+ return { label: 'Archived: ' + a.textContent, run: function () { window.location.assign(href); } };
789
+ });
790
+ }
791
+ // Registered once per door label: draw() calls this on every poll, and only a new label
792
+ // (a different streak, say) rebuilds the scene and reads the archived links again.
793
+ // Order matters here: boot() ran above and its first draw() has normally registered
794
+ // already, with the real door label. sceneLabel deliberately has no initial value, so the
795
+ // line after this function only registers the fallback when that draw did not register.
796
+ var sceneLabel;
797
+ function registerScene(cta) {
798
+ var label = doorLabel(cta);
799
+ if (label === sceneLabel) return;
800
+ sceneLabel = label;
801
+ var next = sceneFor(cta);
802
+ window.__visorScene = next;
803
+ if (window.OTBVisor) window.OTBVisor.scene(next);
804
+ }
805
+ if (sceneLabel == null) registerScene(null);
764
806
  }());
@@ -158,5 +158,7 @@
158
158
  <script src="/builders/sky-panel.js?v=2026-09-30-uat1"></script>
159
159
  <script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
160
160
  <script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
161
+ <!-- task 1005210: the sky as the visor's scene (ADR 0403), after every other sky file -->
162
+ <script src="/builders/sky-visor.js"></script>
161
163
  </body>
162
164
  </html>
@@ -13,7 +13,7 @@
13
13
  // `filed_via` exists on at least one thought; until then it is hidden and
14
14
  // every thought shows (the README's "must not ship" row).
15
15
  //
16
- // Keys: L the list, ? the tour, 0 fit, / the composer, Escape closes, arrows
16
+ // Keys: L the list, ? the tour, 0 fit, / the composer (J jumps, from the visor in game mode), Escape closes, arrows
17
17
  // pan, + and - zoom.
18
18
 
19
19
  (() => {
@@ -75,11 +75,11 @@
75
75
  // An Escape the sky USED (the tour, the menu, a pick) calls preventDefault, so
76
76
  // the shell's full-screen switch leaves that press alone (task 1004327).
77
77
  addEventListener('keydown', (e) => {
78
- if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) { if (e.key === 'Escape') e.target.blur(); return; }
78
+ if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) { if (e.key === 'Escape') { e.target.blur(); if (e.target.id === 'thought' || e.target.id === 'jump') { e.preventDefault(); if (SKY.closeField) SKY.closeField(e.target.id, true); } } return; }
79
79
  const { cam } = S; const step = 60 / cam.k;
80
80
  if (e.key === 'ArrowLeft') { cam.x -= step; S.dirty = true; } else if (e.key === 'ArrowRight') { cam.x += step; S.dirty = true; } else if (e.key === 'ArrowUp') { cam.y -= step; S.dirty = true; } else if (e.key === 'ArrowDown') { cam.y += step; S.dirty = true; }
81
81
  else if (e.key === '+' || e.key === '=') { cam.k = clamp(cam.k * 1.3, .015, 9); S.dirty = true; } else if (e.key === '-') { cam.k = clamp(cam.k / 1.3, .015, 9); S.dirty = true; }
82
- else if (e.key === '0') SKY.fitAll(); else if (e.key === 'Escape') { if (!$('tour').hidden) { e.preventDefault(); SKY.endTour(); } else if (!menu.hidden) { e.preventDefault(); hideMenu(); } else { if (S.picked || !$('panel').hidden) e.preventDefault(); pick(null); } } else if (e.key === '/') { e.preventDefault(); $('thought').focus(); }
82
+ else if (e.key === '0') SKY.fitAll(); else if (e.key === 'Escape') { if (!$('tour').hidden) { e.preventDefault(); SKY.endTour(); } else if (!menu.hidden) { e.preventDefault(); hideMenu(); } else { if (S.picked || !$('panel').hidden) e.preventDefault(); pick(null); } } else if (e.key === '/') { e.preventDefault(); if (SKY.openField) SKY.openField('thought'); else $('thought').focus(); }
83
83
  else if (e.key === '?') SKY.startTour(); else if (e.key.toLowerCase() === 'l') SKY.toggleRoom(); else return;
84
84
  });
85
85
  function pick(h, fly) { if (h && h.kind === 'ltask') { SKY.renderPanel({ kind: 'ltask', ref: h.ref }); S.dirty = true; return; } S.picked = h; SKY.renderPanel(S.picked); SKY.renderRoom(); S.dirty = true; if (fly && S.picked && S.lens !== 'simple') { if (S.picked.kind === 'goal') SKY.fitGoal(S.picked.ref); else if (S.picked.kind === 'idea') SKY.fitThought(S.picked.ref); } }
@@ -148,10 +148,5 @@
148
148
 
149
149
  Object.assign(SKY, { card, menu, glyphHtml, ideaTitle, sizeWords, showCard, showMenu, hideMenu, pick, setLens, togglePill, updateStats, altText, TAU, hash });
150
150
 
151
- // task 1005148: the sky as the visor's scene (ADR 0399 §2.2). The visor owns no camera:
152
- // "Back to centre" is the camera's own fitAll, and the keys listed are the ones bound above.
153
- window.__visorScene = { name: 'sky', place: '', // the sky's own heading names the place
154
- actions: [{ label: 'Back to centre', key: '0', run: () => SKY.fitAll() }],
155
- keys: [{ label: 'Look around', key: 'drag' }, { label: 'List view', key: 'L' }, { label: 'Search', key: '/' }, { label: 'Take the tour', key: '?' }] };
156
- if (window.OTBVisor) window.OTBVisor.scene(window.__visorScene);
151
+ // task 1005148 / 1005210: the sky as the visor's scene lives in sky-visor.js, after this file.
157
152
  })();
@@ -0,0 +1,96 @@
1
+ // modules/hall-ui/public/sky-visor.js — THE SKY as the visor's scene (task 1005148; task
2
+ // 1005210, ADR 0403). Loaded after sky-interaction.js on /goal-map and /thinking.
3
+ //
4
+ // The visor owns no camera (ADR 0399 §2.2): every action here presses one of the sky's own
5
+ // controls or opens one of its own fields, so a pill does exactly what it did in classic mode.
6
+ (() => {
7
+ 'use strict';
8
+
9
+ const SKY = window.OTBSky; const S = SKY.S;
10
+ const { $, on } = SKY;
11
+
12
+ // task 1005210 (ADR 0403): in game mode the visor is the control and the sky is the view. Its
13
+ // top row (the lens switch and every pill), the jump field and the thought composer leave the
14
+ // scene (thinking.css, html.visor-on) and come back as visor actions: writing a thought is the
15
+ // lead action, on the sky's own "/"; jumping is J; each pill is a row in the Actions sheet
16
+ // that presses the pill itself, so a pill does exactly what it did. The rows are read off the
17
+ // page's own buttons (their words, their pressed state, whether this lens shows them), and the
18
+ // scene registers again whenever one of those changes. Panning, zooming and picking a star
19
+ // stay in the sky. In classic mode nothing here shows or moves.
20
+ const FIELDS = ['thought', 'jump'];
21
+ function fieldWrap(id) { const el = $(id); return el && el.closest ? el.closest('.composer, .jump') : null; }
22
+ function openField(id) {
23
+ for (const other of FIELDS) if (other !== id) closeField(other, true);
24
+ const wrap = fieldWrap(id); if (wrap) wrap.classList.add('is-open');
25
+ $(id).focus();
26
+ }
27
+ // A field closes when it is left with nothing in it, or on Escape (its words are kept).
28
+ // The composer stays while a full idea is being asked or a receipt waits to be read.
29
+ function closeField(id, force) {
30
+ const wrap = fieldWrap(id); if (!wrap || !wrap.classList.contains('is-open')) return;
31
+ if (!force) {
32
+ if (wrap.contains(document.activeElement)) return;
33
+ if ($(id).value.trim()) return;
34
+ if (id === 'thought' && (!$('fullSteps').hidden || !$('receipt').hidden)) return;
35
+ }
36
+ wrap.classList.remove('is-open');
37
+ }
38
+ for (const id of FIELDS) {
39
+ const wrap = fieldWrap(id); if (!wrap) continue;
40
+ wrap.addEventListener('focusout', () => setTimeout(() => closeField(id), 0));
41
+ }
42
+ on('receiptX', 'click', () => setTimeout(() => closeField('thought'), 0));
43
+ // the sky binds these keys itself (sky-interaction.js); the rest are rows in the sheet
44
+ const OWN_KEYS = { roomBtn: 'L', tourBtn: '?', fitBtn: '0' };
45
+ function visible(el) { return !!el && !el.hidden && !(el.closest && el.closest('[hidden]')); }
46
+ function rowFor(b) {
47
+ const words = (b.textContent || '').trim();
48
+ const name = words.length > 1 ? words : (b.getAttribute('aria-label') || b.getAttribute('title') || '');
49
+ const pressed = b.getAttribute('aria-pressed');
50
+ const label = name.charAt(0).toUpperCase() + name.slice(1) + (pressed == null ? '' : pressed === 'true' ? ' · on' : ' · off');
51
+ return { label, key: OWN_KEYS[b.id] || '', run: () => b.click() };
52
+ }
53
+ // The foot's clock and legend are the sky's too: in game mode each shows only while the visor
54
+ // has it open (thinking.css), so the scene carries none of their buttons until asked.
55
+ function footOpen(id) { const el = $(id); return !!el && el.classList.contains('is-open'); }
56
+ function footRow(id, name) {
57
+ return { label: name + (footOpen(id) ? ' · on' : ' · off'), key: '', run: () => {
58
+ const el = $(id); if (!el) return;
59
+ const open = !el.classList.contains('is-open');
60
+ el.classList.toggle('is-open', open);
61
+ // a clock classic mode was told to hide comes back through its own pill
62
+ if (open && el.hidden && id === 'time') $('clockBtn').click();
63
+ } };
64
+ }
65
+ function skyScene() {
66
+ const actions = [{ label: 'Write a thought', key: '/', lead: true, run: () => openField('thought') }];
67
+ if (visible(fieldWrap('jump'))) actions.push({ label: 'Jump to a goal or a thought', key: 'j', bind: true, run: () => openField('jump') });
68
+ document.querySelectorAll('#lensGroup button, #tools button').forEach((b) => {
69
+ if (!visible(b)) return;
70
+ // the clock pill keeps classic mode's remembered choice; in game mode the clock opens from here
71
+ if (b.id === 'clockBtn') { actions.push(footRow('time', 'Clock')); return; }
72
+ actions.push(rowFor(b));
73
+ });
74
+ if (visible($('legend'))) actions.push(footRow('legend', 'Legend'));
75
+ return { name: 'sky', place: '', // the sky's own heading names the place
76
+ actions, keys: [{ label: 'Look around', key: 'drag' }, { label: 'Zoom', key: '+ −' }] };
77
+ }
78
+ let sceneSig = null;
79
+ function registerScene() {
80
+ const next = skyScene();
81
+ const sig = next.actions.map((a) => a.label + '|' + a.key).join('\n');
82
+ if (sig === sceneSig) return;
83
+ sceneSig = sig;
84
+ window.__visorScene = next;
85
+ if (window.OTBVisor) window.OTBVisor.scene(next);
86
+ }
87
+ // a pill's pressed state or a lens's set of pills changed: the sheet follows
88
+ if (typeof MutationObserver === 'function') {
89
+ const watch = new MutationObserver(registerScene);
90
+ for (const id of ['tools', 'lensGroup']) { const el = $(id); if (el) watch.observe(el, { subtree: true, attributes: true, attributeFilter: ['aria-pressed', 'hidden'] }); }
91
+ const jump = fieldWrap('jump'); if (jump) watch.observe(jump, { attributes: true, attributeFilter: ['hidden'] });
92
+ for (const id of ['time', 'legend']) { const el = $(id); if (el) watch.observe(el, { attributes: true, attributeFilter: ['class', 'hidden'] }); }
93
+ }
94
+ Object.assign(SKY, { openField, closeField, skyScene });
95
+ registerScene();
96
+ })();
@@ -315,3 +315,11 @@ html.visor-on .composer, html.visor-on .jump { top: calc(140px + env(safe-area-i
315
315
  html.visor-on .panel, html.visor-on .room { top: calc(204px + env(safe-area-inset-top, 0px)); bottom: calc(200px + env(safe-area-inset-bottom, 0px)); }
316
316
  html.visor-on .foot { padding-bottom: calc(132px + env(safe-area-inset-bottom, 0px)); }
317
317
  html.visor-on .tour, html.visor-on .start { bottom: calc(210px + env(safe-area-inset-bottom, 0px)); }
318
+ /* task 1005210 (ADR 0403): with the visor up the sky carries no control of its own. The top
319
+ row's lens switch and pills are rows in the visor's Actions sheet; the composer and the jump
320
+ field open from the visor ("Write a thought" on /, "Jump" on J) and close again when left
321
+ empty (sky-interaction.js openField / closeField). The brand and its figures stay, as words. */
322
+ html.visor-on .top .tools, html.visor-on .top .lens { display: none; }
323
+ html.visor-on .composer:not(.is-open), html.visor-on .jump:not(.is-open) { display: none; }
324
+ html.visor-on .jump.is-open:not([hidden]) { display: block; }
325
+ html.visor-on .time:not(.is-open), html.visor-on .legend:not(.is-open) { display: none; }
@@ -159,5 +159,7 @@
159
159
  <script src="/builders/sky-panel.js?v=2026-09-30-uat1"></script>
160
160
  <script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
161
161
  <script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
162
+ <!-- task 1005210: the sky as the visor's scene (ADR 0403), after every other sky file -->
163
+ <script src="/builders/sky-visor.js"></script>
162
164
  </body>
163
165
  </html>
@@ -133,12 +133,18 @@
133
133
  var scene = null;
134
134
  // A scene says what it is and, if it has a camera, the actions that move it (ADR 0399
135
135
  // §2.2). `keys` are lines for the sheet's "this place" list: keys the scene already binds.
136
+ // An action's `key` is a key the scene already binds, shown beside it; with `bind: true`
137
+ // the visor binds it instead (in game mode, after the character's keys), for a scene button
138
+ // that had no key of its own. `lead: true` puts the action on the glass as the first key
139
+ // hint (ADR 0403: in game mode the scene's buttons are the visor's; task 1005209).
136
140
  function registerScene(s) {
137
141
  if (!s || typeof s !== 'object') return;
138
142
  scene = {
139
143
  name: String(s.name || ''),
140
144
  place: String(s.place || ''),
141
- actions: (Array.isArray(s.actions) ? s.actions : []).filter(function (a) { return a && a.label && typeof a.run === 'function'; }),
145
+ actions: (Array.isArray(s.actions) ? s.actions : []).filter(function (a) { return a && a.label && typeof a.run === 'function'; }).map(function (a) {
146
+ return { label: String(a.label), key: a.key ? String(a.key).toLowerCase() : '', bind: !!a.bind, lead: !!a.lead, run: a.run };
147
+ }),
142
148
  keys: (Array.isArray(s.keys) ? s.keys : []).filter(function (k) { return k && k.label && k.key; }),
143
149
  };
144
150
  render();
@@ -174,6 +180,10 @@
174
180
  function hintHtml(a) {
175
181
  return '<a class="visor__hint" data-visor-act="' + esc(a.id) + '" ' + actionAttrs(a) + '>' + keyHtml(a.key) + '<span>' + esc(a.label) + '</span></a>';
176
182
  }
183
+ // The scene's lead action, as the first quiet hint (task 1005209).
184
+ function placeHintHtml(a, i) {
185
+ return '<button class="visor__hint visor__hint--place" type="button" data-visor-scene="' + i + '">' + (a.key ? keyHtml(a.key) : '') + '<span>' + esc(a.label) + '</span></button>';
186
+ }
177
187
  function actionAttrs(a) {
178
188
  return a.href ? 'href="' + esc(hallHref(a.href)) + '"' : 'href="#" role="button"';
179
189
  }
@@ -192,7 +202,7 @@
192
202
  '</div>' +
193
203
  '<a class="visor__main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
194
204
  '<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>' +
195
- '<div class="visor__hints">' + acts.slice(1).map(hintHtml).join('') +
205
+ '<div class="visor__hints">' + scene.actions.map(function (a, i) { return a.lead ? placeHintHtml(a, i) : ''; }).join('') + acts.slice(1).map(hintHtml).join('') +
196
206
  '<button class="visor__hint visor__hint--sheet" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
197
207
  '<span class="visor__dots" aria-hidden="true"></span>' + keyHtml(SHEET_KEY) + '<span>Actions</span></button>' +
198
208
  '<a class="visor__hint visor__hint--options" href="' + esc(hallHref('/settings')) + '">' + keyHtml('Esc') + '<span>Options</span></a>' +
@@ -264,7 +274,8 @@
264
274
  sheet.setAttribute('role', 'dialog');
265
275
  sheet.setAttribute('aria-labelledby', 'visor-sheet-title');
266
276
  sheet.innerHTML = sheetHtml(id);
267
- sheet.addEventListener('click', onClick);
277
+ // no listener of its own: the sheet sits inside #visor, whose click listener already
278
+ // sees its rows (a second one ran every row twice, so a toggle undid itself; task 1005210)
268
279
  root.appendChild(sheet);
269
280
  var btn = root.querySelector('[data-visor-sheet]');
270
281
  if (btn) { btn.setAttribute('aria-expanded', 'true'); btn.hidden = true; }
@@ -297,6 +308,7 @@
297
308
  // ---------- running an action ----------
298
309
  function run(a) {
299
310
  if (!a) return;
311
+ if (typeof a.run === 'function') { a.run(); return; }
300
312
  if (a.tool) {
301
313
  var C = craft();
302
314
  if (C && typeof C.openTool === 'function') C.openTool(a.tool);
@@ -342,11 +354,15 @@
342
354
  if (k === SHEET_KEY) return 'sheet';
343
355
  var acts = actionsFor(id);
344
356
  for (var i = 0; i < acts.length; i++) if (acts[i].key === k) return acts[i];
357
+ // then the keys the visor binds for the place (task 1005209); the character's win a clash
358
+ var own = scene ? scene.actions : [];
359
+ for (var j = 0; j < own.length; j++) if (own[j].bind && own[j].key === k) return own[j];
345
360
  return null;
346
361
  }
347
362
  // Bubble, on window: a scene or pop-up that used the key has called preventDefault by now.
348
363
  function onKey(e) {
349
- if (!shown() || inField(e.target) || busy()) return;
364
+ // The glass is game mode's alone (visor.css); outside it, its keys are not bound either.
365
+ if (!shown() || document.documentElement.dataset.game !== 'on' || inField(e.target) || busy()) return;
350
366
  var hit = keyFor(character(), e);
351
367
  if (!hit) return;
352
368
  e.preventDefault();
@@ -1,10 +1,24 @@
1
1
  # The visor — the first-person HUD over every scene (task 1005148, BONGOS-V3 R08; ADR 0399)
2
2
 
3
- Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thinking`, and the three scene registrations (`sky-interaction.js`, `city.js`, `home-city.js`). Spec: `docs/specs/bongos-v3-game-conversion.md` C2 `you-are-a-character-in-first-person`. Game mode itself is in `game-mode.md`.
3
+ Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thinking`, and the three scene registrations (`sky-visor.js`, `city.js`, `home-city.js`). Spec: `docs/specs/bongos-v3-game-conversion.md` C2 `you-are-a-character-in-first-person`. Game mode itself is in `game-mode.md`.
4
4
 
5
5
  ## The rule every game page keeps (ADR 0403, owner 2026-11-11)
6
6
 
7
- The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet.
7
+ The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet; the Governor City keeps it since task 1005209 and the sky since task 1005210 (below).
8
+
9
+ ## A place's own buttons on the glass (task 1005209, the Governor City first)
10
+
11
+ - **The scene contract grew two flags.** A scene action's `key` is still a key the scene binds itself, shown beside it in the sheet. With `bind: true` the visor binds that key instead, after the character's keys (a clash goes to the character), for a scene button that had no key of its own. With `lead: true` the action is also the first quiet key hint on the glass (`.visor__hint--place`). Every scene action is still a row under "This place" in the sheet.
12
+ - **The glass binds keys in game mode only.** `onKey` now returns unless `html[data-game="on"]`: the glass was never drawn outside game mode, so its keys no longer act there either (before, a held character's letter keys still ran on a classic page).
13
+ - **The Governor City keeps ADR 0403.** In game mode (`html.visor-on`) the door's button (`#cityStart`) and the archived Street links leave the scene (`city.css`). The door is the lead action, on D, in the door's own words and case ("decide 37 in a row", or "start governing"; lowercase is the door's, task 1005210); it re-registers only when those words change. The archived links are rows in the sheet ("Archived: Street", "Archived: Building Level Views"), read off the page's own archived line so the words and the addresses cannot drift; reached by A and the keyboard. The city line stays on the scene as words, raised to `bottom: 224px` because the extra hint wraps the hints onto a second line at a desk width and the centred accent button rises with it. A building's card is a pop-up, not the scene, and keeps its own buttons and keys. Classic mode is as it was.
14
+
15
+ ## The sky on the glass (task 1005210)
16
+
17
+ - **What left the sky in game mode** (`thinking.css`, under `html.visor-on`): the top row's lens switch and every pill, the jump field, the thought composer, and the foot's clock and legend. The brand line and its figures stay, as words. A building-like pop-up (the panel, the list, the start dialog, the tour) is not the scene and keeps its buttons.
18
+ - **Where it went** (`sky-visor.js`, loaded after every other sky file; the registration moved out of `sky-interaction.js`). "Write a thought" is the lead action, on the sky's own `/`; "Jump to a goal or a thought" is J (bound by the visor). Each visible pill is a row in the sheet, in its own words with "· on" / "· off" for a toggle, and pressing the row presses the pill, so it does exactly what it did; List, the tour and Fit keep the sky's own L, ? and 0. "Clock" and "Legend" open the foot's two parts. The rows are read off the page's buttons and the scene registers again when a pill's pressed state, a pill's presence (the lens decides it) or the foot changes.
19
+ - **The fields open and close.** A field opens with `is-open` (one at a time) and closes when focus leaves it empty, or on Escape, which keeps its words and is spent there (no options menu). The composer stays open while a full idea is being asked or a receipt waits.
20
+ - **A visor fix that came with it.** The sheet had a click listener of its own inside `#visor`'s, so every row ran twice; a toggle row undid itself. The sheet's listener is gone.
21
+ - **Not done:** the tour still spots the pills it explains, which are off the glass in game mode. Tests: `tests/hall_sky_visor.mjs` (both pages' own markup, the real visor and sheets, in a real browser).
8
22
 
9
23
  ## The minimal visor (Helm look 3, task 1005195)
10
24
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.91",
3
+ "version": "1.24.93",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.91",
9
+ "version": "1.24.93",
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.24.91",
3
+ "version": "1.24.93",
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",
@@ -11644,10 +11644,24 @@
11644
11644
  "text": "In the game view the Artist's feedback tab no longer sits on the screen; the Artist opens the feedback sheet from the visor, and only the Artist can open it."
11645
11645
  }
11646
11646
  ],
11647
- "1.24.91": [
11647
+ "1.24.92": [
11648
+ {
11649
+ "id": "1005209",
11650
+ "text": "In game mode the Governor City no longer has a button of its own: \"Decide N in a row\" is now on the visor (press D), and the archived Street links are in the visor's Actions list. Classic mode is unchanged."
11651
+ },
11652
+ {
11653
+ "id": "1005118",
11654
+ "text": "Signing the command line back in is now one Approve click in the browser — no copying, no pasting, and the sign-in key never appears in the chat."
11655
+ },
11648
11656
  {
11649
11657
  "id": "1005206",
11650
11658
  "text": "The screen you land on after signing in, where you pick Game or Classic, now looks like the V3 game start menu: your usual choice is the bright salmon bar with its Enter key, the other a quiet row."
11651
11659
  }
11660
+ ],
11661
+ "1.24.93": [
11662
+ {
11663
+ "id": "1005210",
11664
+ "text": "In game mode the sky (Goals and Thinking) no longer has buttons of its own: \"Write a thought\" is on the visor (press /), jumping is J, and the lens switch, the pills, the clock and the legend are rows in the visor's Actions li"
11665
+ }
11652
11666
  ]
11653
11667
  }
@@ -567,15 +567,16 @@ function printUnauthGuidanceAndExit() {
567
567
  console.error('');
568
568
  if (isTerminalSurface()) {
569
569
  // No checkout here, so `node scripts/bongos/*.js` is not a path this reader has (task 1003730).
570
- console.error(` Recover the fast (UI-first) way — run ${surfaceAction('reauth')}`);
571
- console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
572
- console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
570
+ console.error(` Sign back in — run ${surfaceAction('reauth')}`);
571
+ console.error(' (a page opens in your browser: click "Approve" and you are back in.');
572
+ console.error(' Nothing to copy or paste.)');
573
573
  console.error(' Fallback (browser sign-in): npx bongos login <instance-url>');
574
574
  } else {
575
- console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
575
+ // Task 1005118: one Approve click in the browser. Claude runs it when asked.
576
+ console.error(' Sign back in — ask Claude to "sign me back in" (/builder-reauth), or run:');
576
577
  console.error(' node scripts/bongos/reauth.js');
577
- console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
578
- console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
578
+ console.error(' (a page opens in your browser: click "Approve" and you are back in.');
579
+ console.error(' Nothing to copy or paste.)');
579
580
  console.error(' Fallback (terminal Device Flow): node scripts/bongos/setup.js --force');
580
581
  }
581
582
  console.error('');
@@ -104,14 +104,19 @@ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004
104
104
  // instance's /auth/device/exchange — a hosted instance has no GitHub app of its own.
105
105
  // The instance's OWN admission gate runs at poll (direct) or exchange (federated),
106
106
  // so the stranger path is identical for both.
107
- async function runDeviceFlow(base, { log, idp = null }) {
107
+ // `browser: true` (a returning builder's re-sign-in, task 1005118) asks the
108
+ // instance for its own approval page instead of GitHub's code entry: a browser
109
+ // that is already signed in approves with one click. An older instance ignores
110
+ // the ask and answers GitHub's flow, which this loop runs unchanged.
111
+ async function runDeviceFlow(base, { log, idp = null, browser = false }) {
108
112
  const startUrl = idp ? `${idp.origin}/api/bongos/sso/device/start` : `${base}/api/bongos/auth/device/start`;
109
113
  const start = await fetchJson(startUrl, {
110
114
  method: 'POST',
111
115
  // Federated: the hub resolves + binds the client from the USER-TYPED origin
112
116
  // (`base`), never a client_id we supply — so this CLI can only ever mint an
113
117
  // assertion scoped to the instance the user actually typed (ADR 0141 §1).
114
- ...(idp ? { body: JSON.stringify({ origin: base }) } : {}),
118
+ ...(idp ? { body: JSON.stringify({ origin: base }) }
119
+ : browser ? { body: JSON.stringify({ approve_in_browser: true }) } : {}),
115
120
  });
116
121
  // ADR 0116 nests the slug at error.code (task 1003658). This site was half-converted:
117
122
  // it read the nested shape for idp_signing_unconfigured but kept a FLAT comparison for
@@ -142,10 +147,16 @@ async function runDeviceFlow(base, { log, idp = null }) {
142
147
  const expires = Number(start.data.expires_in) || 900;
143
148
 
144
149
  log('');
150
+ // An approval-page link (it carries the code) is longer than the box: print it
151
+ // whole underneath rather than cut it, so a browser that didn't open can be
152
+ // pointed at it by hand.
153
+ const uri = String(verification_uri);
154
+ const fits = uri.length <= 36;
145
155
  log(' ┌────────────────────────────────────────────┐');
146
- log(` │ Open: ${String(verification_uri).slice(0, 36).padEnd(36)}│`);
156
+ if (fits) log(` │ Open: ${uri.padEnd(36)}│`);
147
157
  log(` │ Code: ${String(user_code).slice(0, 36).padEnd(36)}│`);
148
158
  log(' └────────────────────────────────────────────┘');
159
+ if (!fits) log(` Open: ${uri}`);
149
160
  log('');
150
161
  openBrowser(verification_uri);
151
162
  log(' Waiting for you to approve in the browser…');
@@ -324,7 +324,7 @@ const CARDS = Object.freeze({
324
324
  'You, to sign in.', RULE),
325
325
  session_invalid: card(
326
326
  'Your sign-in to the project has ended.',
327
- 'I will walk you through signing in again — Settings, then one copy and paste.',
327
+ 'I will open a page in your browser — you click Approve, and that is all.',
328
328
  'You, to sign in again.', RULE),
329
329
  instance_mismatch: card(
330
330
  'This computer is signed in to a different project than the one this folder belongs to.',