@bongos/core 1.24.94 → 1.24.96

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 (56) hide show
  1. package/.bongos-core.json +143 -43
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +2 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0405-each-character-has-one-colour-and-the-visor-wears-it.md +31 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +49 -1
  10. package/docs/api-reference.md +3 -2
  11. package/docs/copy-inventory.md +12 -12
  12. package/docs/copy-registry.json +12 -12
  13. package/docs/design/characters/v2/artisan-visor.png +0 -0
  14. package/docs/design/characters/v2/artisan.png +0 -0
  15. package/docs/design/characters/v2/artist-visor.png +0 -0
  16. package/docs/design/characters/v2/artist.png +0 -0
  17. package/docs/design/characters/v2/draw-characters.js +465 -0
  18. package/docs/design/characters/v2/patron-visor.png +0 -0
  19. package/docs/design/characters/v2/patron.png +0 -0
  20. package/docs/design/characters/v2/sheet.html +114 -0
  21. package/docs/module-api-changelog.md +4 -0
  22. package/docs/page-inventory.json +2 -0
  23. package/docs/page-readings.json +35 -35
  24. package/modules/autonomy/db.js +48 -0
  25. package/modules/autonomy/routes/autonomy.js +15 -0
  26. package/modules/government/catalog.js +1 -1
  27. package/modules/hall-ui/public/characters/artisan-face.png +0 -0
  28. package/modules/hall-ui/public/characters/artisan-visor.png +0 -0
  29. package/modules/hall-ui/public/characters/artisan.png +0 -0
  30. package/modules/hall-ui/public/characters/artist-face.png +0 -0
  31. package/modules/hall-ui/public/characters/artist-visor.png +0 -0
  32. package/modules/hall-ui/public/characters/artist.png +0 -0
  33. package/modules/hall-ui/public/characters/patron-face.png +0 -0
  34. package/modules/hall-ui/public/characters/patron-visor.png +0 -0
  35. package/modules/hall-ui/public/characters/patron.png +0 -0
  36. package/modules/hall-ui/public/craft-mode.js +15 -4
  37. package/modules/hall-ui/public/home-city-draw.js +24 -2
  38. package/modules/hall-ui/public/home-city.css +15 -0
  39. package/modules/hall-ui/public/home-city.js +3 -1
  40. package/modules/hall-ui/public/home-runner.js +248 -0
  41. package/modules/hall-ui/public/index.html +8 -5
  42. package/modules/hall-ui/public/style.css +18 -5
  43. package/modules/hall-ui/public/visor.css +47 -5
  44. package/modules/hall-ui/public/visor.js +71 -5
  45. package/modules/hall-ui/records/characters.md +19 -0
  46. package/modules/hall-ui/records/home-runner.md +19 -0
  47. package/modules/hall-ui/records/visor.md +7 -0
  48. package/package-lock.json +2 -2
  49. package/package.json +1 -1
  50. package/release-notes.json +12 -0
  51. package/scripts/hall-preview/server.js +4 -0
  52. package/src/module-api.js +1 -1
  53. package/tests/autonomy_runner_goal_stats.mjs +92 -0
  54. package/tests/character_creation.mjs +44 -7
  55. package/tests/hall_visor.mjs +48 -2
  56. package/tests/home_runner.mjs +115 -0
@@ -0,0 +1,248 @@
1
+ // modules/hall-ui/public/home-runner.js — your Autobongos runner on Home's city
2
+ // (task 1005243, goal 1000142).
3
+ //
4
+ // WHY. Owner direction, 2026-11-12: running your runner should feel like putting a
5
+ // builder on a building in a base-building game. In game mode you tap a tower (a
6
+ // goal), the visor offers "Send runner here", and the tower then carries your
7
+ // runner's marker; tap it again and the visor shows what it has done there and
8
+ // "Recall runner". The owner chose that a runner works ONE tower at a time, so
9
+ // sending it sets your goal picks to that one goal.
10
+ //
11
+ // THE RULES IT KEEPS.
12
+ // - ADR 0403: the scene is the view and the visor is the control. The tower only
13
+ // gets picked; every button is on the glass (OTBVisor.focus).
14
+ // - The server decides who may run a runner: GET /autonomy/fence answers
15
+ // scope.may_run, and only then is a tower pickable. Signed out, a rank without
16
+ // `autonomy.run`, or classic mode: towers stay plain links and nothing is new.
17
+ // - Every write is a route Settings already uses: PUT /autonomy/me/goals and
18
+ // POST /autonomy/runners/:host/run. The numbers come from GET
19
+ // /autonomy/me/goals/:id/stats (the ledger: runner claims shipped there, the
20
+ // tokens their sessions took, the tasks still ready).
21
+ // - The device's face (working, resting until, off…) is runner-button.js's
22
+ // buttonState, the same decision the Settings card draws.
23
+ //
24
+ // The decision (cardFor, stateOf) is PURE and exported for node tests; the wiring
25
+ // below it only runs in a browser.
26
+ (function (root) {
27
+ 'use strict';
28
+
29
+ const RB = () => (root && root.OTBRunnerButton) || (typeof require === 'function' ? require('./runner-button.js') : null);
30
+
31
+ // ---------- pure ----------
32
+
33
+ // The device that speaks for "your runner": one that is working, else one that is
34
+ // switched on, else the most recently seen (the server's order).
35
+ function leadDevice(runners) {
36
+ const list = Array.isArray(runners) ? runners.filter(Boolean) : [];
37
+ return list.find((r) => r.health && r.health.workingTaskId) ||
38
+ list.find((r) => r.run_requested !== false) || list[0] || null;
39
+ }
40
+ function anyOn(runners) { return (Array.isArray(runners) ? runners : []).some((r) => r && r.run_requested !== false); }
41
+
42
+ // The goal the runner is sent to: exactly one pick. Several picks (made in Settings)
43
+ // is not one tower, so no tower carries the marker until the builder sends it.
44
+ function assignedGoal(fence) {
45
+ const g = fence && fence.scope && Array.isArray(fence.scope.goals) ? fence.scope.goals : [];
46
+ return g.length === 1 ? String(g[0]) : null;
47
+ }
48
+
49
+ // The marker's state, from the device's face: working | resting | starting | ready | off.
50
+ function stateOf({ runners, fence, nowMs } = {}) {
51
+ const d = leadDevice(runners);
52
+ if (!d || !anyOn(runners)) return 'off';
53
+ const b = RB().buttonState({ runner: d, fence, nowMs });
54
+ if (d.health && d.health.workingTaskId && b.status === 'Running') return 'working';
55
+ if (/^Resting/.test(b.status)) return 'resting';
56
+ if (b.status === 'Starting…' || b.status === 'Updating…') return 'starting';
57
+ if (b.status === 'Running' || b.status === 'Ready') return 'ready';
58
+ return 'off';
59
+ }
60
+
61
+ // 1.2M tokens, 340k tokens, 900 tokens.
62
+ function tokensText(n) {
63
+ const v = Number(n) || 0;
64
+ if (v >= 1e6) return `${(v / 1e6).toFixed(v >= 1e7 ? 0 : 1).replace(/\.0$/, '')}M tokens`;
65
+ if (v >= 1e3) return `${Math.round(v / 1e3)}k tokens`;
66
+ return `${Math.round(v)} tokens`;
67
+ }
68
+ // "about 40 min", "about 3 h", "about 2 days": an estimate, so rounded like one.
69
+ function aboutText(seconds) {
70
+ const s = Number(seconds);
71
+ if (!Number.isFinite(s) || s <= 0) return '';
72
+ const min = s / 60;
73
+ if (min < 60) return `about ${Math.max(5, Math.round(min / 5) * 5)} min`;
74
+ const h = min / 60;
75
+ if (h < 36) return `about ${Math.round(h)} h`;
76
+ return `about ${Math.round(h / 24)} days`;
77
+ }
78
+ function plural(n, one, many) { return `${n} ${n === 1 ? one : many}`; }
79
+
80
+ // What the visor's card says for one tower. Returns null when the viewer has no
81
+ // runner to send (the tower then stays a plain link), else
82
+ // { title, status: { text, tone }, lines: [string], lead: { label, act } | null }
83
+ // act: 'send' | 'recall' | 'setup' | 'open'.
84
+ function cardFor({ goal, runners, fence, stats, nowMs = Date.now(), titleOf } = {}) {
85
+ if (!goal || !fence || !fence.scope || fence.scope.may_run !== true) return null;
86
+ const id = String(goal.id);
87
+ const title = goal.title || `Goal ${id}`;
88
+ const nameOf = (g) => (typeof titleOf === 'function' && titleOf(g)) || `goal ${g}`;
89
+ const list = Array.isArray(runners) ? runners : [];
90
+ const st = stats || null;
91
+ const done = st ? Number(st.tasks_done) || 0 : 0;
92
+ const left = st ? Number(st.tasks_left) || 0 : null;
93
+ const eta = st && left ? aboutText(left * (Number(st.avg_task_seconds) || 0)) : '';
94
+ const leftLine = left == null ? null : left === 0 ? 'Nothing ready to build here right now.' : `${plural(left, 'task', 'tasks')} ready${eta ? ` · ${eta} for your runner` : ''}`;
95
+ const doneLine = st && done ? `${plural(done, 'task', 'tasks')} done here · ${tokensText(st.tokens)}` : null;
96
+
97
+ if (!list.length) {
98
+ return { title, status: { text: 'No runner yet', tone: 'neutral' },
99
+ lines: ['Set it up on your PC once, then send it to any tower.'], lead: { label: 'Set up a runner', act: 'setup' } };
100
+ }
101
+ const allowed = fence.project && Array.isArray(fence.project.goals) ? fence.project.goals.map(String) : [];
102
+ if (!allowed.includes(id)) {
103
+ return { title, status: { text: 'Closed to runners', tone: 'neutral' },
104
+ lines: ['The project has not opened this goal to runners yet.'], lead: { label: 'Open the goal', act: 'open' } };
105
+ }
106
+ const here = assignedGoal(fence) === id;
107
+ const on = anyOn(list);
108
+ if (here && on) {
109
+ const b = RB().buttonState({ runner: leadDevice(list), fence, nowMs });
110
+ const face = b.status ? `${b.status}${b.countdown ? ` · ${b.countdown}` : ''}` : 'Off';
111
+ return { title, status: { text: face, tone: b.tone || 'neutral' },
112
+ lines: [b.sub, doneLine, leftLine].filter(Boolean), lead: { label: 'Recall runner', act: 'recall' } };
113
+ }
114
+ const where = on && assignedGoal(fence) ? `Your runner is at ${nameOf(assignedGoal(fence))}` :
115
+ on ? 'Your runner is working on several goals' : 'Your runner is resting';
116
+ return { title, status: { text: here ? 'Recalled' : where, tone: 'neutral' },
117
+ lines: [doneLine, leftLine].filter(Boolean), lead: { label: 'Send runner here', act: 'send' } };
118
+ }
119
+
120
+ const api = { cardFor, stateOf, assignedGoal, leadDevice, tokensText, aboutText };
121
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
122
+ if (!root || typeof document === 'undefined') return;
123
+ root.OTBHomeRunnerLib = api;
124
+
125
+ // ---------- the wiring (browser only) ----------
126
+ const API = '/api/bongos';
127
+ const POLL_MS = 15000;
128
+ let fence = null, runners = [], picked = null, stats = null, busy = false, error = '', timer = null;
129
+ let client = null;
130
+ function http() {
131
+ if (!client && root.BongosClient) client = root.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
132
+ return client;
133
+ }
134
+ async function req(method, path, body) {
135
+ const c = http();
136
+ if (!c) return null;
137
+ try { return await c.request(method, API + path, body ? { body } : undefined); } catch (_) { return null; }
138
+ }
139
+ function game() { return document.documentElement.dataset.game === 'on'; }
140
+ function mayRun() { return !!(fence && fence.scope && fence.scope.may_run === true); }
141
+ function active() { return game() && mayRun() && !!root.OTBVisor && typeof root.OTBVisor.focus === 'function'; }
142
+ function towerOf(id) { return document.querySelector(`.home-city .hc-tower[data-goal="${CSS.escape(String(id))}"]`); }
143
+ function titleOf(id) { const t = towerOf(id); return t ? t.getAttribute('data-tip') : null; }
144
+
145
+ // What home-city.js draws: the marker on the assigned tower, and the picked tower.
146
+ function marker() {
147
+ if (!active()) return null;
148
+ const goal = assignedGoal(fence);
149
+ return goal ? { goal, state: stateOf({ runners, fence, nowMs: Date.now() }) } : null;
150
+ }
151
+ function changed() { document.dispatchEvent(new CustomEvent('otb:home-runner')); }
152
+
153
+ async function readState() {
154
+ const [f, r] = await Promise.all([req('GET', '/autonomy/fence'), req('GET', '/autonomy/runners')]);
155
+ fence = f && f.ok && f.data ? f.data : null;
156
+ runners = r && r.ok && r.data && Array.isArray(r.data.runners) ? r.data.runners : [];
157
+ }
158
+ async function readStats() {
159
+ if (!picked) return;
160
+ const s = await req('GET', `/autonomy/me/goals/${encodeURIComponent(picked)}/stats`);
161
+ stats = s && s.ok && s.data ? s.data : null;
162
+ }
163
+
164
+ function show() {
165
+ const V = root.OTBVisor;
166
+ if (!V || typeof V.focus !== 'function') return;
167
+ if (!picked || !active()) { V.focus(null); return; }
168
+ const card = cardFor({ goal: { id: picked, title: titleOf(picked) }, runners, fence, stats, nowMs: Date.now(), titleOf });
169
+ if (!card) { V.focus(null); return; }
170
+ const lead = card.lead ? { label: busy ? (card.lead.act === 'recall' ? 'Recalling…' : 'Sending…') : card.lead.label, busy, run: () => act(card.lead.act) } : null;
171
+ V.focus({ key: `runner:${picked}`, title: card.title, status: card.status, lines: error ? [error].concat(card.lines) : card.lines, lead, onClose: unpick });
172
+ }
173
+
174
+ async function act(what) {
175
+ if (busy || !picked) return;
176
+ if (what === 'open') { root.location.assign(`/goals#/goal/${encodeURIComponent(picked)}`); return; }
177
+ if (what === 'setup') { root.location.assign('/settings#autobongos'); return; }
178
+ busy = true; error = ''; show();
179
+ let ok = true;
180
+ if (what === 'send') {
181
+ const g = await req('PUT', '/autonomy/me/goals', { goal_ids: [Number(picked)] });
182
+ ok = !!(g && g.ok);
183
+ if (ok) {
184
+ const outs = await Promise.all(runners.filter((r) => r.run_requested === false)
185
+ .map((r) => req('POST', `/autonomy/runners/${encodeURIComponent(r.host)}/run`, { run: true })));
186
+ ok = outs.every((o) => o && o.ok);
187
+ }
188
+ } else if (what === 'recall') {
189
+ const outs = await Promise.all(runners.filter((r) => r.run_requested !== false)
190
+ .map((r) => req('POST', `/autonomy/runners/${encodeURIComponent(r.host)}/run`, { run: false })));
191
+ ok = outs.every((o) => o && o.ok);
192
+ }
193
+ if (!ok) error = what === 'send' ? 'Could not send your runner. Try again.' : 'Could not recall your runner. Try again.';
194
+ await readState();
195
+ await readStats();
196
+ busy = false;
197
+ show();
198
+ changed();
199
+ }
200
+
201
+ function pick(id) {
202
+ picked = String(id); stats = null; error = '';
203
+ show(); changed();
204
+ readStats().then(() => { if (picked === String(id)) show(); });
205
+ }
206
+ // The visor's card closed (Escape, its close mark, or a tap on open ground).
207
+ function unpick() {
208
+ if (!picked) return;
209
+ picked = null; stats = null; error = '';
210
+ changed();
211
+ }
212
+
213
+ // A tap on a tower in game mode picks it instead of leaving the page; a tap on open
214
+ // ground lets the pick go. Capture, so it runs before the tower's own link.
215
+ function onClick(e) {
216
+ if (!active()) return;
217
+ const city = e.target.closest && e.target.closest('.home-city .hc-svg');
218
+ if (!city) return;
219
+ const t = e.target.closest('.hc-tower[data-goal], .hc-runner[data-goal]');
220
+ if (!t) { if (picked && !e.target.closest('a')) { root.OTBVisor.focus(null); unpick(); } return; }
221
+ e.preventDefault();
222
+ const id = t.getAttribute('data-goal');
223
+ if (picked === id) return;
224
+ pick(id);
225
+ }
226
+
227
+ async function refresh() {
228
+ await readState();
229
+ if (picked) await readStats();
230
+ if (picked) show();
231
+ changed();
232
+ }
233
+ function poll() {
234
+ clearTimeout(timer);
235
+ timer = setTimeout(async () => {
236
+ if (!document.hidden && game() && mayRun() && runners.length) await refresh();
237
+ poll();
238
+ }, POLL_MS);
239
+ }
240
+
241
+ function start() {
242
+ document.addEventListener('click', onClick, true);
243
+ refresh().then(poll);
244
+ }
245
+ root.OTBHomeRunner = { marker, picked: () => (active() ? picked : null), refresh };
246
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
247
+ else start();
248
+ })(typeof window !== 'undefined' ? window : null);
@@ -19,11 +19,11 @@
19
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
22
- <link rel="stylesheet" href="/builders/visor.css?v=2026-11-11-helm4c">
22
+ <link rel="stylesheet" href="/builders/visor.css?v=2026-11-12-runner">
23
23
  <link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-11-07-brand-mark">
24
24
  <link rel="stylesheet" href="/builders/shell-game.css?v=2026-11-11-view2">
25
25
  <link rel="stylesheet" href="/builders/hall-ask.css">
26
- <link rel="stylesheet" href="/builders/home-city.css?v=2026-11-11-helm4c">
26
+ <link rel="stylesheet" href="/builders/home-city.css?v=2026-11-12-runner">
27
27
  <link rel="stylesheet" href="/builders/home-gallery.css">
28
28
  <link rel="stylesheet" href="/builders/home-idea-box.css">
29
29
  </head>
@@ -116,7 +116,7 @@
116
116
  <script src="/builders/shell.js?v=2026-11-11-view2" defer></script>
117
117
  <script src="/builders/hall-dialog.js" defer></script>
118
118
  <!-- task 1005148: the visor (glass over the scene, game mode only; ADR 0399), after the shell and hall-dialog.js -->
119
- <script src="/builders/visor.js?v=2026-11-11-helm3" defer></script>
119
+ <script src="/builders/visor.js?v=2026-11-12-runner" defer></script>
120
120
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
121
121
  <script src="/builders/palette.js" defer></script>
122
122
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -138,10 +138,13 @@
138
138
  <script src="/builders/city-draw.js" defer></script>
139
139
  <!-- task 1005196: the hour of the city's sky (sets the --sky-now-* tokens the city spends) -->
140
140
  <script src="/builders/sky-time.js?v=2026-11-11-helm4" defer></script>
141
- <script src="/builders/home-city-draw.js?v=2026-11-11-helm4d" defer></script>
141
+ <script src="/builders/home-city-draw.js?v=2026-11-12-runner" defer></script>
142
142
  <script src="/builders/home-gallery.js" defer></script>
143
143
  <script src="/builders/home-idea-box.js" defer></script>
144
- <script src="/builders/home-city.js?v=2026-11-10-visor" defer></script>
144
+ <!-- task 1005243: your runner on Home's towers (game mode; the visor carries its card and button) -->
145
+ <script src="/builders/runner-button.js" defer></script>
146
+ <script src="/builders/home-runner.js" defer></script>
147
+ <script src="/builders/home-city.js?v=2026-11-12-runner" defer></script>
145
148
  <!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
146
149
  both goal-inbox.js's attention-inbox slice below and the /blockers page.
147
150
  Loaded BEFORE goal-inbox.js, which calls it while rendering. -->
@@ -1587,15 +1587,28 @@ html.opt-window-open { overflow: hidden; }
1587
1587
 
1588
1588
  /* THE CHARACTER (task 1005147). A builder who holds a character wears its sprite in
1589
1589
  the top bar in place of the switch: one button, which offers "Change character"
1590
- (signing out). The sprites are pixel art, so they scale by whole pixels. */
1590
+ (signing out). The sprites are pixel art, so they scale by whole pixels.
1591
+ EACH CHARACTER HAS ONE COLOUR (task 1005218; --char-*): the Artisan's is the hall's own
1592
+ accent, the Artist's violet and the Patron's gold are the two colours the second look
1593
+ added, the same three the pictures are drawn in (docs/design/characters/v2). In game mode
1594
+ the visor wears the held character's (visor.css). */
1595
+ html {
1596
+ --char-artisan: var(--accent);
1597
+ --char-artist: color-mix(in oklch, color-mix(in oklch, var(--sea) 82%, var(--accent)) 72%, var(--ink));
1598
+ --char-patron: color-mix(in srgb, var(--warn) 72%, var(--ink));
1599
+ }
1591
1600
  .craft-sprite { display: block; image-rendering: pixelated; object-fit: contain; }
1592
1601
  .craft-switch__me { padding: 0 10px 0 4px; color: var(--ink); }
1593
- /* The chip is the figure's head: the portrait cropped to its top third. */
1594
- .craft-sprite--chip { width: 26px; height: 26px; border-radius: var(--radius-pill); object-fit: cover; object-position: 50% 22%; }
1602
+ /* The chip is the figure's head: its own 32px picture. */
1603
+ .craft-sprite--chip { width: 26px; height: 26px; border-radius: var(--radius-pill); object-fit: cover; background: var(--bg-deep); }
1595
1604
  /* Character creation: each door is the owner's portrait — the figure on its plinth
1596
1605
  in the torch-lit hall — filling the top of the door, the words beneath it. */
1597
- .craft-plinth { display: block; overflow: hidden; }
1598
- .craft-sprite--door { width: 100%; height: auto; aspect-ratio: 250 / 290; transition: transform var(--ds-dur-base) var(--ds-ease); }
1606
+ .craft-plinth { display: block; overflow: hidden; background: var(--bg-deep); }
1607
+ .craft-sprite--door { width: 100%; height: auto; aspect-ratio: 1; transition: transform var(--ds-dur-base) var(--ds-ease); }
1608
+ .craft-door--artisan { --char: var(--char-artisan); }
1609
+ .craft-door--artist { --char: var(--char-artist); }
1610
+ .craft-door--patron { --char: var(--char-patron); }
1611
+ .craft-door--character .craft-door__name { color: var(--char); }
1599
1612
  .craft-door--character:hover .craft-sprite--door,
1600
1613
  .craft-door--character:focus-visible .craft-sprite--door { transform: scale(1.03); }
1601
1614
 
@@ -18,6 +18,21 @@ html[data-game="on"] .visor:not([hidden]) {
18
18
  color: var(--ink); font-family: var(--font-body);
19
19
  }
20
20
 
21
+ /* THE GLASS TAKES YOUR CHARACTER'S COLOUR (task 1005218). --char is the held character's one
22
+ colour (style.css --char-*): the frame's lines, the sight, your name and the rim of your
23
+ picture wear it, and the accent on the glass (the one button, the segment being built, a
24
+ hovered row) IS it, so nothing here names a colour of its own. The Artisan's is the hall's
25
+ accent, so an Artisan's glass is the accent it always was. */
26
+ .visor[data-character="artisan"] { --char: var(--char-artisan, var(--accent)); }
27
+ .visor[data-character="artist"] { --char: var(--char-artist, var(--accent)); }
28
+ .visor[data-character="patron"] { --char: var(--char-patron, var(--accent)); }
29
+ /* on the glass's parts, not the glass: --char reads the hall's accent above, so re-pointing
30
+ the accent on the same element would be a cycle */
31
+ .visor[data-character] > * {
32
+ --accent: var(--char); --accent-ink: var(--char);
33
+ --accent-lift: color-mix(in srgb, var(--char) 58%, var(--ink));
34
+ }
35
+
21
36
  /* the frame: each arc is stretched across the stage, every stroke one hairline at any size. The
22
37
  top arc, its notch and the place all sit inside the top inset, above where a scene starts its
23
38
  own top row; the foot arc runs under the foot row. */
@@ -25,9 +40,9 @@ html[data-game="on"] .visor:not([hidden]) {
25
40
  .visor__frame--top { top: calc(var(--visor-arc) - 50px); height: 42px; }
26
41
  .visor__frame--foot { bottom: calc(var(--visor-inset-bottom) - 44px); height: 18px; }
27
42
  .visor__frame path { vector-effect: non-scaling-stroke; }
28
- .visor__line { fill: none; stroke: var(--ink-soft); stroke-opacity: .6; stroke-width: var(--line-strong); }
43
+ .visor__line { fill: none; stroke: var(--char, var(--ink-soft)); stroke-opacity: .7; stroke-width: var(--line-strong); }
29
44
  .visor__line--under { stroke: var(--bg); stroke-opacity: 1; stroke-width: 5px; }
30
- .visor__dot { fill: var(--ink-soft); stroke: var(--bg); stroke-width: var(--line-strong); }
45
+ .visor__dot { fill: var(--char, var(--ink-soft)); stroke: var(--bg); stroke-width: var(--line-strong); }
31
46
  .visor__notch, .visor__sight { position: absolute; left: 50%; transform: translate(-50%, -50%); overflow: visible; }
32
47
  .visor__notch { top: calc(var(--visor-arc) - 36px); width: 12px; height: 9px; }
33
48
  .visor__sight { top: 50%; width: 24px; height: 24px; }
@@ -76,12 +91,12 @@ html[data-game="on"] .visor:not([hidden]) {
76
91
  transform: perspective(900px) rotateY(16deg); transform-origin: left center;
77
92
  }
78
93
  .visor__sprite {
79
- flex: none; width: 56px; height: 56px; object-fit: cover; object-position: 50% 12%;
80
- background: var(--glass-strong); clip-path: var(--cut-path-sm);
94
+ flex: none; box-sizing: border-box; width: 56px; height: 56px; object-fit: cover; image-rendering: pixelated;
95
+ background: var(--bg-card); border: var(--line-strong) solid var(--char, var(--ink-soft)); clip-path: var(--cut-path-sm);
81
96
  }
82
97
  /* your name and credits stand in a box of the night, cut like the sprite beside it */
83
98
  .visor__who { display: flex; flex-direction: column; gap: 2px; padding: var(--ds-space-hair) var(--ds-space-xs); }
84
- .visor__name { font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1; color: var(--ink-soft); }
99
+ .visor__name { font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1; color: var(--char, var(--ink-soft)); }
85
100
  .visor__credits { display: flex; align-items: baseline; gap: var(--ds-space-xs); white-space: nowrap; }
86
101
  .visor__num { font-family: var(--name-face); font-size: var(--ds-text-headline); font-weight: 400; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
87
102
  .visor__unit { font-size: var(--ds-text-md); letter-spacing: var(--name-space); color: var(--ink-soft); }
@@ -154,5 +169,32 @@ html[data-game="on"] .visor:not([hidden]) {
154
169
  .visor__row--hint:hover { color: var(--ink-soft); }
155
170
  .visor__sheet .visor__key { color: var(--ink-soft); background: none; }
156
171
 
172
+ /* the card for what the scene picked (task 1005243): Home's tower and your runner on it. Centred
173
+ over the accent button it hands its one action to, the night panel between two strong
174
+ hairlines like the sheet, never wider than a short paragraph. The words are the scene's facts;
175
+ the status dot is the hall's status vocabulary (--ok / --warn / --info), never a colour of its own. */
176
+ .visor__focus {
177
+ position: absolute; left: 50%; bottom: calc(var(--visor-inset-bottom) + 72px); transform: translateX(-50%);
178
+ pointer-events: auto; box-sizing: border-box; width: min(20rem, calc(100vw - 2 * var(--ds-space-gutter-narrow)));
179
+ padding: var(--ds-space-xs) var(--ds-space-sm) var(--ds-space-sm); background: var(--bg-card);
180
+ border: 0 solid var(--rule); border-left-width: var(--line-strong); border-right-width: var(--line-strong);
181
+ }
182
+ .visor__focus-head { display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-xs); }
183
+ .visor__focus-title {
184
+ margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
185
+ font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case);
186
+ font-size: var(--ds-text-xl); letter-spacing: var(--name-space); color: var(--ink);
187
+ }
188
+ .visor__focus-status { display: flex; align-items: center; gap: var(--ds-space-xs); margin: 2px 0 0; font-size: var(--ds-text-md); color: var(--ink); }
189
+ .visor__focus-status i { width: 10px; height: 10px; flex: none; background: var(--rule); }
190
+ .visor__focus-status--ok i { background: var(--ok); }
191
+ .visor__focus-status--warn i { background: var(--warn); }
192
+ .visor__focus-status--info i { background: var(--info); }
193
+ .visor__focus-status--danger i { background: var(--danger); }
194
+ .visor__focus-lines { list-style: none; margin: var(--ds-space-xs) 0 0; padding: 0; color: var(--ink-soft); font-size: var(--ds-text-sm); line-height: 1.45; }
195
+ .visor__focus-lines li + li { margin-top: 2px; }
196
+ .visor__focus .visor__key { color: var(--ink-soft); background: none; }
197
+ .visor__main--lead[aria-busy="true"] { cursor: progress; background: var(--accent-lift); }
198
+
157
199
  /* a phone (640px and narrower): the rules live in style.css’s shared phone block, so the
158
200
  visor adds no breakpoint of its own (the design-count ratchet). */
@@ -7,7 +7,8 @@
7
7
  // a small notch under the top arc and a ring-and-dot sight at the centre. Drawn as SVG;
8
8
  // it is ornament, never a control.
9
9
  // - the place, in the name voice under the top arc ("THE SKY", "GOVERNOR CITY", …).
10
- // - the foot row. Left: your sprite (third person), your character's name and your credits
10
+ // - the foot row. Left: your sprite (your character's head, wearing the visor, its rim in
11
+ // the character's colour; task 1005218), your character's name and your credits
11
12
  // (builders.total_credits — credits only, never a payout; the spec's "does not exist").
12
13
  // Centre: your character's main action, the one accent button, with its key. Right: the
13
14
  // quiet key hints, no boxes: your other actions, "Actions" (key A), which opens the
@@ -150,6 +151,64 @@
150
151
  render();
151
152
  }
152
153
 
154
+ // ---------- what the scene has picked (task 1005243) ----------
155
+ // A scene that picks something (Home's tower) hands the visor a small card about it:
156
+ // a title, one status, a few lines, and at most one action. The action becomes the
157
+ // accent button while the card is up (ADR 0403 §4: a scene's lead action reaches the
158
+ // accent button); the character's own main action is back when the card closes.
159
+ // The scene paints the pick; the visor paints the words and the button.
160
+ var focus = null;
161
+ function setFocus(f) {
162
+ var was = focus;
163
+ if (!f || typeof f !== 'object' || !f.title) focus = null;
164
+ else {
165
+ var lead = f.lead && f.lead.label && typeof f.lead.run === 'function' ? { label: String(f.lead.label), run: f.lead.run, busy: !!f.lead.busy } : null;
166
+ focus = {
167
+ key: String(f.key || f.title), title: String(f.title),
168
+ status: f.status && f.status.text ? { text: String(f.status.text), tone: String(f.status.tone || 'neutral') } : null,
169
+ lines: (Array.isArray(f.lines) ? f.lines : []).filter(Boolean).map(String).slice(0, 4),
170
+ lead: lead, onClose: typeof f.onClose === 'function' ? f.onClose : null,
171
+ };
172
+ }
173
+ if (focus && !was) document.addEventListener('keydown', onFocusKey, true);
174
+ if (!focus && was) document.removeEventListener('keydown', onFocusKey, true);
175
+ render();
176
+ }
177
+ // Closed by the person (Escape or the close mark): the scene is told, so it unpicks.
178
+ function dropFocus() {
179
+ var f = focus;
180
+ if (!f) return;
181
+ setFocus(null);
182
+ if (f.onClose) f.onClose();
183
+ }
184
+ // Escape closes the card before it reaches the options menu, but never while the sheet
185
+ // is open: the sheet's own capture listener takes that press.
186
+ function onFocusKey(e) {
187
+ if (!focus || sheet || e.key !== 'Escape' || !shown()) return;
188
+ e.preventDefault();
189
+ e.stopPropagation();
190
+ dropFocus();
191
+ }
192
+ var TONES = { ok: 1, warn: 1, info: 1, neutral: 1, danger: 1 };
193
+ function focusHtml() {
194
+ if (!focus) return '';
195
+ var st = focus.status;
196
+ return '<div class="visor__focus" role="group" aria-label="' + esc(focus.title) + '" aria-live="polite" data-visor-focus>' +
197
+ '<div class="visor__focus-head"><p class="visor__focus-title">' + esc(focus.title) + '</p>' +
198
+ '<button class="visor__close" type="button" data-visor-unfocus aria-label="Close">' + keyHtml('Esc') + '</button></div>' +
199
+ (st ? '<p class="visor__focus-status visor__focus-status--' + (TONES[st.tone] ? st.tone : 'neutral') + '"><i aria-hidden="true"></i>' + esc(st.text) + '</p>' : '') +
200
+ (focus.lines.length ? '<ul class="visor__focus-lines">' + focus.lines.map(function (l) { return '<li>' + esc(l) + '</li>'; }).join('') + '</ul>' : '') +
201
+ '</div>';
202
+ }
203
+ function mainHtml(main) {
204
+ if (focus && focus.lead) {
205
+ return '<button class="visor__main visor__main--lead" type="button" data-visor-lead' + (focus.lead.busy ? ' aria-busy="true"' : '') + '>' +
206
+ '<span>' + esc(focus.lead.label) + '</span></button>';
207
+ }
208
+ return '<a class="visor__main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
209
+ '<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>';
210
+ }
211
+
153
212
  // ---------- drawing ----------
154
213
  var root = null, sheet = null, returnFocus = null;
155
214
  var NS = 'http://www.w3.org/2000/svg';
@@ -189,19 +248,21 @@
189
248
  }
190
249
  function glassHtml(id) {
191
250
  var acts = actionsFor(id), main = acts[0], C = craft();
192
- var sprite = C && typeof C.spriteSrc === 'function' ? C.spriteSrc(id) : '';
251
+ // Your picture on the glass is you wearing the visor (task 1005218); an older craft-mode
252
+ // without that picture still hands over the whole figure.
253
+ var sprite = C && typeof C.portraitSrc === 'function' ? C.portraitSrc(id) : (C && typeof C.spriteSrc === 'function' ? C.spriteSrc(id) : '');
193
254
  var n = credits();
194
255
  return frameSvg() +
195
256
  (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') +
196
257
  progressHtml() +
258
+ focusHtml() +
197
259
  '<div class="visor__foot">' +
198
260
  '<div class="visor__me">' +
199
- (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="250" height="290" decoding="async">' : '') +
261
+ (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="32" height="32" decoding="async">' : '') +
200
262
  '<span class="visor__who"><span class="visor__name">' + esc(NAMES[id]) + '</span>' +
201
263
  '<span class="visor__credits" data-visor-credits>' + creditsHtml(n) + '</span></span>' +
202
264
  '</div>' +
203
- '<a class="visor__main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
204
- '<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>' +
265
+ mainHtml(main) +
205
266
  '<div class="visor__hints">' + scene.actions.map(function (a, i) { return a.lead ? placeHintHtml(a, i) : ''; }).join('') + acts.slice(1).map(hintHtml).join('') +
206
267
  '<button class="visor__hint visor__hint--sheet" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
207
268
  '<span class="visor__dots" aria-hidden="true"></span>' + keyHtml(SHEET_KEY) + '<span>Actions</span></button>' +
@@ -325,6 +386,8 @@
325
386
  var t = e.target && e.target.closest ? e.target : null;
326
387
  if (!t) return;
327
388
  if (t.closest('[data-visor-close]')) { closeSheet(true); return; }
389
+ if (t.closest('[data-visor-unfocus]')) { dropFocus(); return; }
390
+ if (t.closest('[data-visor-lead]')) { if (focus && focus.lead && !focus.lead.busy) { closeSheet(false); focus.lead.run(); } return; }
328
391
  if (t.closest('[data-visor-sheet]')) { openSheet(); return; }
329
392
  var sc = t.closest('[data-visor-scene]');
330
393
  if (sc && scene) { var a = scene.actions[Number(sc.getAttribute('data-visor-scene'))]; closeSheet(false); if (a) a.run(); return; }
@@ -386,6 +449,9 @@
386
449
 
387
450
  window.OTBVisor = {
388
451
  scene: registerScene,
452
+ // a scene's pick: OTBVisor.focus({ title, status, lines, lead: { label, run }, onClose }) or focus(null)
453
+ focus: setFocus,
454
+ focused: function () { return focus ? focus.key : null; },
389
455
  render: render,
390
456
  open: openSheet,
391
457
  close: function () { closeSheet(true); },
@@ -0,0 +1,19 @@
1
+ # The characters — the three pictures, their colours, and where each is worn (task 1005218; ADR 0405)
2
+
3
+ Covers: `public/characters/*.png`, the character parts of `craft-mode.js` (creation doors, the top-bar chip), the `--char-*` tokens in `style.css`, and the character's colour on the visor (`visor.css`). The visor itself is in `visor.md`; character creation's behaviour is task 1005147's and is unchanged.
4
+
5
+ ## What it settled
6
+
7
+ - **The second look.** 64 by 64 pixel art, two animals in one body, as aliens in the Helm night. One animal gives the whole head, the other the whole body: Artisan a ram on a lizard with the long spanner, Artist an ibis on an octopus with the light-brush, Patron a falcon on a lion with the staff of office. They stand three-quarters to the viewer on a lit disc. The six rules the owner's feedback set (recognisable animals, not childish, a role tool, a 2.5D stance, simple shapes, one light) are the header of the drawing script.
8
+ - **The pictures are drawn, never edited.** `node docs/design/characters/v2/draw-characters.js --hall` writes all nine files: `<id>.png` (the whole figure, 64 by 64: the creation doors), `<id>-face.png` (the head, 32 by 32: the top-bar chip) and `<id>-visor.png` (the head wearing the visor, 32 by 32: the visor's corner). `craft-mode.js` hands them out as `spriteSrc`, `faceSrc` and `portraitSrc`.
9
+ - **One colour each** (`style.css`, on `html`, beside the sprite rules): `--char-artisan` is the accent, `--char-artist` a violet and `--char-patron` a gold, both mixed from the hall's own tokens (ADR 0405 §2). No colour is typed in.
10
+ - **Where the colour is worn.** The creation door's name; and, in game mode, the visor: `.visor[data-character]` sets `--char` and re-points `--accent`, `--accent-ink` and `--accent-lift` at it, so the frame's lines, the sight, the name, the picture's rim, the one button and the segment being built all follow with no rule of their own. An Artisan's glass is exactly what it was.
11
+ - **Pixel art scales by nearest pixel** (`image-rendering: pixelated`) everywhere a picture is shown.
12
+
13
+ ## Not done
14
+
15
+ - A scene's own buttons keep the hall's accent under a violet or gold glass (the Governor City's "decide" button). ADR 0403's follow-up tasks move them to the visor.
16
+ - The visor shows the 32px head at 56px (36px on a phone), which is not a whole multiple, so a few pixels are uneven.
17
+ - The pictures keep their own fixed colours when a host re-themes its accent.
18
+
19
+ Tests: `tests/character_creation.mjs` (the nine files, their sizes, the chip's picture) and `tests/hall_visor.mjs` (the portrait, the colour on the glass per character).
@@ -0,0 +1,19 @@
1
+ # Your runner on Home's towers — send it, see it, recall it (task 1005243, goal 1000142)
2
+
3
+ Covers: `home-runner.js` (new), the tower pick and runner marker in `home-city-draw.js` / `home-city.css`, the visor's card (`visor.js` `OTBVisor.focus`, `visor.css` `.visor__focus*`), and the read `GET /api/bongos/autonomy/me/goals/:goalId/stats` (`modules/autonomy`). Game mode only.
4
+
5
+ ## What it settled
6
+
7
+ - **The owner's brief (2026-11-12):** putting your Autobongos runner to work should feel like putting a builder on a building in a base-building game, and stay uncluttered. In game mode you tap a tower on Home (a tower is a goal), the visor offers **Send runner here**, and the tower then carries your runner. Tap it again for what it has done there and **Recall runner**.
8
+ - **One tower at a time (owner, 2026-11-12).** Sending sets your goal picks to that one goal (`PUT /autonomy/me/goals`) and switches on any of your devices that are stopped (`POST /autonomy/runners/:host/run`). Recall switches them off and leaves the pick. Several picks made in Settings are "working on several goals": no tower carries the marker until you send it somewhere.
9
+ - **ADR 0403 kept:** the scene only gets picked (the tower is lit and edged in the accent); the words and the one button are the visor's. `OTBVisor.focus({ title, status, lines, lead: { label, run }, onClose })` is the general mechanism: a scene's pick puts a small card on the glass and its action takes the accent button; Escape (or the card's Esc mark, or a tap on open ground) closes it before the options menu sees the press, and the character's own main action comes back. This answers ADR 0403 §4's open question for one action.
10
+ - **Who sees it:** only a viewer the server says may run a runner (`GET /autonomy/fence` → `scope.may_run`, the `autonomy.run` atom). Signed out, without the atom, or in classic mode a tower stays the plain link to its goal and nothing is new.
11
+ - **The marker** is the agent's robot face in the accent colour (yours, not anyone's agent), ringed by its state: working (accent ring that turns, CSS only, stilled by the hall's reduced-motion rule), starting, resting (dotted info ring), ready (soft ring), recalled (faded). It replaces the agent face `/home/city` would draw on that tower. Tapping it picks its tower.
12
+ - **The numbers are the ledger's**, never the runner's own report: tasks the caller's runner shipped in that goal (`claims.autonomous` + outcome `shipped`), the tokens their sessions took (`session_records.total_tokens`), their average length, and the goal's `ready`/`active` tasks. Time left is that count × the average and is shown only when there is an average ("about 3 h"); with no runner history there it promises no time. The device's face (Running, Resting until 12:31 · 42 min, Device is off…) is `runner-button.js` `buttonState`, the same decision the Settings card draws.
13
+ - **Not yet:** the percent of your Claude usage limit. That number lives only on the runner's PC; task 1005244 has the runner report it in its check-in.
14
+
15
+ ## Looking at it
16
+
17
+ `hall-preview-fixture` in `.claude/launch.json` (port 4602, `--fixture-me`; the plain preview borrows the live `/me`, whose character may not be the Artisan). Set `localStorage hall-game=on`, `OTBCraftMode.setCharacter('artisan')`, and edit `autonomy__fence.json` / `autonomy__runners.json` to a project that allows goals (the checked-in fence has the switch off and no goals, which renders "Closed to runners" — a real state). The harness reads fixtures per request, so `OTBHomeRunner.refresh()` picks a change up without a restart. Fixtures added: `autonomy__me__goals__stats.json`, `autonomy__me__goals.put.json`.
18
+
19
+ Tests: `tests/home_runner.mjs` (the card and marker decisions, run for real), `tests/autonomy_runner_goal_stats.mjs` (the read and route), `tests/hall_visor.mjs` (the card on the glass, its button, Escape order).
@@ -20,6 +20,9 @@ The visor is the control and the scene is the view: every main call to action in
20
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
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).
22
22
 
23
+ ## The glass wears your character’s colour (task 1005218, ADR 0405)
24
+
25
+ The frame’s lines, the sight, your name, the rim of your picture and the accent on the glass (the one button, the segment being built) are the held character’s one colour, and the picture is your character’s head wearing the visor. Where the sections below say the lines are ice or the button is the accent, read "the character’s colour"; for an Artisan the two are the same. The detail is in `characters.md`.
23
26
  ## Home's gallery on the glass (task 1005211)
24
27
 
25
28
  - **What left the gallery in game mode** (`home-gallery.css`, under `html.visor-on`): the order tabs (Most popular, Latest), the ways (The full gallery, Your studio), the two arrows and the full-screen button. A work's label, its heart and a piece in the full gallery stay: they are looking at the art, not leaving it.
@@ -39,6 +42,10 @@ The glass was redrawn to the owner's mock-up "Cloud city: in the visor, minimal"
39
42
  - **Scenes.** The top arc, the notch and the place sit inside the top 64px (`--visor-arc`; the segments under them take the inset to 88px, `--visor-inset-top`), so no scene's top row is struck through. Home no longer shifts its greeting row left (there is no pill top-right to clear). The city's door rises above the foot row at every width, because the accent button is centred where the door stood. The sky's `visor-on` offsets are unchanged.
40
43
  - **Looking at it.** The stand-in scene in `tests/hall_visor.mjs` over the hall's real sheets; put a bright gradient behind it to check midday.
41
44
 
45
+ ## A scene's pick on the glass (task 1005243)
46
+
47
+ `OTBVisor.focus({ key, title, status: { text, tone }, lines, lead: { label, run }, onClose })` puts a small card above the accent button (the night panel between two strong hairlines, like the sheet) and hands `lead` the accent button while it is up; `focus(null)` takes both away. Escape closes it in capture before the options menu, but never while the sheet is open. First user: Home's towers and your runner (`records/home-runner.md`).
48
+
42
49
  ## What it settled (task 1005148)
43
50
 
44
51
  - **The first look (2026-11-10, five mock-up rounds; replaced by the minimal visor above).** A quiet layout with "hairline instruments" from the helmet round: thin curved visor lines across the top, bottom and both sides, a small centre cross, and a compass of nine slanted cells along the top line with the one salmon marker under its centre. No tracker or radar (tried and dropped), no clipped corners: the pills stay round (DESIGN.md "pill or nothing"). No glow (DESIGN.md's rejected near-black-plus-neon HUD). The frame is one SVG in a 1000×1000 box stretched over the stage with non-scaling strokes, so it is a hairline at every size.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.94",
3
+ "version": "1.24.96",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.94",
9
+ "version": "1.24.96",
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.94",
3
+ "version": "1.24.96",
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",