@bongos/core 1.19.164 → 1.19.166

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.164",
6
- "core_contract": "1.19.164",
7
- "source_commit": "779753af3493b89014e91088a53931afe771ec00",
5
+ "core_version": "1.19.166",
6
+ "core_contract": "1.19.166",
7
+ "source_commit": "f0915c151ca43c0539146b5538ab7e5232c0340d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-16T17:05:29.044Z",
9
+ "built_at": "2026-08-16T17:20:37.651Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 305,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1263,
14
+ "functional_verbatim": 1264,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1589,
20
- "tree_sha256": "ea5a8e73501fb69d4e1d6e808d63a241df8b2ccf76728a49282a328b14b63337",
19
+ "file_count": 1590,
20
+ "tree_sha256": "66c7fea0b4a3bc4ad100fbc357028f28c0445c73227a53a90f842938c6e059f8",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -3167,12 +3167,12 @@
3167
3167
  {
3168
3168
  "path": "modules/hall-ui/public/goals-page.js",
3169
3169
  "mode": "0000644",
3170
- "sha256": "f24cfdca48676d882cfa51754fcf1519ade4646a760cb2be1db71fbc27571bca"
3170
+ "sha256": "7e67230de060927c37ab8680e397c994896250c9e027e920ac997a61edbd14aa"
3171
3171
  },
3172
3172
  {
3173
3173
  "path": "modules/hall-ui/public/goals.css",
3174
3174
  "mode": "0000644",
3175
- "sha256": "c52de393285c8ba08494799aff501b947635ccbee93c80777e961b78e2a870c5"
3175
+ "sha256": "407e8f21b95faa725d37c5336986d00af734db351bdaaf58aac350114c58f172"
3176
3176
  },
3177
3177
  {
3178
3178
  "path": "modules/hall-ui/public/goals.html",
@@ -4202,12 +4202,12 @@
4202
4202
  {
4203
4203
  "path": "package-lock.json",
4204
4204
  "mode": "0000644",
4205
- "sha256": "098af85c69e5a9f9aa4dcfd69292d9bbdecfa6bb4294a344740568344fd36673"
4205
+ "sha256": "99da59c846c54ee93a5cc36d2dd760afd344905469a9a6368388de10ee02c506"
4206
4206
  },
4207
4207
  {
4208
4208
  "path": "package.json",
4209
4209
  "mode": "0000644",
4210
- "sha256": "6efe95b7773147ac8801e5d03fe75b7143f1eda728d123dfa6a19dd41deb5f27"
4210
+ "sha256": "0f77d032953f120415ec207ecc3827fb00156aec357f542614445ed644c4b4a8"
4211
4211
  },
4212
4212
  {
4213
4213
  "path": "public-docs/index.html",
@@ -5259,6 +5259,11 @@
5259
5259
  "mode": "0000644",
5260
5260
  "sha256": "5de4203d6a764c8aec374c7802f69f659816d2c0d75bbe70144f7dfb56df2e39"
5261
5261
  },
5262
+ {
5263
+ "path": "scripts/gds/seed-bv1-privacy-invites-tasks.js",
5264
+ "mode": "0000644",
5265
+ "sha256": "bab0c429d0d194cc5e90bc09d59c338cf85143e0788bc3071f94297982c9a5fe"
5266
+ },
5262
5267
  {
5263
5268
  "path": "scripts/gds/seed-bv1-profile-page-tasks.js",
5264
5269
  "mode": "0000644",
@@ -5802,7 +5807,7 @@
5802
5807
  {
5803
5808
  "path": "src/module-api.js",
5804
5809
  "mode": "0000644",
5805
- "sha256": "575799bb117e5a9166aabc3abbba42f9b737fda008bc08e1461147a57831683a"
5810
+ "sha256": "b83c5398b0e936e23705d50233c269ede9f36ea85f3c7b98111fbc501fd6d39a"
5806
5811
  },
5807
5812
  {
5808
5813
  "path": "src/module-loader/catalog.js",
@@ -91,6 +91,12 @@
91
91
  // from GET /tasks?version=…&include=deps. Empty on an older server (the
92
92
  // include=deps enrichment 404s gracefully → the list falls back to plain cards).
93
93
  rollups: new Map(),
94
+ // task 1002965 — goal_id → ms of the LAST WORK DONE on the goal: the newest
95
+ // activity timestamp across its tasks. Filled by loadRollups from the same
96
+ // per-version task fetch the readiness rollups ride on, so the "recently
97
+ // active" sort costs no extra request. A goal with no tasks is absent here
98
+ // and falls back to its own goals.updated_at (see goalActivityMs).
99
+ activity: new Map(),
94
100
  };
95
101
 
96
102
  async function getJSON(path) {
@@ -174,6 +180,39 @@
174
180
  const EMPTY_ROLLUP = { done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, total: 0, nextReady: null };
175
181
  const rollupOf = (goalId) => state.rollups.get(String(goalId)) || EMPTY_ROLLUP;
176
182
 
183
+ // ---- goal ordering (task 1002965) -----------------------------------------
184
+ // The list is ordered by the Archon's within-version priority (sort_order,
185
+ // task 1760) unless the builder picks a date sort. Two dates are offered:
186
+ // created — goals.created_at, straight off GET /goals (GOAL_COLS)
187
+ // activity — the LAST WORK DONE on the goal: the newest task-activity
188
+ // timestamp under it (state.activity, filled by loadRollups),
189
+ // falling back to the goal row's own updated_at when the goal
190
+ // has no tasks at all, so a task-less goal still orders sanely
191
+ // instead of sinking to the bottom forever.
192
+ const msOf = (iso) => { const t = iso ? Date.parse(iso) : NaN; return Number.isNaN(t) ? 0 : t; };
193
+ const taskActivityMs = (t) => Math.max(msOf(t.updated_at), msOf(t.shipped_at));
194
+ const goalCreatedMs = (g) => msOf(g.created_at);
195
+ const goalActivityMs = (g) => state.activity.get(String(g.id)) || msOf(g.updated_at);
196
+
197
+ // value → { label, cmp, stamp } — `stamp` is what the row's meta line shows
198
+ // for that sort (null = show nothing extra, the priority default).
199
+ const GOAL_SORTS = {
200
+ priority: { label: 'priority (version order)', cmp: null, stamp: null },
201
+ newest: { label: 'newest first', cmp: (a, b) => goalCreatedMs(b) - goalCreatedMs(a), stamp: (g) => ({ label: 'created', ms: goalCreatedMs(g) }) },
202
+ oldest: { label: 'oldest first', cmp: (a, b) => goalCreatedMs(a) - goalCreatedMs(b), stamp: (g) => ({ label: 'created', ms: goalCreatedMs(g) }) },
203
+ active: { label: 'recently active', cmp: (a, b) => goalActivityMs(b) - goalActivityMs(a), stamp: (g) => ({ label: 'last work', ms: goalActivityMs(g) }) },
204
+ };
205
+ const GOAL_SORT_PREF = 'otb.goals.sort';
206
+ const currentGoalSort = () => GOAL_SORTS[$('#f-goal-order')?.value] || GOAL_SORTS.priority;
207
+
208
+ // Stable: filteredGoals already returns the server's priority order, and
209
+ // Array#sort is stable in every browser this hall supports — so goals sharing
210
+ // a timestamp (or both missing one) keep their priority order as the tiebreak.
211
+ function sortGoals(rows) {
212
+ const { cmp } = currentGoalSort();
213
+ return cmp ? rows.slice().sort(cmp) : rows;
214
+ }
215
+
177
216
  // Fetch every task for the versions the goals span (one request each, with dep
178
217
  // summaries) and fold into per-goal rollups. Failure-tolerant: an older server
179
218
  // that doesn't honor include=deps just yields empty rollups and the list falls
@@ -185,6 +224,7 @@
185
224
  getJSON(`${API}/tasks?version=${encodeURIComponent(v)}&include=deps`)
186
225
  .then((r) => r.tasks || []).catch(() => [])));
187
226
  const roll = new Map();
227
+ const activity = new Map();
188
228
  for (const t of lists.flat()) {
189
229
  if (t.goal_id == null) continue;
190
230
  const key = String(t.goal_id);
@@ -194,8 +234,16 @@
194
234
  r[bucket] += 1;
195
235
  if (bucket !== 'cut') r.total += 1;
196
236
  if (bucket === 'ready' && !r.nextReady) r.nextReady = t;
237
+ // task 1002965 — ANY task activity counts (owner call), not just ships: a
238
+ // goal being worked right now should read as active before anything lands.
239
+ // updated_at moves on every task write (claim, status flip, edit), so it
240
+ // dominates in practice; shipped_at is folded in because a task can be
241
+ // shipped by a path that stamps it without touching updated_at.
242
+ const ts = taskActivityMs(t);
243
+ if (ts && ts > (activity.get(key) || 0)) activity.set(key, ts);
197
244
  }
198
245
  state.rollups = roll;
246
+ state.activity = activity;
199
247
  }
200
248
 
201
249
  /* ---------------- list view ---------------- */
@@ -263,6 +311,16 @@
263
311
  // task 1002439 — one goal, rendered as a rollup row: criteria ring · title +
264
312
  // meta + readiness rollup · one next action (claim the next ready task, drill
265
313
  // into what's blocking, or a review/achieved note).
314
+ // task 1002965 — under a date sort, the row states the date it was ordered on
315
+ // ("created Aug 4" / "last work Aug 16"); under the priority default it shows
316
+ // nothing extra, so the row is unchanged for anyone who never touches Sort.
317
+ function goalStampHtml(g) {
318
+ const { stamp } = currentGoalSort();
319
+ if (!stamp) return '';
320
+ const { label, ms } = stamp(g);
321
+ return ` · ${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}`;
322
+ }
323
+
266
324
  function goalRowHtml(g) {
267
325
  const d = state.details.get(String(g.id));
268
326
  const crit = d ? (d.criteria || []) : [];
@@ -292,7 +350,7 @@
292
350
  <div class="gr-ringcol">${ringHtml}</div>
293
351
  <div class="gr-main">
294
352
  <a class="gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
295
- <div class="gr-meta">#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}</div>
353
+ <div class="gr-meta">#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}${goalStampHtml(g)}</div>
296
354
  ${achieved ? '' : goalRollLine(r)}
297
355
  </div>
298
356
  <div class="gr-right">${right}</div>
@@ -323,11 +381,14 @@
323
381
  const r = rollupOf(g.id);
324
382
  return g.status === 'open' && r.ready === 0 && r.active === 0 && r.blocked > 0;
325
383
  };
326
- const achieved = rows.filter((g) => g.status === 'achieved');
327
- const archived = rows.filter((g) => g.status === 'archived');
384
+ // task 1002965 — the Sort choice orders goals WITHIN each group; the
385
+ // Active / Blocked / Achieved / Archived grouping (task 1002439's momentum
386
+ // layout) is what the page is for and is never re-ordered away.
387
+ const achieved = sortGoals(rows.filter((g) => g.status === 'achieved'));
388
+ const archived = sortGoals(rows.filter((g) => g.status === 'archived'));
328
389
  const openRows = rows.filter((g) => g.status !== 'achieved' && g.status !== 'archived');
329
- const blocked = openRows.filter(isBlocked);
330
- const active = openRows.filter((g) => !isBlocked(g));
390
+ const blocked = sortGoals(openRows.filter(isBlocked));
391
+ const active = sortGoals(openRows.filter((g) => !isBlocked(g)));
331
392
 
332
393
  let R = 0, A = 0, B = 0;
333
394
  openRows.forEach((g) => { const r = rollupOf(g.id); R += r.ready; A += r.active; B += r.blocked; });
@@ -339,7 +400,16 @@
339
400
 
340
401
  const sec = (label, n) => `<div class="gr-sec"><b>${label}</b><span class="gr-sec__n">${n}</span></div>`;
341
402
  const listOf = (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
342
- const pill = (g) => `<a class="gr-pill" href="#/goal/${escapeHtml(String(g.id))}"><span class="gr-pill__ok">✓</span><b>${escapeHtml(g.title || 'Untitled')}</b><span class="gr-pill__id">#${escapeHtml(String(g.id))}</span></a>`;
403
+ // task 1002965 — an achieved/archived goal is a compact pill, so its sort
404
+ // date rides as its own muted span (the row's `gr-meta` equivalent). The
405
+ // pills row already wraps, so the extra text can't widen the page.
406
+ const pillWhen = (g) => {
407
+ const { stamp } = currentGoalSort();
408
+ if (!stamp) return '';
409
+ const { label, ms } = stamp(g);
410
+ return `<span class="gr-pill__when">${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}</span>`;
411
+ };
412
+ const pill = (g) => `<a class="gr-pill" href="#/goal/${escapeHtml(String(g.id))}"><span class="gr-pill__ok">✓</span><b>${escapeHtml(g.title || 'Untitled')}</b><span class="gr-pill__id">#${escapeHtml(String(g.id))}</span>${pillWhen(g)}</a>`;
343
413
 
344
414
  grid.innerHTML = summary
345
415
  + (active.length ? sec('Active', active.length) + listOf(active) : '')
@@ -1810,17 +1880,34 @@
1810
1880
  }
1811
1881
 
1812
1882
  // task 1932 — any list filter change re-paginates the grid from page 1.
1813
- const onListFilterChange = () => { pagers.list.page = 1; renderList(); };
1883
+ // task 1002965 — and the Sort choice is remembered, so a builder who prefers
1884
+ // "recently active" doesn't re-pick it on every visit. localStorage (not the
1885
+ // kit's `url` mode): this page owns the #/goal/N hash route and its own boot
1886
+ // reads it, so keeping the pref off the URL leaves deep links shareable and
1887
+ // unchanged. Private-mode writes throw — swallowed, the pref is a nicety.
1888
+ const onListFilterChange = () => {
1889
+ try { window.localStorage.setItem(GOAL_SORT_PREF, $('#f-goal-order')?.value || 'priority'); } catch (_) { /* private mode */ }
1890
+ pagers.list.page = 1;
1891
+ renderList();
1892
+ };
1814
1893
  // task 2171 — the list filter bar is a kit filterBar: Version / Status /
1815
1894
  // Membership, all primary (no overflow needed). Control ids are unchanged
1816
1895
  // (f-goal-version, f-goal-status, f-goal-mine) so filteredGoals' reads +
1817
1896
  // populateVersionFilter's dynamic option fill keep working untouched.
1897
+ // task 1002965 adds Sort (f-goal-order — NOT f-goal-sort, which is already
1898
+ // taken by the goal-detail To-do bar further down this file).
1899
+ let savedGoalSort = null;
1900
+ try { savedGoalSort = window.localStorage.getItem(GOAL_SORT_PREF); } catch (_) { /* private mode */ }
1818
1901
  makeFilterBar({
1819
1902
  mount: $('#goals-filters'),
1820
1903
  primary: [
1821
1904
  { id: 'f-goal-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
1822
1905
  { id: 'f-goal-status', label: 'Status', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
1823
1906
  { id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
1907
+ { id: 'f-goal-order',
1908
+ label: 'Sort',
1909
+ value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
1910
+ options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
1824
1911
  ],
1825
1912
  onChange: onListFilterChange,
1826
1913
  });
@@ -251,6 +251,9 @@ a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); b
251
251
  .gr-pill b { color: var(--ink); font-weight: 600; }
252
252
  .gr-pill__ok { width: 17px; height: 17px; border-radius: 50%; background: var(--ok-weak); color: var(--ok); display: grid; place-items: center; font-size: 10px; font-weight: 800; }
253
253
  .gr-pill__id { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
254
+ /* task 1002965 — the date an achieved/archived pill was ordered on, shown only
255
+ while a date sort is active (the pill's counterpart to .gr-meta). */
256
+ .gr-pill__when { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
254
257
  .gr-pills--muted { opacity: .6; }
255
258
 
256
259
  @media (max-width: 640px) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.164",
3
+ "version": "1.19.166",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.164",
9
+ "version": "1.19.166",
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.164",
3
+ "version": "1.19.166",
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",
@@ -0,0 +1,133 @@
1
+ // Seed the privacy-model + invite-inbox build tasks the 1002785 spike scoped,
2
+ // under goal 1000045 "Public builder profiles" (BONGOS-V1, internal).
3
+ //
4
+ // Source of truth: docs/specs/bongos-v1-privacy-and-invites.md (owner-signed
5
+ // 2026-08-16 on the sliver + expiry decisions). Idempotent on source_ref
6
+ // bv1-privacy-PVnn; safe to re-run. PV4 stays backlog deliberately — its
7
+ // instance-invite half attaches to the console wave's R2/R6 (spike 1002786),
8
+ // which seeds into its own goal; only PV6 is promoted (fully independent).
9
+ //
10
+ // Run: node scripts/gds/seed-bv1-privacy-invites-tasks.js
11
+
12
+ const { apiCall } = require('./cli-lib');
13
+
14
+ const VER = 'BONGOS-V1';
15
+ const GOAL = 1000045;
16
+ const SRC = 'planning-session';
17
+ const SPEC = 'docs/specs/bongos-v1-privacy-and-invites.md';
18
+
19
+ // Existing tasks referenced by id (seed-bv1-profiles-tasks.js).
20
+ const R02 = 1002278, R03 = 1002279;
21
+
22
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
23
+
24
+ // Throttled + 429-aware wrapper (same shape as the sibling seeders).
25
+ async function api(method, path, body, _tries = 0) {
26
+ await sleep(1500);
27
+ const r = await apiCall(method, `/api/gds${path}`, body);
28
+ if (r.status === 429 && _tries < 8) {
29
+ const wait = (r.data?.error?.details?.retry_after_seconds || 30) + 1;
30
+ console.log(` …rate-limited, waiting ${wait}s`);
31
+ await sleep(wait * 1000);
32
+ return api(method, path, body, _tries + 1);
33
+ }
34
+ return r;
35
+ }
36
+
37
+ const NEW_TASKS = [
38
+ {
39
+ ref: 'PV1', kind: 'feature', prio: 3, est: 120, crit: ['C2'], deps: [R03],
40
+ title: 'Privacy flags: account_visibility + recruiter_discoverable on platform accounts',
41
+ desc: `Add account_visibility ('public'|'private', default 'public') and recruiter_discoverable (boolean NULL — tri-state; effective = COALESCE(flag, visibility='public')) to platform_identity_accounts via an additive platform_identity_* migration. Extend the own-scoped PATCH write path task ${R03} (R03) establishes for hide_stats to cover both, and add the two settings rows to the profile rail (coordinate P04, task 1002800). The discoverable toggle's copy must state exactly what it grants: the D3 sliver (name, disciplines, active-recently), name-only if stats are hidden. Model + defaults: ${SPEC} D1.`,
42
+ },
43
+ {
44
+ ref: 'PV2', kind: 'feature', prio: 3, est: 180, crit: ['C1', 'C2'], deps: [R02],
45
+ title: 'Effective-visibility enforcement: one shared predicate across every public read',
46
+ desc: `One helper (SQL predicate + port function, the ADR 0171 D2 "enforced next to the data" pattern) that every public surface uses: /u/<handle> returns 404 (never 403) for private accounts TO EVERYONE INCLUDING SELF on the public read (the page renders self-view via the own-scope side door instead — D2's viewer-independence reconciliation); private accounts absent from search, leaderboards, sitemap; the rollup predicate extends profile_state='active' AND NOT hide_stats with visibility. No surface writes its own variant — drift here is a privacy leak. Matrix: ${SPEC} D2. Also depends on PV1 (bv1-privacy-PV1).`,
47
+ },
48
+ {
49
+ ref: 'PV3', kind: 'feature', prio: 3, est: 120, crit: ['C2', 'C5'], deps: [],
50
+ title: 'getRecruiterSliver port + recruiting-surface gating (the pre-accept boundary)',
51
+ desc: `The owner-signed D3 sliver: handle, display name, avatar, disciplines union, active-in-last-30-days boolean — nothing else, name-only when hide_stats is true, never for provisional accounts, never a profile-page link. Authorization-scoped port getRecruiterSliver(githubId) beside getPublicProfileExtras, whitelist-shaped inside platform-identity, its own planted-sentinel leak test (tests/profile_route.mjs pattern), route-gated by project.recruit (the console wave's atom — coordinate spike 1002786 R3). Depends on PV1+PV2 (bv1-privacy-PV1/PV2). Boundary: ${SPEC} D3.`,
52
+ },
53
+ {
54
+ ref: 'PV4', kind: 'feature', prio: 3, est: 120, crit: ['C3'], deps: [],
55
+ title: 'Invite lifecycle: 30-day read-time expiry, seen_at, decline closes the door',
56
+ desc: `Owner-signed: a recruit invite's pre-approved sign-in door expires 30 days after creation, enforced READ-TIME (the sign-in gate's invited-check + every queue view compute created_at + 30 days; no sweeper). seen_at lands on the hub notification row (set on first inbox render) and on goal_membership_requests for parity (goal invites get NO expiry — not an admission door). Decline in the inbox relays a door-close to the instance (invite row → dismissed) so no declined invite leaves a live door. COORDINATION: the invite storage itself is the console wave's R2 (access_requests kind='invite') and rescind-cleans-both-sides is its R6 (spike 1002786) — this task lands the states on whatever R2/R6 ship, which is why it stays backlog until that wave seeds. Lifecycle: ${SPEC} D4.`,
57
+ },
58
+ {
59
+ ref: 'PV5', kind: 'feature', prio: 3, est: 90, crit: ['C3'], deps: [],
60
+ title: 'Hub inbox project slice: recruit invites in the attention card',
61
+ desc: `A project slice in the EXISTING attention card (goal-inbox.js slice pattern + window.OTBAttention.refresh()), on the hub hall + apex home, fed by the existing GET /my-projects/invites. Accept = "open <project> — sign in" link (clicking through IS acceptance, console D2); Decline = the PV4 door-close path. No parallel inbox; federated instances' halls link out only (V1). Notification path IS the inbox — SSE/email explicitly deferred. Depends on PV4 (bv1-privacy-PV4). Shape: ${SPEC} D5.`,
62
+ },
63
+ {
64
+ ref: 'PV6', kind: 'feature', prio: 4, est: 90, crit: [], deps: [], ready: true,
65
+ title: 'Private goals: accepting_requests toggle + owner rescind-pending-invite route',
66
+ desc: `goals.accepting_requests boolean default true — a private goal with it false refuses join-requests with honest copy ("this goal isn't taking requests right now"); invitations unaffected; UI shows the toggle only on private goals. Plus the missing half of core_188's 'cancelled' state: an owner/manager route to rescind a PENDING invitation (only requester-withdraw exists today — modules/lifecycle/routes/goals.js). Design: ${SPEC} D6.`,
67
+ },
68
+ {
69
+ ref: 'PV7', kind: 'feature', prio: 3, est: 90, crit: ['C4'], deps: [],
70
+ title: 'Applications carry the applicant profile: note through the relay, live render in the queue',
71
+ desc: `One rule, no new disclosure class: the reviewer sees what the applicant's profile rules would show them + the applicant's note, evaluated LIVE at review time (never a snapshot — hide takes effect on next render, C2). Public account → public profile view; private account → the D3 sliver (the apply UI states "your application shares: …"). Plumbing: POST /my-projects/join gains an applicant-supplied note (capped ~500 chars, sanitized; replaces the hardcoded 'via platform My Projects'), and the reviewer queue (console R4's page — spike 1002786) resolves the profile from the hub at render. Depends on PV3 (bv1-privacy-PV3). Rule: ${SPEC} D7.`,
72
+ },
73
+ {
74
+ ref: 'PV8', kind: 'verify', prio: 3, est: 60, crit: ['C2'], deps: [],
75
+ title: 'PROOF: the privacy matrix as a test',
76
+ desc: `The D2 matrix, asserted: a private account's /u/<handle> public read 404s to a stranger AND to its own authenticated session (the page self-view comes from the own-scope endpoint — assert both halves, this is P10 task 1002806 sharpened); hidden stats never appear in any response (planted sentinels); the recruiter sliver is exactly D3's shape (leak test on the port); an expired invite refuses sign-in; a declined invite's door is closed instance-side. Depends on PV2+PV3+PV4 (bv1-privacy-PV2/PV3/PV4). Matrix: ${SPEC} D2.`,
77
+ },
78
+ ];
79
+
80
+ (async () => {
81
+ console.log(`Seeding privacy+invites tasks under goal ${GOAL} (${VER})…`);
82
+ const idByRef = {};
83
+ let created = 0, existing = 0;
84
+
85
+ for (const t of NEW_TASKS) {
86
+ const r = await api('POST', '/tasks', {
87
+ version_id: VER,
88
+ goal_id: GOAL,
89
+ title: t.title,
90
+ description: t.desc,
91
+ kind: t.kind,
92
+ discipline: 'engineer',
93
+ priority: t.prio,
94
+ est_minutes: t.est,
95
+ criterion_ids: t.crit,
96
+ source: SRC,
97
+ source_ref: `bv1-privacy-${t.ref}`,
98
+ });
99
+ if (!r.ok) { console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.data)}`); continue; }
100
+ const task = r.data.task || r.data;
101
+ idByRef[t.ref] = Number(task.id);
102
+ if (r.data.idempotent || r.data.existing) existing++; else created++;
103
+ console.log(` ${t.ref} → task ${task.id} (${t.kind}, P${t.prio})`);
104
+ }
105
+
106
+ console.log('Wiring dependencies…');
107
+ const DEP_SETS = [
108
+ ['PV1', [R03]],
109
+ ['PV2', [R02, idByRef.PV1]],
110
+ ['PV3', [idByRef.PV1, idByRef.PV2]],
111
+ ['PV5', [idByRef.PV4]],
112
+ ['PV7', [idByRef.PV3]],
113
+ ['PV8', [idByRef.PV2, idByRef.PV3, idByRef.PV4]],
114
+ ];
115
+ for (const [ref, deps] of DEP_SETS) {
116
+ const id = idByRef[ref];
117
+ const clean = deps.filter(Boolean);
118
+ if (!id || !clean.length) continue;
119
+ const r = await api('PUT', `/tasks/${id}/dependencies`, { depends_on_task_ids: clean });
120
+ console.log(` ${ref} (task ${id}) depends on [${clean.join(', ')}]: ${r.ok ? 'ok' : 'FAILED ' + r.status + ' ' + JSON.stringify(r.data).slice(0, 160)}`);
121
+ }
122
+
123
+ console.log('Promoting the independent root…');
124
+ for (const t of NEW_TASKS.filter((x) => x.ready)) {
125
+ const id = idByRef[t.ref];
126
+ if (!id) continue;
127
+ const r = await api('POST', `/tasks/${id}/promote`, {});
128
+ console.log(` ${t.ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.data).slice(0, 120)}`);
129
+ }
130
+
131
+ console.log(`\nDONE. created=${created} existing=${existing}.`);
132
+ console.log('ref→id:', JSON.stringify(idByRef));
133
+ })();
package/src/module-api.js CHANGED
@@ -755,8 +755,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
755
755
  // landed since 1.19.162 with no explicit bump. run 31959864781. (task 1002620)
756
756
  // 1.19.164 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
757
757
  // landed since 1.19.163 with no explicit bump. run 31960518012. (task 1002620)
758
+ // 1.19.165 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
759
+ // landed since 1.19.164 with no explicit bump. run 31960929146. (task 1002620)
760
+ // 1.19.166 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
761
+ // landed since 1.19.165 with no explicit bump. run 31961272140. (task 1002620)
758
762
  // ---------------------------------------------------------------------------
759
- const CORE_VERSION = '1.19.164'; // CI auto-patch carrier (ADR 0161); see changelog above
763
+ const CORE_VERSION = '1.19.166'; // CI auto-patch carrier (ADR 0161); see changelog above
760
764
 
761
765
  // A namespaced logger so a module's log lines are attributable + consistent.
762
766
  // Usage: const log = api.logger('dev-box'); log.info('mounted');