@bongos/core 1.17.1 → 1.17.3

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.
Files changed (233) hide show
  1. package/.bongos-core.json +521 -281
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/GOVERNANCE.md +1 -1
  15. package/README.md +1 -1
  16. package/clients/bongos-client/README.md +1 -1
  17. package/clients/bongos-client/bongos-client.global.js +2 -0
  18. package/clients/bongos-client/index.cjs +2 -0
  19. package/clients/bongos-client/index.d.ts +4 -2
  20. package/clients/bongos-client/index.mjs +2 -0
  21. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  22. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  23. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  24. package/docs/adr/0020-security-model-public-stub.md +1 -1
  25. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  26. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  27. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  28. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  29. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  30. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  31. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  32. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  33. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  34. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  35. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  36. package/docs/adr/0057-container-cost-ledger.md +3 -3
  37. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  38. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  39. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  40. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  41. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  42. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  43. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  44. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  45. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  46. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  47. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  48. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  49. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  50. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  51. package/docs/adr/README.md +9 -6
  52. package/docs/api/openapi.json +65 -26
  53. package/docs/api-reference.md +3 -2
  54. package/docs/architecture.md +2 -2
  55. package/docs/branding-contract.md +5 -4
  56. package/docs/canonical-permissions.md +1 -1
  57. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  58. package/docs/file-map.md +18 -17
  59. package/docs/handoff-template.md +2 -2
  60. package/docs/modules-contract.md +6 -1
  61. package/docs/onboarding/diagrams/README.md +7 -7
  62. package/docs/onboarding/drachmae-and-karma.md +18 -18
  63. package/docs/onboarding/primer.md +9 -9
  64. package/docs/recipes/audit-replay.md +2 -2
  65. package/docs/recipes/builder-box-lifecycle.md +2 -2
  66. package/docs/recipes/claude-design-loop.md +1 -1
  67. package/docs/recipes/ops-gotchas.md +1 -1
  68. package/docs/recipes/private-npm-distribution.md +17 -0
  69. package/docs/recipes/staging-environment.md +1 -1
  70. package/docs/recipes/upgrading-the-core.md +55 -0
  71. package/docs/research/cursor-competitive-analysis.md +1 -1
  72. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  73. package/modules/discord/module.json +2 -1
  74. package/modules/economy/achievements.js +9 -1
  75. package/modules/economy/credits.js +10 -1
  76. package/modules/hall-ui/public/builders.js +137 -620
  77. package/modules/hall-ui/public/goal-inbox.js +232 -0
  78. package/modules/hall-ui/public/goals-page.js +4 -1
  79. package/modules/hall-ui/public/goals.html +8 -0
  80. package/modules/hall-ui/public/hall-kit.js +474 -0
  81. package/modules/hall-ui/public/hall-widgets.js +14 -5
  82. package/modules/hall-ui/public/index.html +3 -0
  83. package/modules/hall-ui/public/instances-panel.js +24 -3
  84. package/modules/hall-ui/public/new-project-wizard.js +7 -5
  85. package/modules/hall-ui/public/style.css +168 -0
  86. package/modules/hall-ui/public/work.css +194 -0
  87. package/modules/hall-ui/public/work.html +46 -374
  88. package/modules/hall-ui/public/work.js +137 -102
  89. package/modules/lifecycle/criterion-suggest.js +80 -0
  90. package/modules/lifecycle/db.js +42 -5
  91. package/modules/lifecycle/routes/goals.js +26 -0
  92. package/modules/lifecycle/routes/tasks.js +21 -0
  93. package/modules/provisioning/CLAUDE.md +15 -0
  94. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  95. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  96. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  97. package/modules/provisioning/module.json +17 -0
  98. package/modules/provisioning/onboard-plan.js +37 -0
  99. package/modules/provisioning/provisioning.js +539 -0
  100. package/modules/provisioning/rate-limit.js +35 -0
  101. package/modules/provisioning/routes/provisioning.js +607 -0
  102. package/modules/provisioning/tests/provisioning.mjs +232 -0
  103. package/modules/provisioning/ui/.gitkeep +0 -0
  104. package/modules/security/secret-scrub.js +10 -0
  105. package/package-lock.json +2 -2
  106. package/package.json +7 -2
  107. package/scripts/discord/README.md +1 -1
  108. package/scripts/gds/claim.js +98 -48
  109. package/scripts/gds/claude-materialize.js +30 -9
  110. package/scripts/gds/cutover-codemod.js +241 -0
  111. package/scripts/gds/fitness.js +54 -0
  112. package/scripts/gds/init.js +59 -3
  113. package/scripts/gds/lint-infra-exec.js +70 -0
  114. package/scripts/gds/lint-workflow-exec.js +63 -0
  115. package/scripts/gds/mirror-lineage-check.js +14 -5
  116. package/scripts/gds/mirror-redact.js +8 -0
  117. package/scripts/gds/onboard.js +4 -0
  118. package/scripts/gds/package-core.js +18 -3
  119. package/scripts/gds/publish-manifest.js +60 -2
  120. package/scripts/gds/run-routine.js +51 -13
  121. package/scripts/gds/run-unit-tests.js +4 -0
  122. package/scripts/gds/update-channel.js +186 -0
  123. package/scripts/gds/upgrade.js +116 -9
  124. package/scripts/hall-preview/README.md +103 -0
  125. package/scripts/hall-preview/fixtures/access-requests.json +1 -0
  126. package/scripts/hall-preview/fixtures/achievements.json +76 -0
  127. package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
  128. package/scripts/hall-preview/fixtures/audit-log.json +1 -0
  129. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
  130. package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
  131. package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
  132. package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
  133. package/scripts/hall-preview/fixtures/goals.json +502 -0
  134. package/scripts/hall-preview/fixtures/goals__25.json +96 -0
  135. package/scripts/hall-preview/fixtures/goals__26.json +67 -0
  136. package/scripts/hall-preview/fixtures/goals__3.json +103 -0
  137. package/scripts/hall-preview/fixtures/goals__35.json +49 -0
  138. package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
  139. package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
  140. package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
  141. package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
  142. package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
  143. package/scripts/hall-preview/fixtures/inbox.json +330 -0
  144. package/scripts/hall-preview/fixtures/instance.json +124 -0
  145. package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
  146. package/scripts/hall-preview/fixtures/me.json +476 -0
  147. package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
  148. package/scripts/hall-preview/fixtures/override-requests.json +1 -0
  149. package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
  150. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
  151. package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
  152. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
  153. package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
  154. package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
  155. package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
  156. package/scripts/hall-preview/fixtures/public__grades.json +50 -0
  157. package/scripts/hall-preview/fixtures/public__progress.json +124 -0
  158. package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
  159. package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
  160. package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
  161. package/scripts/hall-preview/fixtures/security__docs.json +1 -0
  162. package/scripts/hall-preview/fixtures/security__reports.json +1 -0
  163. package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
  164. package/scripts/hall-preview/fixtures/tasks.json +2420 -0
  165. package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
  166. package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
  167. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
  168. package/scripts/hall-preview/fixtures/versions.json +89 -0
  169. package/scripts/hall-preview/refresh-fixtures.js +77 -0
  170. package/scripts/hall-preview/server.js +178 -0
  171. package/scripts/migrate.sh +29 -4
  172. package/src/bongos/auth.js +14 -1
  173. package/src/bongos/optimizer.js +37 -2
  174. package/src/bongos/serve-internal.js +10 -1
  175. package/src/branding.js +1 -0
  176. package/src/module-api.js +12 -1
  177. package/src/module-loader/loader.js +36 -4
  178. package/src/module-loader/manifest-schema.js +16 -1
  179. package/tests/branding.mjs +11 -0
  180. package/tests/claim_error_surface.mjs +107 -0
  181. package/tests/claude_materialize.mjs +20 -0
  182. package/tests/consumer_layout_boot.mjs +302 -0
  183. package/tests/criterion_suggest.mjs +106 -0
  184. package/tests/cutover_codemod.mjs +184 -0
  185. package/tests/device_flow_start.mjs +25 -2
  186. package/tests/hook_core_resolve.mjs +125 -0
  187. package/tests/infra_exec_paths.mjs +104 -0
  188. package/tests/init.mjs +47 -0
  189. package/tests/migrate_root_resolution.mjs +97 -0
  190. package/tests/mirror_lineage_proof.mjs +14 -0
  191. package/tests/module_loader.mjs +42 -0
  192. package/tests/module_manifest.mjs +19 -0
  193. package/tests/onboard.mjs +13 -0
  194. package/tests/package_core.mjs +8 -0
  195. package/tests/provisioning_github_app_routes.mjs +15 -0
  196. package/tests/publish_manifest.mjs +52 -1
  197. package/tests/run_routine.mjs +26 -1
  198. package/tests/run_routine_two_root.mjs +77 -0
  199. package/tests/scrubber_corpus.mjs +17 -0
  200. package/tests/update_channel.mjs +186 -0
  201. package/tests/upgrade.mjs +131 -0
  202. package/tests/workflow_exec_paths.mjs +98 -0
  203. package/docs/design/container-exterior-tiles.md +0 -49
  204. package/docs/design/container-exterior.md +0 -81
  205. package/docs/design/container-interior.md +0 -77
  206. package/docs/design/world-map-scroll.md +0 -41
  207. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  208. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  209. package/docs/recipes/cloudbongos-standup.md +0 -111
  210. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  211. package/docs/recipes/parallel-load-test.md +0 -32
  212. package/docs/recipes/pixel-art-pipeline.md +0 -411
  213. package/docs/recipes/staging-data.md +0 -50
  214. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  215. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  216. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  217. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  218. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  219. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  220. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  221. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  222. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  223. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  224. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  225. package/scripts/gds/art-stage.js +0 -152
  226. package/server.js +0 -212
  227. package/tests/art_key.mjs +0 -121
  228. package/tests/day_cycle.mjs +0 -197
  229. package/tests/snapshot_picker.mjs +0 -233
  230. package/tests/sprite_facing.mjs +0 -35
  231. package/tests/terrain_picker.mjs +0 -298
  232. package/tests/turn_in_place.mjs +0 -280
  233. package/tests/walkability.mjs +0 -188
@@ -0,0 +1,232 @@
1
+ // goal-inbox.js — the Home "Needs your attention" card (task 2169), grown from
2
+ // the two-sided goal-membership inbox (task 1954, ADR 0112).
3
+ //
4
+ // This card is the SINGLE landing place for actionable items on Home: it owns
5
+ // the section (id 'attention-scroll'), renders the goal-membership slice
6
+ // itself, and hosts the idea-triage slice (#attention-ideas, rendered by
7
+ // builders.js). New actionable item classes join as line-item slices here —
8
+ // never as new Home cards (the anti-accretion rule).
9
+ //
10
+ // The goal slice surfaces a builder's private-goals queue:
11
+ // • "Invitations to you" — pending invites addressed to me (Accept / Decline).
12
+ // • "Requests to approve" — pending join-requests on goals I own or manage
13
+ // (Approve / Deny).
14
+ // • a quiet "your pending requests" line for my own outstanding asks.
15
+ // Reads GET /goals/inbox (task 1952) which already scopes each bucket to the caller.
16
+ // Registers into the window.OTB hall-widget registry (dom-utils.js) exactly like
17
+ // goals.js — same IIFE guard, same BongosClient result-mode getJSON, view:'home'.
18
+ //
19
+ // The whole card is HIDDEN when the inbox is empty, so a quiet home page pays nothing
20
+ // for it. On an older server without the route (404) — or a signed-out visitor (401) —
21
+ // it renders nothing (feature-detect), never an error card.
22
+ (function () {
23
+ 'use strict';
24
+ if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
25
+ const { escapeHtml, toast, rankLabel } = window.OTB;
26
+ const API = '/api/bongos';
27
+ // The generated typed client (served as window.BongosClient), result-mode so the
28
+ // status-inspecting control flow below (401/404 → render nothing) reads 1:1.
29
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
30
+
31
+ async function getJSON(path) {
32
+ const res = await api.request('GET', path);
33
+ if (res.status === 401) { const e = new Error('auth-needed'); e.code = 'auth-needed'; throw e; }
34
+ if (res.status === 404) { const e = new Error('not-found'); e.code = 'not-found'; throw e; }
35
+ if (!res.ok) throw new Error(`${path} → ${res.status}`);
36
+ return res.data;
37
+ }
38
+ async function postJSON(path, body) {
39
+ const res = await api.request('POST', path, { body: body || {} });
40
+ if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = res.data; throw e; }
41
+ return res.data;
42
+ }
43
+
44
+ let host = null;
45
+ let busy = false;
46
+
47
+ // The goal-detail hash route on the Goals page (matches goals.js's row link).
48
+ const goalHref = (goalId) => `/goals#/goal/${encodeURIComponent(goalId)}`;
49
+ const noteHtml = (note) => (note ? `<p class="ginbox__note">“${escapeHtml(note)}”</p>` : '');
50
+
51
+ function invitationRow(inv) {
52
+ const who = inv.invited_by_name || inv.invited_by_login || 'someone';
53
+ return `<li class="ginbox__item" data-goal="${escapeHtml(String(inv.goal_id))}">
54
+ <div class="ginbox__body">
55
+ <a class="ginbox__goal" href="${goalHref(inv.goal_id)}" title="Open ${escapeHtml(inv.goal_title || 'goal')}">${escapeHtml(inv.goal_title || 'a goal')}</a>
56
+ <span class="ginbox__sub">invited by ${escapeHtml(who)}</span>
57
+ ${noteHtml(inv.note)}
58
+ </div>
59
+ <div class="ginbox__actions">
60
+ <button class="btn-ghost" type="button" data-accept-inv="${escapeHtml(String(inv.id))}">Accept</button>
61
+ <button class="btn-ghost" type="button" data-decline-inv="${escapeHtml(String(inv.id))}">Decline</button>
62
+ </div>
63
+ </li>`;
64
+ }
65
+
66
+ function requestRow(r) {
67
+ const who = r.display_name || r.github_login || 'a builder';
68
+ const rank = r.rank ? ` · ${escapeHtml(rankLabel(r.rank, { empty: '' }))}` : '';
69
+ return `<li class="ginbox__item" data-goal="${escapeHtml(String(r.goal_id))}">
70
+ <div class="ginbox__body">
71
+ <a class="ginbox__goal" href="${goalHref(r.goal_id)}" title="Open ${escapeHtml(r.goal_title || 'goal')}">${escapeHtml(r.goal_title || 'a goal')}</a>
72
+ <span class="ginbox__sub">${escapeHtml(who)}${rank} asks to join</span>
73
+ ${noteHtml(r.note)}
74
+ </div>
75
+ <div class="ginbox__actions">
76
+ <button class="btn-ghost" type="button" data-approve-req="${escapeHtml(String(r.id))}">Approve</button>
77
+ <button class="btn-ghost" type="button" data-deny-req="${escapeHtml(String(r.id))}">Deny</button>
78
+ </div>
79
+ </li>`;
80
+ }
81
+
82
+ function pendingLine(myPending) {
83
+ if (!myPending.length) return '';
84
+ const links = myPending
85
+ .map((r) => `<a href="${goalHref(r.goal_id)}">${escapeHtml(r.goal_title || 'a goal')}</a>`)
86
+ .join(', ');
87
+ const noun = myPending.length === 1 ? 'request' : 'requests';
88
+ return `<p class="ginbox__pending card-foot">Your pending join ${noun}: ${links} — awaiting approval.</p>`;
89
+ }
90
+
91
+ // task 2169: this render fills ONLY the goals slice (#attention-goals) — the
92
+ // ideas slice (#attention-ideas) belongs to builders.js's triage renderer.
93
+ // Card visibility + the headline count derive from BOTH via OTBAttention.
94
+ const MAX_GOAL_ROWS = 5;
95
+ function render(inbox) {
96
+ const box = host && host.querySelector('#attention-goals');
97
+ if (!box) return;
98
+ const invitations = inbox.invitations || [];
99
+ const joinRequests = inbox.joinRequests || [];
100
+ const myPending = inbox.myPendingRequests || [];
101
+ if (!invitations.length && !joinRequests.length && !myPending.length) {
102
+ box.innerHTML = '';
103
+ window.OTBAttention.refresh();
104
+ return;
105
+ }
106
+ // Cap the card at a handful of rows — it is a summary, not the queue.
107
+ let budget = MAX_GOAL_ROWS;
108
+ const inv = invitations.slice(0, budget); budget -= inv.length;
109
+ const reqs = joinRequests.slice(0, Math.max(0, budget));
110
+ const overflow = (invitations.length - inv.length) + (joinRequests.length - reqs.length);
111
+ let html = '';
112
+ if (inv.length) {
113
+ html += '<h3 class="ginbox__sec-h">Goal invitations to you</h3>';
114
+ html += `<ul class="ginbox">${inv.map(invitationRow).join('')}</ul>`;
115
+ }
116
+ if (reqs.length) {
117
+ html += '<h3 class="ginbox__sec-h">Join-requests to approve</h3>';
118
+ html += `<ul class="ginbox">${reqs.map(requestRow).join('')}</ul>`;
119
+ }
120
+ if (overflow > 0) {
121
+ html += `<p class="ginbox__pending">…and ${overflow} more waiting — <a href="/goals">open your goals</a>.</p>`;
122
+ }
123
+ html += pendingLine(myPending);
124
+ box.innerHTML = html;
125
+ window.OTBAttention.refresh();
126
+ }
127
+
128
+ async function load() {
129
+ if (!host) return;
130
+ try {
131
+ const inbox = await getJSON(`${API}/goals/inbox`);
132
+ render(inbox);
133
+ } catch (err) {
134
+ // Signed out (401) or an older server without the route (404) → render
135
+ // nothing in the goals slice; the ideas slice may still have content.
136
+ const box = host.querySelector('#attention-goals');
137
+ if (box) box.innerHTML = '';
138
+ window.OTBAttention.refresh();
139
+ if (err.code !== 'auth-needed' && err.code !== 'not-found') {
140
+ console.error('[hall] goal inbox load failed', err);
141
+ }
142
+ }
143
+ }
144
+
145
+ // One action, then refresh the widget (the row it acted on disappears, and the
146
+ // count-pill/live counter re-derive from the fresh inbox). Disable while in flight
147
+ // so a double-click can't fire the resolve twice (the server 409s a re-resolve, but
148
+ // the UI shouldn't invite it).
149
+ async function act(fn, okMsg) {
150
+ if (busy) return;
151
+ busy = true;
152
+ host.querySelectorAll('button').forEach((b) => { b.disabled = true; });
153
+ try {
154
+ await fn();
155
+ toast(okMsg);
156
+ await load();
157
+ } catch (err) {
158
+ toast((err && err.data && err.data.message) || 'Could not do that just now — refresh and retry.');
159
+ host.querySelectorAll('button').forEach((b) => { b.disabled = false; });
160
+ } finally {
161
+ busy = false;
162
+ }
163
+ }
164
+
165
+ function onClick(e) {
166
+ const item = e.target.closest('.ginbox__item');
167
+ const goalId = item && item.dataset.goal;
168
+ if (!goalId) return;
169
+
170
+ const accept = e.target.closest('[data-accept-inv]');
171
+ if (accept) {
172
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/invitations/${encodeURIComponent(accept.dataset.acceptInv)}/respond`, { action: 'accept' }),
173
+ 'Invitation accepted — you’re now a member.');
174
+ return;
175
+ }
176
+ const decline = e.target.closest('[data-decline-inv]');
177
+ if (decline) {
178
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/invitations/${encodeURIComponent(decline.dataset.declineInv)}/respond`, { action: 'decline' }),
179
+ 'Invitation declined.');
180
+ return;
181
+ }
182
+ const approve = e.target.closest('[data-approve-req]');
183
+ if (approve) {
184
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(approve.dataset.approveReq)}/respond`, { action: 'approve' }),
185
+ 'Approved — they’re now a member.');
186
+ return;
187
+ }
188
+ const deny = e.target.closest('[data-deny-req]');
189
+ if (deny) {
190
+ act(() => postJSON(`${API}/goals/${encodeURIComponent(goalId)}/requests/${encodeURIComponent(deny.dataset.denyReq)}/respond`, { action: 'deny' }),
191
+ 'Request denied.');
192
+ }
193
+ }
194
+
195
+ // task 2169: this widget IS the "Needs your attention" card — ONE capped
196
+ // summary hosting every actionable item class: goal invitations +
197
+ // join-requests (this file) and ideas awaiting triage (builders.js renders
198
+ // into #attention-ideas). Future actionable item classes add a slice div
199
+ // here + call OTBAttention.refresh() — never a new Home card.
200
+ function mount(hostEl) {
201
+ host = hostEl;
202
+ host.hidden = true; // stays hidden until a slice finds something actionable
203
+ host.innerHTML =
204
+ '<h2 id="attention-h" class="scroll__h">Needs your attention<span class="count-pill" id="attention-count" hidden></span></h2>' +
205
+ '<div id="attention-goals"></div>' +
206
+ '<div id="attention-ideas"></div>';
207
+ host.addEventListener('click', onClick);
208
+ load();
209
+ }
210
+
211
+ // The card's shared visibility contract: any slice renderer calls refresh()
212
+ // after painting; the card shows iff any slice has actionable rows, and the
213
+ // headline pill is the cross-slice total.
214
+ window.OTBAttention = {
215
+ refresh() {
216
+ const sec = document.getElementById('attention-scroll');
217
+ if (!sec) return;
218
+ const n = sec.querySelectorAll('.ginbox__item, .triage-inbox__item').length;
219
+ const pending = sec.querySelector('.ginbox__pending');
220
+ const pill = document.getElementById('attention-count');
221
+ if (pill) { pill.hidden = !n; pill.textContent = String(n); }
222
+ sec.hidden = n === 0 && !pending;
223
+ },
224
+ };
225
+
226
+ // order 20 → the top of the Home main zone: personal + actionable beats
227
+ // informational. hiddenByDefault so a quiet inbox costs nothing.
228
+ window.OTB.contributeHallWidget({
229
+ id: 'attention-scroll', label: 'Needs your attention', order: 20, mount,
230
+ labelledby: 'attention-h', view: 'home', zone: 'home-main', hiddenByDefault: true,
231
+ });
232
+ })();
@@ -280,6 +280,7 @@
280
280
  <span class="goal-card__title">${escapeHtml(g.title || 'Untitled goal')}</span>
281
281
  </div>
282
282
  <div class="goal-card__meta">
283
+ <span class="goal-card__id" title="Goal id — use this to reference the goal (e.g. 'goal ${escapeHtml(String(g.id))}'). Distinct from the priority position on the left.">#${escapeHtml(String(g.id))}</span>
283
284
  <span>${escapeHtml(g.version_id || '')}</span>
284
285
  ${statusBadge(g.status)}
285
286
  ${blockedPill}
@@ -857,7 +858,9 @@
857
858
  const iAmLead = members.some((m) => sameId(m.builder_id, selfId()) && m.role === 'lead');
858
859
 
859
860
  const h = $('#goal-detail-h');
860
- if (h) h.textContent = g.title || 'Untitled goal';
861
+ // task 2163 — surface the numeric goal id alongside the title (escaped title,
862
+ // so switching from textContent to innerHTML stays injection-safe).
863
+ if (h) h.innerHTML = `<span class="goal-detail__id" title="Goal id">#${escapeHtml(String(g.id))}</span> ${escapeHtml(g.title || 'Untitled goal')}`;
861
864
  const desc = $('#goal-detail-desc');
862
865
  if (desc) desc.textContent = g.description || '';
863
866
  const meta = $('#goal-detail-meta');
@@ -38,6 +38,14 @@
38
38
  .goal-deps--blocked { color: var(--warn); font-weight: 600; }
39
39
  .goal-card__title { font-weight: 650; font-size: 14px; flex: 1; min-width: 0; }
40
40
  .goal-card__meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: 12px; color: var(--ink-faint); }
41
+ /* task 2163 — the real goal id, shown as a monospace #NNN token so it reads as
42
+ an identifier and never gets confused with the priority ordinal on the left. */
43
+ .goal-card__id, .goal-detail__id {
44
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
45
+ font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink-faint);
46
+ }
47
+ .goal-card__id { padding: 0 5px; border: 1px solid var(--rule-soft); border-radius: 5px; }
48
+ .goal-detail__id { font-size: 0.72em; vertical-align: 0.12em; margin-right: 4px; }
41
49
  .goal-card__foot { display: flex; gap: 14px; font-size: 12.5px; color: var(--ink-soft); }
42
50
  .goal-card__foot strong { font-variant-numeric: tabular-nums; }
43
51