@bongos/core 1.19.261 → 1.19.263

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.
@@ -23,6 +23,14 @@
23
23
  (function () {
24
24
  'use strict';
25
25
  const { escapeHtml, $, fmtNum, fmtDate, toast, rankLabel } = window.OTB;
26
+ // Shared task-domain helpers (task 1002463). These lived in two copies here and
27
+ // in the other board and had already drifted — goals-page.js had silently lost
28
+ // friendlyBatchReason's MODULE_CONFLICT case. Destructured so every call site
29
+ // below reads exactly as before; the single copy lives in task-lib.js.
30
+ const {
31
+ priorityRank, touchesConflict, rankTagHtml, deHashRefs,
32
+ creditsPerMinute, buildPromptForTask, friendlyBatchReason,
33
+ } = window.OTBTaskLib;
26
34
  // Shared component kit (hall-kit.js, task 2166): pager, filter bar, tabs,
27
35
  // empty/skeleton states — adopted here by task 2171.
28
36
  const { makePager, paginate, wirePager, pagerBarHtml, makeTabs, makeFilterBar,
@@ -497,40 +505,10 @@
497
505
  // ===================================================================
498
506
 
499
507
  // Board-identical numeric helpers (work.js:214-240).
500
- function creditsPerMinute(t) {
501
- const c = Number(t.credits_reward); const m = Number(t.est_minutes);
502
- if (!c || !m || m <= 0) return -Infinity;
503
- return c / m;
504
- }
505
- function priorityRank(p) {
506
- if (!p) return 99;
507
- const m = /^P(\d+)/i.exec(String(p));
508
- return m ? Number(m[1]) : 99;
509
- }
510
508
  // Two tasks can't run in parallel if their touches[] overlap (exact path or a
511
509
  // directory-prefix either way); empty touches never conflict. Powers the
512
510
  // filter bar's "Run mode" control (work.js:230-239).
513
- function touchesConflict(aList, bList) {
514
- const norm = (p) => String(p).replace(/\/+$/, '');
515
- for (const a of (Array.isArray(aList) ? aList : [])) {
516
- const na = norm(a);
517
- for (const b of (Array.isArray(bList) ? bList : [])) {
518
- const nb = norm(b);
519
- if (na === nb || na.startsWith(nb + '/') || nb.startsWith(na + '/')) return true;
520
- }
521
- }
522
- return false;
523
- }
524
511
  // Per-task rank floor tag (work.js:247-254).
525
- function rankTagHtml(requiresRank) {
526
- const rr = (requiresRank || 'xenos').toLowerCase();
527
- if (rr === 'xenos') {
528
- return '<span class="task__tag task__tag--rank-open" title="No rank floor — any builder can claim this task">Any rank</span>';
529
- }
530
- const label = rr === 'archon' ? 'Archon' : rr.charAt(0).toUpperCase() + rr.slice(1) + '+';
531
- return `<span class="task__tag task__tag--rank" title="Minimum rank required to claim this task">${escapeHtml(label)}</span>`;
532
- }
533
-
534
512
  // Free-text search over the goal's todo list (work.js:261-274, minus the
535
513
  // goal-title match — a single goal makes that redundant).
536
514
  function todoSearchQuery() {
@@ -548,21 +526,6 @@
548
526
 
549
527
  // Copy-prompt (work.js:454-503). deHashRefs stops a bare #NNN autolinking to
550
528
  // GitHub in a pasted session; the prompt points the builder at the hall task URL.
551
- function deHashRefs(text) {
552
- return String(text == null ? '' : text).replace(/#(\d{1,5})\b/g, 'task $1');
553
- }
554
- function buildPromptForTask(t) {
555
- const desc = deHashRefs((t.description || '').trim());
556
- const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
557
- const hallUrl = `${hallBase}#/task/${t.id}`;
558
- return [
559
- `I am working on GDS task ${t.id} (${t.title}) — ${hallUrl}`,
560
- `Read the task description, follow its acceptance criteria, stay strictly within touches[], and ship via /builder-ship when done.`,
561
- ``,
562
- `Task description:`,
563
- desc || '(no description)',
564
- ].join('\n');
565
- }
566
529
  async function copyPromptForTask(taskId, btn) {
567
530
  const t = state.work.todo.find((x) => String(x.id) === String(taskId));
568
531
  if (!t) return;
@@ -697,21 +660,6 @@
697
660
  }
698
661
  }
699
662
  // A single batch-item failure reason → a readable fragment (work.js:545-562).
700
- function friendlyBatchReason(item) {
701
- switch (item && item.reason) {
702
- case 'TASK_NOT_FOUND': return 'no longer exists';
703
- case 'TASK_NOT_READY': return `no longer ready${item.currentStatus ? ` (${item.currentStatus})` : ''}`;
704
- case 'ALREADY_CLAIMED': return 'already claimed';
705
- case 'TOUCHES_CONFLICT': return `overlaps ${item.conflictTaskTitle || 'an active claim'}`;
706
- case 'BATCH_TOUCHES_CONFLICT': return `overlaps task #${item.conflictsWithTaskId} in this selection`;
707
- case 'DEPS_NOT_SHIPPED': return 'waits on unshipped dependencies';
708
- case 'rank_too_low_for_task':
709
- case 'XENOS_RESTRICTED':
710
- case 'INSUFFICIENT_RANK': return 'rank too low';
711
- case 'BUILDER_INACTIVE': return 'account inactive';
712
- default: return (item && item.reason) || 'not claimable';
713
- }
714
- }
715
663
  // Bulk claim the selected ready cards (work.js doBulkClaim, claims/batch is
716
664
  // all-or-nothing: a 409 batch_failed means nothing was claimed). Reloads the
717
665
  // goal's tasks either way so the cards reflect fresh truth.
@@ -194,6 +194,8 @@
194
194
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
195
195
  <!-- shared component kit (window.OTBKit) — pager, filter bar, tabs, empty/skeleton (task 2166). -->
196
196
  <script src="/builders/hall-kit.js"></script>
197
+ <!-- shared TASK-DOMAIN helpers (window.OTBTaskLib) — priority/touches/prompt/refusal-copy, one copy for both boards (task 1002463). -->
198
+ <script src="/builders/task-lib.js"></script>
197
199
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
198
200
  <script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
199
201
  <script src="/builders/goals-page.js?v=2026-07-10-goals5"></script>
@@ -0,0 +1,136 @@
1
+ // modules/hall-ui/public/task-lib.js — the hall's shared TASK-DOMAIN helpers
2
+ // (task 1002463, from idea 1000567).
3
+ //
4
+ // WHY A SIBLING OF hall-kit.js RATHER THAN PART OF IT. hall-kit.js is explicitly
5
+ // "the SIX primitives every page redesign adopts" — pager, ledger, filter bar,
6
+ // tabs, section rail, empty/skeleton/collapsible. Those are UI *markup* factories,
7
+ // and modules/hall-ui/CLAUDE.md's "no bespoke pager / filter / tab / rail markup"
8
+ // rule is scoped to exactly that. The functions below are task-DOMAIN logic
9
+ // (how a task is ranked, whether two touches[] collide, what a refusal means in
10
+ // English). Folding them into the kit would blur a contract that is currently
11
+ // crisp, so they get their own namespace instead.
12
+ //
13
+ // Contract, mirrored from hall-kit.js so load order only needs "before the page
14
+ // script": dependency-free (own esc(), no window.OTB use), no eval-time DOM, and
15
+ // everything hangs off one global. Node-require-safe so it can be unit-tested.
16
+ //
17
+ // WHAT THIS FIXES. Before extraction these lived in TWO copies (work.js and
18
+ // goals-page.js) and had already drifted: goals-page.js's friendlyBatchReason had
19
+ // silently lost the MODULE_CONFLICT case that work.js handles, so a batch-claim
20
+ // refused for a module collision showed the raw code "MODULE_CONFLICT" to the
21
+ // builder on the Goals page and a sentence on the Tasks board. One copy, one
22
+ // behaviour, and the drift cannot re-open.
23
+ (() => {
24
+ 'use strict';
25
+
26
+ // Local HTML-escape — deliberately NOT window.OTB.escapeHtml, so this file
27
+ // keeps hall-kit's dependency-free property (same reasoning, same duplication
28
+ // of ~4 lines being cheaper than a load-order constraint).
29
+ function esc(s) {
30
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({
31
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
32
+ }[c]));
33
+ }
34
+
35
+ // Sort key for a "P3"-style priority string. Unparseable/absent sorts last.
36
+ function priorityRank(p) {
37
+ if (!p) return 99;
38
+ const m = /^P(\d+)/i.exec(String(p));
39
+ return m ? Number(m[1]) : 99;
40
+ }
41
+
42
+ // Do two touches[] lists collide? A path collides with itself and with anything
43
+ // under it, so "src/" conflicts with "src/gds/db.js". Trailing slashes are
44
+ // normalised first so "src" and "src/" are the same path.
45
+ function touchesConflict(aList, bList) {
46
+ const norm = (p) => String(p).replace(/\/+$/, '');
47
+ for (const a of (Array.isArray(aList) ? aList : [])) {
48
+ const na = norm(a);
49
+ for (const b of (Array.isArray(bList) ? bList : [])) {
50
+ const nb = norm(b);
51
+ if (na === nb || na.startsWith(nb + '/') || nb.startsWith(na + '/')) return true;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
57
+ // The rank-floor chip. 'xenos' means no floor at all, which reads as an
58
+ // invitation ("Any rank") rather than a restriction.
59
+ function rankTagHtml(requiresRank) {
60
+ const rr = (requiresRank || 'xenos').toLowerCase();
61
+ if (rr === 'xenos') {
62
+ return '<span class="task__tag task__tag--rank-open" title="No rank floor — any builder can claim this task">Any rank</span>';
63
+ }
64
+ const label = rr === 'archon' ? 'Archon' : rr.charAt(0).toUpperCase() + rr.slice(1) + '+';
65
+ return `<span class="task__tag task__tag--rank" title="Minimum rank required to claim this task">${esc(label)}</span>`;
66
+ }
67
+
68
+ // Strip bare "#NNN" GDS refs from prose we are about to paste elsewhere: on
69
+ // GitHub a bare #NNN autolinks to an ISSUE, and our ids are not GitHub's, so
70
+ // the link 404s. (Same rule the repo enforces in committed markdown.)
71
+ // VERBATIM from the two originals — do not "tidy" either detail:
72
+ // • `\d{1,5}` (not `\d+`) is load-bearing by omission: a 6+ digit run never
73
+ // matches at all, so long numerics are left untouched rather than rewritten.
74
+ // • `text == null ? '' : text` (not `text || ''`) keeps falsy-but-real inputs:
75
+ // deHashRefs(0) must render "0", not "". `|| ''` would swallow it.
76
+ // Both were silently changed in an earlier draft of this extraction and the
77
+ // regression escaped an equivalence pass whose inputs were all strings/null.
78
+ function deHashRefs(text) {
79
+ return String(text == null ? '' : text).replace(/#(\d{1,5})\b/g, 'task $1');
80
+ }
81
+
82
+ // Reward density, for the "best value" sort. -Infinity (not 0) for anything
83
+ // unpriced or unestimated so those sink below every real ratio instead of
84
+ // tying with a genuinely zero-value task.
85
+ function creditsPerMinute(t) {
86
+ const c = Number(t.credits_reward);
87
+ const m = Number(t.est_minutes);
88
+ if (!c || !m || m <= 0) return -Infinity;
89
+ return c / m;
90
+ }
91
+
92
+ // The paste-into-your-agent prompt for one task.
93
+ function buildPromptForTask(t) {
94
+ const desc = deHashRefs((t.description || '').trim());
95
+ const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
96
+ const hallUrl = `${hallBase}#/task/${t.id}`;
97
+ return [
98
+ `I am working on GDS task ${t.id} (${t.title}) — ${hallUrl}`,
99
+ `Read the task description, follow its acceptance criteria, stay strictly within touches[], and ship via /builder-ship when done.`,
100
+ ``,
101
+ `Task description:`,
102
+ desc || '(no description)',
103
+ ].join('\n');
104
+ }
105
+
106
+ // Turn a batch-claim refusal into a sentence. THE DEFAULT MATTERS: an unmapped
107
+ // code falls through to the raw string, which is how the missing MODULE_CONFLICT
108
+ // case leaked a machine code into the Goals page UI. Adding a server-side reason
109
+ // without adding it here degrades quietly rather than loudly, so keep this in
110
+ // step with the claim route's refusal codes.
111
+ function friendlyBatchReason(item) {
112
+ switch (item && item.reason) {
113
+ case 'TASK_NOT_FOUND': return 'no longer exists';
114
+ case 'TASK_NOT_READY': return `no longer ready${item.currentStatus ? ` (${item.currentStatus})` : ''}`;
115
+ case 'ALREADY_CLAIMED': return 'already claimed';
116
+ case 'TOUCHES_CONFLICT': return `overlaps ${item.conflictTaskTitle || 'an active claim'}`;
117
+ case 'BATCH_TOUCHES_CONFLICT': return `overlaps task #${item.conflictsWithTaskId} in this selection`;
118
+ case 'MODULE_CONFLICT': return item.conflictsWithTaskId
119
+ ? `shares module "${item.module_key}" with task #${item.conflictsWithTaskId} in this selection`
120
+ : `shares module "${item.module_key}" with an active claim`;
121
+ case 'DEPS_NOT_SHIPPED': return 'waits on unshipped dependencies';
122
+ case 'rank_too_low_for_task':
123
+ case 'XENOS_RESTRICTED':
124
+ case 'INSUFFICIENT_RANK': return 'rank too low';
125
+ case 'BUILDER_INACTIVE': return 'account inactive';
126
+ default: return (item && item.reason) || 'not claimable';
127
+ }
128
+ }
129
+
130
+ const api = {
131
+ priorityRank, touchesConflict, rankTagHtml, deHashRefs,
132
+ creditsPerMinute, buildPromptForTask, friendlyBatchReason,
133
+ };
134
+ if (typeof window !== 'undefined') window.OTBTaskLib = api;
135
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
136
+ })();
@@ -135,6 +135,8 @@
135
135
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils" defer></script>
136
136
  <!-- shared component kit (window.OTBKit) — tabs, pager, filter bar (task 2166). -->
137
137
  <script src="/builders/hall-kit.js" defer></script>
138
+ <!-- shared TASK-DOMAIN helpers (window.OTBTaskLib) — priority/touches/prompt/refusal-copy, one copy for both boards (task 1002463). -->
139
+ <script src="/builders/task-lib.js" defer></script>
138
140
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail" defer></script>
139
141
  <script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
140
142
  <script src="/builders/work.js?v=2026-07-03-workboard3" defer></script>
@@ -36,6 +36,14 @@
36
36
  // file's exact variants (date-only fmtDate; a 2200ms clearing toast).
37
37
  // fmtAgo / toAuth / getJSON / postJSON stay local below.
38
38
  const { escapeHtml, $, fmtNum } = window.OTB;
39
+ // Shared task-domain helpers (task 1002463). These lived in two copies here and
40
+ // in the other board and had already drifted — goals-page.js had silently lost
41
+ // friendlyBatchReason's MODULE_CONFLICT case. Destructured so every call site
42
+ // below reads exactly as before; the single copy lives in task-lib.js.
43
+ const {
44
+ priorityRank, touchesConflict, rankTagHtml,
45
+ creditsPerMinute, buildPromptForTask, friendlyBatchReason,
46
+ } = window.OTBTaskLib; // deHashRefs is not called here — it is used inside buildPromptForTask
39
47
  // Shared component kit (hall-kit.js, task 2166): tabs, pager, filter bar.
40
48
  const { makePager, paginate, wirePager, pagerBarHtml, makeTabs, makeFilterBar } = window.OTBKit;
41
49
  const fmtDate = (iso) => window.OTB.fmtDate(iso);
@@ -211,48 +219,14 @@
211
219
  const sequentialCache = { key: '', order: null };
212
220
  let sequentialDebounce = null;
213
221
 
214
- function creditsPerMinute(t) {
215
- const c = Number(t.credits_reward);
216
- const m = Number(t.est_minutes);
217
- if (!c || !m || m <= 0) return -Infinity;
218
- return c / m;
219
- }
220
-
221
- function priorityRank(p) {
222
- if (!p) return 99;
223
- const m = /^P(\d+)/i.exec(String(p));
224
- return m ? Number(m[1]) : 99;
225
- }
226
-
227
222
  // task 1756: two tasks can't run in parallel if their touches[] overlap — exact
228
223
  // path match or a directory-prefix either way (mirrors the server's touchesOverlap
229
224
  // / the goals-page conflict estimate). Empty touches never conflict.
230
- function touchesConflict(aList, bList) {
231
- const norm = (p) => String(p).replace(/\/+$/, '');
232
- for (const a of (Array.isArray(aList) ? aList : [])) {
233
- const na = norm(a);
234
- for (const b of (Array.isArray(bList) ? bList : [])) {
235
- const nb = norm(b);
236
- if (na === nb || na.startsWith(nb + '/') || nb.startsWith(na + '/')) return true;
237
- }
238
- }
239
- return false;
240
- }
241
-
242
225
  // task 1752 (+ 1498 / ADR 0084): the minimum rank to claim a task, as a tag shown
243
226
  // on EVERY task row. 'xenos' is the open default → a muted "Any rank" tag (open to
244
227
  // all, not a warning); 'archon' shows bare; other ranks show 'Rank+'. Kept here for
245
228
  // now; the shared task-detail view (task 1753) will lift this into the common
246
229
  // toolkit so every task surface renders rank identically.
247
- function rankTagHtml(requiresRank) {
248
- const rr = (requiresRank || 'xenos').toLowerCase();
249
- if (rr === 'xenos') {
250
- return '<span class="task__tag task__tag--rank-open" title="No rank floor — any builder can claim this task">Any rank</span>';
251
- }
252
- const label = rr === 'archon' ? 'Archon' : rr.charAt(0).toUpperCase() + rr.slice(1) + '+';
253
- return `<span class="task__tag task__tag--rank" title="Minimum rank required to claim this task">${escapeHtml(label)}</span>`;
254
- }
255
-
256
230
  // task 1829: free-text search over the claimable/backlog lists. Matches the
257
231
  // query (case-insensitive substring) against the task id (with or without a
258
232
  // leading '#'), the title, and the goal title (taskState.goalTitles — the
@@ -455,23 +429,6 @@
455
429
  // our records number tasks separately from GitHub. So the scroll we hand the
456
430
  // builder must never carry a bare "#NNN": write "task NNN" + the clickable hall
457
431
  // URL, and strip "#NNN" out of the (free-text) task description too.
458
- function deHashRefs(text) {
459
- return String(text == null ? '' : text).replace(/#(\d{1,5})\b/g, 'task $1');
460
- }
461
-
462
- function buildPromptForTask(t) {
463
- const desc = deHashRefs((t.description || '').trim());
464
- const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
465
- const hallUrl = `${hallBase}#/task/${t.id}`;
466
- return [
467
- `I am working on GDS task ${t.id} (${t.title}) — ${hallUrl}`,
468
- `Read the task description, follow its acceptance criteria, stay strictly within touches[], and ship via /builder-ship when done.`,
469
- ``,
470
- `Task description:`,
471
- desc || '(no description)',
472
- ].join('\n');
473
- }
474
-
475
432
  async function copyPromptForTask(taskId, btn) {
476
433
  const t = taskState.all.find((x) => String(x.id) === String(taskId));
477
434
  if (!t) return;
@@ -546,25 +503,6 @@
546
503
  // /claims/batch/validate) to a builder-readable fragment. Distinct from
547
504
  // friendlyActionError above because a batch item's shape differs (reason,
548
505
  // not error; extra fields like conflictsWithTaskId/module_key).
549
- function friendlyBatchReason(item) {
550
- switch (item && item.reason) {
551
- case 'TASK_NOT_FOUND': return 'no longer exists';
552
- case 'TASK_NOT_READY': return `no longer ready${item.currentStatus ? ` (${item.currentStatus})` : ''}`;
553
- case 'ALREADY_CLAIMED': return 'already claimed';
554
- case 'TOUCHES_CONFLICT': return `overlaps ${item.conflictTaskTitle || 'an active claim'}`;
555
- case 'BATCH_TOUCHES_CONFLICT': return `overlaps task #${item.conflictsWithTaskId} in this selection`;
556
- case 'MODULE_CONFLICT': return item.conflictsWithTaskId
557
- ? `shares module "${item.module_key}" with task #${item.conflictsWithTaskId} in this selection`
558
- : `shares module "${item.module_key}" with an active claim`;
559
- case 'DEPS_NOT_SHIPPED': return 'waits on unshipped dependencies';
560
- case 'rank_too_low_for_task':
561
- case 'XENOS_RESTRICTED':
562
- case 'INSUFFICIENT_RANK': return 'rank too low';
563
- case 'BUILDER_INACTIVE': return 'account inactive';
564
- default: return (item && item.reason) || 'not claimable';
565
- }
566
- }
567
-
568
506
  async function copyText(text) {
569
507
  try { await navigator.clipboard.writeText(text); return true; }
570
508
  catch (_) {
@@ -235,6 +235,28 @@ function publicInstance(row) {
235
235
  // The instance's EFFECTIVE settings: the defaults overlaid with the stored,
236
236
  // on-vocabulary keys. An off-vocabulary or unknown stored value falls back to
237
237
  // its default rather than leaking into the read surface. PURE.
238
+ // The web.env lines that carry this instance's settings, as the runner writes them
239
+ // at standup (task 1003139). PURE (row + prefix in, string out) and owned HERE, next
240
+ // to SETTINGS_VOCAB, rather than in provision.js — the vocabulary and the thing that
241
+ // transmits it must not be able to drift. Values come from effectiveSettings, so a
242
+ // row with no stored settings emits today's behavior EXPLICITLY rather than nothing:
243
+ // an instance must never have to infer a policy from an absent variable.
244
+ //
245
+ // This is the standup leg only. Changing a setting on an ALREADY-RUNNING instance
246
+ // needs a push (re-write web.env + restart, since the branding pack is memoized for
247
+ // the process lifetime) — that is the 'settings-apply' intent, still to be built.
248
+ function settingsEnvLines(row, prefix) {
249
+ const s = effectiveSettings(row);
250
+ return `# Per-project policy the platform decides and this instance obeys (task
251
+ # 1003139). Written at standup from the platform's provisioning_instances.settings;
252
+ # src/branding.js ENV_OVERRIDES maps these onto project.*. Changing one on a RUNNING
253
+ # instance still needs a push (re-write + restart) — not built yet, so today a change
254
+ # takes effect on the next provision run.
255
+ ${prefix}_PLATFORM_VISIBILITY=${s.platform_visibility}
256
+ ${prefix}_JOINABILITY=${s.joinability}
257
+ `;
258
+ }
259
+
238
260
  function effectiveSettings(row) {
239
261
  const stored = row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings)
240
262
  ? row.settings : {};
@@ -783,6 +805,7 @@ async function costLedger(db, { limit = 500 } = {}) {
783
805
  module.exports = {
784
806
  // constants
785
807
  INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, PROJECT_TYPES, INTENT_ACTIONS, TEARDOWN_FROM,
808
+ settingsEnvLines,
786
809
  PROVISION_ENQUEUE_FROM, DOMAIN_ATTACH_FROM, TIER_MONTHLY_USD, CERT_ELIGIBLE_STATES,
787
810
  SETTINGS_VOCAB, DEFAULT_SETTINGS,
788
811
  // pure
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.261",
3
+ "version": "1.19.263",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.261",
9
+ "version": "1.19.263",
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.19.261",
3
+ "version": "1.19.263",
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",
@@ -1157,7 +1157,7 @@ function provisionedEnvPrefix() {
1157
1157
  // stays at neutral localhost and the hall never loads (task 1972). No domain yet
1158
1158
  // (bring-your-own deferred, ADR 0111 §4) ⇒ origins stay neutral until one is set.
1159
1159
  function webEnvBody(inst, { clientId = '', clientSecret = '', appId = null, appPem = null, federation = null } = {}) {
1160
- const prefix = provisionedEnvPrefix();
1160
+ const prefix = provisionedEnvPrefix(), { settingsEnvLines } = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning')); // eslint-disable-line global-require
1161
1161
  const origin = inst.domain ? `https://${inst.domain}` : '';
1162
1162
  const originLines = origin
1163
1163
  ? `# Single-host origins (task 1972): the hall serves on THIS host + OAuth/redirects
@@ -1234,7 +1234,7 @@ ${prefix}_GITHUB_CLIENT_SECRET=${clientSecret || ''}
1234
1234
  ${firstAdminLine}GDS_BRANDING_FILE=${PROVISIONED_BRANDING_FILE}
1235
1235
  ${originLines}${appLines}PORT=${inst.port}
1236
1236
  PGDATABASE=${dbName(inst)}
1237
- ${fedLines}`;
1237
+ ${fedLines}${settingsEnvLines(inst, prefix)}`;
1238
1238
  }
1239
1239
  function webEnvPath(inst, { etcBase = CONFIG.etcBase } = {}) {
1240
1240
  return path.posix.join(etcBase, inst.slug, 'web.env');
package/src/branding.js CHANGED
@@ -54,6 +54,12 @@ const ENV_OVERRIDES = [
54
54
  // front door with one env var and no instance branding.json — cloudbongos.com
55
55
  // sets <PREFIX>_POST_LOGIN_LANDING=/gate/ (the orbs). Task 1002240.
56
56
  ['POST_LOGIN_LANDING', ['landing', 'postLogin']],
57
+ // Per-project policy pushed by the provisioning runner (task 1003139). The pack
58
+ // ships today's behavior, so an instance that never receives a push is unchanged.
59
+ // Read through branding, which is memoized + deep-frozen for the process lifetime
60
+ // — which is exactly why applying a change RESTARTS the instance.
61
+ ['PLATFORM_VISIBILITY', ['project', 'platformVisibility']],
62
+ ['JOINABILITY', ['project', 'joinability']],
57
63
  ];
58
64
 
59
65
  const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
package/src/module-api.js CHANGED
@@ -949,8 +949,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
949
949
  // landed since 1.19.259 with no explicit bump. run 32289043893. (task 1002620)
950
950
  // 1.19.261 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
951
951
  // landed since 1.19.260 with no explicit bump. run 32295699354. (task 1002620)
952
+ // 1.19.262 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
953
+ // landed since 1.19.261 with no explicit bump. run 32298063660. (task 1002620)
954
+ // 1.19.263 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
955
+ // landed since 1.19.262 with no explicit bump. run 32305228786. (task 1002620)
952
956
  // ---------------------------------------------------------------------------
953
- const CORE_VERSION = '1.19.261'; // CI auto-patch carrier (ADR 0161); see changelog above
957
+ const CORE_VERSION = '1.19.263'; // CI auto-patch carrier (ADR 0161); see changelog above
954
958
 
955
959
  // A namespaced logger so a module's log lines are attributable + consistent.
956
960
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -32,6 +32,11 @@ import { makeRunner } from './helpers.mjs';
32
32
 
33
33
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
34
  const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.js'), 'utf8');
35
+ // task 1002463: goals-page.js now pulls its task-domain helpers from the shared
36
+ // window.OTBTaskLib (task-lib.js) instead of holding its own drifted copies. Run
37
+ // the REAL file in the same context rather than stubbing it — stubbing would let
38
+ // the extraction rot without this, the only test that executes either board.
39
+ const TASK_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/task-lib.js'), 'utf8');
35
40
 
36
41
  const { test, summary } = makeRunner();
37
42
 
@@ -126,6 +131,7 @@ async function render({ rank = 'metic', goalStatus = 'open', criteria = Object.v
126
131
  const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, console, setTimeout, clearTimeout, Set, Map, Number, String, Array, Object, JSON, Math, encodeURIComponent, Promise };
127
132
  sandbox.globalThis = sandbox;
128
133
  vm.createContext(sandbox);
134
+ vm.runInContext(TASK_LIB_SRC, sandbox, { filename: 'task-lib.js' });
129
135
  vm.runInContext(SRC, sandbox, { filename: 'goals-page.js' });
130
136
 
131
137
  // Let boot()'s promise chain settle (me → goals → details → route → render).
@@ -0,0 +1,121 @@
1
+ // tests/provisioning_settings_env.mjs
2
+ //
3
+ // The standup leg of the platform->instance settings channel (task 1003139, goal
4
+ // 1000058): a per-project setting the PLATFORM stores must reach the INSTANCE that
5
+ // has to obey it.
6
+ //
7
+ // The gap this closes: PATCH /provisioning/instances/:id/settings wrote
8
+ // platform_visibility / joinability to the platform's jsonb and pushed nothing, and a
9
+ // standalone instance never federates — it holds no hub credential and has no
10
+ // authenticated path back to the platform to pull one. The settings were write-only
11
+ // state. The transport that DOES exist is /etc/<slug>/web.env, read through the
12
+ // branding pack, so the runner now writes them there at standup and src/branding.js
13
+ // resolves them onto project.*.
14
+ //
15
+ // NOT covered here because it is NOT built: pushing a CHANGED setting to an
16
+ // already-running instance (the branding pack is memoized + deep-frozen for the
17
+ // process lifetime, so that needs a re-write + restart — the deferred
18
+ // 'settings-apply' intent). These tests assert the standup leg only, and one of them
19
+ // pins that the generated file does not CLAIM the push exists.
20
+ //
21
+ // DB-free and hermetic: pure builders + a child process with env set. No network.
22
+ //
23
+ // Run: node tests/provisioning_settings_env.mjs
24
+
25
+ import { strict as assert } from 'node:assert';
26
+ import { createRequire } from 'node:module';
27
+ import { execFileSync } from 'node:child_process';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
33
+ const provisioning = require('../modules/provisioning/provisioning.js');
34
+
35
+ let passed = 0, failed = 0;
36
+ function t(name, fn) {
37
+ try { fn(); passed++; console.log(` PASS ${name}`); }
38
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
39
+ }
40
+
41
+ const linesOf = (s) => s.split('\n').filter((l) => l && !l.startsWith('#'));
42
+ const varsOf = (s) => Object.fromEntries(linesOf(s).map((l) => l.split('=')));
43
+
44
+ console.log('settingsEnvLines — the platform\'s stored settings, as the instance reads them:');
45
+
46
+ t('a stored setting is carried verbatim under the pack\'s env prefix', () => {
47
+ const v = varsOf(provisioning.settingsEnvLines(
48
+ { settings: { platform_visibility: 'gated', joinability: 'invite_only' } }, 'CLOUDBONGOS'));
49
+ assert.equal(v.CLOUDBONGOS_PLATFORM_VISIBILITY, 'gated');
50
+ assert.equal(v.CLOUDBONGOS_JOINABILITY, 'invite_only');
51
+ });
52
+
53
+ t('a row with NO stored settings emits today\'s behavior EXPLICITLY, never nothing', () => {
54
+ const v = varsOf(provisioning.settingsEnvLines({}, 'CLOUDBONGOS'));
55
+ // an instance must never have to infer a policy from an absent variable
56
+ assert.equal(v.CLOUDBONGOS_PLATFORM_VISIBILITY, 'public');
57
+ assert.equal(v.CLOUDBONGOS_JOINABILITY, 'apply');
58
+ assert.equal(linesOf(provisioning.settingsEnvLines({}, 'X')).length, 2, 'both keys always present');
59
+ });
60
+
61
+ t('an off-vocabulary stored value falls back to the default, never reaches the instance', () => {
62
+ const v = varsOf(provisioning.settingsEnvLines(
63
+ { settings: { platform_visibility: 'sort-of-public', joinability: 'whenever' } }, 'CLOUDBONGOS'));
64
+ assert.equal(v.CLOUDBONGOS_PLATFORM_VISIBILITY, 'public');
65
+ assert.equal(v.CLOUDBONGOS_JOINABILITY, 'apply');
66
+ });
67
+
68
+ t('the prefix is the pack\'s, not a guess — a wrong prefix silently never fires', () => {
69
+ const v = varsOf(provisioning.settingsEnvLines({}, 'MERCURY'));
70
+ assert.ok('MERCURY_PLATFORM_VISIBILITY' in v);
71
+ assert.ok(!('CLOUDBONGOS_PLATFORM_VISIBILITY' in v));
72
+ });
73
+
74
+ t('every emitted key is in SETTINGS_VOCAB — the two lists cannot drift', () => {
75
+ const keys = linesOf(provisioning.settingsEnvLines({}, 'X')).map((l) => l.split('=')[0].replace(/^X_/, '').toLowerCase());
76
+ assert.deepEqual(keys.sort(), Object.keys(provisioning.SETTINGS_VOCAB).sort(),
77
+ 'a key added to the vocabulary must be carried, or the instance never learns it');
78
+ });
79
+
80
+ console.log('\nThe generated file tells the truth about what it is:');
81
+
82
+ t('it does NOT claim a live push exists — that intent is not built', () => {
83
+ const s = provisioning.settingsEnvLines({}, 'X');
84
+ assert.doesNotMatch(s, /settings-apply/,
85
+ 'no comment may name an intent action the runner cannot dispatch');
86
+ assert.match(s, /next provision run/i, 'it says when a change actually takes effect');
87
+ });
88
+
89
+ console.log('\nwebEnvBody — the settings ride the real per-instance env file:');
90
+
91
+ t('the runner writes them into web.env alongside PORT and PGDATABASE', () => {
92
+ const provision = require('../scripts/gds/provision.js');
93
+ const body = provision.webEnvBody(
94
+ { slug: 'spike-harbor', port: 3010, db_name: 'spike-harbor', target_ref: 'jaxri/spike-harbor',
95
+ hosting_shape: 'standalone', settings: { platform_visibility: 'gated' } }, {});
96
+ assert.match(body, /_PLATFORM_VISIBILITY=gated/);
97
+ assert.match(body, /_JOINABILITY=apply/);
98
+ assert.match(body, /PORT=3010/, 'the rest of the file is untouched');
99
+ });
100
+
101
+ console.log('\nsrc/branding.js — the value actually reaches code:');
102
+
103
+ t('the neutral pack ships today\'s behavior, so an un-pushed instance is unchanged', () => {
104
+ const pack = require('../config/branding.neutral.json');
105
+ assert.equal(pack.project.platformVisibility, 'public');
106
+ assert.equal(pack.project.joinability, 'apply');
107
+ });
108
+
109
+ t('the env override resolves onto project.* in a real process', () => {
110
+ const read = (env) => JSON.parse(execFileSync(process.execPath, ['-e',
111
+ "const b=require('./src/branding.js');const r=(b.branding||b)();console.log(JSON.stringify({v:r.project.platformVisibility,j:r.project.joinability}))"],
112
+ { cwd: ROOT, encoding: 'utf8', env: { ...process.env, ...env } }));
113
+ assert.deepEqual(read({}), { v: 'public', j: 'apply' }, 'no env → the pack default');
114
+ assert.deepEqual(
115
+ read({ CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'invite_only' }),
116
+ { v: 'gated', j: 'invite_only' },
117
+ 'the web.env the runner writes is what the instance reads');
118
+ });
119
+
120
+ console.log(`\nprovisioning_settings_env: ${passed} passed, ${failed} failed`);
121
+ if (failed) process.exit(1);