@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
@@ -76,6 +76,7 @@ const PUBLISH_ALLOWLIST = [
76
76
  'modules/agents/', // the declarative agent registry + spawn seam (goal 1000038) — CORE: its own manifest declares maintenance.status 'core-maintained', and it consumes the core `grade` port, so a released core without it cannot run the MAS machinery the methodology describes. default:false — shipped is not enabled (ADR 0083), exactly like provisioning + platform-identity below. Omitted at module-creation time (tasks 1002485/1002486): the omission was invisible until tests/agents_spawn.mjs became the first PUBLISHABLE file to import it and the R89 boundary guard caught it (task 1002489).
77
77
  'modules/npm-release/', // the deploy page's "where is the work" + version history (task 1002622). default:false — on only in the hall that publishes the core. MUST be here: modules/hall-ui/ ships as one subtree, so a released core without this carries deploy.js's loader and the #nr-work mounts with no routes behind them — the copy-desk asymmetry above.
78
78
  'modules/render-deploy/', // the deploy page of a project whose app deploys through Render (task 1004270). default:false. MUST be here for the npm-release reason: modules/hall-ui/ ships as one subtree, so a released core without it carries deploy.js's loader for #rd-panel with no routes behind it.
79
+ 'modules/market-watch/', // the weekly AI market watch (task 1004935, goal 1000137). default:false and empty for now, but MUST be here: shipped is not enabled, and a released core without it answers "unknown module" to an instance that turns it on; tests/market_watch_module.mjs (publishable) also reads its manifest.
79
80
  'modules/ai-studio/', // AI generation for artists (task 1004916, ADR 0392): keys, prepaid balance, generate. default:false.
80
81
  'modules/raise/', // the /raise investor page (task 1004847, ADR 0384) — ships in core, enabled ONLY on the hub, like platform-identity. Owner decision 2026-10-25: the whole page, its copy and starting numbers included, lives in the repo so builders can contribute to it, accepting that it publishes with the core. Secrets never do: the investor password is RAISE_PASSWORD_HASH in the instance env. default:false.
81
82
  'modules/platform-identity/', // hub identity provider (ADR 0141) — ships in core, enabled ONLY on the hub (cloudbongos.com). MUST be here or the hub's enabledModules() throws "unknown module" and provision.js's federation load MODULE_NOT_FOUNDs (task 1002235, same rationale as provisioning above). default:false — shipped ≠ enabled; a vanilla/self-hosted instance never turns it on.
@@ -89,6 +90,8 @@ const PUBLISH_ALLOWLIST = [
89
90
  'config/scheduled-routines.json', // the routine SWITCHBOARD (ADR 0115) — per routine: enabled, cadence, requiresAutonomy. It carries no *.neutral.json twin because it IS the neutral starter: ADR 0115's contract is that "a fresh instance inherits this whole set from the core template", and the operator then opts in/out HERE. PUBLISHES rather than topology-excludes for that reason — a released core without it would hand a new instance an empty scheduler and no record of what the routines under .claude/scheduled-tasks/ are for. It reached this list when task 1002500 registered it as a protected surface, which forces the choice; before that it was default-denied by silence, which is the accident that rule exists to catch.
90
91
  // --- the portable methodology core, governance, and sanitized docs ---
91
92
  'CLAUDE.md',
93
+ 'AGENTS.md', // the same rules for every other AI model, incl. the human signal (task 1004922)
94
+ 'GEMINI.md', // Gemini's pointer to AGENTS.md (task 1004922)
92
95
  'README.md',
93
96
  'LICENSE',
94
97
  'GOVERNANCE.md',
@@ -117,6 +117,8 @@ function mainCheckoutRoot() {
117
117
  }
118
118
  console.log(`Released task #${r.data.taskId}: ${r.data.taskTitle}`);
119
119
  console.log(`Status returned to 'ready'. No credits awarded.`);
120
+ // task 1004956: a released task is no longer a strand anyone should be fixing.
121
+ require('./strand-lease.js').clearOnExit(r.data.taskId);
120
122
  // task 1002871: a claim worked under the `--here` hatch leaves the MAIN
121
123
  // checkout on the feature branch, and giving the claim up is exactly when that
122
124
  // tree stops being watched. Warn, never restore — the branch may hold real
@@ -25,7 +25,8 @@
25
25
 
26
26
  const fs = require('node:fs');
27
27
  const path = require('node:path');
28
- const { execFileSync } = require('node:child_process');
28
+ const os = require('node:os');
29
+ const { execFile, execFileSync } = require('node:child_process');
29
30
  const { isModuleEnabled } = require('../../src/modules');
30
31
 
31
32
  const TESTS_DIR = path.resolve(__dirname, '..', '..', 'tests');
@@ -79,10 +80,19 @@ const INTEGRATION = new Set([
79
80
  // keeps the week's baseline rule, the year counts from a row's first snapshot inside
80
81
  // it, the pin under a hide and two racing writers. The DB-free sibling is community_crowns.
81
82
  'community_crowns_db',
83
+ // task 1004992: the ways a person signs in against real SQL — the UNIQUE constraints,
84
+ // the minted-key sequence, the GitHub-born trigger, the backfill and a real sign-up
85
+ // race. The DB-free sibling is account_logins.
86
+ 'account_logins_db',
82
87
  // task 1002983: the rising builder and the projects on the move against real SQL — the
83
88
  // year's baseline over 30 days, a quiet row's live total, hidden and private left out.
84
89
  // The DB-free sibling is community_movers.
85
90
  'community_movers_db',
91
+ // task 1004936: the Market Watch tables, hub seed and read queries against real SQL. The
92
+ // CHECKs, partial unique indexes and ON CONFLICT targets are what make the seed
93
+ // idempotent and a resumed run unable to record an item twice. DB-free sibling:
94
+ // market_watch_data.
95
+ 'market_watch_data_db',
86
96
  // bfg, bfg_write, memory_isolation, memory_pull and setup_smoke LEFT this set under task
87
97
  // 1004865: none of them ever needed a database. Each failed on a stale test harness (a fake
88
98
  // server without res.fail, a stub on the wrong module, an old error shape, a renamed session
@@ -98,9 +108,16 @@ const INTEGRATION = new Set([
98
108
  // never was a dedicated consumer-layout-boot workflow, whatever older notes say.
99
109
  'consumer_layout_boot',
100
110
  'claim_gate_open_blocker', 'claim_gate_rebase',
111
+ // task 1004950: what a task needs — the real feed + all three claim surfaces, per capability.
112
+ 'task_needs_db',
113
+ // task 1004947: an artist's page claim never hides or refuses engineering work (real feed + claim).
114
+ 'page_tweak_no_block',
101
115
  // task 1004585: a hall-made goal's spec task gates its other work — the edge, the
102
116
  // claim gate's unsatisfiedDeps read, and the ship-time auto-promotion, on real Postgres.
103
117
  'goal_spec_task_db',
118
+ // task 1004984: unlinking an open task from a criterion, then the reconciler's
119
+ // close — the delivery guard and the sign-off recency clause are one SQL statement.
120
+ 'task_criteria_unlink_db',
104
121
  // task 1003457: proves the blocker-resolve auto-promotion trigger (migration
105
122
  // 009, tightened by core_223) fires on the HALL'S OWN write path — the real
106
123
  // buildGdsRouter() HTTP routes, not a static SQL-text pin (blocker_resolve_guard.mjs,
@@ -231,6 +248,13 @@ const INTEGRATION = new Set([
231
248
  // task 1004750 (ADR 0382 D6): passkey tables from the real migration — single-use
232
249
  // challenges, TTL sweep, unique credential id, sign-count regression.
233
250
  'passkey_foundation_db',
251
+ // task 1004913 (ADR 0385 D4, ADR 0387 D2): the payout approval log from the real
252
+ // migration — the DATABASE refuses edits, deletes, truncation, a non-web or machine
253
+ // signature, a widening narrowing and anything after a revocation.
254
+ 'payout_approvals_db',
255
+ // task 1004914 (ADR 0385 D8): the payout gate tables from the real migration —
256
+ // append-only, no silent hold, who may appeal, decide and co-sign, the gate end to end.
257
+ 'payout_gates_db',
234
258
  // task 1004408: migration platform_identity_028 deletes the old hub invite's
235
259
  // pending rows before a cutoff it reads from the core_upgrades ledger. Only a real
236
260
  // engine runs the DO block, the numeric version compare and the one-shot guard.
@@ -383,6 +407,216 @@ function reportToWorkflow(failures) {
383
407
  fs.appendFileSync(process.env.GITHUB_OUTPUT, `failures=${describeFailures(failures)}\n`);
384
408
  }
385
409
 
410
+ // ── Parallel lane + serial lane (task 1004926) ─────────────────────────────────────────
411
+ // The unit gate used to run every file one at a time (task 1001435 pinned that). Each test
412
+ // already runs in its OWN node process, so nothing in-process can race; what CAN collide is
413
+ // state OUTSIDE the processes. So the runner now runs several files at once, sized to the
414
+ // machine, and keeps every file that touches shared outside state in a SERIAL lane that runs
415
+ // alone, with nothing else running beside it.
416
+ //
417
+ // What sends a file to the serial lane: its name in SERIAL below, or one of SERIAL_MARKERS
418
+ // in its source — a browser launch, a fixed listen port, a fixed temp path, a write inside
419
+ // the repo tree. A NEW test that
420
+ // grows one of those lands in the serial lane on its own; no list to remember.
421
+ //
422
+ // Forcing serial: UNIT_TESTS_SERIAL=1 runs everything one at a time, as before. UNIT_TEST_JOBS=N
423
+ // sets the pool size. --integration and --integration-parked are always serial: their tests
424
+ // share one database.
425
+ //
426
+ // Output does not change shape: each file's lines print in sorted file order (a result that
427
+ // finishes early waits for the ones before it), and the roll-up — teardown aborts, skips,
428
+ // failures, the workflow report, the exit code — is built in that same order.
429
+ const UNIT_TESTS_SERIAL_ENV = 'UNIT_TESTS_SERIAL';
430
+ const UNIT_TEST_JOBS_ENV = 'UNIT_TEST_JOBS';
431
+
432
+ // Files that share state outside their own process in a way the source scan cannot see.
433
+ // Each entry says what it shares. Keep it short: a test that can own its state (mkdtemp,
434
+ // listen(0)) should be fixed to, not listed here.
435
+ //
436
+ // Found by running the pool and diffing against a serial run (task 1004926):
437
+ const SERIAL = new Set([
438
+ // asserts a handler returns in under 50 ms of wall clock; a busy pool makes that a coin toss
439
+ 'agents_event_router',
440
+ ]);
441
+
442
+ // Writes INSIDE the repo: a write call whose path is built from the repo root (ROOT, REPO,
443
+ // REPO_ROOT, repoRoot, REPO_DIR, or `new URL('../…', import.meta.url)`), inline or through a
444
+ // variable assigned from one. Every other test reads that tree. Found by watching the
445
+ // worktree during a pooled run (task 1004926): prelaunch_gate plants a line into
446
+ // src/bongos/prelaunch-gate.js and platform_boot, booting the real server beside it, crashed;
447
+ // file_map drops a probe module into modules/ and fitness_scope_roster saw it; figma_adapter
448
+ // writes src/ui/ and config/design-sources.json and find_untested tripped on it; repo_map
449
+ // dirties scripts/bongos/CLAUDE.md. A temp dir is not caught here, which is the point.
450
+ const REPO_ROOT_IDS = '(?:ROOT|REPO|REPO_ROOT|repoRoot|REPO_DIR)';
451
+ const REPO_PATH = `(?:(?:path\\.)?(?:join|resolve)\\(\\s*${REPO_ROOT_IDS}\\b|new URL\\(\\s*['"\`]\\.\\.\\/)`;
452
+ const FS_WRITE = '(?:writeFileSync|appendFileSync|mkdirSync|rmSync|rmdirSync|unlinkSync|renameSync|mkdtempSync|symlinkSync|fsp\\.rm|fsp\\.writeFile|fs\\.promises\\.writeFile)';
453
+ const WRITE_INLINE_RE = new RegExp(`${FS_WRITE}\\(\\s*${REPO_PATH}`);
454
+ const REPO_PATH_VAR_RE = new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${REPO_PATH}`, 'g');
455
+ function writesInsideRepo(src) {
456
+ if (WRITE_INLINE_RE.test(src)) return true;
457
+ for (const m of src.matchAll(REPO_PATH_VAR_RE)) {
458
+ if (new RegExp(`${FS_WRITE}\\(\\s*${m[1]}\\b`).test(src)) return true;
459
+ }
460
+ return false;
461
+ }
462
+
463
+ // A fixed name under the shared temp dir: path.join(os.tmpdir(), 'name') NOT wrapped in
464
+ // mkdtemp. Every process on the machine sees the same path. A mkdtemp prefix is unique per call.
465
+ const TMP_JOIN_LITERAL = /\bjoin\(\s*os\.tmpdir\(\)\s*,\s*(['"])[^'"$]*\1\s*[,)]/g;
466
+ function usesFixedTempPath(src) {
467
+ for (const m of src.matchAll(TMP_JOIN_LITERAL)) {
468
+ const before = src.slice(Math.max(0, m.index - 40), m.index);
469
+ if (!/mkdtemp(?:Sync)?\(\s*(?:path\.)?$/.test(before)) return true;
470
+ }
471
+ return false;
472
+ }
473
+
474
+ const SERIAL_MARKERS = [
475
+ // One Chrome per test is heavy, and the launch flake (exit 7 with only "TAP version 13")
476
+ // is a resource-starvation symptom; running them beside a full pool invites it.
477
+ { why: 'launches a browser', test: (src) => /\b(?:puppeteer|playwright|chromium)\b|headless/i.test(src) },
478
+ // A literal port is shared by every process on the machine. listen(0) is not.
479
+ { why: 'listens on a fixed port', test: (src) => /\.listen\(\s*[1-9]\d{1,4}\s*[,)]/.test(src) },
480
+ { why: 'uses a fixed temp path', test: usesFixedTempPath },
481
+ { why: 'writes inside the repo', test: writesInsideRepo },
482
+ ];
483
+
484
+ function serialReason(name, src) {
485
+ if (SERIAL.has(name)) return 'listed in SERIAL';
486
+ for (const m of SERIAL_MARKERS) if (m.test(src)) return m.why;
487
+ return null;
488
+ }
489
+
490
+ function resolveJobs(env = process.env, cpus = defaultCpus()) {
491
+ if (/^(1|true|yes|on)$/i.test(String(env[UNIT_TESTS_SERIAL_ENV] || '').trim())) return 1;
492
+ const raw = String(env[UNIT_TEST_JOBS_ENV] || '').trim();
493
+ if (/^[1-9]\d*$/.test(raw)) return Number(raw);
494
+ return Math.max(1, cpus);
495
+ }
496
+
497
+ function defaultCpus() {
498
+ return typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length;
499
+ }
500
+
501
+ // Run one test file in its own process. Resolves (never rejects) with a normalised result,
502
+ // so the verdict logic below reads the same shape whatever ended the child.
503
+ function runFile(filePath, timeout) {
504
+ return new Promise((resolve) => {
505
+ const child = execFile(process.execPath, [filePath], {
506
+ maxBuffer: 16 * 1024 * 1024,
507
+ timeout,
508
+ env: { ...process.env, NODE_ENV: 'test' },
509
+ windowsHide: true,
510
+ }, (err, stdout, stderr) => {
511
+ resolve({
512
+ ok: !err,
513
+ status: err ? (typeof err.code === 'number' ? err.code : null) : 0,
514
+ signal: err ? err.signal || null : null,
515
+ killed: !!(err && (err.killed || err.code === 'ETIMEDOUT')),
516
+ stdout: String(stdout || ''),
517
+ stderr: String(stderr || ''),
518
+ });
519
+ });
520
+ // stdin was 'ignore' (EOF at once) under execFileSync; a pipe nobody closes would leave a
521
+ // suite that reads stdin waiting for the whole budget.
522
+ if (child.stdin) child.stdin.end();
523
+ });
524
+ }
525
+
526
+ // Run `items` through `worker` with at most `jobs` in flight. Plain index hand-out: each
527
+ // slot takes the next index when its last one finishes.
528
+ async function runPool(items, jobs, worker) {
529
+ let next = 0;
530
+ const slot = async () => {
531
+ while (next < items.length) {
532
+ const i = next++;
533
+ await worker(items[i], i);
534
+ }
535
+ };
536
+ await Promise.all(Array.from({ length: Math.min(Math.max(1, jobs), items.length) }, slot));
537
+ }
538
+
539
+ // One file's verdict as printable lines plus its outcome. Pure: the same result always renders
540
+ // the same lines, whichever lane ran it.
541
+ function renderResult(displayName, r, perTestTimeout) {
542
+ const lines = [];
543
+ if (r.ok) {
544
+ const n = countSkips(r.stdout);
545
+ lines.push(` ok ${displayName}${n ? ` (${n} SKIPPED — not run here)` : ''}`);
546
+ return { lines, outcome: 'ok', skips: n };
547
+ }
548
+ // `exit null` on its own is unreadable, and it is what a HANG looks like here: the
549
+ // signal that ended the process is the whole diagnosis, so say it (task 1004052 —
550
+ // a test that hung for the full budget reported `exit null` and nothing else).
551
+ const why = r.killed
552
+ ? `TIMEOUT after ${perTestTimeout / 1000}s${r.signal ? `, ${r.signal}` : ''}`
553
+ : `exit ${r.status}${r.signal ? ` (${r.signal})` : ''}`;
554
+ const stdoutText = r.stdout.trim();
555
+ if (!r.killed && isCleanTeardownAbort(stdoutText, r.stderr)) {
556
+ lines.push(` ok ${displayName} (clean verdict, then libuv teardown abort, ${why})`);
557
+ return { lines, outcome: 'teardown', skips: 0 };
558
+ }
559
+ lines.push(` FAIL ${displayName} (${why})`);
560
+ // task 1002644: stdout used to be IGNORED, so the failing assertion (test files print
561
+ // their FAIL lines to stdout) was discarded. The actual failures first: the file's own
562
+ // FAIL lines, then its stdout tail (the last failure's assertion detail + summary).
563
+ // EMPTY STDOUT IS ITSELF THE CLUE: node:test buffers a file's whole report until the file
564
+ // finishes, so a suite that hangs mid-run emits NOTHING.
565
+ if (!stdoutText) {
566
+ lines.push(' (no output at all — node:test buffers a file\'s report until it finishes,');
567
+ lines.push(' so this suite did not reach the end. To find which case: run it on the same');
568
+ lines.push(' platform with each case writing its name to STDERR before it runs; stderr is');
569
+ lines.push(' unbuffered, so the last name with no completion is the one that hung.)');
570
+ }
571
+ const failLines = stdoutText ? stdoutText.split('\n').filter((l) => /\bFAIL\b/.test(l)).slice(0, 5) : [];
572
+ for (const l of failLines) lines.push(` ${l.trim()}`);
573
+ if (stdoutText) {
574
+ for (const l of stdoutText.split('\n').slice(-5)) lines.push(` ${l}`);
575
+ }
576
+ const stderr = r.stderr.trim().split('\n').slice(-3).join('\n ');
577
+ if (stderr) lines.push(` stderr: ${stderr}`);
578
+ return { lines, outcome: 'fail', skips: 0 };
579
+ }
580
+
581
+ // Run every entry — the serial lane first, alone, one at a time; then the rest `jobs` at a
582
+ // time — print each entry's lines in entry order, and return the results in entry order.
583
+ async function runEntries(entries, { jobs, perTestTimeout, run = runFile, print = (l) => console.log(l) }) {
584
+ const results = new Array(entries.length);
585
+ let printed = 0;
586
+ const flush = () => {
587
+ while (printed < entries.length && results[printed]) {
588
+ for (const l of results[printed].lines) print(l);
589
+ printed++;
590
+ }
591
+ };
592
+ // A TIMEOUT inside the pool can be the pool's fault: a heavy suite that spawns its own
593
+ // children can starve beside `jobs - 1` others. So a pooled file that times out is not
594
+ // judged there — it is re-run ALONE at the end, with the same budget, and that run is its
595
+ // verdict. A file that hangs alone still fails exactly as before. Only a timeout earns the
596
+ // re-run: an assertion failure in the pool is a failure.
597
+ const rerunAlone = [];
598
+ const reran = new Set();
599
+ const work = async (idx, { pooled }) => {
600
+ const e = entries[idx];
601
+ const r = await run(e.file, perTestTimeout);
602
+ if (pooled && r.killed) { rerunAlone.push(idx); return; }
603
+ const out = renderResult(e.displayName, r, perTestTimeout);
604
+ if (!pooled && reran.has(idx)) out.lines.splice(1, 0, ` (timed out beside ${jobs - 1} other file(s) in the pool; this verdict is from a re-run alone)`);
605
+ results[idx] = out;
606
+ flush();
607
+ };
608
+ const all = entries.map((_, i) => i);
609
+ const serialIdx = jobs > 1 ? all.filter((i) => entries[i].serial) : all;
610
+ const poolIdx = jobs > 1 ? all.filter((i) => !entries[i].serial) : [];
611
+ await runPool(serialIdx, 1, (idx) => work(idx, { pooled: false }));
612
+ await runPool(poolIdx, jobs, (idx) => work(idx, { pooled: true }));
613
+ rerunAlone.sort((a, b) => a - b);
614
+ for (const idx of rerunAlone) reran.add(idx);
615
+ await runPool(rerunAlone, 1, (idx) => work(idx, { pooled: false }));
616
+ flush();
617
+ return results;
618
+ }
619
+
386
620
  // Discover modules/<key>/tests/*.mjs. Pure-ish (reads disk; `isEnabled` is
387
621
  // injected so this is unit-testable with zero real modules/ or DB config —
388
622
  // mirrors the injected-dependency pattern the module loader itself uses).
@@ -495,7 +729,7 @@ function discoverModuleTests({ modulesDir = MODULES_DIR, isEnabled = isModuleEna
495
729
  const INTEGRATION_PARKED = new Set([
496
730
  ]);
497
731
 
498
- function main() {
732
+ async function main() {
499
733
  // Two gates from one runner (task 1002221 / audit H1):
500
734
  // default — the DB-free UNIT gate: every test file MINUS the integration set (+ known-
501
735
  // failing / support / disabled-module). Fast, runs in a clean checkout / per PR.
@@ -571,88 +805,35 @@ function main() {
571
805
  }
572
806
  const total = files.length + moduleTests.length;
573
807
 
574
- const failures = [];
575
- const teardownAborts = [];
576
808
  // A GREEN suite that skipped cases is the shape this roll-up used to hide: the suite said so
577
809
  // on its own stdout, which was discarded on success, so the only visible line was `ok` and a
578
810
  // session reading the roll-up could not tell exercised coverage from absent coverage.
579
- const skipped = [];
580
- const runOne = (displayName, filePath) => {
581
- try {
582
- const out = execFileSync(process.execPath, [filePath], {
583
- // task 1002644: stdout used to be IGNORED, so the failing assertion (test
584
- // files print their FAIL lines to stdout) was discarded and the only
585
- // context shown was the last 3 STDERR lines — often stale diagnostic
586
- // noise from an EARLIER, PASSING test. The live example: gate_approvals'
587
- // route-catch console.error dump made every failure in that file read as
588
- // a GraphQL/network outage, and the real flaky assertion was invisible.
589
- stdio: ['ignore', 'pipe', 'pipe'],
590
- maxBuffer: 16 * 1024 * 1024,
591
- timeout: perTestTimeout,
592
- env: { ...process.env, NODE_ENV: 'test' },
593
- });
594
- const n = countSkips(out ? out.toString() : '');
595
- if (n) skipped.push({ name: displayName, n });
596
- console.log(` ok ${displayName}${n ? ` (${n} SKIPPED — not run here)` : ''}`);
597
- } catch (err) {
598
- // `exit null` on its own is unreadable, and it is what a HANG looks like here: the
599
- // signal that ended the process is the whole diagnosis, so say it (task 1004052 —
600
- // a test that hung for the full budget reported `exit null` and nothing else).
601
- const why = err.killed
602
- ? `TIMEOUT after ${perTestTimeout / 1000}s${err.signal ? `, ${err.signal}` : ''}`
603
- : `exit ${err.status}${err.signal ? ` (${err.signal})` : ''}`;
604
- const stdoutText = (err.stdout ? err.stdout.toString() : '').trim();
605
- if (!err.killed && isCleanTeardownAbort(stdoutText, err.stderr ? err.stderr.toString() : '')) {
606
- console.log(` ok ${displayName} (clean verdict, then libuv teardown abort, ${why})`);
607
- teardownAborts.push(displayName);
608
- return;
609
- }
610
- console.log(` FAIL ${displayName} (${why})`);
611
- // The actual failures first: the file's own FAIL lines, then its stdout
612
- // tail (the last failure's assertion detail + summary), both bounded.
613
- // EMPTY STDOUT IS ITSELF THE CLUE, and it used to print as a blank gap. node:test
614
- // buffers a file's whole report until the file finishes, so a suite that hangs
615
- // mid-run emits NOTHING — which reads as "no detail available" when it actually
616
- // means "it never got to the end". Name that, and name how to find the case.
617
- if (!stdoutText) {
618
- console.log(' (no output at all — node:test buffers a file\'s report until it finishes,');
619
- console.log(' so this suite did not reach the end. To find which case: run it on the same');
620
- console.log(' platform with each case writing its name to STDERR before it runs; stderr is');
621
- console.log(' unbuffered, so the last name with no completion is the one that hung.)');
622
- }
623
- const failLines = stdoutText ? stdoutText.split('\n').filter((l) => /\bFAIL\b/.test(l)).slice(0, 5) : [];
624
- for (const l of failLines) console.log(` ${l.trim()}`);
625
- if (stdoutText) {
626
- for (const l of stdoutText.split('\n').slice(-5)) console.log(` ${l}`);
627
- }
628
- const stderr = (err.stderr ? err.stderr.toString() : '').trim().split('\n').slice(-3).join('\n ');
629
- if (stderr) console.log(` stderr: ${stderr}`);
630
- failures.push(displayName);
631
- }
632
- };
633
-
634
- // EXECUTION IS SERIAL, ONE PROCESS PER TEST — and that is load-bearing, not
635
- // incidental (task 1001435). runOne uses execFileSync, which blocks, and these
636
- // are plain sequential loops: no Promise.all, no worker pool, no concurrency
637
- // anywhere in this file. Pinned by tests/unit_runner_serial.mjs.
638
811
  //
639
- // Why it is written down. A flaky-`unit` report (this task, from idea #353)
640
- // diagnosed "parallel execution racing on shared global state" and asked whether
641
- // the runner fans tests out concurrently in one process. It does not, and never
642
- // has — so an in-process race is ruled out as a cause by construction, and any
643
- // future flake investigation should start somewhere else. The three suites that
644
- // report named were audited then:
645
- // - fitness.mjs — writes only under mkdtempSync(os.tmpdir()); no repo
646
- // writes, no env mutation. Clean.
647
- // - session_index.mjs — mutates no global state at all. Clean.
648
- // - secret_box.mjs — DID have a real hole, but not this one: it redirected
649
- // only HOME, which os.homedir() ignores on Windows, so
650
- // its sandbox was a no-op there. Fixed under this task.
651
- // Collisions between separate processes remain possible through state OUTSIDE
652
- // them — the real home directory, generated files in the repo, a second run on
653
- // the same machine — which is where to look next, not here.
654
- for (const name of files) runOne(name, path.join(TESTS_DIR, `${name}.mjs`));
655
- for (const { key, name, file } of moduleTests) runOne(`${key}/${name}`, file);
812
+ // Execution: see "Parallel lane + serial lane" above (task 1004926). Every file still runs in
813
+ // its own node process with the same budget and the same verdict rules; up to `jobs` of them
814
+ // run at once, and the files that share state outside their process run alone first. The
815
+ // integration gate is always serial — its tests share one database.
816
+ const entries = [
817
+ ...files.map((name) => ({ displayName: name, file: path.join(TESTS_DIR, `${name}.mjs`) })),
818
+ ...moduleTests.map(({ key, name, file }) => ({ displayName: `${key}/${name}`, file })),
819
+ ];
820
+ const jobs = integrationMode ? 1 : resolveJobs();
821
+ if (jobs > 1) {
822
+ for (const e of entries) e.serial = serialReason(path.basename(e.file, '.mjs'), fs.readFileSync(e.file, 'utf8'));
823
+ const serialCount = entries.filter((e) => e.serial).length;
824
+ console.log(`run-unit-tests: ${jobs} at a time; ${serialCount} file(s) in the serial lane run alone first (set ${UNIT_TESTS_SERIAL_ENV}=1 to run everything one at a time)`);
825
+ }
826
+ const results = await runEntries(entries, { jobs, perTestTimeout });
827
+
828
+ const failures = [];
829
+ const teardownAborts = [];
830
+ const skipped = [];
831
+ results.forEach((r, i) => {
832
+ const name = entries[i].displayName;
833
+ if (r.outcome === 'fail') failures.push(name);
834
+ else if (r.outcome === 'teardown') teardownAborts.push(name);
835
+ else if (r.skips) skipped.push({ name, n: r.skips });
836
+ });
656
837
 
657
838
  const label = integrationMode ? 'run-unit-tests --integration' : 'run-unit-tests';
658
839
  console.log('');
@@ -673,6 +854,11 @@ function main() {
673
854
  process.exit(1);
674
855
  }
675
856
 
676
- if (require.main === module) main();
857
+ if (require.main === module) {
858
+ main().catch((err) => {
859
+ console.error(err && err.stack ? err.stack : err);
860
+ process.exit(1);
861
+ });
862
+ }
677
863
 
678
- module.exports = { discoverModuleTests, isCleanTeardownAbort, countSkips, describeFailures, STATUS_DESCRIPTION_MAX, INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING };
864
+ module.exports = { resolveJobs, serialReason, runFile, renderResult, runEntries, SERIAL, UNIT_TESTS_SERIAL_ENV, UNIT_TEST_JOBS_ENV, discoverModuleTests, isCleanTeardownAbort, countSkips, describeFailures, STATUS_DESCRIPTION_MAX, INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING };
@@ -0,0 +1,56 @@
1
+ {
2
+ "_comment": "Example spec for scripts/bongos/seed-tasks.js (task 1004979). Copy it, fill in your goal, and run with --dry-run first. criterion_ids are the goal's criterion SLUGS, never positional Cn refs (learning 1000188). A links[] body value {\"$task\": \"R03\"} becomes that task's id.",
3
+ "version_id": "EXAMPLE-V1",
4
+ "goal_id": 1000001,
5
+ "source": "planning-session",
6
+ "source_ref_prefix": "example-v1-onboarding",
7
+ "tasks": [
8
+ {
9
+ "ref": "R01",
10
+ "title": "Spike: where the welcome checklist is stored",
11
+ "description": "Output an ADR naming the table and who writes it.",
12
+ "kind": "spike",
13
+ "discipline": "engineer",
14
+ "priority": 4,
15
+ "est_minutes": 90,
16
+ "criterion_ids": ["newcomer-knows-what-to-do-next"]
17
+ },
18
+ {
19
+ "ref": "R02",
20
+ "title": "The welcome checklist API",
21
+ "description": "Build to R01's ADR. Done-when: a new builder's checklist is returned by one GET.",
22
+ "kind": "feature",
23
+ "discipline": "engineer",
24
+ "priority": 4,
25
+ "est_minutes": 180,
26
+ "criterion_ids": ["newcomer-knows-what-to-do-next"],
27
+ "deps": ["R01"]
28
+ },
29
+ {
30
+ "ref": "R03",
31
+ "title": "The welcome checklist on the home page",
32
+ "description": "Render the checklist from R02 on the newcomer's home page.",
33
+ "kind": "feature",
34
+ "discipline": "ui",
35
+ "priority": 3,
36
+ "est_minutes": 150,
37
+ "criterion_ids": ["newcomer-knows-what-to-do-next"],
38
+ "deps": ["R02"]
39
+ },
40
+ {
41
+ "ref": "R04",
42
+ "title": "Proof: a newcomer finds their next step without asking",
43
+ "description": "A brand-new builder signs in and finds what to do next unaided.",
44
+ "kind": "verify",
45
+ "discipline": "engineer",
46
+ "priority": 3,
47
+ "est_minutes": 60,
48
+ "criterion_ids": ["newcomer-knows-what-to-do-next"],
49
+ "deps": ["R03"]
50
+ }
51
+ ],
52
+ "promote": ["R01"],
53
+ "links": [
54
+ { "method": "PATCH", "path": "/inbox/1000002", "body": { "status": "promoted", "promoted_to_task_id": { "$task": "R03" } } }
55
+ ]
56
+ }