@bongos/core 1.24.14 → 1.24.15

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 (161) hide show
  1. package/.bongos-core.json +296 -136
  2. package/.claude/skills/blocker-review/SKILL.md +2 -0
  3. package/.claude/skills/session-board/SKILL.md +29 -0
  4. package/.claude/skills/strand-fix/SKILL.md +1 -1
  5. package/clients/bongos-client/README.md +1 -1
  6. package/clients/bongos-client/bongos-client.global.js +14 -0
  7. package/clients/bongos-client/index.cjs +14 -0
  8. package/clients/bongos-client/index.d.ts +23 -2
  9. package/clients/bongos-client/index.mjs +14 -0
  10. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +4 -0
  11. package/docs/api/openapi.json +369 -6
  12. package/docs/api-reference.md +19 -4
  13. package/docs/architecture.md +6 -0
  14. package/docs/copy-inventory.md +138 -123
  15. package/docs/copy-registry.json +296 -171
  16. package/docs/file-map.md +1 -0
  17. package/docs/module-api-changelog.md +2 -2
  18. package/docs/page-inventory.json +6 -1
  19. package/docs/page-readings.json +1040 -1021
  20. package/migrations/core_289_task_short_name.sql +34 -0
  21. package/modules/ai-studio/db.js +10 -0
  22. package/modules/ai-studio/gemini.js +73 -5
  23. package/modules/ai-studio/openrouter.js +17 -2
  24. package/modules/ai-studio/routes/ai-studio.js +7 -1
  25. package/modules/ai-studio/studio.js +8 -0
  26. package/modules/builder-settings/checkout-freshness.js +71 -0
  27. package/modules/builder-settings/routes/settings.js +26 -0
  28. package/modules/builder-settings/settings.js +2 -0
  29. package/modules/economy/migrations/economy_002_project_account.sql +83 -0
  30. package/modules/economy/module.json +1 -1
  31. package/modules/economy/project-account.js +166 -0
  32. package/modules/economy/routes/project-account.js +40 -0
  33. package/modules/hall-ui/public/ai-studio.css +3 -0
  34. package/modules/hall-ui/public/ai-studio.html +2 -1
  35. package/modules/hall-ui/public/ai-studio.js +72 -1
  36. package/modules/hall-ui/public/atlas.html +1 -1
  37. package/modules/hall-ui/public/blockers.html +1 -1
  38. package/modules/hall-ui/public/board-room.html +1 -1
  39. package/modules/hall-ui/public/city.html +1 -1
  40. package/modules/hall-ui/public/collab.html +1 -1
  41. package/modules/hall-ui/public/commands.html +1 -1
  42. package/modules/hall-ui/public/copy-desk.html +1 -1
  43. package/modules/hall-ui/public/deploy.html +1 -1
  44. package/modules/hall-ui/public/diagrams.html +1 -1
  45. package/modules/hall-ui/public/drachmae.html +1 -1
  46. package/modules/hall-ui/public/fleet.html +1 -1
  47. package/modules/hall-ui/public/gate.html +1 -1
  48. package/modules/hall-ui/public/github-chip.js +85 -0
  49. package/modules/hall-ui/public/goal-map.html +1 -1
  50. package/modules/hall-ui/public/goals.html +1 -1
  51. package/modules/hall-ui/public/government.html +1 -1
  52. package/modules/hall-ui/public/idea.html +1 -1
  53. package/modules/hall-ui/public/ideas.html +1 -1
  54. package/modules/hall-ui/public/index.html +1 -1
  55. package/modules/hall-ui/public/market-watch-item.html +2 -2
  56. package/modules/hall-ui/public/market-watch-item.js +10 -3
  57. package/modules/hall-ui/public/market-watch-item.states.json +41 -0
  58. package/modules/hall-ui/public/market-watch-lib.js +36 -5
  59. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  60. package/modules/hall-ui/public/market-watch-settings.html +1 -1
  61. package/modules/hall-ui/public/market-watch.css +5 -0
  62. package/modules/hall-ui/public/market-watch.html +2 -2
  63. package/modules/hall-ui/public/modules.html +1 -1
  64. package/modules/hall-ui/public/my-agents.html +1 -1
  65. package/modules/hall-ui/public/my-agents.js +3 -0
  66. package/modules/hall-ui/public/primer.html +1 -1
  67. package/modules/hall-ui/public/profile.html +1 -1
  68. package/modules/hall-ui/public/project-settings.html +1 -1
  69. package/modules/hall-ui/public/ranks.html +1 -1
  70. package/modules/hall-ui/public/roadmap.html +1 -1
  71. package/modules/hall-ui/public/roster.html +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings.html +1 -1
  74. package/modules/hall-ui/public/shell.js +22 -22
  75. package/modules/hall-ui/public/street.html +1 -1
  76. package/modules/hall-ui/public/studio.html +1 -1
  77. package/modules/hall-ui/public/style.css +28 -0
  78. package/modules/hall-ui/public/task.html +1 -1
  79. package/modules/hall-ui/public/thinking.html +1 -1
  80. package/modules/hall-ui/public/tweak-editor.html +1 -1
  81. package/modules/hall-ui/public/watch.html +1 -1
  82. package/modules/hall-ui/public/work.html +1 -1
  83. package/modules/ideas/blockers.js +13 -7
  84. package/modules/lifecycle/db-claim-reads.js +1 -1
  85. package/modules/lifecycle/db-ship.js +2 -2
  86. package/modules/lifecycle/db-tasks.js +4 -4
  87. package/modules/lifecycle/migrations/lifecycle_019_pay_type_gates.sql +50 -0
  88. package/modules/lifecycle/module.json +1 -0
  89. package/modules/lifecycle/pay-type-gates.js +180 -0
  90. package/modules/lifecycle/publish-reconciler.js +31 -1
  91. package/modules/lifecycle/rebase-gate.js +3 -2
  92. package/modules/lifecycle/routes/claims.js +3 -2
  93. package/modules/lifecycle/routes/pay-type-gates.js +59 -0
  94. package/modules/lifecycle/routes/project-settings.js +4 -0
  95. package/modules/lifecycle/routes/task-write-routes.js +43 -0
  96. package/modules/lifecycle/ship-card.js +4 -1
  97. package/modules/lifecycle/task-name-db.js +55 -0
  98. package/modules/lifecycle/task-name.js +140 -0
  99. package/modules/market-watch/db.js +10 -1
  100. package/modules/market-watch/migrations/market_watch_004_entry_corrections.sql +20 -0
  101. package/modules/market-watch/pages.js +20 -1
  102. package/modules/market-watch/proposals.js +57 -7
  103. package/modules/market-watch/routes/pages.js +2 -0
  104. package/modules/market-watch/routes/rules.js +4 -0
  105. package/modules/platform-identity/terms-acceptance.js +9 -9
  106. package/modules/public-landing/public/account-google.states.json +14 -0
  107. package/modules/public-landing/public/assets/cosmos.css +12 -0
  108. package/modules/public-landing/public/assets/hub-bar.js +44 -1
  109. package/modules/public-landing/public/privacy.html +5 -1
  110. package/modules/public-landing/public/projects-google.states.json +14 -0
  111. package/modules/public-landing/public/projects.html +39 -0
  112. package/modules/public-landing/public/terms.html +12 -2
  113. package/modules/ui-design/kit/fixtures/me-google.json +10 -0
  114. package/modules/ui-design/kit/fixtures/me-sign-in-methods-google.json +4 -0
  115. package/modules/ui-design/kit/serve.js +7 -1
  116. package/package-lock.json +2 -2
  117. package/package.json +1 -1
  118. package/release-notes.json +49 -1
  119. package/scripts/bongos/claim.js +4 -2
  120. package/scripts/bongos/land-watch.js +1 -0
  121. package/scripts/bongos/report-checkout.js +30 -0
  122. package/scripts/bongos/run-unit-tests.js +11 -0
  123. package/scripts/bongos/sessions-board.js +264 -0
  124. package/scripts/bongos/ship-finish.js +1 -0
  125. package/scripts/bongos/ship-flow.js +5 -2
  126. package/scripts/bongos/strand-lease.js +5 -3
  127. package/scripts/bongos/strand-watch.js +38 -7
  128. package/scripts/gds/report-checkout.js +7 -0
  129. package/scripts/gds/sessions-board.js +7 -0
  130. package/src/bongos/agent-standing-rules.js +13 -3
  131. package/src/bongos/routes/me.js +7 -1
  132. package/src/module-api.js +1 -1
  133. package/tests/agent_standing_rules.mjs +18 -1
  134. package/tests/ai_studio_billing.mjs +13 -0
  135. package/tests/ai_studio_gemini_models.mjs +81 -0
  136. package/tests/ai_studio_requests.mjs +24 -0
  137. package/tests/blocker_detail_hydration.mjs +5 -3
  138. package/tests/blocker_tasks_named_db.mjs +74 -0
  139. package/tests/claim_gate_ci_unblock.mjs +1 -1
  140. package/tests/github_chip.mjs +217 -0
  141. package/tests/hall_ai_studio.mjs +129 -5
  142. package/tests/host_topology_skips.mjs +1 -0
  143. package/tests/hub_account_page.mjs +3 -2
  144. package/tests/landing_legal_pages.mjs +3 -2
  145. package/tests/market_watch_data_db.mjs +6 -0
  146. package/tests/market_watch_pages.mjs +87 -2
  147. package/tests/market_watch_rules.mjs +101 -5
  148. package/tests/market_watch_rules_db.mjs +42 -0
  149. package/tests/nav_permission_atoms.mjs +1 -1
  150. package/tests/pay_terms.mjs +8 -0
  151. package/tests/pay_type_gates.mjs +234 -0
  152. package/tests/pay_type_gates_db.mjs +84 -0
  153. package/tests/project_account.mjs +125 -0
  154. package/tests/project_account_db.mjs +97 -0
  155. package/tests/self_healing_start.mjs +162 -0
  156. package/tests/sessions_board.mjs +153 -0
  157. package/tests/stop_refcheck.mjs +40 -0
  158. package/tests/task_name.mjs +237 -0
  159. package/tests/task_name_db.mjs +79 -0
  160. package/tests/terms_acceptance.mjs +25 -3
  161. package/tests/terms_grandfather_db.mjs +11 -4
@@ -228,7 +228,7 @@
228
228
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
229
229
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
230
230
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
231
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
231
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
232
232
  <script src="/builders/palette.js" defer></script>
233
233
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
234
234
  <script src="/builders/vote-control.js"></script>
@@ -119,7 +119,7 @@
119
119
  <script src="/builders/welcome-boot.js?v=2026-10-23-city1" defer></script>
120
120
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab" defer></script>
121
121
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch" defer></script>
122
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages" defer></script>
122
+ <script src="/builders/shell.js?v=2026-10-30-github-chip" defer></script>
123
123
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
124
124
  <script src="/builders/palette.js" defer></script>
125
125
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -68,13 +68,13 @@
68
68
  </div>
69
69
  </div>
70
70
 
71
- <dialog class="mw-dialog" id="mw-propose" aria-label="Propose a change"></dialog>
71
+ <dialog class="mw-dialog" id="mw-propose" aria-labelledby="mw-propose-title"></dialog>
72
72
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
73
73
 
74
74
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
75
75
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
76
76
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
77
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
77
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
78
78
  <script src="/builders/palette.js" defer></script>
79
79
  <script src="/builders/hall-kit.js"></script>
80
80
  <script src="/builders/market-watch-lib.js"></script>
@@ -36,7 +36,7 @@
36
36
  const last = o.latest;
37
37
  const word = it.kind === 'partner' ? 'fit' : 'likeness';
38
38
  const runs = v.runs.length;
39
- const solid = v.entries.filter((e) => e.confidence === 3).length;
39
+ const solid = v.entries.filter((e) => e.confidence === 3 && !(e.corrected && e.corrected.wrong)).length;
40
40
  const pinned = MW.pins.has(it.id);
41
41
  $('mw-card').innerHTML = `<div class="mw-holo"><span class="mw-holo__kind">${esc(it.kind)}</span><span class="mw-holo__ini" aria-hidden="true">${esc(MW.initials(it.name))}</span><div class="mw-holo__nm" title="${esc(it.name)}">${esc(it.name)}</div></div>
42
42
  <div class="mw-lvl"><div class="mw-ring" style="--p:${last == null ? 0 : Math.round((last / 10) * 100)}"><b>${last == null ? '–' : esc(last)}</b></div>
@@ -85,13 +85,20 @@
85
85
  if (k === 'confidence') return (a.confidence - b.confidence) || (a.id - b.id);
86
86
  return String(a[k] || '').localeCompare(String(b[k] || '')) || (a.id - b.id);
87
87
  }
88
+ // The small note under an entry an approved proposal corrected (task 1005016): when,
89
+ // and why on hover. The reason is a member's words, so it is escaped.
90
+ function fixedNote(c) {
91
+ if (!c) return '';
92
+ const what = c.wrong ? 'marked wrong' : 'corrected';
93
+ return `<span class="mw-fixed mw-one"${c.reason ? ` title="${esc(c.reason)}"` : ''}>${what} ${esc(MW.fmtDay(c.on))}${c.reason ? ` · ${esc(c.reason)}` : ''}</span>`;
94
+ }
88
95
  function drawLog() {
89
96
  const rows = VIEW.entries.filter((e) => !TYPE || e.type === TYPE).sort((a, b) => (Number(b.pinned) - Number(a.pinned)) || SORT.dir * cmp(a, b));
90
- $('mw-log').innerHTML = rows.length ? rows.map((e) => `<tr class="${e.pinned ? 'pin' : ''}">
97
+ $('mw-log').innerHTML = rows.length ? rows.map((e) => `<tr class="${[e.pinned ? 'pin' : '', e.corrected && e.corrected.wrong ? 'wrong' : ''].filter(Boolean).join(' ')}">
91
98
  <td class="mw-date">${esc(MW.fmtDay(e.found_on))}</td>
92
99
  <td class="mw-c-run mw-run">${e.run == null ? '–' : `run ${esc(e.run)}`}</td>
93
100
  <td>${MW.typePill(e.type)}</td>
94
- <td class="mw-tk"><div class="mw-one" title="${esc(e.takeaway)}">${esc(e.takeaway)}</div></td>
101
+ <td class="mw-tk"><div class="mw-one" title="${esc(e.takeaway)}">${esc(e.takeaway)}</div>${fixedNote(e.corrected)}</td>
95
102
  <td class="mw-c-conf">${MW.confDots(e.confidence)}</td>
96
103
  <td><a class="mw-src" href="${MW.safeHref(e.source_url)}" target="_blank" rel="noopener noreferrer">${esc(e.source_label || 'source')} ↗</a></td></tr>`).join('')
97
104
  : `<tr><td colspan="6" class="mw-quiet">${TYPE ? `No ${esc(TYPE)} entries yet.` : 'Nothing found on it yet.'}</td></tr>`;
@@ -89,6 +89,47 @@
89
89
  ]
90
90
  }
91
91
  },
92
+ "correct": {
93
+ "auth": true,
94
+ "_": "Correct an entry (task 1005016): the entry picker, the box that starts as the entry's own words, and the mark-as-wrong choice.",
95
+ "actions": [
96
+ [
97
+ "wait",
98
+ 900
99
+ ],
100
+ [
101
+ "clickIfVisible",
102
+ "[data-craft-close]"
103
+ ],
104
+ [
105
+ "wait",
106
+ 200
107
+ ],
108
+ [
109
+ "click",
110
+ "[data-act=\"propose\"]"
111
+ ],
112
+ [
113
+ "wait",
114
+ 300
115
+ ],
116
+ [
117
+ "select",
118
+ "#mw-propose select[name=\"kind\"]",
119
+ "correct_entry"
120
+ ],
121
+ [
122
+ "wait",
123
+ 200
124
+ ]
125
+ ],
126
+ "expect": {
127
+ "visible": [
128
+ "#mw-propose input[name=\"takeaway\"]",
129
+ "#mw-propose input[name=\"wrong\"]"
130
+ ]
131
+ }
132
+ },
92
133
  "filtered": {
93
134
  "auth": true,
94
135
  "_": "The case file filtered to one type through the kit filter bar (insight): one row on the seeded Copilot item.",
@@ -152,15 +152,15 @@
152
152
  var it = ctx.item || {};
153
153
  var h = '<form method="dialog" novalidate>';
154
154
  if (mode === 'scout') {
155
- h += '<span class="mw-label">send a new scout</span>';
155
+ h += '<span class="mw-label" id="mw-propose-title">send a new scout</span>';
156
156
  h += '<label>What should we watch?<select name="scout"><option value="competitor">A competitor</option><option value="partner">A partner</option><option value="topic">A topic</option></select></label>';
157
157
  h += '<label>Its name<input name="name" maxlength="120" autocomplete="off" required></label>';
158
158
  h += '<label>A link, if it has one<input name="url" type="url" maxlength="500" placeholder="https://" autocomplete="off"></label>';
159
159
  } else if (mode === 'stop') {
160
- h += '<span class="mw-label">stop tracking</span>';
160
+ h += '<span class="mw-label" id="mw-propose-title">stop tracking</span>';
161
161
  h += '<p class="mw-quiet" style="margin:0 0 12px">Asks the government to take ' + esc(it.name) + ' off the watch. Its case file stays readable.</p>';
162
162
  } else {
163
- h += '<span class="mw-label">propose a change</span>';
163
+ h += '<span class="mw-label" id="mw-propose-title">propose a change</span>';
164
164
  h += '<label>What should change?<select name="kind">';
165
165
  CHANGE_KINDS.forEach(function (k) {
166
166
  if (k[0] === 'move_kind' && it.kind === 'topic') return;
@@ -175,7 +175,11 @@
175
175
  h += '<div data-for="add_source" hidden><label>The source<input name="url" type="url" maxlength="500" placeholder="https://" autocomplete="off"></label></div>';
176
176
  h += '<div data-for="correct_entry" hidden><label>Which entry<select name="log_entry_id">';
177
177
  (ctx.entries || []).forEach(function (e) { h += '<option value="' + esc(e.id) + '">' + esc(fmtDay(e.found_on)) + ' · ' + esc(String(e.takeaway).slice(0, 70)) + '</option>'; });
178
- h += '</select></label></div>';
178
+ h += '</select></label>';
179
+ // The correction itself (task 1005016): the entry as it should read, or a flag that
180
+ // it is wrong. Approving the proposal applies it to the entry.
181
+ h += '<label>It should read<input name="takeaway" maxlength="500" autocomplete="off" placeholder="the takeaway, corrected"></label>';
182
+ h += '<label class="mw-check"><input name="wrong" type="checkbox" value="true"> Mark it as wrong instead</label></div>';
179
183
  }
180
184
  h += '<label>Why, in one line<input name="reason" maxlength="280" autocomplete="off" required placeholder="' + (mode === 'scout' ? 'e.g. they launched an agent marketplace this week' : mode === 'stop' ? 'e.g. they shut down in September' : 'e.g. they now pay maintainers, so economy should be 4') + '"></label>';
181
185
  h += '<p class="mw-dialog__err" role="alert"></p>';
@@ -192,7 +196,11 @@
192
196
  var b = { kind: kind, item_id: item.id, reason: v('reason') };
193
197
  if (kind === 'rescore') { b.dimension = v('dimension'); b.value = v('value'); }
194
198
  if (kind === 'add_source') b.url = v('url');
195
- if (kind === 'correct_entry') b.log_entry_id = v('log_entry_id');
199
+ if (kind === 'correct_entry') {
200
+ b.log_entry_id = v('log_entry_id');
201
+ var w = f.elements.wrong;
202
+ if (w && w.checked) b.wrong = true; else b.takeaway = v('takeaway');
203
+ }
196
204
  return b;
197
205
  }
198
206
 
@@ -200,6 +208,11 @@
200
208
  var doc = root.document;
201
209
  var dlg = doc.getElementById('mw-propose');
202
210
  dlg.innerHTML = dialogHtml(mode, ctx);
211
+ // The dialog's accessible name is its own visible title, so a screen reader hears
212
+ // "stop tracking" or "send a new scout" when that is what opened (task 1005018). A
213
+ // fixed aria-label would win over aria-labelledby, so none is left on the element.
214
+ dlg.removeAttribute('aria-label');
215
+ dlg.setAttribute('aria-labelledby', 'mw-propose-title');
203
216
  var f = dlg.querySelector('form');
204
217
  var err = dlg.querySelector('.mw-dialog__err');
205
218
  var sync = function () {
@@ -208,10 +221,28 @@
208
221
  };
209
222
  if (f.elements.kind) f.elements.kind.addEventListener('change', sync);
210
223
  sync();
224
+ // Correct an entry: the box starts as the picked entry's own words, to edit; marking
225
+ // it wrong instead greys the box out. Only the 'change' dialog has these fields.
226
+ if (mode === 'change') {
227
+ var pick = f.elements.log_entry_id;
228
+ var tk = f.elements.takeaway;
229
+ var wr = f.elements.wrong;
230
+ var fill = function () {
231
+ var e = (ctx.entries || []).filter(function (x) { return String(x.id) === String(pick.value); })[0];
232
+ tk.value = e ? String(e.takeaway) : '';
233
+ };
234
+ pick.addEventListener('change', fill);
235
+ fill();
236
+ wr.addEventListener('change', function () { tk.disabled = wr.checked; });
237
+ }
211
238
  dlg.querySelector('[data-cancel]').addEventListener('click', function () { dlg.close(); });
212
239
  f.addEventListener('submit', function (ev) {
213
240
  ev.preventDefault();
214
241
  var body = bodyFor(mode, f, ctx.item || {});
242
+ if (body.kind === 'correct_entry' && !body.wrong) {
243
+ var was = (ctx.entries || []).filter(function (x) { return String(x.id) === String(body.log_entry_id); })[0];
244
+ if (!body.takeaway || (was && String(was.takeaway).trim() === body.takeaway)) { err.textContent = 'Change the words, or mark it as wrong.'; return; }
245
+ }
215
246
  if (!body.reason) { err.textContent = 'Say why, in one line.'; return; }
216
247
  var btn = f.querySelector('[type=submit]');
217
248
  btn.disabled = true;
@@ -43,7 +43,7 @@
43
43
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
44
44
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
45
45
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
46
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
46
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
47
47
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
48
48
  <script src="/builders/palette.js" defer></script>
49
49
  <script src="/builders/hall-kit.js"></script>
@@ -105,7 +105,7 @@
105
105
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
106
106
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
107
107
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
108
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
108
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
109
109
  <script src="/builders/palette.js" defer></script>
110
110
  <script src="/builders/hall-kit.js"></script>
111
111
  <script src="/builders/market-watch-settings.js"></script>
@@ -145,6 +145,9 @@
145
145
  .mw-log tr.pin td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
146
146
  .mw-log td.mw-tk { max-width: 0; width: 100%; }
147
147
  .mw-date, .mw-run { font: 400 12px var(--font-mono); color: var(--ink-faint); white-space: nowrap; }
148
+ /* an entry an approved proposal corrected (task 1005016): one small note under the line */
149
+ .mw-fixed { display: block; font-size: 11.5px; color: var(--ink-faint); }
150
+ .mw-log tr.wrong .mw-tk .mw-one { text-decoration: line-through; color: var(--ink-faint); }
148
151
  @media (max-width: 760px) {
149
152
  .mw-log thead { display: none; }
150
153
  .mw-log tr { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 4px 10px; padding: 10px 12px; border-bottom: 1px solid var(--rule-soft); }
@@ -162,6 +165,8 @@
162
165
  .mw-dialog::backdrop { background: var(--scrim); }
163
166
  .mw-dialog label { display: block; font-size: 13px; color: var(--ink-soft); margin-bottom: 12px; }
164
167
  .mw-dialog input, .mw-dialog select { display: block; width: 100%; margin-top: 6px; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 8px 9px; min-height: 36px; }
168
+ .mw-dialog label.mw-check { display: flex; gap: 8px; align-items: center; }
169
+ .mw-dialog label.mw-check input { display: inline-block; width: auto; min-height: 0; margin: 0; }
165
170
  .mw-dialog .mw-row2 { display: grid; grid-template-columns: minmax(0, 1fr) 90px; gap: 8px; }
166
171
  .mw-dialog__acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
167
172
  .mw-dialog__err { color: var(--danger); font-size: 12.5px; margin: 0 0 8px; min-height: 1em; }
@@ -96,13 +96,13 @@
96
96
  </div>
97
97
  </div>
98
98
 
99
- <dialog class="mw-dialog" id="mw-propose" aria-label="Propose a change"></dialog>
99
+ <dialog class="mw-dialog" id="mw-propose" aria-labelledby="mw-propose-title"></dialog>
100
100
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
101
101
 
102
102
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
103
103
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
104
104
  <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
105
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
105
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
106
106
  <script src="/builders/palette.js" defer></script>
107
107
  <script src="/builders/hall-kit.js"></script>
108
108
  <script src="/builders/market-watch-lib.js"></script>
@@ -128,7 +128,7 @@
128
128
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
129
129
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
130
130
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
131
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
131
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
132
132
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
133
133
  <script src="/builders/palette.js" defer></script>
134
134
  <script src="/builders/brand-apply.js"></script>
@@ -62,7 +62,7 @@
62
62
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
63
63
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
64
64
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
65
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
65
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
66
66
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
67
67
  <script src="/builders/palette.js" defer></script>
68
68
  <script src="/builders/gov-living.js?v=2026-10-10-real1"></script>
@@ -239,6 +239,9 @@
239
239
  const laws = arr(sent && sent.laws).map((l, i) => ({ id: str(l.id), no: Number(l.no) || i + 1, text: str(l.text), effect: str(l.effect_on_ai) }));
240
240
  const mine = [];
241
241
  if (inter) mine.push({ key: 'interaction', title: 'How it talks to you', sent: !!inter.inject, text: inter.inject ? str(inter.contract) : '' });
242
+ // task 1005021: your computer's project folder, as your own session last reported it.
243
+ const co = obj(me.checkout_freshness);
244
+ if (co && co.behind && co.line) mine.push({ key: 'checkout', title: 'Your computer', sent: true, text: str(co.line), note: 'Your next session will try to update it. If it cannot, your agent will offer to.' });
242
245
  if (wand) {
243
246
  mine.push({
244
247
  key: 'wandering', title: 'Off-task', sent: !!wand.inject,
@@ -113,7 +113,7 @@
113
113
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
114
114
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
115
115
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
116
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
116
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
117
117
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
118
  <script src="/builders/palette.js" defer></script>
119
119
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -208,7 +208,7 @@
208
208
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
209
209
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
210
210
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
211
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
211
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
212
212
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
213
213
  <script src="/builders/palette.js" defer></script>
214
214
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -72,7 +72,7 @@
72
72
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
73
73
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
74
74
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
75
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
75
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
76
76
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
77
77
  <script src="/builders/palette.js" defer></script>
78
78
  <script src="/builders/hall-kit.js"></script>
@@ -74,7 +74,7 @@
74
74
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
75
75
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
76
76
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
77
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
77
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
78
78
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
79
79
  <script src="/builders/palette.js" defer></script>
80
80
  <script src="/builders/founding-mark.js?v=2026-09-30-founding"></script>
@@ -44,7 +44,7 @@
44
44
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
45
45
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
46
46
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
47
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
47
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
48
48
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
49
49
  <script src="/builders/palette.js" defer></script>
50
50
  <!-- The chart kit and the timeline's series builders (task 1003447). Both MUST
@@ -56,7 +56,7 @@
56
56
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
57
57
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
58
58
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
59
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
59
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
60
60
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
61
  <script src="/builders/palette.js" defer></script>
62
62
  <!-- the branding pack's currency label, loaded BEFORE roster.js. -->
@@ -112,7 +112,7 @@
112
112
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
113
113
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
114
114
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
115
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
115
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
116
116
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
117
117
  <script src="/builders/palette.js" defer></script>
118
118
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
@@ -487,7 +487,7 @@
487
487
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
488
488
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
489
489
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
490
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
490
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
491
491
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
492
492
  <script src="/builders/palette.js" defer></script>
493
493
  <!-- shared component kit (window.OTBKit) — section rail (task 2166). -->
@@ -535,10 +535,8 @@
535
535
  TRAIL.backLinkHtml() +
536
536
  '<h2 class="topbar-title" id="topbar-title"></h2>' +
537
537
  '<div class="topbar-side"><span class="topbar-note" id="welcome-line"></span>' +
538
- // task 2178: the jump-palette slot. Rendered inert + hidden — palette.js
539
- // reveals and wires it when the page loads it. A page without palette.js
540
- // therefore shows no slot and runs no palette code (progressive
541
- // enhancement): the shell never calls into it, so nothing can throw.
538
+ // task 2178: the jump-palette slot, inert + hidden until palette.js wires it; a page without
539
+ // palette.js shows no slot and runs no palette code — the shell never calls into it.
542
540
  '<button class="topbar-jump" id="topbar-jump" type="button" hidden aria-label="Jump to a page or record">' +
543
541
  '<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="4.5"/><path d="M11.5 11.5 15 15"/></svg>' +
544
542
  '<span class="topbar-jump__label">Jump to…</span>' +
@@ -778,12 +776,23 @@
778
776
  if (res.data.moved) renderCoreMoveNotice(res.data.moved); // task 1004731: loads core-move-notice.js, which draws it
779
777
  }, function () { /* fail-soft: no banner is the correct answer to no answer */ });
780
778
  }
779
+ // One hall script on demand, stamped with the shell's own ?v= (task 1004996 made it one helper).
780
+ function loadHallScript(file, onload, onerror) {
781
+ var head = document.head, tag = head && head.appendChild && document.createElement ? document.createElement('script') : null;
782
+ if (!tag) { if (onerror) onerror(); return; }
783
+ tag.src = '/builders/' + file + (SHELL_V ? '?v=' + SHELL_V : '');
784
+ tag.onload = onload; tag.onerror = onerror || null;
785
+ head.appendChild(tag);
786
+ }
781
787
  function renderCoreMoveNotice(moved) {
782
- if (!document.head || !document.head.appendChild) return;
783
- var tag = document.createElement('script');
784
- tag.src = '/builders/core-move-notice.js' + (SHELL_V ? '?v=' + SHELL_V : '');
785
- tag.onload = function () { if (window.CoreMoveNotice) window.CoreMoveNotice.render(moved, { api: api, href: SOFTWARE_UPDATE_HREF }); };
786
- document.head.appendChild(tag);
788
+ loadHallScript('core-move-notice.js', function () { if (window.CoreMoveNotice) window.CoreMoveNotice.render(moved, { api: api, href: SOFTWARE_UPDATE_HREF }); });
789
+ }
790
+ var githubChipAsked = false; // task 1004996: the Link GitHub chip (github-chip.js), asked once a page
791
+ function loadGithubChip(builder) {
792
+ var hub = window.OTB && window.OTB.hubOrigin ? window.OTB.hubOrigin() : null;
793
+ if (githubChipAsked || !hub) return;
794
+ githubChipAsked = true;
795
+ loadHallScript('github-chip.js', function () { if (window.OTBGithubChip) window.OTBGithubChip.mount({ api: api, builder: builder, hub: hub }); });
787
796
  }
788
797
 
789
798
  // Where the banner's Upgrade goes (task 1004296). The platform's hall runs the deploy
@@ -1063,18 +1072,9 @@
1063
1072
  var positionsLibAsk = null;
1064
1073
  function positionsLib() {
1065
1074
  if (window.GovPositions) return Promise.resolve(window.GovPositions);
1066
- if (!positionsLibAsk) {
1067
- positionsLibAsk = new Promise(function (resolve) {
1068
- var head = document.head;
1069
- if (!head || !head.appendChild || !document.createElement) { resolve(null); return; }
1070
- var tag = document.createElement('script');
1071
- tag.src = '/builders/gov-positions.js' + (SHELL_V ? '?v=' + SHELL_V : '');
1072
- tag.onload = function () { resolve(window.GovPositions || null); };
1073
- tag.onerror = function () { resolve(null); };
1074
- head.appendChild(tag);
1075
- });
1076
- }
1077
- return positionsLibAsk;
1075
+ return positionsLibAsk || (positionsLibAsk = new Promise(function (resolve) {
1076
+ loadHallScript('gov-positions.js', function () { resolve(window.GovPositions || null); }, function () { resolve(null); });
1077
+ }));
1078
1078
  }
1079
1079
  // `sent` is /me's `positions` block as the server resolved it (undefined: not told).
1080
1080
  function loadPositions(builder, sent) {
@@ -1176,7 +1176,7 @@
1176
1176
  }
1177
1177
  if (state.builder) { renderUser(state.builder); applyCraftEmphasis(state.builder); }
1178
1178
  // task 1004713: which seats this builder holds — /me's server-resolved block.
1179
- if (state.builder && access.authed === true) loadPositions(state.builder, state.positions);
1179
+ if (state.builder && access.authed === true) { loadPositions(state.builder, state.positions); loadGithubChip(state.builder); }
1180
1180
  }
1181
1181
  document.querySelectorAll('#app-sidebar [data-gate]').forEach(function (el) {
1182
1182
  var ok = gateAllows(el.dataset.gate);
@@ -231,7 +231,7 @@
231
231
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
232
232
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
233
233
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
234
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
234
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
235
235
  <script src="/builders/palette.js" defer></script>
236
236
  <script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
237
237
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3"></script>
@@ -219,7 +219,7 @@
219
219
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
220
220
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
221
221
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
222
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
222
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
223
223
  <script src="/builders/palette.js" defer></script>
224
224
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
225
225
  (task 1003829), which the older reviews still show. -->
@@ -4416,6 +4416,34 @@ a.stat-tile:hover .stat-tile__sub { color: var(--ink-soft); }
4416
4416
  white-space: nowrap;
4417
4417
  }
4418
4418
 
4419
+ /* ---------- the Link GitHub chip (task 1004996, github-chip.js) ----------
4420
+ Very small and quiet on purpose: shown only to someone with no GitHub linked,
4421
+ it is a plain link, never a banner — ideating and art go on without it. The
4422
+ engineer's glyph says what it is for; the label folds away on a phone. */
4423
+ .gh-chip {
4424
+ display: inline-flex;
4425
+ align-items: center;
4426
+ gap: 5px;
4427
+ min-height: 24px;
4428
+ min-width: 24px;
4429
+ padding: 0 8px 0 6px;
4430
+ border: 1px solid var(--rule);
4431
+ border-radius: var(--radius-pill, 999px);
4432
+ color: var(--ink-soft);
4433
+ font-size: 12px;
4434
+ line-height: 1;
4435
+ text-decoration: none;
4436
+ white-space: nowrap;
4437
+ flex-shrink: 0;
4438
+ }
4439
+ .gh-chip:hover { color: var(--ink); border-color: var(--ink-faint); }
4440
+ .gh-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
4441
+ .gh-chip svg { width: 13px; height: 13px; flex-shrink: 0; }
4442
+ @media (max-width: 640px) {
4443
+ .gh-chip { padding: 0; justify-content: center; }
4444
+ .gh-chip__label { display: none; }
4445
+ }
4446
+
4419
4447
  .palette { position: fixed; inset: 0; z-index: 2100; }
4420
4448
  .palette__scrim { position: absolute; inset: 0; background: var(--scrim); }
4421
4449
  .palette__panel {
@@ -59,7 +59,7 @@
59
59
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
60
60
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
61
61
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
62
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
62
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
63
63
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
64
64
  <script src="/builders/palette.js" defer></script>
65
65
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N deep-link overlay
@@ -141,7 +141,7 @@
141
141
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
142
142
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
143
143
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
144
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
144
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
145
145
  <script src="/builders/palette.js" defer></script>
146
146
 
147
147
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -145,7 +145,7 @@
145
145
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
146
146
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
147
147
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
148
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
148
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
149
149
  <script src="/builders/palette.js" defer></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
151
151
  <script src="/builders/tweak-editor-lib.js?v=2026-09-29-tw27"></script>
@@ -166,7 +166,7 @@
166
166
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
167
167
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
168
168
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
169
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
169
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
170
170
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
171
171
  <script src="/builders/palette.js" defer></script>
172
172
  <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
@@ -174,7 +174,7 @@
174
174
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
175
175
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
176
176
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
177
- <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
177
+ <script src="/builders/shell.js?v=2026-10-30-github-chip"></script>
178
178
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
179
179
  <script src="/builders/palette.js" defer></script>
180
180
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -12,11 +12,17 @@ const { pool } = require('../../src/module-api');
12
12
  // Shared by listOpenBlockers + getBlocker so the two reads can never drift
13
13
  // (task 1002459 — getBlocker used to omit this column entirely, so a
14
14
  // link-then-verify against the detail route read as a false negative).
15
- const BLOCKING_TASK_IDS_SQL = `COALESCE(
16
- (SELECT array_agg(tb.task_id ORDER BY tb.task_id)
17
- FROM task_blockers tb WHERE tb.blocker_id = b.id),
18
- ARRAY[]::bigint[]
19
- ) AS blocking_task_ids`;
15
+ // task 1004958: ONE lateral read gives both shapes — the ids (the hall reads those)
16
+ // and the same tasks with their 2-3 word names, so a session can write
17
+ // "task N - payout approvals" instead of a bare number.
18
+ const BLOCKING_TASK_IDS_SQL = `COALESCE(bl.ids, ARRAY[]::bigint[]) AS blocking_task_ids,
19
+ COALESCE(bl.tasks, '[]'::json) AS blocking_tasks`;
20
+ const BLOCKING_TASKS_JOIN = `LEFT JOIN LATERAL (
21
+ SELECT array_agg(tb.task_id ORDER BY tb.task_id) AS ids,
22
+ json_agg(json_build_object('id', tb.task_id, 'short_name', bt.short_name) ORDER BY tb.task_id) AS tasks
23
+ FROM task_blockers tb LEFT JOIN tasks bt ON bt.id = tb.task_id
24
+ WHERE tb.blocker_id = b.id
25
+ ) bl ON true`;
20
26
 
21
27
  // Optional { limit, offset } pushes the page into SQL so a small slice (the Home
22
28
  // attention inbox reads limit=5 on every load) never scans the whole open set
@@ -32,7 +38,7 @@ async function listOpenBlockers({ limit, offset } = {}) {
32
38
  `SELECT b.id, b.title, b.body_md, b.status, b.created_at,
33
39
  b.source, b.source_ref,
34
40
  ${BLOCKING_TASK_IDS_SQL}
35
- FROM blockers b
41
+ FROM blockers b ${BLOCKING_TASKS_JOIN}
36
42
  WHERE b.status = 'open'
37
43
  ORDER BY b.id${page}`,
38
44
  params
@@ -50,7 +56,7 @@ async function getBlocker(id) {
50
56
  `SELECT b.id, b.title, b.body_md, b.status, b.created_at, b.resolved_at, b.resolved_by,
51
57
  b.resolution_note, b.source, b.source_ref,
52
58
  ${BLOCKING_TASK_IDS_SQL}
53
- FROM blockers b WHERE b.id = $1`,
59
+ FROM blockers b ${BLOCKING_TASKS_JOIN} WHERE b.id = $1`,
54
60
  [id]
55
61
  );
56
62
  return rows[0] || null;