@bongos/core 1.24.40 → 1.24.41

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 (69) hide show
  1. package/.bongos-core.json +75 -90
  2. package/docs/canonical-permissions.md +2 -1
  3. package/docs/copy-inventory.md +116 -120
  4. package/docs/copy-registry.json +151 -207
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-inventory.json +11 -69
  7. package/docs/page-readings.json +382 -763
  8. package/modules/hall-ui/public/ai-studio.html +2 -2
  9. package/modules/hall-ui/public/atlas.html +2 -2
  10. package/modules/hall-ui/public/blockers.html +2 -2
  11. package/modules/hall-ui/public/board-room.html +2 -2
  12. package/modules/hall-ui/public/city.html +2 -2
  13. package/modules/hall-ui/public/collab.html +2 -2
  14. package/modules/hall-ui/public/commands.html +2 -2
  15. package/modules/hall-ui/public/copy-desk.html +2 -2
  16. package/modules/hall-ui/public/craft-mode.js +1 -1
  17. package/modules/hall-ui/public/deploy.html +2 -2
  18. package/modules/hall-ui/public/gate.js +12 -5
  19. package/modules/hall-ui/public/goal-map.html +2 -2
  20. package/modules/hall-ui/public/goals.html +2 -2
  21. package/modules/hall-ui/public/government.html +2 -2
  22. package/modules/hall-ui/public/idea.html +2 -2
  23. package/modules/hall-ui/public/ideas.html +2 -2
  24. package/modules/hall-ui/public/index.html +2 -2
  25. package/modules/hall-ui/public/market-watch-item.html +2 -2
  26. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  27. package/modules/hall-ui/public/market-watch.html +2 -2
  28. package/modules/hall-ui/public/modules.html +2 -2
  29. package/modules/hall-ui/public/money.html +2 -2
  30. package/modules/hall-ui/public/my-agents.html +2 -2
  31. package/modules/hall-ui/public/palette.js +10 -0
  32. package/modules/hall-ui/public/primer.html +2 -2
  33. package/modules/hall-ui/public/profile.html +2 -2
  34. package/modules/hall-ui/public/project-settings.html +2 -2
  35. package/modules/hall-ui/public/roadmap.html +2 -2
  36. package/modules/hall-ui/public/roster.html +2 -2
  37. package/modules/hall-ui/public/sessions.js +21 -10
  38. package/modules/hall-ui/public/settings.html +2 -2
  39. package/modules/hall-ui/public/shell.js +1 -2
  40. package/modules/hall-ui/public/street.html +2 -2
  41. package/modules/hall-ui/public/studio.html +2 -2
  42. package/modules/hall-ui/public/task.html +2 -2
  43. package/modules/hall-ui/public/thinking.html +2 -2
  44. package/modules/hall-ui/public/tweak-editor.html +2 -2
  45. package/modules/hall-ui/public/watch.html +120 -7
  46. package/modules/hall-ui/public/watch.js +11 -0
  47. package/modules/hall-ui/public/watch.states.json +49 -1
  48. package/modules/hall-ui/public/work.html +2 -2
  49. package/package-lock.json +2 -2
  50. package/package.json +1 -1
  51. package/release-notes.json +6 -0
  52. package/scripts/bongos/gen-hall-sitemap.js +2 -0
  53. package/src/bongos/serve-internal.js +15 -22
  54. package/src/module-api.js +1 -1
  55. package/tests/autonomy_builder_scope.mjs +2 -1
  56. package/tests/hall_audit.mjs +1 -1
  57. package/tests/hall_loading.mjs +13 -2
  58. package/tests/hall_nav.mjs +5 -2
  59. package/tests/hall_oversight_tabs.mjs +162 -0
  60. package/tests/hall_oversight_world.mjs +23 -9
  61. package/tests/hall_page_gate_map.mjs +7 -2
  62. package/tests/hall_roster_world.mjs +14 -9
  63. package/tests/hall_sessions_ledger.mjs +2 -1
  64. package/tests/nav_permission_atoms.mjs +3 -2
  65. package/tests/sessions_kit_pager.mjs +8 -4
  66. package/modules/hall-ui/public/gate.html +0 -76
  67. package/modules/hall-ui/public/gate.states.json +0 -60
  68. package/modules/hall-ui/public/sessions.html +0 -128
  69. package/modules/hall-ui/public/sessions.states.json +0 -59
@@ -0,0 +1,162 @@
1
+ // tests/hall_oversight_tabs.mjs — one oversight page (task 1005049, owner decision
2
+ // 2026-11-03, idea 1001315 item 1).
3
+ //
4
+ // Watch, Sessions and Autobongos (/gate) were three oversight pages in the same
5
+ // sidebar group. Sessions and Autobongos are tabs on Watch now:
6
+ //
7
+ // * OLD URLS LAND ON THE RIGHT TAB. /sessions and /gate 302 to /watch#sessions
8
+ // and /watch#autobongos (serve-internal.js RETIRED_TO_SECTION), with any query
9
+ // string kept; the real middleware runs here, on a bare app.
10
+ // * EVERYTHING THE TWO PAGES SHOWED IS ON WATCH. The tabs exist, their panes hold
11
+ // the ledger, the transcript reader and the runner panel, and the scripts that
12
+ // drew the pages still draw them.
13
+ // * A TAB LOADS WHEN IT OPENS. Each session-ledger load is an audited read on the
14
+ // server, so sessions.js and gate.js ask for nothing until Watch first opens
15
+ // their tab, and only once.
16
+ // * THE PALETTE STILL FINDS THEM: its Watch jumps are real tab ids.
17
+ //
18
+ // Run: node tests/hall_oversight_tabs.mjs
19
+
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import fs from 'node:fs';
23
+ import http from 'node:http';
24
+ import path from 'node:path';
25
+ import vm from 'node:vm';
26
+ import { createRequire } from 'node:module';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
32
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
33
+ const WATCH_HTML = read('watch.html');
34
+ const WATCH_JS = read('watch.js');
35
+
36
+ async function withHall(fn) {
37
+ const express = require('express');
38
+ const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
39
+ const app = express();
40
+ mountInternalSurfaces(app);
41
+ const server = app.listen(0, '127.0.0.1');
42
+ await new Promise((r) => server.once('listening', r));
43
+ const { port } = server.address();
44
+ const get = (p, host) => new Promise((resolve, reject) => {
45
+ http.get({ host: '127.0.0.1', port, path: p, headers: { host } }, (res) => {
46
+ res.resume();
47
+ res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || '' }));
48
+ }).on('error', reject);
49
+ });
50
+ try { await fn(get); } finally { server.close(); }
51
+ }
52
+
53
+ const TAB_OF = { sessions: 'sessions', gate: 'autobongos' };
54
+
55
+ test('RUN: /sessions and /gate each 302 to their tab on Watch', async () => {
56
+ await withHall(async (get) => {
57
+ for (const [page, tab] of Object.entries(TAB_OF)) {
58
+ for (const form of [`/${page}`, `/${page}/`, `/${page}.html`]) {
59
+ const r = await get(form, 'builders.127.0.0.1');
60
+ assert.equal(r.status, 302, `${form} is a temporary redirect`);
61
+ assert.equal(r.location, `/watch#${tab}`, `${form} lands on #${tab}`);
62
+ }
63
+ }
64
+ const q = await get('/sessions?from=mail', 'builders.127.0.0.1');
65
+ assert.equal(q.location, '/watch?from=mail#sessions', 'a query string rides along; the tab is the fragment');
66
+ // The map the two joined still carries the page it was built for (task 1005047).
67
+ assert.equal((await get('/fleet', 'builders.127.0.0.1')).location, '/money#hosting');
68
+ });
69
+ });
70
+
71
+ test('Watch has a Sessions and an Autobongos tab, each with its pane', () => {
72
+ assert.match(WATCH_JS, /\{ id: 'sessions', label: 'Sessions', panel: 'pane-sessions' \}/);
73
+ assert.match(WATCH_JS, /\{ id: 'autobongos', label: 'Autobongos', panel: 'pane-autobongos' \}/);
74
+ for (const pane of ['pane-sessions', 'pane-autobongos']) {
75
+ assert.match(WATCH_HTML, new RegExp(`<div id="${pane}" role="tabpanel" aria-labelledby="tab-${pane.slice(5)}" hidden>`));
76
+ }
77
+ });
78
+
79
+ test('everything the two pages showed is in those panes', () => {
80
+ const sessions = WATCH_HTML.slice(WATCH_HTML.indexOf('<div id="pane-sessions"'), WATCH_HTML.indexOf('<div id="pane-autobongos"'));
81
+ for (const id of ['sess-sealed', 'stats-scroll', 'sess-ledger-scroll', 'session-type-filter', 'sessions-table', 'sessions-pager-mount', 'transcript-scroll']) {
82
+ assert.match(sessions, new RegExp(`id="${id}"`), `the Sessions tab holds #${id}`);
83
+ }
84
+ const runner = WATCH_HTML.slice(WATCH_HTML.indexOf('<div id="pane-autobongos"'));
85
+ for (const id of ['gate-sealed', 'runner-body', 'gate-archon-only', 'fence-panel', 'fence-body', 'fence-builders']) {
86
+ assert.match(runner, new RegExp(`id="${id}"`), `the Autobongos tab holds #${id}`);
87
+ }
88
+ // Their scripts load before watch.js, which boots them.
89
+ const i = (s) => WATCH_HTML.indexOf(s);
90
+ assert.ok(i('/builders/sessions.js') > 0 && i('/builders/sessions.js') < i('/builders/watch.js'));
91
+ assert.ok(i('/builders/gate.js') > 0 && i('/builders/gate.js') < i('/builders/watch.js'));
92
+ // No element id on the page is used twice: three pages' markup now share one document.
93
+ const ids = [...WATCH_HTML.replace(/<!--[\s\S]*?-->/g, '').matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
94
+ assert.deepEqual(ids.filter((x, k) => ids.indexOf(x) !== k), [], 'every id on Watch is said once');
95
+ });
96
+
97
+ test('the pages are gone, and nothing in the sidebar points at them', () => {
98
+ assert.ok(!fs.existsSync(path.join(HALL, 'sessions.html')));
99
+ assert.ok(!fs.existsSync(path.join(HALL, 'gate.html')));
100
+ assert.ok(!/href: '\/(sessions|gate)'/.test(read('shell.js')));
101
+ assert.ok(!/'gate', 'sessions'/.test(read('craft-mode.js')), 'Governing mode no longer lists them');
102
+ });
103
+
104
+ function bootTab(file, { sharedMe = false } = {}) {
105
+ const requests = [];
106
+ const el = () => ({ hidden: true, textContent: '', innerHTML: '', value: '', dataset: {}, style: {}, addEventListener() {}, querySelector: () => null, querySelectorAll: () => [], appendChild() {} });
107
+ const els = {};
108
+ const getEl = (id) => (els[id] = els[id] || el());
109
+ const document = { getElementById: getEl, querySelector: (s) => getEl(String(s).replace(/^#/, '')), querySelectorAll: () => [], createElement: el, addEventListener() {} };
110
+ const window = {
111
+ BongosClient: { createClient: () => ({ request: async (m, p) => { requests.push(p); return { status: 200, ok: true, data: { builder: { id: 1, rank: 'xenos' } } }; } }) },
112
+ OTB: { escapeHtml: (s) => String(s), $: document.querySelector, num: Number, fmtInt: String, fmtCompact: String, fmtDate: String, rankLabel: (r) => r, toast() {} },
113
+ OTBKit: { emptyStateHtml: (m) => m, makePager: () => ({ page: 1, perPage: 25 }) },
114
+ location: { assign() {} },
115
+ localStorage: { getItem: () => null, setItem() {} },
116
+ addEventListener() {},
117
+ };
118
+ // The shell's own /me read (shell.js probeMe shares it on window.__hallReads).
119
+ // A viewer below the floor, so each tab stops at its own sealed notice after /me.
120
+ if (sharedMe) window.__hallReads = { '/me': Promise.resolve({ builder: { id: 1, rank: 'xenos' } }) };
121
+ const sandbox = { window, document, console: { log() {}, warn() {}, error() {} }, setTimeout, clearTimeout, Promise, URLSearchParams };
122
+ vm.createContext(sandbox);
123
+ vm.runInContext(read(file), sandbox, { filename: file });
124
+ return { window, requests, getEl };
125
+ }
126
+
127
+ for (const [file, tab] of [['sessions.js', 'sessions'], ['gate.js', 'autobongos']]) {
128
+ test(`${file} asks for nothing until Watch opens its tab, then boots once`, async () => {
129
+ const t = bootTab(file);
130
+ await new Promise((r) => setImmediate(r));
131
+ assert.equal(t.requests.length, 0, `${file} read the server before its tab opened`);
132
+ assert.equal(typeof t.window.OTBWatchTabs[tab], 'function', `${file} hands Watch a boot for #${tab}`);
133
+ t.window.OTBWatchTabs[tab]();
134
+ await new Promise((r) => setImmediate(r));
135
+ const first = t.requests.length;
136
+ assert.ok(first >= 1, 'opening the tab loads it');
137
+ t.window.OTBWatchTabs[tab]();
138
+ await new Promise((r) => setImmediate(r));
139
+ assert.equal(t.requests.length, first, 'opening it again does not load it twice');
140
+ });
141
+ }
142
+
143
+ for (const [file, tab] of [['sessions.js', 'sessions'], ['gate.js', 'autobongos']]) {
144
+ test(`${file} adopts the shell's /me read rather than asking again`, async () => {
145
+ const t = bootTab(file, { sharedMe: true });
146
+ t.window.OTBWatchTabs[tab]();
147
+ for (let i = 0; i < 5; i++) await new Promise((r) => setImmediate(r));
148
+ assert.ok(!t.requests.some((p) => /\/me$/.test(p)), `${file} asked /me although the shell had`);
149
+ assert.equal(t.getEl(file === 'gate.js' ? 'gate-sealed' : 'sess-sealed').hidden, false, 'and still acts on the answer');
150
+ });
151
+ }
152
+
153
+ test('watch.js boots a tab from its own change hook', () => {
154
+ assert.match(WATCH_JS, /onChange: \(id\) => \{\s*const boot = window\.OTBWatchTabs && window\.OTBWatchTabs\[id\];\s*if \(boot\) boot\(\);/);
155
+ });
156
+
157
+ test('the palette\'s Watch jumps are real watch.js tab ids', () => {
158
+ const ids = [...WATCH_JS.matchAll(/\{ id: '([a-z-]+)', label: '[^']*', panel:/g)].map((m) => m[1]);
159
+ const declared = [...read('palette.js').matchAll(/href:\s*'\/watch#([a-z-]+)'/g)].map((m) => m[1]);
160
+ assert.deepEqual(declared, ids, 'one jump per Watch tab, in order');
161
+ assert.ok(declared.includes('sessions') && declared.includes('autobongos'));
162
+ });
@@ -17,7 +17,8 @@ import { fileURLToPath } from 'node:url';
17
17
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
18
  const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
19
19
  const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
20
- const PAGES = ['government', 'watch', 'gate'];
20
+ // task 1005049: the /gate page is Watch's Autobongos tab; its script and sheet stay.
21
+ const PAGES = ['government', 'watch'];
21
22
 
22
23
  test('every oversight page links the family sheet, before its own', () => {
23
24
  for (const p of PAGES) {
@@ -27,6 +28,11 @@ test('every oversight page links the family sheet, before its own', () => {
27
28
  assert.ok(fam > 0, `${p}.html links oversight.css`);
28
29
  assert.ok(own > fam, `${p}.html links its own sheet after the family's`);
29
30
  }
31
+ // Watch's two folded-in tabs bring their sheets, after the family's too.
32
+ const watch = read('watch.html');
33
+ for (const sheet of ['sessions', 'gate']) {
34
+ assert.ok(watch.indexOf(`/builders/${sheet}.css`) > watch.indexOf('/builders/oversight.css'), `watch.html links ${sheet}.css after the family's`);
35
+ }
30
36
  });
31
37
 
32
38
  test('the family sheet spends only the fifteen: no literal colour', () => {
@@ -110,6 +116,10 @@ test('the sealed state is a panel with the floor written from the pack, on all f
110
116
  assert.match(read(`${p}.html`), /class="scroll" id="[a-z-]*sealed(-scroll)?" /, `${p}.html seals with a .scroll panel`);
111
117
  assert.match(read(`${p}.js`), /rankLabel\('metic'\)/, `${p}.js writes the floor from the pack's rank label`);
112
118
  }
119
+ // The Autobongos and Sessions tabs keep their own sealed panels inside Watch.
120
+ assert.match(read('watch.html'), /class="scroll" id="gate-sealed" /);
121
+ assert.match(read('watch.html'), /class="scroll" id="sess-sealed" /);
122
+ assert.match(read('gate.js'), /rankLabel\('metic'\)/);
113
123
  });
114
124
 
115
125
  test('the vocabulary wall holds: the family is named oversight on the government surface', () => {
@@ -278,7 +288,9 @@ test('RUN: a metric its snapshots never carried is skipped, not drawn as a cliff
278
288
 
279
289
  // ---- task 1004829: the government pages read like the rebuilt Home ----------
280
290
 
281
- const GOV_PAGES = ['watch', 'gate', 'sessions', 'project-settings', 'money', 'deploy', 'government'];
291
+ const GOV_PAGES = ['watch', 'project-settings', 'money', 'deploy', 'government'];
292
+ // task 1005049: gate.js and sessions.js draw Watch's tabs now; their scripts still must not.
293
+ const GOV_SCRIPTS = [...GOV_PAGES, 'gate', 'sessions'];
282
294
 
283
295
  test('every government page head holds only its h1, and no script writes into it', () => {
284
296
  // No page shows its head (task 1004836). A sub-line there is a line nobody
@@ -291,7 +303,7 @@ test('every government page head holds only its h1, and no script writes into it
291
303
  assert.doesNotMatch(head[1], /page-head__eyebrow|page-head__sub/, `${page}.html's hidden head holds only the h1`);
292
304
  }
293
305
  const subIds = ['watch-sub', 'gate-sub', 'ledger-sub', 'ps-sub', 'dp-sub', 'fl-sub', 'gov-sub'];
294
- for (const page of GOV_PAGES) {
306
+ for (const page of GOV_SCRIPTS) {
295
307
  const js = read(`${page}.js`);
296
308
  for (const id of subIds) assert.ok(!js.includes(id), `${page}.js no longer writes into #${id}`);
297
309
  }
@@ -300,7 +312,7 @@ test('every government page head holds only its h1, and no script writes into it
300
312
  test('a failed load on Watch and Sessions is said in the page body', () => {
301
313
  assert.match(read('watch.js'), /showSealed\('Could not check your access\. Reload to try again\.', 'Could not load'\)/);
302
314
  assert.match(read('sessions.js'), /showSealed\('Could not check your access\. Reload to try again\.', 'Could not load'\)/);
303
- assert.match(read('sessions.html'), /id="ledger-status" role="status"/, 'the count and a failed page load sit on the ledger itself');
315
+ assert.match(read('watch.html'), /id="ledger-status" role="status"/, 'the count and a failed page load sit on the ledger itself');
304
316
  });
305
317
 
306
318
  test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is told the controls are the Archon\'s', () => {
@@ -312,10 +324,12 @@ test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is
312
324
  assert.ok(!/renderRow\(pr|OTBAsk/.test(js), 'no held-PR row or ask card is drawn');
313
325
  assert.match(js, /if \(rank !== 'archon'\) \{ showArchonOnly\(\); return; \}/, 'a Metic gets the note, not an empty page');
314
326
  assert.match(js, /The Autobongos controls are for the \$\{rankLabel\('archon'\)\}/, 'the rank is named from the pack');
315
- const html = read('gate.html');
316
- assert.match(html, /<title>Autobongos — /, 'the page says what it is');
317
- assert.match(html, /data-page-title="Autobongos"/);
318
- assert.match(html, /id="gate-archon-only"/);
327
+ // task 1005049: the panel is Watch's Autobongos tab, and /gate lands there.
328
+ const html = read('watch.html');
329
+ const pane = html.slice(html.indexOf('<div id="pane-autobongos"'));
330
+ assert.match(pane, /id="gate-archon-only"/);
319
331
  assert.ok(!/gate-list|hall-ask\.js/.test(html), 'the list mount and the ask-card script are gone');
320
- assert.match(read('shell.js'), /\{ id: 'gate', label: 'Autobongos', icon: 'gate', href: '\/gate'/, 'the nav names it, at the same URL');
332
+ assert.match(read('watch.js'), /\{ id: 'autobongos', label: 'Autobongos', panel: 'pane-autobongos' \}/, 'the tab says what it is');
333
+ assert.ok(!/href: '\/gate'/.test(read('shell.js')), 'the sidebar no longer lists it');
334
+ assert.match(fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8'), /gate: 'watch#autobongos'/, 'and the old URL lands on the tab');
321
335
  });
@@ -120,8 +120,10 @@ const ORACLE = {
120
120
 
121
121
  // Metic+ operator surfaces (the Governance nav group).
122
122
  '/builders/watch': 'requireGovernmentPage',
123
- '/builders/gate': 'requireGovernmentPage',
124
- '/builders/sessions': 'requireGovernmentPage',
123
+ // Gone (task 1005049, owner decision 2026-11-03): Sessions and Autobongos are tabs
124
+ // on Watch, behind Watch's gate, and both URLs 302 to their tab before any gate runs.
125
+ '/builders/gate': null,
126
+ '/builders/sessions': null,
125
127
  // The five fundamentals of the project (task 1003280, goal 1000072). Same tier as
126
128
  // its neighbours: a steering surface, metic+ per ADR 0157. Its two WRITES gate
127
129
  // independently on `project.settings.manage`, so this row is only about the shell.
@@ -360,6 +362,9 @@ const GONE_PAGES = {
360
362
  '/builders/ranks': 'task 1005046 — the rank ladder is a Primer chapter; /ranks redirects there',
361
363
  '/builders/drachmae': 'task 1005046 — credits and karma is a Primer chapter; /drachmae redirects there',
362
364
  '/builders/diagrams': 'task 1005046 — each diagram sits in its Primer chapter; /diagrams redirects there',
365
+ // Task 1005049 — one oversight page: each is a tab on Watch, and the old URL 302s to it.
366
+ '/builders/sessions': 'task 1005049 — the session log is Watch\'s Sessions tab; /sessions redirects there',
367
+ '/builders/gate': 'task 1005049 — the runner panel is Watch\'s Autobongos tab; /gate redirects there',
363
368
  };
364
369
 
365
370
  test('a row that is null because the page left the hall serves no page', () => {
@@ -230,12 +230,15 @@ test('sessions: the nowrap + wrapper pair is gone, and with it the sticky-header
230
230
  const css = decomment(read('sessions.css'));
231
231
  assert.doesNotMatch(css, /white-space:\s*nowrap/, 'no blanket nowrap on every cell');
232
232
  assert.doesNotMatch(css, /ledger-table-wrap/, 'no wrapper — the shell\'s card strip handles a phone');
233
- assert.doesNotMatch(read('sessions.html'), /ledger-table-wrap/);
233
+ assert.doesNotMatch(sessionsPane(), /ledger-table-wrap/);
234
234
  assert.doesNotMatch(css, /var\(--info\)|ledger-sid/, 'the session id is no longer painted in the info colour');
235
235
  noAlias(css, 'sessions.css');
236
236
  tokensDefined(css, 'sessions.css');
237
237
  });
238
238
 
239
+ // task 1005049: the session ledger is Watch's Sessions tab; its markup is that pane.
240
+ const sessionsPane = () => { const w = read('watch.html'); return w.slice(w.indexOf('<div id="pane-sessions"'), w.indexOf('<div id="pane-autobongos"')); };
241
+
239
242
  test('sessions: every id on a row is a door, the earned figure is shown, the totals ride the top strip', () => {
240
243
  const src = read('sessions.js');
241
244
  assert.match(src, /<a class="fact-chip" href="\/builders\/task\/\$\{encodeURIComponent\(String\(t\)\)\}">task \$\{escapeHtml\(String\(t\)\)\}<\/a>/,
@@ -244,8 +247,8 @@ test('sessions: every id on a row is a door, the earned figure is shown, the tot
244
247
  assert.match(src, /<span class="ov-mono" title="\$\{escapeHtml\(s\.session_id\)\}">/, 'the session id is a mono fact with the full id on hover');
245
248
  assert.match(src, /fmtInt\(s\.drachmae_earned\)/, 'what the session earned is on the row');
246
249
  assert.doesNotMatch(src, /`#\$\{escapeHtml\(t\)\}`/, 'no bare hash-prefixed task text');
247
- const html = read('sessions.html');
248
- assert.match(html, /<section class="ov-summary" id="stats-scroll"/, 'the totals are the top-of-page strip');
250
+ const html = sessionsPane();
251
+ assert.match(html, /<section class="ov-summary" id="stats-scroll"/, 'the totals are the top-of-tab strip');
249
252
  assert.match(html, /<div class="stat-row" id="stats-body"><\/div>/);
250
253
  const ths = (html.match(/<th[ >]/g) || []).length;
251
254
  // task 1002516: a tenth column, "Plain speech" — the transcript accessibility
@@ -259,8 +262,9 @@ test('sessions: every id on a row is a door, the earned figure is shown, the tot
259
262
  // empty state short by a column, which no screenshot pass would catch.
260
263
  assert.equal(ths, 11, 'eleven columns: session, builder, kind, tasks, earned, turns, tokens, tool errors, duration, plain speech, recorded');
261
264
  assert.match(src, new RegExp(`colspan="${ths}"`), 'the empty row spans them all');
262
- const fam = html.indexOf('/builders/oversight.css');
263
- assert.ok(fam > 0 && html.indexOf('/builders/sessions.css') > fam, 'sessions.html links the family sheet before its own');
265
+ const page = read('watch.html');
266
+ const fam = page.indexOf('/builders/oversight.css');
267
+ assert.ok(fam > 0 && page.indexOf('/builders/sessions.css') > fam, 'watch.html links the family sheet before the sessions sheet');
264
268
  });
265
269
 
266
270
  test('sessions: the floor is written from the pack\'s rank label, never a hardcoded rank', () => {
@@ -268,9 +272,9 @@ test('sessions: the floor is written from the pack\'s rank label, never a hardco
268
272
  const sealed = src.slice(src.indexOf('function showSealed'), src.indexOf('// ---- paging state'));
269
273
  assert.match(sealed, /const floor = rankLabel\('metic'\)/);
270
274
  // a failed load names its own heading; the rank floor is the default (task 1004829)
271
- assert.match(sealed, /\$\('#gate-h'\)\.textContent = heading \|\| `\$\{floor\} and up`/);
275
+ assert.match(sealed, /\$\('#sess-sealed-h'\)\.textContent = heading \|\| `\$\{floor\} and up`/);
272
276
  assert.doesNotMatch(decomment(src), /Archons only|Metic rank up|Archon-only/);
273
- assert.doesNotMatch(read('sessions.html'), /Archon/);
277
+ assert.doesNotMatch(sessionsPane(), /Archon/);
274
278
  });
275
279
 
276
280
  // =========================================================================
@@ -299,8 +303,9 @@ test('the states files exercise the redesign\'s new states', () => {
299
303
  const goals = JSON.parse(read('goals.states.json'));
300
304
  assert.ok(goals.states.filters && goals.states.cards, 'goals: the Filters disclosure and the cards view are measured');
301
305
  assert.deepEqual(goals.states.cards.expect.value, [['#f-goal-view', 'cards']]);
302
- const sessions = JSON.parse(read('sessions.states.json'));
303
- assert.deepEqual(sessions.states.ledger.expect.visible, ['#stats-scroll', '#sessions-scroll']);
306
+ // task 1005049: the ledger is Watch's Sessions tab, measured as that page's state.
307
+ const watch = JSON.parse(read('watch.states.json'));
308
+ assert.deepEqual(watch.states.sessions.expect.visible, ['#pane-sessions', '#stats-scroll', '#sess-ledger-scroll']);
304
309
  });
305
310
 
306
311
  test('the roster draws the circuit divider in CSS, on the first row\'s cell and not the row', () => {
@@ -19,7 +19,8 @@ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
19
19
  const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
20
20
 
21
21
  const JS = read('sessions.js');
22
- const HTML = read('sessions.html');
22
+ // task 1005049: the ledger is Watch's Sessions tab; its markup lives in watch.html.
23
+ const HTML = (() => { const w = read('watch.html'); return w.slice(w.indexOf('<div id="pane-sessions"'), w.indexOf('<div id="pane-autobongos"')); })();
23
24
 
24
25
  // Slice `const ACCESSIBILITY_TITLE = {...}` through the end of accessibilityCell
25
26
  // and run it with the one helper it closes over.
@@ -95,8 +95,9 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
95
95
  // task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
96
96
  // Market Watch settings nav item and its fuel glyph; task 1005041: the Update pill replaced the
97
97
  // core-update banner; task 1005045: the Market Watch settings item left (one sidebar item);
98
- // task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer).
99
- const SHELL_SHA = 'baf8a15f025eba68';
98
+ // task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer);
99
+ // task 1005049: Sessions and Autobongos left the sidebar (tabs on Watch).
100
+ const SHELL_SHA = 'bb0abbadab2508ca';
100
101
 
101
102
  test('shell.js has not changed without its cache-bust being bumped', () => {
102
103
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -33,7 +33,8 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
33
  const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
34
34
  const KIT_JS = read('modules/hall-ui/public/hall-kit.js');
35
35
  const SESSIONS_JS = read('modules/hall-ui/public/sessions.js');
36
- const SESSIONS_HTML = read('modules/hall-ui/public/sessions.html');
36
+ // task 1005049: the ledger is Watch's Sessions tab, so its markup lives in watch.html.
37
+ const SESSIONS_HTML = read('modules/hall-ui/public/watch.html');
37
38
 
38
39
  const { test, summary } = makeRunner();
39
40
  const tick = () => new Promise((r) => setTimeout(r, 0));
@@ -147,6 +148,9 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
147
148
  vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
148
149
  if (dropKit) delete sandbox.window.OTBKit;
149
150
  vm.runInContext(SESSIONS_JS, sandbox, { filename: 'sessions.js' });
151
+ // The tab boots when Watch first opens it (task 1005049); a second open is a no-op.
152
+ sandbox.window.OTBWatchTabs.sessions();
153
+ sandbox.window.OTBWatchTabs.sessions();
150
154
 
151
155
  const searches = () => requests.filter((u) => u.includes('/sessions/search'));
152
156
  const q = (n) => {
@@ -161,7 +165,7 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
161
165
  // 1. the page markup no longer carries a pager of its own
162
166
  // ---------------------------------------------------------------------------
163
167
 
164
- await test('sessions.html carries a mount, not a hand-rolled pager', () => {
168
+ await test('the Sessions tab carries a mount, not a hand-rolled pager', () => {
165
169
  assert.ok(!/id="ledger-pager"/.test(SESSIONS_HTML), 'the bespoke <nav class="pager"> is gone');
166
170
  assert.ok(!/id="pager-(prev|next|pos)"/.test(SESSIONS_HTML), 'its bespoke button ids are gone');
167
171
  assert.match(SESSIONS_HTML, /id="sessions-pager-mount"/);
@@ -307,14 +311,14 @@ await test('a missing kit degrades to a named failure, not a blank page', async
307
311
  await tick(); await tick();
308
312
  assert.equal(h.searches().length, 0, 'no corpus request without a pager to place it');
309
313
  // in the page body: the head that used to carry it is hidden (task 1004829)
310
- assert.match(h.getEl('gate-body').textContent, /could not be loaded/);
314
+ assert.match(h.getEl('sess-sealed-body').textContent, /could not be loaded/);
311
315
  });
312
316
 
313
317
  await test('the rank gate still runs before any corpus request', async () => {
314
318
  const h = harness({ total: 320, viewerRank: 'xenos' });
315
319
  await tick(); await tick();
316
320
  assert.equal(h.searches().length, 0, 'a xenos must not even ask');
317
- assert.equal(h.getEl('gate-scroll').hidden, false, 'they get the sealed notice');
321
+ assert.equal(h.getEl('sess-sealed').hidden, false, 'they get the sealed notice');
318
322
  });
319
323
 
320
324
  summary();
@@ -1,76 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Autobongos — {{worldName}}</title>
7
- <meta name="description" content="The Autobongos runner: switch unattended building on or off, choose the goals it may work, and pause any builder's runner.">
8
- <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
- <link rel="stylesheet" href="/builders/oversight.css">
13
- <link rel="stylesheet" href="/builders/gate.css">
14
- </head>
15
- <body data-page="gate" data-page-title="Autobongos">
16
-
17
- <div class="app">
18
- <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
19
- <div class="app-scrim" id="app-scrim" hidden></div>
20
- <div class="app-body">
21
- <header class="app-topbar" id="app-topbar"></header>
22
- <div class="app-content">
23
- <!-- The page's one h1, for screen readers: no page shows its head (task
24
- 1004836), so its sub-line went in task 1004829. -->
25
- <div class="page-head page-head--board">
26
- <h1 class="page-head__h">Autobongos</h1>
27
- </div>
28
-
29
- <main id="hall-main">
30
- <!-- task 1005043: the page loader, shown until any section below is revealed. -->
31
- <p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…</p>
32
- <!-- task 1003469: the sealed state is a panel with a heading, not a bare
33
- line on the ground. gate.js writes the real floor into the lede from
34
- the pack's rank label, so the copy cannot drift from ADR 0157. -->
35
- <section class="scroll" id="gate-sealed" aria-labelledby="gate-sealed-h" hidden>
36
- <h2 class="scroll__h" id="gate-sealed-h">Restricted</h2>
37
- <p class="scroll__lede" id="gate-sealed-body">This page opens at a higher rank.</p>
38
- </section>
39
-
40
- <section id="gate-body" hidden>
41
- <!-- The Autobongos fence (task 1003908, ADR 0307 §6): what is allowed to
42
- reach main unattended. This page held the held-PR list too until task
43
- 1004859 removed it; the URL stays /gate so bookmarks keep working.
44
- Archon-only — a Metic gets the note below instead, and the routes
45
- behind the panel are system:true/archon server-side regardless. -->
46
- <p class="scroll__lede" id="gate-archon-only" hidden></p>
47
- <section class="scroll" id="fence-panel" aria-labelledby="fence-h" hidden>
48
- <h2 class="scroll__h" id="fence-h">Autobongos runner</h2>
49
- <!-- One panel (task 1004509): a checklist while anything the runner
50
- needs is missing, one status line with the switch once it is all
51
- ticked. Drawn from /autonomy/runners + /autonomy/fence together. -->
52
- <div id="fence-body"><span class="kit-skel"></span></div>
53
- <!-- Every builder's runner (task 1004501): what each one picked to
54
- work, whether you paused it, and whether it is checking in. -->
55
- <div id="fence-builders"></div>
56
- </section>
57
- </section>
58
- </main>
59
-
60
- <footer class="app-foot" id="app-foot"></footer>
61
- </div>
62
- </div>
63
- </div>
64
-
65
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
66
-
67
- <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
68
- <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
69
- <script src="/builders/craft-mode.js?v=2026-11-05-sections"></script>
70
- <script src="/builders/shell.js?v=2026-11-05-sections"></script>
71
- <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
72
- <script src="/builders/palette.js" defer></script>
73
- <script src="/builders/hall-kit.js"></script>
74
- <script src="/builders/gate.js?v=2026-10-25-autobongos"></script>
75
- </body>
76
- </html>
@@ -1,60 +0,0 @@
1
- {
2
- "_": "Autobongos — the owner's runner panel behind requireGovernmentPage, still served at /gate (task 1004859 removed the held-PR list). Measured at three widths in both modes rather than looked at once.",
3
- "page": "/builders/gate",
4
- "surface": "hall-ui",
5
- "stub": {
6
- "prefix": "/builders/"
7
- },
8
- "modeQuery": false,
9
- "_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
10
- "ignoreRequests": [
11
- "/api/(gds|bongos)/me$",
12
- "/api/(gds|bongos)/me/",
13
- "/api/(gds|bongos)/goals",
14
- "/api/(gds|bongos)/inbox/",
15
- "/api/(gds|bongos)/versions",
16
- "/api/(gds|bongos)/builders",
17
- "/api/(gds|bongos)/access-requests",
18
- "/api/(gds|bongos)/autonomy/"
19
- ],
20
- "ignoreConsole": [
21
- "/api/(gds|bongos)/",
22
- "\\b401\\b",
23
- "\\b403\\b",
24
- "\\b404\\b",
25
- "\\b501\\b",
26
- "Unauthorized",
27
- "Forbidden",
28
- "Not Found",
29
- "Not Implemented",
30
- "load failed"
31
- ],
32
- "states": {
33
- "queue": {
34
- "auth": true,
35
- "actions": [
36
- [
37
- "wait",
38
- 700
39
- ]
40
- ],
41
- "expect": {
42
- "visible": [
43
- "#gate-body"
44
- ]
45
- }
46
- },
47
- "out": {
48
- "auth": false
49
- }
50
- },
51
- "reducedMotion": {
52
- "auth": true,
53
- "actions": [
54
- [
55
- "wait",
56
- 600
57
- ]
58
- ]
59
- }
60
- }