@bongos/core 1.23.21 → 1.24.1

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 (457) hide show
  1. package/.bongos-core.json +983 -428
  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 +6 -3
  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 +94 -0
  27. package/clients/bongos-client/index.cjs +94 -0
  28. package/clients/bongos-client/index.d.ts +154 -4
  29. package/clients/bongos-client/index.mjs +94 -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 +87 -0
  33. package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
  34. package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
  35. package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
  36. package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
  37. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  38. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  39. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  40. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  41. package/docs/adr/README.md +10 -1
  42. package/docs/api/openapi.json +8619 -5885
  43. package/docs/api-reference.md +68 -9
  44. package/docs/architecture.md +37 -1
  45. package/docs/canonical-permissions.md +1 -1
  46. package/docs/copy-inventory.md +420 -337
  47. package/docs/copy-registry.json +1321 -500
  48. package/docs/file-map.md +4 -0
  49. package/docs/module-api-changelog.md +7 -0
  50. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  51. package/docs/onboarding/diagrams/README.md +1 -1
  52. package/docs/onboarding/drachmae-and-karma.md +15 -15
  53. package/docs/onboarding/primer.md +2 -2
  54. package/docs/page-inventory.json +55 -6
  55. package/docs/page-readings.json +1185 -1011
  56. package/docs/recipes/google-sign-in.md +60 -0
  57. package/docs/recipes/ops-gotchas.md +1 -1
  58. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  59. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  60. package/migrations/core_283_payout_approvals.sql +183 -0
  61. package/migrations/core_284_payout_gates.sql +179 -0
  62. package/migrations/core_285_task_needs.sql +74 -0
  63. package/modules/ai-studio/CLAUDE.md +15 -0
  64. package/modules/ai-studio/billing.js +67 -0
  65. package/modules/ai-studio/context.js +29 -0
  66. package/modules/ai-studio/db.js +172 -0
  67. package/modules/ai-studio/gemini.js +75 -0
  68. package/modules/ai-studio/generate.js +119 -0
  69. package/modules/ai-studio/keys.js +73 -0
  70. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  71. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  72. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  73. package/modules/ai-studio/module.json +26 -0
  74. package/modules/ai-studio/openrouter.js +90 -0
  75. package/modules/ai-studio/payments.js +105 -0
  76. package/modules/ai-studio/pricing.js +92 -0
  77. package/modules/ai-studio/routes/ai-studio.js +252 -0
  78. package/modules/ai-studio/studio.js +121 -0
  79. package/modules/autonomy/fence.js +64 -1
  80. package/modules/builder-settings/builder-needs.js +27 -1
  81. package/modules/copy-desk/routes/copy-desk.js +5 -0
  82. package/modules/economy/achievements.js +22 -10
  83. package/modules/economy/cost-classify.js +9 -0
  84. package/modules/economy/cost-write-guard.js +52 -0
  85. package/modules/economy/cost.js +2 -1
  86. package/modules/economy/credits.js +18 -15
  87. package/modules/economy/goal-reward.js +161 -0
  88. package/modules/economy/reward.js +4 -0
  89. package/modules/economy/routes/cost.js +33 -7
  90. package/modules/government/catalog.js +6 -0
  91. package/modules/government/db.js +108 -0
  92. package/modules/government/docket.js +36 -0
  93. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  94. package/modules/government/pollers/board-expiry.js +108 -20
  95. package/modules/government/protected-surfaces.json +1 -0
  96. package/modules/government/routes/government.js +50 -18
  97. package/modules/government/routing.js +39 -5
  98. package/modules/government/waiting.js +74 -0
  99. package/modules/grading/grader-subagent.js +30 -11
  100. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  101. package/modules/hall-ui/public/ai-studio.css +193 -0
  102. package/modules/hall-ui/public/ai-studio.html +98 -0
  103. package/modules/hall-ui/public/ai-studio.js +556 -0
  104. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  105. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  106. package/modules/hall-ui/public/blockers-lib.js +15 -1
  107. package/modules/hall-ui/public/blockers.html +8 -0
  108. package/modules/hall-ui/public/blockers.js +1 -1
  109. package/modules/hall-ui/public/city.html +15 -15
  110. package/modules/hall-ui/public/claim-action.js +3 -0
  111. package/modules/hall-ui/public/collab.html +12 -0
  112. package/modules/hall-ui/public/craft-mode.js +4 -1
  113. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  114. package/modules/hall-ui/public/drachmae.html +1 -1
  115. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  120. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  121. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  122. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  123. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  124. package/modules/hall-ui/public/goal-inbox.js +1 -1
  125. package/modules/hall-ui/public/goals-render.js +4 -0
  126. package/modules/hall-ui/public/goals.html +8 -0
  127. package/modules/hall-ui/public/gov-living.js +6 -2
  128. package/modules/hall-ui/public/hall-ask.css +5 -0
  129. package/modules/hall-ui/public/hall-ask.js +39 -3
  130. package/modules/hall-ui/public/notes-panel.css +20 -0
  131. package/modules/hall-ui/public/notes-panel.js +146 -0
  132. package/modules/hall-ui/public/project-settings.html +1 -1
  133. package/modules/hall-ui/public/project-settings.js +7 -5
  134. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  135. package/modules/hall-ui/public/settings.html +15 -0
  136. package/modules/hall-ui/public/street-deck.js +12 -3
  137. package/modules/hall-ui/public/studio.css +90 -8
  138. package/modules/hall-ui/public/studio.html +39 -10
  139. package/modules/hall-ui/public/studio.js +148 -18
  140. package/modules/hall-ui/public/task.html +2 -0
  141. package/modules/hall-ui/public/task.js +4 -0
  142. package/modules/hall-ui/public/tweak-editor.js +12 -3
  143. package/modules/hall-ui/public/welcome-boot.css +42 -6
  144. package/modules/hall-ui/public/welcome-boot.js +20 -6
  145. package/modules/hall-ui/public/work.js +20 -0
  146. package/modules/hall-ui/records/ai-studio.md +36 -0
  147. package/modules/hall-ui/records/blockers-people.md +24 -0
  148. package/modules/lifecycle/db-analytics.js +10 -3
  149. package/modules/lifecycle/db-claims.js +32 -4
  150. package/modules/lifecycle/db-goals.js +48 -18
  151. package/modules/lifecycle/db-shared.js +9 -0
  152. package/modules/lifecycle/db-tasks.js +15 -6
  153. package/modules/lifecycle/db-versions.js +21 -8
  154. package/modules/lifecycle/done-when.js +23 -1
  155. package/modules/lifecycle/lifecycle.js +5 -0
  156. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  157. package/modules/lifecycle/module.json +2 -1
  158. package/modules/lifecycle/notes.js +219 -0
  159. package/modules/lifecycle/page-notice.js +124 -0
  160. package/modules/lifecycle/page-tweak-artist.js +18 -1
  161. package/modules/lifecycle/publish-reconciler.js +67 -20
  162. package/modules/lifecycle/routes/claims.js +5 -2
  163. package/modules/lifecycle/routes/goals.js +3 -1
  164. package/modules/lifecycle/routes/notes.js +91 -0
  165. package/modules/lifecycle/routes/project-settings.js +43 -35
  166. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  167. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  168. package/modules/lifecycle/routes/tasks.js +12 -12
  169. package/modules/lifecycle/ship-assurance.js +4 -1
  170. package/modules/lifecycle/ship-card.js +8 -2
  171. package/modules/lifecycle/task-needs-gate.js +70 -0
  172. package/modules/market-watch/CLAUDE.md +15 -0
  173. package/modules/market-watch/db.js +340 -0
  174. package/modules/market-watch/key.js +58 -0
  175. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  176. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  177. package/modules/market-watch/module.json +26 -0
  178. package/modules/market-watch/pollers/hub-seed.js +54 -0
  179. package/modules/market-watch/pollers/weekly-run.js +100 -0
  180. package/modules/market-watch/research.js +350 -0
  181. package/modules/market-watch/run.js +245 -0
  182. package/modules/market-watch/seed/market-data.json +38 -0
  183. package/modules/market-watch/seed/partners-data.json +497 -0
  184. package/modules/market-watch/seed.js +252 -0
  185. package/modules/market-watch/store.js +230 -0
  186. package/modules/memory/routes/memory.js +14 -2
  187. package/modules/memory/routes/search.js +8 -2
  188. package/modules/onboarding/howto-moments.js +8 -1
  189. package/modules/platform-identity/account-logins.js +192 -0
  190. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  191. package/modules/platform-identity/platform-identity.js +5 -4
  192. package/modules/platform-identity/routes/my-projects.js +6 -0
  193. package/modules/platform-identity/routes/profile.js +18 -0
  194. package/modules/platform-identity/routes/public-profile.js +10 -1
  195. package/modules/platform-identity/routes/sso.js +26 -1
  196. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  197. package/modules/provisioning/credential-preflight.js +13 -3
  198. package/modules/provisioning/pollers/app-liveness.js +25 -5
  199. package/modules/provisioning/routes/provisioning.js +1 -1
  200. package/modules/provisioning/starter-bundles.js +8 -2
  201. package/modules/public-landing/public/account.html +10 -2
  202. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  203. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  205. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  209. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  210. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  213. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  214. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  221. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  222. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  223. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  224. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  225. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  226. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  227. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  228. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  229. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  230. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  233. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  244. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  245. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  246. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  247. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  248. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  249. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  250. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  251. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  252. package/modules/public-landing/public/community.html +1 -1
  253. package/modules/public-landing/public/connections.html +2 -2
  254. package/modules/public-landing/public/guild.html +1 -1
  255. package/modules/public-landing/public/invitations.html +2 -2
  256. package/modules/public-landing/public/projects.html +5 -5
  257. package/modules/raise/views/nda.html +2 -0
  258. package/modules/specialities/db.js +32 -1
  259. package/modules/specialities/routes/specialities.js +10 -6
  260. package/modules/status-ui/public/index.html +1 -1
  261. package/package-lock.json +2 -2
  262. package/package.json +1 -1
  263. package/release-notes.json +286 -0
  264. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  265. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  266. package/scripts/bongos/claim.js +22 -1
  267. package/scripts/bongos/db-backup-nightly.sh +29 -0
  268. package/scripts/bongos/deploy-divergence.js +7 -0
  269. package/scripts/bongos/doc-cli-guard.js +6 -0
  270. package/scripts/bongos/fitness.js +3 -2
  271. package/scripts/bongos/gen-diagrams.js +3 -3
  272. package/scripts/bongos/lib.sh +4 -5
  273. package/scripts/bongos/migration-namespace.js +13 -1
  274. package/scripts/bongos/plain-cards.js +4 -0
  275. package/scripts/bongos/provision-github-setup.js +3 -1
  276. package/scripts/bongos/provision-units.js +6 -1
  277. package/scripts/bongos/publish-manifest.js +4 -0
  278. package/scripts/bongos/release.js +2 -0
  279. package/scripts/bongos/run-unit-tests.js +268 -82
  280. package/scripts/bongos/seed-tasks.example.json +56 -0
  281. package/scripts/bongos/seed-tasks.js +222 -0
  282. package/scripts/bongos/seed-verify-criteria.js +1 -2
  283. package/scripts/bongos/ship-finish.js +2 -0
  284. package/scripts/bongos/ship-resume.js +2 -0
  285. package/scripts/bongos/strand-lease.js +250 -0
  286. package/scripts/bongos/strand-watch.js +129 -12
  287. package/scripts/bongos/version-literal-guard.js +2 -2
  288. package/scripts/discord/post.js +4 -3
  289. package/scripts/gds/seed-tasks.js +7 -0
  290. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  291. package/scripts/hall-preview/server.js +21 -0
  292. package/src/bongos/auth-config.js +29 -1
  293. package/src/bongos/auth-discord.js +3 -3
  294. package/src/bongos/auth-github.js +3 -6
  295. package/src/bongos/auth-google.js +217 -0
  296. package/src/bongos/device-approval.js +140 -0
  297. package/src/bongos/llm-pricing.js +17 -1
  298. package/src/bongos/metrics.js +48 -2
  299. package/src/bongos/middleware/rate-limit.js +30 -11
  300. package/src/bongos/module-scope-map.js +16 -1
  301. package/src/bongos/optimizer.js +15 -1
  302. package/src/bongos/payout-approvals.js +342 -0
  303. package/src/bongos/payout-gates.js +364 -0
  304. package/src/bongos/prelaunch-gate.js +7 -9
  305. package/src/bongos/prelaunch-switch.js +20 -0
  306. package/src/bongos/route-rank-check.js +3 -0
  307. package/src/bongos/routes/auth-google.js +246 -0
  308. package/src/bongos/routes/auth.js +38 -1
  309. package/src/bongos/routes/backup.js +33 -9
  310. package/src/bongos/routes/builders.js +57 -30
  311. package/src/bongos/routes/github.js +9 -0
  312. package/src/bongos/routes/me.js +18 -0
  313. package/src/bongos/routes/my-payouts.js +133 -0
  314. package/src/bongos/routes/payout-approvals.js +312 -0
  315. package/src/bongos/routes/payout-gates.js +179 -0
  316. package/src/bongos/routes.js +15 -1
  317. package/src/bongos/secret-box.js +18 -3
  318. package/src/bongos/serve-internal.js +47 -19
  319. package/src/bongos/sign-in-pages.js +138 -0
  320. package/src/bongos/task-needs.js +146 -0
  321. package/src/bongos-downloads.js +18 -1
  322. package/src/instance-config.js +28 -0
  323. package/src/module-api.js +14 -1
  324. package/src/platform-server.js +27 -1
  325. package/tests/account_logins.mjs +326 -0
  326. package/tests/account_logins_db.mjs +161 -0
  327. package/tests/ai_studio_billing.mjs +253 -0
  328. package/tests/ai_studio_generate.mjs +282 -0
  329. package/tests/ai_studio_keys.mjs +159 -0
  330. package/tests/ai_studio_requests.mjs +319 -0
  331. package/tests/auth_backchannel_logout_route.mjs +201 -0
  332. package/tests/auto_satisfy_criteria.mjs +10 -1
  333. package/tests/autobongos_tweak_lane.mjs +144 -0
  334. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  335. package/tests/backfill_subagent_rewards.mjs +17 -1
  336. package/tests/backup_trigger_lock.mjs +119 -0
  337. package/tests/batched_writes_parity.mjs +340 -0
  338. package/tests/blockers_people.mjs +198 -0
  339. package/tests/bounded_rate_maps.mjs +184 -0
  340. package/tests/claimable_blocker_status.mjs +1 -1
  341. package/tests/claims_batch_routes.mjs +421 -0
  342. package/tests/conductor_main_e2e.mjs +4 -0
  343. package/tests/config_read_cache.mjs +171 -0
  344. package/tests/copy_desk_page_editor.mjs +12 -0
  345. package/tests/cost_write_guard.mjs +200 -0
  346. package/tests/craft_achievements.mjs +33 -10
  347. package/tests/criterion_goal_attach.mjs +1 -1
  348. package/tests/db_backup_nightly.mjs +73 -1
  349. package/tests/deploy_divergence_line.mjs +7 -0
  350. package/tests/effective_visibility_predicate.mjs +12 -0
  351. package/tests/fitness.mjs +19 -0
  352. package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
  353. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  354. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  355. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  356. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  357. package/tests/github_one_trip.mjs +34 -0
  358. package/tests/github_repo_visibility.mjs +20 -0
  359. package/tests/goal_achievement.mjs +30 -1
  360. package/tests/goal_achievement_reward.mjs +205 -0
  361. package/tests/goal_archive_disposition.mjs +5 -5
  362. package/tests/goal_sort_staleness.mjs +2 -1
  363. package/tests/goal_tier.mjs +11 -1
  364. package/tests/google_fonts_nonblocking.mjs +84 -0
  365. package/tests/google_sign_in.mjs +401 -0
  366. package/tests/government_board_expiry.mjs +94 -0
  367. package/tests/government_protected_surfaces.mjs +1 -1
  368. package/tests/government_routes.mjs +146 -23
  369. package/tests/grader.mjs +31 -0
  370. package/tests/hall_ai_studio.mjs +347 -0
  371. package/tests/hall_city.mjs +8 -0
  372. package/tests/hall_page_gate_map.mjs +3 -0
  373. package/tests/hall_studio_home.mjs +61 -2
  374. package/tests/home_attention_cards.mjs +1 -1
  375. package/tests/hook_shell_tools.mjs +8 -0
  376. package/tests/hooks_drain.mjs +1 -1
  377. package/tests/host_topology_skips.mjs +2 -0
  378. package/tests/howto_moments.mjs +17 -0
  379. package/tests/human_signal.mjs +141 -0
  380. package/tests/install_script_reader.mjs +51 -0
  381. package/tests/integration_fixture_ids.mjs +82 -0
  382. package/tests/invite_expiry_db.mjs +13 -1
  383. package/tests/lib_sh_resolution.mjs +0 -4
  384. package/tests/market_watch_data.mjs +244 -0
  385. package/tests/market_watch_data_db.mjs +308 -0
  386. package/tests/market_watch_module.mjs +114 -0
  387. package/tests/market_watch_run.mjs +503 -0
  388. package/tests/metrics.mjs +124 -0
  389. package/tests/migration_namespace.mjs +14 -0
  390. package/tests/module_api.mjs +2 -0
  391. package/tests/module_loader.mjs +1 -1
  392. package/tests/module_skills_hook.mjs +69 -0
  393. package/tests/notes.mjs +182 -0
  394. package/tests/own_scope_guard.mjs +5 -0
  395. package/tests/page_tweak_marker_sql.mjs +76 -0
  396. package/tests/page_tweak_no_block.mjs +135 -0
  397. package/tests/page_tweak_notice.mjs +103 -0
  398. package/tests/payout_approvals.mjs +253 -0
  399. package/tests/payout_approvals_db.mjs +106 -0
  400. package/tests/payout_gates.mjs +322 -0
  401. package/tests/payout_gates_db.mjs +113 -0
  402. package/tests/planning_session_skill.mjs +13 -3
  403. package/tests/project_settings_page.mjs +29 -5
  404. package/tests/project_settings_spend_cap_route.mjs +235 -0
  405. package/tests/promotion_sweep.mjs +29 -1
  406. package/tests/provision.mjs +3 -2
  407. package/tests/provisioning_app_status.mjs +18 -0
  408. package/tests/provisioning_credential_preflight.mjs +53 -0
  409. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  410. package/tests/public_landing_projects.mjs +37 -0
  411. package/tests/publish_reconciler_overlap.mjs +60 -0
  412. package/tests/rank_change_broadcast_port.mjs +151 -0
  413. package/tests/seed_tasks.mjs +176 -0
  414. package/tests/seed_verify_criteria.mjs +2 -6
  415. package/tests/session_start_freshness.mjs +66 -1
  416. package/tests/shell_hooks.mjs +209 -0
  417. package/tests/ship_assurance.mjs +28 -0
  418. package/tests/ship_card.mjs +20 -5
  419. package/tests/site_classified_once.mjs +142 -0
  420. package/tests/speciality_db_shape.mjs +23 -0
  421. package/tests/speciality_session_skills.mjs +48 -2
  422. package/tests/strand_lease.mjs +249 -0
  423. package/tests/strand_watch.mjs +58 -0
  424. package/tests/task_criteria_routes.mjs +275 -0
  425. package/tests/task_criteria_unlink_db.mjs +123 -0
  426. package/tests/task_needs.mjs +233 -0
  427. package/tests/task_needs_db.mjs +110 -0
  428. package/tests/unit_runner_serial.mjs +266 -43
  429. package/tests/version_close_route.mjs +1 -1
  430. package/tests/version_literals.mjs +5 -7
  431. package/tests/welcome_boot.mjs +16 -5
  432. package/tests/welcome_boot_browser.mjs +36 -4
  433. package/docs/recipes/bongos-cli-release.md +0 -66
  434. package/scripts/bongos/backfill-disciplines.js +0 -144
  435. package/scripts/bongos/backfill-session-earnings.js +0 -123
  436. package/scripts/bongos/backfill-session-types.js +0 -87
  437. package/scripts/bongos/build-bongos.js +0 -94
  438. package/scripts/bongos/cleanup-orphans.js +0 -74
  439. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  440. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  441. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  442. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  443. package/scripts/bongos/smoke-batch-release.sh +0 -316
  444. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  445. package/scripts/bongos/smoke-dependencies.sh +0 -143
  446. package/scripts/gds/backfill-disciplines.js +0 -7
  447. package/scripts/gds/backfill-session-earnings.js +0 -7
  448. package/scripts/gds/backfill-session-types.js +0 -7
  449. package/scripts/gds/cleanup-orphans.js +0 -7
  450. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  451. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  452. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  453. package/scripts/gds/smoke-batch-claim.sh +0 -8
  454. package/scripts/gds/smoke-batch-release.sh +0 -8
  455. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  456. package/scripts/gds/smoke-dependencies.sh +0 -8
  457. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -22,6 +22,8 @@ const buildInstanceRouter = require('./routes/instance');
22
22
  const buildCoreUpdateRouter = require('./routes/core-update');
23
23
  const buildFeedbackRouter = require('./routes/feedback');
24
24
  const buildAuthRouter = require('./routes/auth');
25
+ // Google sign-in + the browser page that approves a CLI sign-in (task 1004993).
26
+ const buildAuthGoogleRouter = require('./routes/auth-google');
25
27
  const buildGithubRouter = require('./routes/github');
26
28
  const buildMeRouter = require('./routes/me');
27
29
  // The own-scoped session self-service (list / revoke one / sign out everywhere,
@@ -29,6 +31,11 @@ const buildMeRouter = require('./routes/me');
29
31
  // aggregator or the admin pair in routes/auth.js — see that file's header.
30
32
  const buildMySessionsRouter = require('./routes/my-sessions');
31
33
  const buildMyPasskeysRouter = require('./routes/my-passkeys');
34
+ // The payout approval step and its append-only log (task 1004913, ADR 0385 D4).
35
+ const buildPayoutApprovalsRouter = require('./routes/payout-approvals');
36
+ // Who may be paid at all: age tier, sanctions, a hold that tells the person (task 1004914).
37
+ const buildPayoutGatesRouter = require('./routes/payout-gates');
38
+ const buildMyPayoutsRouter = require('./routes/my-payouts');
32
39
  const buildBuildersRouter = require('./routes/builders');
33
40
  // The LIFECYCLE routes (tasks · claims · versions · done-when · goals ·
34
41
  // dependencies · analytics) carved into modules/lifecycle/
@@ -121,7 +128,10 @@ function buildGdsRouter() {
121
128
  /^\/provisioning\/instances\/\d+\/logo$/.test(req.path) ||
122
129
  // task 1004496: a reference image on a page feedback doc — up to 4 MB decoded,
123
130
  // ~5.3 MB as base64. modules/page-notes/routes/page-notes.js mounts its own 8 MB parser.
124
- /^\/page-notes\/\d+\/images$/.test(req.path)
131
+ /^\/page-notes\/\d+\/images$/.test(req.path) ||
132
+ // task 1004917: one sketch, image or PDF for an AI studio request — up to
133
+ // 8 MB decoded. modules/ai-studio/routes/ai-studio.js mounts its own 12 MB parser.
134
+ req.path === '/ai-studio/uploads'
125
135
  ) return next();
126
136
  return globalJson(req, res, next);
127
137
  });
@@ -159,10 +169,14 @@ function buildGdsRouter() {
159
169
  router.use(buildCoreUpdateRouter());
160
170
  router.use(buildFeedbackRouter());
161
171
  router.use(buildAuthRouter());
172
+ router.use(buildAuthGoogleRouter());
162
173
  router.use(buildGithubRouter());
163
174
  router.use(buildMeRouter());
164
175
  router.use(buildMySessionsRouter());
165
176
  router.use(buildMyPasskeysRouter());
177
+ router.use(buildPayoutApprovalsRouter());
178
+ router.use(buildPayoutGatesRouter());
179
+ router.use(buildMyPayoutsRouter());
166
180
  router.use(buildBuildersRouter());
167
181
  // tasks · claims · versions · done-when · goals · dependencies · analytics now
168
182
  // mount via moduleLoader.mountModuleRoutes below (lifecycle module, BV1.R86) —
@@ -26,6 +26,7 @@ const crypto = require('node:crypto');
26
26
  const fs = require('node:fs');
27
27
  const path = require('node:path');
28
28
  const ic = require('../instance-config');
29
+ const { readConfigFileCached, clearConfigReadCache } = require('../instance-config'); // destructured: knip credits only this form
29
30
 
30
31
  const VERSION = 'v1';
31
32
  const IV_BYTES = 12;
@@ -58,9 +59,22 @@ function decodeMasterKey(raw) {
58
59
  return null;
59
60
  }
60
61
 
62
+ // task 1004924: encrypt/decrypt/isConfigured run on request paths, and each used
63
+ // to decode the key afresh and, with no env key, read the key file from disk —
64
+ // a sync read blocking the event loop on every call. The decode is memoised on
65
+ // the raw string (one entry per source: an env change is simply a new string),
66
+ // and the file goes through the cached config read (60 s TTL, keyed by the
67
+ // resolved paths, dropped by provisionMasterKey).
68
+ const _decoded = { env: { raw: undefined, key: null }, file: { raw: undefined, key: null } };
69
+ function decodeMemo(slot, raw) {
70
+ const m = _decoded[slot];
71
+ if (m.raw !== raw) { m.raw = raw; m.key = decodeMasterKey(raw); }
72
+ return m.key;
73
+ }
74
+
61
75
  // Load the master key from env, then the local operator file. Returns Buffer|null.
62
76
  function loadMasterKey() {
63
- const fromEnv = decodeMasterKey(process.env.BUILDER_SECRET_KEY);
77
+ const fromEnv = decodeMemo('env', process.env.BUILDER_SECRET_KEY);
64
78
  if (fromEnv) return fromEnv;
65
79
  // A set-but-malformed env value is an operator mistake worth one loud line —
66
80
  // but we still fail closed (return null) rather than fall back to something weak.
@@ -69,8 +83,8 @@ function loadMasterKey() {
69
83
  console.error('[gds] BUILDER_SECRET_KEY is set but is not a 32-byte hex/base64 value — secret storage disabled until fixed.');
70
84
  }
71
85
  try {
72
- const raw = ic.readConfigFileSync('builder-secret.key');
73
- const fromFile = raw && decodeMasterKey(raw);
86
+ const raw = readConfigFileCached('builder-secret.key');
87
+ const fromFile = raw && decodeMemo('file', raw);
74
88
  if (fromFile) return fromFile;
75
89
  } catch (_) { /* not configured */ }
76
90
  return null;
@@ -121,6 +135,7 @@ function provisionMasterKey(opts = {}) {
121
135
  return { ok: false, error: (e && (e.code || e.message)) || 'write_failed' };
122
136
  }
123
137
  try { fs.chmodSync(file, 0o600); } catch (_) { /* best-effort tighten */ }
138
+ clearConfigReadCache(); // a cached "no key file" must not outlive the write
124
139
  return { ok: true, created: true };
125
140
  }
126
141
 
@@ -41,7 +41,7 @@ const gdsAuth = require('./auth');
41
41
  const { loadIdpConfig } = require('./auth-config');
42
42
  const { buildStampedHtml, hashFile } = require('../asset-version');
43
43
  const { buildInfo } = require('../build-info');
44
- const { buildBongosDownloads } = require('../bongos-downloads');
44
+ const { buildBongosDownloads, installScriptReader } = require('../bongos-downloads');
45
45
  const { clientBranding, branding, applyBrandConditionals } = require('../branding');
46
46
  const { nightTintCss } = require('../night-tint');
47
47
  const { prelaunchGate } = require('./prelaunch-gate');
@@ -500,8 +500,22 @@ function injectModuleHallWidgets(html, scripts = hallWidgetScripts({ isEnabled:
500
500
  // while the builders host keeps its own isBuildersHost guard (it's served by the
501
501
  // richer dedicated block, not the generic loop).
502
502
  function isStatusHost(req) {
503
- const host = (req.headers.host || '').toLowerCase().split(':')[0];
504
- return host.startsWith('status.');
503
+ return siteOf(req).host.startsWith('status.');
504
+ }
505
+
506
+ // WHICH SITE IS THIS REQUEST FOR, worked out ONCE per request (task 1004963) into
507
+ // req.bongosSite, which every host predicate reads instead of re-parsing the Host
508
+ // header (and the buildersOrigin URL) in each of ~15 handlers. Keyed on the raw
509
+ // Host it came from, so a changed header is re-classified, never answered stale.
510
+ // `surface` is filled by the mounted app's surfaceOf(), which knows its surfaces.
511
+ function siteOf(req) {
512
+ const raw = req.headers.host || '';
513
+ const cached = req.bongosSite;
514
+ if (cached && cached.raw === raw) return cached;
515
+ const host = raw.toLowerCase().split(':')[0];
516
+ const cfg = host.startsWith('builders.') ? '' : configuredBuildersHost();
517
+ const builders = host.startsWith('builders.') || (cfg !== '' && host === cfg);
518
+ return (req.bongosSite = { raw, host, builders, surface: null, surfaceFor: null });
505
519
  }
506
520
  // A single-host instance — one with no dedicated builders.<apex> subdomain, e.g. a
507
521
  // co-located instance on a one-level subdomain like emersonian-circles.cloudbongos.com
@@ -521,10 +535,7 @@ function configuredBuildersHost() {
521
535
  } catch { return ''; }
522
536
  }
523
537
  function isBuildersHost(req) {
524
- const host = (req.headers.host || '').toLowerCase().split(':')[0];
525
- if (host.startsWith('builders.')) return true;
526
- const cfg = configuredBuildersHost();
527
- return cfg !== '' && host === cfg;
538
+ return siteOf(req).builders;
528
539
  }
529
540
 
530
541
  // Is this instance served entirely from ONE host — publicOrigin and buildersOrigin
@@ -575,6 +586,13 @@ function mountInternalSurfaces(app) {
575
586
  // It sits ABOVE the metrics middleware deliberately: a refused knock is still
576
587
  // a request this instance served, and hiding gate 401s would make the traffic
577
588
  // graph lie about how much attention an unannounced instance is getting.
589
+ // Ahead of even the gate: classify the request's site ONCE (siteOf, task 1004963).
590
+ // Pure string work, no I/O. Both gates were checked for per-request I/O then, and
591
+ // need no new cache: they read process.env and the memoised branding singleton; a
592
+ // 'gated' instance's session lookup is already memoised per request (RESOLVED_SESSIONS
593
+ // in auth.js), and caching it across requests would let a revoked session through.
594
+ app.use((req, _res, next) => { surfaceOf(req); next(); });
595
+
578
596
  app.use(prelaunchGate());
579
597
 
580
598
  app.use(metrics.httpMiddleware());
@@ -706,11 +724,11 @@ function mountInternalSurfaces(app) {
706
724
  // a spoofed Host can't redirect a `curl | sh`). The fallback comes from the
707
725
  // branding pack, not a hardcoded host brand (ADR 0062 §3).
708
726
  const BONGOS_PUBLIC_ORIGIN = resolveEnv('PUBLIC_ORIGIN') || branding().domains.publicOrigin;
709
- const readBongosInstallScript = (name) => {
710
- try {
711
- return fs.readFileSync(path.join(ROOT, 'infra', name), 'utf8').split('__GDS_API_BASE__').join(BONGOS_PUBLIC_ORIGIN);
712
- } catch { return null; }
713
- };
727
+ // Read + substituted once per name, not per request (task 1004924).
728
+ const readBongosInstallScript = installScriptReader({
729
+ readFile: (name) => fs.readFileSync(path.join(ROOT, 'infra', name), 'utf8'),
730
+ origin: BONGOS_PUBLIC_ORIGIN,
731
+ });
714
732
  app.get('/downloads/bongos/latest.json', (_req, res) => {
715
733
  res.set('Cache-Control', 'public, max-age=60');
716
734
  res.json(bongosDownloads.latestJson());
@@ -790,6 +808,17 @@ function mountInternalSurfaces(app) {
790
808
  // hall's). urlPrefix '/' because the surface is served at the host root.
791
809
  html: injectBrandingInto(buildStampedHtml(s.dir, '/')),
792
810
  }));
811
+ // The generic module surface this request's Host is for (first match, as the old
812
+ // some()/find() pair), or null — kept on req.bongosSite (task 1004963). First called
813
+ // by the top-of-chain classifier, which only runs once webSurfaces is built.
814
+ function surfaceOf(req) {
815
+ const site = siteOf(req);
816
+ if (site.surfaceFor !== webSurfaces) {
817
+ site.surface = webSurfaces.find((s) => site.host.startsWith(s.host)) || null;
818
+ site.surfaceFor = webSurfaces;
819
+ }
820
+ return site.surface;
821
+ }
793
822
  // True iff this request's Host matches a GENERIC module web surface (status-style,
794
823
  // served by the loop below) — replaces the literal isStatusHost() guard the
795
824
  // /builders middleware used, so those guards skip every generic surface host with
@@ -797,13 +826,11 @@ function mountInternalSurfaces(app) {
797
826
  // partitioned out above), so the dedicated /builders blocks below do NOT self-skip
798
827
  // the builders host — they are what serves it.
799
828
  function isModuleSurfaceHost(req) {
800
- const host = (req.headers.host || '').toLowerCase().split(':')[0];
801
- return webSurfaces.some((s) => host.startsWith(s.host));
829
+ return surfaceOf(req) !== null;
802
830
  }
803
831
 
804
832
  app.use((req, res, next) => {
805
- const host = (req.headers.host || '').toLowerCase().split(':')[0];
806
- const surface = webSurfaces.find((s) => host.startsWith(s.host));
833
+ const surface = surfaceOf(req);
807
834
  if (!surface) return next();
808
835
  // /api/* already handled above; everything else on a surface host serves from
809
836
  // the module's static dir. The index page comes from the stamped in-memory copy
@@ -909,7 +936,7 @@ function mountInternalSurfaces(app) {
909
936
  app.use((req, res, next) => {
910
937
  if (isModuleSurfaceHost(req) || isBuildersHost(req)) return next();
911
938
  if (req.path !== '/builders' && !req.path.startsWith('/builders/')) return next();
912
- const apex = (req.headers.host || '').toLowerCase().split(':')[0];
939
+ const apex = siteOf(req).host;
913
940
  const rest = req.path.replace(/^\/builders\/?/, ''); // '/builders'→'', '/builders/watch'→'watch'
914
941
  const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
915
942
  return res.redirect(301, `https://builders.${apex}/${rest}${qs}`);
@@ -1296,8 +1323,9 @@ function mountInternalSurfaces(app) {
1296
1323
  // Claude works for them. Every read behind it is the viewer's own and
1297
1324
  // requireBuilder (GET /me and its /me/* prefs, /specialities), so sign-in is the
1298
1325
  // whole gate; the governing reads it adds are gated by their own routes.
1326
+ // task 1004921: `ai-studio`, the studio's AI pages; every route behind it is the viewer's own.
1299
1327
  const SIGNEDIN_BUILDERS_PAGE_RE =
1300
- /^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|my-agents|commands)(?:\.html)?\/?$/;
1328
+ /^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|ai-studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|my-agents|commands)(?:\.html)?\/?$/;
1301
1329
  // The diagram image assets under /builders/diagrams/ ride the same tier as the
1302
1330
  // page that embeds them.
1303
1331
  const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
@@ -1467,4 +1495,4 @@ function mountInternalSurfaces(app) {
1467
1495
  return app;
1468
1496
  }
1469
1497
 
1470
- module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, resolveClientBundleJs, generateClientBundleJs, clientBundleJs, tryLoadInstanceConfig };
1498
+ module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, siteOf, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, resolveClientBundleJs, generateClientBundleJs, clientBundleJs, tryLoadInstanceConfig };
@@ -0,0 +1,138 @@
1
+ // src/bongos/sign-in-pages.js — the small server-rendered pages a sign-in shows
2
+ // before a session exists: the "Continue with GitHub / Continue with Google"
3
+ // chooser, and the page where a person approves a command-line sign-in in their
4
+ // browser (task 1004993, ADR 0396).
5
+ //
6
+ // WHY ITS OWN FILE. Both are pure over their inputs and the branding pack, so a
7
+ // test can render them without a server, and routes/auth.js (which shows the
8
+ // chooser) and routes/auth-google.js (which shows the approval page) can share
9
+ // one look without requiring each other. Same palette and stylesheet as the
10
+ // auth error page in routes/auth.js, so every sign-in screen reads as one place.
11
+ 'use strict';
12
+
13
+ const { branding } = require('../branding');
14
+ const { escapeHtml } = require('../html-escape');
15
+
16
+ const GITHUB_MARK = '<svg aria-hidden="true" width="18" height="18" viewBox="0 0 16 16" style="vertical-align:-3px;margin-right:8px"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59 .4 .07 .55-.17 .55-.38 0-.19-.01-.82-.01-1.49-2.01 .37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53 .63-.01 1.08 .58 1.23 .82 .72 1.21 1.87 .87 2.33 .66 .07-.52 .28-.87 .51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87 .31-1.59 .82-2.15-.08-.2-.36-1.02 .08-2.12 0 0 .67-.21 2.2 .82 .64-.18 1.32-.27 2-.27 .68 0 1.36 .09 2 .27 1.53-1.04 2.2-.82 2.2-.82 .44 1.1 .16 1.92 .08 2.12 .51 .56 .82 1.27 .82 2.15 0 3.07-1.87 3.75-3.65 3.95 .29 .25 .54 .73 .54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21 .15 .46 .55 .38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>';
17
+ const GOOGLE_MARK = '<svg aria-hidden="true" width="18" height="18" viewBox="0 0 48 48" style="vertical-align:-3px;margin-right:8px"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>';
18
+
19
+ function worldName() {
20
+ try { return branding().identity?.worldName || 'Sign in'; } catch { return 'Sign in'; }
21
+ }
22
+
23
+ // One page shell: the hall stylesheet, a header, and a body the caller built
24
+ // (already escaped). Never a script: these pages work with forms and links alone.
25
+ function renderPage({ title, heading, subtitle, bodyHtml }) {
26
+ return `<!doctype html>
27
+ <html lang="en"><head>
28
+ <meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30
+ <meta name="referrer" content="no-referrer">
31
+ <title>${escapeHtml(worldName())} — ${escapeHtml(title)}</title>
32
+ <link rel="stylesheet" href="/builders/style.css">
33
+ <style>
34
+ .signinChoices{display:flex;flex-direction:column;gap:12px;max-width:340px;margin:8px auto 0}
35
+ .signinChoices .landing__btn{display:block;text-align:center}
36
+ .signinCode{font:600 1.6rem/1.2 ui-monospace,Menlo,Consolas,monospace;letter-spacing:.12em;text-align:center;margin:12px 0}
37
+ .signinRow{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
38
+ .signinRow form{margin:0}
39
+ .signinRow button{cursor:pointer}
40
+ .signinCodeForm{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
41
+ .signinCodeForm input{font:inherit;padding:8px 10px;text-transform:uppercase;letter-spacing:.1em;max-width:12em}
42
+ </style>
43
+ </head><body>
44
+ <header class="hall">
45
+ <h1>${escapeHtml(heading)}</h1>
46
+ ${subtitle ? `<p class="hall__sub">${escapeHtml(subtitle)}</p>` : ''}
47
+ </header>
48
+ <main>
49
+ <section class="scroll">
50
+ <div class="landing">
51
+ ${bodyHtml}
52
+ </div>
53
+ </section>
54
+ </main>
55
+ </body></html>`;
56
+ }
57
+
58
+ // The two ways in, side by side. Both hrefs are built by the caller from
59
+ // validated values; they are escaped here all the same.
60
+ function choiceButtons({ githubHref, googleHref }) {
61
+ return ` <div class="signinChoices">
62
+ <a class="landing__btn" href="${escapeHtml(githubHref)}">${GITHUB_MARK}Continue with GitHub</a>
63
+ <a class="landing__btn" href="${escapeHtml(googleHref)}">${GOOGLE_MARK}Continue with Google</a>
64
+ </div>`;
65
+ }
66
+
67
+ // The sign-in chooser /auth/web/start shows when Google sign-in is switched on.
68
+ function renderSignInChooser({ githubHref, googleHref }) {
69
+ return renderPage({
70
+ title: 'sign in',
71
+ heading: 'Sign in',
72
+ subtitle: 'New here? Either button creates your account.',
73
+ bodyHtml: `${choiceButtons({ githubHref, googleHref })}
74
+ <p class="landing__note">GitHub is only needed to create a project or to write code. Ideas, art and reviews work with either.</p>`,
75
+ });
76
+ }
77
+
78
+ // The command-line approval page, in its four states:
79
+ // 'enter' — no code, or one that is unknown or expired: ask for it
80
+ // 'signin' — a live code, nobody signed in: offer both ways in
81
+ // 'confirm' — a live code and a signed-in person: show it, Approve or Cancel
82
+ // 'done' — after the person decided (approved or cancelled)
83
+ function renderDeviceApprovalPage(view) {
84
+ const { state } = view;
85
+ if (state === 'signin') {
86
+ return renderPage({
87
+ title: 'approve command-line sign-in',
88
+ heading: 'Sign in to continue',
89
+ subtitle: 'Your command line is waiting for you to sign in.',
90
+ bodyHtml: ` <p class="signinCode">${escapeHtml(view.userCode)}</p>
91
+ ${choiceButtons(view)}
92
+ <p class="landing__note">After you sign in you come straight back here to approve it.</p>`,
93
+ });
94
+ }
95
+ if (state === 'confirm') {
96
+ return renderPage({
97
+ title: 'approve command-line sign-in',
98
+ heading: 'Approve this sign-in?',
99
+ subtitle: `A command line is asking to sign in as ${view.name}.`,
100
+ bodyHtml: ` <p>Check this code matches the one in your terminal:</p>
101
+ <p class="signinCode">${escapeHtml(view.userCode)}</p>
102
+ <div class="signinRow">
103
+ <form method="post" action="${escapeHtml(view.action)}">
104
+ <input type="hidden" name="user_code" value="${escapeHtml(view.userCode)}">
105
+ <input type="hidden" name="decision" value="approve">
106
+ <button class="landing__btn" type="submit">Approve</button>
107
+ </form>
108
+ <form method="post" action="${escapeHtml(view.action)}">
109
+ <input type="hidden" name="user_code" value="${escapeHtml(view.userCode)}">
110
+ <input type="hidden" name="decision" value="deny">
111
+ <button class="landing__btn" type="submit">Cancel</button>
112
+ </form>
113
+ </div>
114
+ <p class="landing__note">Only approve if you just started a sign-in on your own computer. If someone sent you this link, press Cancel.</p>`,
115
+ });
116
+ }
117
+ if (state === 'done') {
118
+ return renderPage({
119
+ title: view.approved ? 'signed in' : 'cancelled',
120
+ heading: view.approved ? 'You’re signed in' : 'Sign-in cancelled',
121
+ subtitle: view.approved ? 'Go back to your terminal — it carries on by itself.' : 'Nothing was signed in. You can close this page.',
122
+ bodyHtml: ' <p class="landing__note">You can close this page.</p>',
123
+ });
124
+ }
125
+ return renderPage({
126
+ title: 'approve command-line sign-in',
127
+ heading: 'Enter your code',
128
+ subtitle: view.problem || 'Type the code your terminal shows.',
129
+ bodyHtml: ` <form class="signinCodeForm" method="get" action="${escapeHtml(view.action)}">
130
+ <label for="code" class="landing__note" style="width:100%;text-align:center">Code</label>
131
+ <input id="code" name="code" autocomplete="off" autocapitalize="characters" spellcheck="false" placeholder="ABCD-EFGH" maxlength="12">
132
+ <button class="landing__btn" type="submit">Continue</button>
133
+ </form>
134
+ <p class="landing__note">Codes last ten minutes. If yours ran out, start the sign-in again in your terminal.</p>`,
135
+ });
136
+ }
137
+
138
+ module.exports = { renderSignInChooser, renderDeviceApprovalPage };
@@ -0,0 +1,146 @@
1
+ // A task says what it NEEDS; a builder has CAPABILITIES (task 1004950, ADR 0395).
2
+ //
3
+ // Most work needs nothing beyond the ordinary build loop. Some needs a person who
4
+ // holds something an agent on a builder machine does not:
5
+ //
6
+ // owner — a decision, account or permission only the project owner holds.
7
+ // server — hands on the live host: its DNS, its secrets, its database by hand.
8
+ //
9
+ // `tasks.needs` is its OWN set, deliberately not a value of `automation_tag`.
10
+ // That tag answers "can this run unattended overnight?" (it feeds the session-fit
11
+ // projection); needs answers "who can do this at all?". A task can be
12
+ // overnight-safe and need the server, or need the owner and be live-only — one
13
+ // field cannot carry both answers without making one of them lie.
14
+ //
15
+ // A builder's capabilities are resolved server-side, per request, uncached — like
16
+ // rank (ADR 0016). Nothing in a request body can assert one:
17
+ // owner from the government's owner seat (positionsFor), with the founding
18
+ // builder as the fallback — read exactly the way the criterion UAT
19
+ // sign-off reads it (criterion-uat-db.holdsOwnerSeat).
20
+ // server from builders.capability_grants, which only the owner sets. Default
21
+ // off. Holding the owner seat does NOT imply it: the owner's own agent
22
+ // runs on a builder machine too.
23
+ //
24
+ // What narrows and what does not: the lists that RECOMMEND work (the claimable
25
+ // feed, the session optimizer, the autonomous runner) leave out a task whose
26
+ // needs the viewer lacks. Every task stays readable everywhere else. The claim
27
+ // itself is refused only on `server` (an agent without the grant cannot do it);
28
+ // `owner` work stays claimable, because the owner claims it.
29
+
30
+ 'use strict';
31
+
32
+ const TASK_NEEDS = Object.freeze(['owner', 'server']);
33
+ // The capabilities an owner can grant by hand. `owner` is never granted — it is
34
+ // the seat itself.
35
+ const GRANTABLE_CAPABILITIES = Object.freeze(['server']);
36
+
37
+ // normalizeNeeds — validate a request value into a sorted, de-duplicated array.
38
+ // Accepts an array, a comma-separated string, null/'' (= none). Returns
39
+ // { ok: true, needs } or { ok: false, bad: [...] } naming the unknown values.
40
+ function normalizeNeeds(value) {
41
+ if (value === null || value === undefined || value === '') return { ok: true, needs: [] };
42
+ const list = Array.isArray(value) ? value : (typeof value === 'string' ? value.split(',') : null);
43
+ if (!list) return { ok: false, bad: [String(value)] };
44
+ const cleaned = list.map((v) => (typeof v === 'string' ? v.trim().toLowerCase() : v)).filter((v) => v !== '');
45
+ const bad = cleaned.filter((v) => !TASK_NEEDS.includes(v));
46
+ if (bad.length > 0) return { ok: false, bad: bad.map(String) };
47
+ return { ok: true, needs: TASK_NEEDS.filter((n) => cleaned.includes(n)) };
48
+ }
49
+
50
+ // missingNeeds — the needs of a task the holder of `capabilities` lacks. Pure.
51
+ // An unknown need on a row (a value written by a newer release) counts as
52
+ // missing: fail closed, never advertise work nobody here can do.
53
+ function missingNeeds(needs, capabilities) {
54
+ const have = new Set(Array.isArray(capabilities) ? capabilities : []);
55
+ return (Array.isArray(needs) ? needs : []).filter((n) => !have.has(n));
56
+ }
57
+
58
+ // claimRefusalNeeds — the subset of missing needs that REFUSES a claim. Only
59
+ // `server`: owner work is claimable (by the owner, who the feed shows it to).
60
+ function claimRefusalNeeds(needs, capabilities) {
61
+ return missingNeeds(needs, capabilities).filter((n) => n !== 'owner');
62
+ }
63
+
64
+ const SERVER_REFUSAL_MESSAGE =
65
+ 'This task needs server access, which you do not have yet, so pick another task or ask the project owner for it.';
66
+
67
+ // holdsOwnerSeatVia — the government port's answer. No port or a failed read
68
+ // answers false; the founding-builder fallback still applies.
69
+ async function holdsOwnerSeatVia(government, builderId) {
70
+ if (!government || typeof government.positionsFor !== 'function') return false;
71
+ try {
72
+ const p = await government.positionsFor(builderId);
73
+ return !!(p && Array.isArray(p.held) && p.held.some((h) => h && h.kind === 'owner'));
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ // resolveCapabilities — the capabilities `builderId` holds right now, as a sorted
80
+ // array. `deps` (tests): { pool, government, getFoundingBuilder }.
81
+ async function resolveCapabilities(builderId, deps = {}) {
82
+ if (builderId === null || builderId === undefined) return [];
83
+ const pool = deps.pool || require('./pool').pool;
84
+ const government = deps.government !== undefined
85
+ ? deps.government
86
+ : require('../module-seams').resolveOptional('government');
87
+ const getFounder = deps.getFoundingBuilder || require('./db-kernel').getFoundingBuilder;
88
+
89
+ const caps = new Set();
90
+ let isOwner = await holdsOwnerSeatVia(government, builderId);
91
+ if (!isOwner) {
92
+ const founder = await Promise.resolve().then(getFounder).catch(() => null);
93
+ isOwner = !!(founder && String(founder.id) === String(builderId));
94
+ }
95
+ if (isOwner) caps.add('owner');
96
+
97
+ const { rows } = await pool.query('SELECT capability_grants FROM builders WHERE id = $1', [builderId]);
98
+ const grants = rows[0] && Array.isArray(rows[0].capability_grants) ? rows[0].capability_grants : [];
99
+ for (const g of grants) if (GRANTABLE_CAPABILITIES.includes(g)) caps.add(g);
100
+
101
+ return TASK_NEEDS.filter((c) => caps.has(c));
102
+ }
103
+
104
+ // setCapabilityGrants — the owner grants or withdraws a builder's capabilities
105
+ // (PATCH /builders/:id/capabilities). The owner check lives HERE, at the write,
106
+ // not only in the route: `actorBuilderId` must hold the owner seat right now, or
107
+ // it throws NOT_PROJECT_OWNER — so no other caller can grant server access by
108
+ // reaching this function. It is deliberately NOT on the module doorway (see
109
+ // READ_SURFACE). A machine principal holds none: an agent's access is the
110
+ // person's it runs for. Returns the row, null when absent; throws
111
+ // TARGET_IS_SYSTEM_PRINCIPAL like setBuilderTester.
112
+ async function setCapabilityGrants({ actorBuilderId, builderId, grants }, deps = {}) {
113
+ const pool = deps.pool || require('./pool').pool;
114
+ const actorCaps = await resolveCapabilities(actorBuilderId, deps);
115
+ if (!actorCaps.includes('owner')) throw { code: 'NOT_PROJECT_OWNER' };
116
+ const { rows: found } = await pool.query('SELECT id, system_role FROM builders WHERE id = $1', [builderId]);
117
+ if (!found[0]) return null;
118
+ if (found[0].system_role) throw { code: 'TARGET_IS_SYSTEM_PRINCIPAL' };
119
+ const { rows } = await pool.query(
120
+ `UPDATE builders SET capability_grants = $2::text[] WHERE id = $1
121
+ RETURNING id, github_login, display_name, capability_grants`,
122
+ [builderId, GRANTABLE_CAPABILITIES.filter((g) => grants.includes(g))]
123
+ );
124
+ return rows[0] || null;
125
+ }
126
+
127
+ // feedNeedsClause — the SQL predicate a recommend list applies: the row's needs
128
+ // are a subset of the viewer's capabilities. `param` is the placeholder holding
129
+ // the capabilities text[]. A NULL needs reads as none.
130
+ function feedNeedsClause(alias, param) {
131
+ return `COALESCE(${alias}.needs, '{}'::text[]) <@ ${param}::text[]`;
132
+ }
133
+
134
+ // READ_SURFACE — what the module doorway publishes (src/module-api.js taskNeeds):
135
+ // the vocabulary, the reads and the SQL predicate. Never the grant write.
136
+ const READ_SURFACE = Object.freeze({
137
+ TASK_NEEDS, GRANTABLE_CAPABILITIES, SERVER_REFUSAL_MESSAGE,
138
+ normalizeNeeds, claimRefusalNeeds, resolveCapabilities, feedNeedsClause,
139
+ });
140
+
141
+ module.exports = {
142
+ READ_SURFACE,
143
+ resolveCapabilities,
144
+ setCapabilityGrants,
145
+ feedNeedsClause,
146
+ };
@@ -62,4 +62,21 @@ function buildBongosDownloads(cfg) {
62
62
  return { redirectFor, latestJson, versionJson, version };
63
63
  }
64
64
 
65
- module.exports = { buildBongosDownloads };
65
+ // installScriptReader — the one-line installers (/downloads/bongos/install.sh and
66
+ // install.ps1) with the API base substituted in, read from disk ONCE per name
67
+ // (task 1004924). They were re-read and re-substituted on every request, a sync
68
+ // read blocking the event loop. The files only change on a deploy, which restarts
69
+ // the process, so a per-process memo is exact. A missing file (null) is memoised
70
+ // too. `readFile(name)` returns the raw text or throws; it is the test seam.
71
+ function installScriptReader({ readFile, origin }) {
72
+ const memo = new Map();
73
+ return function read(name) {
74
+ if (memo.has(name)) return memo.get(name);
75
+ let out = null;
76
+ try { out = String(readFile(name)).split('__GDS_API_BASE__').join(origin); } catch { out = null; }
77
+ memo.set(name, out);
78
+ return out;
79
+ };
80
+ }
81
+
82
+ module.exports = { buildBongosDownloads, installScriptReader };
@@ -178,6 +178,32 @@ function readConfigFileSync(name, { encoding = 'utf8', b = safeBrand() } = {}) {
178
178
  return null;
179
179
  }
180
180
 
181
+ // readConfigFileCached — readConfigFileSync for the REQUEST path (task 1004924).
182
+ // authConfigured()/discordConfigured() and the secret box run per request, and on
183
+ // a box with no env creds each call was up to four sync ENOENT reads blocking the
184
+ // event loop. This answers from memory for `ttlMs` (default 60 s, so a creds file
185
+ // an operator drops in still takes effect without a restart), caching a miss
186
+ // (null) as well as a hit. The key is the resolved read-path list, so a changed
187
+ // home or config dir is a fresh read, never a stale answer from another dir.
188
+ // A non-ENOENT error is not cached: it propagates, exactly as the uncached read.
189
+ const CONFIG_READ_TTL_MS = 60_000;
190
+ const _configReadCache = new Map(); // key -> { value, at }
191
+ function readConfigFileCached(name, { ttlMs = CONFIG_READ_TTL_MS, now = Date.now } = {}) {
192
+ const b = safeBrand();
193
+ const key = configReadPaths(name, b).join('\n');
194
+ const hit = _configReadCache.get(key);
195
+ const t = now();
196
+ if (hit && t - hit.at < ttlMs) return hit.value;
197
+ const value = readConfigFileSync(name, { b });
198
+ _configReadCache.set(key, { value, at: t });
199
+ return value;
200
+ }
201
+ // Drop the cached reads (all of them). For a writer that just changed a config
202
+ // file in this process (provisionMasterKey) and for tests.
203
+ function clearConfigReadCache() {
204
+ _configReadCache.clear();
205
+ }
206
+
181
207
  // Resolve an env var by SUFFIX across the configured prefix + legacy prefixes.
182
208
  // resolveEnv('API_BASE') tries <PREFIX>_API_BASE (BONGOS_ on vanilla, OTB_ on
183
209
  // the OTB instance), then CLOUDBONGOS_/OTB_/GDS_/PMS_API_BASE. Returns undefined
@@ -349,6 +375,8 @@ module.exports = {
349
375
  configPath,
350
376
  configReadPaths,
351
377
  readConfigFileSync,
378
+ readConfigFileCached,
379
+ clearConfigReadCache,
352
380
  SESSION_FILENAMES,
353
381
  readSessionConfigSync,
354
382
  resolveEnv,
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.23.21'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.1'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -197,6 +197,12 @@ module.exports = {
197
197
  // which a module cannot enforce without being able to ask who that is. It is
198
198
  // a READ of an existing row; it grants nothing by itself.
199
199
  getFoundingBuilder: (...a) => require('./bongos/db-kernel').getFoundingBuilder(...a),
200
+ // taskNeeds — what a task needs (owner, server) and what a builder can do
201
+ // (task 1004950, ADR 0395). Core because the session optimizer applies the
202
+ // same feed rule and cannot require a module; capabilities are read fresh
203
+ // per call, like rank. The READ surface only — granting server access is
204
+ // core's owner-seat route, never a module's call.
205
+ get taskNeeds() { const { READ_SURFACE } = require('./bongos/task-needs'); return READ_SURFACE; },
200
206
  insertAuditLog: (...a) => require('./bongos/db-kernel').insertAuditLog(...a),
201
207
  get LIVE_RANK_LADDER() { return require('./bongos/db-kernel').LIVE_RANK_LADDER; },
202
208
  // The ladder as the canonical 1-based rank → tier map (BV1.R108) — modules
@@ -244,6 +250,13 @@ module.exports = {
244
250
  // therefore reaches via the exposed `pool` and its own client instead.
245
251
  get grantedScopeCoversRepos() { return require('./bongos/auth-config').grantedScopeCoversRepos; },
246
252
 
253
+ // --- prelaunchActive() — is this instance in pre-launch (stealth: the shared
254
+ // password gate is on)? Exposed for the provisioning module's adopt preflight
255
+ // (task 1004998): while it is on, a project may not adopt a PUBLIC repo. Read
256
+ // here rather than from the env vars so the gate and its callers can never
257
+ // disagree about which variables switch it on.
258
+ get prelaunchActive() { return require('./bongos/prelaunch-switch').enabled; },
259
+
247
260
  // --- KERNEL trust classifiers (ADR 0091 §1, KERNEL_FILES) — the deterministic
248
261
  // route→rank + protected-path matchers. They are KERNEL (the trust
249
262
  // boundary, never "called" by a domain), exposed so a module that runs the