@bongos/core 1.21.35 → 1.21.37

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 (78) hide show
  1. package/.bongos-core.json +100 -80
  2. package/clients/bongos-client/index.d.ts +3 -3
  3. package/docs/api/openapi.json +25 -35
  4. package/docs/api-reference.md +1 -1
  5. package/docs/copy-inventory.md +8 -8
  6. package/docs/copy-registry.json +8 -8
  7. package/docs/module-api-changelog.md +4 -0
  8. package/docs/page-inventory.json +4 -1
  9. package/docs/page-readings.json +569 -523
  10. package/modules/autonomy/matter-acts.js +41 -0
  11. package/modules/autonomy/routes/autonomy.js +2 -0
  12. package/modules/government/board.js +4 -0
  13. package/modules/government/db.js +14 -0
  14. package/modules/government/decision-board.js +90 -11
  15. package/modules/government/docket-vocab.js +5 -0
  16. package/modules/government/docket.js +11 -0
  17. package/modules/government/routes/government.js +2 -1
  18. package/modules/hall-ui/public/atlas.html +3 -3
  19. package/modules/hall-ui/public/blockers.html +3 -3
  20. package/modules/hall-ui/public/board-room.html +3 -3
  21. package/modules/hall-ui/public/city.html +3 -3
  22. package/modules/hall-ui/public/collab.html +3 -3
  23. package/modules/hall-ui/public/commands.html +3 -3
  24. package/modules/hall-ui/public/copy-desk.html +3 -3
  25. package/modules/hall-ui/public/craft-mode.js +44 -6
  26. package/modules/hall-ui/public/deploy.html +3 -3
  27. package/modules/hall-ui/public/diagrams.html +3 -3
  28. package/modules/hall-ui/public/drachmae.html +3 -3
  29. package/modules/hall-ui/public/fleet.html +3 -3
  30. package/modules/hall-ui/public/gate.html +3 -3
  31. package/modules/hall-ui/public/goal-map.html +3 -3
  32. package/modules/hall-ui/public/goals.html +3 -3
  33. package/modules/hall-ui/public/government.html +3 -3
  34. package/modules/hall-ui/public/idea.html +3 -3
  35. package/modules/hall-ui/public/ideas.html +3 -3
  36. package/modules/hall-ui/public/index.html +3 -3
  37. package/modules/hall-ui/public/modules.html +3 -3
  38. package/modules/hall-ui/public/primer.html +3 -3
  39. package/modules/hall-ui/public/profile.html +3 -3
  40. package/modules/hall-ui/public/project-settings.html +3 -3
  41. package/modules/hall-ui/public/ranks.html +3 -3
  42. package/modules/hall-ui/public/roadmap.html +3 -3
  43. package/modules/hall-ui/public/roster.html +3 -3
  44. package/modules/hall-ui/public/sessions.html +3 -3
  45. package/modules/hall-ui/public/settings.html +3 -3
  46. package/modules/hall-ui/public/street-card.js +62 -2
  47. package/modules/hall-ui/public/street-desk.js +101 -8
  48. package/modules/hall-ui/public/street-inside.js +6 -3
  49. package/modules/hall-ui/public/street.css +15 -0
  50. package/modules/hall-ui/public/street.html +3 -3
  51. package/modules/hall-ui/public/street.js +73 -4
  52. package/modules/hall-ui/public/street.states.json +124 -0
  53. package/modules/hall-ui/public/studio.html +3 -3
  54. package/modules/hall-ui/public/style.css +121 -2
  55. package/modules/hall-ui/public/task.html +3 -3
  56. package/modules/hall-ui/public/thinking.html +3 -3
  57. package/modules/hall-ui/public/tweak-editor.html +3 -3
  58. package/modules/hall-ui/public/watch.html +3 -3
  59. package/modules/hall-ui/public/work.html +3 -3
  60. package/modules/hall-ui/records/craft-mode.md +8 -0
  61. package/modules/ideas/governor-docket.js +91 -0
  62. package/modules/lifecycle/governor-docket.js +69 -0
  63. package/modules/lifecycle/matter-acts.js +63 -0
  64. package/modules/lifecycle/routes/done-when.js +26 -12
  65. package/modules/lifecycle/routes/gate-approvals.js +74 -59
  66. package/modules/lifecycle/routes/lifecycle.js +2 -0
  67. package/package-lock.json +2 -2
  68. package/package.json +1 -1
  69. package/release-notes.json +16 -0
  70. package/src/bongos/route-rank-check.js +2 -0
  71. package/src/bongos/routes/builders.js +56 -1
  72. package/src/module-api.js +1 -1
  73. package/tests/craft_mode_looks.mjs +170 -0
  74. package/tests/craft_mode_nav.mjs +7 -5
  75. package/tests/governor_matter_acts.mjs +241 -0
  76. package/tests/hall_street_desk.mjs +244 -4
  77. package/tests/hall_street_inside.mjs +3 -3
  78. package/tests/hall_tweak_editor.mjs +1 -1
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ // modules/autonomy/matter-acts.js — what a PASSED Automation matter carries out
4
+ // (task 1004561, goal 1000125). When a project's decision table puts Automation
5
+ // to a sitting, the sitting's passed matter acts here, after the close commits:
6
+ // the switch on or off, or one goal allowed. Each act goes through the SAME db
7
+ // function its direct route calls (POST /autonomy/fence, POST
8
+ // /autonomy/fence/goals), so a matter can do nothing the route could not.
9
+ // The government module only checks the act's shape and calls in through the
10
+ // 'governor.matterAct' contribution point (a module never requires a sibling).
11
+
12
+ const api = require('../../src/module-api');
13
+ const db = require('./db');
14
+
15
+ const log = api.logger('autonomy');
16
+ const POINT = 'governor.matterAct';
17
+
18
+ const why = (ctx) => `a passed Automation matter (matter ${ctx && ctx.matterId})`;
19
+
20
+ async function switchFence(enabled, ctx) {
21
+ const row = await db.setFenceEnabled({ enabled, reason: why(ctx), builderId: ctx.decidedBy });
22
+ if (!row) return { done: false, reason: 'fence_uninitialised' };
23
+ log.info(`[gds] autonomy fence ${enabled ? 'ENABLED' : 'DISABLED'} by ${why(ctx)}, proposed by builder ${ctx.decidedBy}`);
24
+ return { done: true };
25
+ }
26
+
27
+ const ACTS = Object.freeze({
28
+ switch_on: (_action, ctx) => switchFence(true, ctx),
29
+ switch_off: (_action, ctx) => switchFence(false, ctx),
30
+ allow_goal: async (action, ctx) => {
31
+ await db.allowGoal({ goalId: action.goal_id, note: why(ctx), builderId: ctx.decidedBy });
32
+ log.info(`[gds] autonomy fence: goal ${action.goal_id} ALLOWED by ${why(ctx)}`);
33
+ return { done: true };
34
+ },
35
+ });
36
+
37
+ function contributeAutomationMatterActs() {
38
+ for (const [type, act] of Object.entries(ACTS)) api.contribute(POINT, { area: 'automation', type, act });
39
+ }
40
+
41
+ module.exports = { ACTS, contributeAutomationMatterActs };
@@ -108,6 +108,8 @@ const governorDocket = require('../governor-docket');
108
108
 
109
109
  module.exports = function buildAutonomyRouter() {
110
110
  governorDocket.contributeAutonomyToDocket();
111
+ // what a passed Automation matter carries out (task 1004561)
112
+ require('../matter-acts').contributeAutomationMatterActs();
111
113
  const router = express.Router();
112
114
 
113
115
  // The one Discord line for a runner idle on a setting (task 1004539). The port
@@ -1370,6 +1370,9 @@ async function boardPassForIdea(ideaId) {
1370
1370
  });
1371
1371
  }
1372
1372
 
1373
+ // boardPassForIdea for a list, in one read (task 1004560).
1374
+ const boardPassedIdeaIds = async (ids) => new Set(await db.listPassedSubjectIds('full_idea', ids));
1375
+
1373
1376
  // ── the summons: what is waiting on THIS member's vote (task 1003271) ───────
1374
1377
  //
1375
1378
  // THE PROBLEM THIS EXISTS TO FIX. Nine items sat open for two days with zero
@@ -1485,6 +1488,7 @@ module.exports = {
1485
1488
  maybeCloseAfterVote,
1486
1489
  latestRejectionForIdea,
1487
1490
  boardPassForIdea,
1491
+ boardPassedIdeaIds,
1488
1492
  validateProposedConstitution,
1489
1493
  proposeAmendment,
1490
1494
  boardRoomView,
@@ -712,6 +712,19 @@ async function getLatestPassedItem(subjectType, subjectId) {
712
712
  return rows[0] || null;
713
713
  }
714
714
 
715
+ // Which of these subjects the board passed, in ONE read (task 1004560: the
716
+ // Ideas-to-ratify desk asks for every open Full Idea at once).
717
+ async function listPassedSubjectIds(subjectType, subjectIds) {
718
+ const ids = (subjectIds || []).map(Number).filter((n) => Number.isInteger(n) && n > 0);
719
+ if (!ids.length) return [];
720
+ const { rows } = await pool.query(
721
+ `SELECT DISTINCT subject_id FROM government_board_items
722
+ WHERE subject_type = $1 AND subject_id = ANY($2::bigint[]) AND outcome = 'passed'`,
723
+ [subjectType, ids],
724
+ );
725
+ return rows.map((r) => String(r.subject_id));
726
+ }
727
+
715
728
  // The expiry sweep's read (R11): every OPEN item whose deadline has passed, on
716
729
  // the database clock, oldest deadline first. Served by the partial index
717
730
  // government_board_items_open_by_deadline (government_003 built it for exactly
@@ -872,6 +885,7 @@ module.exports = {
872
885
  getExpiredOpenBoardItems,
873
886
  getLatestReturnedItemWithObjection,
874
887
  getLatestPassedItem,
888
+ listPassedSubjectIds,
875
889
  createAmendment,
876
890
  getAmendment,
877
891
  listDecisionRules,
@@ -50,7 +50,9 @@ async function applyPassedDecisionRules(client, closedRow, amendment) {
50
50
  }
51
51
 
52
52
  // A passed matter: act where the building can act by itself, and build the
53
- // announcement. Never throws — the decision already stands.
53
+ // announcement. Never throws — the decision already stands. Runs after the
54
+ // close commits (People since task 1004529; Automation, Release, Review and
55
+ // Treasury since task 1004561, through their owning modules' acts).
54
56
  async function actOnPassedMatter(closed) {
55
57
  const area = decisionRules.areaForSubjectType(closed.subject_type);
56
58
  let matter = null;
@@ -71,11 +73,30 @@ async function actOnPassedMatter(closed) {
71
73
  await db.unassignRank({ builderId: action.builder_id, rankKey: action.rank_key });
72
74
  result = { ...result, action: action.type, done: true, builder_id: String(action.builder_id), rank_key: action.rank_key };
73
75
  }
76
+ } else if (area !== 'people' && action && MATTER_ACTIONS[area]) {
77
+ // The owning module carries it out, through its direct route's own
78
+ // function (task 1004561). Re-validated first: the stored row was checked
79
+ // when opened and is immutable since, so a refusal means it was altered.
80
+ const v = validateMatterAction(area, action);
81
+ const doer = v.ok ? matterActFor(area, v.action.type) : null;
82
+ if (!v.ok) {
83
+ result = { ...result, action: action.type || null, done: false, reason: 'bad_action' };
84
+ } else if (!doer) {
85
+ result = { ...result, action: v.action.type, done: false, reason: 'not_wired' };
86
+ } else {
87
+ const out = (await doer.act(v.action, { decidedBy: matter.proposed_by, matterId: String(matter.id) })) || {};
88
+ result = { ...result, action: v.action.type, done: out.done === true, ...(out.reason ? { reason: String(out.reason) } : {}) };
89
+ }
74
90
  }
75
91
  } catch (err) {
76
92
  log.error(`[government] matter ${closed.subject_id} passed at ${area} but its act did NOT run (re-drivable from the passed item): ${err && err.message}`);
77
93
  result = { ...result, done: false, reason: 'act_failed' };
78
94
  }
95
+ // A refusal (already decided elsewhere, the module off) leaves the decision
96
+ // standing; it is said loudly here and carried as `acted: false`.
97
+ if (result.action && !result.done && result.reason !== 'act_failed') {
98
+ log.warn(`[government] matter ${closed.subject_id} passed at ${area} but its act was refused: ${result.reason || 'not done'}`);
99
+ }
79
100
  return {
80
101
  result,
81
102
  payload: {
@@ -153,22 +174,74 @@ async function decisionRulesView() {
153
174
  // the whole point — People decided by a council's consent sits as exactly that.
154
175
  const MATTER_TITLE_MAX = 200;
155
176
  const MATTER_DETAIL_MAX = 5000;
156
- const MATTER_ACTION_TYPES = Object.freeze(['assign_rank', 'unassign_rank']);
177
+ // What a passed matter may carry out, per building (task 1004561). People acts
178
+ // here, in this module; every other act is carried out by the module that owns
179
+ // the building's direct route, through the same function that route calls,
180
+ // contributed to 'governor.matterAct' as { area, type, act }. This table is only
181
+ // the SHAPE of each act (field → kind), checked when the matter is opened and
182
+ // again before it acts. 'id' is a positive integer, 'rank' a custom rank key,
183
+ // 'usd' a non-negative amount or null (no limit).
184
+ const MATTER_ACTIONS = Object.freeze({
185
+ people: Object.freeze({
186
+ assign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
187
+ unassign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
188
+ }),
189
+ automation: Object.freeze({
190
+ switch_on: Object.freeze({}),
191
+ switch_off: Object.freeze({}),
192
+ allow_goal: Object.freeze({ goal_id: 'id' }),
193
+ }),
194
+ release: Object.freeze({
195
+ approve_override: Object.freeze({ request_id: 'id' }),
196
+ approve_gate: Object.freeze({ pr: 'id' }),
197
+ }),
198
+ review: Object.freeze({
199
+ satisfy_criterion: Object.freeze({ criterion_id: 'id' }),
200
+ }),
201
+ treasury: Object.freeze({
202
+ set_budget: Object.freeze({ builder_id: 'id', monthly_usd: 'usd' }),
203
+ }),
204
+ });
205
+ const MATTER_ACT_POINT = 'governor.matterAct';
206
+ const MAX_BUDGET_USD = 100000;
207
+
208
+ function fieldValue(kind, raw) {
209
+ if (kind === 'id') {
210
+ const n = Number(raw);
211
+ return Number.isSafeInteger(n) && n > 0 && raw !== null && raw !== '' && typeof raw !== 'boolean' ? { ok: true, v: n } : { ok: false };
212
+ }
213
+ if (kind === 'rank') return typeof raw === 'string' && /^[a-z][a-z0-9-]*$/.test(raw) && raw.length <= 64 ? { ok: true, v: raw } : { ok: false };
214
+ if (kind === 'usd') {
215
+ if (raw === null) return { ok: true, v: null };
216
+ return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 && raw <= MAX_BUDGET_USD ? { ok: true, v: raw } : { ok: false };
217
+ }
218
+ return { ok: false };
219
+ }
157
220
 
158
221
  function validateMatterAction(area, action) {
159
222
  if (action == null) return { ok: true, action: null };
160
- if (area !== 'people') return { ok: false, reason: 'action_not_supported_here', expected: 'only People can act by itself today (assign_rank / unassign_rank)' };
223
+ const acts = MATTER_ACTIONS[area];
224
+ if (!acts) return { ok: false, reason: 'action_not_supported_here', expected: `a matter acts by itself only at ${Object.keys(MATTER_ACTIONS).join(', ')}` };
161
225
  if (typeof action !== 'object' || Array.isArray(action)) return { ok: false, reason: 'bad_action' };
226
+ const shape = acts[action.type];
227
+ if (!shape) return { ok: false, reason: 'bad_action', expected: Object.keys(acts) };
162
228
  for (const k of Object.keys(action)) {
163
- if (!['type', 'builder_id', 'rank_key'].includes(k)) return { ok: false, reason: 'unknown_field', field: `action.${k}` };
229
+ if (k !== 'type' && !Object.prototype.hasOwnProperty.call(shape, k)) return { ok: false, reason: 'unknown_field', field: `action.${k}` };
164
230
  }
165
- if (!MATTER_ACTION_TYPES.includes(action.type)) return { ok: false, reason: 'bad_action', expected: MATTER_ACTION_TYPES };
166
- const builderId = Number(action.builder_id);
167
- if (!Number.isInteger(builderId) || builderId <= 0) return { ok: false, reason: 'bad_action', field: 'action.builder_id' };
168
- if (typeof action.rank_key !== 'string' || !/^[a-z][a-z0-9-]*$/.test(action.rank_key) || action.rank_key.length > 64) {
169
- return { ok: false, reason: 'bad_action', field: 'action.rank_key' };
231
+ const out = { type: action.type };
232
+ for (const [k, kind] of Object.entries(shape)) {
233
+ if (!Object.prototype.hasOwnProperty.call(action, k)) return { ok: false, reason: 'bad_action', field: `action.${k}` };
234
+ const f = fieldValue(kind, action[k]);
235
+ if (!f.ok) return { ok: false, reason: 'bad_action', field: `action.${k}` };
236
+ out[k] = f.v;
170
237
  }
171
- return { ok: true, action: { type: action.type, builder_id: builderId, rank_key: action.rank_key } };
238
+ return { ok: true, action: out };
239
+ }
240
+
241
+ // The owning module's act for (area, type), or null when that module is off.
242
+ function matterActFor(area, type) {
243
+ return api.contributions(MATTER_ACT_POINT)
244
+ .find((c) => c && c.area === area && c.type === type && typeof c.act === 'function') || null;
172
245
  }
173
246
 
174
247
  async function openMatter({ area, title, detailMd = null, action = null, proposedBy } = {}) {
@@ -190,7 +263,10 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
190
263
  };
191
264
  }
192
265
  if (!resolved.sitting) return { ok: false, reason: 'no_one_holds_this', area, what_this_means: `No one holds ${resolved.name} yet, so nothing there can be decided.` };
193
- if (a.action) {
266
+ if (a.action && area !== 'people' && !matterActFor(area, a.action.type)) {
267
+ return { ok: false, reason: 'action_not_wired', area, what_this_means: `Nothing at ${resolved.name} can carry that out right now, so it would pass and do nothing. Turn on the module that owns ${resolved.name}, or file the matter without an act.` };
268
+ }
269
+ if (a.action && area === 'people') {
194
270
  const rank = await db.getRankByKey(a.action.rank_key);
195
271
  if (!rank) return { ok: false, reason: 'unknown_rank', rank_key: a.action.rank_key };
196
272
  if (rank.is_system) return { ok: false, reason: 'seeded_rank', rank_key: a.action.rank_key, expected: 'a custom rank — a seeded rank follows the builder\'s rank' };
@@ -295,6 +371,9 @@ module.exports = {
295
371
  proposeDecisionRules,
296
372
  decisionRulesView,
297
373
  validateMatterAction,
374
+ matterActFor,
375
+ MATTER_ACTIONS,
376
+ MATTER_ACT_POINT,
298
377
  openMatter,
299
378
  checkActAlone,
300
379
  };
@@ -101,6 +101,11 @@ const DESKS = Object.freeze([
101
101
  ['automation.switch', 'automation'], ['automation.goals', 'automation'], ['automation.runners', 'automation'],
102
102
  ['people.invite', 'people'], ['release.core', 'release'],
103
103
  ['rules.settings', 'rules'], ['rules.modules', 'rules'], ['treasury.backup', 'treasury'],
104
+ // the Planning desks (task 1004560): the goals in their worked order, and the
105
+ // ideas a ratifier may take from here
106
+ ['planning.order', 'planning'], ['planning.ratify', 'planning'],
107
+ // task 1004561: a builder's monthly spending limit, and a sign-off undone
108
+ ['treasury.budgets', 'treasury'], ['review.signed', 'review'],
104
109
  ].map(([key, building]) => Object.freeze({ key, building })));
105
110
 
106
111
  module.exports = {
@@ -250,8 +250,19 @@ function normalizeDeskRow(raw, { desk, building }) {
250
250
  opened_at: iso(raw.opened_at),
251
251
  ref: raw.ref && typeof raw.ref === 'object' ? { type: String(raw.ref.type), id: String(raw.ref.id) } : null,
252
252
  acts,
253
+ ...deskExtras(raw),
253
254
  };
254
255
  }
256
+ // Two optional shapes a desk row may carry (task 1004560): `pips`, promises kept
257
+ // of total drawn as squares (at most 12), and `set`, the ordered set the row sits
258
+ // in (a version id) so the desk can reorder its rows and save the order whole.
259
+ function deskExtras(raw) {
260
+ const out = {};
261
+ const p = raw.pips;
262
+ if (p && Number.isInteger(p.of) && p.of > 0 && p.of <= 12 && Number.isInteger(p.on) && p.on >= 0 && p.on <= p.of) out.pips = { on: p.on, of: p.of };
263
+ if (typeof raw.set === 'string' && /^[A-Za-z0-9._-]{1,64}$/.test(raw.set)) out.set = raw.set;
264
+ return out;
265
+ }
255
266
 
256
267
  // One floor as a contributor handed it → { count, line, lines, hot }. Pure.
257
268
  // Anything that is not a plain object is not a floor (→ null, read as
@@ -110,6 +110,7 @@ const governmentPort = {
110
110
  // is no counterpart that records an outcome, and there must never be one: an
111
111
  // outcome is written by a close, never by the module reading it.
112
112
  boardPassForIdea: board.boardPassForIdea,
113
+ boardPassedIdeaIds: board.boardPassedIdeaIds,
113
114
  // task 1004429 (BV2.PS04): a finished genesis stage is put to the board — the
114
115
  // genesis home (task 1004423) calls this when a stage's tasks are done. The
115
116
  // finished-tasks gate lives inside board.js; the item is an ordinary one under
@@ -789,7 +790,7 @@ module.exports = function buildGovernmentRouter() {
789
790
  proposedBy: req.builder.id,
790
791
  });
791
792
  if (!out.ok) {
792
- if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this') {
793
+ if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this' || out.reason === 'action_not_wired') {
793
794
  return res.fail(out.reason, 409, { area: out.area, ...(out.gate ? { gate: out.gate } : {}), what_this_means: out.what_this_means });
794
795
  }
795
796
  return res.fail('invalid_matter', 400, {
@@ -91,9 +91,9 @@
91
91
 
92
92
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
93
93
 
94
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
95
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
96
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
94
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
95
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
96
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
97
97
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
98
98
  <script src="/builders/palette.js" defer></script>
99
99
  <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
@@ -50,9 +50,9 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
54
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
55
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
53
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
54
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
55
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
56
56
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
57
57
  <script src="/builders/palette.js" defer></script>
58
58
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -60,9 +60,9 @@
60
60
 
61
61
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
62
62
 
63
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
64
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
65
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
63
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
64
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
65
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
66
66
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
67
67
  <script src="/builders/palette.js" defer></script>
68
68
  <script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
@@ -83,9 +83,9 @@
83
83
  </div>
84
84
  </div>
85
85
 
86
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
87
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
88
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
86
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
87
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
88
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
89
89
  <script src="/builders/palette.js" defer></script>
90
90
  <script src="/builders/city-draw.js?v=2026-10-02-city1"></script>
91
91
  <script src="/builders/city.js?v=2026-10-02-city1"></script>
@@ -148,9 +148,9 @@
148
148
 
149
149
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
150
150
 
151
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
152
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
153
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
151
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
152
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
153
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
154
154
  <script src="/builders/palette.js" defer></script>
155
155
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
156
156
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
@@ -54,9 +54,9 @@
54
54
 
55
55
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
56
56
 
57
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
58
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
59
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
57
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
58
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
59
+ <script src="/builders/shell.js?v=2026-10-03-looks"></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
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -177,9 +177,9 @@
177
177
 
178
178
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
179
179
 
180
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
181
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
182
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
180
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
181
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
182
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
183
183
  <script src="/builders/palette.js" defer></script>
184
184
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
185
185
  <script src="/builders/hall-kit.js"></script>
@@ -8,8 +8,9 @@
8
8
  //
9
9
  // It is a MODE the existing hall wears, never a second hall (the founding-mode
10
10
  // precedent, docs/design/project-startup-direction.md). The nav follows the mode
11
- // (task 1004493, MODE_TABS below); the tools and colours that follow it are their
12
- // own tasks (1004494 onward). Legacy mode is the hall exactly as it was.
11
+ // (task 1004493, MODE_TABS below), and so does the look (task 1004495, LOOKS
12
+ // below); the mode tools are their own tasks (1004496 onward). Legacy mode is the
13
+ // hall exactly as it was.
13
14
  //
14
15
  // NO MODE is the default and the fallback: a reader who closes the picker, a
15
16
  // browser whose storage throws, a page that opts out — none of them get the
@@ -44,15 +45,15 @@
44
45
 
45
46
  // ---------- each mode's tabs (task 1004493) ----------
46
47
  // A mode shows only the nav items its craft needs, by NAV id (shell.js). ALWAYS
47
- // is the owner's list of tabs no mode may hide (2026-10-01: Builders first), and
48
- // every mode's list is ALWAYS plus its own. A mode with no entry here — Legacy,
48
+ // is the owner's list of tabs no mode may hide (2026-10-01: Builders first;
49
+ // 2026-10-03: Collab and Profile too), and every mode's list is ALWAYS plus its own. A mode with no entry here — Legacy,
49
50
  // or no mode at all — shows every tab, exactly as the hall did before.
50
51
  // Government lives in Governing alone, and Governing is the shortest list.
51
52
  // Hiding is cosmetic: a hidden tab's page still opens by its URL (ADR 0016).
52
- var ALWAYS_TABS = ['roster'];
53
+ var ALWAYS_TABS = ['roster', 'collab', 'profile'];
53
54
  var MODE_TABS = {
54
55
  building: ['home', 'roadmap', 'goals', 'goal-map', 'work', 'blockers', 'thinking', 'studio',
55
- 'standing', 'profile', 'collab',
56
+ 'standing',
56
57
  'primer', 'ranks', 'economy', 'commands', 'diagrams', 'atlas', 'modules', 'apidocs'],
57
58
  artistry: ['studio', 'goals', 'thinking'],
58
59
  ideating: ['goal-map', 'thinking', 'goals'],
@@ -91,6 +92,40 @@
91
92
  else head.removeAttribute('data-craft-off');
92
93
  }
93
94
  }
95
+ // ---------- each mode's look (task 1004495) ----------
96
+ // Artistry, Ideating and Governing each wear a look: the colour tokens in
97
+ // style.css (keyed on html[data-craft]), plus two things only this script can
98
+ // draw — the mode's words beside the page title, and its door's scene framed
99
+ // under the brand (artHtml, the picker's own art, so the two never drift).
100
+ // Building, Legacy and no mode have no entry here and wear nothing.
101
+ var LOOKS = { artistry: 'the studio · night', ideating: 'the sky', governing: 'the city' };
102
+ function remove(el) { if (el && el.parentNode) el.parentNode.removeChild(el); }
103
+ function applyLook(mode) {
104
+ var look = Object.prototype.hasOwnProperty.call(LOOKS, mode) ? LOOKS[mode] : null;
105
+ var plate = document.getElementById('craft-plate');
106
+ if (plate && plate.getAttribute('data-plate') !== (look ? mode : null)) { remove(plate); plate = null; }
107
+ var nav = document.getElementById('app-sidebar');
108
+ var brand = look && nav && nav.querySelector ? nav.querySelector('.app-sidebar__brand') : null;
109
+ if (!plate && brand && brand.parentNode) {
110
+ plate = document.createElement('div');
111
+ plate.id = 'craft-plate';
112
+ plate.className = 'craft-plate';
113
+ plate.setAttribute('data-plate', mode);
114
+ plate.setAttribute('aria-hidden', 'true');
115
+ plate.innerHTML = artHtml(mode);
116
+ brand.parentNode.insertBefore(plate, brand.nextSibling);
117
+ }
118
+ var mark = document.getElementById('craft-eyebrow');
119
+ if (!look) { remove(mark); return; }
120
+ var title = document.getElementById('topbar-title');
121
+ if (!mark && title && title.parentNode) {
122
+ mark = document.createElement('span');
123
+ mark.id = 'craft-eyebrow';
124
+ mark.className = 'craft-eyebrow';
125
+ title.parentNode.insertBefore(mark, title.nextSibling);
126
+ }
127
+ if (mark) mark.textContent = look;
128
+ }
94
129
  // sessionStorage: this session's mode, or that the picker was already shown.
95
130
  var MODE_KEY = 'hall-craft';
96
131
  var ASKED_KEY = 'hall-craft-asked';
@@ -138,6 +173,7 @@
138
173
  var sw = document.getElementById('craft-switch');
139
174
  if (sw) sw.classList.toggle('craft-switch--unset', !mode);
140
175
  applyNav();
176
+ applyLook(mode);
141
177
  try { document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: mode } })); } catch (_) { /* old engine */ }
142
178
  }
143
179
 
@@ -344,6 +380,8 @@
344
380
  ALWAYS_TABS: ALWAYS_TABS.slice(),
345
381
  tabsFor: tabsFor,
346
382
  applyNav: applyNav,
383
+ LOOKS: LOOKS,
384
+ artHtml: artHtml,
347
385
  switchHtml: switchHtml,
348
386
  mount: mount,
349
387
  onAccess: onAccess,
@@ -74,9 +74,9 @@
74
74
 
75
75
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
76
76
 
77
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
78
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
79
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
77
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
78
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
79
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
80
80
  <script src="/builders/palette.js" defer></script>
81
81
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
82
82
  <script src="/builders/hall-kit.js"></script>
@@ -97,9 +97,9 @@
97
97
 
98
98
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
99
99
  time so the chrome exists before the deferred page scripts execute. -->
100
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
101
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
102
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
100
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
101
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
102
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -98,9 +98,9 @@
98
98
 
99
99
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
100
100
 
101
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
102
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
103
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
101
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
102
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
103
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
104
104
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
105
105
  <script src="/builders/palette.js" defer></script>
106
106
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -54,9 +54,9 @@
54
54
 
55
55
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
56
56
 
57
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
58
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
59
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
57
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
58
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
59
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
62
62
  <script src="/builders/hall-kit.js"></script>
@@ -77,9 +77,9 @@
77
77
  <div id="gate-modal-root"></div>
78
78
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
79
79
 
80
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
81
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
82
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
80
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
81
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
82
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
83
83
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
84
84
  <script src="/builders/palette.js" defer></script>
85
85
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -137,9 +137,9 @@
137
137
  </div>
138
138
  </div>
139
139
 
140
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
141
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
142
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
140
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
141
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
142
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
143
143
  <script src="/builders/palette.js" defer></script>
144
144
 
145
145
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -230,9 +230,9 @@
230
230
 
231
231
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
232
232
 
233
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
234
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
235
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
233
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
234
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
235
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
236
236
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
237
237
  <script src="/builders/palette.js" defer></script>
238
238
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -134,9 +134,9 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
138
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
139
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
137
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
138
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
139
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
140
140
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
141
141
  <script src="/builders/palette.js" defer></script>
142
142
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -50,9 +50,9 @@
50
50
 
51
51
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
52
52
 
53
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
54
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
55
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
53
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
54
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
55
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref