@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.
- package/.bongos-core.json +25 -20
- package/docs/copy-inventory.md +134 -132
- package/docs/copy-registry.json +180 -162
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +63 -49
- package/modules/hall-ui/public/dom-utils.js +31 -0
- package/modules/hall-ui/public/goals-page.js +34 -0
- package/modules/hall-ui/public/goals-render.js +21 -7
- package/modules/hall-ui/public/style.css +9 -0
- package/modules/hall-ui/public/task.js +20 -3
- package/modules/hall-ui/public/work.js +16 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/goal_category_ui.mjs +279 -0
- package/tests/goal_criterion_confirm_ui.mjs +6 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Work on a task from your dev box
|
|
2
2
|
|
|
3
|
-
> Your dev box
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
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
|
|
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`
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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`
|
|
55
|
-
> either verb runs the
|
|
56
|
-
>
|
|
57
|
-
>
|
|
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
|
|
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.
|
|
85
|
+
### 4. Freshen the code — `/builder-sync`
|
|
71
86
|
|
|
72
|
-
Re-runs the box's source
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
99
|
-
live in the top-level `tests/`, not inside `modules/<key>/`).
|
|
100
|
-
- **Idle boxes
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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');
|