@bongos/core 1.24.20 → 1.24.22

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.
@@ -58,6 +58,9 @@ function fenceDocketMatters(project, mayManage) {
58
58
  tally: null,
59
59
  href: '/watch',
60
60
  ref: { type: 'autonomy_fence', id: 'project' },
61
+ context: project.enabled === false
62
+ ? 'Unattended building lets AI runners claim and build tasks on their own, with nobody at the keyboard. It is switched off, so no runner builds anything until someone turns it on.'
63
+ : 'Unattended building lets AI runners claim and build tasks on their own. It is on, but no goal is on its allowed list, so the runners have nothing they may work on yet.',
61
64
  ...(detail ? { detail } : {}),
62
65
  // The floor desk's acts (task 1004559), only for the switch's own atom.
63
66
  ...(mayManage ? { acts: project.enabled === false ? [{ key: 'switch_on' }] : [{ key: 'allow', pick: 'open_goals', except: [] }, { key: 'switch_off', options: OFF_REASONS }] } : {}),
@@ -165,12 +165,44 @@ function tally(t) {
165
165
  };
166
166
  }
167
167
 
168
+ // WHAT A CARD IS ABOUT (task 1005013). A matter may carry `context`: a few plain
169
+ // sentences from the module that owns it saying what kind of thing this is, what
170
+ // it belongs to (a goal, a task, a module, by name), why it waits and what
171
+ // answering does — so a card is never only a bare title. A matter that brings
172
+ // none gets its source's sentence below; every source the design names has one.
173
+ // It may also carry `question`: the plain question a card asks when no agent
174
+ // stored an ask, in place of the raw title.
175
+ const SOURCE_CONTEXT = Object.freeze({
176
+ board: 'A sitting of the board. Its members vote, and the result is decided by the rule in the charter.',
177
+ blockers: 'A blocker: work that cannot go on until a person decides something. Deciding it releases the work that waits on it.',
178
+ access_requests: 'Someone asked to join this project as a builder. Admitting them lets them sign in and claim work; nobody joins until someone with the right says yes.',
179
+ goal_join_requests: 'Someone asked to join a goal team. Members of a goal may claim its tasks.',
180
+ override_requests: 'A request to let finished work through although it failed its automatic check or grade. Approving it treats the work as passed.',
181
+ removal_proposals: 'A later change deleted work that had already shipped. A person confirms whether that work is really gone, or keeps it counted.',
182
+ redteam_reports: 'A security report: someone says they found a flaw in the project itself. Confirming it means the flaw is real and needs a fix; disputing it means it is not.',
183
+ criteria_uat: 'A done-when promise of a goal. The work for it has shipped; a person checks it on the live site and signs it off, which closes the promise.',
184
+ core_update: 'A newer version of Bongos, the software this project runs on, is available. Taking it updates the project to that version.',
185
+ autobongos_fence: 'The switch for unattended building: whether AI runners may build on their own, and which goals they may work on.',
186
+ module_rechecks: 'A module (an add-on to the project) is being checked again for safety. One that failed waits for someone to run the check again.',
187
+ treasury_spending: 'A goal has used up its money for the month and asks to borrow from the reserve so its work can go on.',
188
+ });
189
+ const MAX_CONTEXT = 600;
190
+ const MAX_QUESTION = 160;
191
+ function clip(v, max) {
192
+ if (typeof v !== 'string') return null;
193
+ const t = v.replace(/\s+/g, ' ').trim();
194
+ if (!t) return null;
195
+ return t.length <= max ? t : t.slice(0, max - 1).replace(/\s+\S*$/, '') + '…';
196
+ }
197
+
168
198
  // One matter as a contributor handed it → the docket's one shape. Pure. Unknown
169
199
  // rule → null (the card says so), unknown state → 'moving' (never a false
170
200
  // "needs you"), unknown building → the source's own building.
171
201
  function normalizeMatter(raw, { source, building }) {
172
202
  if (!raw || typeof raw !== 'object' || raw.id == null || !raw.title) return null;
173
203
  const b = vocab.BUILDING_KEYS.includes(raw.building) ? raw.building : building;
204
+ const context = clip(raw.context, MAX_CONTEXT) || SOURCE_CONTEXT[source] || null;
205
+ const question = clip(raw.question, MAX_QUESTION);
174
206
  return {
175
207
  id: `${source}:${raw.id}`,
176
208
  source,
@@ -186,6 +218,8 @@ function normalizeMatter(raw, { source, building }) {
186
218
  tally: tally(raw.tally),
187
219
  href: raw.href ? String(raw.href) : null,
188
220
  ref: raw.ref && typeof raw.ref === 'object' ? { type: String(raw.ref.type), id: String(raw.ref.id) } : null,
221
+ ...(context ? { context } : {}),
222
+ ...(question ? { question } : {}),
189
223
  ...actsField(raw.acts),
190
224
  ...detailField(raw.detail),
191
225
  };
@@ -364,6 +398,13 @@ const PASS_RULE_NEEDS = Object.freeze({
364
398
  majority: 'needs more than half',
365
399
  unanimous: 'needs everyone who votes',
366
400
  });
401
+ // the same rule as the last sentence of a sitting's context (task 1005013)
402
+ const PASS_RULE_SENTENCE = Object.freeze({
403
+ first_ratifier: 'One yes decides it.',
404
+ consent: 'It passes unless someone objects.',
405
+ majority: 'It needs a yes from more than half the members.',
406
+ unanimous: 'It needs a yes from everyone who votes.',
407
+ });
367
408
 
368
409
  // One open or recently closed sitting, for one viewer. Pure.
369
410
  //
@@ -402,6 +443,14 @@ function boardMatter(item, { viewerId, members }) {
402
443
  : item.subject_type === 'constitutional_amendment' ? 'charter · amendment'
403
444
  : item.subject_type === 'genesis_stage' ? 'charter · founding'
404
445
  : 'planning · idea to ratify';
446
+ const passWords = PASS_RULE_SENTENCE[constitution.pass_rule] ? ' ' + PASS_RULE_SENTENCE[constitution.pass_rule] : '';
447
+ const context = (matterArea
448
+ ? `A decision put to a vote at ${building}: the charter says matters here are decided by a sitting, not by one person acting alone. A yes carries out what it proposes.`
449
+ : item.subject_type === 'constitutional_amendment'
450
+ ? 'A proposed change to the charter, the rules for who decides what in this project. If it passes, the new rule takes effect at once.'
451
+ : item.subject_type === 'genesis_stage'
452
+ ? 'A founding stage of this project has finished its work. Closing it moves the project on to the next stage.'
453
+ : 'A full idea someone wrote up, put to the board. If it passes, it becomes part of the plan: a new goal or a done-when promise of one.') + passWords;
405
454
  const forN = votes.filter((v) => v.direction === 'yes').length;
406
455
  const againstN = votes.filter((v) => v.direction === 'no').length;
407
456
  const castIds = new Set(votes.map((v) => String(v.voter_id)));
@@ -433,6 +482,7 @@ function boardMatter(item, { viewerId, members }) {
433
482
  tally: { for: forN, against: againstN, open: isOpen ? open : 0, needs: PASS_RULE_NEEDS[constitution.pass_rule] || null },
434
483
  href: `/board-room?item=${encodeURIComponent(item.id)}`,
435
484
  ref: { type: 'board_item', id: item.id },
485
+ context,
436
486
  };
437
487
  }
438
488
 
@@ -962,6 +1012,7 @@ module.exports = {
962
1012
  normalizeDeskRow,
963
1013
  loadGovernment,
964
1014
  normalizeMatter,
1015
+ SOURCE_CONTEXT,
965
1016
  gateAllows,
966
1017
  homeLine,
967
1018
  boardMatter,
@@ -227,6 +227,8 @@ function borrowMatter(borrow, { title, canAnswer = false } = {}) {
227
227
  deadline: null,
228
228
  href: `/goal/${b.goal_id}`,
229
229
  ref: { type: 'pot_borrow', id: b.id },
230
+ question: `Lend ${name} ${usd(b.amount_usd)} from the reserve?`,
231
+ context: `Each goal gets a monthly pot of money for its AI work. ${name} has spent ${usd(b.spent_usd)} of its ${usd(b.budget_usd)} this month, so its work stops until it gets more. Lending from the reserve lets it go on; saying no pauses it until next month.`,
230
232
  detail: `${name} has spent ${usd(b.spent_usd)} of its ${usd(b.budget_usd)} pot this month and asks the reserve for ${usd(b.amount_usd)}.`,
231
233
  ...(canAnswer ? { acts: [{ key: ACT_KEY, options: [
232
234
  { key: 'lend', label: `Lend ${usd(b.amount_usd)}` }, { key: 'half', label: `Lend ${usd(half)}` }, { key: 'no', label: 'Say no' },
@@ -83,6 +83,7 @@
83
83
  var lines = ['I need to make a decision in my project and want help understanding it.', ''];
84
84
  lines.push('Where: the ' + str(cv.buildingName) + ' building' + (cv.waits ? ' (' + str(cv.waits) + ')' : '') + '.');
85
85
  lines.push('The question: ' + str(cv.question));
86
+ if (cv.context) lines.push('What it is: ' + str(cv.context));
86
87
  if (cv.said) lines.push('What it says: ' + str(cv.said));
87
88
  var card = cv.card || {};
88
89
  var more = card.more && card.more.text ? str(card.more.text) : str(card.fact || '');
@@ -255,8 +256,10 @@
255
256
  function drawCard(doc, K, cv, on, o) {
256
257
  var shown = answerable(cv);
257
258
  var matter = matterOf(on.docket && on.docket(), cv.id);
258
- // the question in full: a matter with no stored card is titled by its own words, cut to fit the deck
259
- if (matter && !cv.askReal && matter.title) shown.question = questionOf(matter.title);
259
+ // the question in full: a matter with no stored card asks its own plain question
260
+ // when it wrote one (task 1005013), else is titled by its own words, cut to fit the deck
261
+ if (matter && !cv.askReal && matter.question) shown.question = questionOf(matter.question);
262
+ else if (matter && !cv.askReal && matter.title) shown.question = questionOf(matter.title);
260
263
  var art = K.drawAsk(doc, shown, on, o);
261
264
  // and everything else it says, one tap away rather than cut
262
265
  var full = fullText(matter, shown);
@@ -231,6 +231,7 @@ html[data-fullscreen="on"] .city-top { right: 184px; }
231
231
  .city-card .city-ask.ask { padding: 14px 16px; gap: 10px; }
232
232
  .city-card .city-ask .ask__q { font-size: 17px; line-height: 1.3; font-weight: 400; }
233
233
  .city-card .city-ask .ask__said { font-size: 13px; }
234
+ .city-card .city-ask .ask__ctx { font-size: 13px; line-height: 1.45; }
234
235
  .city-card .city-ask .ask__opt { min-height: 44px; padding: 6px 12px 6px 10px; border-radius: 14px; }
235
236
  .city-card .city-ask .ask__label { font-size: 14px; }
236
237
  .city-card .city-ask .ask__result { font-size: 12px; }
@@ -338,6 +338,9 @@
338
338
 
339
339
  // ---------- one ask card ----------
340
340
  const STATE_ORDER = Object.freeze({ needs: 1, moving: 2, settled: 3 });
341
+ // a blocker read straight from GET /blockers carries no docket context; this is
342
+ // the docket's own sentence for one (modules/government/docket.js SOURCE_CONTEXT)
343
+ const BLOCKER_CONTEXT = 'A blocker: work that cannot go on until a person decides something. Deciding it releases the work that waits on it.';
341
344
  function makeAsk(base, human, ctx, sources) {
342
345
  const opened = time(base.opened_at);
343
346
  const deadline = time(base.deadline);
@@ -375,7 +378,8 @@
375
378
  href: base.href || null,
376
379
  acts: base.acts || [],
377
380
  tally: base.tally || null,
378
- raw: { title: str(base.raw_title), detail: str(base.raw_detail), slop: slopScore(rawText) },
381
+ // what the card is about and the plain question it asks (task 1005013)
382
+ raw: { title: str(base.raw_title), detail: str(base.raw_detail), slop: slopScore(rawText), context: str(base.raw_context), question: str(base.raw_question) },
379
383
  human: human || null,
380
384
  format,
381
385
  shown: format.ok ? human : null,
@@ -661,7 +665,7 @@
661
665
  const stored = storedOf(m.ask);
662
666
  asks.push(makeAsk({
663
667
  id: m.id, source: m.source, building, state: m.state, opened_at: m.opened_at, deadline: m.deadline,
664
- rule: m.rule, ref: m.ref, href: m.href, acts: m.acts, tally: m.tally, raw_title: m.title, raw_detail: m.detail, from_docket: true, ask_real: !!stored, route: m.route,
668
+ rule: m.rule, ref: m.ref, href: m.href, acts: m.acts, tally: m.tally, raw_title: m.title, raw_detail: m.detail, raw_context: m.context, raw_question: m.question, from_docket: true, ask_real: !!stored, route: m.route,
665
669
  }, humanOf(stored), ctx, ASK_SOURCES));
666
670
  });
667
671
  arr(o.blockers, 'blockers').forEach((b) => {
@@ -672,7 +676,7 @@
672
676
  const stored = storedOf(b.ask);
673
677
  asks.push(makeAsk({
674
678
  id, source: 'blockers', building: 'planning', state: 'needs', opened_at: b.created_at, deadline: null,
675
- rule: 'decide', ref: { type: 'blocker', id: String(b.id) }, href: '/blockers#blocker-' + b.id, acts: [], raw_title: b.title, raw_detail: b.body_md, ask_real: !!stored,
679
+ rule: 'decide', ref: { type: 'blocker', id: String(b.id) }, href: '/blockers#blocker-' + b.id, acts: [], raw_title: b.title, raw_detail: b.body_md, raw_context: BLOCKER_CONTEXT, ask_real: !!stored,
676
680
  }, humanOf(stored), ctx, ASK_SOURCES));
677
681
  });
678
682
  asks.sort(compareAsks);
@@ -123,6 +123,8 @@ body[data-page="street"] .street-toast { z-index: 13; }
123
123
  it is not a control, so it draws no ring */
124
124
  .ask__q:focus, .ask__q:focus-visible { outline: none; box-shadow: none; }
125
125
  .ask__said { margin: -4px 0 0; font-size: 15.5px; line-height: 1.5; color: var(--ink-soft); }
126
+ /* what the card is about, under its question (task 1005013) */
127
+ .ask__ctx { margin: -4px 0 0; font-size: 14.5px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
126
128
  .ask__likes { margin: -2px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
127
129
  .ask__likes li { display: flex; gap: 10px; font-size: 14.5px; line-height: 1.4; color: var(--ink); }
128
130
  .ask__likes li::before { content: ""; flex: none; width: 5px; height: 5px; margin-top: 8px; border-radius: 50%; background: var(--ink-faint); }
@@ -160,6 +162,7 @@ body[data-page="street"] .street-toast { z-index: 13; }
160
162
  .ask--here { gap: 14px; padding: 18px 18px 16px; border-radius: 22px; background: var(--gov-chip); border: 1px solid var(--gov-chip-edge); }
161
163
  .ask--here .ask__q { font-size: 23px; }
162
164
  .ask--here .ask__said { font-size: 14.5px; }
165
+ .ask--here .ask__ctx { font-size: 14px; }
163
166
  .ask--here .ask__opt { min-height: 54px; border-radius: 15px; }
164
167
 
165
168
  /* a building's card on the street: glass beside its pin */
@@ -213,11 +213,14 @@
213
213
  routing: shown && shown.routing_note ? str(shown.routing_note) : '', tally: ask.tally || null, kind: ask.kind || null,
214
214
  // a board's vote shows its seats, one pip each, lit as the sitting's tally says
215
215
  seats: boardSeats(c.docket, ask.waits_on ? str(ask.waits_on.seat) : ''),
216
+ // what this is and what it belongs to, in plain words (task 1005013)
217
+ context: ask.raw ? plain(ask.raw.context) : '',
216
218
  card: cv,
217
219
  };
218
220
  if (!shown) {
219
221
  // no stored ask: the matter in its own words, and its floor's desk, where its acts are
220
- view.question = fit(plain(ask.raw ? ask.raw.title : ''), 90);
222
+ // the matter's own plain question when it wrote one, else its title (task 1005013)
223
+ view.question = ask.raw && ask.raw.question ? fit(plain(ask.raw.question), 120) : fit(plain(ask.raw ? ask.raw.title : ''), 90);
221
224
  view.desk = '/builders/street#building=' + str(ask.building);
222
225
  view.foot = footOf(ask, '');
223
226
  return view;
@@ -436,6 +439,7 @@
436
439
  q.setAttribute('tabindex', '-1');
437
440
  if (o.qid) q.setAttribute('id', o.qid);
438
441
  card.appendChild(q);
442
+ if (cv.context) card.appendChild(h('p', 'ask__ctx', cv.context));
439
443
  if (cv.batch && cv.likes) {
440
444
  const list = h('ul', 'ask__likes');
441
445
  cv.likes.forEach((l) => list.appendChild(h('li', '', l.question)));
@@ -45,6 +45,9 @@ function blockerDocketMatter(b, mayResolve) {
45
45
  tally: null,
46
46
  href: `/blockers#blocker-${b.id}`,
47
47
  ref: { type: 'blocker', id: b.id },
48
+ context: holds
49
+ ? `A blocker: ${holds === 1 ? 'one task is' : `${holds} tasks are`} stopped until a person decides this. Deciding it lets that work go on.`
50
+ : 'A blocker: an AI session or a builder is stuck until a person decides this. Deciding it lets the work go on.',
48
51
  // the card's "more" (task 1004566): the body GET /blockers shows everyone
49
52
  detail: b.body_md || null,
50
53
  // the floor desk's one act (task 1004560): decide, by one of its options
@@ -41,6 +41,8 @@ function overrideMatter(r, mayDecide) {
41
41
  tally: null,
42
42
  href: `/task/${encodeURIComponent(r.task_id)}`,
43
43
  ref: { type: 'override_request', id: r.id },
44
+ question: 'Let this work through although it failed its check?',
45
+ context: `${r.requester_display_name || r.requester_login || 'A builder'} asks to let the task "${r.task_title || r.task_id}" through. Its automatic check or grade failed it, so it cannot land on its own. Approving treats the work as passed; holding it keeps it out until it is fixed.`,
44
46
  // the card's "more" (task 1004566): the reason GET /override-requests shows
45
47
  detail: r.rationale || null,
46
48
  };
@@ -60,6 +62,8 @@ function removalMatter(r) {
60
62
  tally: null,
61
63
  href: `/task/${encodeURIComponent(r.proposal_task_id)}`,
62
64
  ref: { type: 'task', id: r.proposal_task_id },
65
+ question: 'Is this shipped work really gone?',
66
+ context: `A later change deleted files that earlier shipped work put there${r.goal_title ? `, in the goal "${r.goal_title}"` : ''}. Moves and rewrites can look like deletions, so a person decides: Remove means that work no longer counts as alive; Keep means it still does. Nothing is undone either way.`,
63
67
  // the card's "more" (task 1004566): the proposal's own text, from the same read
64
68
  detail: r.description || null,
65
69
  // POST /removal-proposals/:id/decide, the docket source's own atom (task 1004559)
@@ -84,12 +88,31 @@ function uatActs(c, { force = false } = {}) {
84
88
  return acts;
85
89
  }
86
90
 
91
+ // What a Review card says (task 1005013): the promise in quotes, the goal it
92
+ // belongs to, that its work shipped, and what the sign-off closes. Pure.
93
+ function quoteOf(md) {
94
+ // the promise's own last full stop goes: the sentence around it supplies one
95
+ const t = String(md || '').replace(/[*_`#>]+/g, '').replace(/\s+/g, ' ').trim().replace(/\.+$/, '');
96
+ return t.length <= 220 ? t : t.slice(0, 219).replace(/\s+\S*$/, '') + '…';
97
+ }
98
+ function uatContext(c) {
99
+ const n = Number(c.shipped_total) || 0;
100
+ const goal = c.goal_title ? `the goal "${c.goal_title}"` : 'this version';
101
+ const built = n === 1 ? 'The one task built for it has shipped.' : n > 1 ? `All ${n} tasks built for it have shipped.` : 'Its work has shipped.';
102
+ const how = c.backend_only === true
103
+ ? 'It has nothing to see on screen, so check it works and sign it off.'
104
+ : "Try it on the live site, then record a short video of it working and sign it off on the goal's page.";
105
+ return `The promise: "${quoteOf(c.criterion_md)}". It is one of the done-when promises of ${goal}. ${built} ${how} That closes the promise; the goal is achieved once all its promises are closed.`;
106
+ }
107
+
87
108
  function uatMatter(c, opts) {
88
109
  return {
89
110
  id: c.id,
90
111
  rule: 'signoff',
91
112
  state: 'needs',
92
113
  title: c.criterion_md,
114
+ question: c.backend_only === true ? 'Is this promise kept? Check it and sign it off.' : 'Does this work on the live site now?',
115
+ context: uatContext(c),
93
116
  short: 'waiting for a tester',
94
117
  where: c.goal_title ? `review · ${c.goal_title}` : 'review · a promise to sign off',
95
118
  opened_at: null,
@@ -118,6 +141,9 @@ function joinRequestMatter(r, mine) {
118
141
  tally: null,
119
142
  href: r.goal_id ? `/goals#/goal/${encodeURIComponent(r.goal_id)}` : '/goals',
120
143
  ref: { type: 'goal_membership_request', id: r.id },
144
+ context: mine
145
+ ? `You asked to join the team of the goal "${r.goal_title || r.goal_id}". Its lead decides; once you are in, you can claim its tasks.`
146
+ : `${r.display_name || r.github_login || 'Someone'} asks to join the team of the goal "${r.goal_title || r.goal_id}". Members of a goal may claim its tasks; its lead decides who joins.`,
121
147
  };
122
148
  }
123
149
 
@@ -258,6 +258,8 @@ function accessRequestDocketMatter(r) {
258
258
  tally: null,
259
259
  href: '/watch',
260
260
  ref: { type: 'access_request', id: r.id },
261
+ question: `Let ${r.display_name || r.github_login} join this project?`,
262
+ context: `${r.display_name || r.github_login} (GitHub: ${r.github_login}) asked to join this project as a builder${r.vouched ? ', signed in through Bongos so the name is really theirs' : ''}. Admitting them lets them sign in and claim work; declining keeps them out.`,
261
263
  // the card's "more" (task 1004566): their note, as GET /access-requests shows it
262
264
  detail: r.note || null,
263
265
  };
@@ -357,7 +357,7 @@ module.exports = function provisioningRoutes() {
357
357
  // exactly the set the panel preselected. An explicit [] is the different
358
358
  // answer "none of them". maxItems is a sanity ceiling well above a catalog
359
359
  // that is three modules wide today.
360
- modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, ship_mode: { type: 'string', enum: ['auto', 'manual'] }, // the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
360
+ modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, ship_mode: { type: 'string', enum: ['auto', 'manual'] }, genesis: { type: 'boolean' }, // the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
361
361
  })) return;
362
362
  const body = req.body || {};
363
363
  const detailFault = badProjectDetail(body.detail);
@@ -472,7 +472,9 @@ module.exports = function provisioningRoutes() {
472
472
  // Born into genesis (task 1004506, ../birth.js): only the row this request MADE, and before the
473
473
  // provision intent exists, so the runner's web.env already carries the marker and the demo's box.
474
474
  // Best-effort: a project that misses its marker opens as an ordinary hall; the create still stands.
475
- if (created) await birth.stampBirth(pool, instance.id).catch((err) => log.error('[provisioning] birth marker not stamped', err));
475
+ // Genesis is OFF by default (owner, 2026-11-01): a new project opens as the full hall. Only a demo (its
476
+ // time box lives on the birth marker) or an explicit `genesis: true` (the test toggle) is born into it.
477
+ if (created && (demoAsk.demo || body.genesis === true)) await birth.stampBirth(pool, instance.id).catch((err) => log.error('[provisioning] birth marker not stamped', err));
476
478
  // The wizard's ship mode (task 1004629) rides beside it; a miss means the project asks at its first ship.
477
479
  if (created) await birth.stampShipMode(pool, instance.id, body.ship_mode).catch((err) => log.error('[provisioning] ship mode not stamped', err));
478
480
  const demoRow = created && demoAsk.demo ? await demo.startDemoProject(pool, provisioning, instance.id, demoAsk.demo) : null;
@@ -56,6 +56,8 @@ function redteamDocketMatter(r) {
56
56
  tally: null,
57
57
  href: '/watch',
58
58
  ref: { type: 'vulnerability_report', id: r.id },
59
+ question: 'Is this security flaw real?',
60
+ context: `A security report: someone found what they say is a flaw in this project itself${r.target ? ` (in ${r.target})` : ''}${r.severity ? `, rated ${r.severity}` : ''}. "${r.title}". ${r.status === 'repro_attempted' ? 'Someone has tried to reproduce it. ' : ''}Confirm it if it is real, so it is fixed and the reporter earns the bounty; dispute it if it is not.`,
59
61
  // the card's "more" (task 1004566): what GET /security/reports shows this
60
62
  // same atom — what it targets and the report's description
61
63
  detail: [r.target ? `Target: ${r.target}` : '', r.description || ''].filter(Boolean).join('\n\n') || null,
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.20",
3
+ "version": "1.24.22",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.20",
9
+ "version": "1.24.22",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.20",
3
+ "version": "1.24.22",
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",
@@ -11009,5 +11009,17 @@
11009
11009
  "id": "1005032",
11010
11010
  "text": "The Builders page now has the circuit-trace divider from the owner's Studio idea between the column headings and the first row, in light and dark and on phones."
11011
11011
  }
11012
+ ],
11013
+ "1.24.21": [
11014
+ {
11015
+ "id": "1005013",
11016
+ "text": "Every card in the city now says what it is. Under each question sits a short plain explanation: what kind of thing it is, which goal, task, person or module it belongs to, and what answering does. A Review card used to show a"
11017
+ }
11018
+ ],
11019
+ "1.24.22": [
11020
+ {
11021
+ "id": "1005034",
11022
+ "text": "New projects now open as the full hall instead of starting in the guided setup (genesis and founding mode). The guided setup is paused for now and stays available for demo projects and for test projects that ask for it. Projec"
11023
+ }
11012
11024
  ]
11013
11025
  }
@@ -77,6 +77,9 @@ function moduleRecheckMatter(r, mayRerun) {
77
77
  proposer: null,
78
78
  tally: null,
79
79
  href: '/modules',
80
+ context: failed
81
+ ? `${r.module_key} is a module (an add-on to this project). Version ${r.version} failed its automatic security check, so it is held back until someone runs the check again.`
82
+ : `${r.module_key} is a module (an add-on to this project). Version ${r.version} is being checked again for safety; nothing is needed unless the check fails.`,
80
83
  // "<key>@<version>": the reassess route's two path parts (task 1004566)
81
84
  ref: { type: 'module_version', id: `${r.module_key}@${r.version}` },
82
85
  // the one act a governor takes on it, under the reassess route's own atom,
@@ -143,6 +143,8 @@ function coreUpdateDocketMatters(update) {
143
143
  tally: null,
144
144
  href: '/settings#software-update',
145
145
  ref: { type: 'core_version', id: String(update.latest) },
146
+ question: `Update Bongos to ${update.latest}?`,
147
+ context: `Bongos is the software this project runs on. This project runs ${update.running}; ${update.latest} is out. Taking it moves the project to the new version on its next restart.`,
146
148
  }];
147
149
  }
148
150
  // The running core, or null when it cannot be named. Read lazily and fail-soft,
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.20'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.22'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,139 @@
1
+ // tests/city_card_context.mjs — EVERY CITY CARD SAYS WHAT IT IS (task 1005013).
2
+ //
3
+ // The owner, 2026-10-29: the cards "exist without context". A Review card showed a
4
+ // done-when criterion's raw wording with no question; a security card "Security
5
+ // report: <title>". What this pins:
6
+ // * the docket passes a matter's own `context` and `question` through, bounded,
7
+ // and gives every source the design names a plain sentence of its own when a
8
+ // matter brought none — no card is ever only a bare title;
9
+ // * every source's mapper writes context naming what it belongs to (the goal,
10
+ // the task, the person, the module) — the Review card names its goal, its
11
+ // shipped tasks and what the sign-off closes, and asks a real question;
12
+ // * the hall carries both to the card, and the one card drawing (drawAsk: deck,
13
+ // street, interior, city) shows the context under the question.
14
+ //
15
+ // Run: node --test tests/city_card_context.mjs
16
+
17
+ import { test } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import { createRequire } from 'node:module';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const docket = require('../modules/government/docket.js');
23
+ const lifecycleDocket = require('../modules/lifecycle/governor-docket.js');
24
+ const ideasDocket = require('../modules/ideas/governor-docket.js');
25
+ const reportsRoute = require('../modules/security/routes/reports.js');
26
+ const accessRoute = require('../modules/onboarding/routes/access-requests.js');
27
+ const coreUpdate = require('../src/bongos/routes/core-update.js');
28
+ const moduleRechecks = require('../src/bongos/module-rechecks.js');
29
+ const treasury = require('../modules/government/treasury.js');
30
+ const living = require('../modules/hall-ui/public/gov-living.js');
31
+ const deck = require('../modules/hall-ui/public/street-deck.js');
32
+
33
+ const at = { source: 'criteria_uat', building: 'review' };
34
+
35
+ test('every source the design names has its own plain sentence, used when a matter brings none', () => {
36
+ for (const s of docket.EXPECTED_SOURCES) {
37
+ const words = docket.SOURCE_CONTEXT[s.source];
38
+ assert.ok(typeof words === 'string' && words.length > 40, `${s.source} has a sentence`);
39
+ assert.equal(docket.normalizeMatter({ id: 1, title: 't' }, { source: s.source, building: s.building }).context, words);
40
+ }
41
+ });
42
+
43
+ test('a matter\'s own context and question win, bounded and on one line; junk is dropped', () => {
44
+ const m = docket.normalizeMatter({ id: 1, title: 't', context: ' Its own\nwords. ', question: ' Does it work? ' }, at);
45
+ assert.equal(m.context, 'Its own words.');
46
+ assert.equal(m.question, 'Does it work?');
47
+ const long = docket.normalizeMatter({ id: 1, title: 't', context: 'word '.repeat(400), question: 'q '.repeat(200) }, at);
48
+ assert.ok(long.context.length <= 600 && long.context.endsWith('…'));
49
+ assert.ok(long.question.length <= 160);
50
+ for (const bad of [null, '', ' ', 42, { x: 1 }]) {
51
+ const n = docket.normalizeMatter({ id: 1, title: 't', context: bad, question: bad }, at);
52
+ assert.equal(n.context, docket.SOURCE_CONTEXT.criteria_uat, 'falls back to the source sentence');
53
+ assert.ok(!('question' in n));
54
+ }
55
+ assert.equal(docket.normalizeMatter({ id: 1, title: 't' }, { source: 'nobody_named', building: 'review' }).context, undefined, 'an unknown source invents nothing');
56
+ });
57
+
58
+ test('the Review card asks a real question and names the goal, the shipped work and what the sign-off closes', () => {
59
+ const c = { id: 9, criterion_md: 'Builders can sign in with **Google**', goal_id: 7, goal_title: 'Google sign-in', shipped_total: 3, backend_only: false };
60
+ const m = lifecycleDocket.uatMatter(c, {});
61
+ assert.equal(m.question, 'Does this work on the live site now?');
62
+ assert.match(m.context, /The promise: "Builders can sign in with Google"/);
63
+ assert.match(m.context, /done-when promises of the goal "Google sign-in"/);
64
+ assert.match(m.context, /All 3 tasks built for it have shipped/);
65
+ assert.match(m.context, /record a short video/);
66
+ assert.match(m.context, /the goal is achieved once all its promises are closed/);
67
+ assert.equal(m.title, c.criterion_md, 'the title stays the promise itself (desks and the street list read it)');
68
+ const back = lifecycleDocket.uatMatter({ ...c, backend_only: true, shipped_total: 1, goal_title: null }, {});
69
+ assert.match(back.question, /Check it and sign it off/);
70
+ assert.match(back.context, /nothing to see on screen/);
71
+ assert.match(back.context, /The one task built for it has shipped/);
72
+ assert.match(back.context, /of this version/);
73
+ });
74
+
75
+ test('every other source names what it belongs to', () => {
76
+ const lc = lifecycleDocket;
77
+ assert.match(lc.overrideMatter({ id: 1, task_id: 4, task_title: 'Fix login', requester_login: 'kai' }, true).context, /"Fix login".*failed/);
78
+ assert.match(lc.removalMatter({ proposal_task_id: 2, title: 'x', goal_title: 'Docs' }).context, /goal "Docs"/);
79
+ assert.match(lc.joinRequestMatter({ id: 3, goal_id: 5, goal_title: 'Maps', display_name: 'Ana' }, false).context, /Ana asks to join the team of the goal "Maps"/);
80
+ assert.match(lc.joinRequestMatter({ id: 3, goal_id: 5, goal_title: 'Maps' }, true).context, /^You asked to join/);
81
+ assert.match(ideasDocket.blockerDocketMatter({ id: 1, title: 'x', blocking_task_ids: [1, 2] }, true).context, /2 tasks are stopped/);
82
+ const r = reportsRoute.redteamDocketMatter({ id: 2, title: 'No limit on tries', status: 'open', target: '/login', severity: 'high' });
83
+ assert.equal(r.question, 'Is this security flaw real?');
84
+ assert.match(r.context, /flaw in this project itself \(in \/login\), rated high\. "No limit on tries"/);
85
+ const a = accessRoute.accessRequestDocketMatter({ id: 1, github_login: 'kai', display_name: 'Kai', vouched: true });
86
+ assert.equal(a.question, 'Let Kai join this project?');
87
+ assert.match(a.context, /GitHub: kai/);
88
+ const [u] = coreUpdate.coreUpdateDocketMatters({ reading: 'ok', behind: true, latest: '1.25.0', running: '1.24.1' });
89
+ assert.match(u.context, /runs 1\.24\.1; 1\.25\.0 is out/);
90
+ assert.match(moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'image-tools', version: '1.2.0', security_failed: true }, true).context, /image-tools is a module.*failed its automatic security check/);
91
+ const b = treasury.borrowMatter({ id: 1, goal_id: 3, spent_usd: 20, budget_usd: 20, amount_usd: 10, filed_at: null }, { title: 'Maps' });
92
+ assert.match(b.context, /Maps has spent \$20/);
93
+ assert.match(b.question, /^Lend Maps/);
94
+ });
95
+
96
+ test('board sittings say what is being decided and by what rule', () => {
97
+ const item = (subject_type, pass_rule) => ({ id: 1, subject_type, subject_id: 1, subject: { title: 'An idea', author_id: 9 }, constitution: { pass_rule }, votes: [], closed_at: null });
98
+ assert.match(docket.boardMatter(item('full_idea', 'first_ratifier'), { viewerId: 1, members: [] }).context, /becomes part of the plan.*One yes decides it\./);
99
+ assert.match(docket.boardMatter(item('constitutional_amendment', 'majority'), { viewerId: 1, members: [] }).context, /change to the charter.*more than half/);
100
+ assert.match(docket.boardMatter(item('genesis_stage', 'consent'), { viewerId: 1, members: [] }).context, /founding stage.*unless someone objects/);
101
+ });
102
+
103
+ // a DOM just big enough for drawAsk
104
+ function fakeDoc() {
105
+ const make = (tag) => ({
106
+ tag, className: '', textContent: '', children: [], attrs: {},
107
+ setAttribute(k, v) { this.attrs[k] = v; }, appendChild(c) { this.children.push(c); return c; }, addEventListener() {},
108
+ });
109
+ return { createElement: make, createTextNode: (t) => ({ tag: '#text', textContent: t, children: [] }) };
110
+ }
111
+ function find(node, cls) {
112
+ if (node.className && String(node.className).split(' ').includes(cls)) return node;
113
+ for (const c of node.children || []) { const hit = find(c, cls); if (hit) return hit; }
114
+ return null;
115
+ }
116
+
117
+ test('the hall carries context and question to the card, and the card draws the context under its question', () => {
118
+ const matter = docket.normalizeMatter(lifecycleDocket.uatMatter({ id: 9, criterion_md: 'Builders can sign in with Google', goal_title: 'Google sign-in', shipped_total: 3 }, {}), at);
119
+ const model = living.livingModel({ docket: { matters: [matter] }, now: Date.now() });
120
+ const ask = model.city.asks.find((x) => x.id === matter.id);
121
+ assert.ok(ask, 'the matter is an ask');
122
+ assert.equal(ask.raw.question, 'Does this work on the live site now?');
123
+ assert.equal(ask.raw.context, matter.context);
124
+ const cv = deck.cardView(ask, {});
125
+ assert.equal(cv.question, 'Does this work on the live site now?');
126
+ assert.equal(cv.context, matter.context);
127
+ const card = deck.drawAsk(fakeDoc(), cv, {}, {});
128
+ const q = find(card, 'ask__q');
129
+ const ctx = find(card, 'ask__ctx');
130
+ assert.ok(q && ctx, 'question and context are both drawn');
131
+ assert.equal(ctx.textContent, matter.context);
132
+ assert.ok(card.children.indexOf(ctx) === card.children.indexOf(q) + 1, 'the context sits right under the question');
133
+ });
134
+
135
+ test('a blocker read straight from GET /blockers carries the docket\'s blocker sentence', () => {
136
+ const model = living.livingModel({ blockers: [{ id: 4, title: 'Pick a host', status: 'open', created_at: '2026-10-01T00:00:00Z' }], now: Date.now() });
137
+ const ask = model.city.asks.find((x) => x.id === 'blockers:4');
138
+ assert.equal(ask.raw.context, docket.SOURCE_CONTEXT.blockers, 'the two copies of the sentence agree');
139
+ });
@@ -67,7 +67,7 @@ function project({ pots = [], borrows = [], cap = null, spend = {}, lastMonth =
67
67
  return { rows: [{ month: params[0], total_usd: thisMonth }, ...(st.lastMonth ? [{ month: params[1], total_usd: st.lastMonth }] : [])] };
68
68
  }
69
69
  if (/^SELECT t\.goal_id, COALESCE\(SUM\(cl\.amount_usd\), 0\) AS total_usd FROM cost_log cl JOIN tasks t/.test(s)) {
70
- assert.equal(params[0], '2026-10');
70
+ assert.match(params[0], /^\d{4}-\d{2}$/); // the month asked for: NOW's in the direct cases (pinned by t.month below), the real clock's over the route
71
71
  return { rows: Object.entries(st.spend).map(([goal_id, total_usd]) => ({ goal_id, total_usd })) };
72
72
  }
73
73
  if (/^SELECT goal_id, budget_usd, set_by, set_at FROM government_pots/.test(s)) return { rows: st.pots };
@@ -98,7 +98,9 @@ test('normalizeMatter: the one shape, prefixed id, ISO dates', () => {
98
98
  assert.deepEqual(m.proposer, { id: '3', login: 'theo', name: null });
99
99
  assert.deepEqual(m.tally, { for: 2, against: 1, open: 2, needs: 'x' });
100
100
  assert.deepEqual(m.ref, { type: 'a', id: '5' });
101
- assert.deepEqual(Object.keys(m).sort(), ['building', 'deadline', 'href', 'id', 'opened_at', 'proposer', 'ref', 'rule', 'short', 'source', 'state', 'tally', 'title', 'where'].sort());
101
+ // `context` is the source's own sentence when the matter brought none (task 1005013)
102
+ assert.deepEqual(Object.keys(m).sort(), ['building', 'context', 'deadline', 'href', 'id', 'opened_at', 'proposer', 'ref', 'rule', 'short', 'source', 'state', 'tally', 'title', 'where'].sort());
103
+ assert.equal(m.context, docket.SOURCE_CONTEXT.access_requests);
102
104
  });
103
105
 
104
106
  test('normalizeMatter carries a matter\'s detail for the card\'s "more" only as bounded text (task 1004566)', () => {
@@ -425,7 +425,7 @@ test('the decision deck on a real site: the docket\'s own asks in their own word
425
425
  noPrototype(p, 'deck');
426
426
  const deck = p.byId.get('streetDeck');
427
427
  assert.equal(deck.hidden, false, 'the deck opens');
428
- const docketTitles = fx('government__docket').matters.map((m) => m.title).concat(fx('blockers').blockers.map((b) => b.title));
428
+ const docketTitles = fx('government__docket').matters.flatMap((m) => [m.title, m.question]).filter(Boolean).concat(fx('blockers').blockers.map((b) => b.title));
429
429
  assert.ok(docketTitles.some((t) => deck.textContent.includes(t)), 'a real ask, as its source words it');
430
430
  // the format's refusal is the prototype's: a real ask goes to its floor's desk, where its real acts are
431
431
  assert.doesNotMatch(deck.textContent, /sent back|until its agent writes it|prototype data|pass it on/, 'no refusal, no key, no seats to pass to');
@@ -52,6 +52,12 @@ require(path.join(PUB, 'street-desk.js')); // the line rule's fit, on the global
52
52
  const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
53
53
  const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
54
54
  const NOW = Date.parse('2026-10-09T14:00:00.000Z');
55
+ // The page reads the clock itself (street-depth.js, GovFeeds.model(raw, Date.now())), and the fixtures are
56
+ // October's: pin the page's clock to NOW so a later real month does not read them as stale and hide a tile.
57
+ class PinnedDate extends Date {
58
+ constructor(...a) { super(...(a.length ? a : [NOW])); }
59
+ static now() { return NOW; }
60
+ }
55
61
  const clone = (o) => JSON.parse(JSON.stringify(o));
56
62
 
57
63
  function feeds(over = {}) {
@@ -475,7 +481,7 @@ async function bootPage({ withLiving = true } = {}) {
475
481
  const window = { innerWidth: 1440, innerHeight: 900, location: { hash: '' }, localStorage: { getItem: () => 'dark', setItem() {} }, matchMedia: () => ({ matches: true, addEventListener() {} }), addEventListener() {} };
476
482
  class MutationObserver { observe() {} }
477
483
  window.fetch = fetch;
478
- const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, MutationObserver, Set });
484
+ const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date: PinnedDate, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, MutationObserver, Set });
479
485
  ctx.globalThis = ctx;
480
486
  for (const f of (withLiving ? ['gov-living.js', 'gov-feeds.js'] : []).concat(['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street.js'])) vm.runInContext(read(f), ctx);
481
487
  await window.GovStreetPage.ready;