@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.
- package/.bongos-core.json +36 -21
- package/config/branding.neutral.json +52 -12
- package/docs/copy-inventory.md +133 -136
- package/docs/copy-registry.json +164 -221
- package/modules/hall-ui/public/goals-page.js +8 -60
- package/modules/hall-ui/public/goals.html +2 -0
- package/modules/hall-ui/public/task-lib.js +136 -0
- package/modules/hall-ui/public/work.html +2 -0
- package/modules/hall-ui/public/work.js +8 -70
- package/modules/provisioning/provisioning.js +23 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision.js +2 -2
- package/src/branding.js +6 -0
- package/src/module-api.js +5 -1
- package/tests/goal_criterion_confirm_ui.mjs +6 -0
- package/tests/provisioning_settings_env.mjs +121 -0
- package/tests/task_lib.mjs +115 -0
|
@@ -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
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/scripts/gds/provision.js
CHANGED
|
@@ -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.
|
|
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);
|