@bongos/core 1.19.877 → 1.19.879

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 (45) hide show
  1. package/.bongos-core.json +54 -84
  2. package/docs/copy-inventory.md +195 -283
  3. package/docs/copy-registry.json +254 -1141
  4. package/docs/file-map.md +2 -2
  5. package/docs/module-api-changelog.md +5 -1
  6. package/docs/recipes/autobongos-windows-host.md +26 -1
  7. package/modules/autonomy/digest.js +344 -0
  8. package/modules/hall-ui/module.json +1 -1
  9. package/modules/hall-ui/public/builders.js +1 -13
  10. package/modules/hall-ui/public/dom-utils.js +4 -4
  11. package/modules/hall-ui/public/hall-render.js +2 -18
  12. package/modules/hall-ui/public/index.html +0 -3
  13. package/modules/hall-ui/public/panel.css +8 -9
  14. package/modules/hall-ui/public/settings.css +0 -5
  15. package/modules/hall-ui/public/settings.html +0 -29
  16. package/modules/hall-ui/public/settings.js +1 -26
  17. package/modules/hall-ui/public/shell.js +0 -1
  18. package/modules/hall-ui/public/style.css +0 -44
  19. package/modules/hall-ui/public/watch.html +0 -7
  20. package/modules/hall-ui/public/watch.js +1 -37
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/scripts/gds/autobongos-digest.js +123 -0
  24. package/scripts/gds/autobongos-run.js +7 -0
  25. package/scripts/gds/autobongos-service.ps1 +7 -1
  26. package/scripts/gds/provision-core-upgrade.js +23 -3
  27. package/src/bongos/serve-internal.js +11 -11
  28. package/src/module-api.js +1 -1
  29. package/tests/autonomy_digest.mjs +302 -0
  30. package/tests/core_upgrade_runner.mjs +43 -1
  31. package/tests/government_page_gates.mjs +1 -1
  32. package/tests/hall_landing_boot.mjs +1 -1
  33. package/tests/hall_nav.mjs +1 -1
  34. package/tests/hall_oversight_world.mjs +5 -5
  35. package/tests/hall_page_gate_map.mjs +11 -2
  36. package/tests/hall_settings_world.mjs +21 -18
  37. package/modules/hall-ui/public/box-panel.js +0 -456
  38. package/modules/hall-ui/public/harbor.css +0 -9
  39. package/modules/hall-ui/public/harbor.html +0 -69
  40. package/modules/hall-ui/public/harbor.js +0 -367
  41. package/modules/hall-ui/public/harbor.states.json +0 -77
  42. package/modules/hall-ui/public/pair.css +0 -23
  43. package/modules/hall-ui/public/pair.html +0 -58
  44. package/modules/hall-ui/public/pair.js +0 -132
  45. package/modules/hall-ui/public/pair.states.json +0 -14
@@ -0,0 +1,302 @@
1
+ // tests/autonomy_digest.mjs — the night, readable afterwards (task 1003906).
2
+ //
3
+ // The done-when is "a night produces one digest answering all five questions
4
+ // without anyone opening a log". So the spine of this file is one test that
5
+ // feeds a realistic night through and asserts all five answers are present and
6
+ // correct — because five separately-passing sections can still add up to a page
7
+ // that does not answer the question.
8
+
9
+ import assert from 'node:assert/strict';
10
+ import { test } from 'node:test';
11
+ import { createRequire } from 'node:module';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const { buildDigest, renderDigest, windowsFromGauge, humanDuration } = require('../modules/autonomy/digest.js');
15
+ const cli = require('../scripts/gds/autobongos-digest.js');
16
+
17
+ const T0 = Date.parse('2026-09-19T02:00:00.000Z');
18
+ const at = (m) => new Date(T0 + m * 60000).toISOString();
19
+ const NOW_S = Math.floor((T0 + 8 * 3600e3) / 1000);
20
+
21
+ const night = () => [
22
+ { at: at(0), event: 'boot_reconcile', ok: true, leftover: 0 },
23
+ { at: at(1), event: 'worked', task_id: 111, title: 'A shipped thing', goal_id: 1000119,
24
+ duration_s: 2820, cost_usd: 3.41, verified: true, ledger_shape: 'shipped',
25
+ undetermined_decisions: ['Cached for 60s rather than 5m'] },
26
+ { at: at(50), event: 'worked', task_id: 222, title: 'A stranded thing', goal_id: 1000119,
27
+ duration_s: 5400, cost_usd: 6.8, verified: false, ledger_shape: 'unlanded',
28
+ reason: 'the grade passed and the land never finished', blocker_id: '1000161',
29
+ undetermined_decisions: ['Sorted inactive last', 'Kept the id tie-break', 'Chose ascending'] },
30
+ { at: at(120), event: 'fenced', code: 'grader_unavailable', reason: 'the grader is bypassed' },
31
+ { at: at(121), event: 'fenced', code: 'grader_unavailable', reason: 'the grader is bypassed' },
32
+ { at: at(122), event: 'cadence', mode: 'probing', class: 'transient', consecutive_failures: 3, wait_s: 60 },
33
+ { at: at(200), event: 'nothing_claimable', goals: [1000119],
34
+ skipped: [{ id: 333, goal_id: 1000119, reasons: ['needs_migration'] }] },
35
+ { at: at(201), event: 'hold', reason: 'seven-day pace: 78% used' },
36
+ ];
37
+ const gauge = {
38
+ five_hour: { used_percentage: 41, resets_at: NOW_S + 3600 },
39
+ seven_day: { used_percentage: 78, resets_at: NOW_S + 240000 },
40
+ };
41
+
42
+ // ── the done-when, in one test ─────────────────────────────────────────────────
43
+
44
+ test('one night produces one page answering all five questions', () => {
45
+ const text = renderDigest(buildDigest(night(), { gauge, nowEpochS: NOW_S }));
46
+
47
+ // 1. what shipped — and ONLY what the ledger agreed shipped.
48
+ assert.match(text, /SHIPPED — 1 task/);
49
+ assert.match(text, /111\s+A shipped thing/);
50
+ assert.doesNotMatch(text.split('ON HOLD')[0], /222/, 'a task that ran but did not ship is not "shipped"');
51
+
52
+ // 2. what is on hold, and why.
53
+ assert.match(text, /ON HOLD/);
54
+ assert.match(text, /222\s+ran but did not ship — unlanded/);
55
+ assert.match(text, /blocker 1000161/);
56
+ assert.match(text, /fence: grader_unavailable ×2/);
57
+ assert.match(text, /every one halted/);
58
+ assert.match(text, /capacity: held 1 time/);
59
+
60
+ // 3. what each worker decided that its spec did not determine.
61
+ assert.match(text, /DECIDED FOR YOU — 4 decisions across 2 tasks/);
62
+ assert.match(text, /Cached for 60s rather than 5m/);
63
+ assert.match(text, /sentence the task description should have contained/);
64
+ assert.match(text, /Thinnest spec: 222/);
65
+
66
+ // 4. what it cost.
67
+ assert.match(text, /COST/);
68
+ assert.match(text, /\$10\.21 across 2 worker runs/);
69
+
70
+ // 5. where both limit windows stand.
71
+ assert.match(text, /Five-hour window\s+41% used, resets in 60m/);
72
+ assert.match(text, /Seven-day window\s+78% used/);
73
+ });
74
+
75
+ test('every section prints even when the night was empty', () => {
76
+ // A digest whose quiet sections vanish teaches its reader that absence means
77
+ // "fine" — and then the morning a section is missing because the DATA is
78
+ // missing reads exactly the same.
79
+ const text = renderDigest(buildDigest([], { gauge, nowEpochS: NOW_S }));
80
+ for (const heading of ['SHIPPED', 'ON HOLD', 'DECIDED FOR YOU', 'COST', 'LIMITS', 'RUNNER']) {
81
+ assert.ok(text.includes(heading), `${heading} must print on an empty night`);
82
+ }
83
+ assert.match(text, /nothing shipped/);
84
+ assert.match(text, /nothing is waiting on you/);
85
+ assert.match(text, /no worker had to fill a gap/);
86
+ });
87
+
88
+ // ── the answers, individually ──────────────────────────────────────────────────
89
+
90
+ test('shipped is the LEDGER’s answer, never the worker’s', () => {
91
+ const d = buildDigest([
92
+ { at: at(0), event: 'worked', task_id: 1, verified: true, ledger_shape: 'shipped' },
93
+ { at: at(1), event: 'worked', task_id: 2, verified: false, ledger_shape: 'still_held' },
94
+ { at: at(2), event: 'worked', task_id: 3, outcome: 'claims_shipped', ledger_shape: 'shipped_no_artifact', verified: false },
95
+ ], { nowEpochS: NOW_S });
96
+ assert.deepEqual(d.shipped.map((s) => s.taskId), ['1']);
97
+ assert.deepEqual(d.onHold.ranButDidNotShip.map((r) => r.shape), ['still_held', 'shipped_no_artifact']);
98
+ });
99
+
100
+ test('an unworkable queue is reported as a queue problem, not as breakage', () => {
101
+ const d = buildDigest([{ at: at(0), event: 'nothing_claimable', skipped: [{ id: 9, reasons: ['needs_migration'] }] }], { nowEpochS: NOW_S });
102
+ assert.equal(d.onHold.unworkable.length, 1);
103
+ const text = renderDigest(d);
104
+ assert.match(text, /this is not a runner failure/);
105
+ });
106
+
107
+ test('the decisions list groups by task and names the thinnest spec', () => {
108
+ // The SHAPE is the signal: one task with six decisions is a thin spec, six
109
+ // tasks with one each is a normal night.
110
+ const d = buildDigest([
111
+ { at: at(0), event: 'worked', task_id: 1, verified: true, undetermined_decisions: ['a'] },
112
+ { at: at(1), event: 'worked', task_id: 2, verified: true, undetermined_decisions: ['b', 'c', 'd'] },
113
+ { at: at(2), event: 'worked', task_id: 3, verified: true, undetermined_decisions: [] },
114
+ ], { nowEpochS: NOW_S });
115
+ assert.equal(d.decisionCount, 4);
116
+ assert.equal(d.decisions.length, 2, 'a task with no decisions is not listed');
117
+ assert.equal(d.thinnestSpec.taskId, '2');
118
+ });
119
+
120
+ test('a decision from a task that did NOT ship is still surfaced', () => {
121
+ // These are the ones that matter most: the judgement was made, the work is not
122
+ // in, and the call is sitting in a branch nobody has read.
123
+ const text = renderDigest(buildDigest([
124
+ { at: at(0), event: 'worked', task_id: 7, title: 'T', verified: false, ledger_shape: 'unlanded', undetermined_decisions: ['picked a default'] },
125
+ ], { nowEpochS: NOW_S }));
126
+ assert.match(text, /picked a default/);
127
+ assert.match(text, /\[did not ship\]/);
128
+ });
129
+
130
+ test('an unreported cost is excluded and counted, never coerced to zero', () => {
131
+ const d = buildDigest([
132
+ { at: at(0), event: 'worked', task_id: 1, verified: true, cost_usd: 2.5 },
133
+ { at: at(1), event: 'worked', task_id: 2, verified: true, cost_usd: null },
134
+ { at: at(2), event: 'worked', task_id: 3, verified: true },
135
+ ], { nowEpochS: NOW_S });
136
+ assert.equal(d.cost.totalUsd, 2.5, 'a worker whose cost was not reported must not make the night look cheaper');
137
+ assert.equal(d.cost.reported, 1);
138
+ assert.equal(d.cost.unreported, 2);
139
+ assert.match(renderDigest(d), /2 did not report a cost/);
140
+ });
141
+
142
+ // ── the limit windows ──────────────────────────────────────────────────────────
143
+
144
+ test('a window whose reset has PASSED is reported as stale, not as headroom', () => {
145
+ // "41% used, resets in 0s" invites a decision about headroom that no longer
146
+ // exists: the percentage was true of a window that has since rolled.
147
+ const [five] = windowsFromGauge({ five_hour: { used_percentage: 41, resets_at: NOW_S - 60 } }, NOW_S);
148
+ assert.equal(five.known, false);
149
+ assert.equal(five.stale, true);
150
+ assert.match(five.detail, /has since reset/);
151
+ });
152
+
153
+ test('a window the gauge never reported says so rather than reading as zero', () => {
154
+ const [five, seven] = windowsFromGauge({ seven_day: { used_percentage: 12, resets_at: NOW_S + 999 } }, NOW_S);
155
+ assert.equal(five.known, false);
156
+ assert.equal(five.detail, 'not reported');
157
+ assert.equal(seven.known, true);
158
+ for (const g of [null, undefined, {}, 'nope']) {
159
+ assert.deepEqual(windowsFromGauge(g, NOW_S).map((w) => w.known), [false, false], JSON.stringify(g));
160
+ }
161
+ });
162
+
163
+ // ── the runner footer ──────────────────────────────────────────────────────────
164
+
165
+ test('a night that ended probing says so — "nothing shipped" has two causes', () => {
166
+ const text = renderDigest(buildDigest([
167
+ { at: at(0), event: 'cadence', mode: 'full', class: 'quiet' },
168
+ { at: at(1), event: 'cadence', mode: 'probing', class: 'transient' },
169
+ ], { nowEpochS: NOW_S }));
170
+ assert.match(text, /Ended the night PROBING/);
171
+ });
172
+
173
+ test('an event this digest does not know about is counted, never dropped', () => {
174
+ // A digest that silently ignores what it does not understand is how a new
175
+ // failure mode stays invisible for a week.
176
+ const d = buildDigest([{ at: at(0), event: 'something_new_entirely' }], { nowEpochS: NOW_S });
177
+ assert.equal(d.runner.unrecognised, 1);
178
+ assert.equal(d.runner.byEvent.something_new_entirely, 1);
179
+ assert.match(renderDigest(d), /a kind this digest does not know about/);
180
+ });
181
+
182
+ test('malformed rows never take the night down', () => {
183
+ const d = buildDigest([null, undefined, 42, 'nope', {}, { event: 'worked', verified: true }], { nowEpochS: NOW_S });
184
+ assert.equal(d.shipped.length, 1);
185
+ assert.doesNotThrow(() => renderDigest(d));
186
+ });
187
+
188
+ // ── the CLI half ───────────────────────────────────────────────────────────────
189
+
190
+ test('--since accepts a span and defaults to local midnight', () => {
191
+ const now = new Date('2026-09-19T14:30:00');
192
+ assert.equal(cli.sinceMs('24h', now), now.getTime() - 86400e3);
193
+ assert.equal(cli.sinceMs('90m', now), now.getTime() - 5400e3);
194
+ assert.equal(cli.sinceMs('3d', now), now.getTime() - 3 * 86400e3);
195
+ // Local midnight, because "last night" is a local idea — a UTC cut would split
196
+ // an owner's evening in half depending on where they live.
197
+ const midnight = new Date(now); midnight.setHours(0, 0, 0, 0);
198
+ assert.equal(cli.sinceMs(null, now), midnight.getTime());
199
+ assert.equal(cli.sinceMs('soon', now), null, 'an unparseable span must refuse, not guess');
200
+ });
201
+
202
+ test('a torn last line costs one row, not the night', async () => {
203
+ // A JSONL written by a process that can be killed mid-write has a torn last
204
+ // line more often than never.
205
+ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path');
206
+ const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ab-digest-')), 'runs.jsonl');
207
+ fs.writeFileSync(f, `${JSON.stringify({ at: at(0), event: 'worked', task_id: 1, verified: true })}\n{"at":"x","event":"wor`);
208
+ const { rows, skipped, missing } = cli.readRows(f);
209
+ assert.equal(missing, false);
210
+ assert.equal(rows.length, 1);
211
+ assert.equal(skipped, 1);
212
+ });
213
+
214
+ test('a missing log is reported, not mistaken for a quiet night', async () => {
215
+ const { rows, missing } = cli.readRows('/definitely/not/here/runs.jsonl');
216
+ assert.deepEqual(rows, []);
217
+ assert.equal(missing, true);
218
+ });
219
+
220
+ test('humanDuration reads like a person wrote it', () => {
221
+ assert.equal(humanDuration(45), '45s');
222
+ assert.equal(humanDuration(2820), '47m');
223
+ assert.equal(humanDuration(10080), '2.8h');
224
+ assert.equal(humanDuration(null), null);
225
+ });
226
+
227
+ // ── the grader's three findings (task 1003906) ─────────────────────────────────
228
+
229
+ test('the "did not ship" reason is the LEDGER’s, not the worker’s', () => {
230
+ // THE INVERSION THIS GOAL EXISTS TO UNDO. `row.reason` is verdict.reason — the
231
+ // WORKER's account of itself — and quoting it here means the digest reports the
232
+ // thing being checked rather than the check. A worker that stopped dead needing
233
+ // a decision reports success; the ledger says what actually happened.
234
+ const d = buildDigest([{
235
+ at: at(0), event: 'worked', task_id: 9, title: 'T', verified: false, ledger_shape: 'unlanded',
236
+ reason: 'I finished the work and shipped it',
237
+ ledger_reason: 'the grade passed and the land never finished',
238
+ ledger_remedy: '/merge-mode is the documented manual fallback',
239
+ }], { nowEpochS: NOW_S });
240
+
241
+ assert.equal(d.onHold.ranButDidNotShip[0].reason, 'the grade passed and the land never finished');
242
+ assert.equal(d.onHold.ranButDidNotShip[0].workerSaid, 'I finished the work and shipped it');
243
+
244
+ const text = renderDigest(d);
245
+ assert.match(text, /the grade passed and the land never finished/);
246
+ assert.match(text, /next: \/merge-mode/);
247
+ // The disagreement is the single most useful line on the page.
248
+ assert.match(text, /the worker said: I finished the work and shipped it/);
249
+ });
250
+
251
+ test('the worker’s account is printed only when it disagrees', () => {
252
+ // A worker that agreed with the ledger tells the reader nothing twice.
253
+ const same = 'the grade parked this';
254
+ const text = renderDigest(buildDigest([{
255
+ at: at(0), event: 'worked', task_id: 9, verified: false, ledger_shape: 'ungraded', reason: same, ledger_reason: same,
256
+ }], { nowEpochS: NOW_S }));
257
+ assert.equal((text.match(/the grade parked this/g) || []).length, 1);
258
+ assert.doesNotMatch(text, /the worker said:/);
259
+ });
260
+
261
+ test('a row from before ledger_reason existed still shows something', () => {
262
+ const d = buildDigest([{ at: at(0), event: 'worked', task_id: 9, verified: false, ledger_shape: 'unlanded', reason: 'older row' }], { nowEpochS: NOW_S });
263
+ assert.equal(d.onHold.ranButDidNotShip[0].reason, 'older row');
264
+ });
265
+
266
+ test('the digest calls a gauge stale on the SAME rule the runner holds on', () => {
267
+ // A digest that applied a laxer test would print live headroom about a window
268
+ // the runner had already decided it could not vouch for — two surfaces
269
+ // disagreeing about one file, with the reader believing the wrong one.
270
+ const gaugeMod = require('../modules/autonomy/gauge.js');
271
+ const tooOld = gaugeMod.DEFAULT_STALE_AFTER_S + 60;
272
+
273
+ const [fresh] = windowsFromGauge(
274
+ { five_hour: { used_percentage: 41, resets_at: NOW_S + 7200, observed_at: NOW_S - 60 } }, NOW_S);
275
+ assert.equal(fresh.known, true, 'a recent reading is usable');
276
+
277
+ const [stale] = windowsFromGauge(
278
+ { five_hour: { used_percentage: 41, resets_at: NOW_S + 7200, observed_at: NOW_S - tooOld } }, NOW_S);
279
+ assert.equal(stale.known, false);
280
+ assert.equal(stale.stale, true);
281
+ assert.match(stale.detail, /older than the runner trusts/);
282
+ assert.doesNotMatch(stale.detail, /resets in/, 'never offer headroom from a reading the runner rejected');
283
+ });
284
+
285
+ test('a window with no observed_at falls back to the file’s written_at', () => {
286
+ // One window can go unreported while the other is carried forward — the exact
287
+ // shape gauge.js reads.
288
+ const gaugeMod = require('../modules/autonomy/gauge.js');
289
+ const old = NOW_S - gaugeMod.DEFAULT_STALE_AFTER_S - 60;
290
+ const [w] = windowsFromGauge({ written_at: old, five_hour: { used_percentage: 41, resets_at: NOW_S + 7200 } }, NOW_S);
291
+ assert.equal(w.stale, true);
292
+ });
293
+
294
+ test('the period bounds survive a log too long to spread as arguments', () => {
295
+ // `Math.min(...times)` throws past the engine's argument cap, and a reporting
296
+ // CLI that dies on a big log fails exactly when the log is worth reading.
297
+ const many = Array.from({ length: 200000 }, (_, i) => ({ at: at(i % 1000), event: 'cadence', mode: 'full' }));
298
+ let d;
299
+ assert.doesNotThrow(() => { d = buildDigest(many, { nowEpochS: NOW_S }); });
300
+ assert.equal(d.period.rows, 200000);
301
+ assert.equal(d.period.fromMs, T0);
302
+ });
@@ -28,6 +28,8 @@
28
28
 
29
29
  import { strict as assert } from 'node:assert';
30
30
  import { createRequire } from 'node:module';
31
+ import fs from 'node:fs';
32
+ import path from 'node:path';
31
33
 
32
34
  const require = createRequire(import.meta.url);
33
35
  const R = require('../scripts/gds/provision-core-upgrade.js');
@@ -46,13 +48,19 @@ const t = async (name, fn) => {
46
48
  // instant. `apply:true` because the interesting behaviour is the applied path.
47
49
  function makeDeps({ shape = 'control-plane', served = '1.19.760', available = ['1.19.758', '1.19.760', '1.19.762'], installed = '1.19.760', queries = [] } = {}) {
48
50
  const ran = [];
49
- const exec = (cmd) => {
51
+ // task 1004053: the CWD is half of what makes a relative CLI path resolve, and the
52
+ // bug that shipped was invisible to every assertion that read only the command
53
+ // string. Captured alongside, so a test can check the pair.
54
+ const ranWith = [];
55
+ const exec = (cmd, opts) => {
50
56
  ran.push(cmd);
57
+ ranWith.push({ cmd, cwd: opts && opts.cwd });
51
58
  if (/\/version/.test(cmd)) return { ok: true, stdout: served == null ? '' : JSON.stringify({ coreVersion: served }) };
52
59
  return { ok: true, stdout: '' };
53
60
  };
54
61
  return {
55
62
  ran,
63
+ ranWith,
56
64
  queries,
57
65
  db: { query: async (sql, params) => { queries.push({ sql, params }); return { rows: [] }; } },
58
66
  provisioning: { recordEvent: async () => {}, clearErrorNote: async () => {} },
@@ -245,6 +253,40 @@ await t('drives upgrade.js with the registry, the health gate, the version read-
245
253
  assert.ok(cmd.includes('--commit-pin'));
246
254
  });
247
255
 
256
+ await t('the upgrade CLI is run somewhere it ACTUALLY EXISTS — the bug that broke every standalone tenant (task 1004053)', async () => {
257
+ // The whole failure was a path that resolved nowhere:
258
+ // Error: Cannot find module '/srv/<base>/<slug>/scripts/gds/upgrade.js'
259
+ // No assertion about FLAGS could have caught it, so this one resolves the composed
260
+ // cwd + relative CLI against the real filesystem, for every shape the door admits.
261
+ for (const shape of ['control-plane', 'standalone', 'co-tenant']) {
262
+ const d = makeDeps({ served: '1.19.762' });
263
+ await R.coreUpgradeInstance(inst({ hosting_shape: shape, slug: 'hermeslines-marketing', port: 3011 }), d, intent());
264
+ const call = d.ranWith.find((c) => /upgrade\.js/.test(c.cmd));
265
+ assert.ok(call, `${shape}: it ran the upgrade CLI`);
266
+ const rel = (call.cmd.match(/node (\S*scripts\/gds\/upgrade\.js)/) || [])[1];
267
+ assert.ok(rel, `${shape}: the command names the CLI`);
268
+ assert.ok(fs.existsSync(path.join(call.cwd, rel)),
269
+ `${shape}: ${path.join(call.cwd, rel)} must exist — this is the assertion the original bug would have failed`);
270
+ }
271
+ });
272
+
273
+ await t('a standalone tenant is NAMED with --instance, because the CLI no longer runs inside it', async () => {
274
+ const d = makeDeps({ served: '1.19.762' });
275
+ await R.coreUpgradeInstance(inst({ hosting_shape: 'standalone', slug: 'hermeslines-marketing', port: 3011 }), d, intent());
276
+ const cmd = d.ran.find((c) => /upgrade\.js/.test(c));
277
+ assert.match(cmd, /--instance \S*hermeslines-marketing/, 'the target is named explicitly');
278
+ assert.ok(cmd.includes('--service hermeslines-marketing'), 'and its own unit is still the one restarted');
279
+ });
280
+
281
+ await t('the control-plane row carries NO --instance — its target IS the checkout the CLI runs in', async () => {
282
+ // Naming it would be harmless but wrong, and the shapes that already worked must not
283
+ // change behaviour on the way to fixing the one that never did.
284
+ const d = makeDeps({ served: '1.19.762' });
285
+ await R.coreUpgradeInstance(inst(), d, intent());
286
+ const cmd = d.ran.find((c) => /upgrade\.js/.test(c));
287
+ assert.ok(!cmd.includes('--instance'), 'unchanged for the platform itself');
288
+ });
289
+
248
290
  await t('the health URL and the version URL are LOOPBACK — the apex is behind basic_auth and a CDN (ADR 0293 D6)', async () => {
249
291
  const d = makeDeps();
250
292
  await R.coreUpgradeInstance(inst(), d, intent());
@@ -64,7 +64,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
64
64
  // unaffected — it held right up to the deletion.
65
65
  //
66
66
  // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
67
- // the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
67
+ // the monitoring pages (/watch, /gate) now open to the working rank. That gate
68
68
  // was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
69
69
  // were first written against — and was renamed once the floor moved; the audience here,
70
70
  // not the name, is the contract.
@@ -91,7 +91,7 @@ async function boot({ meStatus }) {
91
91
  wirePager: () => {},
92
92
  },
93
93
  OTBHallRender: () => ({
94
- SETUP_CONFIG_KEY: 'k', analyticsState: {}, lbState: {}, onboardingBox: () => makeEl(),
94
+ SETUP_CONFIG_KEY: 'k', analyticsState: {}, lbState: {},
95
95
  shipState: {}, shipsPager: { page: 1 }, welcomeDismissalKey: () => 'k',
96
96
  mountHallWidgets: spy('mountHallWidgets'),
97
97
  renderLanding: spy('renderLanding'),
@@ -287,7 +287,7 @@ test('sidebar groups read by audience, in order', () => {
287
287
  // 'deploy' (task 1003159, ADR 0293) is the SECOND archon item, beside 'government':
288
288
  // its gate is the `core.pin.move` atom, archon-floor and system:true. It sits with
289
289
  // the steering surfaces because that is what it is — the owner's third release gate.
290
- assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions', 'project-settings', 'deploy', 'government']);
290
+ assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'gate', 'sessions', 'project-settings', 'deploy', 'government']);
291
291
  });
292
292
 
293
293
  test('Economy lives in Docs and points at the reward-schedule page', () => {
@@ -1,5 +1,5 @@
1
1
  // tests/hall_oversight_world.mjs — the oversight family in the chrome world
2
- // (task 1003469, goal 1000081): government, watch, harbor and gate share one
2
+ // (task 1003469, goal 1000081): government, watch and gate share one
3
3
  // archetype sheet (oversight.css) the way the records share record.css and the
4
4
  // reading rooms room.css. These are static pins over the shipped source: the
5
5
  // family sheet is linked and spends only the fifteen, every page renders the
@@ -17,7 +17,7 @@ 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', 'harbor', 'gate'];
20
+ const PAGES = ['government', 'watch', 'gate'];
21
21
 
22
22
  test('every oversight page links the family sheet, before its own', () => {
23
23
  for (const p of PAGES) {
@@ -76,8 +76,8 @@ test('Government renders the rank description the create form collects', () => {
76
76
  assert.match(read('government.html'), /id="gov-create-desc"/, 'the form still collects it');
77
77
  });
78
78
 
79
- test('Harbor and Gate carry no coloured band: state is a pill', () => {
80
- for (const p of ['harbor', 'gate']) {
79
+ test('Gate carries no coloured band: state is a pill', () => {
80
+ for (const p of ['gate']) {
81
81
  assert.ok(!/__roof|__seal/.test(read(`${p}.js`)), `${p}.js emits no roof or seal band`);
82
82
  assert.ok(!/__roof|__seal/.test(read(`${p}.css`)), `${p}.css has no band rule`);
83
83
  assert.match(read(`${p}.js`), /fact-pill fact-pill--/, `${p}.js paints state as a .fact-pill`);
@@ -91,7 +91,7 @@ test('the shared confirm modal sits on --z-modal, closed by one hairline and no
91
91
  const modal = css.slice(css.indexOf('.ov-modal {'), css.indexOf('.ov-modal .scroll__h'));
92
92
  assert.ok(!/box-shadow/.test(modal), 'no shadow under the dialog');
93
93
  assert.match(modal, /border: var\(--line\) solid var\(--rule\)/, 'one rule hairline');
94
- for (const p of ['harbor', 'gate']) {
94
+ for (const p of ['gate']) {
95
95
  assert.match(read(`${p}.js`), /className = 'ov-modal-bg'/, `${p}.js opens the family modal`);
96
96
  assert.ok(!/-modal-bg/.test(read(`${p}.css`)), `${p}.css no longer styles a modal of its own`);
97
97
  }
@@ -44,7 +44,6 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
44
44
  const ORACLE = {
45
45
  // Any signed-in builder — no rank floor.
46
46
  '/builders/work': 'requireBuilderPage',
47
- '/builders/pair': 'requireBuilderPage',
48
47
  '/builders/settings': 'requireBuilderPage',
49
48
  '/builders/atlas': 'requireBuilderPage',
50
49
  '/builders/modules': 'requireBuilderPage',
@@ -74,7 +73,6 @@ const ORACLE = {
74
73
 
75
74
  // Metic+ operator surfaces (the Governance nav group).
76
75
  '/builders/watch': 'requireGovernmentPage',
77
- '/builders/harbor': 'requireGovernmentPage',
78
76
  '/builders/gate': 'requireGovernmentPage',
79
77
  '/builders/sessions': 'requireGovernmentPage',
80
78
  // The five fundamentals of the project (task 1003280, goal 1000072). Same tier as
@@ -155,6 +153,14 @@ const ORACLE = {
155
153
  // catch. GONE_PAGES below keeps the row honest if the file ever returns.
156
154
  '/builders/my-projects': null,
157
155
 
156
+ // Gone with the dev box (task 1003890, goal 1000091). Harbor was the Archon
157
+ // view of the box fleet and pair the device-pairing page; the fleet was turned
158
+ // off at the control plane by task 1003886, so both pages drove an API that no
159
+ // longer answers. Their gates left serve-internal.js with them — a gate in
160
+ // front of nothing is the dead-guard shape the check below exists to catch.
161
+ '/builders/harbor': null,
162
+ '/builders/pair': null,
163
+
158
164
  // Gone too, and for a reason worth keeping in front of the next reader: this
159
165
  // was a bespoke refusal PAGE ("not yet — ship three tasks to graduate"), the
160
166
  // deniedTo of the thetes+ gate. Task 1002613 replaced that whole idea with a
@@ -278,6 +284,9 @@ const GONE_PAGES = {
278
284
  '/builders/my-projects': 'task 1003308 — the hub owns "my projects"',
279
285
  // Task 1003450 — the shell's ?denied= banner replaced the bespoke refusal page.
280
286
  '/builders/not-ready': 'task 1003450 — the shell renders the refusal from ?denied=',
287
+ // Task 1003890 — the dev-box deprecation removed the fleet these two drove.
288
+ '/builders/harbor': 'task 1003890 — the dev box is gone, so its Archon view is too',
289
+ '/builders/pair': 'task 1003890 — the dev box is gone, so its pairing page is too',
281
290
  };
282
291
 
283
292
  test('a row that is null because the page left the hall serves no page', () => {
@@ -1,5 +1,6 @@
1
1
  // tests/hall_settings_world.mjs — the PANEL family in the chrome world
2
- // (task 1003308, goal 1000081): settings, profile, pair.
2
+ // (task 1003308, goal 1000081): settings and profile. Pair left with the dev
3
+ // box (task 1003890).
3
4
  //
4
5
  // tests/hall_tokens.mjs pins the TIER these pages spend; this pins how they
5
6
  // spend it. Everything here is a fact that is silent in a browser: the page
@@ -9,9 +10,9 @@
9
10
  //
10
11
  // What it locks:
11
12
  // * ONE ARCHETYPE SHEET. panel.css holds the row, the switch, the field, the
12
- // chip control and the family's voices; the four page sheets hold only what
13
- // one page has. Every page links panel.css BEFORE its own sheet.
14
- // * THE ONE h1. None of the five had one — the hall predates the floor — so
13
+ // chip control and the family's voices; the page sheets hold only what one
14
+ // page has. Every page links panel.css BEFORE its own sheet.
15
+ // * THE ONE h1. None of them had one — the hall predates the floor — so
15
16
  // every render failed one-header-main-h1. The h1 is the page's name in the
16
17
  // room voice, and it lives in the static markup so the DOM has it in every
17
18
  // state, not only the happy path.
@@ -26,14 +27,14 @@
26
27
  // * WHITE ON THE ACCENT, AND THE ONE EXCEPTION. Eight literals across three
27
28
  // sheets are gone; the Discord blurple pair stays, because it is Discord's
28
29
  // mark and not the world's, and it is named as the exception in a comment.
29
- // * THE COMPAT ALIASES. Five sheets, zero aliases spent.
30
+ // * THE COMPAT ALIASES. Every sheet in the family, zero aliases spent.
30
31
  //
31
32
  // Reproduce the renders these came from (the family needs the fixture harness —
32
33
  // every panel is filled from /me/* reads the kit stub does not carry):
33
34
  // node scripts/hall-preview/server.js --port 4621 --fixture-me
34
35
  // node modules/ui-design/kit/render.js --states modules/hall-ui/public/settings.states.json \
35
36
  // --base-in http://builders.localhost:4621 --base-out http://builders.localhost:4621
36
- // (and profile / pair .states.json the same way)
37
+ // (and profile .states.json the same way)
37
38
 
38
39
  import { test } from 'node:test';
39
40
  import assert from 'node:assert/strict';
@@ -48,9 +49,10 @@ const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
48
49
 
49
50
  // not-ready was the fourth until task 1003450 deleted the page — see
50
51
  // tests/hall_page_gate_map.mjs (GONE_PAGES) for why, and for the row that stops
51
- // it coming back ungated. The archetype is unaffected: three pages still spend it.
52
- const PAGES = ['settings', 'profile', 'pair'];
53
- const SHEETS = ['panel.css', 'settings.css', 'pair.css', 'profile.css'];
52
+ // it coming back ungated; pair left the same way in task 1003890. The archetype
53
+ // is unaffected: the pages that remain still spend it.
54
+ const PAGES = ['settings', 'profile'];
55
+ const SHEETS = ['panel.css', 'settings.css', 'profile.css'];
54
56
 
55
57
  const panelCss = read('panel.css');
56
58
  const settingsCss = read('settings.css');
@@ -94,7 +96,7 @@ test('the archetype holds the row, the switch, the field and the voices; the pag
94
96
  for (const sel of ['.prow', '.sw', '.sw__track', '.sw__knob', '.field-label', '.pinput', '.chip-ctl', '.panel-note', '.panel-status', '.gate']) {
95
97
  assert.ok(ruleOf(panelCss, sel), `panel.css defines ${sel}`);
96
98
  }
97
- for (const sheet of ['settings.css', 'pair.css', 'profile.css']) {
99
+ for (const sheet of ['settings.css', 'profile.css']) {
98
100
  const css = read(sheet);
99
101
  for (const sel of ['.prow', '.sw', '.sw__track', '.pinput', '.chip-ctl']) {
100
102
  assert.equal(ruleOf(css, sel), null, `${sheet} must not redefine ${sel} — that is how the family would drift`);
@@ -104,7 +106,7 @@ test('the archetype holds the row, the switch, the field and the voices; the pag
104
106
 
105
107
  // ---- 2. the one h1, in the static markup ----------------------------------
106
108
 
107
- test('each of the five pages has exactly one h1, in the family\'s page head', () => {
109
+ test('each page in the family has exactly one h1, in the family\'s page head', () => {
108
110
  for (const p of PAGES) {
109
111
  const html = read(`${p}.html`).replace(/<!--[\s\S]*?-->/g, '');
110
112
  const h1s = html.match(/<h1\b/g) || [];
@@ -301,7 +303,7 @@ test('no sheet in the family spends a compat alias', () => {
301
303
  });
302
304
 
303
305
  test('white on the accent is gone, and the Discord mark is the one named exception', () => {
304
- for (const sheet of ['panel.css', 'pair.css', 'profile.css']) {
306
+ for (const sheet of ['panel.css', 'profile.css']) {
305
307
  const bare = stripComments(read(sheet));
306
308
  assert.doesNotMatch(bare, /#[0-9a-f]{3,8}\b/i, `${sheet} carries no colour literal`);
307
309
  assert.doesNotMatch(bare, /:\s*white\b/i, `${sheet} carries no named white`);
@@ -318,16 +320,17 @@ test('white on the accent is gone, and the Discord mark is the one named excepti
318
320
  assert.match(selector, /\.settings-btn--discord/, `${lit} is spent only on the Discord button`);
319
321
  }
320
322
  assert.match(settingsCss, /THIRD-PARTY MARK, KEPT ON PURPOSE/, 'the exception is named in a comment so a literal sweep leaves it alone');
321
- // the save/accent buttons take --accent-on. `.pair-approve` and `.craft-save`
322
- // are members of the base pill rule AND the save rule, so the save rule is
323
- // found by its own name and the two are asserted as its members.
323
+ // the save/accent buttons take --accent-on. `.craft-save` is a member of the
324
+ // base pill rule AND the save rule, so the save rule is found by its own name
325
+ // and the member is asserted against it. (`.pair-approve` was its sibling here
326
+ // until task 1003890 removed the pair page.)
324
327
  const save = ruleOf(panelCss, '.pbtn--save');
325
328
  assert.ok(save, '.pbtn--save has a rule');
326
329
  assert.match(save, /background:\s*var\(--accent\)/);
327
330
  assert.match(save, /color:\s*var\(--accent-on\)/, 'the dark ink for the salmon — white on it fails AA');
328
331
  const saveSelector = stripComments(panelCss).match(/([^{}]*\.pbtn--save[^{}]*)\{/);
329
332
  assert.ok(saveSelector, 'the save rule\'s selector list is readable');
330
- for (const member of ['.craft-save', '.pair-approve']) {
333
+ for (const member of ['.craft-save']) {
331
334
  assert.ok(saveSelector[1].split(',').map((s) => s.trim()).includes(member), `${member} is the accent door on its page`);
332
335
  }
333
336
  });
@@ -394,14 +397,14 @@ test('the profile identity band is a MODIFIER on the shell\'s .profile, never a
394
397
 
395
398
  // ---- 6. the floors are reproducible ------------------------------------------
396
399
 
397
- test('all five pages have a states file, so the floors are reproducible', () => {
400
+ test('every page in the family has a states file, so the floors are reproducible', () => {
398
401
  for (const p of PAGES) {
399
402
  const json = JSON.parse(read(`${p}.states.json`));
400
403
  assert.equal(json.surface, 'hall-ui', `${p}.states.json names its surface`);
401
404
  assert.ok(Object.keys(json.states).length >= 2, `${p}.states.json renders more than the default state`);
402
405
  }
403
406
  assert.ok(Object.keys(JSON.parse(read('settings.states.json')).states).length >= 4, 'settings renders every rail group');
404
- for (const fx of ['me__sessions', 'auth__app-pair__info']) {
407
+ for (const fx of ['me__sessions']) {
405
408
  assert.ok(fs.existsSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', `${fx}.json`)),
406
409
  `the harness serves ${fx} — without it the page only ever rendered its empty state`);
407
410
  }