@bongos/core 1.24.74 → 1.24.76

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 (43) hide show
  1. package/.bongos-core.json +77 -42
  2. package/.claude/skills/blocker-review/SKILL.md +10 -0
  3. package/clients/bongos-client/index.d.ts +1 -1
  4. package/docs/api/openapi.json +6 -2
  5. package/docs/api-reference.md +2 -2
  6. package/docs/copy-inventory.md +55 -47
  7. package/docs/copy-registry.json +143 -71
  8. package/docs/module-api-changelog.md +2 -0
  9. package/docs/packs/engineer.md +2 -2
  10. package/docs/page-inventory.json +2 -0
  11. package/docs/page-readings.json +10 -10
  12. package/modules/government/board.js +10 -4
  13. package/modules/government/db.js +7 -5
  14. package/modules/government/genesis-close.js +45 -0
  15. package/modules/government/migrations/government_038_vote_cast_via.sql +23 -0
  16. package/modules/government/routes/government.js +4 -5
  17. package/modules/hall-ui/public/board-room.js +1 -0
  18. package/modules/hall-ui/public/deploy.html +1 -1
  19. package/modules/hall-ui/public/deploy.js +56 -5
  20. package/modules/hall-ui/public/genesis-home.js +7 -12
  21. package/modules/hall-ui/public/home-city.js +7 -2
  22. package/modules/hall-ui/public/home-idea-box.css +33 -0
  23. package/modules/hall-ui/public/home-idea-box.js +106 -0
  24. package/modules/hall-ui/public/index.html +2 -0
  25. package/modules/hall-ui/public/project-settings.css +15 -0
  26. package/modules/hall-ui/records/home-city.md +21 -0
  27. package/modules/ideas/routes/blockers.js +47 -1
  28. package/modules/onboarding/genesis-home.js +65 -8
  29. package/modules/onboarding/routes/onboarding.js +15 -1
  30. package/package-lock.json +2 -2
  31. package/package.json +1 -1
  32. package/release-notes.json +18 -0
  33. package/scripts/bongos/blocker-recheck.js +111 -0
  34. package/scripts/bongos/government-walk.js +4 -4
  35. package/scripts/gds/blocker-recheck.js +7 -0
  36. package/scripts/hall-preview/server.js +1 -0
  37. package/src/module-api.js +1 -1
  38. package/tests/deploy_page_projects.mjs +49 -0
  39. package/tests/genesis_home.mjs +86 -1
  40. package/tests/government_agent_asks.mjs +92 -2
  41. package/tests/government_board_genesis_stage.mjs +83 -0
  42. package/tests/government_seed.mjs +1 -0
  43. package/tests/home_idea_box.mjs +178 -0
@@ -215,7 +215,7 @@
215
215
  "surface": "builders",
216
216
  "title": "Board Room",
217
217
  "states_read": ["room","out","sitting-meta","objection-form"],
218
- "files_hash": "9461cd6cbc270ae2",
218
+ "files_hash": "0e6e17f03283a4a6",
219
219
  "reading_hash": "020553cb2a438d56",
220
220
  "counts": {"lines":51,"placed":18,"shared":9,"unplaced":24},
221
221
  "omitted": {"data":15},
@@ -246,18 +246,18 @@
246
246
  {"key":"L0023","section":"{…} Full Idea","text":"Scope it as best as you can here","placement":"unplaced"},
247
247
  {"key":"L0024","section":"{…} Full Idea","text":"What should an {…} or {…} know prior to building","placement":"unplaced"},
248
248
  {"key":"L0025","section":"{…} Full Idea","text":"How is this long term justified in the project","placement":"unplaced"},
249
- {"key":"L0026","section":"{…} Full Idea","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":480,"string_id":"d73256fbc395","occurrences":9},
249
+ {"key":"L0026","section":"{…} Full Idea","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":484,"string_id":"d73256fbc395","occurrences":9},
250
250
  {"key":"L0027","section":"Constitutional amendment amendment","text":"Constitutional amendment","placement":"unplaced","occurrences":3},
251
251
  {"key":"L0028","section":"Constitutional amendment amendment","text":"amendment","placement":"unplaced","occurrences":3},
252
252
  {"key":"L0029","section":"Constitutional amendment amendment","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":173,"string_id":"9a803563c948"},
253
253
  {"key":"L0030","section":"Constitutional amendment amendment","text":"no clock — waits for a {…} yes","placement":"unplaced"},
254
254
  {"key":"L0031","section":"Constitutional amendment amendment","text":"Proposes","placement":"unplaced","occurrences":3},
255
- {"key":"L0032","section":"Constitutional amendment amendment","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":480,"string_id":"d73256fbc395","occurrences":3},
255
+ {"key":"L0032","section":"Constitutional amendment amendment","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":484,"string_id":"d73256fbc395","occurrences":3},
256
256
  {"key":"L0033","section":"Constitutional amendment amendment","text":"Closed sittings","placement":"unplaced"},
257
257
  {"key":"L0034","section":"{…} Full Idea","text":"passed","placement":"placed","file":"modules/hall-ui/public/government.js","line":679,"string_id":"da5368d10f85","occurrences":8},
258
258
  {"key":"L0035","section":"{…} and {…}","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":5},
259
259
  {"key":"L0036","section":"{…} and {…}","text":"Full Idea","placement":"unplaced","occurrences":5},
260
- {"key":"L0037","section":"{…} and {…}","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":480,"string_id":"d73256fbc395","occurrences":5},
260
+ {"key":"L0037","section":"{…} and {…}","text":"{…} · who votes, and how it {…}","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":484,"string_id":"d73256fbc395","occurrences":5},
261
261
  {"key":"L0038","section":"Constitutional amendment amendment","text":"passed","placement":"placed","file":"modules/hall-ui/public/government.js","line":679,"string_id":"da5368d10f85","occurrences":2},
262
262
  {"key":"L0039","section":"Constitutional amendment amendment","text":"{\"{…}_{…}\":\"{…}\",\"membership\":\"{…}\",\"window_minutes\":1440,\"close_early_on_{…}_turnout\":true}","placement":"unplaced","occurrences":2},
263
263
  {"key":"L0040","section":"Constitutional amendment amendment","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":2},
@@ -604,7 +604,7 @@
604
604
  "surface": "builders",
605
605
  "title": "Deploy",
606
606
  "states_read": ["work","out"],
607
- "files_hash": "a0ab8445ee5bf294",
607
+ "files_hash": "5fa46643d448b50c",
608
608
  "reading_hash": "76c11d0fbc739610",
609
609
  "counts": {"lines":19,"placed":5,"shared":9,"unplaced":5},
610
610
  "omitted": {"data":3},
@@ -974,8 +974,8 @@
974
974
  "surface": "builders",
975
975
  "title": "Builders' Hall",
976
976
  "states_read": ["home","out","founding","genesis","decide","demo"],
977
- "files_hash": "5c8e267006cb62e1",
978
- "reading_hash": "4cc2e5e86483a66e",
977
+ "files_hash": "fc8a81cb746dba23",
978
+ "reading_hash": "ae7a0c466a3aee85",
979
979
  "counts": {"lines":28,"placed":9,"shared":10,"unplaced":9},
980
980
  "omitted": {"data":4},
981
981
  "notes": ["founding: an action failed (page.focus: Timeout 6000ms exceeded.); read as it stood"],
@@ -999,7 +999,7 @@
999
999
  {"key":"L0017","section":"Needs your attention13","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":320,"string_id":"e181637c5a5e"},
1000
1000
  {"key":"L0018","section":"Needs your attention13","text":"Requires project {…} fix","placement":"unplaced"},
1001
1001
  {"key":"L0019","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":533,"string_id":"1883ad7f07fc"},
1002
- {"key":"L0020","section":"Needs your attention13","text":"core 1.24.71","placement":"unplaced"},
1002
+ {"key":"L0020","section":"Needs your attention13","text":"core 1.24.74","placement":"unplaced"},
1003
1003
  {"key":"L0021","section":"Needs your attention13","text":"Skip","placement":"unplaced"},
1004
1004
  {"key":"L0022","section":"Needs your attention13","text":"Built on","placement":"placed","file":"modules/hall-ui/public/welcome-boot.js","line":181,"string_id":"fef59346ffee"},
1005
1005
  {"key":"L0023","section":"Needs your attention13","text":"cloudbongos.com","placement":"unplaced"},
@@ -1346,7 +1346,7 @@
1346
1346
  "surface": "builders",
1347
1347
  "title": "Money",
1348
1348
  "states_read": ["money","hosting","out"],
1349
- "files_hash": "cbf3f8ed30ccfff6",
1349
+ "files_hash": "282ef6a957569657",
1350
1350
  "reading_hash": "285d714c7aba1fc9",
1351
1351
  "counts": {"lines":82,"placed":25,"shared":9,"unplaced":48},
1352
1352
  "omitted": {"data":25},
@@ -2323,7 +2323,7 @@
2323
2323
  "surface": "builders",
2324
2324
  "title": "Project settings",
2325
2325
  "states_read": ["default"],
2326
- "files_hash": "2c0f365687004dda",
2326
+ "files_hash": "f56e2084906369f2",
2327
2327
  "reading_hash": "849399ad428ae9c7",
2328
2328
  "counts": {"lines":42,"placed":22,"shared":9,"unplaced":11},
2329
2329
  "omitted": {"data":31},
@@ -255,18 +255,21 @@ function validateVote({ direction, reasonMd, sectionKey } = {}) {
255
255
  // { ok: true, vote, fresh: true, karma_delta, self_cast } — recorded now
256
256
  // { ok: true, vote, fresh: false, self_cast } — idempotent re-cast
257
257
  // { ok: false, reason: 'item_not_found' | 'item_closed' | 'vote_window_expired'
258
- // | 'not_board_member' | <validateVote reason> }
258
+ // | 'not_board_member' | 'bad_cast_via' | <validateVote reason> }
259
+ // `castVia` (task 1005121, government_038) is internal — never read from a request: 'ballot', or 'stage_close' (genesis-close.js: a founder's confirm given by closing a genesis stage — genesis_stage + yes only, no vote karma).
259
260
  // 'anti_self_vote' is GONE as of task 1003273 — an author may cast; what they
260
261
  // cannot do is decide alone, which shouldCloseAfterVote and tallyOutcome enforce.
261
262
  // A self-cast is recorded and visible but earns no karma (karma_delta: 0).
262
- async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null } = {}) {
263
+ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null, castVia = 'ballot' } = {}) {
263
264
  const id = Number(itemId);
264
265
  if (!Number.isFinite(id) || id <= 0) return { ok: false, reason: 'item_not_found' };
266
+ if (castVia !== 'ballot' && castVia !== 'stage_close') return { ok: false, reason: 'bad_cast_via' };
265
267
  const v = validateVote({ direction, reasonMd, sectionKey });
266
268
  if (!v.ok) return v;
267
269
 
268
270
  const item = await db.getBoardItemForVote(id);
269
271
  if (!item) return { ok: false, reason: 'item_not_found' };
272
+ if (castVia === 'stage_close' && (item.subject_type !== 'genesis_stage' || v.direction !== 'yes')) return { ok: false, reason: 'bad_cast_via' };
270
273
  if (item.closed_at) return { ok: false, reason: 'item_closed' };
271
274
  // The clock is checked here as well as by the R11 poller: a vote landing in
272
275
  // the gap between expiry and the sweep would be a vote after time. The
@@ -309,6 +312,7 @@ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKe
309
312
 
310
313
  const inserted = await db.insertBoardVote({
311
314
  itemId: id, voterId, direction: v.direction, reasonMd: v.reasonMd, sectionKey: v.sectionKey,
315
+ ...(castVia === 'ballot' ? {} : { castVia }), // db defaults a ballot
312
316
  });
313
317
  if (!inserted) {
314
318
  // UNIQUE(item_id, voter_id) — the idempotent path. Return what stands.
@@ -317,8 +321,8 @@ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKe
317
321
  }
318
322
  // Karma rides the FRESH insert only, and the whole route refused a closed or
319
323
  // expired item above, so every paid vote is by construction in-window.
320
- if (isSelfCast) {
321
- return { ok: true, vote: inserted, fresh: true, karma_delta: 0, self_cast: true };
324
+ if (isSelfCast || castVia === 'stage_close') {
325
+ return { ok: true, vote: inserted, fresh: true, karma_delta: 0, self_cast: isSelfCast };
322
326
  }
323
327
  await db.insertVoteKarma({ voterId, itemId: id, delta: VOTE_KARMA_DELTA });
324
328
  return { ok: true, vote: inserted, fresh: true, karma_delta: VOTE_KARMA_DELTA, self_cast: false };
@@ -1103,6 +1107,7 @@ async function genesisStageStatus() {
1103
1107
  remaining_seconds: item.remaining_seconds ?? null,
1104
1108
  voted: cast.map((v) => ({
1105
1109
  builder_id: String(v.voter_id), github_login: v.github_login, display_name: v.display_name, direction: v.direction,
1110
+ cast_via: v.cast_via || 'ballot',
1106
1111
  })),
1107
1112
  waiting_on: members.filter((m) => !castBy.has(String(m.builder_id))).map((m) => ({
1108
1113
  builder_id: m.builder_id, github_login: m.github_login, display_name: m.display_name,
@@ -1155,6 +1160,7 @@ async function boardRoomView({ closedLimit = 20, viewerId = null } = {}) {
1155
1160
  reason_md: v.reason_md,
1156
1161
  section_key: v.section_key,
1157
1162
  cast_at: v.cast_at,
1163
+ cast_via: v.cast_via || 'ballot', // task 1005121: a ballot, or a confirm given by closing a genesis stage
1158
1164
  });
1159
1165
  }
1160
1166
  const project = (item) => {
@@ -494,13 +494,15 @@ async function getBoardItemAuthor(item) {
494
494
  // The open ballot's one write. Idempotency is STRUCTURAL — UNIQUE(item_id,
495
495
  // voter_id); the loser of a double-cast gets null and the caller reads the
496
496
  // standing vote back.
497
- async function insertBoardVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null }) {
497
+ // cast_via (government_038, task 1005121): 'ballot', or 'stage_close' for the
498
+ // confirm a founder gives by closing a genesis stage. board.js decides which.
499
+ async function insertBoardVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null, castVia = 'ballot' }) {
498
500
  const { rows } = await pool.query(
499
- `INSERT INTO government_board_votes (item_id, voter_id, direction, reason_md, section_key)
500
- VALUES ($1, $2, $3, $4, $5)
501
+ `INSERT INTO government_board_votes (item_id, voter_id, direction, reason_md, section_key, cast_via)
502
+ VALUES ($1, $2, $3, $4, $5, $6)
501
503
  ON CONFLICT (item_id, voter_id) DO NOTHING
502
504
  RETURNING *`,
503
- [itemId, voterId, direction, reasonMd, sectionKey],
505
+ [itemId, voterId, direction, reasonMd, sectionKey, castVia],
504
506
  );
505
507
  return rows[0] || null;
506
508
  }
@@ -1321,7 +1323,7 @@ async function insertVoteKarma({ voterId, itemId, delta }) {
1321
1323
  }
1322
1324
 
1323
1325
  // ── the constitution in force (task 1005140) ─────────────────────────────────
1324
- // ONE row (government_037). The board block is stored exactly as sanitizeBoard
1326
+ // ONE row (government_038). The board block is stored exactly as sanitizeBoard
1325
1327
  // returns it; constitution.js owns the rules for when it is written and how the
1326
1328
  // running process is brought in step with it. `q` is a client or the pool.
1327
1329
  async function getConstitutionRow(q = pool) {
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ // modules/government/genesis-close.js — closing a genesis stage is ONE call
3
+ // (task 1005121). Its own file so board.js stays under the size ratchet; it is
4
+ // composed only from board.js's exported functions (looked up at call time, so a
5
+ // test's stubs on board's db bite exactly as they do for board itself).
6
+ const board = require('./board.js');
7
+
8
+ // Close a genesis stage in ONE call (task 1005121; owner ruling 2026-11-07:
9
+ // "closing a stage shouldn't be such a big thing"). Opens the sitting (or reads
10
+ // the one already open) and, where the item's constitution says one person
11
+ // decides (first_ratifier) and the caller is a member under its snapshot, casts
12
+ // the caller's yes in the same breath — recorded cast_via 'stage_close', so the
13
+ // ledger tells a confirm given by closing the stage from a clicked ballot — and
14
+ // closes it. The same rule at every stage: nothing here forks on which stage.
15
+ // Returns, never throws for a refusal:
16
+ // { closed: true, item, stage, cast } — the sitting passed
17
+ // { closed: false, item, stage, opened, status } — a real sitting waits;
18
+ // `status` is genesisStageStatus' entry for this stage (who has voted, who
19
+ // it waits on, the rule in words, the clock)
20
+ // { closed: false, reason, unfinished? } — openGenesisStageClose refused
21
+ async function closeGenesisStage({ stageKey, taskIds, closedBy = null } = {}) {
22
+ const r = await board.openGenesisStageClose({ stageKey, taskIds, openedBy: closedBy });
23
+ if (!r.opened && r.reason !== 'already_open') return { closed: false, reason: r.reason, unfinished: r.unfinished || [] };
24
+ const item = r.item;
25
+ const stage = r.stage || board.GENESIS_STAGES.find((s) => s.key === stageKey);
26
+ if (!item) return { closed: false, reason: 'already_open' };
27
+ const constitution = item.constitution || {};
28
+ let cast = null;
29
+ if (constitution.pass_rule === 'first_ratifier' && closedBy != null && closedBy !== '') {
30
+ const v = await board.castVote({ itemId: item.id, voterId: closedBy, direction: 'yes', castVia: 'stage_close' });
31
+ if (v.ok) {
32
+ cast = { cast_via: v.vote ? v.vote.cast_via || 'stage_close' : 'stage_close', fresh: v.fresh };
33
+ const c = await board.maybeCloseAfterVote(item.id);
34
+ if (c && c.closed && c.outcome === 'passed') return { closed: true, item: c.item || item, stage, cast };
35
+ }
36
+ }
37
+ let status = null;
38
+ try {
39
+ const all = await board.genesisStageStatus();
40
+ status = (all.stages || []).find((s) => s.key === stage.key) || null;
41
+ } catch (_) { /* the answer still says a sitting is open, and its id */ }
42
+ return { closed: false, item, stage, opened: !!r.opened, status, cast };
43
+ }
44
+
45
+ module.exports = { closeGenesisStage };
@@ -0,0 +1,23 @@
1
+ -- government_038_vote_cast_via.sql — HOW a vote was cast (task 1005121).
2
+ --
3
+ -- Closing a genesis stage is one call: where the constitution says one person
4
+ -- decides (first_ratifier) and that person is the founder asking to close it, the
5
+ -- close casts their confirming yes in the same breath (board.js closeGenesisStage).
6
+ -- The owner's condition (2026-11-07): a ratification carries a person's name, so
7
+ -- the record must say HOW it was cast — a confirm given by closing the stage — not
8
+ -- merely that the founder voted. Every vote before this column is a ballot.
9
+ --
10
+ -- cast_via 'ballot' a vote cast in the Board Room (the vote route)
11
+ -- 'stage_close' the confirm given by closing a genesis stage; only
12
+ -- board.js writes it, only on a genesis_stage item, only yes
13
+
14
+ BEGIN;
15
+
16
+ ALTER TABLE government_board_votes
17
+ ADD COLUMN IF NOT EXISTS cast_via text NOT NULL DEFAULT 'ballot';
18
+
19
+ ALTER TABLE government_board_votes DROP CONSTRAINT IF EXISTS government_board_votes_cast_via_check;
20
+ ALTER TABLE government_board_votes ADD CONSTRAINT government_board_votes_cast_via_check
21
+ CHECK (cast_via IN ('ballot', 'stage_close') AND (cast_via = 'ballot' OR direction = 'yes'));
22
+
23
+ COMMIT;
@@ -126,12 +126,11 @@ const governmentPort = {
126
126
  // outcome is written by a close, never by the module reading it.
127
127
  boardPassForIdea: board.boardPassForIdea,
128
128
  boardPassedIdeaIds: board.boardPassedIdeaIds,
129
- // task 1004429 (BV2.PS04): a finished genesis stage is put to the board — the
130
- // genesis home (task 1004423) calls this when a stage's tasks are done. The
131
- // finished-tasks gate lives inside board.js; the item is an ordinary one under
132
- // the constitution in force. And the founding band's read: per stage, what it
133
- // is waiting on. A pass is announced as `board.genesis_stage.passed`.
129
+ // task 1004429 (BV2.PS04): a finished genesis stage is put to the board (the gate
130
+ // lives in board.js; an ordinary item under the constitution in force), and the
131
+ // founding band's read. task 1005121: closeGenesisStage closes it in one call.
134
132
  openGenesisStageClose: board.openGenesisStageClose,
133
+ closeGenesisStage: require('../genesis-close').closeGenesisStage, // task 1005121: one call closes a stage
135
134
  // task 1004941: POST /government/board/matters' own openMatter, for a module's proposal (callers gate on board.item.open)
136
135
  openMatter: board.openMatter,
137
136
  // task 1004427 (BV2.PS13): the genesis charter + legislation choice, filed as
@@ -100,6 +100,7 @@
100
100
  // rank/permission-CRUD refusals stayed with the Permissions surface — this page
101
101
  // cannot provoke them.
102
102
  const ERROR_COPY = {
103
+ bad_cast_via: 'That vote could not be recorded that way — cast it from the ballot.',
103
104
  not_board_member: 'Only members of this item’s board may vote on it — membership follows the rules that were in force when the window opened.',
104
105
  item_closed: 'This sitting has closed — refreshing.',
105
106
  vote_window_expired: 'This sitting’s clock has run out — the sweep is closing it.',
@@ -90,6 +90,6 @@
90
90
  <script src="/builders/hall-ask.js"></script>
91
91
  <!-- the in-place update window a hosted project's hall opens (task 1005041) -->
92
92
  <script src="/builders/deploy-handoff.js?v=2026-11-03-handoff"></script>
93
- <script src="/builders/deploy.js?v=2026-11-03-handoff"></script>
93
+ <script src="/builders/deploy.js?v=2026-11-09-status"></script>
94
94
  </body>
95
95
  </html>
@@ -426,12 +426,63 @@
426
426
  Reconnecting sets up <strong>lasting</strong> access: the hub keeps a key of its own for this repository, so this change and every later upgrade are saved to GitHub without asking you again.</p>`;
427
427
  }
428
428
 
429
+ // ---- the plain status line (task 1005012) ----------------------------------
430
+ //
431
+ // The owner opened this page to put merged work live and found neither a button nor a
432
+ // sentence: the box was already on the newest version — it had moved there by itself on
433
+ // its update rule — and the page said only "nothing to deploy". So the box LEADS with one
434
+ // line in words, up to date or not, and says how the next version reaches it. Every
435
+ // fact in it is the server's: `behind`, the served version and the update rule. The page
436
+ // still compares no versions; it only words what it was told.
437
+ //
438
+ // How the next version arrives depends on the rule alone. On "Stay put" nothing arrives
439
+ // by itself, so saying it would is the one sentence here that could be false.
440
+ const AUTO_RULES = new Set(['patch', 'minor']);
441
+ function nextVersionWords(p) {
442
+ if (AUTO_RULES.has(p.update_channel)) {
443
+ return 'When a newer version is published, it arrives on its own under the update rule below — nothing needs pressing.';
444
+ }
445
+ if (p.update_channel === 'pinned') {
446
+ return 'The update rule below is Stay put, so a newer version waits until you change the update rule.';
447
+ }
448
+ return 'When a newer version is published, a deploy button appears here.';
449
+ }
450
+
451
+ // Merged is not live: a merge reaches this project only inside a published version. On
452
+ // the hall's own view the npm-release module lists where each piece of work has got to,
453
+ // so the line points at it when it is there, and says nothing about a list that is not.
454
+ function waitingWorkWords() {
455
+ const mods = window.__MODULES__;
456
+ const listed = view.isSelf && !!(mods && mods.enabled && mods.enabled['npm-release']);
457
+ return `Work merged after that is not live until a published version carries it${
458
+ listed ? ' — “Where is the work” on this page shows where each piece has got to' : ''}.`;
459
+ }
460
+
461
+ function upToDateHtml(p) {
462
+ const v = escapeHtml(String(p.served_version));
463
+ return `
464
+ <div class="dp-status dp-status--ok" role="status">
465
+ <p class="ov-row__sub"><strong>Up to date.</strong> This project runs core ${v}, the newest version
466
+ it can take. Every piece of merged work inside core ${v} is live. There is nothing to deploy.</p>
467
+ <p class="ov-row__sub">${escapeHtml(nextVersionWords(p))} ${escapeHtml(waitingWorkWords())}</p>
468
+ </div>`;
469
+ }
470
+
471
+ // Behind: the one primary action on the page. It is a PERSON'S button and stays one —
472
+ // the project's law is that going live waits for a person, so pressing it opens the
473
+ // check and the confirm (ADR 0293 D4), and nothing restarts until "Move to …" is pressed.
474
+ // The id is the step-one id it always was, so the two-step wiring is unchanged.
429
475
  function stepOneHtml(p) {
476
+ const to = escapeHtml(String(p.recommended_version));
477
+ const auto = AUTO_RULES.has(p.update_channel)
478
+ ? ' It would also arrive on its own under the update rule below; this does it now.' : '';
430
479
  return `
431
- <p class="ov-row__sub">Core <strong>${escapeHtml(String(p.recommended_version))}</strong> is published and
432
- this box is not on it yet.</p>
433
- <div class="ps-controls">
434
- <button type="button" class="btn" id="dp-preview-${escapeHtml(String(p.instance_id))}">See what would change</button>
480
+ <div class="dp-status dp-status--behind" role="status">
481
+ <p class="ov-row__sub"><strong>Not up to date.</strong> Core <strong>${to}</strong> is published; this project still runs ${escapeHtml(String(p.served_version))}.${auto}</p>
482
+ <div class="ps-controls">
483
+ <button type="button" class="btn btn--primary" id="dp-preview-${escapeHtml(String(p.instance_id))}">Deploy core ${to} now</button>
484
+ </div>
485
+ <p class="ov-row__sub dp-status__note">You see what it brings in and confirm before anything restarts.</p>
435
486
  </div>`;
436
487
  }
437
488
 
@@ -613,7 +664,7 @@
613
664
  if (moving.has(p.instance_id)) action = movingHtml(p);
614
665
  else if (p.open_intent) action = busyHtml(p);
615
666
  else if (p.reading === 'no_answer') action = '';
616
- else if (!p.behind) action = `<p class="ov-row__sub">Nothing newer is published that this box can take. There is nothing to deploy.</p>`;
667
+ else if (!p.behind) action = upToDateHtml(p);
617
668
  else action = step === 'confirm' ? stepTwoHtml(p, isSelf) : stepOneHtml(p);
618
669
  if (!moving.has(p.instance_id)) action += settledHtml(p);
619
670
  return `
@@ -334,19 +334,14 @@
334
334
  return window.OTB.sleep(3500);
335
335
  }).then(function () { window.location.reload(); }));
336
336
  } else if (t.hasAttribute('data-genesis-close')) {
337
- // The solo fast-track (spec D4): when the founder's one yes decides the sitting
338
- // and nobody else is asked, the close and the confirm are one click.
337
+ // One call (task 1005121): where the founder's one yes decides, the server
338
+ // closes the stage with it; otherwise it answers 202 with what the sitting
339
+ // waits on, and that sentence is shown.
339
340
  act(send('POST', '/founding/stages/' + encodeURIComponent(t.getAttribute('data-genesis-close')) + '/close', {})
340
- .then(function () { return send('GET', '/founding/genesis'); })
341
- .then(function (d) {
342
- var g = d && d.genesis;
343
- var s = g && g.stages.find(function (x) { return x.state === 'now'; });
344
- var b = s && s.board;
345
- var solo = b && b.state === 'sitting' && b.one_vote_decides && b.item_id
346
- && (b.waiting_on || []).length === 1 && String(b.waiting_on[0].builder_id) === String(d.viewer_id);
347
- return solo ? send('POST', '/government/board/items/' + encodeURIComponent(b.item_id) + '/votes', { direction: 'yes' }) : null;
348
- })
349
- .then(function () { return load(host, state); }));
341
+ .then(function (out) {
342
+ if (out && out.closed === false && out.next) toast(out.next);
343
+ return load(host, state);
344
+ }));
350
345
  } else if (t.hasAttribute('data-genesis-confirm')) {
351
346
  act(send('POST', '/government/board/items/' + encodeURIComponent(t.getAttribute('data-genesis-confirm')) + '/votes', { direction: 'yes' })
352
347
  .then(function () { return load(host, state); }));
@@ -57,7 +57,7 @@
57
57
  const SKY_LABEL = 'The project\'s ideas as a night sky over the city\'s real skyline, with this week\'s ideators ranked beside it';
58
58
  const CITY_LABEL = 'The project as a city: one tower per working area, as tall as what has shipped there, with the builders of the week beside it';
59
59
 
60
- let host = null, model = null, viewerId = null, t = 0, target = 0, raf = null, run = null, lastWeek = -1, shown = false, gallery = null;
60
+ let host = null, model = null, viewerId = null, t = 0, target = 0, raf = null, run = null, lastWeek = -1, shown = false, gallery = null, ideaBox = null;
61
61
  const rungs = new Map();
62
62
 
63
63
  function mode() {
@@ -292,11 +292,15 @@
292
292
 
293
293
  // ---- the artist's Home (task 1005060): in Artistry the same section is the gallery,
294
294
  // home-gallery.js, the bench in the museum the city's art district opens onto ----
295
+ // In Ideating, the idea box (home-idea-box.js) opens Home above the sky, and shows even
296
+ // when the sky could not be read: filing an idea needs no picture of the others.
295
297
  function craftHome() {
296
298
  const art = !!gallery && document.documentElement.dataset.craft === 'artistry';
299
+ const idea = !!ideaBox && craft() === 'ideating';
297
300
  host.classList.toggle('hc--gallery', art);
298
301
  if (gallery) gallery.show(art);
299
- host.hidden = !(art || shown);
302
+ if (ideaBox) ideaBox.show(idea);
303
+ host.hidden = !(art || idea || shown);
300
304
  }
301
305
 
302
306
  // ---- the governor's Home is the street; tests swap the hop for a recorder ----
@@ -338,6 +342,7 @@
338
342
  wireTip();
339
343
  wireDoors();
340
344
  if (window.OTBHomeGallery) gallery = window.OTBHomeGallery.attach(host);
345
+ if (window.OTBHomeIdeaBox) ideaBox = window.OTBHomeIdeaBox.attach(host);
341
346
  document.addEventListener('otb:craft', craftHome);
342
347
  craftHome();
343
348
  new MutationObserver(() => { if (model) drawScene(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
@@ -0,0 +1,33 @@
1
+ /* modules/hall-ui/public/home-idea-box.css — the idea box that opens the ideator's Home
2
+ (home-idea-box.js; record modules/hall-ui/records/home-city.md § "The idea box").
3
+ It sits at the top of the city's section, over the sky. Every colour is a hall token
4
+ or a mix of one; the tokens carry both modes, so no rule here is written per mode.
5
+ Every type size and radius is a --ds-* scale token (DESIGN.md, the Fifteen Rule), and
6
+ the narrow layout comes from wrapping rather than a breakpoint, so the sheet adds no
7
+ typed-in value to the design counts. No :root, no motion. */
8
+
9
+ .hib { padding: clamp(16px, 4vw, 28px) clamp(16px, 4vw, 28px) clamp(14px, 3vw, 20px); display: grid; gap: 10px; }
10
+ .hib[hidden] { display: none; }
11
+ .hib-h { margin: 0; font: 600 var(--ds-text-4xl)/1.25 var(--font-display); color: var(--ink); }
12
+ .hib-h label { cursor: text; }
13
+ /* The box takes the row; on a narrow screen the button wraps under it */
14
+ .hib-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
15
+ .hib .hib-title {
16
+ width: 100%; min-width: 0; box-sizing: border-box;
17
+ font: 400 var(--ds-text-xl)/1.4 var(--font-body); color: var(--ink);
18
+ background: var(--bg-card); border: var(--line) solid var(--rule-soft); border-radius: var(--radius-sm);
19
+ }
20
+ .hib textarea.hib-title {
21
+ flex: 1 1 16rem; width: auto; min-height: 48px; max-height: 40vh; padding: 12px 20px;
22
+ font-size: var(--ds-text-2xl); line-height: 22px;
23
+ border-radius: var(--ds-radius-panel); resize: none; overflow-y: auto;
24
+ }
25
+ .hib .hib-title::placeholder { color: var(--ink-faint); }
26
+ .hib .hib-title:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-color: transparent; }
27
+ .hib-file { min-height: 48px; padding: 0 22px; font-size: var(--ds-text-lg); flex-shrink: 0; }
28
+ .hib-said { margin: 0; min-height: 1.4em; font: 400 var(--ds-text-md)/1.4 var(--font-body); color: var(--ink-soft); }
29
+ .hib-said--err { color: var(--danger); }
30
+ .hib-said a { color: var(--ink); }
31
+
32
+ /* The setup guide is about building on your own machine; in Ideating, Home opens on the box */
33
+ html[data-craft="ideating"] #welcome-scroll { display: none; }
@@ -0,0 +1,106 @@
1
+ // modules/hall-ui/public/home-idea-box.js — the ideator's Home opens on the idea box
2
+ // (goal 1000095, the owner's "work-first hall", 2026-11-04; record
3
+ // modules/hall-ui/records/home-city.md § "The idea box").
4
+ //
5
+ // In Ideating, the first thing on Home is a place to type an idea and file it; the sky
6
+ // and the ladder of ideators stay under it, secondary. It is ONE box and a button
7
+ // (owner, 2026-11-05: "only one box"): Enter files, Shift+Enter starts a new line, and
8
+ // the box grows with what is typed. The first line is the idea's title, any lines after
9
+ // it its details. Filing is POST /inbox as the sky's quick composer files
10
+ // (sky-composer.js): { title, body_md, kind: 'feature' }. A failed filing keeps the
11
+ // words where they were and says why; a filed one says where it went and links to
12
+ // Your thinking, where every thought the builder filed is listed.
13
+ //
14
+ // It is not a hall widget of its own (Home's stats zone holds one): home-city.js
15
+ // attaches it at the top of its section and shows it when the craft is Ideating, the
16
+ // way it shows the gallery in Artistry. It reads nothing; it writes one thing.
17
+ (function () {
18
+ 'use strict';
19
+
20
+ var API = '/api/bongos';
21
+
22
+ // ---- pure: what the line under the box says (node-testable) ----
23
+ function explain(res) {
24
+ var err = res && res.data && res.data.error;
25
+ var code = err && (typeof err === 'object' ? err.code : err);
26
+ var details = (err && typeof err === 'object' && err.details) || {};
27
+ if (details.reason) return 'Not filed: ' + details.reason + '. Your idea is still here.';
28
+ if (res && (res.status === 401 || res.status === 403)) return 'Not filed: your session may have expired. Reload and try again.';
29
+ if (code) return 'Not filed: ' + String(code).replace(/_/g, ' ') + '. Your idea is still here.';
30
+ return 'Not filed' + (res && res.status ? ' (' + res.status + ')' : '') + '. Nothing was saved; your idea is still here.';
31
+ }
32
+ // the first non-empty line is the title; whatever follows it is the details
33
+ function body(text) {
34
+ var lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
35
+ while (lines.length && !lines[0].trim()) lines.shift();
36
+ return { title: (lines.shift() || '').trim(), body_md: lines.join('\n').trim(), kind: 'feature' };
37
+ }
38
+ var Lib = { explain: explain, body: body };
39
+ if (typeof module !== 'undefined' && module.exports) module.exports = Lib;
40
+ if (typeof document === 'undefined') return;
41
+
42
+ function attach(host) {
43
+ var api = null, filing = false;
44
+ var root = document.createElement('form');
45
+ root.className = 'hib';
46
+ root.setAttribute('novalidate', '');
47
+ root.setAttribute('aria-labelledby', 'hib-h');
48
+ root.hidden = true;
49
+ root.innerHTML =
50
+ '<h2 class="hib-h" id="hib-h"><label for="hib-title">What is your idea?</label></h2>' +
51
+ '<div class="hib-row">' +
52
+ '<textarea id="hib-title" class="hib-title" rows="1" placeholder="One line is enough" aria-describedby="hib-keys"></textarea>' +
53
+ '<button type="submit" class="btn hib-file">File idea</button>' +
54
+ '</div>' +
55
+ '<p class="sr-only" id="hib-keys">Enter files the idea. Shift and Enter starts a new line.</p>' +
56
+ '<p class="hib-said" role="status" aria-live="polite"></p>';
57
+ host.insertBefore(root, host.firstChild);
58
+ var title = root.querySelector('.hib-title');
59
+ var said = root.querySelector('.hib-said'), btn = root.querySelector('.hib-file');
60
+
61
+ // the box is as tall as what is in it
62
+ function fit() { title.style.height = 'auto'; title.style.height = title.scrollHeight + 'px'; }
63
+ title.addEventListener('input', fit);
64
+ // Enter files, Shift+Enter is a new line; a key that is still composing a character is left alone
65
+ title.addEventListener('keydown', function (e) {
66
+ if (e.key !== 'Enter' || e.shiftKey || e.isComposing) return;
67
+ e.preventDefault();
68
+ if (typeof root.requestSubmit === 'function') root.requestSubmit(); else root.dispatchEvent(new Event('submit', { cancelable: true }));
69
+ });
70
+
71
+ function say(text, link) {
72
+ said.textContent = text;
73
+ said.classList.toggle('hib-said--err', !link && !!text);
74
+ if (link) {
75
+ var a = document.createElement('a');
76
+ a.href = '/thinking';
77
+ a.textContent = 'See it in Your thinking';
78
+ said.append(' ', a);
79
+ }
80
+ }
81
+
82
+ root.addEventListener('submit', function (e) {
83
+ e.preventDefault();
84
+ if (filing) return;
85
+ var b = body(title.value);
86
+ if (!b.title) { say('Type your idea first.'); title.focus(); return; }
87
+ api = api || (window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false }));
88
+ if (!api) { say(explain(null)); return; }
89
+ filing = true; btn.disabled = true; say('');
90
+ api.request('POST', API + '/inbox', { body: b }).catch(function () { return null; }).then(function (res) {
91
+ filing = false; btn.disabled = false;
92
+ if (!res || !res.ok || !res.data || !res.data.idea) { say(explain(res)); return; }
93
+ title.value = ''; fit();
94
+ say(res.data.routed_task ? 'Filed, and it is already a task.' : 'Filed. It waits for the next triage.', true);
95
+ title.focus();
96
+ });
97
+ });
98
+
99
+ return {
100
+ // shown in Ideating
101
+ show: function (on) { root.hidden = !on; },
102
+ };
103
+ }
104
+
105
+ window.OTBHomeIdeaBox = { attach: attach, lib: Lib };
106
+ })();
@@ -23,6 +23,7 @@
23
23
  <link rel="stylesheet" href="/builders/hall-ask.css">
24
24
  <link rel="stylesheet" href="/builders/home-city.css">
25
25
  <link rel="stylesheet" href="/builders/home-gallery.css">
26
+ <link rel="stylesheet" href="/builders/home-idea-box.css">
26
27
  </head>
27
28
  <body data-page="home">
28
29
 
@@ -133,6 +134,7 @@
133
134
  <script src="/builders/city-draw.js" defer></script>
134
135
  <script src="/builders/home-city-draw.js" defer></script>
135
136
  <script src="/builders/home-gallery.js" defer></script>
137
+ <script src="/builders/home-idea-box.js" defer></script>
136
138
  <script src="/builders/home-city.js" defer></script>
137
139
  <!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
138
140
  both goal-inbox.js's attention-inbox slice below and the /blockers page.
@@ -212,6 +212,21 @@
212
212
  }
213
213
  .dp-settled .ps-controls { margin: 0.4rem 0 0.2rem; }
214
214
 
215
+ /* The box's plain status line (task 1005012): up to date, or not and here is the button.
216
+ The same left-mark grammar as the settled states — ok when nothing waits, the accent
217
+ when a published version is waiting for a person's press. */
218
+ .dp-status { margin: 0.4rem 0 0; }
219
+ .dp-status--ok {
220
+ border-left: 3px solid var(--ok);
221
+ padding-left: 0.7rem;
222
+ }
223
+ .dp-status--behind {
224
+ border-left: 3px solid var(--accent);
225
+ padding-left: 0.7rem;
226
+ }
227
+ .dp-status .ps-controls { margin: 0.5rem 0 0.3rem; }
228
+ .dp-status__note { color: var(--ink-soft); }
229
+
215
230
  /* The project's update rule (task 1004445) — a quiet setting under the move, set apart by
216
231
  a hairline so it does not read as part of whichever state sits above it. */
217
232
  .dp-rule {