@bongos/core 1.19.370 → 1.19.372

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.
@@ -1,12 +1,13 @@
1
1
  # Work on a task from your dev box
2
2
 
3
- > Your dev box is **task-scoped**: claim a task and the box pulls **only the code
4
- > that task needs** — the module(s) its work touches, on top of a small base
5
- > instance — never the whole repo. Your GitHub token never leaves the server; the
6
- > box fetches its slice through the control plane. See
7
- > [ADR 0148](../adr/<redacted>.md) (the scope model) and
8
- > [ADR 0150](../adr/<redacted>.md) (how the box
9
- > comes up).
3
+ > Your dev box serves your **rank scope**: Metic+ boxes carry the **whole repo**;
4
+ > newcomer (starter) boxes carry the safe starter surface. Task-scoped slices are
5
+ > **paused** per [ADR 0193](../adr/<redacted>.md) (the
6
+ > machinery from [ADR 0148](../adr/<redacted>.md) remains;
7
+ > `BOX_TASK_SCOPE=on` re-enables it). Your GitHub token never leaves the server —
8
+ > see "How git works on a box" below, and
9
+ > [ADR 0150](../adr/<redacted>.md) for how the
10
+ > box comes up.
10
11
 
11
12
  ## TL;DR
12
13
 
@@ -15,32 +16,48 @@ In your dev box's terminal:
15
16
  ```bash
16
17
  /builder-start # see what you can claim
17
18
  /builder-claim <N> # claim task N (locks it to your session)
18
- /builder-sync # pull task N's code onto the box now (re-scopes in place)
19
+ /builder-sync # pull the latest main onto the box right now
19
20
  # …work in /workspace…
20
21
  /builder-ship # smoke + review, then land + deploy — awards credits
21
22
  ```
22
23
 
23
- That's the whole loop. `/builder-sync` is the new step: it materializes the claimed
24
- task's code within seconds instead of waiting for the box's ~10-minute refresh.
24
+ That's the whole loop. `/builder-sync` pulls fresh code within seconds instead of
25
+ waiting for the box's ~10-minute refresh.
26
+
27
+ ## How git works on a box — read this BEFORE diagnosing "stuck"
28
+
29
+ The box holds **no stored git credential, ever** — and that is the design, not a
30
+ fault ([ADR 0055](../adr/<redacted>.md)):
31
+
32
+ - **Reads are server-mediated, per request.** Every remote git operation goes
33
+ through `/usr/local/bin/otb-git-credential`, which asks the control plane
34
+ (`GET /box/source-access`) for a **short-lived, read-only** credential. The
35
+ server checks your **live** rank + status on every single request — demotion or
36
+ deactivation cuts off the next fetch instantly.
37
+ - **`git push` always fails with 403 here — by design.** The read credential
38
+ cannot write. Landing code is the server's job: `/builder-ship` uploads your
39
+ work, opens the PR, and lands it. Never try to fix the 403 by embedding a
40
+ token or editing git config.
41
+ - **Source refreshes from `main` every ~10 minutes** (the `box-source-fetch`
42
+ cron; log at `/tmp/box-source-fetch.log`). `/builder-sync` runs the same fetch
43
+ on demand.
44
+ - **If a remote git op says `could not read Username`, or the sync log shows
45
+ 401:** the box's GDS session expired. Run `/builder-reauth`, then
46
+ `/builder-sync`. That is the whole fix.
47
+
48
+ The box writes these same facts to `/workspace/CLAUDE.local.md` on every sync, so
49
+ a Claude session on the box loads them automatically (the file is gitignored and
50
+ box-written — don't edit it).
25
51
 
26
52
  ## The model — what's on your box
27
53
 
28
- A task-scoped box carries two layers:
29
-
30
- 1. **The base instance** — always present, whatever you've claimed: the methodology
31
- (`CLAUDE.md`, `.claude/`), the build tooling (`scripts/`, `src/bongos/`),
32
- `docs/`, `infra/`, and the **test suite** (`tests/`). Enough for Claude to hold
33
- context and run the `/builder-*` CLI, and to run your module's tests.
34
- 2. **Your claimed task's code** — the module directories the task's goal scopes
35
- (`goals.scope_modules` → globs, [ADR 0086](../adr/<redacted>.md)). Claim a `dev-box` task → you get
36
- `modules/dev-box/`; claim a `game` task → you get the game world too. Nothing
37
- else.
38
-
39
- What you do **not** get: other builders' modules, the product/game runtime (unless
40
- your task is game-scoped), and — the point — **no GitHub credential on the box**.
41
- The box asks the control plane (`GET /box/source-access`) for its slice on each
42
- fetch; the server holds the token and returns a scoped, cone-mode sparse-checkout
43
- spec. Lose the claim or a rank, and the next fetch is cut off at the server.
54
+ - **Metic and above:** the full repo in `/workspace` — same tree as a laptop
55
+ clone, refreshed from `main` by the sync cron.
56
+ - **Below Metic (starter):** the starter surface — methodology (`CLAUDE.md`,
57
+ `.claude/`), build tooling (`scripts/`, `src/bongos/`), `docs/`, `infra/`,
58
+ `tests/`, and the sandbox-runnable product code. Rank, checked live by the
59
+ server, is what bounds a box — not the task you claimed
60
+ ([ADR 0193](../adr/<redacted>.md)).
44
61
 
45
62
  ## Step by step
46
63
 
@@ -51,11 +68,10 @@ If you haven't yet, run **`/builder-connect`** (or follow the
51
68
  box, then **`/builder-setup`** once on the box to sign the Bongos CLI in. The browser
52
69
  terminal drops you into `/workspace` — the one working folder.
53
70
 
54
- > **`bongos shell` / `bongos code` re-scope on connect (BV1.R101).** Connecting with
55
- > either verb runs the sync for you first, so you land in `/workspace` already scoped
56
- > to your active claim — no manual `/builder-sync` needed just to arrive. (The browser
57
- > terminal drops you straight into `/workspace`; run `/builder-sync` there if you
58
- > claimed or switched since your last sync.)
71
+ > **`bongos shell` / `bongos code` sync on connect (BV1.R101).** Connecting with
72
+ > either verb runs the source fetch for you first, so you land in `/workspace`
73
+ > already fresh. (The browser terminal drops you straight into `/workspace`; run
74
+ > `/builder-sync` there if you want the latest immediately.)
59
75
 
60
76
  ### 2. See what you can claim — `/builder-start`
61
77
 
@@ -64,21 +80,18 @@ claims. Pick one.
64
80
 
65
81
  ### 3. Claim it — `/builder-claim <N>`
66
82
 
67
- Locks task N to your session. **This is what drives your box's scope:** source
68
- access is computed from your **active claim** — its task → goal → `scope_modules`.
83
+ Locks task N to your session so no parallel session collides with your work.
69
84
 
70
- ### 4. Pull the code now — `/builder-sync`
85
+ ### 4. Freshen the code — `/builder-sync`
71
86
 
72
- Re-runs the box's source-fetch immediately, so `/workspace` re-scopes to task N's
73
- modules within seconds. (A ~10-minute cron does this on its own as a backstop — you
74
- just don't want to wait for it.) Off a box there's nothing to sync — your source is
75
- your local checkout — and the command tells you so.
87
+ Re-runs the box's source fetch immediately so `/workspace` is on the current
88
+ `main` before you branch. (The ~10-minute cron does this on its own as a
89
+ backstop.) Off a box there's nothing to sync — your source is your local
90
+ checkout — and the command tells you so.
76
91
 
77
92
  ### 5. Work — in `/workspace`
78
93
 
79
- `/workspace` now holds the base instance **plus** task N's module code. One folder:
80
- when you switch tasks it **re-scopes in place**, so there are no per-task folders to
81
- manage.
94
+ One folder, always. When you switch tasks, keep working in place.
82
95
 
83
96
  ### 6. Ship — `/builder-ship`
84
97
 
@@ -90,19 +103,20 @@ One box, one working folder, one active claim at a time (the server enforces
90
103
  one-claim-per-session). To move to a different task:
91
104
 
92
105
  1. `/builder-ship` (or `/builder-release`) the current claim.
93
- 2. `/builder-claim <M>` the new one.
94
- 3. `/builder-sync` — the box re-scopes to task M in place.
106
+ 2. `/builder-claim <M>` the new one — and keep working in `/workspace`.
95
107
 
96
108
  ## Notes + gotchas
97
109
 
98
- - **`tests/` is always in the base** — so a module's tests run on the box (they
99
- live in the top-level `tests/`, not inside `modules/<key>/`).
100
- - **Idle boxes rest.** A box with no activity is reclaimed after ~10–15 min; the
101
- next connect re-provisions a fresh one and re-fetches your scope. **Uncommitted
102
- work on a reaped box is lost** — ship or commit before you step away.
110
+ - **`tests/` is on every box** — a starter box can run the suite for the code it
111
+ holds (tests live in the top-level `tests/`, not inside `modules/<key>/`).
112
+ - **Idle boxes park, they don't die.** After ~2 hours of no activity the box is
113
+ **parked**: a snapshot is kept and the droplet is destroyed, so compute billing
114
+ stops and a wake restores your filesystem. Only weeks of staying parked lead to
115
+ reclaim (with a warning first). Still: **ship or commit before you step away** —
116
+ a snapshot is a safety net, not a workflow.
103
117
  - **`/builder-sync` is idempotent** — safe to re-run; it just does, on demand, what
104
118
  the cron does on its schedule.
105
- - **A fresh box comes up with the base already cloned.** If a box ever comes up
119
+ - **A fresh box comes up with the code already cloned.** If a box ever comes up
106
120
  *bare* (no code, no terminal), that's the packaging bug
107
121
  [ADR 0150](../adr/<redacted>.md) fixed on the
108
122
  control plane — flag it rather than working around it.
@@ -269,11 +269,42 @@
269
269
  return (data && typeof data === 'object') ? data : {};
270
270
  }
271
271
 
272
+ // task 1003276 (goal 1000072, criterion legibility-categories) — a work
273
+ // CATEGORY as a chip. Lives HERE, not in goals-lib.js, because three surfaces
274
+ // render it — the Goals page, the Tasks board and the task page — and
275
+ // dom-utils.js is the only script all three load. One renderer means the
276
+ // goal's chip and the task's inherited chip cannot drift.
277
+ //
278
+ // A LABEL and nothing more: no href, no pointer affordance. A per-category
279
+ // destination would be a navigation tier, and that is precisely how an
280
+ // advisory word becomes the second hierarchy in practice even when no code
281
+ // gates on it (ADR 0192 — a category authorises nothing).
282
+ //
283
+ // `inherited` marks a TASK's chip. A task stores no category; it reads its
284
+ // goal's through goal_id, and saying so out loud stops anyone hunting for a
285
+ // per-task editor that does not exist.
286
+ //
287
+ // Uncategorised renders NOTHING rather than an "Uncategorised" pill: most
288
+ // goals will carry one, and a board of grey placeholders reads worse than a
289
+ // board with gaps. The Category filter's "not categorised yet" option is how
290
+ // you find them.
291
+ function categoryChip(category, opts) {
292
+ const label = category && category.label ? String(category.label).trim() : '';
293
+ if (!label) return '';
294
+ const inherited = !!(opts && opts.inherited);
295
+ const title = inherited
296
+ ? `Category: ${label} — inherited from this task's goal (a task carries no category of its own)`
297
+ : `Category: ${label}`;
298
+ return `<span class="gr-cat${inherited ? ' gr-cat--inherited' : ''}" title="${escapeHtml(title)}">`
299
+ + `${escapeHtml(label)}${inherited ? '<i class="gr-cat__from" aria-hidden="true">via goal</i>' : ''}</span>`;
300
+ }
301
+
272
302
  window.OTB = {
273
303
  escapeHtml, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn,
274
304
  errCode, errMessage, errDetails, // task 1002462 — ADR 0116 envelope readers
275
305
  contributeSettingsPanel, settingsPanels,
276
306
  contributeHallWidget, hallWidgets, hallWidgetIds,
277
307
  splitCriterion,
308
+ categoryChip, // task 1003276 — shared by the Goals page, the Tasks board and the task page
278
309
  };
279
310
  })();
@@ -228,6 +228,34 @@
228
228
  }
229
229
  }
230
230
 
231
+ // task 1003276 — the Category dropdown. Unlike Version and Owner (derived from
232
+ // the loaded goals), the options come from the project's SET — GET
233
+ // /work-categories — so a category nobody has used yet is still offered, and a
234
+ // client never hard-codes the labels ("present in every builders hall on any
235
+ // project" only holds if the vocabulary travels over the wire). The 'none'
236
+ // option finds the uncategorised, which is the set this feature exists to
237
+ // shrink. Best-effort: a failed fetch leaves the control with just "— any —".
238
+ async function populateCategoryFilter() {
239
+ const sel = $('#f-goal-category');
240
+ if (!sel) return;
241
+ let cats = [];
242
+ try {
243
+ cats = (await getJSON(`${API}/work-categories`))?.categories || [];
244
+ } catch (_) { return; }
245
+ for (const c of cats) {
246
+ const opt = document.createElement('option');
247
+ opt.value = c.slug;
248
+ opt.textContent = c.label;
249
+ sel.appendChild(opt);
250
+ }
251
+ if (cats.length) {
252
+ const opt = document.createElement('option');
253
+ opt.value = 'none';
254
+ opt.textContent = 'not categorised yet';
255
+ sel.appendChild(opt);
256
+ }
257
+ }
258
+
231
259
  // Archon check for the detail view's manage/visibility affordances (the
232
260
  // server-side rank gates are the real enforcement). The task-1760 drag-reorder
233
261
  // affordance this once also served is retired — see the note where
@@ -797,6 +825,9 @@
797
825
  // task 1003135 — fire-and-forget: the Owner options fill when the (cached)
798
826
  // directory resolves; boot never waits on a label lookup.
799
827
  populateOwnerFilter().catch(() => {});
828
+ // task 1003276 — same fire-and-forget posture: boot never waits on the
829
+ // category vocabulary, and the board renders fine before it lands.
830
+ populateCategoryFilter().catch(() => {});
800
831
  // Prefetch details (ring + members on the cards). Sequentially-parallel,
801
832
  // failure-tolerant: a card without detail still renders.
802
833
  // task 1002439 — also fold in per-goal task-readiness rollups (one request
@@ -859,6 +890,9 @@
859
890
  { id: 'f-goal-status', label: 'Status', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
860
891
  { id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
861
892
  { id: 'f-goal-owner', label: 'Owner', options: [{ value: '', label: '— any —' }] },
893
+ // task 1003276 — filled from GET /work-categories after boot
894
+ // (populateCategoryFilter), the populateOwnerFilter pattern.
895
+ { id: 'f-goal-category', label: 'Category', options: [{ value: '', label: '— any —' }] },
862
896
  { id: 'f-goal-order',
863
897
  label: 'Sort',
864
898
  value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
@@ -19,7 +19,7 @@
19
19
 
20
20
  window.OTBGoalsRender = function createGoalsRender(ctx) {
21
21
  const { API, curLabel, getJSON, patchJSON, postJSON, sameId, selfId, state } = ctx;
22
- const { escapeHtml, $, fmtNum, fmtDate, toast, rankLabel } = window.OTB;
22
+ const { escapeHtml, $, fmtNum, fmtDate, toast, rankLabel, categoryChip } = window.OTB;
23
23
  const {
24
24
  priorityRank, touchesConflict, rankTagHtml, deHashRefs,
25
25
  creditsPerMinute, buildPromptForTask, friendlyBatchReason,
@@ -81,11 +81,18 @@
81
81
  const q = ($('#f-goals-search')?.value || '').trim().toLowerCase();
82
82
  const needle = q.replace(/^#/, '');
83
83
  const owner = $('#f-goal-owner')?.value || '';
84
+ // task 1003276 — the Category dropdown. Matches on the SLUG (the stable
85
+ // machine key) so re-wording a label never breaks a filter, and 'none' is
86
+ // the uncategorised set — which must be findable, because "what has nobody
87
+ // placed yet" is exactly the prompt this feature is built around.
88
+ const cat = $('#f-goal-category')?.value || '';
84
89
  const matchesSearch = (g) => !needle
85
90
  || String(g.id).includes(needle)
86
91
  || (g.title || '').toLowerCase().includes(needle);
87
92
  const matchesOwner = (g) => !owner
88
93
  || (owner === 'none' ? g.created_by == null : String(g.created_by) === owner);
94
+ const matchesCategory = (g) => !cat
95
+ || (cat === 'none' ? !g.category : (g.category?.slug === cat));
89
96
  const myId = selfId();
90
97
  const iAmMember = (g) => {
91
98
  const d = state.details.get(String(g.id));
@@ -96,6 +103,7 @@
96
103
  (!status || g.status === status) &&
97
104
  matchesSearch(g) &&
98
105
  matchesOwner(g) &&
106
+ matchesCategory(g) &&
99
107
  (!mineOnly || iAmMember(g)));
100
108
  }
101
109
  function goalStampHtml(g) {
@@ -127,10 +135,13 @@
127
135
  right = `<span class="gr-quiet">No tasks yet</span>`;
128
136
  }
129
137
  const metaHtml = `#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}${goalStampHtml(g)}`;
130
- return { r, achieved, ringHtml, goalHref, right, metaHtml };
138
+ // task 1003276 — the category chip. Computed here with the rest of the facts
139
+ // so the row and the card (task 1003136) cannot drift on it.
140
+ const catHtml = categoryChip(g.category);
141
+ return { r, achieved, ringHtml, goalHref, right, metaHtml, catHtml };
131
142
  }
132
143
  function goalRowHtml(g) {
133
- const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 44);
144
+ const { r, achieved, ringHtml, goalHref, right, metaHtml, catHtml } = goalRenderFacts(g, 44);
134
145
  // NOTE: a rollup row nests real links (title → goal, Claim → task overlay,
135
146
  // drill → goal), so the row itself is a <div>, not an <a> — nested anchors
136
147
  // are invalid. Only the explicit links navigate.
@@ -138,20 +149,20 @@
138
149
  <div class="gr-ringcol">${ringHtml}</div>
139
150
  <div class="gr-main">
140
151
  <a class="gr-title" href="${goalHref}"${subtitleAttr(g)}>${escapeHtml(g.title || 'Untitled goal')}</a>
141
- <div class="gr-meta">${metaHtml}</div>
152
+ <div class="gr-meta">${metaHtml}${catHtml}</div>
142
153
  ${achieved ? '' : goalRollLine(r)}
143
154
  </div>
144
155
  <div class="gr-right">${right}</div>
145
156
  </div>`;
146
157
  }
147
158
  function goalCardHtml(g) {
148
- const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 36);
159
+ const { r, achieved, ringHtml, goalHref, right, metaHtml, catHtml } = goalRenderFacts(g, 36);
149
160
  return `<div class="goal-card" data-goal-id="${escapeHtml(String(g.id))}">
150
161
  <div class="goal-card__head">
151
162
  <div class="gr-ringcol">${ringHtml}</div>
152
163
  <a class="goal-card__title gr-title" href="${goalHref}"${subtitleAttr(g)}>${escapeHtml(g.title || 'Untitled goal')}</a>
153
164
  </div>
154
- <div class="goal-card__meta">${metaHtml}</div>
165
+ <div class="goal-card__meta">${metaHtml}${catHtml}</div>
155
166
  ${achieved ? '' : goalRollLine(r)}
156
167
  <div class="goal-card__foot">${right}</div>
157
168
  </div>`;
@@ -709,7 +720,10 @@
709
720
  // still never re-derives the protected roster.
710
721
  const gateSpan = g.scope_protected
711
722
  ? '<span class="goal-badge goal-badge--locked" title="Protected module scope — join by request; an Archon admits members">🛡 Archon-mediated</span>' : '';
712
- meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${lockSpan} ${gateSpan} ${scope} ${depsSpan}`;
723
+ // task 1003276 — the human category, beside the machine-readable module
724
+ // scope so the difference is visible: `scope` is the wall that refuses work
725
+ // (ADR 0086); the chip is a word for a person and gates nothing (ADR 0192).
726
+ meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${categoryChip(g.category)} ${lockSpan} ${gateSpan} ${scope} ${depsSpan}`;
713
727
  }
714
728
 
715
729
  // task 1762: the goal's own owner/manager (or an Archon) can invite/manage
@@ -2172,3 +2172,12 @@ html.palette-open { overflow: hidden; }
2172
2172
  .palette__panel { margin-top: 6vh; }
2173
2173
  .palette__group { min-width: 56px; }
2174
2174
  }
2175
+ /* task 1003276 — the human CATEGORY chip. Quiet by design: it sits beside the
2176
+ meta line, not above the title, because it is a label a person scans, never a
2177
+ status or a permission (ADR 0192). No hover/pointer affordance and no href —
2178
+ a per-category destination would be a navigation tier, which is exactly the
2179
+ second hierarchy this feature must not become. On a TASK the chip is the one
2180
+ inherited from its goal, and says so, so nobody hunts for a per-task editor. */
2181
+ .gr-cat { display: inline-flex; align-items: baseline; gap: 5px; margin-left: 8px; padding: 1px 8px; border: 1px solid var(--rule-soft); border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: .01em; color: var(--ink-soft); background: var(--surface-2); vertical-align: baseline; white-space: nowrap; }
2182
+ .gr-cat--inherited { font-weight: 550; color: var(--ink-faint); }
2183
+ .gr-cat__from { font-style: normal; font-weight: 500; font-size: 10px; color: var(--ink-faint); opacity: .85; }
@@ -193,7 +193,7 @@
193
193
  `Task description:\n${desc}`;
194
194
  }
195
195
 
196
- function renderTask(t, lastClaim, viewer) {
196
+ function renderTask(t, lastClaim, viewer, goal) {
197
197
  document.title = `#${t.id} — ${t.title || 'Task'} — ${document.title.split(' — ').pop()}`;
198
198
  const sub = $('#task-sub');
199
199
  if (sub) sub.textContent = `Task #${t.id}`;
@@ -222,7 +222,14 @@
222
222
  }
223
223
 
224
224
  if (t.goal_id) {
225
- parts.push(`<p class="td-goal">Goal: <a href="/goals#/goal/${encodeURIComponent(t.goal_id)}">Goal #${escapeHtml(String(t.goal_id))}</a></p>`);
225
+ // task 1003276 — the goal line names the goal and carries the category this
226
+ // task INHERITS from it. Two legibility fixes in one line: the link used to
227
+ // read "Goal #1000072", which tells a person nothing they didn't already
228
+ // know, and the chip answers "what kind of work is this" without opening
229
+ // the goal. Marked inherited because a task carries no category of its own.
230
+ const goalName = goal && goal.title ? ` — ${escapeHtml(goal.title)}` : '';
231
+ const catChip = window.OTB.categoryChip(goal && goal.category, { inherited: true });
232
+ parts.push(`<p class="td-goal">Goal: <a href="/goals#/goal/${encodeURIComponent(t.goal_id)}">Goal #${escapeHtml(String(t.goal_id))}</a>${goalName}${catChip}</p>`);
226
233
  }
227
234
 
228
235
  // Claim history (task 751 — best-effort: the API surfaces the most recent
@@ -356,7 +363,17 @@
356
363
  const me = await getJSON(`${API}/me`, { softAuth: true });
357
364
  viewer = (me && me.builder) ? me.builder : null;
358
365
  } catch (_) { /* cosmetic — the server enforces regardless */ }
359
- renderTask(d.task, d.last_claim || null, viewer);
366
+ // task 1003276 — the task's goal, for its name and the category the task
367
+ // inherits. Best-effort and non-blocking on failure: the page has always
368
+ // rendered without it, and a category is a nicety, never a prerequisite.
369
+ let goal = null;
370
+ if (d.task.goal_id != null) {
371
+ try {
372
+ const g = await getJSON(`${API}/goals/${encodeURIComponent(d.task.goal_id)}`, { softAuth: true });
373
+ goal = (g && g.goal) ? g.goal : null;
374
+ } catch (_) { /* cosmetic — the goal link still resolves without it */ }
375
+ }
376
+ renderTask(d.task, d.last_claim || null, viewer, goal);
360
377
  } catch (err) {
361
378
  if (err && err.code === AUTH_NEEDED) { toAuth(); return; }
362
379
  if (err && err.status === 404) { renderNotFound(id); return; }
@@ -337,6 +337,12 @@
337
337
  // task 1002955: how old is this task? A ready queue reads as equally fresh
338
338
  // without it; the exact date rides the tooltip.
339
339
  const ageChip = window.OTBKit.ageChipHtml(t.created_at);
340
+ // task 1003276 — the category this task inherits from its goal, marked as
341
+ // inherited so nobody looks for a per-task editor that does not exist. It
342
+ // is a label only: it never filters or orders this board, because nothing
343
+ // a person may not act on should decide what the queue offers (ADR 0192).
344
+ const catChip = window.OTB.categoryChip(
345
+ (taskState.goalCategories || {})[String(t.goal_id)], { inherited: true });
340
346
  const selected = selection.claim.has(String(t.id));
341
347
  return `
342
348
  <div class="task${selected ? ' task--selected' : ''}" data-task-id="${t.id}">
@@ -358,6 +364,7 @@
358
364
  <span>${cpmText}</span>
359
365
  ${touchesChip}
360
366
  ${ageChip}
367
+ ${catChip}
361
368
  </div>
362
369
  </div>
363
370
  <div class="task__credits">
@@ -1071,9 +1078,17 @@
1071
1078
 
1072
1079
  // task 1755: goal id → title map for the Goal filter (best-effort).
1073
1080
  taskState.goalTitles = {};
1081
+ // task 1003276 — a task INHERITS its goal's category (it stores none of its
1082
+ // own), so the board resolves the label the same way the schema does: through
1083
+ // goal_id. The goals fetch below is already made for the titles, so the chip
1084
+ // costs no extra request and no server change.
1085
+ taskState.goalCategories = {};
1074
1086
  try {
1075
1087
  const gr = await getJSON(`${API}/goals`);
1076
- (gr.goals || []).forEach((g) => { taskState.goalTitles[g.id] = g.title; });
1088
+ (gr.goals || []).forEach((g) => {
1089
+ taskState.goalTitles[g.id] = g.title;
1090
+ taskState.goalCategories[g.id] = g.category || null;
1091
+ });
1077
1092
  } catch (_) { /* filter labels fall back to 'Goal #id' */ }
1078
1093
 
1079
1094
  // Claimable + active claims in one call. A 403 means the primer gate — show
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.370",
3
+ "version": "1.19.372",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.370",
9
+ "version": "1.19.372",
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.370",
3
+ "version": "1.19.372",
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/src/module-api.js CHANGED
@@ -1170,8 +1170,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1170
1170
  // landed since 1.19.368 with no explicit bump. run 32903289072. (task 1002620)
1171
1171
  // 1.19.370 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1172
1172
  // landed since 1.19.369 with no explicit bump. run 32904231221. (task 1002620)
1173
+ // 1.19.371 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1174
+ // landed since 1.19.370 with no explicit bump. run 32904582252. (task 1002620)
1175
+ // 1.19.372 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1176
+ // landed since 1.19.371 with no explicit bump. run 32905166786. (task 1002620)
1173
1177
  // ---------------------------------------------------------------------------
1174
- const CORE_VERSION = '1.19.370'; // CI auto-patch carrier (ADR 0161); see changelog above
1178
+ const CORE_VERSION = '1.19.372'; // CI auto-patch carrier (ADR 0161); see changelog above
1175
1179
 
1176
1180
  // A namespaced logger so a module's log lines are attributable + consistent.
1177
1181
  // Usage: const log = api.logger('dev-box'); log.info('mounted');