@bongos/core 1.23.22 → 1.24.2

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 (435) hide show
  1. package/.bongos-core.json +933 -413
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +96 -0
  27. package/clients/bongos-client/index.cjs +96 -0
  28. package/clients/bongos-client/index.d.ts +155 -4
  29. package/clients/bongos-client/index.mjs +96 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
  33. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  34. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  35. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  36. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  37. package/docs/adr/README.md +6 -2
  38. package/docs/api/openapi.json +8651 -5833
  39. package/docs/api-reference.md +66 -10
  40. package/docs/architecture.md +37 -1
  41. package/docs/canonical-permissions.md +1 -1
  42. package/docs/copy-inventory.md +427 -339
  43. package/docs/copy-registry.json +1376 -500
  44. package/docs/file-map.md +1 -0
  45. package/docs/module-api-changelog.md +7 -0
  46. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  47. package/docs/onboarding/diagrams/README.md +1 -1
  48. package/docs/onboarding/drachmae-and-karma.md +15 -15
  49. package/docs/onboarding/primer.md +2 -2
  50. package/docs/page-inventory.json +57 -7
  51. package/docs/page-readings.json +1210 -1027
  52. package/docs/recipes/google-sign-in.md +60 -0
  53. package/docs/recipes/ops-gotchas.md +1 -1
  54. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  55. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  56. package/migrations/core_283_payout_approvals.sql +183 -0
  57. package/migrations/core_284_payout_gates.sql +179 -0
  58. package/migrations/core_285_task_needs.sql +74 -0
  59. package/modules/ai-studio/billing.js +67 -0
  60. package/modules/ai-studio/context.js +29 -0
  61. package/modules/ai-studio/db.js +172 -0
  62. package/modules/ai-studio/gemini.js +75 -0
  63. package/modules/ai-studio/generate.js +119 -0
  64. package/modules/ai-studio/keys.js +73 -0
  65. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  66. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  67. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  68. package/modules/ai-studio/openrouter.js +90 -0
  69. package/modules/ai-studio/payments.js +105 -0
  70. package/modules/ai-studio/pricing.js +92 -0
  71. package/modules/ai-studio/routes/ai-studio.js +235 -5
  72. package/modules/ai-studio/studio.js +121 -0
  73. package/modules/autonomy/fence.js +64 -1
  74. package/modules/builder-settings/builder-needs.js +27 -1
  75. package/modules/copy-desk/routes/copy-desk.js +5 -0
  76. package/modules/economy/achievements.js +22 -10
  77. package/modules/economy/cost-classify.js +9 -0
  78. package/modules/economy/cost-write-guard.js +52 -0
  79. package/modules/economy/cost.js +2 -1
  80. package/modules/economy/credits.js +18 -15
  81. package/modules/economy/goal-reward.js +161 -0
  82. package/modules/economy/reward.js +4 -0
  83. package/modules/economy/routes/cost.js +33 -7
  84. package/modules/government/access-routes.js +70 -0
  85. package/modules/government/access.js +88 -0
  86. package/modules/government/asks.js +5 -2
  87. package/modules/government/catalog.js +6 -0
  88. package/modules/government/db.js +72 -0
  89. package/modules/government/docket.js +48 -2
  90. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  91. package/modules/government/migrations/government_036_access_holders.sql +33 -0
  92. package/modules/government/pollers/board-expiry.js +108 -20
  93. package/modules/government/protected-surfaces.json +1 -0
  94. package/modules/government/routes/government.js +31 -0
  95. package/modules/government/routing.js +49 -7
  96. package/modules/government/waiting.js +78 -0
  97. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  98. package/modules/hall-ui/public/ai-studio.css +193 -0
  99. package/modules/hall-ui/public/ai-studio.html +98 -0
  100. package/modules/hall-ui/public/ai-studio.js +556 -0
  101. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  102. package/modules/hall-ui/public/blockers-by-person.js +158 -0
  103. package/modules/hall-ui/public/blockers-lib.js +15 -1
  104. package/modules/hall-ui/public/blockers.html +8 -0
  105. package/modules/hall-ui/public/blockers.js +1 -1
  106. package/modules/hall-ui/public/claim-action.js +3 -0
  107. package/modules/hall-ui/public/collab.html +12 -0
  108. package/modules/hall-ui/public/craft-mode.js +4 -1
  109. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  110. package/modules/hall-ui/public/drachmae.html +1 -1
  111. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  115. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  120. package/modules/hall-ui/public/goal-inbox.js +1 -1
  121. package/modules/hall-ui/public/goals-render.js +4 -0
  122. package/modules/hall-ui/public/goals.html +8 -0
  123. package/modules/hall-ui/public/gov-living.js +6 -2
  124. package/modules/hall-ui/public/government-access.js +108 -0
  125. package/modules/hall-ui/public/government.html +21 -0
  126. package/modules/hall-ui/public/hall-ask.css +5 -0
  127. package/modules/hall-ui/public/hall-ask.js +39 -3
  128. package/modules/hall-ui/public/notes-panel.css +20 -0
  129. package/modules/hall-ui/public/notes-panel.js +146 -0
  130. package/modules/hall-ui/public/project-settings.html +1 -1
  131. package/modules/hall-ui/public/project-settings.js +7 -5
  132. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  133. package/modules/hall-ui/public/settings.html +15 -0
  134. package/modules/hall-ui/public/street-deck.js +12 -3
  135. package/modules/hall-ui/public/studio.css +90 -8
  136. package/modules/hall-ui/public/studio.html +39 -10
  137. package/modules/hall-ui/public/studio.js +148 -18
  138. package/modules/hall-ui/public/task.html +2 -0
  139. package/modules/hall-ui/public/task.js +4 -0
  140. package/modules/hall-ui/public/tweak-editor.js +12 -3
  141. package/modules/hall-ui/public/welcome-boot.css +42 -6
  142. package/modules/hall-ui/public/welcome-boot.js +20 -6
  143. package/modules/hall-ui/public/work.js +20 -0
  144. package/modules/hall-ui/records/ai-studio.md +36 -0
  145. package/modules/hall-ui/records/blockers-people.md +38 -0
  146. package/modules/lifecycle/db-claims.js +32 -4
  147. package/modules/lifecycle/db-goals.js +18 -5
  148. package/modules/lifecycle/db-tasks.js +15 -6
  149. package/modules/lifecycle/done-when.js +23 -1
  150. package/modules/lifecycle/lifecycle.js +5 -0
  151. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  152. package/modules/lifecycle/module.json +2 -1
  153. package/modules/lifecycle/notes.js +219 -0
  154. package/modules/lifecycle/page-notice.js +124 -0
  155. package/modules/lifecycle/page-tweak-artist.js +18 -1
  156. package/modules/lifecycle/publish-reconciler.js +67 -20
  157. package/modules/lifecycle/routes/claims.js +5 -2
  158. package/modules/lifecycle/routes/goals.js +3 -1
  159. package/modules/lifecycle/routes/notes.js +91 -0
  160. package/modules/lifecycle/routes/project-settings.js +43 -35
  161. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  162. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  163. package/modules/lifecycle/routes/tasks.js +12 -12
  164. package/modules/lifecycle/task-needs-gate.js +70 -0
  165. package/modules/market-watch/CLAUDE.md +15 -0
  166. package/modules/market-watch/db.js +340 -0
  167. package/modules/market-watch/key.js +58 -0
  168. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  169. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  170. package/modules/market-watch/module.json +26 -0
  171. package/modules/market-watch/pollers/hub-seed.js +54 -0
  172. package/modules/market-watch/pollers/weekly-run.js +100 -0
  173. package/modules/market-watch/research.js +350 -0
  174. package/modules/market-watch/run.js +245 -0
  175. package/modules/market-watch/seed/market-data.json +38 -0
  176. package/modules/market-watch/seed/partners-data.json +497 -0
  177. package/modules/market-watch/seed.js +252 -0
  178. package/modules/market-watch/store.js +230 -0
  179. package/modules/memory/routes/memory.js +14 -2
  180. package/modules/memory/routes/search.js +8 -2
  181. package/modules/platform-identity/account-logins.js +192 -0
  182. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  183. package/modules/platform-identity/platform-identity.js +5 -4
  184. package/modules/platform-identity/routes/my-projects.js +6 -0
  185. package/modules/platform-identity/routes/profile.js +18 -0
  186. package/modules/platform-identity/routes/public-profile.js +10 -1
  187. package/modules/platform-identity/routes/sso.js +26 -1
  188. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  189. package/modules/provisioning/credential-preflight.js +13 -3
  190. package/modules/provisioning/pollers/app-liveness.js +25 -5
  191. package/modules/provisioning/routes/provisioning.js +1 -1
  192. package/modules/provisioning/starter-bundles.js +6 -2
  193. package/modules/public-landing/public/account.html +10 -2
  194. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  195. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  196. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  197. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  198. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  199. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  200. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  201. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  202. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  203. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  205. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  209. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  210. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  213. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  214. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  221. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  222. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  223. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  224. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  225. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  226. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  227. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  228. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  229. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  230. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  233. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  244. package/modules/public-landing/public/community.html +1 -1
  245. package/modules/public-landing/public/connections.html +2 -2
  246. package/modules/public-landing/public/guild.html +1 -1
  247. package/modules/public-landing/public/invitations.html +2 -2
  248. package/modules/public-landing/public/projects.html +5 -5
  249. package/modules/raise/views/nda.html +2 -0
  250. package/modules/specialities/db.js +32 -1
  251. package/modules/specialities/routes/specialities.js +10 -6
  252. package/modules/status-ui/public/index.html +1 -1
  253. package/package-lock.json +2 -2
  254. package/package.json +1 -1
  255. package/release-notes.json +230 -0
  256. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  257. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  258. package/scripts/bongos/claim.js +22 -1
  259. package/scripts/bongos/db-backup-nightly.sh +29 -0
  260. package/scripts/bongos/deploy-divergence.js +7 -0
  261. package/scripts/bongos/doc-cli-guard.js +6 -0
  262. package/scripts/bongos/fitness.js +3 -2
  263. package/scripts/bongos/gen-diagrams.js +3 -3
  264. package/scripts/bongos/government-walk.js +3 -0
  265. package/scripts/bongos/lib.sh +4 -5
  266. package/scripts/bongos/migration-namespace.js +13 -1
  267. package/scripts/bongos/plain-cards.js +4 -0
  268. package/scripts/bongos/provision-github-setup.js +3 -1
  269. package/scripts/bongos/provision-units.js +6 -1
  270. package/scripts/bongos/publish-manifest.js +3 -0
  271. package/scripts/bongos/release.js +2 -0
  272. package/scripts/bongos/run-unit-tests.js +268 -82
  273. package/scripts/bongos/seed-tasks.example.json +56 -0
  274. package/scripts/bongos/seed-tasks.js +222 -0
  275. package/scripts/bongos/seed-verify-criteria.js +1 -2
  276. package/scripts/bongos/ship-finish.js +2 -0
  277. package/scripts/bongos/ship-resume.js +2 -0
  278. package/scripts/bongos/strand-lease.js +250 -0
  279. package/scripts/bongos/strand-watch.js +26 -5
  280. package/scripts/bongos/version-literal-guard.js +2 -2
  281. package/scripts/discord/post.js +4 -3
  282. package/scripts/gds/seed-tasks.js +7 -0
  283. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  284. package/scripts/hall-preview/server.js +21 -0
  285. package/src/bongos/auth-config.js +29 -1
  286. package/src/bongos/auth-discord.js +3 -3
  287. package/src/bongos/auth-github.js +3 -6
  288. package/src/bongos/auth-google.js +217 -0
  289. package/src/bongos/device-approval.js +140 -0
  290. package/src/bongos/llm-pricing.js +17 -1
  291. package/src/bongos/metrics.js +48 -2
  292. package/src/bongos/middleware/rate-limit.js +30 -11
  293. package/src/bongos/module-scope-map.js +12 -1
  294. package/src/bongos/optimizer.js +15 -1
  295. package/src/bongos/payout-approvals.js +342 -0
  296. package/src/bongos/payout-gates.js +364 -0
  297. package/src/bongos/prelaunch-gate.js +7 -9
  298. package/src/bongos/prelaunch-switch.js +20 -0
  299. package/src/bongos/route-rank-check.js +7 -0
  300. package/src/bongos/routes/auth-google.js +246 -0
  301. package/src/bongos/routes/auth.js +38 -1
  302. package/src/bongos/routes/backup.js +33 -9
  303. package/src/bongos/routes/builders.js +57 -30
  304. package/src/bongos/routes/github.js +9 -0
  305. package/src/bongos/routes/me.js +18 -0
  306. package/src/bongos/routes/my-payouts.js +133 -0
  307. package/src/bongos/routes/payout-approvals.js +312 -0
  308. package/src/bongos/routes/payout-gates.js +179 -0
  309. package/src/bongos/routes.js +15 -1
  310. package/src/bongos/secret-box.js +18 -3
  311. package/src/bongos/serve-internal.js +47 -19
  312. package/src/bongos/sign-in-pages.js +138 -0
  313. package/src/bongos/task-needs.js +146 -0
  314. package/src/bongos-downloads.js +18 -1
  315. package/src/instance-config.js +28 -0
  316. package/src/module-api.js +14 -1
  317. package/tests/access_holders.mjs +214 -0
  318. package/tests/account_logins.mjs +326 -0
  319. package/tests/account_logins_db.mjs +161 -0
  320. package/tests/ai_studio_billing.mjs +253 -0
  321. package/tests/ai_studio_generate.mjs +282 -0
  322. package/tests/ai_studio_keys.mjs +159 -0
  323. package/tests/ai_studio_requests.mjs +319 -0
  324. package/tests/auth_backchannel_logout_route.mjs +201 -0
  325. package/tests/auto_satisfy_criteria.mjs +10 -1
  326. package/tests/autobongos_tweak_lane.mjs +144 -0
  327. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  328. package/tests/backfill_subagent_rewards.mjs +17 -1
  329. package/tests/backup_trigger_lock.mjs +119 -0
  330. package/tests/blockers_people.mjs +198 -0
  331. package/tests/bounded_rate_maps.mjs +184 -0
  332. package/tests/claimable_blocker_status.mjs +1 -1
  333. package/tests/claims_batch_routes.mjs +421 -0
  334. package/tests/config_read_cache.mjs +171 -0
  335. package/tests/copy_desk_page_editor.mjs +12 -0
  336. package/tests/cost_write_guard.mjs +200 -0
  337. package/tests/craft_achievements.mjs +33 -10
  338. package/tests/criterion_goal_attach.mjs +1 -1
  339. package/tests/db_backup_nightly.mjs +73 -1
  340. package/tests/deploy_divergence_line.mjs +7 -0
  341. package/tests/effective_visibility_predicate.mjs +12 -0
  342. package/tests/fitness.mjs +19 -0
  343. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  344. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  345. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  346. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  347. package/tests/github_one_trip.mjs +34 -0
  348. package/tests/github_repo_visibility.mjs +20 -0
  349. package/tests/goal_achievement.mjs +30 -1
  350. package/tests/goal_achievement_reward.mjs +205 -0
  351. package/tests/goal_sort_staleness.mjs +2 -1
  352. package/tests/goal_tier.mjs +11 -1
  353. package/tests/google_fonts_nonblocking.mjs +84 -0
  354. package/tests/google_sign_in.mjs +401 -0
  355. package/tests/government_board_expiry.mjs +94 -0
  356. package/tests/government_board_membership.mjs +4 -1
  357. package/tests/government_protected_surfaces.mjs +1 -1
  358. package/tests/government_routes.mjs +1 -1
  359. package/tests/government_seed.mjs +1 -0
  360. package/tests/hall_ai_studio.mjs +347 -0
  361. package/tests/hall_page_gate_map.mjs +3 -0
  362. package/tests/hall_studio_home.mjs +61 -2
  363. package/tests/home_attention_cards.mjs +1 -1
  364. package/tests/human_signal.mjs +141 -0
  365. package/tests/install_script_reader.mjs +51 -0
  366. package/tests/integration_fixture_ids.mjs +82 -0
  367. package/tests/invite_expiry_db.mjs +13 -1
  368. package/tests/lib_sh_resolution.mjs +0 -4
  369. package/tests/market_watch_data.mjs +244 -0
  370. package/tests/market_watch_data_db.mjs +308 -0
  371. package/tests/market_watch_module.mjs +114 -0
  372. package/tests/market_watch_run.mjs +503 -0
  373. package/tests/metrics.mjs +124 -0
  374. package/tests/migration_namespace.mjs +14 -0
  375. package/tests/module_api.mjs +2 -0
  376. package/tests/module_loader.mjs +1 -1
  377. package/tests/notes.mjs +182 -0
  378. package/tests/own_scope_guard.mjs +5 -0
  379. package/tests/page_tweak_marker_sql.mjs +76 -0
  380. package/tests/page_tweak_no_block.mjs +135 -0
  381. package/tests/page_tweak_notice.mjs +103 -0
  382. package/tests/payout_approvals.mjs +253 -0
  383. package/tests/payout_approvals_db.mjs +106 -0
  384. package/tests/payout_gates.mjs +322 -0
  385. package/tests/payout_gates_db.mjs +113 -0
  386. package/tests/planning_session_skill.mjs +13 -3
  387. package/tests/project_settings_page.mjs +29 -5
  388. package/tests/project_settings_spend_cap_route.mjs +235 -0
  389. package/tests/promotion_sweep.mjs +29 -1
  390. package/tests/provision.mjs +3 -2
  391. package/tests/provisioning_app_status.mjs +18 -0
  392. package/tests/provisioning_credential_preflight.mjs +53 -0
  393. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  394. package/tests/publish_reconciler_overlap.mjs +60 -0
  395. package/tests/rank_change_broadcast_port.mjs +151 -0
  396. package/tests/seed_tasks.mjs +176 -0
  397. package/tests/seed_verify_criteria.mjs +2 -6
  398. package/tests/site_classified_once.mjs +142 -0
  399. package/tests/speciality_db_shape.mjs +23 -0
  400. package/tests/speciality_session_skills.mjs +48 -2
  401. package/tests/strand_lease.mjs +249 -0
  402. package/tests/strand_watch.mjs +10 -0
  403. package/tests/task_criteria_routes.mjs +275 -0
  404. package/tests/task_criteria_unlink_db.mjs +123 -0
  405. package/tests/task_needs.mjs +233 -0
  406. package/tests/task_needs_db.mjs +110 -0
  407. package/tests/unit_runner_serial.mjs +266 -43
  408. package/tests/version_literals.mjs +5 -7
  409. package/tests/welcome_boot.mjs +16 -5
  410. package/tests/welcome_boot_browser.mjs +36 -4
  411. package/docs/recipes/bongos-cli-release.md +0 -66
  412. package/scripts/bongos/backfill-disciplines.js +0 -144
  413. package/scripts/bongos/backfill-session-earnings.js +0 -123
  414. package/scripts/bongos/backfill-session-types.js +0 -87
  415. package/scripts/bongos/build-bongos.js +0 -94
  416. package/scripts/bongos/cleanup-orphans.js +0 -74
  417. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  418. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  419. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  420. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  421. package/scripts/bongos/smoke-batch-release.sh +0 -316
  422. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  423. package/scripts/bongos/smoke-dependencies.sh +0 -143
  424. package/scripts/gds/backfill-disciplines.js +0 -7
  425. package/scripts/gds/backfill-session-earnings.js +0 -7
  426. package/scripts/gds/backfill-session-types.js +0 -7
  427. package/scripts/gds/cleanup-orphans.js +0 -7
  428. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  429. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  430. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  431. package/scripts/gds/smoke-batch-claim.sh +0 -8
  432. package/scripts/gds/smoke-batch-release.sh +0 -8
  433. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  434. package/scripts/gds/smoke-dependencies.sh +0 -8
  435. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -1,72 +1,295 @@
1
1
  // tests/unit_runner_serial.mjs
2
2
  //
3
- // The `unit` gate runs its tests SERIALLY, one child process each — pinned here
4
- // because that property is now load-bearing, not incidental (task 1001435).
3
+ // The `unit` gate runs several test files at once, and keeps the files that share state
4
+ // outside their own process in a SERIAL lane that runs alone (task 1004926). This file used
5
+ // to pin the opposite — that the runner was strictly one-at-a-time (task 1001435) — by
6
+ // reading the runner's source. It now drives the runner's own pieces instead:
5
7
  //
6
- // A flaky-`unit` report (idea #353) diagnosed "parallel execution racing on shared
7
- // global state" and asked, correctly, whether run-unit-tests fans tests out
8
- // concurrently in one process. It does not: runOne calls execFileSync, which
9
- // BLOCKS, from plain sequential for-loops. That rules an in-process race out by
10
- // construction, and scripts/bongos/run-unit-tests.js now records the audit that
11
- // followed from it — including that two of the three suites the report named
12
- // (fitness, session_index) mutate no global state at all.
13
- //
14
- // All of that reasoning is only true while execution stays serial. Make the runner
15
- // concurrent and every word of it silently becomes wrong, with no test to say so —
16
- // so this suite fails if that happens, and whoever does it has to revisit the note
17
- // rather than leave a stale explanation behind.
18
- //
19
- // Static source assertions on purpose: actually observing concurrency would mean
20
- // running the whole gate from inside the gate.
8
+ // - the pool size: UNIT_TESTS_SERIAL=1 forces one at a time, UNIT_TEST_JOBS=N sets it,
9
+ // the default is the CPU count;
10
+ // - which files go to the serial lane: a browser launch, a fixed listen port, a fixed
11
+ // temp path, or a name in SERIAL;
12
+ // - the scheduler: a serial-lane file never overlaps anything, the pool never exceeds
13
+ // its size, and output prints in entry order whatever order the files finish in;
14
+ // - the real child-process path: pass, fail, a stdin read, and a timeout, through the
15
+ // same runFile the gate uses.
21
16
  //
22
17
  // Run: node tests/unit_runner_serial.mjs
23
18
 
24
19
  import { strict as assert } from 'node:assert';
25
20
  import fs from 'node:fs';
21
+ import os from 'node:os';
26
22
  import path from 'node:path';
23
+ import { createRequire } from 'node:module';
27
24
  import { fileURLToPath } from 'node:url';
28
25
 
29
26
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
- const RUNNER = path.join(ROOT, 'scripts', 'bongos', 'run-unit-tests.js');
31
- const src = fs.readFileSync(RUNNER, 'utf8');
27
+ const require = createRequire(import.meta.url);
28
+ const runner = require(path.join(ROOT, 'scripts', 'bongos', 'run-unit-tests.js'));
29
+ const { resolveJobs, serialReason, runFile, runEntries, renderResult, SERIAL, UNIT_TESTS_SERIAL_ENV, UNIT_TEST_JOBS_ENV } = runner;
32
30
 
33
31
  let passed = 0;
34
32
  let failed = 0;
35
- function t(name, fn) {
36
- try { fn(); passed++; console.log(` PASS ${name}`); }
33
+ async function t(name, fn) {
34
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
37
35
  catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
38
36
  }
39
37
 
40
- t('each test runs in its OWN process, spawned synchronously', () => {
41
- assert.match(src, /execFileSync\(\s*process\.execPath/,
42
- 'the runner must spawn each test with execFileSync (blocking, one process per test)');
38
+ // ── pool size ────────────────────────────────────────────────────────────────────────
39
+
40
+ await t('default pool size is the CPU count', () => {
41
+ assert.equal(resolveJobs({}, 8), 8);
42
+ assert.equal(resolveJobs({}, 0), 1, 'never below one');
43
+ });
44
+
45
+ await t('UNIT_TESTS_SERIAL=1 forces one at a time, whatever the machine', () => {
46
+ assert.equal(UNIT_TESTS_SERIAL_ENV, 'UNIT_TESTS_SERIAL');
47
+ for (const v of ['1', 'true', 'yes', 'on', 'TRUE']) assert.equal(resolveJobs({ UNIT_TESTS_SERIAL: v }, 16), 1, v);
48
+ assert.equal(resolveJobs({ UNIT_TESTS_SERIAL: '1', UNIT_TEST_JOBS: '8' }, 16), 1, 'serial wins over a jobs count');
49
+ assert.equal(resolveJobs({ UNIT_TESTS_SERIAL: '0' }, 16), 16, '0 does not force serial');
50
+ });
51
+
52
+ await t('UNIT_TEST_JOBS=N sets the pool size; junk is ignored', () => {
53
+ assert.equal(UNIT_TEST_JOBS_ENV, 'UNIT_TEST_JOBS');
54
+ assert.equal(resolveJobs({ UNIT_TEST_JOBS: '3' }, 16), 3);
55
+ assert.equal(resolveJobs({ UNIT_TEST_JOBS: '1' }, 16), 1);
56
+ for (const junk of ['0', '-2', 'many', '2.5']) assert.equal(resolveJobs({ UNIT_TEST_JOBS: junk }, 6), 6, junk);
57
+ });
58
+
59
+ // ── serial-lane classification ──────────────────────────────────────────────────────
60
+
61
+ await t('a browser launch goes to the serial lane', () => {
62
+ assert.equal(serialReason('x', "import { chromium } from 'playwright';"), 'launches a browser');
63
+ assert.equal(serialReason('x', "const b = await puppeteer.launch({ headless: 'new' });"), 'launches a browser');
64
+ });
65
+
66
+ await t('a fixed listen port goes to the serial lane; listen(0) does not', () => {
67
+ assert.equal(serialReason('x', 'server.listen(3100, () => {});'), 'listens on a fixed port');
68
+ assert.equal(serialReason('x', 'server.listen(8080)'), 'listens on a fixed port');
69
+ assert.equal(serialReason('x', 'server.listen(0, () => {});'), null);
70
+ assert.equal(serialReason('x', "assert.match(out, /PORT=3003/);"), null, 'a port in an assertion is not a listen');
71
+ });
72
+
73
+ await t('a fixed temp path goes to the serial lane; a mkdtemp prefix does not', () => {
74
+ assert.equal(serialReason('x', "const p = path.join(os.tmpdir(), 'probe-home');"), 'uses a fixed temp path');
75
+ assert.equal(serialReason('x', "const d = fs.mkdtempSync(path.join(os.tmpdir(), 'probe-'));"), null);
76
+ assert.equal(serialReason('x', "const d = mkdtempSync(join(os.tmpdir(), 'probe-'));"), null);
77
+ assert.equal(serialReason('x', 'const p = path.join(os.tmpdir(), `probe-${process.pid}`);'), null, 'a per-process name is not shared');
78
+ // A file with one mkdtemp and one fixed path is still serial — the fixed one decides.
79
+ assert.equal(serialReason('x', "fs.mkdtempSync(path.join(os.tmpdir(), 'a-'));\nconst p = path.join(os.tmpdir(), 'fixed');"), 'uses a fixed temp path');
80
+ });
81
+
82
+ await t('a name in SERIAL is serial whatever its source says', () => {
83
+ SERIAL.add('__probe_listed__');
84
+ try {
85
+ assert.equal(serialReason('__probe_listed__', ''), 'listed in SERIAL');
86
+ } finally { SERIAL.delete('__probe_listed__'); }
87
+ assert.equal(serialReason('plain', 'assert.equal(1, 1);'), null);
88
+ });
89
+
90
+ await t('the real suite: every browser-launching test file lands in the serial lane', () => {
91
+ // The scan has to keep seeing the files it exists for. A pattern that silently stopped
92
+ // matching would put Chrome back in the pool, and green would look the same.
93
+ for (const name of ['page_reader', 'ui_design_skills']) {
94
+ const src = fs.readFileSync(path.join(ROOT, 'tests', `${name}.mjs`), 'utf8');
95
+ assert.equal(serialReason(name, src), 'launches a browser', name);
96
+ }
97
+ // and not everything: the pool has to have something in it
98
+ const sample = fs.readdirSync(path.join(ROOT, 'tests')).filter((f) => f.endsWith('.mjs')).slice(0, 200);
99
+ const serial = sample.filter((f) => serialReason(f, fs.readFileSync(path.join(ROOT, 'tests', f), 'utf8')));
100
+ assert.ok(serial.length < sample.length / 2, `most files run in the pool (${serial.length}/${sample.length} serial)`);
101
+ });
102
+
103
+ // ── scheduler ────────────────────────────────────────────────────────────────────────
104
+
105
+ // A fake `run` that finishes each file after a set delay and records what overlapped.
106
+ function fakeRun(delays) {
107
+ const log = { inFlight: new Set(), maxInFlight: 0, overlapsWithSerial: [], started: [] };
108
+ const run = async (file) => {
109
+ log.inFlight.add(file);
110
+ log.started.push(file);
111
+ log.maxInFlight = Math.max(log.maxInFlight, log.inFlight.size);
112
+ if (file.startsWith('S') && log.inFlight.size > 1) log.overlapsWithSerial.push([...log.inFlight]);
113
+ for (const other of log.inFlight) if (other !== file && other.startsWith('S')) log.overlapsWithSerial.push([...log.inFlight]);
114
+ await new Promise((r) => setTimeout(r, delays[file] ?? 5));
115
+ log.inFlight.delete(file);
116
+ return { ok: !file.endsWith('!'), status: file.endsWith('!') ? 1 : 0, signal: null, killed: false, stdout: file.endsWith('!') ? ' FAIL case\n0 passed, 1 failed' : '1 passed, 0 failed', stderr: '' };
117
+ };
118
+ return { run, log };
119
+ }
120
+
121
+ const entry = (file) => ({ displayName: file, file, serial: file.startsWith('S') ? 'listed in SERIAL' : null });
122
+
123
+ await t('output prints in entry order even when later files finish first', async () => {
124
+ const files = ['a', 'b', 'c', 'd', 'e', 'f'];
125
+ // a is slowest, so every other file finishes before it
126
+ const { run } = fakeRun({ a: 60, b: 5, c: 10, d: 1, e: 20, f: 2 });
127
+ const printed = [];
128
+ const results = await runEntries(files.map(entry), { jobs: 4, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
129
+ assert.deepEqual(printed, files.map((f) => ` ok ${f}`));
130
+ assert.deepEqual(results.map((r) => r.outcome), files.map(() => 'ok'));
43
131
  });
44
132
 
45
- t('the run loops are sequential — no concurrency primitive in the runner', () => {
46
- // Promise.all / allSettled over the file list, a worker pool, or the async
47
- // execFile would each make the suites overlap and reintroduce the shared-state
48
- // class the recorded audit rules out.
49
- assert.doesNotMatch(src, /Promise\.(all|allSettled|race)\s*\(/,
50
- 'a Promise.all over the test list would run suites concurrently');
51
- assert.doesNotMatch(src, /require\(['"]node:worker_threads['"]\)|from ['"]node:worker_threads['"]/,
52
- 'a worker pool would run suites concurrently');
53
- assert.doesNotMatch(src, /\bexecFile\s*\(/,
54
- 'the ASYNC execFile would overlap suites; the runner uses execFileSync');
133
+ await t('the pool runs files concurrently but never more than its size', async () => {
134
+ const files = Array.from({ length: 12 }, (_, i) => `p${String(i).padStart(2, '0')}`);
135
+ const { run, log } = fakeRun(Object.fromEntries(files.map((f) => [f, 15])));
136
+ await runEntries(files.map(entry), { jobs: 3, perTestTimeout: 1000, run, print: () => {} });
137
+ assert.equal(log.maxInFlight, 3);
55
138
  });
56
139
 
57
- t('the loop over the discovered test files is a plain for-of', () => {
58
- assert.match(src, /for \(const name of files\) runOne\(/,
59
- 'sequential iteration is the mechanism — not a map() returning promises');
140
+ await t('a serial-lane file never runs beside anything else', async () => {
141
+ const files = ['S1', 'a', 'b', 'S2', 'c', 'd', 'e', 'S3'];
142
+ const { run, log } = fakeRun(Object.fromEntries(files.map((f) => [f, 10])));
143
+ const printed = [];
144
+ await runEntries(files.map(entry), { jobs: 4, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
145
+ assert.deepEqual(log.overlapsWithSerial, [], 'no serial file overlapped another run');
146
+ assert.ok(log.maxInFlight > 1, 'the pool part still ran in parallel');
147
+ assert.deepEqual(printed, files.map((f) => ` ok ${f}`), 'still printed in entry order');
60
148
  });
61
149
 
62
- t('the serial guarantee is EXPLAINED where it is relied on, not just true by accident', () => {
63
- // The audit recorded beside the loop is what a future flake investigation reads
64
- // instead of re-deriving it. If the loop is rewritten the note must be revisited,
65
- // which is the whole point of pinning it next to the mechanism.
66
- assert.match(src, /EXECUTION IS SERIAL, ONE PROCESS PER TEST/,
67
- 'run-unit-tests.js must carry the note explaining why serialism is relied upon');
150
+ await t('jobs=1 runs everything one at a time, in entry order', async () => {
151
+ const files = ['S1', 'a', 'b', 'c'];
152
+ const { run, log } = fakeRun({ a: 15, b: 1, c: 5 });
153
+ await runEntries(files.map(entry), { jobs: 1, perTestTimeout: 1000, run, print: () => {} });
154
+ assert.equal(log.maxInFlight, 1);
155
+ assert.deepEqual(log.started, files);
68
156
  });
69
157
 
158
+ await t('a failure keeps its verdict and detail lines, in its place', async () => {
159
+ const files = ['a', 'b!', 'c'];
160
+ const { run } = fakeRun({ a: 20, 'b!': 1, c: 1 });
161
+ const printed = [];
162
+ const results = await runEntries(files.map(entry), { jobs: 3, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
163
+ assert.deepEqual(results.map((r) => r.outcome), ['ok', 'fail', 'ok']);
164
+ assert.equal(printed[0], ' ok a');
165
+ assert.equal(printed[1], ' FAIL b! (exit 1)');
166
+ assert.ok(printed.includes(' FAIL case'), 'the file\'s own FAIL line is shown');
167
+ assert.equal(printed[printed.length - 1], ' ok c');
168
+ });
169
+
170
+ await t('a pooled TIMEOUT is re-run alone, and the alone run is the verdict', async () => {
171
+ const calls = [];
172
+ let inFlight = 0;
173
+ const run = async (file) => {
174
+ inFlight++;
175
+ const alone = inFlight === 1;
176
+ calls.push({ file, alone });
177
+ await new Promise((r) => setTimeout(r, 5));
178
+ inFlight--;
179
+ // 'slow' only finishes when nothing else is running; 'hang' never finishes
180
+ const killed = file === 'hang' || (file === 'slow' && !calls.filter((c) => c.file === 'slow').at(-1).alone);
181
+ return { ok: !killed, status: killed ? null : 0, signal: killed ? 'SIGTERM' : null, killed, stdout: killed ? '' : '1 passed, 0 failed', stderr: '' };
182
+ };
183
+ const files = ['a', 'hang', 'slow', 'z'];
184
+ const printed = [];
185
+ const results = await runEntries(files.map(entry), { jobs: 4, perTestTimeout: 1000, run, print: (l) => printed.push(l) });
186
+ assert.deepEqual(results.map((r) => r.outcome), ['ok', 'fail', 'ok', 'ok'], 'starved-only passes; a real hang still fails');
187
+ assert.equal(calls.filter((c) => c.file === 'slow').length, 2);
188
+ assert.equal(calls.filter((c) => c.file === 'slow').at(-1).alone, true, 'the re-run had nothing beside it');
189
+ assert.equal(calls.filter((c) => c.file === 'a').length, 1, 'a file that passed is not re-run');
190
+ assert.equal(printed[0], ' ok a');
191
+ assert.equal(printed[1], ' FAIL hang (TIMEOUT after 1s, SIGTERM)');
192
+ assert.match(printed[2], /re-run alone/);
193
+ assert.equal(printed.at(-1), ' ok z', 'still in entry order');
194
+ });
195
+
196
+ await t('an assertion failure in the pool is NOT re-run', async () => {
197
+ const { run, log } = fakeRun({});
198
+ await runEntries(['a', 'b!'].map(entry), { jobs: 2, perTestTimeout: 1000, run, print: () => {} });
199
+ assert.deepEqual(log.started.filter((f) => f === 'b!'), ['b!']);
200
+ });
201
+
202
+ await t('a write inside the repo goes to the serial lane; a write into a temp dir does not', () => {
203
+ const why = 'writes inside the repo';
204
+ assert.equal(serialReason('x', "const probe = path.join(ROOT, 'modules', 'p');\nmkdirSync(probe);"), why);
205
+ assert.equal(serialReason('x', "fs.writeFileSync(path.join(REPO_ROOT, 'config', 'x.json'), '{}');"), why);
206
+ assert.equal(serialReason('x', "const target = new URL('../src/a.js', import.meta.url);\nfs.writeFileSync(target, s);"), why);
207
+ assert.equal(serialReason('x', "const tmp = fs.mkdtempSync(path.join(ROOT, '.tmp-'));"), why);
208
+ assert.equal(serialReason('x', "const src = path.join(ROOT, 'config', 'a.json');\nfs.readFileSync(src, 'utf8');"), null, 'reading the repo is fine');
209
+ assert.equal(serialReason('x', "const d = fs.mkdtempSync(path.join(os.tmpdir(), 'w-'));\nfs.writeFileSync(path.join(d, 'f'), 'x');"), null);
210
+ });
211
+
212
+ await t('the real suite: every repo-writer found by watching a pooled run is in the serial lane', () => {
213
+ // Each of these changed a file another test reads while the pool ran (task 1004926).
214
+ for (const name of ['prelaunch_gate', 'file_map', 'figma_adapter', 'repo_map', 'government_vocabulary', 'cloud_bongos_pack', 'search_ingest_nul']) {
215
+ const src = fs.readFileSync(path.join(ROOT, 'tests', `${name}.mjs`), 'utf8');
216
+ assert.equal(serialReason(name, src), 'writes inside the repo', name);
217
+ }
218
+ // agents_event_router times a handler at 50 ms of wall clock — a busy pool makes that a coin toss
219
+ assert.equal(serialReason('agents_event_router', ''), 'listed in SERIAL');
220
+ });
221
+
222
+ // ── verdict rendering (unchanged rules)─────────────────────────────────────────────
223
+
224
+ await t('renderResult: timeout, skips and the libuv teardown abort read as before', () => {
225
+ const timeout = renderResult('slow', { ok: false, status: null, signal: 'SIGTERM', killed: true, stdout: '', stderr: '' }, 90_000);
226
+ assert.equal(timeout.outcome, 'fail');
227
+ assert.equal(timeout.lines[0], ' FAIL slow (TIMEOUT after 90s, SIGTERM)');
228
+ assert.match(timeout.lines[1], /no output at all/);
229
+
230
+ const skips = renderResult('sk', { ok: true, status: 0, signal: null, killed: false, stdout: ' SKIP one — no db\n1 passed, 0 failed, 1 skipped', stderr: '' }, 90_000);
231
+ assert.deepEqual(skips, { lines: [' ok sk (1 SKIPPED — not run here)'], outcome: 'ok', skips: 1 });
232
+
233
+ const abort = renderResult('ab', { ok: false, status: 127, signal: null, killed: false, stdout: '3 passed, 0 failed', stderr: 'Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)' }, 90_000);
234
+ assert.equal(abort.outcome, 'teardown');
235
+ assert.equal(abort.lines[0], ' ok ab (clean verdict, then libuv teardown abort, exit 127)');
236
+ });
237
+
238
+ // ── the real child-process path ─────────────────────────────────────────────────────
239
+
240
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'unit-runner-pool-'));
241
+ try {
242
+ const write = (name, body) => { const p = path.join(dir, name); fs.writeFileSync(p, body); return p; };
243
+ const pass = write('pass.mjs', "console.log(' PASS one'); console.log('1 passed, 0 failed');\n");
244
+ const fail = write('fail.mjs', "console.log(' FAIL one'); console.log('0 passed, 1 failed'); console.error('boom'); process.exit(3);\n");
245
+ const stdin = write('stdin.mjs', "process.stdin.resume(); process.stdin.on('end', () => { console.log('1 passed, 0 failed'); });\n");
246
+ const hang = write('hang.mjs', 'setInterval(() => {}, 1000);\n');
247
+
248
+ await t('runFile: a passing file resolves ok with its stdout', async () => {
249
+ const r = await runFile(pass, 20_000);
250
+ assert.equal(r.ok, true);
251
+ assert.equal(r.status, 0);
252
+ assert.match(r.stdout, /1 passed, 0 failed/);
253
+ });
254
+
255
+ await t('runFile: a failing file resolves with its exit code, stdout and stderr', async () => {
256
+ const r = await runFile(fail, 20_000);
257
+ assert.equal(r.ok, false);
258
+ assert.equal(r.status, 3);
259
+ assert.equal(r.killed, false);
260
+ assert.match(r.stdout, /FAIL {2}one/);
261
+ assert.match(r.stderr, /boom/);
262
+ assert.equal(renderResult('fail', r, 20_000).lines[0], ' FAIL fail (exit 3)');
263
+ });
264
+
265
+ await t('runFile: stdin reads see end-of-input at once, as under the old ignored stdin', async () => {
266
+ const r = await runFile(stdin, 20_000);
267
+ assert.equal(r.ok, true, 'a suite that reads stdin must not wait for the whole budget');
268
+ });
269
+
270
+ await t('runFile: a hung file is killed at the budget and reads as a TIMEOUT', async () => {
271
+ const r = await runFile(hang, 1_500);
272
+ assert.equal(r.ok, false);
273
+ assert.equal(r.killed, true);
274
+ assert.match(renderResult('hang', r, 1_500).lines[0], /^ {2}FAIL {2}hang \(TIMEOUT after 1\.5s/);
275
+ });
276
+
277
+ await t('runEntries end to end: real files, a pool of 3, sorted output and outcomes', async () => {
278
+ const entries = [
279
+ { displayName: 'fail', file: fail, serial: null },
280
+ { displayName: 'pass', file: pass, serial: 'listed in SERIAL' },
281
+ { displayName: 'stdin', file: stdin, serial: null },
282
+ ];
283
+ const printed = [];
284
+ const results = await runEntries(entries, { jobs: 3, perTestTimeout: 20_000, print: (l) => printed.push(l) });
285
+ assert.deepEqual(results.map((r) => r.outcome), ['fail', 'ok', 'ok']);
286
+ assert.equal(printed[0], ' FAIL fail (exit 3)');
287
+ assert.deepEqual(printed.slice(-2), [' ok pass', ' ok stdin']);
288
+ });
289
+ } finally {
290
+ fs.rmSync(dir, { recursive: true, force: true });
291
+ }
292
+
70
293
  console.log('');
71
294
  console.log(`unit_runner_serial: ${passed} passed, ${failed} failed`);
72
295
  process.exit(failed ? 1 : 0);
@@ -75,7 +75,7 @@ test('an unknown future prefix is caught too', () => {
75
75
 
76
76
  // ---- what the guard must NOT catch ---------------------------------------
77
77
  test('a COMMENT naming an old version is not a violation', () => {
78
- // smoke-claim-consistency.sh legitimately says "PMS-V2 P6 not live yet".
78
+ // A script may legitimately say "PMS-V2 P6 not live yet" in a comment.
79
79
  const r = scan({
80
80
  'scripts/bongos/d.sh': '# TODO: once PMS-V2 P6 ships, drop this',
81
81
  'scripts/bongos/e.js': "// version_id: 'GDS-V3' used to live here",
@@ -133,12 +133,10 @@ test('smoke-write.sh resolves its version instead of naming one', () => {
133
133
  assert.match(sh, /exit 1/, 'and stops if it cannot');
134
134
  });
135
135
 
136
- test('all four bash smokes resolve, and none names a version', () => {
137
- for (const f of ['smoke-write.sh', 'smoke-claim-consistency.sh', 'smoke-batch-release.sh', 'smoke-batch-claim.sh']) {
138
- const sh = read('scripts', 'bongos', f);
139
- assert.match(sh, /version-select\.js/, `${f} resolves`);
140
- assert.match(sh, /'version_id': '\$\{VERSION_ID\}'/, `${f} uses the resolved id`);
141
- }
136
+ test('the bash smoke writes the resolved id, not a named version', () => {
137
+ // smoke-write.sh is the one bash smoke left (task 1005002 deleted the other three).
138
+ const sh = read('scripts', 'bongos', 'smoke-write.sh');
139
+ assert.match(sh, /'version_id': '\$\{VERSION_ID\}'/, 'smoke-write.sh uses the resolved id');
142
140
  });
143
141
 
144
142
  test('capture.js no longer keeps its own copy of the rule', () => {
@@ -75,12 +75,23 @@ test('every light sits inside the painting, and every pulse climbs inside its to
75
75
  assert.deepEqual(W.pulseRuns(W.TOWERS, 3, 7), runs, 'seeded: every visit draws the same picture');
76
76
  });
77
77
 
78
- test('the banner names the project from the branding pack, never a hardcoded word', () => {
79
- assert.equal(W.bannerText({ identity: { worldName: 'Harbor', productName: 'Cloud Bongos' } }), 'Working on the core of Harbor');
80
- assert.equal(W.bannerText({ identity: { productName: 'Cloud Bongos' } }), 'Working on the core of Cloud Bongos');
81
- assert.equal(W.bannerText(undefined), 'Working on the core of this project', 'no branding: still a sentence');
78
+ test('the banner is the logo, named from the branding pack, never a hardcoded word (task 1004953)', () => {
79
+ assert.equal(W.bannerText, undefined, 'the "Working on the core of" banner is gone');
80
+ const harbor = W.logoHtml({ identity: { worldName: 'Harbor', productName: 'Cloud Bongos' } });
81
+ assert.match(harbor, /<svg class="wb__mark"/, 'no project logo: the Bongos mark');
82
+ assert.match(harbor, /class="wb__ring"[^>]*pathLength="100"/, 'the ring can draw in');
83
+ assert.match(harbor, /class="wb__moonlet"[^>]*fill="#fb9569"/, 'the salmon moon');
84
+ assert.match(harbor, /<span class="wb__name">Harbor<\/span>/);
85
+ assert.match(W.logoHtml(undefined), /<span class="wb__name">this project<\/span>/, 'no branding: still a name');
86
+ const own = W.logoHtml({ identity: { worldName: 'Harbor' }, theme: { logo: '/logo.png?a="b"' } });
87
+ assert.match(own, /<img class="wb__logo" src="\/logo\.png\?a=&quot;b&quot;"/, 'a project logo takes the mark\'s place, escaped');
88
+ assert.doesNotMatch(own, /wb__mark/);
82
89
  const branding = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
83
- assert.equal(W.bannerText(branding), 'Working on the core of ' + branding.identity.worldName);
90
+ assert.match(W.logoHtml(branding), new RegExp('>' + branding.identity.worldName + '<'));
91
+ });
92
+
93
+ test('the credit links to cloudbongos.com (task 1004953)', () => {
94
+ assert.deepEqual(W.BUILT_ON, { href: 'https://cloudbongos.com', label: 'cloudbongos.com' });
84
95
  });
85
96
 
86
97
  test('the owner\'s role mapping: destination and craft mode', () => {
@@ -297,17 +297,49 @@ test('the way out sinks and fades into the hall, which is usable at once', { ski
297
297
  await ctx.close();
298
298
  });
299
299
 
300
- test('the banner names the project, the motion is laid on, and the drawn tower and city are gone', { skip }, async () => {
300
+ test('the banner is the animated logo, the motion is laid on, and the drawn tower and city are gone', { skip }, async () => {
301
301
  const ctx = await hall();
302
302
  const { page } = await open(ctx);
303
303
  await signIn(page);
304
- await page.waitForSelector('#welcome-boot .wb-motion');
304
+ await page.waitForSelector('#welcome-boot.wb--lit .wb-motion');
305
305
  const s = await page.evaluate(() => ({
306
- banner: document.querySelector('.wb__banner .wb__eyebrow').textContent,
306
+ banner: document.querySelector('.wb__banner').textContent,
307
+ name: document.querySelector('.wb__banner .wb__name').textContent,
308
+ mark: !!document.querySelector('.wb__banner svg.wb__mark .wb__moonlet'),
309
+ ringAnim: getComputedStyle(document.querySelector('.wb__ring')).animationName,
307
310
  pulses: document.querySelectorAll('#wb-fx .wb-pulse').length,
308
311
  cars: document.querySelectorAll('#wb-fx .wb-car').length,
309
312
  old: document.querySelectorAll('.wb-floor, .wb-bldg, .wb-person, #wb-scene, #wb-beam, #wb-city').length,
310
313
  }));
311
- assert.deepEqual(s, { banner: 'Working on the core of Harbor', pulses: 21, cars: 3, old: 0 });
314
+ assert.doesNotMatch(s.banner, /Working on the core of/, 'task 1004953: the banner text is gone');
315
+ assert.deepEqual({ name: s.name, mark: s.mark, ringAnim: s.ringAnim, pulses: s.pulses, cars: s.cars, old: s.old },
316
+ { name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses: 21, cars: 3, old: 0 });
312
317
  await ctx.close();
313
318
  });
319
+
320
+ test('the "Built on cloudbongos.com" credit sits bottom right, clear of the choices, at desktop and phone width', { skip }, async () => {
321
+ for (const viewport of [{ width: 1280, height: 800 }, { width: 390, height: 760 }]) {
322
+ const ctx = await hall();
323
+ const { page } = await open(ctx);
324
+ await page.setViewportSize(viewport);
325
+ await page.evaluate(() => window.OTBShell.applyAccess({ authed: false }));
326
+ await page.waitForSelector('#welcome-boot .wb__built');
327
+ await page.click('[data-wb-skip]');
328
+ const s = await page.evaluate(() => {
329
+ const a = document.querySelector('.wb__built').getBoundingClientRect();
330
+ const c = document.querySelector('.wb__choices').getBoundingClientRect();
331
+ const el = document.querySelector('.wb__built');
332
+ return {
333
+ text: el.textContent, href: el.getAttribute('href'),
334
+ right: Math.round(innerWidth - a.right), bottom: Math.round(innerHeight - a.bottom),
335
+ clear: a.top >= c.bottom || a.right <= c.left || a.left >= c.right, tall: a.height >= 24,
336
+ };
337
+ });
338
+ assert.equal(s.text, 'Built on cloudbongos.com');
339
+ assert.equal(s.href, 'https://cloudbongos.com');
340
+ assert.ok(s.right <= 24 && s.bottom <= 24, 'bottom-right corner at ' + viewport.width + 'w');
341
+ assert.ok(s.clear, 'does not cover the choices at ' + viewport.width + 'w');
342
+ assert.ok(s.tall, 'a 24px target');
343
+ await ctx.close();
344
+ }
345
+ });
@@ -1,66 +0,0 @@
1
- # Releasing the standalone `bongos` CLI
2
-
3
- How the compiled `bongos` binaries get built, published, and installed — the
4
- "no Node, no repo" on-ramp (task [#1248](https://example.com/builders#/task/1248), V4.R81).
5
- The release flow: **CI builds, the operator publishes to a public releases repo, the server serves
6
- branded redirects.** There is no cross-repo write token in CI — publishing is a
7
- deliberate operator step.
8
-
9
- ## The moving parts
10
-
11
- | Piece | Where | Role |
12
- |---|---|---|
13
- | Build wrapper | [`scripts/bongos/build-bongos.js`](../../scripts/bongos/build-bongos.js) | `bun build --compile` → per-platform binaries in `dist/` |
14
- | CI | `.github/workflows/build-bongos-cli.yml` (instance-side) | runs the wrapper, uploads `bongos-cli-binaries` artifacts (no publish) |
15
- | Release pointer | `config/bongos-cli.json` (instance-side) | `releases_repo` + `version` + per-target asset filenames |
16
- | Redirects | [`src/bongos-downloads.js`](../../src/bongos-downloads.js) → `/downloads/bongos/*` in [`serve-internal.js`](../../src/bongos/serve-internal.js) | `/downloads/bongos/<target>` 302s to the release asset; `/version` + `/latest.json` for update-checks |
17
- | Installer | `infra/bongos-install.sh` + `.ps1` (instance-side) | served (origin-pinned) at `/downloads/bongos/install.sh` / `.ps1` |
18
- | Package managers | `packaging/homebrew/bongos.rb`, `packaging/scoop/bongos.json` (instance-side) | brew/scoop templates (fill sha256 at release) |
19
-
20
- ## What the user runs
21
-
22
- ```sh
23
- # macOS / Linux — no Node required:
24
- curl -fsSL https://example.com/downloads/bongos/install.sh | sh
25
- # Windows (PowerShell):
26
- irm https://example.com/downloads/bongos/install.ps1 | iex
27
- # or, once the tap/bucket is published:
28
- brew install example-owner/bongos/bongos # macOS/Linux
29
- scoop bucket add bongos https://github.com/example-owner/scoop-bongos; scoop install bongos
30
- ```
31
-
32
- Then: `bongos login <instance-url>` → `bongos start`. Writing code needs a checkout of the
33
- project repo, where the full CLI is installed.
34
-
35
- ## One-time owner setup (tracked as the open blocker on [#1248](https://example.com/builders#/task/1248))
36
-
37
- 1. **Create a PUBLIC releases repo** — `example-owner/example-bongos` (matches `config/bongos-cli.json`'s `releases_repo`). Public so GitHub's release CDN serves the downloads at $0; it holds ONLY the binaries, never source.
38
- 2. *(optional, for brew/scoop)* create the tap repo `example-owner/homebrew-bongos` and/or the bucket `lars689/scoop-bongos`.
39
- 3. *(optional)* point `get.bongos.dev` at the instance with a DNS CNAME → `example.com`, so the branded `curl -fsSL https://get.bongos.dev | sh` works (it just proxies `/downloads/bongos/install.sh`).
40
-
41
- ## Cutting a release
42
-
43
- 1. **Bump** `config/bongos-cli.json`: `version` + every `assets.*` filename to the new version. (Claim a task; this rides the normal ship flow.)
44
- 2. **Push** → `build-bongos-cli.yml` runs and produces the `bongos-cli-binaries` artifact. Wait for green.
45
- 3. **Download** the artifacts:
46
- ```sh
47
- gh run download -R example-owner/example --name bongos-cli-binaries --dir dist
48
- ```
49
- 4. **Publish** them as a `v<version>` release on the public repo:
50
- ```sh
51
- gh release create v0.1.0 -R example-owner/example-bongos --title "bongos v0.1.0" --notes "Standalone bongos CLI" dist/bongos-*
52
- ```
53
- The tag and asset filenames MUST match `config/bongos-cli.json` exactly — the redirect is `releases/download/v<version>/<asset>`; a renamed asset 404s.
54
- 5. *(package managers)* compute `shasum -a 256 dist/bongos-*`, fill the `sha256`/`hash` fields in `packaging/homebrew/bongos.rb` + `packaging/scoop/bongos.json`, and copy them to the tap/bucket repos.
55
- 6. **Ship** the `config/bongos-cli.json` bump. The `/downloads/bongos/*` redirects follow within a minute of deploy.
56
-
57
- Verify: `curl -fsSL https://example.com/downloads/bongos/version` shows the new version, and `curl -fsSL .../install.sh | sh` installs a working binary.
58
-
59
- ## The binary architecture (why some verbs aren't in it)
60
-
61
- `bin/bongos.js` is one dispatcher with two runtimes ([task 1248](https://example.com/builders#/task/1248)):
62
-
63
- - **repo / `npm i -g`** → runs under `node`; spawns each sibling `scripts/bongos/*.js` (unchanged).
64
- - **compiled binary** → `process.execPath` is the `bongos` binary (no `node`, no scripts on disk), so it dispatches the **bundled on-ramp verbs in-process**: `login`/`setup`, `reauth`, `paste-token`, `api`, `init`, `onboard`. The heavy work verbs (`ship`/`claim`/`start`/`status`/`recall`/`cost`/`release`/`module`/`package-core`) and `doctor` are **not** bundled — they need a full repo + git to run, and pulling their dependency trees in would bloat the binary. Run those in a clone, where `npm i -g` gives the full CLI. A bare-binary user who invokes one gets a clear "get onto a real environment first" message.
65
-
66
- Follow-ups (not blocking the first release): SHA-pin `oven-sh/setup-bun`, code-sign the mac/win binaries, and expand the bundled verb set if a self-host workflow needs more offline.