@bongos/core 1.20.45 → 1.20.47
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 +64 -49
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +85 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +170 -161
- package/docs/copy-registry.json +333 -212
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +96 -96
- package/modules/hall-ui/public/bulk-toolbar.js +291 -0
- package/modules/hall-ui/public/goals-page.js +29 -44
- package/modules/hall-ui/public/goals-render.js +33 -21
- package/modules/hall-ui/public/goals.html +12 -16
- package/modules/hall-ui/public/modules.css +45 -0
- package/modules/hall-ui/public/modules.html +23 -1
- package/modules/hall-ui/public/modules.js +94 -2
- package/modules/hall-ui/public/style.css +74 -3
- package/modules/hall-ui/public/work.css +4 -66
- package/modules/hall-ui/public/work.html +2 -1
- package/modules/hall-ui/public/work.js +17 -236
- package/modules/hall-ui/records/catalog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/module-artifact.js +21 -1
- package/scripts/gds/module.js +15 -1
- package/scripts/hall-preview/server.js +3 -0
- package/src/bongos/module-store.js +30 -1
- package/src/bongos/routes/modules.js +91 -1
- package/src/module-api.js +1 -1
- package/tests/bulk_toolbar.mjs +202 -0
- package/tests/claim_action.mjs +13 -4
- package/tests/goal_category_ui.mjs +1 -0
- package/tests/goal_criterion_confirm_ui.mjs +2 -0
- package/tests/goal_forest_page.mjs +1 -0
- package/tests/goal_invite_consent.mjs +1 -1
- package/tests/goal_mine_lens.mjs +1 -0
- package/tests/hall_board_world.mjs +1 -1
- package/tests/hall_catalog_world.mjs +1 -1
- package/tests/hall_mine_lens.mjs +1 -0
- package/tests/module_store_howto.mjs +244 -0
|
@@ -97,6 +97,15 @@
|
|
|
97
97
|
return `<span class="ov-fact">upkeep <b>${escapeHtml(status)}</b></span>`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// A module installed from the store links the how-to its version shipped with
|
|
101
|
+
// (task 1004366). The server sets store_howto only for a published key@version,
|
|
102
|
+
// and only to a /builders/modules?howto= path, so anything else is dropped.
|
|
103
|
+
function howtoFact(m) {
|
|
104
|
+
const href = m.store_howto;
|
|
105
|
+
if (typeof href !== 'string' || !href.startsWith('/builders/modules?howto=')) return '';
|
|
106
|
+
return `<a class="ov-fact mod-howto" href="${escapeHtml(href)}">how-to</a>`;
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
// The lines the row's details disclosure opens onto: the facts the payload
|
|
101
110
|
// carries that the card never rendered — what the module contributes, the
|
|
102
111
|
// core it needs, who maintains it and what it hands off to.
|
|
@@ -143,7 +152,7 @@
|
|
|
143
152
|
<div class="ov-row__main">
|
|
144
153
|
<span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
|
|
145
154
|
${desc ? `<p class="ov-row__sub mod-row__desc">${escapeHtml(desc)}</p>` : ''}
|
|
146
|
-
<div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}</div>
|
|
155
|
+
<div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}</div>
|
|
147
156
|
${lines ? `<details class="mod-more"${openKeys.has(m.key) ? ' open' : ''}><summary aria-label="Details for ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
|
|
148
157
|
</div>
|
|
149
158
|
<div class="ov-row__right mod-row__right">${rightHtml(m)}</div>
|
|
@@ -320,5 +329,88 @@
|
|
|
320
329
|
}
|
|
321
330
|
}
|
|
322
331
|
|
|
323
|
-
|
|
332
|
+
// ---- one store version's how-to (task 1004366, ADR 0347) ----
|
|
333
|
+
//
|
|
334
|
+
// ?howto=<key>&version=<X.Y.Z> opens the HOWTO.md that version shipped with in
|
|
335
|
+
// place of the catalog. A query, not a #/ route: the hall's hash namespace is
|
|
336
|
+
// frozen to the idea and blocker overlays.
|
|
337
|
+
|
|
338
|
+
// The author wrote this file, so a link in it may point anywhere. Only an absolute
|
|
339
|
+
// http(s) link, a mailto: or an in-page #anchor becomes an <a>; a relative path
|
|
340
|
+
// means nothing outside the author's repo, so it renders as its text.
|
|
341
|
+
function howtoLinkHref(href) {
|
|
342
|
+
return /^(https?:\/\/|mailto:|#)/i.test(String(href || '')) ? href : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The same rule the manifest schema enforces on howto.artifactUrl (an https link on
|
|
346
|
+
// claude.ai), checked again here because this page writes it into an href.
|
|
347
|
+
function isClaudeArtifactUrl(u) {
|
|
348
|
+
try {
|
|
349
|
+
const url = new URL(String(u));
|
|
350
|
+
return url.protocol === 'https:' && (url.hostname === 'claude.ai' || url.hostname.endsWith('.claude.ai'));
|
|
351
|
+
} catch (_) { return false; }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function howtoFail(message) {
|
|
355
|
+
const doc = $('#howto-doc');
|
|
356
|
+
if (doc) { doc.removeAttribute('aria-busy'); doc.innerHTML = kit.emptyStateHtml(message); }
|
|
357
|
+
const print = $('#howto-print');
|
|
358
|
+
if (print) print.hidden = true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function loadHowto(key, version) {
|
|
362
|
+
const catalog = $('#core-scroll');
|
|
363
|
+
if (catalog) catalog.hidden = true;
|
|
364
|
+
const sub = $('#modules-sub');
|
|
365
|
+
if (sub) sub.hidden = true;
|
|
366
|
+
// A how-to is a document, so the page opts into the reading room (room.css
|
|
367
|
+
// scopes its column width and article sizes to body.room).
|
|
368
|
+
document.body.classList.add('room');
|
|
369
|
+
const view = $('#howto-view');
|
|
370
|
+
if (view) view.hidden = false;
|
|
371
|
+
const print = $('#howto-print');
|
|
372
|
+
if (print) print.addEventListener('click', () => window.print());
|
|
373
|
+
|
|
374
|
+
const res = await api.request('GET', `${API}/store/modules/${encodeURIComponent(key)}/versions/${encodeURIComponent(version)}/howto`);
|
|
375
|
+
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
|
|
376
|
+
const body = res.data || {};
|
|
377
|
+
if (!res.ok) {
|
|
378
|
+
const err = body.error || {};
|
|
379
|
+
howtoFail((typeof err === 'object' && err.message) || body.message || `Could not load the how-to (${res.status}).`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const title = String(body.title || key);
|
|
383
|
+
document.title = `${title} ${body.version} — how-to`;
|
|
384
|
+
const h = $('#howto-h');
|
|
385
|
+
if (h) h.textContent = `${title} — how to use it`;
|
|
386
|
+
const meta = $('#howto-meta');
|
|
387
|
+
if (meta) {
|
|
388
|
+
meta.innerHTML = `<span class="ov-mono">${escapeHtml(key)}</span> · version <span class="ov-mono">${escapeHtml(String(body.version))}</span>`
|
|
389
|
+
+ (body.delisted ? ' · <span class="fact-pill fact-pill--warn">delisted</span>' : '');
|
|
390
|
+
}
|
|
391
|
+
const extra = $('#howto-extra');
|
|
392
|
+
if (extra && body.artifact_url && isClaudeArtifactUrl(body.artifact_url)) {
|
|
393
|
+
extra.href = body.artifact_url;
|
|
394
|
+
extra.hidden = false;
|
|
395
|
+
}
|
|
396
|
+
const doc = $('#howto-doc');
|
|
397
|
+
if (doc) {
|
|
398
|
+
// HTML comments are the scaffold's writing prompts (ADR 0347 D2); they are
|
|
399
|
+
// not part of the how-to, so they are removed rather than shown as text.
|
|
400
|
+
const md = String(body.markdown || '').replace(/<!--[\s\S]*?-->/g, '');
|
|
401
|
+
doc.innerHTML = window.OTBMdReader.parseMarkdown(md, howtoLinkHref).html;
|
|
402
|
+
doc.removeAttribute('aria-busy');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const params = new URLSearchParams(window.location.search);
|
|
407
|
+
const howtoKey = params.get('howto');
|
|
408
|
+
if (howtoKey) {
|
|
409
|
+
loadHowto(howtoKey, params.get('version') || '').catch((err) => {
|
|
410
|
+
console.error('[modules] how-to failed to load', err);
|
|
411
|
+
howtoFail('Could not load the how-to. Return to the modules list and try again.');
|
|
412
|
+
});
|
|
413
|
+
} else {
|
|
414
|
+
load();
|
|
415
|
+
}
|
|
324
416
|
})();
|
|
@@ -2876,16 +2876,87 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
2876
2876
|
}
|
|
2877
2877
|
.bulk-toolbar__clear:hover { color: var(--ink); }
|
|
2878
2878
|
|
|
2879
|
+
/* ---- shared by the Tasks board AND the goal board's cards (task 1003141) ----
|
|
2880
|
+
These three used to live in work.css, which goals.html borrowed whole. They
|
|
2881
|
+
are single-class rules no other sheet restates, so moving them earlier in the
|
|
2882
|
+
cascade changes nothing — checked by computed style on both pages. */
|
|
2883
|
+
|
|
2884
|
+
/* Filtered-count line under the filter bar — the label voice, with the number
|
|
2885
|
+
itself stepping up to ink so the count reads before the words. */
|
|
2886
|
+
.claim-count-line {
|
|
2887
|
+
margin: 0 0 12px;
|
|
2888
|
+
font-family: var(--label-face);
|
|
2889
|
+
font-size: var(--label-size);
|
|
2890
|
+
letter-spacing: var(--label-space);
|
|
2891
|
+
text-transform: var(--label-case);
|
|
2892
|
+
color: var(--ink-faint);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
.wip__release {
|
|
2896
|
+
display: inline-flex;
|
|
2897
|
+
align-items: center;
|
|
2898
|
+
justify-content: center;
|
|
2899
|
+
min-height: 30px;
|
|
2900
|
+
padding: 0 14px;
|
|
2901
|
+
background: none;
|
|
2902
|
+
border: var(--line) solid var(--rule);
|
|
2903
|
+
color: var(--ink-soft);
|
|
2904
|
+
border-radius: var(--radius-pill);
|
|
2905
|
+
font-size: 13px;
|
|
2906
|
+
text-transform: lowercase;
|
|
2907
|
+
cursor: pointer;
|
|
2908
|
+
transition: background 150ms, color 150ms, border-color 150ms;
|
|
2909
|
+
}
|
|
2910
|
+
.wip__release:hover:not(:disabled) {
|
|
2911
|
+
background: var(--danger-weak);
|
|
2912
|
+
color: var(--danger);
|
|
2913
|
+
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
|
2914
|
+
}
|
|
2915
|
+
.wip__release:disabled { opacity: 0.55; cursor: default; }
|
|
2916
|
+
|
|
2917
|
+
.wip__select {
|
|
2918
|
+
appearance: none;
|
|
2919
|
+
-webkit-appearance: none;
|
|
2920
|
+
position: relative;
|
|
2921
|
+
width: 24px;
|
|
2922
|
+
height: 24px;
|
|
2923
|
+
margin: 0;
|
|
2924
|
+
cursor: pointer;
|
|
2925
|
+
}
|
|
2926
|
+
.wip__select::before {
|
|
2927
|
+
content: "";
|
|
2928
|
+
position: absolute;
|
|
2929
|
+
left: 5px;
|
|
2930
|
+
top: 5px;
|
|
2931
|
+
width: 14px;
|
|
2932
|
+
height: 14px;
|
|
2933
|
+
border: var(--line) solid var(--rule);
|
|
2934
|
+
border-radius: 3px;
|
|
2935
|
+
}
|
|
2936
|
+
.wip__select:checked::before { background: var(--accent); border-color: var(--accent); }
|
|
2937
|
+
.wip__select:checked::after {
|
|
2938
|
+
content: "";
|
|
2939
|
+
position: absolute;
|
|
2940
|
+
left: 9px;
|
|
2941
|
+
top: 8px;
|
|
2942
|
+
width: 4px;
|
|
2943
|
+
height: 8px;
|
|
2944
|
+
border: solid var(--accent-on);
|
|
2945
|
+
border-width: 0 1.5px 1.5px 0;
|
|
2946
|
+
transform: rotate(42deg);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2879
2949
|
/* The shell's .app-content already ends in 108px of bottom air at desktop
|
|
2880
2950
|
width, which is what clears the bar — but at 768 it drops to 48px for the
|
|
2881
2951
|
phone, and there the bar is TALLER (its actions wrap to their own row). So
|
|
2882
|
-
the phone width buys the room back, scoped to
|
|
2883
|
-
|
|
2952
|
+
the phone width buys the room back, scoped to the two pages that carry the
|
|
2953
|
+
toolbar (the goal board gained the full one in task 1003141). */
|
|
2884
2954
|
@media (max-width: 768px) {
|
|
2885
2955
|
/* At the drawer breakpoint the sidebar leaves the flow, so the bar spans. */
|
|
2886
2956
|
.bulk-toolbar { left: 0; padding: 10px 14px; gap: 10px; }
|
|
2887
2957
|
.bulk-toolbar__actions { margin-left: 0; width: 100%; }
|
|
2888
|
-
body[data-page="work"] .app-content
|
|
2958
|
+
body[data-page="work"] .app-content,
|
|
2959
|
+
body[data-page="goals"] .app-content { padding-bottom: 200px; }
|
|
2889
2960
|
}
|
|
2890
2961
|
|
|
2891
2962
|
.task__credits { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; padding-top: 2px; }
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
verification state, which DESIGN.md §Reuse names as the one sanctioned
|
|
15
15
|
exception to the Fifteen Rule.
|
|
16
16
|
|
|
17
|
-
goals.html LOADS THIS SHEET
|
|
17
|
+
goals.html NO LONGER LOADS THIS SHEET (task 1003141): what the goal board's
|
|
18
|
+
cards borrowed from it — .claim-count-line, .wip__release, .wip__select — now
|
|
19
|
+
lives in style.css, so this sheet is the board's alone. */
|
|
18
20
|
|
|
19
21
|
/* `.page-head--board` MOVED to style.css (task 1003305) when the ideas composer
|
|
20
22
|
became its third consumer: a modifier on the shell's own .page-head is shell
|
|
@@ -35,17 +37,6 @@
|
|
|
35
37
|
.wip-builder { color: var(--ink); font-weight: 500; }
|
|
36
38
|
.wip-builder small { color: var(--ink-soft); font-weight: 400; }
|
|
37
39
|
|
|
38
|
-
/* Filtered-count line under the filter bar — the label voice, with the number
|
|
39
|
-
itself stepping up to ink so the count reads before the words. */
|
|
40
|
-
.claim-count-line {
|
|
41
|
-
margin: 0 0 12px;
|
|
42
|
-
font-family: var(--label-face);
|
|
43
|
-
font-size: var(--label-size);
|
|
44
|
-
letter-spacing: var(--label-space);
|
|
45
|
-
text-transform: var(--label-case);
|
|
46
|
-
color: var(--ink-faint);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
.backlog-count {
|
|
50
41
|
font-family: var(--font-mono);
|
|
51
42
|
font-size: 11px;
|
|
@@ -67,27 +58,6 @@
|
|
|
67
58
|
|
|
68
59
|
/* In-flight table: release action on the viewer's own claims. */
|
|
69
60
|
.wip__action { text-align: right; white-space: nowrap; }
|
|
70
|
-
.wip__release {
|
|
71
|
-
display: inline-flex;
|
|
72
|
-
align-items: center;
|
|
73
|
-
justify-content: center;
|
|
74
|
-
min-height: 30px;
|
|
75
|
-
padding: 0 14px;
|
|
76
|
-
background: none;
|
|
77
|
-
border: var(--line) solid var(--rule);
|
|
78
|
-
color: var(--ink-soft);
|
|
79
|
-
border-radius: var(--radius-pill);
|
|
80
|
-
font-size: 13px;
|
|
81
|
-
text-transform: lowercase;
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
transition: background 150ms, color 150ms, border-color 150ms;
|
|
84
|
-
}
|
|
85
|
-
.wip__release:hover:not(:disabled) {
|
|
86
|
-
background: var(--danger-weak);
|
|
87
|
-
color: var(--danger);
|
|
88
|
-
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
|
89
|
-
}
|
|
90
|
-
.wip__release:disabled { opacity: 0.55; cursor: default; }
|
|
91
61
|
.wip__mine-none { color: var(--ink-faint); }
|
|
92
62
|
.wip__row--mine { background: var(--accent-tint); }
|
|
93
63
|
|
|
@@ -121,7 +91,7 @@
|
|
|
121
91
|
.history__when { color: var(--ink-soft); font-size: 12.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
122
92
|
|
|
123
93
|
|
|
124
|
-
/* ---- task 1759: multi-select
|
|
94
|
+
/* ---- task 1759: multi-select (the toolbar AND the checkbox are in style.css) ----
|
|
125
95
|
Re-cut for the 24px floor and the world (task 1003304). The floating dark
|
|
126
96
|
pill is gone: it painted an INVERTED surface (background: var(--ink)) with
|
|
127
97
|
three rgba(255,255,255,…) literals on top, which was correct only for as long
|
|
@@ -130,38 +100,6 @@
|
|
|
130
100
|
content column instead, inset by the rail, so it never has to invert. */
|
|
131
101
|
|
|
132
102
|
.wip__select-cell { width: 1%; text-align: center; }
|
|
133
|
-
.wip__select {
|
|
134
|
-
appearance: none;
|
|
135
|
-
-webkit-appearance: none;
|
|
136
|
-
position: relative;
|
|
137
|
-
width: 24px;
|
|
138
|
-
height: 24px;
|
|
139
|
-
margin: 0;
|
|
140
|
-
cursor: pointer;
|
|
141
|
-
}
|
|
142
|
-
.wip__select::before {
|
|
143
|
-
content: "";
|
|
144
|
-
position: absolute;
|
|
145
|
-
left: 5px;
|
|
146
|
-
top: 5px;
|
|
147
|
-
width: 14px;
|
|
148
|
-
height: 14px;
|
|
149
|
-
border: var(--line) solid var(--rule);
|
|
150
|
-
border-radius: 3px;
|
|
151
|
-
}
|
|
152
|
-
.wip__select:checked::before { background: var(--accent); border-color: var(--accent); }
|
|
153
|
-
.wip__select:checked::after {
|
|
154
|
-
content: "";
|
|
155
|
-
position: absolute;
|
|
156
|
-
left: 9px;
|
|
157
|
-
top: 8px;
|
|
158
|
-
width: 4px;
|
|
159
|
-
height: 8px;
|
|
160
|
-
border: solid var(--accent-on);
|
|
161
|
-
border-width: 0 1.5px 1.5px 0;
|
|
162
|
-
transform: rotate(42deg);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
103
|
/* ---- "Only mine" lens (the board's tab-wide filter; goal board's To-do bar
|
|
166
104
|
borrows the same class) --------------------------------------------------
|
|
167
105
|
Deliberately QUIETER than the filter bar below it: a checkbox in the label
|
|
@@ -189,7 +189,8 @@
|
|
|
189
189
|
<script src="/builders/work-summary.js" defer></script>
|
|
190
190
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail" defer></script>
|
|
191
191
|
<script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
|
|
192
|
-
<script src="/builders/
|
|
192
|
+
<script src="/builders/bulk-toolbar.js" defer></script>
|
|
193
|
+
<script src="/builders/work.js?v=2026-09-30-bulktoolbar" defer></script>
|
|
193
194
|
<script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
|
|
194
195
|
</body>
|
|
195
196
|
</html>
|
|
@@ -187,9 +187,9 @@
|
|
|
187
187
|
// The hall's ONE claim/release action (task 1003458). Everything this board
|
|
188
188
|
// used to hand-roll four times — the POST, the refusal sentence, the
|
|
189
189
|
// stale-view refresh, the all-or-nothing batch report — lives in
|
|
190
|
-
// claim-action.js now; doClaim/doRelease
|
|
191
|
-
//
|
|
192
|
-
//
|
|
190
|
+
// claim-action.js now; doClaim/doRelease below keep only what is
|
|
191
|
+
// genuinely this page: button text and the re-entrancy guard (the bulk
|
|
192
|
+
// toolbar is bulk-toolbar.js, built from this same action). friendlyBatchReason is injected rather
|
|
193
193
|
// than imported there, so claim-action.js stays dependency-free.
|
|
194
194
|
// refreshLive is a hoisted declaration; toast and API are already bound above.
|
|
195
195
|
const claimAction = makeClaimAction({
|
|
@@ -291,18 +291,6 @@
|
|
|
291
291
|
// String(claim_id) -> task_id for the viewer's OWN in-flight claims (release
|
|
292
292
|
// is only ever offered on those rows to begin with — see renderInFlight).
|
|
293
293
|
const selection = { claim: new Set(), release: new Map() };
|
|
294
|
-
// Last server dry-run (POST /claims/batch/validate) result, keyed by the
|
|
295
|
-
// sorted selected-id set so an unrelated re-render doesn't refire it.
|
|
296
|
-
const validateCache = { key: '', parallelSafe: null, byId: new Map() };
|
|
297
|
-
let validateDebounce = null;
|
|
298
|
-
// Per-task dependency edges for the "can these be sequenced?" cycle check —
|
|
299
|
-
// fetched lazily via GET /tasks/:id (task-detail.js's call) since the
|
|
300
|
-
// claimable feed doesn't carry dependencies[] per row. Stable for a ready
|
|
301
|
-
// task within one page session, so caching by id is safe.
|
|
302
|
-
const depsCache = new Map(); // task_id -> [dependency task_id, ...]
|
|
303
|
-
const sequentialCache = { key: '', order: null };
|
|
304
|
-
let sequentialDebounce = null;
|
|
305
|
-
|
|
306
294
|
// task 1756: two tasks can't run in parallel if their touches[] overlap — exact
|
|
307
295
|
// path match or a directory-prefix either way (mirrors the server's touchesOverlap
|
|
308
296
|
// / the goals-page conflict estimate). Empty touches never conflict.
|
|
@@ -700,207 +688,18 @@
|
|
|
700
688
|
}
|
|
701
689
|
}
|
|
702
690
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
validateCache.key = key;
|
|
716
|
-
validateCache.parallelSafe = out.parallelSafe;
|
|
717
|
-
validateCache.byId = out.byId;
|
|
718
|
-
renderBulkToolbar();
|
|
719
|
-
}, 350);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async function fetchDeps(taskId) {
|
|
723
|
-
if (depsCache.has(taskId)) return depsCache.get(taskId);
|
|
724
|
-
let deps = [];
|
|
725
|
-
try {
|
|
726
|
-
const d = await getJSON(`${API}/tasks/${encodeURIComponent(taskId)}`, { softAuth: true });
|
|
727
|
-
if (d && d.task && Array.isArray(d.task.dependencies)) {
|
|
728
|
-
deps = d.task.dependencies.map((x) => Number(x.id)).filter((x) => Number.isFinite(x));
|
|
729
|
-
}
|
|
730
|
-
} catch (_) { /* treat as no known dependencies */ }
|
|
731
|
-
depsCache.set(taskId, deps);
|
|
732
|
-
return deps;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Kahn's algorithm restricted to edges BETWEEN the selected ids (an
|
|
736
|
-
// out-of-selection dependency doesn't block sequencing the selection
|
|
737
|
-
// itself). Returns the topological order, or null if the restricted graph
|
|
738
|
-
// has a cycle (can't be sequenced).
|
|
739
|
-
function topoOrder(ids, edgesByTask) {
|
|
740
|
-
const remaining = new Set(ids);
|
|
741
|
-
const order = [];
|
|
742
|
-
let progressed = true;
|
|
743
|
-
while (remaining.size && progressed) {
|
|
744
|
-
progressed = false;
|
|
745
|
-
for (const id of Array.from(remaining)) {
|
|
746
|
-
const deps = (edgesByTask.get(id) || []).filter((d) => remaining.has(d));
|
|
747
|
-
if (deps.length === 0) {
|
|
748
|
-
order.push(id);
|
|
749
|
-
remaining.delete(id);
|
|
750
|
-
progressed = true;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
return remaining.size ? null : order;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function scheduleSequentialCheck(key, ids) {
|
|
758
|
-
if (sequentialDebounce) clearTimeout(sequentialDebounce);
|
|
759
|
-
sequentialDebounce = setTimeout(async () => {
|
|
760
|
-
const edgesByTask = new Map();
|
|
761
|
-
await Promise.all(ids.map(async (id) => { edgesByTask.set(id, await fetchDeps(id)); }));
|
|
762
|
-
sequentialCache.key = key;
|
|
763
|
-
sequentialCache.order = topoOrder(ids, edgesByTask);
|
|
764
|
-
renderBulkToolbar();
|
|
765
|
-
}, 350);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function renderBulkToolbar() {
|
|
769
|
-
const bar = $('#bulk-toolbar');
|
|
770
|
-
if (!bar) return;
|
|
771
|
-
if (selectionTotal() === 0) { bar.hidden = true; return; }
|
|
772
|
-
bar.hidden = false;
|
|
773
|
-
|
|
774
|
-
const countEl = $('#bulk-count');
|
|
775
|
-
if (countEl) {
|
|
776
|
-
const parts = [];
|
|
777
|
-
if (selection.claim.size) parts.push(`${selection.claim.size} to claim`);
|
|
778
|
-
if (selection.release.size) parts.push(`${selection.release.size} to release`);
|
|
779
|
-
countEl.textContent = parts.join(' · ');
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const claimBtn = $('#bulk-claim-btn');
|
|
783
|
-
if (claimBtn) {
|
|
784
|
-
claimBtn.disabled = selection.claim.size === 0;
|
|
785
|
-
claimBtn.textContent = selection.claim.size > 1 ? `Bulk claim (${selection.claim.size})` : 'Bulk claim';
|
|
786
|
-
}
|
|
787
|
-
const releaseBtn = $('#bulk-release-btn');
|
|
788
|
-
if (releaseBtn) {
|
|
789
|
-
releaseBtn.disabled = selection.release.size === 0;
|
|
790
|
-
releaseBtn.textContent = selection.release.size > 1 ? `Bulk release (${selection.release.size})` : 'Bulk release';
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const parallelBtn = $('#bulk-parallel-btn');
|
|
794
|
-
if (parallelBtn) {
|
|
795
|
-
if (selection.claim.size < 2) {
|
|
796
|
-
parallelBtn.disabled = true;
|
|
797
|
-
parallelBtn.title = 'Select at least 2 waiting tasks to copy a parallel-session prompt.';
|
|
798
|
-
} else {
|
|
799
|
-
const ids = selectedClaimIds();
|
|
800
|
-
const key = selectionKey(ids);
|
|
801
|
-
if (validateCache.key === key && validateCache.parallelSafe != null) {
|
|
802
|
-
parallelBtn.disabled = !validateCache.parallelSafe;
|
|
803
|
-
if (!validateCache.parallelSafe) {
|
|
804
|
-
const bad = [...validateCache.byId.values()].filter((r) => !r.valid);
|
|
805
|
-
const detail = bad.map((r) => `#${r.task_id}: ${(r.reasons || []).map(friendlyBatchReason).join(', ')}`).join(' · ');
|
|
806
|
-
parallelBtn.title = detail || 'Selected tasks conflict and cannot run in parallel.';
|
|
807
|
-
} else {
|
|
808
|
-
parallelBtn.title = '';
|
|
809
|
-
}
|
|
810
|
-
} else {
|
|
811
|
-
parallelBtn.disabled = true;
|
|
812
|
-
parallelBtn.title = 'Checking for conflicts…';
|
|
813
|
-
scheduleValidate(key, ids);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const sequentialBtn = $('#bulk-sequential-btn');
|
|
819
|
-
if (sequentialBtn) {
|
|
820
|
-
if (selection.claim.size < 2) {
|
|
821
|
-
sequentialBtn.disabled = true;
|
|
822
|
-
sequentialBtn.title = 'Select at least 2 waiting tasks to copy a sequential prompt.';
|
|
823
|
-
} else {
|
|
824
|
-
const ids = selectedClaimIds();
|
|
825
|
-
const key = selectionKey(ids);
|
|
826
|
-
if (sequentialCache.key === key) {
|
|
827
|
-
sequentialBtn.disabled = sequentialCache.order == null;
|
|
828
|
-
sequentialBtn.title = sequentialCache.order == null
|
|
829
|
-
? 'These tasks depend on each other in a cycle and cannot be ordered.'
|
|
830
|
-
: '';
|
|
831
|
-
} else {
|
|
832
|
-
sequentialBtn.disabled = true;
|
|
833
|
-
sequentialBtn.title = 'Checking dependency order…';
|
|
834
|
-
scheduleSequentialCheck(key, ids);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function clearSelection() {
|
|
841
|
-
selection.claim.clear();
|
|
842
|
-
selection.release.clear();
|
|
843
|
-
renderClaimable();
|
|
844
|
-
renderInFlight();
|
|
845
|
-
renderBulkToolbar();
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function buildBulkParallelPrompt(tasks) {
|
|
849
|
-
const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
|
|
850
|
-
const lines = [
|
|
851
|
-
`I want to work on ${tasks.length} Bongos tasks IN PARALLEL — open ${tasks.length} separate Claude Code sessions, each in its own worktree, one per task below, so they can run at the same time without touching each other's files.`,
|
|
852
|
-
'',
|
|
853
|
-
];
|
|
854
|
-
tasks.forEach((t, i) => {
|
|
855
|
-
lines.push(`Session ${i + 1} — task ${t.id} (${t.title}) — ${hallBase}#/task/${t.id}`);
|
|
856
|
-
lines.push(`Claim it with /builder-claim ${t.id}, follow its acceptance criteria, stay within touches[], ship via /builder-ship when done.`);
|
|
857
|
-
lines.push('');
|
|
858
|
-
});
|
|
859
|
-
return lines.join('\n').trim();
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function buildBulkSequentialPrompt(tasks) {
|
|
863
|
-
const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
|
|
864
|
-
const lines = [
|
|
865
|
-
`I want to work on ${tasks.length} Bongos tasks IN ORDER, one after another in this same session — claim, build, and ship each before moving to the next.`,
|
|
866
|
-
'',
|
|
867
|
-
];
|
|
868
|
-
tasks.forEach((t, i) => {
|
|
869
|
-
lines.push(`${i + 1}. Task ${t.id} (${t.title}) — ${hallBase}#/task/${t.id}`);
|
|
870
|
-
});
|
|
871
|
-
lines.push('');
|
|
872
|
-
lines.push('For each: /builder-claim <id>, follow its acceptance criteria, stay within touches[], /builder-ship <id> before starting the next.');
|
|
873
|
-
return lines.join('\n');
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
async function doBulkClaim() {
|
|
877
|
-
const ids = selectedClaimIds();
|
|
878
|
-
if (!ids.length) return;
|
|
879
|
-
const btn = $('#bulk-claim-btn');
|
|
880
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
|
|
881
|
-
try {
|
|
882
|
-
const out = await claimAction.claimMany(ids);
|
|
883
|
-
if (out.ok) selection.claim.clear();
|
|
884
|
-
} finally {
|
|
885
|
-
renderBulkToolbar();
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
async function doBulkRelease() {
|
|
890
|
-
const entries = [...selection.release.entries()]; // [claimId, taskId]
|
|
891
|
-
if (!entries.length) return;
|
|
892
|
-
const label = entries.length === 1 ? `#${entries[0][1]}` : `${entries.length} tasks`;
|
|
893
|
-
const pronoun = entries.length === 1 ? 'It returns' : 'They return';
|
|
894
|
-
if (!window.confirm(`Release ${label}? ${pronoun} to the queue and no ${curLabel()} are earned.`)) return;
|
|
895
|
-
const btn = $('#bulk-release-btn');
|
|
896
|
-
if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
|
|
897
|
-
try {
|
|
898
|
-
const out = await claimAction.releaseMany(entries);
|
|
899
|
-
if (out.ok) selection.release.clear();
|
|
900
|
-
} finally {
|
|
901
|
-
renderBulkToolbar();
|
|
902
|
-
}
|
|
903
|
-
}
|
|
691
|
+
// The toolbar itself (render, bulk claim/release, the two prompts) is the
|
|
692
|
+
// hall's shared one — bulk-toolbar.js. This page keeps the selection and how
|
|
693
|
+
// Clear repaints its own cards.
|
|
694
|
+
const bulk = window.OTBBulkToolbar.makeBulkToolbar({
|
|
695
|
+
ids: { bar: 'bulk-toolbar', count: 'bulk-count', claim: 'bulk-claim-btn', release: 'bulk-release-btn', parallel: 'bulk-parallel-btn', sequential: 'bulk-sequential-btn', clear: 'bulk-clear-btn' },
|
|
696
|
+
selection,
|
|
697
|
+
findTask: (id) => taskState.all.find((t) => Number(t.id) === id),
|
|
698
|
+
claimAction, getJSON, API, copyText, toast, curLabel, describeReason: friendlyBatchReason,
|
|
699
|
+
onClear: () => { renderClaimable(); renderInFlight(); },
|
|
700
|
+
});
|
|
701
|
+
function renderBulkToolbar() { bulk.render(); }
|
|
702
|
+
function clearSelection() { bulk.clearSelection(); }
|
|
904
703
|
|
|
905
704
|
async function doClaim(taskId, btn) {
|
|
906
705
|
const key = `claim:${taskId}`;
|
|
@@ -1373,26 +1172,8 @@
|
|
|
1373
1172
|
doRelease(rel.dataset.claimId, rel.dataset.taskId, rel);
|
|
1374
1173
|
});
|
|
1375
1174
|
|
|
1376
|
-
// task 1759: the bottom bulk-action toolbar.
|
|
1377
|
-
|
|
1378
|
-
$('#bulk-release-btn')?.addEventListener('click', doBulkRelease);
|
|
1379
|
-
$('#bulk-clear-btn')?.addEventListener('click', clearSelection);
|
|
1380
|
-
$('#bulk-parallel-btn')?.addEventListener('click', async () => {
|
|
1381
|
-
const ids = selectedClaimIds();
|
|
1382
|
-
const tasks = ids.map((id) => taskState.all.find((t) => Number(t.id) === id)).filter(Boolean);
|
|
1383
|
-
if (tasks.length < 2) return;
|
|
1384
|
-
const ok = await copyText(buildBulkParallelPrompt(tasks));
|
|
1385
|
-
toast(ok ? `Parallel-session prompt for ${tasks.length} tasks copied.` : 'Could not copy to the clipboard.');
|
|
1386
|
-
});
|
|
1387
|
-
$('#bulk-sequential-btn')?.addEventListener('click', async () => {
|
|
1388
|
-
const ids = selectedClaimIds();
|
|
1389
|
-
const key = selectionKey(ids);
|
|
1390
|
-
if (sequentialCache.key !== key || sequentialCache.order == null) return;
|
|
1391
|
-
const tasks = sequentialCache.order.map((id) => taskState.all.find((t) => Number(t.id) === id)).filter(Boolean);
|
|
1392
|
-
if (tasks.length < 2) return;
|
|
1393
|
-
const ok = await copyText(buildBulkSequentialPrompt(tasks));
|
|
1394
|
-
toast(ok ? `Sequential prompt for ${tasks.length} tasks copied.` : 'Could not copy to the clipboard.');
|
|
1395
|
-
});
|
|
1175
|
+
// task 1759: the bottom bulk-action toolbar (shared: bulk-toolbar.js).
|
|
1176
|
+
bulk.wire();
|
|
1396
1177
|
}
|
|
1397
1178
|
|
|
1398
1179
|
document.addEventListener('DOMContentLoaded', () => {
|
|
@@ -8,3 +8,5 @@
|
|
|
8
8
|
- **The island index is the map's keyboard and phone path.** The scene is a hover surface: SVG `<g>`s with mouse listeners, **no focusable element**, and at 390px thirty islands in 360px with nothing under them. `drawIndex` renders every node of the current view as a dense `.ov-row` under a `kit-collapse` (`#ab-index`) whose title is a disclosure button (`aria-expanded`/`aria-controls`) laying the SAME `detailHTML` inline; hovering a row lights the island, and **the index opens itself under 640px (`matchMedia`), where the list is the read.**
|
|
9
9
|
|
|
10
10
|
Also gone, and the one rule in that list: `.ab-caption` takes the lede's `--measure`, never a fresh `90ch`. **Gotcha:** `gen-atlas.js` run from the MAIN checkout walks `.claude/worktrees/*` and emits thousands of context nodes — run it from a worktree, or copy a worktree's `atlas.json` over, before previewing the atlas there.
|
|
11
|
+
|
|
12
|
+
**A store version's how-to is a view of the Modules page, not a new page (task 1004366).** `/builders/modules?howto=<key>&version=<X.Y.Z>` hides the catalog and shows that version's `HOWTO.md` in the reading room's article card: the page adds `body.room` so `room.css`'s sizes apply, and the markdown goes through `md-reader.js` (author-written, so every byte is escaped; only absolute http(s), mailto and #anchor links become links). A query, not a `#/` route — the hash namespace is frozen to the idea and blocker overlays. "Save as PDF" is `window.print()` with an `@media print` block in `modules.css` that keeps only the document and prints in the system ink (`CanvasText`), because a literal colour is the token tier's. Harness fixture: `store__modules__howto.json`.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.47",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.47",
|
|
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.20.
|
|
3
|
+
"version": "1.20.47",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8383,5 +8383,17 @@
|
|
|
8383
8383
|
"id": "1004473",
|
|
8384
8384
|
"text": "The Builders Hall now has a Commands page listing every slash command and every terminal command, with what each does, when to use it and what it costs, including the ones hidden from Claude's menu."
|
|
8385
8385
|
}
|
|
8386
|
+
],
|
|
8387
|
+
"1.20.46": [
|
|
8388
|
+
{
|
|
8389
|
+
"id": "1004366",
|
|
8390
|
+
"text": "Every store module version now has a readable how-to page in the hall, with a Save as PDF button and an optional link to a Claude page."
|
|
8391
|
+
}
|
|
8392
|
+
],
|
|
8393
|
+
"1.20.47": [
|
|
8394
|
+
{
|
|
8395
|
+
"id": "1003141",
|
|
8396
|
+
"text": "The Goals page now has the same bulk actions as the Tasks board: claim several tasks, release your own, or copy a prompt to work them in parallel or in order. Both pages now share one toolbar, so they stay in step."
|
|
8397
|
+
}
|
|
8386
8398
|
]
|
|
8387
8399
|
}
|