@bongos/core 1.21.46 → 1.21.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +37 -52
- package/.claude/skills/bongos-feedback/SKILL.md +5 -4
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +2 -2
- package/docs/file-map.md +0 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +242 -243
- package/modules/hall-ui/public/commands-lib.js +1 -1
- package/modules/hall-ui/public/street-card.css +8 -2
- package/modules/hall-ui/public/street-desk.js +78 -9
- package/modules/hall-ui/public/street.css +4 -1
- package/modules/hall-ui/public/street.js +9 -6
- package/modules/hall-ui/records/governor-street.md +14 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/pack-env-prefix.js +73 -0
- package/scripts/gds/provision-repo.js +13 -0
- package/scripts/gds/provision-units.js +6 -17
- package/scripts/gds/provision.js +8 -2
- package/src/instance-config.js +28 -1
- package/src/module-api.js +1 -1
- package/tests/hall_street_desk.mjs +64 -2
- package/tests/instance_config.mjs +53 -0
- package/tests/instance_feedback.mjs +12 -0
- package/tests/provision.mjs +103 -0
- package/tests/skill_menu_ruling.mjs +1 -1
- package/.claude/skills/feedback/SKILL.md +0 -39
- package/scripts/bongos-mcp.js +0 -28
- package/scripts/gds/feedback-latest.js +0 -224
- package/tests/feedback_latest.mjs +0 -122
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
{
|
|
25
25
|
key: 'find', label: 'Finding things out and working with others',
|
|
26
26
|
blurb: 'Looking up what the project already knows, asking for help, and passing work on.',
|
|
27
|
-
names: ['status', 'recall', 'ask-for-help', 'collab-review', 'session-handoff', 'read-session-export'
|
|
27
|
+
names: ['status', 'recall', 'ask-for-help', 'collab-review', 'session-handoff', 'read-session-export'],
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
key: 'owner', label: 'Owner routines',
|
|
@@ -108,8 +108,10 @@ body[data-page="street"] {
|
|
|
108
108
|
|
|
109
109
|
.mcard-btn:focus-visible, .mcard-textbtn:focus-visible, .mcard-form__text:focus-visible, .mcard-form__select:focus-visible, .mcard-chip:focus-visible, .mcard-chips__field:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
110
110
|
|
|
111
|
-
/* the toast: one short line over the street
|
|
112
|
-
|
|
111
|
+
/* the toast: one short line over the street. Centred with translate, not
|
|
112
|
+
transform: the enter animation ends on transform none, which put the toast
|
|
113
|
+
half its width right of centre (task 1004562) */
|
|
114
|
+
.street-toast { position: absolute; left: 50%; top: 192px; translate: -50% 0; max-width: calc(100% - 32px); box-sizing: border-box; font-size: 15px; line-height: 1.35; color: var(--ink); padding: 11px 20px; border-radius: 999px; background: var(--bg-card); border: 1px solid var(--street-rule-strong); z-index: 10; pointer-events: none; text-align: center; }
|
|
113
115
|
.street-toast[hidden] { display: none; }
|
|
114
116
|
|
|
115
117
|
/* the city answers: a building rises (the prototype's rise) or lights, a pin settles */
|
|
@@ -128,6 +130,10 @@ body[data-page="street"] {
|
|
|
128
130
|
@media (max-width: 560px) {
|
|
129
131
|
.street-toast { top: 96px; border-radius: 16px; }
|
|
130
132
|
}
|
|
133
|
+
/* the desk's toast after an act (Desks.dc.html, task 1004562): an ink pill with
|
|
134
|
+
the orange dot, low over the desk, just above the six readings */
|
|
135
|
+
.street-toast--desk { top: auto; bottom: 150px; display: flex; align-items: center; gap: 10px; width: max-content; font-size: 14px; background: var(--ink); color: var(--bg); border: 0; border-radius: 999px; text-align: left; }
|
|
136
|
+
.street-toast--desk::before { content: ''; flex: none; width: 8px; height: 8px; border-radius: 999px; background: var(--accent); }
|
|
131
137
|
|
|
132
138
|
/* The Kill Switch: under reduced motion nothing moves; the street still
|
|
133
139
|
answers, at rest (the toast, the new pins, the lit windows). */
|
|
@@ -96,6 +96,50 @@
|
|
|
96
96
|
if (d < 14) return d + ' days';
|
|
97
97
|
return Math.round(d / 7) + ' weeks';
|
|
98
98
|
}
|
|
99
|
+
// The record line's age, in the board's short words: 'today', '3d', '2w', '1mo'
|
|
100
|
+
// (task 1004562: the long age was cut off the end of nearly every record item).
|
|
101
|
+
function shortAge(at, now) {
|
|
102
|
+
const t = at ? Date.parse(at) : NaN;
|
|
103
|
+
if (!isFinite(t) || !now) return '';
|
|
104
|
+
const d = Math.floor(Math.max(0, now - t) / 86400000);
|
|
105
|
+
if (d < 1) return 'today';
|
|
106
|
+
if (d < 14) return d + 'd';
|
|
107
|
+
if (d < 60) return Math.round(d / 7) + 'w';
|
|
108
|
+
return Math.round(d / 30) + 'mo';
|
|
109
|
+
}
|
|
110
|
+
// One record item: the title, then what came of it and when, in 30
|
|
111
|
+
// characters. The age always shows; the outcome only when the title fits
|
|
112
|
+
// beside it whole ('Lea → metic · 2w' on the board).
|
|
113
|
+
function recordText(h, now) {
|
|
114
|
+
const title = str(h && h.title).replace(/\s+/g, ' ').trim();
|
|
115
|
+
const age = shortAge(h && h.at, now);
|
|
116
|
+
const out = str(h && h.outcome).trim();
|
|
117
|
+
const tail = (parts) => parts.filter(Boolean).join(' · ');
|
|
118
|
+
const full = tail([title, out, age]);
|
|
119
|
+
if (full.length <= LIMITS.recordLen) return full;
|
|
120
|
+
const room = LIMITS.recordLen - (age ? age.length + 3 : 0);
|
|
121
|
+
return fit(tail([fit(title, room), age]), LIMITS.recordLen);
|
|
122
|
+
}
|
|
123
|
+
// A quiet floor's tile says what its number counts, in two words that read as
|
|
124
|
+
// a label: the floor line's own words when they follow that same number and
|
|
125
|
+
// are short ('5 seats'), else the floor's name ('3 testers', '2 signed off'),
|
|
126
|
+
// else the line's last two words ('1 went live'). Never the first two words of
|
|
127
|
+
// a longer line, which read 'Lu, Bo' or 'piece of' (task 1004562).
|
|
128
|
+
function floorLabel(count, line, name) {
|
|
129
|
+
const first = str(line).split(' · ')[0].replace(/\s+/g, ' ').trim();
|
|
130
|
+
const m = /^([$]?[0-9][0-9,.%]*)\s+(.+)$/.exec(first);
|
|
131
|
+
const same = !!m && m[1] === str(count).trim();
|
|
132
|
+
const w = (s) => s.split(' ').filter(Boolean);
|
|
133
|
+
if (same && w(m[2]).length <= LIMITS.tileWords) return m[2];
|
|
134
|
+
const nm = str(name).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
135
|
+
if (nm && !/^§/.test(nm) && w(nm).length <= LIMITS.tileWords) return nm;
|
|
136
|
+
if (same) return w(m[2]).slice(-LIMITS.tileWords).join(' ');
|
|
137
|
+
return 'here';
|
|
138
|
+
}
|
|
139
|
+
// A count agrees with its noun: '1 rank', '2 open votes' (task 1004562).
|
|
140
|
+
const SINGULAR = Object.freeze({ ranks: 'rank', runners: 'runner', builders: 'builder', invites: 'invite', settings: 'setting', modules: 'module', backups: 'backup', updates: 'update' });
|
|
141
|
+
const PLURAL = Object.freeze({ 'open vote': 'open votes', update: 'updates' });
|
|
142
|
+
function agree(n, noun) { return Number(n) === 1 ? (SINGULAR[noun] || noun) : (PLURAL[noun] || noun); }
|
|
99
143
|
|
|
100
144
|
// The words a floor counts its rows in, and says when it is cleared.
|
|
101
145
|
const NOUNS = Object.freeze({
|
|
@@ -105,6 +149,10 @@
|
|
|
105
149
|
const CLEAR = Object.freeze({
|
|
106
150
|
access_requests: 'Everyone is in', goal_join_requests: 'No one is asking', override_requests: 'No overrides asked',
|
|
107
151
|
gate_approvals: 'Everything ready is live', blockers: 'Nothing blocked on you', board: 'Every vote is in', idea: 'No ideas waiting',
|
|
152
|
+
// every other floor that holds matters says its own line too (task 1004562)
|
|
153
|
+
redteam_reports: 'No reports open', removal_proposals: 'No removals asked', grade_appeals: 'No appeals open',
|
|
154
|
+
module_rechecks: 'Every module checked', criteria_uat: 'Nothing waits for a tester', core_update: 'Core is up to date',
|
|
155
|
+
autobongos_fence: 'The switch is set', standing_rules: 'No rules proposed', treasury_spending: 'No spending asked',
|
|
108
156
|
});
|
|
109
157
|
// The card's acts under the desk's words (the board's button labels).
|
|
110
158
|
const ACT_WORDS = Object.freeze({
|
|
@@ -246,7 +294,12 @@
|
|
|
246
294
|
if (card && card.subject) card.subject.council = councilOf(ctx.docket, card.building) ? card.building : null;
|
|
247
295
|
const actions = [];
|
|
248
296
|
const choices = {};
|
|
297
|
+
// 'open' leaves for an older page: a row that can act here keeps its detail
|
|
298
|
+
// in place (tap the name) and drops the link; only a row with no act of its
|
|
299
|
+
// own may link out (owner standard, task 1004562)
|
|
300
|
+
const inPlace = acts.some((a) => a && a.key !== 'open' && a.key !== 'record');
|
|
249
301
|
acts.forEach((a) => {
|
|
302
|
+
if (a.key === 'open' && inPlace) return;
|
|
250
303
|
// the core's 'take' is its own word ('take it'), not the rank's 'take back'
|
|
251
304
|
const word = a.key === 'take' && card.subject && card.subject.kind === 'core_pin' ? 'take_core' : a.key;
|
|
252
305
|
if (actions.length >= LIMITS.actions || !ACT_WORDS[word]) return;
|
|
@@ -311,7 +364,18 @@
|
|
|
311
364
|
// a desk row that stands in for a source (the core update's check-then-take)
|
|
312
365
|
// replaces that source's matter rows on this floor
|
|
313
366
|
const over = desked.map((r) => r.over).filter(Boolean);
|
|
314
|
-
|
|
367
|
+
const here = floorMatters(key, floor, docket).filter((m) => !done[str(m.id)]);
|
|
368
|
+
// the stand-in keeps what it stands for: a matter that needs you still
|
|
369
|
+
// needs you on the desk (orange status, its age), as on the floor's sign
|
|
370
|
+
// and the street's count (task 1004562)
|
|
371
|
+
desked.forEach((r) => {
|
|
372
|
+
if (!r.over) return;
|
|
373
|
+
const stood = here.filter((m) => m.source === r.over && stateOf(m) === 'needs');
|
|
374
|
+
if (!stood.length) return;
|
|
375
|
+
r.needs = true;
|
|
376
|
+
if (!r.opened) r.opened = stood.reduce((a, m) => (m.opened_at && (!a || Date.parse(m.opened_at) < Date.parse(a)) ? m.opened_at : a), null);
|
|
377
|
+
});
|
|
378
|
+
return here.filter((m) => over.indexOf(m.source) < 0).map((m) => rowOf(m, c))
|
|
315
379
|
.concat(desked)
|
|
316
380
|
.sort((a, b) => (b.needs - a.needs) || ((Date.parse(a.opened || 0) || 0) - (Date.parse(b.opened || 0) || 0)));
|
|
317
381
|
}
|
|
@@ -400,9 +464,11 @@
|
|
|
400
464
|
const quiet = rows.length === 0;
|
|
401
465
|
let status, hot = false;
|
|
402
466
|
if (!docket) status = 'the docket could not be read';
|
|
403
|
-
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + noun + (age ? ' · oldest ' + age : ''); }
|
|
404
|
-
else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + moving + (age ? ' · oldest ' + age : ''); }
|
|
405
|
-
|
|
467
|
+
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + agree(needs.length, noun) + (age ? ' · oldest ' + age : ''); }
|
|
468
|
+
else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + agree(counted.length, moving) + (age ? ' · oldest ' + age : ''); }
|
|
469
|
+
// only tools left (an invite, a setting): the floor's line, unless that line
|
|
470
|
+
// still counts what you just did ('1 needs you' after you let them in)
|
|
471
|
+
else if (rows.length) status = /needs? you/.test(str(fl.line)) ? 'nothing needs you' : fl.line;
|
|
406
472
|
else if (cleared) status = 'floor clear';
|
|
407
473
|
else status = 'nothing needs you · ' + fl.line;
|
|
408
474
|
const history = docket && Array.isArray(docket.history) ? docket.history.filter((h) => h && h.building === o.key) : [];
|
|
@@ -419,13 +485,16 @@
|
|
|
419
485
|
// the order's one number: the share of its goals' promises kept
|
|
420
486
|
const on = ordered.reduce((a, r) => a + (r.pips || []).filter((p) => p.on).length, 0);
|
|
421
487
|
const of = ordered.reduce((a, r) => a + (r.pips || []).length, 0);
|
|
422
|
-
tile(needs.length, noun);
|
|
488
|
+
tile(needs.length, agree(needs.length, noun));
|
|
423
489
|
if (of) tile(Math.round((100 * on) / of) + '%', 'promises kept');
|
|
424
|
-
} else if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
|
|
425
|
-
|
|
490
|
+
} else if (rows.length) { tile(needs.length, agree(needs.length, noun)); tile(counted.length - needs.length, agree(counted.length - needs.length, moving)); }
|
|
491
|
+
// a quiet floor's own number; never on a floor you just cleared, and never a
|
|
492
|
+
// 'needs you' count on a floor where nothing does (task 1004562)
|
|
493
|
+
else if (fl.count && !cleared && !/needs? you/.test(str(fl.line))) tile(fl.count, floorLabel(fl.count, fl.line, spec[0]));
|
|
426
494
|
tile(history.length, 'on record');
|
|
427
|
-
|
|
428
|
-
|
|
495
|
+
// what you did keeps its ' · now'; a closed item keeps its age (task 1004562)
|
|
496
|
+
const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t, LIMITS.recordLen - 6) + ' · now', mine: true }))
|
|
497
|
+
.concat(history.map((h, i) => ({ key: 'h' + i, text: recordText(h, o.now), mine: false })))
|
|
429
498
|
.slice(0, LIMITS.record);
|
|
430
499
|
return {
|
|
431
500
|
key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
|
|
@@ -344,7 +344,10 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
344
344
|
.desk-quiet--cleared .desk-quiet__tick { stroke: var(--street-accent-text); }
|
|
345
345
|
.desk-quiet__line { font-size: 20px; font-weight: 300; }
|
|
346
346
|
.desk-quiet__sub { font-size: 13.5px; color: var(--ink-faint); }
|
|
347
|
-
|
|
347
|
+
/* the record is ONE line, as on the board: items that do not fit wrap onto a
|
|
348
|
+
second line that is clipped away, so the newest stay and none is cut in half
|
|
349
|
+
(task 1004562) */
|
|
350
|
+
.desk-record { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 22px; border-top: 1px solid var(--street-rule); margin-top: 18px; padding-top: 14px; box-sizing: border-box; line-height: 20px; max-height: 35px; overflow: hidden; }
|
|
348
351
|
.desk-record__label { font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }
|
|
349
352
|
.desk-record__item { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }
|
|
350
353
|
.desk-record__item--mine { color: var(--ink); }
|
|
@@ -707,11 +707,11 @@
|
|
|
707
707
|
draw();
|
|
708
708
|
return send(req).then(function (out) {
|
|
709
709
|
desk.busy = false;
|
|
710
|
-
if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
|
|
710
|
+
if (!out.ok) { say(C.refusal(out.status, out.body), true); draw(); return false; }
|
|
711
711
|
desk.order[k] = null;
|
|
712
712
|
desk.mine[k] = [dm.save.council ? 'order sent to council' : 'order set'].concat(desk.mine[k] || []);
|
|
713
713
|
bumpToday();
|
|
714
|
-
say(dm.save.said);
|
|
714
|
+
say(dm.save.said, true);
|
|
715
715
|
draw();
|
|
716
716
|
return load().then(function () { return true; });
|
|
717
717
|
});
|
|
@@ -760,7 +760,7 @@
|
|
|
760
760
|
var C = window.GovMatterCard, st = view.inside;
|
|
761
761
|
if (!C || !st || !r.card || desk.busy) return Promise.resolve(false);
|
|
762
762
|
var refused = C.check(r.card, key, input);
|
|
763
|
-
if (refused) { say(refused); return Promise.resolve(false); }
|
|
763
|
+
if (refused) { say(refused, true); return Promise.resolve(false); }
|
|
764
764
|
var k = st.key + ':' + (view.insideModel ? view.insideModel.selFloor : 0);
|
|
765
765
|
var req = C.request(r.card, key, input);
|
|
766
766
|
var settle = function (body) {
|
|
@@ -774,14 +774,14 @@
|
|
|
774
774
|
if (st.floor == null && view.insideModel) st.floor = view.insideModel.selFloor;
|
|
775
775
|
bumpToday();
|
|
776
776
|
if (a.building && a.kind) card.answer = { building: a.building, matter: r.matter, kind: a.kind, at: Date.now() };
|
|
777
|
-
say(a.toast || 'Done.');
|
|
777
|
+
say(a.toast || 'Done.', true);
|
|
778
778
|
};
|
|
779
779
|
if (!req) return new Promise(function (res) { clearRow(r.key, function () { settle({}); draw(); res(true); }); });
|
|
780
780
|
desk.busy = true;
|
|
781
781
|
draw();
|
|
782
782
|
return send(req).then(function (out) {
|
|
783
783
|
desk.busy = false;
|
|
784
|
-
if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
|
|
784
|
+
if (!out.ok) { say(C.refusal(out.status, out.body), true); draw(); return false; }
|
|
785
785
|
return new Promise(function (res) {
|
|
786
786
|
clearRow(r.key, function () {
|
|
787
787
|
settle(out.body);
|
|
@@ -1230,9 +1230,12 @@
|
|
|
1230
1230
|
for (var j = 0; j < pins.length; j++) if (String(pins[j].getAttribute('data-matters') || pins[j].getAttribute('data-matter')).split(' ').indexOf(a.matter) >= 0) pins[j].className += ' street-pin--answer';
|
|
1231
1231
|
}
|
|
1232
1232
|
var toastTimer = null;
|
|
1233
|
-
|
|
1233
|
+
// desk: the Desks board's toast after an act on a floor (an ink pill with the
|
|
1234
|
+
// orange dot, low over the desk); the street's own toast otherwise (task 1004562)
|
|
1235
|
+
function say(text, desk) {
|
|
1234
1236
|
var t = $('streetToast');
|
|
1235
1237
|
if (!t || !text) return;
|
|
1238
|
+
t.className = 'street-toast' + (desk ? ' street-toast--desk' : '');
|
|
1236
1239
|
t.textContent = text;
|
|
1237
1240
|
t.hidden = false;
|
|
1238
1241
|
if (typeof setTimeout !== 'function') return;
|
|
@@ -33,3 +33,17 @@ Files: `street-inside.js` (`window.GovInside`, node-require-safe, pure): the pro
|
|
|
33
33
|
- **Narrow.** Under 900 px the board becomes a column: the drawing in its own sideways strip (never scaled under 0.6, so a floor stays a 24 px target), then the panel.
|
|
34
34
|
|
|
35
35
|
Tests: `tests/hall_street_inside.mjs` runs the prototype's own `Component` beside `street-inside.js` on the prototype's sample floors and holds every building's section, panel bindings and every district equal to it in both modes, then the docket rules (no invented numbers) and the page in a vm (the persistent lift, the flip, the drawers, the amendment note, the card from a table row, back to the street, a district's tasks). The states file renders every building, a floor pick, the amendment note and a district.
|
|
36
|
+
|
|
37
|
+
## The floor desks, walked (task 1004562)
|
|
38
|
+
|
|
39
|
+
Every building and every floor desk was walked in light, dark and at phone width (375 px) against `Desks.dc.html` and the line rule of `Ground.dc.html`. The live site is behind its password gate, so the walk ran on the real pages in the hall preview harness with its fixtures. What it settled:
|
|
40
|
+
|
|
41
|
+
- **A quiet floor's tile says what it counts.** Two words that read as a label: the floor line's own noun when it follows the same number ("5 seats"), else the floor's name ("3 testers", "2 signed off"), else the line's last two words ("1 went live"). Never the first two words of a longer line ("3 Lu, Bo", "1 piece of"). `floorLabel()` in `street-desk.js`.
|
|
42
|
+
- **A cleared floor shows no count of the work just done.** The docket's floor count lags the act, so a cleared floor draws no floor-count tile, and a floor left with only a tool (the invite) does not repeat a stale "1 needs you" as its status. Floors that hold matters each say their own clear line ("No removals asked").
|
|
43
|
+
- **Counts agree with their nouns**: "2 open votes", "1 rank".
|
|
44
|
+
- **The record is one line.** Each item keeps its age in the board's short words ("· 3d", "· 2w"); the outcome shows only when the title fits beside it; what you did keeps its "· now". Items that do not fit wrap onto a clipped second line, so none is cut in half.
|
|
45
|
+
- **A stand-in row keeps the need it stands for.** The Release desk's core row replaces the core-update matter; it now carries that matter's need, so the status is orange where the floor sign and the street count already said "needs you".
|
|
46
|
+
- **No link out beside an act.** A row that can act in place drops the server's `open` act; its detail opens in place by its name. A row with no act of its own (a Full Idea that needs its page to finish, Work categories) still links, as the standard allows.
|
|
47
|
+
- **The desk's toast is the board's**: an ink pill with the orange dot, low over the desk. The street's own toast is centred again (its enter animation had undone the centring).
|
|
48
|
+
|
|
49
|
+
Left as found, on purpose: the six readings stay under a building (task 1004526 marks the building's own reading), the old month drawers stay under the desk, and the recording sign-off still goes to its page (an open owner choice).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.48",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.48",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.48",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -9040,5 +9040,21 @@
|
|
|
9040
9040
|
"id": "1004572",
|
|
9041
9041
|
"text": "Setting up GitHub sign-in for a new project now says what is really happening at each step, and always leaves a way forward if it gets stuck."
|
|
9042
9042
|
}
|
|
9043
|
+
],
|
|
9044
|
+
"1.21.47": [
|
|
9045
|
+
{
|
|
9046
|
+
"id": "1004562",
|
|
9047
|
+
"text": "Walked every building and floor desk of the Governor City in light, dark and on a phone, and fixed what did not match the design: number tiles now say what they count, a cleared floor no longer shows a stale count, counts read"
|
|
9048
|
+
}
|
|
9049
|
+
],
|
|
9050
|
+
"1.21.48": [
|
|
9051
|
+
{
|
|
9052
|
+
"id": "1004576",
|
|
9053
|
+
"text": "Saying \"Bongos feedback: …\" or suggesting how Bongos should behave now reliably sends it to the Bongos team; an unused leftover skill that confused this was removed."
|
|
9054
|
+
},
|
|
9055
|
+
{
|
|
9056
|
+
"id": "1004573",
|
|
9057
|
+
"text": "A project brought in from an existing repo now always picks up the sign-in and settings the platform gives it, instead of silently ignoring them."
|
|
9058
|
+
}
|
|
9043
9059
|
]
|
|
9044
9060
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/pack-env-prefix.js — which env-var names a hosted project reads, and
|
|
4
|
+
// whether its own pack agrees with what the provisioning runner writes (task 1004573).
|
|
5
|
+
//
|
|
6
|
+
// WHY. The runner writes every hosted project's /etc/<slug>/web.env under ONE prefix
|
|
7
|
+
// (provisionedEnvPrefix). A project ADOPTED from an existing repo brings its own
|
|
8
|
+
// config/branding.json, and mercury's said envPrefix CLOUDBONGOS while web.env carried
|
|
9
|
+
// BONGOS_* — so the project read none of its sign-in credentials, its address or any
|
|
10
|
+
// setting, and the only symptom anyone saw was "did not report sign-in ready".
|
|
11
|
+
//
|
|
12
|
+
// Its own file because both provision-units.js (web.env) and provision-repo.js (the
|
|
13
|
+
// adopt scaffold) need it, and provision-units already requires provision-repo — a
|
|
14
|
+
// helper living in either would make a require cycle. Requires only provision-config.
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const { REPO_ROOT } = require('./provision-config.js');
|
|
19
|
+
|
|
20
|
+
// The pack whose envPrefix a provisioned instance inherits unless its own pack says
|
|
21
|
+
// otherwise (see the long note on ENV_PREFIX_PACK's history in provision-units.js).
|
|
22
|
+
const ENV_PREFIX_PACK = 'config/branding.neutral.json';
|
|
23
|
+
|
|
24
|
+
// The env-var prefix a provisioned instance actually RESOLVES — i.e. the envPrefix
|
|
25
|
+
// of the pack it loads (ENV_PREFIX_PACK). src/instance-config.resolveEnv
|
|
26
|
+
// reads <envPrefix>_<SUFFIX>, and src/branding.resolveBranding applies
|
|
27
|
+
// <envPrefix>_{PUBLIC,BUILDERS,…}_ORIGIN overrides — so a web.env var only takes
|
|
28
|
+
// effect under THIS prefix. Read from the pack (fail-soft to the vanilla default)
|
|
29
|
+
// so it stays in lockstep instead of a magic string. (The prior slug-based prefix
|
|
30
|
+
// was a latent bug: it never matched the neutral pack's CLOUDBONGOS envPrefix, so
|
|
31
|
+
// the OAuth creds were silently unreadable — task 1972.)
|
|
32
|
+
function provisionedEnvPrefix() {
|
|
33
|
+
const { FALLBACK_ENV_PREFIX } = require('../../src/instance-config'); // eslint-disable-line global-require
|
|
34
|
+
try {
|
|
35
|
+
const pack = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, ENV_PREFIX_PACK), 'utf8'));
|
|
36
|
+
return pack.envPrefix || FALLBACK_ENV_PREFIX;
|
|
37
|
+
} catch { return FALLBACK_ENV_PREFIX; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The envPrefix a pack's text names, or null (no pack, not JSON, no envPrefix).
|
|
41
|
+
function packEnvPrefix(packText) {
|
|
42
|
+
try {
|
|
43
|
+
const pack = JSON.parse(String(packText || ''));
|
|
44
|
+
return pack && typeof pack.envPrefix === 'string' ? pack.envPrefix : null;
|
|
45
|
+
} catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Does the instance's OWN pack read the names this runner writes? PURE: give it the
|
|
49
|
+
// pack's text (or null when it could not be read) and the prefix written; it returns
|
|
50
|
+
// a sentence naming the mismatch, or null when the names agree or the pack says nothing.
|
|
51
|
+
function envPrefixMismatch(packText, written = provisionedEnvPrefix()) {
|
|
52
|
+
const own = packEnvPrefix(packText);
|
|
53
|
+
if (!own || own === written) return null;
|
|
54
|
+
return `the project's config/branding.json reads settings named ${own}_*, but the platform wrote ${written}_* — set its envPrefix to "${written}"`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The adopt scaffold's half of the fix: rewrite ONLY the envPrefix value in an adopted
|
|
58
|
+
// repo's config/branding.json so the project reads the names this runner writes,
|
|
59
|
+
// leaving every other byte of the owner's file as they wrote it (a JSON round-trip
|
|
60
|
+
// would reflow it). PURE. Returns { text, from } when it changed something, or null
|
|
61
|
+
// when the names already agree, the pack names no prefix, or it is not JSON.
|
|
62
|
+
function normalizePackEnvPrefix(packText, written = provisionedEnvPrefix()) {
|
|
63
|
+
const raw = String(packText || '');
|
|
64
|
+
const own = packEnvPrefix(raw);
|
|
65
|
+
if (!own || own === written) return null;
|
|
66
|
+
const re = /("envPrefix"\s*:\s*)"[^"]*"/;
|
|
67
|
+
if (!re.test(raw)) return null;
|
|
68
|
+
const text = raw.replace(re, (_m, key) => `${key}"${written}"`);
|
|
69
|
+
if (packEnvPrefix(text) !== written) return null;
|
|
70
|
+
return { text, from: own };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { ENV_PREFIX_PACK, provisionedEnvPrefix, envPrefixMismatch, normalizePackEnvPrefix };
|
|
@@ -19,6 +19,7 @@ const { CONFIG, REPO_ROOT, coreVersionSafe, provisionerBotEmail, provisionerBotN
|
|
|
19
19
|
// upgrade.js can reach these three without pulling provision*.js into the merge
|
|
20
20
|
// resolver's executed closure; re-exported below so every existing caller is unchanged.
|
|
21
21
|
const { instanceRepoRemote, parseTargetRef, resolveOwnerGithubToken } = require('./provision-repo-auth.js');
|
|
22
|
+
const { normalizePackEnvPrefix } = require('./pack-env-prefix.js'); // an adopted pack's envPrefix (task 1004573)
|
|
22
23
|
const { CORE_PKG, compareSemver, isStable } = require('./update-channel.js');
|
|
23
24
|
|
|
24
25
|
function dbName(inst) { return inst.db_name || inst.slug; }
|
|
@@ -832,6 +833,18 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
832
833
|
} else {
|
|
833
834
|
writeFile(giPath, INSTANCE_GITIGNORE);
|
|
834
835
|
}
|
|
836
|
+
// Adopt layers config only where ABSENT, so an owner's existing config/branding.json
|
|
837
|
+
// keeps whatever envPrefix it carried — and web.env is written under the platform's.
|
|
838
|
+
// Mercury's said CLOUDBONGOS: it read none of its credentials or settings (task 1004573).
|
|
839
|
+
// Rewrite just that one value, in this same commit, so the repo and web.env agree.
|
|
840
|
+
const packPath = path.join(root, 'config', 'branding.json');
|
|
841
|
+
if (mode === 'adopt' && fs.existsSync(packPath)) {
|
|
842
|
+
const fixed = normalizePackEnvPrefix(fs.readFileSync(packPath, 'utf8'));
|
|
843
|
+
if (fixed) {
|
|
844
|
+
writeFile(packPath, fixed.text);
|
|
845
|
+
log(` [scaffold] config/branding.json envPrefix ${fixed.from} → the platform's, so the project reads the settings it is given`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
835
848
|
exec(`git -C ${root} config user.name "${provisionerBotName()}"`);
|
|
836
849
|
exec(`git -C ${root} config user.email "${provisionerBotEmail()}"`);
|
|
837
850
|
exec(`git -C ${root} add -A`);
|
|
@@ -162,23 +162,10 @@ WantedBy=timers.target
|
|
|
162
162
|
// branding-file env line is written at all. Reading the neutral pack HERE is still correct: it is
|
|
163
163
|
// the prefix floor an instance inherits unless its own pack overrides envPrefix, and every live
|
|
164
164
|
// co-tenant pack pins `envPrefix: "CLOUDBONGOS"` explicitly.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
// reads <envPrefix>_<SUFFIX>, and src/branding.resolveBranding applies
|
|
170
|
-
// <envPrefix>_{PUBLIC,BUILDERS,…}_ORIGIN overrides — so a web.env var only takes
|
|
171
|
-
// effect under THIS prefix. Read from the pack (fail-soft to the vanilla default)
|
|
172
|
-
// so it stays in lockstep instead of a magic string. (The prior slug-based prefix
|
|
173
|
-
// was a latent bug: it never matched the neutral pack's CLOUDBONGOS envPrefix, so
|
|
174
|
-
// the OAuth creds were silently unreadable — task 1972.)
|
|
175
|
-
function provisionedEnvPrefix() {
|
|
176
|
-
const { FALLBACK_ENV_PREFIX } = require('../../src/instance-config');
|
|
177
|
-
try {
|
|
178
|
-
const pack = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, ENV_PREFIX_PACK), 'utf8'));
|
|
179
|
-
return pack.envPrefix || FALLBACK_ENV_PREFIX;
|
|
180
|
-
} catch { return FALLBACK_ENV_PREFIX; }
|
|
181
|
-
}
|
|
165
|
+
// ENV_PREFIX_PACK + provisionedEnvPrefix (and the task-1004573 pack checks) live in
|
|
166
|
+
// pack-env-prefix.js — provision-repo.js needs them too, and it cannot require this file
|
|
167
|
+
// (this file requires it). Re-exported below so every existing caller is unchanged.
|
|
168
|
+
const { ENV_PREFIX_PACK, provisionedEnvPrefix, envPrefixMismatch, normalizePackEnvPrefix } = require('./pack-env-prefix.js');
|
|
182
169
|
|
|
183
170
|
// The per-instance secrets file body (the standup runbook's /etc/<inst>/web.env).
|
|
184
171
|
// The OAuth client id/secret are GUIDED/manual inputs (no GitHub create-API) — the
|
|
@@ -430,6 +417,8 @@ function renamedEnvCarry(body) {
|
|
|
430
417
|
|
|
431
418
|
module.exports = {
|
|
432
419
|
ENV_PREFIX_PACK,
|
|
420
|
+
envPrefixMismatch,
|
|
421
|
+
normalizePackEnvPrefix,
|
|
433
422
|
backupScriptPath,
|
|
434
423
|
backupService,
|
|
435
424
|
backupServicePath,
|
package/scripts/gds/provision.js
CHANGED
|
@@ -50,7 +50,7 @@ const crypto = require('node:crypto');
|
|
|
50
50
|
const retry = require('./intent-retry.js'); // when a failed intent is tried again (task 1004447)
|
|
51
51
|
const { CONFIG, MANIFEST_UA, softFailResult, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
|
|
52
52
|
const { accountPreflight, appUser, alreadyScaffolded, buildCorePinRefreshCommit, coreCheckoutHasGit, dbCreateCmd, dbName, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd, ensureInstanceUserCmd, generateDbPassword, grantInstanceRepoReadCmd, instanceDbRole, instanceStateDir, instanceUser, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
53
|
-
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, renamedEnvCarry, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
53
|
+
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, envPrefixMismatch, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, renamedEnvCarry, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
54
54
|
const { caddyBlock, caddySnippetPath, checkCaddyWiring, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, ingressPlaneGap, ingressPreflight, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
55
55
|
const { resolveProcessManager } = require('./provision-process.js');
|
|
56
56
|
const { capacityPreflight, statusCapacityLine } = require('./provision-capacity.js');
|
|
@@ -1097,7 +1097,13 @@ async function placeManifestCreds(inst, mrow, deps) {
|
|
|
1097
1097
|
if (!authConfigured) {
|
|
1098
1098
|
// The App now EXISTS on the owner's account — keep its id + slug on the row so
|
|
1099
1099
|
// the wizard can name it and say why "try again" makes a second one (task 1004572).
|
|
1100
|
-
|
|
1100
|
+
// Say WHY when we can see it: an adopted repo's own pack may read other names (task 1004573).
|
|
1101
|
+
// A cloud-host checkout lives on the control plane (the scaffold reads it the same way), so
|
|
1102
|
+
// this is a plain file read — no shell. An unreadable or non-JSON pack names nothing.
|
|
1103
|
+
let packText = null;
|
|
1104
|
+
if (standalone) { try { packText = (deps.readFile || fs.readFileSync)(path.join(standaloneRoot(inst), 'config', 'branding.json'), 'utf8'); } catch { packText = null; } }
|
|
1105
|
+
const why = envPrefixMismatch(packText);
|
|
1106
|
+
return { ok: false, codeSpent: true, appId: creds.appId, appSlug: creds.appSlug, error: `the credentials are placed, but the project did not report sign-in ready in time${why ? ` — ${why}` : ''}` };
|
|
1101
1107
|
}
|
|
1102
1108
|
log(' [oauth-manifest] ✓ /healthz auth_configured=true — sign-in is live');
|
|
1103
1109
|
} else {
|
package/src/instance-config.js
CHANGED
|
@@ -190,7 +190,7 @@ function readConfigFileSync(name, { encoding = 'utf8', b = safeBrand() } = {}) {
|
|
|
190
190
|
// per process naming the old spelling, the new one, and the release the old
|
|
191
191
|
// stops working in; see warnLegacyEnv below. `warn` is injectable so a test can
|
|
192
192
|
// assert the warning fires rather than only that the value resolves.
|
|
193
|
-
function resolveEnv(suffix, { env = process.env, prefix = envPrefix(), legacy = LEGACY_ENV_PREFIXES, warn = warnLegacyEnv } = {}) {
|
|
193
|
+
function resolveEnv(suffix, { env = process.env, prefix = envPrefix(), legacy = LEGACY_ENV_PREFIXES, warn = warnLegacyEnv, warnPack = warnPackPrefix } = {}) {
|
|
194
194
|
const canonical = `${prefix}_${suffix}`;
|
|
195
195
|
const canonicalValue = prefix ? env[canonical] : undefined;
|
|
196
196
|
const canonicalSet = canonicalValue !== undefined && canonicalValue !== '';
|
|
@@ -200,6 +200,22 @@ function resolveEnv(suffix, { env = process.env, prefix = envPrefix(), legacy =
|
|
|
200
200
|
// on the winner would leave the "both set" operator with no notice at all.
|
|
201
201
|
const seen = new Set([prefix].filter(Boolean));
|
|
202
202
|
let winner = canonicalSet ? canonicalValue : undefined;
|
|
203
|
+
// THE PLATFORM'S SPELLING IS ALWAYS HEARD (task 1004573). The platform writes
|
|
204
|
+
// every hosted project's web.env as BONGOS_* whatever that project's own pack
|
|
205
|
+
// says. A project adopted from a repo whose config/branding.json still carried
|
|
206
|
+
// envPrefix CLOUDBONGOS read none of it — not its sign-in credentials, not its
|
|
207
|
+
// address, not one setting its owner chose — and nothing said so (mercury,
|
|
208
|
+
// 2026-10-04). So when the pack names another prefix, BONGOS_* is read right
|
|
209
|
+
// after it, ahead of the legacy list. The pack's own spelling still wins.
|
|
210
|
+
if (prefix && prefix !== FALLBACK_ENV_PREFIX) {
|
|
211
|
+
seen.add(FALLBACK_ENV_PREFIX);
|
|
212
|
+
const name = `${FALLBACK_ENV_PREFIX}_${suffix}`;
|
|
213
|
+
const v = env[name];
|
|
214
|
+
if (!canonicalSet && v !== undefined && v !== '') {
|
|
215
|
+
winner = v;
|
|
216
|
+
warnPack({ prefix, name });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
203
219
|
for (const pfx of legacy) {
|
|
204
220
|
if (!pfx || seen.has(pfx)) continue;
|
|
205
221
|
seen.add(pfx);
|
|
@@ -229,6 +245,17 @@ function warnLegacyEnv({ legacyName, canonicalName, shadowed, differs, log = con
|
|
|
229
245
|
return true;
|
|
230
246
|
}
|
|
231
247
|
|
|
248
|
+
// One line per process when a value had to come from the platform's BONGOS_*
|
|
249
|
+
// spelling because this project's pack names another prefix. Not a deprecation —
|
|
250
|
+
// BONGOS_ is the canonical name — so it says what to change instead: the pack.
|
|
251
|
+
let _packPrefixWarned = false;
|
|
252
|
+
function warnPackPrefix({ prefix, name, log = console } = {}) {
|
|
253
|
+
if (_packPrefixWarned) return false;
|
|
254
|
+
_packPrefixWarned = true;
|
|
255
|
+
log.warn(`[bongos] config/branding.json sets envPrefix "${prefix}", but ${name} (the platform's spelling) is what is set — reading it. Set envPrefix to "${FALLBACK_ENV_PREFIX}" to match.`);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
232
259
|
// NOT exported, deliberately: warnLegacyEnv, LEGACY_ENV_SUNSET_RELEASE and the
|
|
233
260
|
// ledger are internals. resolveEnv's injectable `warn` is the whole test seam a
|
|
234
261
|
// caller needs, and a test of the DEFAULT path captures console.warn — which
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.48'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|