@bongos/core 1.21.72 → 1.21.74
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.
- package/.bongos-core.json +87 -72
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md +44 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +139 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +40 -36
- package/docs/copy-registry.json +97 -51
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +329 -330
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +69 -1
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.css +15 -0
- package/modules/hall-ui/public/project-settings.html +7 -7
- package/modules/hall-ui/public/project-settings.js +42 -0
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/shell.js +9 -0
- package/modules/hall-ui/public/street.html +3 -3
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/lifecycle/routes/project-settings.js +65 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/shipping-mode.js +71 -0
- package/src/bongos/project-settings.js +67 -0
- package/src/module-api.js +7 -1
- package/tests/craft_mode_nav.mjs +117 -1
- package/tests/hall_city.mjs +5 -3
- package/tests/hall_nav.mjs +3 -1
- package/tests/hall_street.mjs +2 -1
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/project_settings_page.mjs +8 -5
- package/tests/ship_mode.mjs +171 -0
|
@@ -99,6 +99,8 @@
|
|
|
99
99
|
studio: '<path d="M9 15.5V8.5"/><path d="M9 8.5C9 5.5 6.5 3.5 3.5 3.5c0 3 2.5 5 5.5 5Z"/><path d="M9 10C9 7.5 11.2 5.5 14 5.5c0 2.7-2.2 4.5-5 4.5Z"/>',
|
|
100
100
|
settings: '<circle cx="9" cy="9" r="2.5"/><path d="M9 2.5v2m0 9v2M2.5 9h2m9 0h2M4.4 4.4l1.4 1.4m6.4 6.4 1.4 1.4m0-9.2-1.4 1.4M5.8 12.2l-1.4 1.4"/>',
|
|
101
101
|
admin: '<path d="M9 2.5 15 5v4c0 3.5-2.4 6-6 7-3.6-1-6-3.5-6-7V5l6-2.5Z"/>',
|
|
102
|
+
// The governor city: a pediment over columns, the Governing mode's own glyph.
|
|
103
|
+
city: '<path d="M2.5 7 9 3l6.5 4M4 7.5v6.5M7 7.5v6.5M11 7.5v6.5M14 7.5v6.5M2.5 15.5h13"/>',
|
|
102
104
|
signout: '<path d="M7 3.5H4a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h3M12 12l3-3-3-3M15 9H7"/>',
|
|
103
105
|
};
|
|
104
106
|
function icon(name) {
|
|
@@ -210,6 +212,13 @@
|
|
|
210
212
|
// so the badge counts ballots this viewer can actually cast, never "items
|
|
211
213
|
// open somewhere".
|
|
212
214
|
{ id: 'board-room', label: 'Board Room', icon: 'admin', href: '/board-room', gate: 'perm:board.vote.cast', count: 'board_votes' },
|
|
215
|
+
// The governor city (task 1004524): the owner put it on the sidebar on
|
|
216
|
+
// 2026-10-08, reversing "leave it off". It is the Governing mode's lead tab
|
|
217
|
+
// (craft-mode.js folds that mode's other tabs into a dropdown under it).
|
|
218
|
+
// Gated on the atom its page is (requireBoardVotePage), and it sits in this
|
|
219
|
+
// head-less group for the reason the Board Room does: the atom floors below
|
|
220
|
+
// any rank head. Cosmetic only (ADR 0016).
|
|
221
|
+
{ id: 'city', label: 'City', icon: 'city', href: '/city', gate: 'perm:board.vote.cast' },
|
|
213
222
|
// task 1004326 (BV2.TW15 of goal 1000095): the Ideas board (/ideas, task
|
|
214
223
|
// 1002928) and the Copy desk (/copy-desk, task 1003113) are ARCHIVED — off
|
|
215
224
|
// the nav on the owner's call of 2026-09-27, NOT deleted. Both pages and
|
|
@@ -203,9 +203,9 @@
|
|
|
203
203
|
</div>
|
|
204
204
|
</div>
|
|
205
205
|
|
|
206
|
-
<script src="/builders/shell-trail.js?v=2026-10-
|
|
207
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
208
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
206
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
207
|
+
<script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
|
|
208
|
+
<script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
|
|
209
209
|
<script src="/builders/palette.js" defer></script>
|
|
210
210
|
<script src="/builders/city-draw.js?v=2026-10-04-pins1"></script>
|
|
211
211
|
<script src="/builders/street-draw.js?v=2026-10-04-pins1"></script>
|
|
@@ -176,9 +176,9 @@
|
|
|
176
176
|
|
|
177
177
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
178
178
|
|
|
179
|
-
<script src="/builders/shell-trail.js?v=2026-10-
|
|
180
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
181
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
179
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
180
|
+
<script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
|
|
181
|
+
<script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
|
|
182
182
|
<script src="/builders/palette.js" defer></script>
|
|
183
183
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
184
184
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
|
|
55
55
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
56
56
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
57
|
-
<script src="/builders/shell-trail.js?v=2026-10-
|
|
58
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
59
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
57
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
58
|
+
<script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
|
|
59
|
+
<script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
|
|
60
60
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
61
61
|
<script src="/builders/palette.js" defer></script>
|
|
62
62
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
@@ -138,9 +138,9 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
|
-
<script src="/builders/shell-trail.js?v=2026-10-
|
|
142
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
143
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
141
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
142
|
+
<script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
|
|
143
|
+
<script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
|
|
144
144
|
<script src="/builders/palette.js" defer></script>
|
|
145
145
|
|
|
146
146
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
@@ -142,9 +142,9 @@
|
|
|
142
142
|
|
|
143
143
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
144
144
|
|
|
145
|
-
<script src="/builders/shell-trail.js?v=2026-10-
|
|
146
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
147
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
145
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
146
|
+
<script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
|
|
147
|
+
<script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
|
|
148
148
|
<script src="/builders/palette.js" defer></script>
|
|
149
149
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
150
150
|
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
@@ -165,9 +165,9 @@
|
|
|
165
165
|
|
|
166
166
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
167
167
|
chrome exists before the page script runs. -->
|
|
168
|
-
<script src="/builders/shell-trail.js?v=2026-10-
|
|
169
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
170
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
168
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
169
|
+
<script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
|
|
170
|
+
<script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
|
|
171
171
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
172
172
|
<script src="/builders/palette.js" defer></script>
|
|
173
173
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,9 +171,9 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell-trail.js?v=2026-10-
|
|
175
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
176
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
174
|
+
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
175
|
+
<script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
|
|
176
|
+
<script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
|
|
177
177
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
178
178
|
<script src="/builders/palette.js" defer></script>
|
|
179
179
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -34,6 +34,29 @@ const goalCategory = require('../goal-category');
|
|
|
34
34
|
|
|
35
35
|
const log = api.logger('lifecycle');
|
|
36
36
|
|
|
37
|
+
// --- ship mode: who starts a ship (task 1004628, ADR 0369) -------------------
|
|
38
|
+
//
|
|
39
|
+
// One row shape, two readers: the settings page (inside GET /project-settings) and
|
|
40
|
+
// any builder's CLI or AI session (GET /project-settings/ship-mode), so the sentence
|
|
41
|
+
// a person reads and the one an agent obeys are the same words.
|
|
42
|
+
function shipModeRow(resolved) {
|
|
43
|
+
return {
|
|
44
|
+
key: 'ship_mode',
|
|
45
|
+
title: 'Ship mode',
|
|
46
|
+
editable: true,
|
|
47
|
+
state: 'live',
|
|
48
|
+
value: { mode: resolved.mode, chosen: resolved.chosen, modes: api.projectSettings.SHIP_MODES },
|
|
49
|
+
sentence: api.projectSettings.describeShipMode(resolved),
|
|
50
|
+
choices: [
|
|
51
|
+
{ mode: 'auto', label: 'Auto (recommended)', sentence: 'When the AI finishes a task it ships and lands it. Nobody has to type a command.' },
|
|
52
|
+
{ mode: 'manual', label: 'Manual', sentence: 'The AI stops before each ship and asks a person to approve it.' },
|
|
53
|
+
],
|
|
54
|
+
source: resolved.source,
|
|
55
|
+
invalid: resolved.invalid,
|
|
56
|
+
reads: { file: 'project_settings[\'ship.mode\'] (migration core_231)', adr: 'ADR 0369' },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
// --- the three rows that are READ, never written -----------------------------
|
|
38
61
|
//
|
|
39
62
|
// Each names the committed artefact it resolves from, because "you cannot edit this"
|
|
@@ -214,12 +237,14 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
214
237
|
// Inactive included: the set's SHAPE is what this page edits, and a deactivated
|
|
215
238
|
// category the owner cannot see is one they cannot bring back.
|
|
216
239
|
const categories = await goalCategory.listCategories({ includeInactive: true });
|
|
240
|
+
const shipMode = await api.projectSettings.resolveShipMode();
|
|
217
241
|
|
|
218
242
|
// A settings read is a configuration comparison — a cached one is wrong the
|
|
219
243
|
// moment somebody turns a knob (the rot.js precedent).
|
|
220
244
|
res.set('Cache-Control', 'no-store');
|
|
221
245
|
res.json({
|
|
222
246
|
rows: [
|
|
247
|
+
shipModeRow(shipMode),
|
|
223
248
|
{
|
|
224
249
|
key: 'rot_timer',
|
|
225
250
|
title: 'Stale timer',
|
|
@@ -316,7 +341,47 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
316
341
|
}
|
|
317
342
|
});
|
|
318
343
|
|
|
344
|
+
// GET /project-settings/ship-mode — the ship mode alone, for the CLI and the AI.
|
|
345
|
+
//
|
|
346
|
+
// rank: any signed-in builder. Unlike the full settings read this carries no
|
|
347
|
+
// reward internals or file paths an agent could not already read, and every
|
|
348
|
+
// builder's AI session must know whether to ship on its own or stop and ask —
|
|
349
|
+
// gating it Metic+ would leave a newcomer's session guessing.
|
|
350
|
+
router.get('/project-settings/ship-mode', api.requireBuilder, async (req, res) => {
|
|
351
|
+
try {
|
|
352
|
+
const resolved = await api.projectSettings.resolveShipMode();
|
|
353
|
+
res.set('Cache-Control', 'no-store');
|
|
354
|
+
const row = shipModeRow(resolved);
|
|
355
|
+
res.json({ mode: resolved.mode, chosen: resolved.chosen, source: resolved.source, sentence: row.sentence, choices: row.choices });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
log.error(`GET /project-settings/ship-mode: ${err && err.message}`);
|
|
358
|
+
res.fail('project_settings_read_failed', { status: 500, message: 'internal error' });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// PATCH /project-settings/ship-mode — body { mode: 'auto' | 'manual' }.
|
|
363
|
+
//
|
|
364
|
+
// rank: project.settings.manage (metic+, government_010) — the same atom as the
|
|
365
|
+
// other writes on this page. Validated with the resolver's own parser, so the
|
|
366
|
+
// page, the CLI and the reader can never disagree about what a mode is.
|
|
367
|
+
router.patch('/project-settings/ship-mode', api.requireBuilder, api.requirePermission('project.settings.manage'), async (req, res) => {
|
|
368
|
+
if (api.validateOrRespond(req, res, { mode: { required: true, type: 'string' } })) return;
|
|
369
|
+
const parsed = api.projectSettings.parseShipMode(req.body.mode);
|
|
370
|
+
if (!parsed.mode) {
|
|
371
|
+
return res.fail('bad_ship_mode', { status: 400, message: `mode: ${parsed.reason}` });
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await api.projectSettings.set(api.projectSettings.KEYS.SHIP_MODE, parsed.mode, req.builder.id);
|
|
375
|
+
const resolved = await api.projectSettings.resolveShipMode();
|
|
376
|
+
res.json({ ok: true, mode: resolved.mode, chosen: resolved.chosen, source: resolved.source, sentence: api.projectSettings.describeShipMode(resolved) });
|
|
377
|
+
} catch (err) {
|
|
378
|
+
log.error(`PATCH /project-settings/ship-mode: ${err && err.message}`);
|
|
379
|
+
res.fail('project_settings_write_failed', { status: 500, message: 'internal error' });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
319
383
|
return router;
|
|
320
384
|
};
|
|
321
385
|
|
|
322
386
|
module.exports.settingsDeskRows = settingsDeskRows;
|
|
387
|
+
module.exports.shipModeRow = shipModeRow;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.74",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.74",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.74",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9242,5 +9242,17 @@
|
|
|
9242
9242
|
"id": "1004578",
|
|
9243
9243
|
"text": "While a new project is being set up, founders now get a little cloud-hopping game: each cloud is a public project on the platform, in its own planet colour. When the project goes live, clouds and stars fill the screen and fade"
|
|
9244
9244
|
}
|
|
9245
|
+
],
|
|
9246
|
+
"1.21.73": [
|
|
9247
|
+
{
|
|
9248
|
+
"id": "1004625",
|
|
9249
|
+
"text": "The governor city now has its own tab, and in Governing mode it leads the sidebar with every other governing page tucked into one dropdown under it."
|
|
9250
|
+
}
|
|
9251
|
+
],
|
|
9252
|
+
"1.21.74": [
|
|
9253
|
+
{
|
|
9254
|
+
"id": "1004628",
|
|
9255
|
+
"text": "Projects now have a Ship mode setting. Auto, the default, means the AI ships finished work on its own. Manual means it asks first. Owners can switch it on the project settings page."
|
|
9256
|
+
}
|
|
9245
9257
|
]
|
|
9246
9258
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/shipping-mode.js — read or set the project's ship mode (task 1004628, ADR 0369).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Ship mode says WHO starts a ship: in `auto` the AI finishes a task and ships
|
|
5
|
+
// it with no commands from the person; in `manual` it stops and asks first. An AI
|
|
6
|
+
// session has to know which before it ships, and a person has to be able to change
|
|
7
|
+
// it without opening the settings page — so the mode is one command away.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node scripts/gds/shipping-mode.js print the mode (any builder)
|
|
11
|
+
// node scripts/gds/shipping-mode.js auto|manual set it (needs project.settings.manage)
|
|
12
|
+
// node scripts/gds/shipping-mode.js --json print the mode as JSON
|
|
13
|
+
//
|
|
14
|
+
// Also `bongos ship-mode [auto|manual]`.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { apiCall, requireSession, errCode, cliExit } = require('./cli-lib');
|
|
19
|
+
|
|
20
|
+
const USAGE = 'usage: node scripts/gds/shipping-mode.js [auto|manual] [--json]';
|
|
21
|
+
|
|
22
|
+
// What a person sees. One sentence from the server, plus how to change it.
|
|
23
|
+
function describe(data) {
|
|
24
|
+
const lines = [`Ship mode: ${data.mode}${data.chosen ? '' : ' (default — not chosen yet)'}`];
|
|
25
|
+
if (data.sentence) lines.push(data.sentence);
|
|
26
|
+
lines.push(data.mode === 'manual'
|
|
27
|
+
? 'Change it: node scripts/gds/shipping-mode.js auto'
|
|
28
|
+
: 'Change it: node scripts/gds/shipping-mode.js manual');
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main(argv) {
|
|
33
|
+
const args = argv.filter((a) => a !== '--json');
|
|
34
|
+
const json = argv.includes('--json');
|
|
35
|
+
if (args.includes('--help') || args.includes('-h')) { console.log(USAGE); return 0; }
|
|
36
|
+
if (args.length > 1) { console.error(USAGE); return 2; }
|
|
37
|
+
|
|
38
|
+
await requireSession();
|
|
39
|
+
|
|
40
|
+
if (args.length === 0) {
|
|
41
|
+
const r = await apiCall('GET', '/api/bongos/project-settings/ship-mode');
|
|
42
|
+
if (!r.ok) {
|
|
43
|
+
console.error(`Could not read the ship mode (${r.status}${errCode(r.data) ? `: ${errCode(r.data)}` : ''}).`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
console.log(json ? JSON.stringify(r.data) : describe(r.data));
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mode = String(args[0]).trim().toLowerCase();
|
|
51
|
+
if (mode !== 'auto' && mode !== 'manual') { console.error(USAGE); return 2; }
|
|
52
|
+
const r = await apiCall('PATCH', '/api/bongos/project-settings/ship-mode', { mode });
|
|
53
|
+
if (!r.ok) {
|
|
54
|
+
if (r.status === 403) {
|
|
55
|
+
console.error('Only the project owner (or someone with project settings access) can change the ship mode.');
|
|
56
|
+
} else {
|
|
57
|
+
console.error(`Could not set the ship mode (${r.status}${errCode(r.data) ? `: ${errCode(r.data)}` : ''}).`);
|
|
58
|
+
}
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
console.log(json ? JSON.stringify(r.data) : `Saved. ${describe(r.data)}`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (require.main === module) {
|
|
66
|
+
main(process.argv.slice(2))
|
|
67
|
+
.then((code) => cliExit(code))
|
|
68
|
+
.catch((err) => { console.error('fatal:', err && err.message ? err.message : err); cliExit(1); });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { describe, main };
|
|
@@ -49,8 +49,20 @@ const { logger } = require('./logger');
|
|
|
49
49
|
const SETTING_KEYS = {
|
|
50
50
|
ROT_DAYS: 'attention.rot_days',
|
|
51
51
|
FOCUS_VERSION: 'roadmap.focus_version',
|
|
52
|
+
SHIP_MODE: 'ship.mode',
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
// SHIP MODE (task 1004628, ADR 0369) — WHO starts a ship, never HOW it lands.
|
|
56
|
+
// auto — the AI finishes a task, then ships and lands it with no commands from
|
|
57
|
+
// the person. The default, because a new owner who is handed terminal
|
|
58
|
+
// commands at every ship reads the project as broken (owner feedback,
|
|
59
|
+
// 2026-10-08).
|
|
60
|
+
// manual — the AI stops before shipping and the person approves each ship.
|
|
61
|
+
// It is NOT the deploy mode (config/deploy.json, ci|laptop): that one decides the
|
|
62
|
+
// mechanics of landing once a ship starts, is committed config, and is unchanged.
|
|
63
|
+
const SHIP_MODES = Object.freeze(['auto', 'manual']);
|
|
64
|
+
const DEFAULT_SHIP_MODE = 'auto';
|
|
65
|
+
|
|
54
66
|
// The last-resort rot timer, in days. 30 because that is the shortest window in
|
|
55
67
|
// which "nobody has touched this" is evidence of rot rather than evidence of a
|
|
56
68
|
// normal week — a fortnight would fire on every ordinary holiday, and a quarter is
|
|
@@ -202,6 +214,56 @@ async function setFocusVersionId(versionId, builderId = null) {
|
|
|
202
214
|
return String(versionId);
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
// parseShipMode(raw) — one candidate as a ship mode, or why it is unusable. Pure,
|
|
218
|
+
// never throws, case-insensitive and trimmed so a hand-typed "Manual " still reads.
|
|
219
|
+
function parseShipMode(raw) {
|
|
220
|
+
if (raw == null || String(raw).trim() === '') return { reason: 'empty' };
|
|
221
|
+
const v = String(raw).trim().toLowerCase();
|
|
222
|
+
if (!SHIP_MODES.includes(v)) return { reason: `must be one of ${SHIP_MODES.join(' | ')}` };
|
|
223
|
+
return { mode: v };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// resolveShipMode() — the project's ship mode and where it came from.
|
|
227
|
+
// Returns { mode, source, chosen, invalid }:
|
|
228
|
+
// mode — always 'auto' or 'manual'
|
|
229
|
+
// source — 'project_settings' | 'default'
|
|
230
|
+
// chosen — true only when someone actually SET a usable value. "Never chosen" is
|
|
231
|
+
// a real answer the first-ship prompt (task 1004630) asks about, so it
|
|
232
|
+
// must not be confused with "chose the default".
|
|
233
|
+
// invalid — null, or { where, raw, reason } for a stored value that is unusable.
|
|
234
|
+
// A database that cannot be read answers the default rather than failing a ship —
|
|
235
|
+
// the same reporting posture as resolveRotDays.
|
|
236
|
+
async function resolveShipMode() {
|
|
237
|
+
let row = null;
|
|
238
|
+
try {
|
|
239
|
+
row = await getSetting(SETTING_KEYS.SHIP_MODE);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
logger.error(`project_settings read failed; falling back to the default ship mode: ${err && err.message}`);
|
|
242
|
+
return { mode: DEFAULT_SHIP_MODE, source: 'default', chosen: false, invalid: null };
|
|
243
|
+
}
|
|
244
|
+
if (row && row.value != null && row.value !== '') {
|
|
245
|
+
const got = parseShipMode(row.value);
|
|
246
|
+
if (got.mode) return { mode: got.mode, source: 'project_settings', chosen: true, invalid: null };
|
|
247
|
+
return {
|
|
248
|
+
mode: DEFAULT_SHIP_MODE,
|
|
249
|
+
source: 'default',
|
|
250
|
+
chosen: false,
|
|
251
|
+
invalid: { where: `project_settings['${SETTING_KEYS.SHIP_MODE}']`, raw: String(row.value), reason: got.reason },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { mode: DEFAULT_SHIP_MODE, source: 'default', chosen: false, invalid: null };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// describeShipMode(resolved) — one plain sentence for a person or an AI session.
|
|
258
|
+
function describeShipMode(resolved) {
|
|
259
|
+
const r = resolved || {};
|
|
260
|
+
const what = r.mode === 'manual'
|
|
261
|
+
? 'Manual: the AI stops before each ship and waits for a person to approve it.'
|
|
262
|
+
: 'Auto: when the AI finishes a task it ships and lands it with no commands from a person.';
|
|
263
|
+
const how = r.chosen ? '' : ' (the default — nobody has chosen yet)';
|
|
264
|
+
return `${what}${how}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
205
267
|
// parseDays is exported for the tests only — the range check and the default are
|
|
206
268
|
// asserted against the SAME constants the resolver reads, because a test that
|
|
207
269
|
// re-typed 30 or 365 would be the second copy of the number this file exists to
|
|
@@ -218,4 +280,9 @@ module.exports = {
|
|
|
218
280
|
describeRotTimer,
|
|
219
281
|
getFocusVersionId,
|
|
220
282
|
setFocusVersionId,
|
|
283
|
+
SHIP_MODES,
|
|
284
|
+
DEFAULT_SHIP_MODE,
|
|
285
|
+
parseShipMode,
|
|
286
|
+
resolveShipMode,
|
|
287
|
+
describeShipMode,
|
|
221
288
|
};
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.74'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -462,6 +462,12 @@ module.exports = {
|
|
|
462
462
|
// page's own anti-drift rule forbids, caught one layer up on task 1003280's grade.
|
|
463
463
|
get MIN_ROT_DAYS() { return require('./bongos/project-settings').MIN_ROT_DAYS; },
|
|
464
464
|
get MAX_ROT_DAYS() { return require('./bongos/project-settings').MAX_ROT_DAYS; },
|
|
465
|
+
// Ship mode (task 1004628, ADR 0369): who starts a ship — the AI (auto) or a
|
|
466
|
+
// person (manual). The settings page writes it; the CLI and the AI read it.
|
|
467
|
+
get SHIP_MODES() { return require('./bongos/project-settings').SHIP_MODES; },
|
|
468
|
+
get parseShipMode() { return require('./bongos/project-settings').parseShipMode; },
|
|
469
|
+
get resolveShipMode() { return require('./bongos/project-settings').resolveShipMode; },
|
|
470
|
+
get describeShipMode() { return require('./bongos/project-settings').describeShipMode; },
|
|
465
471
|
},
|
|
466
472
|
readConfigFileSync: instanceConfig.readConfigFileSync,
|
|
467
473
|
configHome: instanceConfig.configHome,
|
package/tests/craft_mode_nav.mjs
CHANGED
|
@@ -187,7 +187,7 @@ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mod
|
|
|
187
187
|
p.signIn('thetes');
|
|
188
188
|
assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
|
|
189
189
|
assert.ok(!p.headShowing('government'), 'nor its group head');
|
|
190
|
-
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'profile', 'collab', 'roster'], 'what a thetes board member in Governing sees');
|
|
190
|
+
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'city', 'profile', 'collab', 'roster'], 'what a thetes board member in Governing sees');
|
|
191
191
|
p.signIn('archon');
|
|
192
192
|
assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
|
|
193
193
|
assert.ok(p.headShowing('government'), 'and its group head comes back with it, which only a mode pass after the gate can know');
|
|
@@ -202,3 +202,119 @@ test('shell.js applies the mode AFTER its gate loop, and style.css hides what th
|
|
|
202
202
|
assert.match(read('style.css'), /#app-sidebar \[data-craft-off\] \{ display: none !important; \}/);
|
|
203
203
|
assert.doesNotMatch(CRAFT, /\.hidden\s*=/, 'the mode never writes `hidden`, which the gate owns');
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
// ---------- the Governing fold (owner, 2026-10-08) ----------
|
|
207
|
+
// Governing leads with the City and folds its other own tabs into one dropdown
|
|
208
|
+
// under it. The fold MOVES nodes, which the parsed sidebar above cannot model, so
|
|
209
|
+
// these cases run the real craft-mode.js against a small working tree: nodes with
|
|
210
|
+
// parents, insertBefore and comments, laid out as shell.js's NAV groups are.
|
|
211
|
+
function treeNode(tag, nodeType = 1) {
|
|
212
|
+
const attrs = {};
|
|
213
|
+
const n = {
|
|
214
|
+
tagName: tag, nodeType, id: '', className: '', dataset: {}, hidden: false, childNodes: [], parentNode: null, _html: '', type: '',
|
|
215
|
+
setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
|
|
216
|
+
hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
|
|
217
|
+
get classList() {
|
|
218
|
+
const self = this;
|
|
219
|
+
const list = () => self.className.split(/\s+/).filter(Boolean);
|
|
220
|
+
return {
|
|
221
|
+
contains: (c) => list().includes(c),
|
|
222
|
+
add: (c) => { if (!list().includes(c)) self.className = list().concat(c).join(' '); },
|
|
223
|
+
remove: (c) => { self.className = list().filter((x) => x !== c).join(' '); },
|
|
224
|
+
toggle(c, on) { if (on === undefined) on = !this.contains(c); if (on) this.add(c); else this.remove(c); return on; },
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
get nextSibling() { const p = this.parentNode; return p ? p.childNodes[p.childNodes.indexOf(this) + 1] || null : null; },
|
|
228
|
+
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
229
|
+
appendChild(c) { return this.insertBefore(c, null); },
|
|
230
|
+
insertBefore(c, ref) {
|
|
231
|
+
if (c.parentNode) c.parentNode.removeChild(c);
|
|
232
|
+
const i = ref ? this.childNodes.indexOf(ref) : -1;
|
|
233
|
+
if (i === -1) this.childNodes.push(c); else this.childNodes.splice(i, 0, c);
|
|
234
|
+
c.parentNode = this;
|
|
235
|
+
return c;
|
|
236
|
+
},
|
|
237
|
+
removeChild(c) { this.childNodes.splice(this.childNodes.indexOf(c), 1); c.parentNode = null; return c; },
|
|
238
|
+
walk(fn) { for (const c of this.childNodes) { if (c.nodeType === 1) { fn(c); c.walk(fn); } } },
|
|
239
|
+
querySelectorAll(sel) { const out = []; this.walk((c) => { if (sel.startsWith('.') && c.classList.contains(sel.slice(1))) out.push(c); }); return out; },
|
|
240
|
+
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; },
|
|
241
|
+
addEventListener() {}, focus() {},
|
|
242
|
+
};
|
|
243
|
+
return n;
|
|
244
|
+
}
|
|
245
|
+
function bootTree({ mode = null, activeId = null } = {}) {
|
|
246
|
+
const el = (tag, cls, id) => { const n = treeNode(tag); n.className = cls || ''; n.id = id || ''; return n; };
|
|
247
|
+
const sidebar = el('NAV', '', 'app-sidebar');
|
|
248
|
+
// shell.js's NAV, group by group: the unlabeled one holds its items directly,
|
|
249
|
+
// a labeled one a head and a .nav-items wrapper (renderSidebar's shape).
|
|
250
|
+
for (const block of SHELL.slice(SHELL.indexOf('var NAV = ['), SHELL.indexOf('// ---------- nav group collapse')).split('{ group: ').slice(1)) {
|
|
251
|
+
const label = /^'([^']*)'/.exec(block)[1];
|
|
252
|
+
const group = el('DIV', 'nav-group' + (label ? ' nav-group--collapsible' : ''));
|
|
253
|
+
let into = group;
|
|
254
|
+
if (label) {
|
|
255
|
+
const head = el('BUTTON', 'nav-group__head');
|
|
256
|
+
head.dataset.group = label.toLowerCase();
|
|
257
|
+
group.appendChild(head);
|
|
258
|
+
into = group.appendChild(el('DIV', 'nav-items', 'nav-items-' + label.toLowerCase()));
|
|
259
|
+
}
|
|
260
|
+
for (const m of block.matchAll(/\{ id: '([a-z-]+)', label:/g)) {
|
|
261
|
+
into.appendChild(el('A', 'nav-item' + (m[1] === activeId ? ' nav-item--active' : ''), 'nav-' + m[1]));
|
|
262
|
+
}
|
|
263
|
+
sidebar.appendChild(group);
|
|
264
|
+
}
|
|
265
|
+
const root = treeNode('HTML');
|
|
266
|
+
const byId = (id) => { if (sidebar.id === id) return sidebar; let hit = null; sidebar.walk((c) => { if (!hit && c.id === id) hit = c; }); return hit; };
|
|
267
|
+
const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
|
|
268
|
+
const documentObj = {
|
|
269
|
+
body: treeNode('BODY'), documentElement: root, activeElement: null,
|
|
270
|
+
getElementById: byId, querySelector: () => null, querySelectorAll: () => [],
|
|
271
|
+
createElement: (t) => treeNode(t.toUpperCase()), createComment: (t) => Object.assign(treeNode('#comment', 8), { data: t }),
|
|
272
|
+
addEventListener() {}, removeEventListener() {},
|
|
273
|
+
};
|
|
274
|
+
const windowObj = {
|
|
275
|
+
location: { search: '' }, matchMedia: () => ({ matches: false }),
|
|
276
|
+
localStorage: mk({}), sessionStorage: mk(mode ? { 'hall-craft': mode } : {}),
|
|
277
|
+
};
|
|
278
|
+
const sandbox = { window: windowObj, document: documentObj, console, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
|
|
279
|
+
sandbox.globalThis = sandbox;
|
|
280
|
+
vm.createContext(sandbox);
|
|
281
|
+
vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
|
|
282
|
+
const craft = windowObj.OTBCraftMode;
|
|
283
|
+
craft.mount();
|
|
284
|
+
const fold = () => byId('craft-fold');
|
|
285
|
+
const ids = (n) => n.querySelectorAll('.nav-item').map((a) => a.id.slice(4));
|
|
286
|
+
const order = () => ids(sidebar);
|
|
287
|
+
return { craft, sidebar, fold, ids, order, byId };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
test('GOVERNING LEADS WITH THE CITY: its other tabs fold into one dropdown right under it', () => {
|
|
291
|
+
const before = bootTree().order();
|
|
292
|
+
const p = bootTree({ mode: 'governing' });
|
|
293
|
+
const fold = p.fold();
|
|
294
|
+
assert.ok(fold, 'the dropdown exists in Governing');
|
|
295
|
+
assert.equal(p.byId('nav-city').nextSibling, fold, 'directly under the City tab');
|
|
296
|
+
const own = p.craft.tabsFor('governing').filter((id) => !p.craft.ALWAYS_TABS.includes(id) && id !== 'city');
|
|
297
|
+
assert.deepEqual(JSON.parse(JSON.stringify(p.ids(fold))), JSON.parse(JSON.stringify(own)), 'every other Governing tab is in it, in the mode\'s order');
|
|
298
|
+
assert.ok(fold.classList.contains('nav-group--collapsed'), 'a dropdown starts closed');
|
|
299
|
+
const head = fold.querySelector('.nav-group__head');
|
|
300
|
+
assert.equal(head.getAttribute('aria-expanded'), 'false');
|
|
301
|
+
assert.equal(head.getAttribute('aria-controls'), 'nav-items-craft-fold', 'the head names the region it opens');
|
|
302
|
+
assert.match(head.innerHTML, /More governing/);
|
|
303
|
+
assert.ok(head.hasAttribute('data-craft-off') === false, 'its head shows while a tab inside does');
|
|
304
|
+
assert.ok(p.sidebar.querySelectorAll('.nav-group__head').find((h) => h.dataset.group === 'government').hasAttribute('data-craft-off'),
|
|
305
|
+
'the emptied Government head hides, so no label sits over nothing');
|
|
306
|
+
p.craft.choose('building');
|
|
307
|
+
assert.equal(p.fold(), null, 'leaving Governing removes the dropdown');
|
|
308
|
+
assert.deepEqual(p.order(), before, 'and puts every tab back exactly where it was');
|
|
309
|
+
p.craft.choose('governing');
|
|
310
|
+
p.craft.choose('governing');
|
|
311
|
+
assert.equal(p.sidebar.querySelectorAll('.craft-fold').length, 1, 'choosing it again builds no second dropdown');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('the dropdown opens on its own when the page you are on is inside it', () => {
|
|
315
|
+
const p = bootTree({ mode: 'governing', activeId: 'watch' });
|
|
316
|
+
assert.ok(!p.fold().classList.contains('nav-group--collapsed'), 'on Watch, the dropdown is open');
|
|
317
|
+
assert.equal(p.fold().querySelector('.nav-group__head').getAttribute('aria-expanded'), 'true');
|
|
318
|
+
assert.equal(bootTree({ mode: 'artistry' }).fold(), null, 'no other mode folds anything');
|
|
319
|
+
assert.equal(bootTree().fold(), null, 'nor does no mode');
|
|
320
|
+
});
|
package/tests/hall_city.mjs
CHANGED
|
@@ -271,8 +271,9 @@ test('the page holds only the wordmark, the switch, the city, one button and one
|
|
|
271
271
|
});
|
|
272
272
|
|
|
273
273
|
// task 1004544 (owner decision 2026-10-02): the city sits in the hall chrome
|
|
274
|
-
// behind the shell's full-screen switch, the sky's mechanism (task 1004327)
|
|
275
|
-
//
|
|
274
|
+
// behind the shell's full-screen switch, the sky's mechanism (task 1004327).
|
|
275
|
+
// The owner put the CITY on the sidebar on 2026-10-08 (the Governing mode's
|
|
276
|
+
// lead tab); the street still has no link of its own.
|
|
276
277
|
test('the city sits in the hall chrome, behind the shell\'s full-screen switch', () => {
|
|
277
278
|
const html = read('city.html').replace(/<!--[\s\S]*?-->/g, '');
|
|
278
279
|
assert.match(html, /<body data-page="city"[^>]*\sdata-fullscreen-switch>/, 'the bare attribute: the page lays itself out for full screen');
|
|
@@ -285,7 +286,8 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
|
|
|
285
286
|
assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
|
|
286
287
|
assert.match(css, /html\[data-fullscreen="on"\] \.city-top \{ right: 184px; \}/, 'the strip steps clear of the corner exit');
|
|
287
288
|
const shell = read('shell.js');
|
|
288
|
-
assert.
|
|
289
|
+
assert.match(shell, /\{ id: 'city', label: 'City', icon: 'city', href: '\/city', gate: 'perm:board\.vote\.cast' \}/, 'the city is a tab, gated on its page\'s atom');
|
|
290
|
+
assert.doesNotMatch(shell, /href:\s*'(\/builders)?\/street'/, 'the street has no sidebar link: the way in is the city');
|
|
289
291
|
});
|
|
290
292
|
|
|
291
293
|
test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -267,7 +267,9 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
267
267
|
// purpose (task 1003479, and tests/goal_map_page.mjs pinned it); the owner
|
|
268
268
|
// granted the sky that slot on 2026-09-24 (ADR 0227 §11), under Goals because
|
|
269
269
|
// it is the goals seen as one picture.
|
|
270
|
-
|
|
270
|
+
// 2026-10-08: 'city' — the governor city — joins after the Board Room, on the
|
|
271
|
+
// same atom and for the same reason (owner call; it leads the Governing mode).
|
|
272
|
+
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'goal-map', 'work', 'blockers', 'board-room', 'city', 'thinking', 'studio'],
|
|
271
273
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
272
274
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
273
275
|
// task 1003444: 'people' LEFT the group — the owner deleted the hall's
|
package/tests/hall_street.mjs
CHANGED
|
@@ -504,7 +504,8 @@ test('the street sits in the hall chrome, behind the shell\'s full-screen switch
|
|
|
504
504
|
assert.match(css, /\.street-stage \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
|
|
505
505
|
assert.match(css, /html\[data-fullscreen="on"\] \.street-stage \{ top: 0; left: 0; \}/);
|
|
506
506
|
assert.match(css, /html\[data-fullscreen="on"\] \.street-top \{ padding-right: 184px; \}/, 'the strip steps clear of the corner exit');
|
|
507
|
-
|
|
507
|
+
// The city became a tab on 2026-10-08 (owner); the street is still reached through it.
|
|
508
|
+
assert.doesNotMatch(read('shell.js'), /href:\s*'(\/builders)?\/street'/, 'no sidebar link (owner: leave it off the sidebar)');
|
|
508
509
|
});
|
|
509
510
|
|
|
510
511
|
test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-10-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-08-city-tab/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|