@bongos/core 1.19.1044 → 1.19.1045

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.
@@ -36,6 +36,10 @@
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, errCode, errDetails } = window.OTB; // task 1002462: envelope readers
39
+ // The hall's ONE claim/release refusal copy (task 1003458). This board's own
40
+ // copy is gone: a second surface that wants a claim button consumes this
41
+ // instead of lifting the switch, which is how the two drifted apart before.
42
+ const { friendlyClaimError, makeClaimAction } = window.OTBClaimAction;
39
43
  // Shared task-domain helpers (task 1002463). These lived in two copies here and
40
44
  // in the other board and had already drifted — goals-page.js had silently lost
41
45
  // friendlyBatchReason's MODULE_CONFLICT case. Destructured so every call site
@@ -180,6 +184,18 @@
180
184
  return data;
181
185
  }
182
186
 
187
+ // The hall's ONE claim/release action (task 1003458). Everything this board
188
+ // used to hand-roll four times — the POST, the refusal sentence, the
189
+ // stale-view refresh, the all-or-nothing batch report — lives in
190
+ // claim-action.js now; doClaim/doRelease/doBulkClaim/doBulkRelease below keep
191
+ // only what is genuinely this page: button text, the re-entrancy guard, the
192
+ // confirm() wording and the selection. friendlyBatchReason is injected rather
193
+ // than imported there, so claim-action.js stays dependency-free.
194
+ // refreshLive is a hoisted declaration; toast and API are already bound above.
195
+ const claimAction = makeClaimAction({
196
+ postJSON, toast, API, refresh: refreshLive, describeBatchReason: friendlyBatchReason,
197
+ });
198
+
183
199
  // ---- paginated lists (task 1931/1955 → kit, task 2170) --------------------
184
200
  // All four Work-page lists paginate through the shared OTBKit pager (the
185
201
  // canonical copy of the helper that used to live here). localStorage keys are
@@ -627,51 +643,6 @@
627
643
  }
628
644
  }
629
645
  }
630
-
631
- // ---- claim / release actions ---------------------------------------------
632
-
633
- // Map the server's typed error code to a builder-readable line. The codes are
634
- // exactly those POST /claims and POST /claims/:id/resolve can return.
635
- function friendlyActionError(status, data) {
636
- // task 1002462: read the ADR 0116 envelope. `data.error` is an OBJECT, so the
637
- // old `const code = data && data.error` compared a string to an object and
638
- // EVERY case below fell through to the generic default — a wrong-but-plausible
639
- // toast on every claim/release refusal.
640
- const det = errDetails(data);
641
- switch (errCode(data)) {
642
- case 'ALREADY_CLAIMED':
643
- return 'Someone else claimed that task first. The board will refresh.';
644
- case 'TASK_NOT_READY':
645
- return 'That task is no longer ready to be claimed.';
646
- case 'TOUCHES_CONFLICT': {
647
- const titles = Array.isArray(det.conflicts) ? det.conflicts.map((c) => c.title).filter(Boolean) : [];
648
- return titles.length
649
- ? `That task overlaps an active claim: ${titles.join('; ')}. Wait for it to ship.`
650
- : 'That task overlaps another active claim. Wait for it to ship.';
651
- }
652
- case 'DEPS_NOT_SHIPPED':
653
- return 'That task waits on unshipped dependencies. Pick another for now.';
654
- case 'rank_too_low_for_task':
655
- case 'XENOS_RESTRICTED':
656
- case 'INSUFFICIENT_RANK':
657
- return 'Your rank is not yet high enough for that task.';
658
- case 'newcomer_must_ship_first':
659
- return det.what_to_do || 'Ship your current claim before opening another.';
660
- case 'NOT_YOUR_CLAIM':
661
- return 'That claim is not yours to release.';
662
- case 'ALREADY_RESOLVED':
663
- return 'That claim was already resolved. The board will refresh.';
664
- case 'BUILDER_INACTIVE':
665
- return 'Your account is inactive. Contact an Archon.';
666
- default:
667
- return `The board could not complete that (${status}). Try again.`;
668
- }
669
- }
670
-
671
- // Maps a single batch-item failure reason (POST /claims/batch,
672
- // /claims/batch/validate) to a builder-readable fragment. Distinct from
673
- // friendlyActionError above because a batch item's shape differs (reason,
674
- // not error; extra fields like conflictsWithTaskId/module_key).
675
646
  async function copyText(text) {
676
647
  try { await navigator.clipboard.writeText(text); return true; }
677
648
  catch (_) {
@@ -735,18 +706,15 @@
735
706
  async function scheduleValidate(key, ids) {
736
707
  if (validateDebounce) clearTimeout(validateDebounce);
737
708
  validateDebounce = setTimeout(async () => {
738
- try {
739
- const result = await postJSON(`${API}/claims/batch/validate`, { task_ids: ids });
740
- validateCache.key = key;
741
- validateCache.parallelSafe = !!result.parallel_safe;
742
- validateCache.byId = new Map((result.results || []).map((r) => [String(r.task_id), r]));
743
- } catch (_) {
744
- // Best-effort — leave Parallel disabled-with-a-generic-title on failure
745
- // rather than risk copying a prompt for tasks we couldn't verify.
746
- validateCache.key = key;
747
- validateCache.parallelSafe = false;
748
- validateCache.byId = new Map();
749
- }
709
+ // The dry run lives in claim-action.js with the rest of the claim path; it
710
+ // is best-effort there, so a failure lands as parallelSafe:false with an
711
+ // empty map rather than a throw — Parallel stays disabled with a generic
712
+ // title instead of copying a prompt for tasks nobody verified. The
713
+ // debounce, the cache key and the re-render stay here: page furniture.
714
+ const out = await claimAction.validateMany(ids);
715
+ validateCache.key = key;
716
+ validateCache.parallelSafe = out.parallelSafe;
717
+ validateCache.byId = out.byId;
750
718
  renderBulkToolbar();
751
719
  }, 350);
752
720
  }
@@ -911,33 +879,8 @@
911
879
  const btn = $('#bulk-claim-btn');
912
880
  if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
913
881
  try {
914
- if (ids.length === 1) {
915
- await postJSON(`${API}/claims`, { task_id: ids[0], bind_session: false });
916
- toast(`You claimed #${ids[0]}. Open a terminal to start work.`);
917
- } else {
918
- await postJSON(`${API}/claims/batch`, { task_ids: ids, bind_session: false });
919
- toast(`Claimed ${ids.length} tasks. Open a terminal to start work.`);
920
- }
921
- selection.claim.clear();
922
- await refreshLive();
923
- } catch (err) {
924
- if (err && err.message === 'redirecting to auth') return;
925
- // task 1002462: same envelope defect — this tested `err.data.error === 'batch_failed'`
926
- // against an object, so an all-or-nothing batch refusal rendered as the generic toast
927
- // and the per-task reasons were dropped on the floor.
928
- if (err.status === 409 && errCode(err.data) === 'batch_failed') {
929
- // One read covers both envelopes — errDetails falls back to the body (task 1003657).
930
- const bdet = errDetails(err.data);
931
- const failures = Array.isArray(bdet.failures) ? bdet.failures : [];
932
- const lines = failures.map((f) => `#${f.task_id} (${friendlyBatchReason(f)})`);
933
- toast(lines.length
934
- ? `The batch could not be claimed — ${lines.join('; ')}.`
935
- : 'The batch could not be claimed. It was left untouched (all-or-nothing).');
936
- await refreshLive().catch(() => {});
937
- } else {
938
- toast(friendlyActionError(err.status, err.data));
939
- if (err.status === 409) await refreshLive().catch(() => {});
940
- }
882
+ const out = await claimAction.claimMany(ids);
883
+ if (out.ok) selection.claim.clear();
941
884
  } finally {
942
885
  renderBulkToolbar();
943
886
  }
@@ -952,22 +895,8 @@
952
895
  const btn = $('#bulk-release-btn');
953
896
  if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
954
897
  try {
955
- if (entries.length === 1) {
956
- await postJSON(`${API}/claims/${encodeURIComponent(entries[0][0])}/resolve`, { outcome: 'abandoned' });
957
- toast(`Released #${entries[0][1]}. It returns to the queue.`);
958
- } else {
959
- const result = await postJSON(`${API}/claims/batch/release`, { claim_ids: entries.map(([cid]) => Number(cid)) });
960
- const released = Array.isArray(result.released) ? result.released.length : 0;
961
- const failed = Array.isArray(result.failures) ? result.failures.length : 0;
962
- toast(failed
963
- ? `Released ${released} of ${entries.length} — ${failed} could not be released.`
964
- : `Released ${released} tasks. They return to the queue.`);
965
- }
966
- selection.release.clear();
967
- await refreshLive();
968
- } catch (err) {
969
- if (err && err.message === 'redirecting to auth') return;
970
- toast(friendlyActionError(err.status, err.data));
898
+ const out = await claimAction.releaseMany(entries);
899
+ if (out.ok) selection.release.clear();
971
900
  } finally {
972
901
  renderBulkToolbar();
973
902
  }
@@ -979,18 +908,10 @@
979
908
  pending.add(key);
980
909
  if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
981
910
  try {
982
- // bind_session:false — a web claim has no working session, so leave it
983
- // unbound (see claims.js resolveCreatorSessionId) and the builder can ship
984
- // it later from any of their own terminals.
985
- await postJSON(`${API}/claims`, { task_id: Number(taskId), bind_session: false });
986
- toast(`You claimed #${taskId}. Open a terminal to start work.`);
987
- await refreshLive();
988
- } catch (err) {
989
- if (err && err.message === 'redirecting to auth') return;
990
- toast(friendlyActionError(err.status, err.data));
991
- // A conflict/already-claimed means our view is stale — pull fresh truth.
992
- if (err.status === 409) await refreshLive().catch(() => {});
993
- if (btn) { btn.disabled = false; btn.textContent = 'Claim'; }
911
+ const out = await claimAction.claim(taskId);
912
+ // Leave the button disabled while the page is navigating to sign-in —
913
+ // re-enabling it invites a click that cannot land.
914
+ if (!out.ok && !out.redirected && btn) { btn.disabled = false; btn.textContent = 'Claim'; }
994
915
  } finally {
995
916
  pending.delete(key);
996
917
  }
@@ -1003,17 +924,8 @@
1003
924
  pending.add(key);
1004
925
  if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
1005
926
  try {
1006
- // No DELETE /claims/:id exists — release is resolve(outcome=abandoned),
1007
- // the same path /builder-release uses. Scoped to the caller's own claim
1008
- // server-side (NOT_YOUR_CLAIM).
1009
- await postJSON(`${API}/claims/${encodeURIComponent(claimId)}/resolve`, { outcome: 'abandoned' });
1010
- toast(`Released #${taskId}. It returns to the queue.`);
1011
- await refreshLive();
1012
- } catch (err) {
1013
- if (err && err.message === 'redirecting to auth') return;
1014
- toast(friendlyActionError(err.status, err.data));
1015
- if (err.status === 409) await refreshLive().catch(() => {});
1016
- if (btn) { btn.disabled = false; btn.textContent = 'Release'; }
927
+ const out = await claimAction.release(claimId, taskId);
928
+ if (!out.ok && !out.redirected && btn) { btn.disabled = false; btn.textContent = 'Release'; }
1017
929
  } finally {
1018
930
  pending.delete(key);
1019
931
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1044",
3
+ "version": "1.19.1045",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1044",
9
+ "version": "1.19.1045",
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.1044",
3
+ "version": "1.19.1045",
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",
@@ -7565,5 +7565,11 @@
7565
7565
  "id": "1003518",
7566
7566
  "text": "A blocker that was linked to the wrong task — a duplicate, or a mistake — can now be unlinked, and the task goes back to being claimable if nothing else was holding it. Before this the only way out was to mark the blocker reso"
7567
7567
  }
7568
+ ],
7569
+ "1.19.1045": [
7570
+ {
7571
+ "id": "1003458",
7572
+ "text": "The Tasks board and the Goals page now claim and release work through one shared piece of code instead of two copies that had already drifted apart — the same refusal was worded differently on each page, and one had lost its \""
7573
+ }
7568
7574
  ]
7569
7575
  }
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1044'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1045'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');