@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
@@ -129,9 +129,19 @@
129
129
  <p><b>These numbers describe; they do not govern.</b> Your cross-project record is descriptive analytics and never authority. What you may do inside any project is decided by that project alone, from its own records, at the moment you ask. A large record grants nothing and a hidden one takes nothing away; no project is required to read it, and none may treat it as permission. A rank shown here is that project&rsquo;s own standing for you, as that project reported it &mdash; the platform holds no rank of its own.</p>
130
130
  <p><b>What is public.</b> Once you have signed in with your platform account in your browser, here or to a project, and accepted these terms, your profile and that record are public by default: at your handle&rsquo;s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in that way stays provisional and is published nowhere.</p>
131
131
  <p><b>What you control.</b> From your settings you can hide your statistics, drop out of the recruiting directory, or make your account private. Each takes effect immediately, everywhere, including on a page someone already has open. Hiding removes the platform&rsquo;s combined view; it does not retract the per-project numbers each project publishes itself, so a determined reader could still add them up the hard way.</p>
132
- <p><b>Your acceptance is recorded.</b> The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in with your platform account in your browser, here or to a project, and only once &mdash; signing in again does not change the date you accepted. Signing in from the command line does not record it: you approve that sign-in on GitHub&rsquo;s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in once in your browser. This is version <b>2026-08-29</b>.</p>
132
+ <p><b>Your acceptance is recorded.</b> The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in with your platform account in your browser, here or to a project, and only once &mdash; signing in again does not change the date you accepted. When these terms change, your next sign-in that way records that you accepted the new version. Signing in from the command line does not record it: you approve that sign-in on GitHub&rsquo;s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in once in your browser. This is version <b>2026-10-30</b>.</p>
133
133
  <p><b>Accounts from before these terms.</b> If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in that way.</p>
134
134
  <!--apex-only:end-->
135
+ <h2>AI features</h2>
136
+ <p>Some features, such as AI Studio, send what you submit to an AI model run by another company, reached through OpenRouter. To use them:</p>
137
+ <ul>
138
+ <li>You must be 18 or over. The AI companies only serve adults, so these features stay off until your account records that you are.</li>
139
+ <li>You agree to follow the terms and usage policies of the AI company whose model answers your request, and OpenRouter's own terms. Which company that is shows on <a href="https://openrouter.ai/models">openrouter.ai</a>.</li>
140
+ <li>You may not use what these features produce to train or build a competing AI model, or try to trick a model into breaking its own rules.</li>
141
+ <li>You are responsible for what you submit and for what you do with the results, including that you have the right to use any image or file you upload.</li>
142
+ </ul>
143
+ <p>When you pay with your balance, the price is what the AI company charged plus a small markup, shown before you confirm. When you use your own key, you pay that company directly and its terms apply to you as its customer. AI output can be wrong; check it before you rely on it.</p>
144
+
135
145
  <h2>Fair use</h2>
136
146
  <p>Don't use {{productName}} to break the law, to attack this site or other people's projects, to mine, spam or otherwise abuse hosted resources, or to harass other builders. The owner may deactivate a builder or take a project offline for breaking these, and may set further house rules inside the hall.</p>
137
147
 
@@ -140,7 +150,7 @@
140
150
 
141
151
  <h2>Changes</h2>
142
152
  <p>These terms ship with the software and change with it. The version served on this page is the one that applies; the repository keeps the history. <a href="{{sourceRepoUrl}}">Read the source.</a></p>
143
- <p>Questions go through the <a href="/contact">contact page</a>. What is kept about you is on the <a href="/privacy">privacy page</a>. Revised August 2026.</p>
153
+ <p>Questions go through the <a href="/contact">contact page</a>. What is kept about you is on the <a href="/privacy">privacy page</a>. Revised October 2026.</p>
144
154
  </section>
145
155
  </main>
146
156
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "_": "A Google-only builder (task 1004996): the stand-in login ~<handle> a Google-born row carries until GitHub is linked. Aliased as me by the *-google states files, beside me-sign-in-methods-google.",
3
+ "builder": {
4
+ "id": "1",
5
+ "github_login": "~builder",
6
+ "display_name": "A Builder",
7
+ "avatar_url": "/__kit/avatar.svg",
8
+ "rank": "archon"
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "_": "A Google-only person (task 1004996): no GitHub way in, so the bar draws the Link GitHub chip. Aliased as me__sign-in-methods by the *-google states files.",
3
+ "methods": [{ "provider": "google", "email": "octo@example.com" }]
4
+ }
@@ -585,8 +585,14 @@ function myGuilds(res) {
585
585
  // The fixture builder is GitHub-born with Google linked, held for the run so a
586
586
  // Remove reads back. Each way in carries the route's own unlink_refused, by
587
587
  // account-logins.js unlinkRefusal's rule, and DELETE refuses as the route does.
588
+ // A states file may alias `me__sign-in-methods` to a fixture's `methods` (task 1004996: the
589
+ // Link GitHub chip pictures a Google-only person through `me-sign-in-methods-google`).
588
590
  let _ways = null;
589
- function waysHeld() { return _ways || (_ways = [{ provider: 'github', email: null }, { provider: 'google', email: 'octo@example.com' }]); }
591
+ function waysHeld() {
592
+ if (_ways) return _ways;
593
+ if (ALIAS['me__sign-in-methods']) return (_ways = heldFixture('me__sign-in-methods').methods.map((m) => ({ provider: m.provider, email: m.email || null })));
594
+ return (_ways = [{ provider: 'github', email: null }, { provider: 'google', email: 'octo@example.com' }]);
595
+ }
590
596
  function wayRefusal(provider, count) { return provider === 'github' ? 'born_with' : count <= 1 ? 'last_way_in' : null; }
591
597
  function signInMethods(res) {
592
598
  const held = waysHeld();
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.14",
3
+ "version": "1.24.15",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.14",
9
+ "version": "1.24.15",
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.24.14",
3
+ "version": "1.24.15",
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",
@@ -10922,7 +10922,55 @@
10922
10922
  "text": "The weekly Market Watch run now also reads the links members added to a tracked item, treating them safely as outside text and staying within its page-reading limit."
10923
10923
  }
10924
10924
  ],
10925
- "1.24.14": [
10925
+ "1.24.15": [
10926
+ {
10927
+ "id": "1005027",
10928
+ "text": "The automatic checks that gate releases now agree with the new terms rule, so finished work can reach the live site again."
10929
+ },
10930
+ {
10931
+ "id": "1005026",
10932
+ "text": "Approving a correction to a Market Watch log entry works again, and the release lane that puts finished work on the live site is unblocked."
10933
+ },
10934
+ {
10935
+ "id": "1005025",
10936
+ "text": "AI Studio is now adults only and only uses AI providers that do not keep or train on your prompts; the terms and privacy pages explain the AI features, and everyone re-accepts the updated terms at their next sign-in."
10937
+ },
10938
+ {
10939
+ "id": "1004958",
10940
+ "text": "Every task now has a short two or three word name, and the AI writes \"task 1004745 - payout approvals\" instead of a bare number: in the startup notes, the sessions board, claim and ship messages, stuck-ship notes and blockers."
10941
+ },
10942
+ {
10943
+ "id": "1003182",
10944
+ "text": "The project now has an account record that shows every movement of money and why it happened: who was paid, for which task, and under which payment-processor transfer. Entries can never be edited or deleted, a retried payout c"
10945
+ },
10946
+ {
10947
+ "id": "1005024",
10948
+ "text": "The AI Studio works again with your own Google key even when Google retires a model, and you can now paste a picture or take a photo with your camera."
10949
+ },
10950
+ {
10951
+ "id": "1004996",
10952
+ "text": "People who signed in with Google and have not linked GitHub now see a tiny \"Link GitHub\" chip in the top bar, with the engineer icon so it reads as needed for coding. One click starts linking. It disappears once GitHub is link"
10953
+ },
10954
+ {
10955
+ "id": "1005023",
10956
+ "text": "The slop reminder no longer pops up on the app's own status messages, only when someone actually corrects the AI."
10957
+ },
10958
+ {
10959
+ "id": "1005018",
10960
+ "text": "People using a screen reader on Market Watch now hear the right name for the pop-up they opened (\"stop tracking\" or \"send a new scout\") instead of always \"Propose a change\"."
10961
+ },
10962
+ {
10963
+ "id": "1005021",
10964
+ "text": "Each builder's computer now updates its own project folder at the start of a session, saving any old leftovers first, and tells them plainly when it can't."
10965
+ },
10966
+ {
10967
+ "id": "1004957",
10968
+ "text": "Ask the AI \"what are my sessions doing\" (or type /session-board) and it shows a table of everything you have going at once: each session, its task, whether it is building, waiting to land, back from review or stuck, and what i"
10969
+ },
10970
+ {
10971
+ "id": "1005016",
10972
+ "text": "When the owner approves a member's correction to a Market Watch finding, the finding now actually changes (or is marked wrong), the item page shows a small \"corrected\" note, and who, when and why are kept."
10973
+ },
10926
10974
  {
10927
10975
  "id": "1004906",
10928
10976
  "text": "The payment processor is chosen: Stripe Connect. Each project's money will sit in its own Stripe account, so we never hold anyone's money. J.P. Morgan Payments is recorded as the backup. Nothing is bought or opened yet, and th"
@@ -12,6 +12,7 @@ const path = require('node:path');
12
12
  const { cliClient, requireSession, fmtTouches, printOnboardingNudge, rewardPolicy, costPlusOnlyNote, detectWorktreeName, cliExit } = require('./cli-lib');
13
13
  const { nextNumbers, padAdr, padMig } = require('./next-number');
14
14
  const { CORE_NS } = require('./migration-namespace');
15
+ const { taskRef } = require('../../modules/lifecycle/task-name.js'); // task 1004958
15
16
  const { contextPack, formatContextPack, resolveWorktreeRoot } = require('./context-pack');
16
17
  const { printClaimRecall } = require('./claim-recall');
17
18
  const { decideWorktreeClaim, ESCAPE_FLAG } = require('./worktree-claim-guard');
@@ -238,6 +239,7 @@ function printClaimCard(task, widgetsEnabled = true) {
238
239
  const model = shipCard.buildCardModel({
239
240
  stage: 'claimed',
240
241
  taskId: task.id,
242
+ shortName: task.short_name,
241
243
  title: task.title,
242
244
  description: task.description || '',
243
245
  estMinutes: task.est_minutes != null ? Number(task.est_minutes) : null,
@@ -391,13 +393,13 @@ function claimFailureGuidance({ code, message, d }, r, taskId, ctx) {
391
393
  // so this one leaves it alone instead of piling onto the same /strand-fix.
392
394
  const leases = (ctx && ctx.strandLeases) || [];
393
395
  for (const t of owed) {
394
- lines.push(` - task ${t.id}${t.title ? ` ${clip(t.title)}` : ''}`);
396
+ lines.push(` - ${taskRef(t.id, t.short_name)}${t.title ? ` ${clip(t.title)}` : ''}`);
395
397
  if (t.reason) lines.push(` flagged: ${clip(t.reason)}`);
396
398
  const held = leases.find((l) => String(l.task_id) === String(t.id) && !l.mine);
397
399
  if (held) lines.push(` already being fixed by ${held.by} — leave it to that session`);
398
400
  }
399
401
  if (waiting.length) lines.push(' Waiting, not on you (set aside; they land on their own):');
400
- for (const t of waiting) lines.push(` - task ${t.id}${t.title ? ` ${clip(t.title)}` : ''} — ${clip(t.waits_on || t.reason || 'outside you')}`);
402
+ for (const t of waiting) lines.push(` - ${taskRef(t.id, t.short_name)}${t.title ? ` ${clip(t.title)}` : ''} — ${clip(t.waits_on || t.reason || 'outside you')}`);
401
403
  lines.push(` ${clip(d.hint, 600) || 'Clear it with /strand-fix N, or rebase the branch and re-ship — or /builder-release it to give up on one.'}`);
402
404
  return { lines, exit: QUEUE_GATED_EXIT };
403
405
  }
@@ -118,6 +118,7 @@ function buildShippedEnvelope(crumb, task) {
118
118
  ...inputs,
119
119
  postGradeStatus: 'shipped',
120
120
  taskId: crumb.taskId,
121
+ shortName: inputs.shortName || (task && task.short_name),
121
122
  // Prefer a fact from the fresh task row when the breadcrumb didn't carry it.
122
123
  title: inputs.title || (task && task.title) || `Task #${crumb.taskId}`,
123
124
  valueSummary: inputs.valueSummary || (task && task.value_summary) || '',
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // scripts/bongos/report-checkout.js — tell the server how far behind this computer's
3
+ // project folder is (task 1005021).
4
+ //
5
+ // WHY. Session start heals a stale main folder when it safely can; when it cannot, nobody
6
+ // would know — the owner's sat 1,000 changes behind for a week. This sends the facts to
7
+ // PATCH /me/checkout-freshness, and GET /me then tells the person and their agent.
8
+ // Spawned detached by .claude/hooks/session-start.js, so it never delays a session.
9
+ // Silent and fail-open: no session, offline, an older server — nothing happens.
10
+ //
11
+ // Usage: node scripts/bongos/report-checkout.js <behind> <branch> <reason>
12
+
13
+ 'use strict';
14
+
15
+ const { apiCall, readSessionToken, cliExit } = require('./cli-lib');
16
+
17
+ async function main() {
18
+ const [behindRaw, branch, reason] = process.argv.slice(2);
19
+ const behind = Number.parseInt(behindRaw, 10);
20
+ if (!Number.isInteger(behind) || behind < 0 || !reason) return cliExit(0);
21
+ if (!readSessionToken()) return cliExit(0);
22
+ try {
23
+ await apiCall('PATCH', '/api/bongos/me/checkout-freshness',
24
+ { behind, branch: branch && branch !== 'HEAD' ? branch : '', reason },
25
+ { allowUnauthenticated: true });
26
+ } catch (_) { /* fail-open */ }
27
+ return cliExit(0);
28
+ }
29
+
30
+ if (require.main === module) main();
@@ -266,6 +266,13 @@ const INTEGRATION = new Set([
266
266
  // cannot be edited, deleted or truncated; unknown types and blank amounts are
267
267
  // refused; the reads' SQL resolves a task's own set over the project's.
268
268
  'pay_terms_db',
269
+ // task 1004991 (ADR 0385 pay-type gates): the gate table from the real migration —
270
+ // append-only, no eu_labour row, evidence required — and the verdict SQL.
271
+ 'pay_type_gates_db',
272
+ // task 1004958: core_289's CHECK and the task-name backfill against real Postgres.
273
+ 'task_name_db',
274
+ // task 1004958: the blocker read's lateral join that names held tasks, against real Postgres.
275
+ 'blocker_tasks_named_db',
269
276
  // task 1004988 (ADR 0385 D2): credit_log from the real migration — Postgres
270
277
  // refuses a spend, redemption or transfer row; only a reversal of an existing
271
278
  // row may be negative, and core_194's clawback still books.
@@ -274,6 +281,10 @@ const INTEGRATION = new Set([
274
281
  // Postgres, the writer's ON CONFLICT turns it into a no-op, and a ledger already
275
282
  // holding a duplicate still migrates (a warning, no index, nothing deleted).
276
283
  'credit_log_idea_unique_db',
284
+ // task 1003182: the project-account record from the real migration — Postgres refuses a
285
+ // causeless or wrongly-directed movement, any UPDATE/DELETE/TRUNCATE, and a second row
286
+ // for one processor reference.
287
+ 'project_account_db',
277
288
  // task 1004408: migration platform_identity_028 deletes the old hub invite's
278
289
  // pending rows before a cutoff it reads from the core_upgrades ledger. Only a real
279
290
  // engine runs the DO block, the numeric version compare and the one-shot guard.
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ //
4
+ // scripts/bongos/sessions-board.js — every session a builder has going, and where they
5
+ // are jammed, as a board the AI shows in chat (task 1004957).
6
+ //
7
+ // WHY. A builder running several sessions at once could not see where things were stuck:
8
+ // which session was building, which piece was waiting to land, which had come back from
9
+ // review, which ship was stuck — and the one that matters most, that new work was paused
10
+ // for EVERY session because too many ships were stuck (the REBASE_REQUIRED queue gate).
11
+ // The owner wanted this as a board the AI shows in chat (/session-board), not a hall
12
+ // page, plus a one-line version at session start when something is jammed, so a new
13
+ // session knows not to pile on.
14
+ //
15
+ // WHERE IT READS. Nothing new on the server: GET /me already carries the builder's active
16
+ // claims (one per session, with the worktree it lives in) and the stuck ships that gate
17
+ // them (needs_rebase_tasks); the confirmed and completed task lists say what is waiting to
18
+ // land and what came back from review. Whether the gate is shut is the server's own rule
19
+ // (modules/lifecycle/rebase-gate.js), read here rather than re-derived. Who is fixing a
20
+ // stuck ship is the local strand lease (strand-lease.js, task 1004956).
21
+ //
22
+ // Usage:
23
+ // node scripts/bongos/sessions-board.js # the board, as markdown for chat
24
+ // node scripts/bongos/sessions-board.js --json # the same, as data
25
+ // node scripts/bongos/sessions-board.js --line # ONE line at session start, only when jammed
26
+ //
27
+ // The same line also rides strand-watch.js --line (the stuck-ship startup note) through
28
+ // lineNow(), which is how it reaches a new session: the owner chose that over adding a
29
+ // startup step of its own (2026-10-30).
30
+ //
31
+ // --line is FAIL-OPEN and SILENT when nothing is jammed, and carries ids and fixed words
32
+ // only — never a task title, which is builder-authored text injected into every session
33
+ // (the strand-watch.js trust boundary).
34
+
35
+ const { apiCall, hasFlag } = require('./cli-lib');
36
+ const { splitRebaseGate } = require('../../modules/lifecycle/rebase-gate.js');
37
+ // task 1004958: every task is written "task N - <name>"; safeTaskName passes only the
38
+ // generated name's fixed shape, so it is safe in the startup line too.
39
+ const { safeTaskName, taskRef } = require('../../modules/lifecycle/task-name.js');
40
+
41
+ const PAGE_LIMIT = 200;
42
+ const safeId = (v) => (/^\d{1,19}$/.test(String(v)) ? String(v) : null);
43
+
44
+ // A stuck ship's reason, in words a person reads.
45
+ const REASON_WORDS = Object.freeze({
46
+ land_not_proven: 'it may have landed, but that could not be proven',
47
+ pr_not_landed: 'its change is waiting to merge',
48
+ unresolvable_conflict: 'its change overlaps newer work',
49
+ branch_modifies_executed_code: 'it changes code the merge helper will not run; a person lands it',
50
+ red_check: 'a check failed on its change',
51
+ });
52
+ function reasonWords(reason) {
53
+ const key = String(reason || '').replace(/^strand:/, '').split(':')[0];
54
+ if (/^waiting:/.test(String(reason || ''))) return `waiting on ${String(reason).slice(8)}`;
55
+ return REASON_WORDS[key] || 'stuck on its way in';
56
+ }
57
+
58
+ function ageWords(iso, now) {
59
+ const t = Date.parse(iso || '');
60
+ if (!Number.isFinite(t)) return '';
61
+ const h = (now - t) / 3600000;
62
+ if (h < 1) return `${Math.max(1, Math.round(h * 60))}m`;
63
+ if (h < 48) return `${Math.round(h)}h`;
64
+ return `${Math.round(h / 24)}d`;
65
+ }
66
+
67
+ const upper = (t) => t.charAt(0).toUpperCase() + t.slice(1);
68
+
69
+ // boardOf(input) → { sessions, landing, back, stuck, gate, jams }. PURE.
70
+ // me GET /me (active_claims, needs_rebase_tasks, builder.id)
71
+ // confirmed confirmed tasks (any builder; filtered to this one by shipped_by)
72
+ // completed completed tasks (same)
73
+ // leases live strand leases [{ task_id, holder, label, taken_at }]
74
+ // here this session's worktree { holder, label } or null
75
+ function boardOf({ me, confirmed = [], completed = [], leases = [], here = null, now = Date.now(), truncated = false } = {}) {
76
+ const meId = me && me.builder && me.builder.id != null ? String(me.builder.id) : null;
77
+ const claims = (me && Array.isArray(me.active_claims)) ? me.active_claims : [];
78
+ const owed = (me && Array.isArray(me.needs_rebase_tasks)) ? me.needs_rebase_tasks : [];
79
+ const mine = (t) => t && meId && String(t.shipped_by || t.claimed_by || '') === meId;
80
+ const stuckIds = new Set(owed.map((t) => String(t.id)));
81
+ const leaseFor = (id) => leases.find((l) => String(l.task_id) === String(id)) || null;
82
+
83
+ const sessions = claims.map((c) => ({
84
+ task_id: String(c.task_id),
85
+ name: safeTaskName(c.short_name),
86
+ title: c.title || null,
87
+ session: c.worktree_name || null,
88
+ here: !!(here && c.worktree_name && c.worktree_name === here.label),
89
+ stage: 'building',
90
+ since: ageWords(c.claimed_at, now),
91
+ waiting_on: c.release_requested_at ? 'someone asked for this task back' : null,
92
+ }));
93
+ const landing = confirmed.filter((t) => mine(t) && !stuckIds.has(String(t.id))).map((t) => ({
94
+ task_id: String(t.id), name: safeTaskName(t.short_name), title: t.title || null, stage: 'waiting to land', since: ageWords(t.updated_at, now),
95
+ }));
96
+ const back = completed.filter(mine).map((t) => ({
97
+ task_id: String(t.id), name: safeTaskName(t.short_name), title: t.title || null, stage: 'back from review', since: ageWords(t.updated_at, now),
98
+ waiting_on: 'fix what review found and ask again (or an artist approves a held page)',
99
+ }));
100
+ const split = splitRebaseGate(owed);
101
+ const stuck = owed.map((t) => {
102
+ const lease = leaseFor(t.id);
103
+ const waits = split.waiting.find((w) => String(w.id) === String(t.id));
104
+ return {
105
+ task_id: String(t.id),
106
+ name: safeTaskName(t.short_name),
107
+ title: t.title || null,
108
+ stage: 'stuck',
109
+ since: ageWords(t.needs_rebase_at, now),
110
+ why: reasonWords(t.needs_rebase_reason),
111
+ set_aside: !!waits,
112
+ fixer: lease ? (here && lease.holder === here.holder ? 'this session' : `session ${String(lease.label || '').replace(/[^\w.-]/g, '') || 'another session'}`) : null,
113
+ };
114
+ });
115
+ const gate = { shut: split.gates, fixable: split.fixable.length };
116
+
117
+ const jams = [];
118
+ if (gate.shut) {
119
+ jams.push({
120
+ kind: 'paused',
121
+ text: `New work is paused for every one of your sessions: ${split.fixable.length} stuck ship${split.fixable.length === 1 ? '' : 's'} must come down first.`,
122
+ task_ids: split.fixable.map((t) => String(t.id)),
123
+ });
124
+ }
125
+ for (const s of stuck) {
126
+ if (s.set_aside) continue;
127
+ jams.push({
128
+ kind: 'stuck',
129
+ text: s.fixer ? `${upper(taskRef(s.task_id, s.name))} is stuck (${s.why}); ${s.fixer} is fixing it.` : `${upper(taskRef(s.task_id, s.name))} is stuck (${s.why}); nobody is fixing it yet.`,
130
+ task_ids: [s.task_id],
131
+ fixer: s.fixer,
132
+ });
133
+ }
134
+ // A full page of confirmed or completed tasks means older ones of this builder's may
135
+ // be past it: the board says so rather than passing a capped list off as complete.
136
+ return { sessions, landing, back, stuck, gate, jams, truncated: !!truncated };
137
+ }
138
+
139
+ // The board as markdown for chat: jams first, then one table.
140
+ function markdown(b) {
141
+ const out = [];
142
+ out.push('**Jams**');
143
+ if (!b.jams.length) out.push('- Nothing is jammed.');
144
+ for (const j of b.jams) out.push(`- ${j.text}`);
145
+ out.push('');
146
+ const rows = [
147
+ ...b.sessions.map((s) => [s.session ? `${s.session}${s.here ? ' (this one)' : ''}` : '(no folder recorded)', s, s.waiting_on || '—']),
148
+ ...b.stuck.map((s) => ['—', s, s.set_aside ? `${s.why} (set aside; it does not pause new work)` : s.fixer ? `${s.why}; ${s.fixer} is fixing it` : `${s.why}; nobody is fixing it`]),
149
+ ...b.landing.map((s) => ['—', s, 'the merge; nothing for you to do']),
150
+ ...b.back.map((s) => ['—', s, s.waiting_on]),
151
+ ];
152
+ if (!rows.length) {
153
+ out.push('No sessions are building anything, and nothing of yours is in flight.');
154
+ return out.join('\n');
155
+ }
156
+ // A title is builder-authored: no link, emphasis, code or tag syntax reaches the chat,
157
+ // and a pipe cannot break the table.
158
+ const cell = (v) => String(v == null ? '' : v).replace(/[`*_[\]<>#~]/g, '').replace(/\|/g, '\\|').replace(/\s+/g, ' ').trim();
159
+ out.push('| Session | Task | Stage | Waiting on |');
160
+ out.push('|---|---|---|---|');
161
+ for (const [session, s, waits] of rows) {
162
+ const title = s.title ? ` — ${cell(s.title).slice(0, 70)}` : '';
163
+ out.push(`| ${cell(session)} | ${taskRef(s.task_id, s.name)}${title} | ${s.stage}${s.since ? ` (${s.since})` : ''} | ${cell(waits)} |`);
164
+ }
165
+ if (b.truncated) out.push('', 'More tasks were waiting to land or back from review than one read returns, so some older ones of yours may be missing from this board.');
166
+ return out.join('\n');
167
+ }
168
+
169
+ // ONE line for session start, or null. Only the pause is worth interrupting a new session
170
+ // for — single stuck ships are strand-watch's line already. Ids, generated task names
171
+ // (safeTaskName) and fixed words only.
172
+ function line(b) {
173
+ if (!b || !b.gate.shut) return null;
174
+ const paused = b.jams.find((j) => j.kind === 'paused');
175
+ const ids = (paused ? paused.task_ids : []).map(safeId).filter(Boolean);
176
+ const nameOf = (id) => { const s = b.stuck.find((x) => x.task_id === id); return taskRef(id, s && s.name); };
177
+ const fixing = b.stuck.filter((s) => s.fixer && ids.includes(s.task_id)).map((s) => `${taskRef(s.task_id, s.name)} by ${s.fixer.replace(/[^\w .-]/g, '')}`);
178
+ const free = ids.filter((id) => !b.stuck.some((s) => s.task_id === id && s.fixer));
179
+ return `Sessions board: new work is paused for every session of this builder until ${ids.length} stuck ship${ids.length === 1 ? '' : 's'} (${ids.map(nameOf).join(', ')}) come down. `
180
+ + (fixing.length ? `Already being fixed: ${fixing.join('; ')}. ` : '')
181
+ + (free.length ? `Nobody is on ${free.map(nameOf).join(', ')} yet — take one with /strand-fix, or do unclaimed work. ` : 'Do not pile on: carry on with unclaimed work. ')
182
+ + 'This note is for you, not the builder; show them /session-board only if they ask where things are stuck.';
183
+ }
184
+
185
+ // Who is fixing which stuck ship on this machine (strand-lease.js), and which session
186
+ // this is. No checkout: nobody is fixing anything that we can see.
187
+ function localLeases() {
188
+ try {
189
+ const lease = require('./strand-lease.js');
190
+ return { here: lease.currentHolder(), leases: lease.liveLeases({ dir: lease.leaseDir() }) };
191
+ } catch { return { here: null, leases: [] }; }
192
+ }
193
+
194
+ async function read() {
195
+ const body = (r) => (r && (r.data || r)) || {};
196
+ const [me, confirmed, completed] = await Promise.all([
197
+ apiCall('GET', '/api/bongos/me').then(body),
198
+ apiCall('GET', `/api/bongos/tasks?status=confirmed&limit=${PAGE_LIMIT}`).then((r) => body(r).tasks || []).catch(() => []),
199
+ apiCall('GET', `/api/bongos/tasks?status=completed&limit=${PAGE_LIMIT}`).then((r) => body(r).tasks || []).catch(() => []),
200
+ ]);
201
+ const { leases, here } = localLeases();
202
+ const truncated = confirmed.length >= PAGE_LIMIT || completed.length >= PAGE_LIMIT;
203
+ return { me, confirmed, completed, leases, here, truncated };
204
+ }
205
+
206
+ // THE STARTUP READ, shared across session starts. The pause needs only the stuck ships
207
+ // on /me, so the line asks for nothing else, and the answer is cached for every session
208
+ // on the machine (probe-cache, the strand-watch line's own cache): four sessions opened
209
+ // together make ONE call, not four times three. Stuck ships are measured in hours, so a
210
+ // few minutes' age is as true as fresh. A failed read throws, so it is never cached.
211
+ const OWED_CACHE_KEY = 'sessions-board-owed';
212
+ const OWED_FRESH_MS = 5 * 60 * 1000;
213
+ async function readOwed() {
214
+ const { cached } = require('./probe-cache.js');
215
+ const { value } = await cached(OWED_CACHE_KEY, async () => {
216
+ const r = await apiCall('GET', '/api/bongos/me');
217
+ const me = (r && (r.data || r)) || {};
218
+ if (!me.builder || !Array.isArray(me.needs_rebase_tasks)) throw new Error('unreadable');
219
+ return { builder: { id: me.builder.id }, needs_rebase_tasks: me.needs_rebase_tasks };
220
+ }, { ttlMs: OWED_FRESH_MS });
221
+ const { leases, here } = localLeases();
222
+ return { me: value, leases, here };
223
+ }
224
+
225
+ // The startup line, read and bounded: the line, or null on silence, a slow read or any
226
+ // failure. Never throws.
227
+ async function lineNow({ deadlineMs = 4000 } = {}) {
228
+ try {
229
+ const deadline = new Promise((_, rej) => setTimeout(() => rej(new Error('deadline')), deadlineMs).unref());
230
+ return line(boardOf(await Promise.race([readOwed(), deadline])));
231
+ } catch { return null; }
232
+ }
233
+
234
+ // The chat board as text. Fail OPEN: the board is read in chat, so a failed read is a
235
+ // sentence there, never a stack trace or a non-zero exit.
236
+ async function boardText({ json = false, readBoard = read } = {}) {
237
+ let input;
238
+ try {
239
+ input = await readBoard();
240
+ } catch (e) {
241
+ return json
242
+ ? JSON.stringify({ available: false, reason: String((e && e.message) || e) }, null, 2)
243
+ : '**Jams**\n- The board could not be read just now (the server did not answer). Nothing is known to be jammed or clear; try again in a minute.';
244
+ }
245
+ const b = boardOf(input);
246
+ return json ? JSON.stringify(b, null, 2) : markdown(b);
247
+ }
248
+
249
+ async function main() {
250
+ if (hasFlag('--line')) {
251
+ const one = await lineNow();
252
+ if (one) console.log(one);
253
+ process.exitCode = 0;
254
+ return;
255
+ }
256
+ console.log(await boardText({ json: hasFlag('--json') }));
257
+ process.exitCode = 0;
258
+ }
259
+
260
+ if (require.main === module) {
261
+ main().catch((e) => { console.error('sessions-board: failed —', e.message || e); process.exitCode = 1; });
262
+ }
263
+
264
+ module.exports = { boardOf, markdown, boardText, line, lineNow, readOwed, reasonWords, OWED_CACHE_KEY };
@@ -344,6 +344,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
344
344
  stage, // task 1004379: 'held' from finishHeld; undefined ⇒ derived from postGradeStatus
345
345
  postGradeStatus,
346
346
  taskId,
347
+ shortName: card.shortName,
347
348
  title: card.title,
348
349
  valueSummary: card.valueSummary,
349
350
  notes: card.notes,
@@ -442,6 +442,7 @@ async function shipMain() {
442
442
  const model = shipCard.buildCardModel({
443
443
  stage: 'review',
444
444
  taskId,
445
+ shortName: ac.short_name,
445
446
  title: ac.title,
446
447
  valueSummary: summary,
447
448
  fileCount: committedFiles.length || null,
@@ -780,6 +781,7 @@ async function shipMain() {
780
781
  taskId, summary, advanceToMerge, skipMerge, postGradeStatus, doneLine: 'Done.', dbOnly, held,
781
782
  // task 1270: data for the deterministic completion card.
782
783
  card: {
784
+ shortName: ac.short_name,
783
785
  title: ac.title,
784
786
  valueSummary: summary,
785
787
  notes,
@@ -968,7 +970,7 @@ async function regradeMain(opts = {}) {
968
970
  await attachVisual();
969
971
  await finishShip({
970
972
  taskId, summary: summaryArg || task.value_summary || '', held: true, postGradeStatus: 'completed', doneLine: 'Done.',
971
- card: { title: task.title, valueSummary: summaryArg || task.value_summary || '', notes: '' },
973
+ card: { shortName: task.short_name, title: task.title, valueSummary: summaryArg || task.value_summary || '', notes: '' },
972
974
  });
973
975
  return;
974
976
  }
@@ -1088,7 +1090,7 @@ async function regradeMain(opts = {}) {
1088
1090
  taskId, summary, advanceToMerge: confirmedStatus === 'confirmed', skipMerge, dbOnly: true,
1089
1091
  postGradeStatus: confirmedStatus,
1090
1092
  doneLine: 'Done — confirmed with no artifact (grader had nothing to read).',
1091
- card: { title: task.title, valueSummary: summary, notes: '' },
1093
+ card: { shortName: task.short_name, title: task.title, valueSummary: summary, notes: '' },
1092
1094
  });
1093
1095
  return;
1094
1096
  }
@@ -1190,6 +1192,7 @@ async function regradeMain(opts = {}) {
1190
1192
  doneLine: g.passed ? 'Done — re-grade passed.' : 'Done — re-grade still failing; fix and try again.',
1191
1193
  // task 1270: completion card for the re-grade path too.
1192
1194
  card: {
1195
+ shortName: task.short_name,
1193
1196
  title: task.title,
1194
1197
  valueSummary: summary,
1195
1198
  // Empty by design, not by omission (task 1003702). A re-grade takes no
@@ -35,6 +35,7 @@
35
35
  // node scripts/bongos/strand-lease.js release <task-id> # drop this worktree's lease
36
36
  // node scripts/bongos/strand-lease.js show [<task-id>] # who holds what
37
37
 
38
+ const { taskRef } = require('../../modules/lifecycle/task-name.js'); // task 1004958
38
39
  const fs = require('node:fs');
39
40
  const path = require('node:path');
40
41
 
@@ -197,11 +198,12 @@ function describe(lease, now = Date.now()) {
197
198
  }
198
199
 
199
200
  // The clause the strand note appends: the strands OTHER sessions hold, so this one leaves
200
- // them alone. Null when nobody else holds one.
201
- function othersClause(leases, holder, now = Date.now()) {
201
+ // them alone. Null when nobody else holds one. `names` maps a task id to its short name
202
+ // (task 1004958) — a lease is local and carries none, so the caller hands in what it knows.
203
+ function othersClause(leases, holder, now = Date.now(), names = {}) {
202
204
  const others = (leases || []).filter((l) => l.holder !== holder);
203
205
  if (!others.length) return null;
204
- const list = others.map((l) => `task ${safeId(l.task_id)} by ${describe(l, now)}`).join('; ');
206
+ const list = others.map((l) => `${taskRef(safeId(l.task_id), names[String(l.task_id)])} by ${describe(l, now)}`).join('; ');
205
207
  return `Already being fixed: ${list}. Leave ${others.length === 1 ? 'that one' : 'those'} to that session and carry on with your own work.`;
206
208
  }
207
209