@bongos/core 1.19.262 → 1.19.264
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 +33 -18
- 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/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship.js +41 -7
- package/src/module-api.js +5 -1
- package/tests/goal_criterion_confirm_ui.mjs +6 -0
- package/tests/ship_error_shape.mjs +153 -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 (_) {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.264",
|
|
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.264",
|
|
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.264",
|
|
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/ship.js
CHANGED
|
@@ -729,7 +729,7 @@ async function uploadSessionOnShip(taskId) {
|
|
|
729
729
|
recordUploadSkip(taskId, {
|
|
730
730
|
code: `http_${r.status}`,
|
|
731
731
|
reason:
|
|
732
|
-
`POST /api/gds/sessions returned ${r.status}${r.data && r.data.error ? ` (${r.data
|
|
732
|
+
`POST /api/gds/sessions returned ${r.status}${r.data && r.data.error ? ` (${apiErrorLine(r.data)})` : ''} — ` +
|
|
733
733
|
'the API rejected or could not store the digest. This ship will NOT appear in the BFG session corpus.',
|
|
734
734
|
});
|
|
735
735
|
}
|
|
@@ -1268,7 +1268,7 @@ async function ciLandServer(taskId, branch, valueSummary) {
|
|
|
1268
1268
|
id: taskId,
|
|
1269
1269
|
body: { branch, head_sha: headSha, value_summary: valueSummary, bundle_b64: bundleB64 },
|
|
1270
1270
|
});
|
|
1271
|
-
if (r.status === 403 && r.data
|
|
1271
|
+
if (r.status === 403 && normalizeApiError(r.data).code === 'box_scope') {
|
|
1272
1272
|
console.log(' ci-land: this session is BOX-SCOPED — run /builder-reauth (browser, no terminal) to mint a real session, then re-ship. Task stays at confirmed.');
|
|
1273
1273
|
return landBailed('this CLI session is box-scoped', '/builder-reauth');
|
|
1274
1274
|
}
|
|
@@ -3160,9 +3160,39 @@ function normalizeApiError(data) {
|
|
|
3160
3160
|
return {
|
|
3161
3161
|
code: isEnvelope ? errRaw.code : errRaw,
|
|
3162
3162
|
message: isEnvelope ? errRaw.message : undefined,
|
|
3163
|
+
// task 1002789: `details` is where the envelope carries the structured
|
|
3164
|
+
// payload the flat shape used to put at the TOP level (claimWorktree,
|
|
3165
|
+
// step/reason, floors). A site that reads `data.<field>` directly is
|
|
3166
|
+
// reading a field that moved. Fall back to the body so a legacy flat
|
|
3167
|
+
// response still resolves.
|
|
3168
|
+
details: (isEnvelope && errRaw.details && typeof errRaw.details === 'object')
|
|
3169
|
+
? errRaw.details
|
|
3170
|
+
: (data && typeof data === 'object' ? data : {}),
|
|
3163
3171
|
};
|
|
3164
3172
|
}
|
|
3165
3173
|
|
|
3174
|
+
// task 1002789: the ONE renderer for a failed API call on the ship path.
|
|
3175
|
+
// Every generic bail site used to hand the raw body to console.error (or
|
|
3176
|
+
// interpolate `data.error` into a template, which stringifies the ADR 0116
|
|
3177
|
+
// envelope object to the literal "[object Object]"). Both hide a fully
|
|
3178
|
+
// diagnosable server error: console.error's util.inspect stops at depth 2, so
|
|
3179
|
+
// the `details` that names WHICH git step failed prints as "[Object]".
|
|
3180
|
+
// Collapse the body to one readable line instead, whichever shape it is.
|
|
3181
|
+
// Pure + exported for tests/ship_error_shape.mjs.
|
|
3182
|
+
function apiErrorLine(data, status) {
|
|
3183
|
+
const { code, message, details } = normalizeApiError(data);
|
|
3184
|
+
const head = code
|
|
3185
|
+
? (message && message !== code ? `${code}: ${message}` : code)
|
|
3186
|
+
: (message || (status ? `HTTP ${status}` : 'unknown error'));
|
|
3187
|
+
// Only the small scalar fields — a details blob can carry a whole bundle.
|
|
3188
|
+
const extras = Object.entries(details || {})
|
|
3189
|
+
.filter(([k, v]) => k !== 'code' && k !== 'message' && k !== 'error'
|
|
3190
|
+
&& (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))
|
|
3191
|
+
.map(([k, v]) => `${k}: ${String(v).replace(/\s+/g, ' ').trim()}`);
|
|
3192
|
+
const line = extras.length ? `${head} (${extras.join('; ')})` : head;
|
|
3193
|
+
return status ? `${line} [HTTP ${status}]` : line;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3166
3196
|
// task 1002809: classify a FAILED post-land POST /tasks/<id>/ship. The merge
|
|
3167
3197
|
// already succeeded, so the server may simply have landed the task first
|
|
3168
3198
|
// (reconcile-on-read — task 1049 — or the ~5-min reconciler racing this call).
|
|
@@ -4068,13 +4098,14 @@ async function shipMain() {
|
|
|
4068
4098
|
},
|
|
4069
4099
|
});
|
|
4070
4100
|
if (!r.ok) {
|
|
4071
|
-
|
|
4072
|
-
|
|
4101
|
+
const resolveErr = normalizeApiError(r.data);
|
|
4102
|
+
if (resolveErr.code === 'CLAIM_SESSION_MISMATCH') {
|
|
4103
|
+
const wt = resolveErr.details.claimWorktree;
|
|
4073
4104
|
console.error(`This claim was created in a different session${wt ? ` (worktree: ${wt})` : ''}.`);
|
|
4074
4105
|
console.error('You can only ship from the session that originally claimed this task.');
|
|
4075
4106
|
console.error(`To force-release from this session: node scripts/gds/release.js ${taskId} --force`);
|
|
4076
4107
|
} else {
|
|
4077
|
-
console.error(`resolve failed
|
|
4108
|
+
console.error(`resolve failed: ${apiErrorLine(r.data, r.status)}`);
|
|
4078
4109
|
}
|
|
4079
4110
|
process.exit(1);
|
|
4080
4111
|
}
|
|
@@ -4110,7 +4141,7 @@ async function shipMain() {
|
|
|
4110
4141
|
// verify ran). --verified is a no-op for every other kind.
|
|
4111
4142
|
const confirm = await (await cliClient()).tasks.postTasksIdConfirm({ id: taskId, body: { verified } });
|
|
4112
4143
|
if (!confirm.ok) {
|
|
4113
|
-
if (confirm.status === 409 && confirm.data
|
|
4144
|
+
if (confirm.status === 409 && normalizeApiError(confirm.data).code === 'verify_attestation_required') {
|
|
4114
4145
|
console.error('');
|
|
4115
4146
|
console.error(`This is a VERIFICATION task — it won't confirm until you attest the verify actually passed.`);
|
|
4116
4147
|
console.error(`Run the verification, then re-ship with --verified:`);
|
|
@@ -4139,7 +4170,7 @@ async function shipMain() {
|
|
|
4139
4170
|
console.error(` task.confirm.grade_bypass${floorNote} to POST /api/gds/tasks/${taskId}/confirm.`);
|
|
4140
4171
|
process.exit(1);
|
|
4141
4172
|
}
|
|
4142
|
-
console.error(`manual-confirm failed
|
|
4173
|
+
console.error(`manual-confirm failed: ${apiErrorLine(confirm.data, confirm.status)}`);
|
|
4143
4174
|
process.exit(1);
|
|
4144
4175
|
}
|
|
4145
4176
|
postGradeStatus = confirm.data.taskStatus;
|
|
@@ -4490,6 +4521,9 @@ module.exports = {
|
|
|
4490
4521
|
// 'pending' (success), not failure. Exported so the semantics are unit-tested.
|
|
4491
4522
|
classifyLandOutcome,
|
|
4492
4523
|
normalizeApiError,
|
|
4524
|
+
// task 1002789: the single one-line renderer for a failed API call. Exported
|
|
4525
|
+
// so the "[object Object]" regression is pinned against BOTH wire shapes.
|
|
4526
|
+
apiErrorLine,
|
|
4493
4527
|
postLandShipOutcome,
|
|
4494
4528
|
// task 1718: pure poll-path decision — enable-failure polls the SERVER (not a
|
|
4495
4529
|
// dead-end strand at confirmed), enable-success watches GitHub. Exported so the
|
package/src/module-api.js
CHANGED
|
@@ -951,8 +951,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
951
951
|
// landed since 1.19.260 with no explicit bump. run 32295699354. (task 1002620)
|
|
952
952
|
// 1.19.262 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
953
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)
|
|
956
|
+
// 1.19.264 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
957
|
+
// landed since 1.19.263 with no explicit bump. run 32396082154. (task 1002620)
|
|
954
958
|
// ---------------------------------------------------------------------------
|
|
955
|
-
const CORE_VERSION = '1.19.
|
|
959
|
+
const CORE_VERSION = '1.19.264'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
956
960
|
|
|
957
961
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
958
962
|
// 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).
|