@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
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ // modules/government/access-routes.js — the handlers behind the three ACCESS HOLDERS
4
+ // routes (task 1005000). The routes themselves — path, permission gate and pin — are
5
+ // declared in routes/government.js, where route-rank-check reads them; only the bodies
6
+ // live here, so the router file stays under the size ratchet.
7
+ //
8
+ // GET /government/access (board.vote.cast) — read
9
+ // POST /government/access/:accessKey/holders (government.manage) — record
10
+ // DELETE /government/access/:accessKey/holders/:builderId (government.manage) — remove
11
+ //
12
+ // ⚠ A record of who to ask, never authority (access.js). Not act-alone routes: they
13
+ // record a fact about the world outside the project; they decide nothing a sitting
14
+ // votes on.
15
+
16
+ const access = require('./access');
17
+
18
+ // deps: { db, log, parseId, validateOrRespond } — the router's own, passed in so a
19
+ // test that stubs the db by property reaches these handlers too.
20
+ function accessHandlers({ db, log, parseId, validateOrRespond }) {
21
+ const keyOf = (req, res) => {
22
+ const key = String(req.params.accessKey || '');
23
+ if (access.isAccessKey(key)) return key;
24
+ res.fail('unknown_access', { status: 400, message: `Name one of: ${access.KEYS.join(', ')}.` });
25
+ return null;
26
+ };
27
+
28
+ async function read(req, res) {
29
+ try {
30
+ res.json({ access: access.accessView(await db.listAccessHolders()) });
31
+ } catch (err) {
32
+ log.error('[gds] GET /government/access', err);
33
+ res.fail('access_read_failed', { status: 500, message: 'internal error' });
34
+ }
35
+ }
36
+
37
+ async function grant(req, res) {
38
+ if (validateOrRespond(req, res, { builder_id: { required: true, type: 'integer', min: 1 } })) return;
39
+ const key = keyOf(req, res);
40
+ if (key === null) return;
41
+ const builderId = req.body.builder_id;
42
+ try {
43
+ const who = await db.getActiveBuilder(builderId);
44
+ if (!who) return res.fail('builder_not_found', { status: 404, message: 'No active builder with that id.' });
45
+ const added = await db.grantAccess(key, builderId, req.builder.id);
46
+ res.status(added ? 201 : 200).json({ ok: true, access_key: key, builder_id: String(builderId), added });
47
+ } catch (err) {
48
+ log.error('[gds] POST /government/access/:accessKey/holders', err);
49
+ res.fail('access_grant_failed', { status: 500, message: 'internal error' });
50
+ }
51
+ }
52
+
53
+ async function revoke(req, res) {
54
+ const key = keyOf(req, res);
55
+ if (key === null) return;
56
+ const builderId = parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
57
+ if (builderId === null) return;
58
+ try {
59
+ const removed = await db.revokeAccess(key, builderId);
60
+ res.json({ ok: true, access_key: key, builder_id: String(builderId), removed });
61
+ } catch (err) {
62
+ log.error('[gds] DELETE /government/access/:accessKey/holders/:builderId', err);
63
+ res.fail('access_revoke_failed', { status: 500, message: 'internal error' });
64
+ }
65
+ }
66
+
67
+ return { read, grant, revoke };
68
+ }
69
+
70
+ module.exports = { accessHandlers };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ // modules/government/access.js — ACCESS HOLDERS: who holds the keys that live OUTSIDE
4
+ // Bongos, so a blocker that needs one reaches the person who has it (task 1005000,
5
+ // goal 1000111).
6
+ //
7
+ // WHY. Some blockers can only be cleared by whoever holds access no rank or Lead
8
+ // models: the GitHub repository's admin (token scopes, branch protection, the App's
9
+ // permissions), the server, the domain's DNS, the hosting account, package publishing,
10
+ // payments. An Archon in Bongos may not hold the GitHub admin, and a Lead never does.
11
+ // It lived only in prose (docs/credentials.md "account logins the owner holds") and in
12
+ // blocker text — blocker 1000199, the publish token missing GitHub's `workflow` scope,
13
+ // could only be filed to "the owner". The builder's words: repo owner is a category of
14
+ // blockers that go beyond normality.
15
+ //
16
+ // WHAT IT IS. A fixed vocabulary of outside-access keys (ACCESS, extendable here and in
17
+ // the migration's CHECK together), a record of which builders hold each
18
+ // (government_access_holders, written by the owner only), and a seat, `access:<key>`,
19
+ // that an ask can wait on. routing.js resolves that seat to the recorded holders, and
20
+ // to the owner when nobody is recorded — the same fall every other seat takes.
21
+ //
22
+ // ⚠ A RECORD OF WHO TO ASK, NEVER AN AUTHORITY GRANT. Holding github-admin here unlocks
23
+ // no Bongos route and no rank: this module is not read by requireRank, the permission
24
+ // resolver, or any gate, and an access key is not a permission atom. No secret is
25
+ // stored either — the row says WHO holds an access, never a credential or a token.
26
+ //
27
+ // PURE: no db.
28
+
29
+ const ACCESS = Object.freeze([
30
+ // name: how the seat reads after "waits on the"; needs: what a blocker on it needs.
31
+ Object.freeze({ key: 'github-admin', name: 'repo owner', needs: 'repo-owner access', what: 'Admin on the GitHub repository: token scopes, branch protection, the GitHub App.' }),
32
+ Object.freeze({ key: 'server-ssh', name: 'server access', needs: 'server access', what: 'A login on the server the project runs on.' }),
33
+ Object.freeze({ key: 'dns', name: 'domain settings', needs: 'access to the domain settings', what: 'The DNS account for the project\'s domain.' }),
34
+ Object.freeze({ key: 'hosting', name: 'hosting account', needs: 'access to the hosting account', what: 'The account that pays for and runs the project\'s servers.' }),
35
+ Object.freeze({ key: 'npm-publish', name: 'package publishing', needs: 'package publishing rights', what: 'Publishing rights for the project\'s packages.' }),
36
+ Object.freeze({ key: 'payments', name: 'payments', needs: 'access to payments', what: 'The account money moves through.' }),
37
+ ]);
38
+ const KEYS = Object.freeze(ACCESS.map((a) => a.key));
39
+ const isAccessKey = (k) => KEYS.includes(String(k || ''));
40
+ const byKey = (k) => ACCESS.find((a) => a.key === k) || null;
41
+
42
+ // The seat grammar's access half, for asks.js SEAT and the migration's CHECK.
43
+ const SEAT_PART = `access:(${KEYS.join('|')})`;
44
+
45
+ // "the repo owner" — how the seat reads after "waits on".
46
+ function accessLabel(key) {
47
+ const a = byKey(key);
48
+ return a ? `the ${a.name}` : 'the owner';
49
+ }
50
+
51
+ // "needs repo-owner access" — how a blocker on this seat reads under its holder.
52
+ function needsWords(key) {
53
+ const a = byKey(key);
54
+ return a ? `needs ${a.needs}` : 'needs outside access';
55
+ }
56
+
57
+ // The holders read as the seat resolver wants them: { key: [{ builder_id, github_login, display_name }] }.
58
+ function holdersByKey(rows) {
59
+ const out = {};
60
+ for (const k of KEYS) out[k] = [];
61
+ for (const r of rows || []) {
62
+ if (r && isAccessKey(r.access_key) && r.builder_id != null) out[r.access_key].push(r);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ // The read GET /government/access answers: every access, its words, who holds it.
68
+ function accessView(rows) {
69
+ const held = holdersByKey(rows);
70
+ return ACCESS.map((a) => ({
71
+ key: a.key,
72
+ name: a.name,
73
+ // the one display form, for the list and the picker alike
74
+ title: a.name.charAt(0).toUpperCase() + a.name.slice(1),
75
+ what: a.what,
76
+ seat: `access:${a.key}`,
77
+ holders: held[a.key].map((r) => ({
78
+ id: String(r.builder_id),
79
+ login: r.github_login || null,
80
+ name: r.display_name || r.github_login || null,
81
+ since: r.granted_at || null,
82
+ })),
83
+ // nobody recorded: the seat falls to the owner, as every unheld seat does
84
+ falls_to_owner: held[a.key].length === 0,
85
+ }));
86
+ }
87
+
88
+ module.exports = { ACCESS, KEYS, SEAT_PART, isAccessKey, accessLabel, needsWords, holdersByKey, accessView };
@@ -44,10 +44,12 @@ const MARKUP = /\*\*|`|^#|\[[^\]]*\]\(|https?:\/\/|<\/?[a-z]/im;
44
44
  // Control characters other than tab and line breaks (line breaks have their own rule).
45
45
  // eslint-disable-next-line no-control-regex
46
46
  const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
47
+ const access = require('./access');
47
48
  const BUILDINGS = Object.freeze(['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury']);
48
49
  // The seats an ask can wait on (gov-living.js seatResolver's grammar). An
49
50
  // unseated building falls to the owner when it is routed (owner ruling).
50
- const SEAT = new RegExp(`^(owner|(lead|board):(${BUILDINGS.join('|')})|committee:(${BUILDINGS.join('|')})\\.[a-z][a-z0-9_]{0,31})$`);
51
+ // `access:<key>` (task 1005000): whoever holds that outside access (access.js).
52
+ const SEAT = new RegExp(`^(owner|(lead|board):(${BUILDINGS.join('|')})|committee:(${BUILDINGS.join('|')})\\.[a-z][a-z0-9_]{0,31}|${access.SEAT_PART})$`);
51
53
  // An answer's key: what a tap sends. Its act, optional: the matter's own act it
52
54
  // takes ('go ahead', 'record', 'force:tested_by_hand'); the deck maps it onto
53
55
  // the desk row's real route and refuses one the row does not offer.
@@ -150,7 +152,7 @@ function checkAsk(raw) {
150
152
  // ---- who it waits on: a seat the direction desk can route ----
151
153
  const seat = str(c.waits_on).trim();
152
154
  if (!seat) err('waits_on', 'missing', 'Say who this waits on.');
153
- else if (!SEAT.test(seat)) err('waits_on', 'unknown_seat', `"${seat.slice(0, 40)}" is not a seat: say owner, lead:<building>, committee:<building>.<name> or board:<building>.`);
155
+ else if (!SEAT.test(seat)) err('waits_on', 'unknown_seat', `"${seat.slice(0, 40)}" is not a seat: say owner, lead:<building>, committee:<building>.<name>, board:<building> or access:<key>.`);
154
156
 
155
157
  // ---- what happens if nobody answers ----
156
158
  lineRule('if_nobody_answers', c.if_nobody_answers, 'Say what happens if nobody answers.');
@@ -206,6 +208,7 @@ const ASK_REQUIRED_MESSAGE = 'An AI session files a blocker with an ask: a quest
206
208
  function seatWords(seat) {
207
209
  const s = str(seat);
208
210
  if (s === 'owner') return 'the owner';
211
+ if (/^access:/.test(s)) return access.accessLabel(s.slice(7));
209
212
  const m = /^(lead|board|committee):([a-z]+)(?:\.([a-z0-9_]+))?$/.exec(s);
210
213
  if (!m) return s;
211
214
  const building = m[2].charAt(0).toUpperCase() + m[2].slice(1);
@@ -238,6 +238,12 @@ const PERMISSIONS = [
238
238
  // Review the store's proposed sub-categories (task 1004355, ADR 0342 D3): approve,
239
239
  // rename then approve, or merge into an existing one (a merge moves its modules).
240
240
  { key: 'module.subcategory.review', system: false, floor: 'metic', guards: 'GET /store/subcategories/queue, POST /store/subcategories/:id/approve, POST /store/subcategories/:id/merge' },
241
+ // The project's authorised person for money (task 1004913, ADR 0385 D4): sign,
242
+ // narrow or revoke a standing payout rule, hold a noticed run, approve one payout,
243
+ // sign one equity grant or contract, and read the approval log. Archon by default;
244
+ // not a system key, so a project may hand it to a treasurer rank of its own. The
245
+ // routes additionally refuse anything but a person's own web session.
246
+ { key: 'payout.approve', system: false, floor: 'archon', guards: 'GET /payouts/approvals, GET /payouts/rules, POST /payouts/rules[/:id/{narrow,revoke,runs,runs/:runKey/hold}], POST /payouts/{approve,grants,contracts}' },
241
247
  // Take a module out of the store (task 1003797, ADR 0338 D2). Stops new installs,
242
248
  // versions and downloads; never reaches an instance that already holds it.
243
249
  { key: 'module.delist', system: false, floor: 'metic', guards: 'POST /store/modules/:key/delist, POST /store/modules/:key/versions/:version/release (a version the promotion gate held on tester — task 1003804), POST /store/modules/:key/versions/:version/withdraw (take a version back — task 1004895)' },
@@ -429,9 +429,26 @@ async function openBoardItem({ subjectType, subjectId, openedBy = null, constitu
429
429
  RETURNING *`,
430
430
  [subjectType, subjectId, openedBy, windowMinutes, JSON.stringify(constitution || {})],
431
431
  );
432
+ // Tell the expiry poller (task 1004960): it sleeps until the soonest deadline
433
+ // it knows of, so a window shorter than its idle wait must wake it sooner.
434
+ // closes_at is now() + the window, so the window IS the time until close.
435
+ // A listener the poller registers (not a require: the poller requires this
436
+ // file). Never let a hint fail an open.
437
+ if (rows[0] && windowMinutes != null) {
438
+ for (const fn of deadlineListeners) {
439
+ try { fn(Number(windowMinutes) * 60_000); } catch (_) { /* best-effort */ }
440
+ }
441
+ }
432
442
  return rows[0] || null;
433
443
  }
434
444
 
445
+ // Who wants to hear that a timed window opened (the expiry poller's start()).
446
+ const deadlineListeners = new Set();
447
+ function onBoardDeadline(fn) {
448
+ deadlineListeners.add(fn);
449
+ return () => deadlineListeners.delete(fn);
450
+ }
451
+
435
452
  async function getOpenBoardItem(subjectType, subjectId) {
436
453
  const { rows } = await pool.query(
437
454
  `SELECT * FROM government_board_items
@@ -871,6 +888,41 @@ async function listActiveArchons() {
871
888
  return rows;
872
889
  }
873
890
 
891
+ // ACCESS HOLDERS (task 1005000, government_036): who holds each outside access.
892
+ // Active builders only — someone who left the project is nobody to ask. A record
893
+ // of who to ask, never authority: no gate reads these rows (access.js).
894
+ async function listAccessHolders() {
895
+ const { rows } = await pool.query(
896
+ `SELECT h.access_key, h.builder_id, h.granted_at, b.github_login, b.display_name
897
+ FROM government_access_holders h
898
+ JOIN builders b ON b.id = h.builder_id
899
+ WHERE b.status = 'active' AND b.system_role IS NULL
900
+ ORDER BY h.access_key, h.granted_at, h.builder_id`,
901
+ );
902
+ return rows;
903
+ }
904
+
905
+ // Record that a builder holds an access. Idempotent: an existing row is kept as it
906
+ // was (its original granted_at/by). → true when a row was added.
907
+ async function grantAccess(accessKey, builderId, grantedBy) {
908
+ const { rowCount } = await pool.query(
909
+ `INSERT INTO government_access_holders (access_key, builder_id, granted_by)
910
+ VALUES ($1, $2, $3)
911
+ ON CONFLICT (access_key, builder_id) DO NOTHING`,
912
+ [accessKey, builderId, grantedBy],
913
+ );
914
+ return rowCount > 0;
915
+ }
916
+
917
+ // → true when a row was removed.
918
+ async function revokeAccess(accessKey, builderId) {
919
+ const { rowCount } = await pool.query(
920
+ 'DELETE FROM government_access_holders WHERE access_key = $1 AND builder_id = $2',
921
+ [accessKey, builderId],
922
+ );
923
+ return rowCount > 0;
924
+ }
925
+
874
926
  // One builder of this project, or null: who a Lead appointment names. Only an
875
927
  // active person (no machine identity) can be appointed.
876
928
  async function getActiveBuilder(builderId) {
@@ -1114,6 +1166,21 @@ async function getExpiredOpenBoardItems(limit = 100) {
1114
1166
  return rows;
1115
1167
  }
1116
1168
 
1169
+ // The expiry poller's alarm clock (task 1004960): how many milliseconds until
1170
+ // the soonest OPEN deadline, measured on the DATABASE clock (so a skewed
1171
+ // process clock can never make the poller wake early or late), or null when no
1172
+ // open item has a clock. ONE read, served by the same partial index as the
1173
+ // sweep — the poller sleeps until this deadline instead of asking every minute.
1174
+ async function msUntilSoonestOpenDeadline() {
1175
+ const { rows } = await pool.query(
1176
+ `SELECT EXTRACT(EPOCH FROM (MIN(closes_at) - now())) * 1000 AS ms
1177
+ FROM government_board_items
1178
+ WHERE closed_at IS NULL AND closes_at IS NOT NULL`,
1179
+ );
1180
+ const ms = rows[0] ? rows[0].ms : null;
1181
+ return ms == null ? null : Number(ms);
1182
+ }
1183
+
1117
1184
  // The summons read (task 1003271): every OPEN item this builder could still
1118
1185
  // vote on, with the two facts that decide it — who authored the subject, and
1119
1186
  // whether this builder has already cast. Membership is NOT decided here: it
@@ -1260,6 +1327,8 @@ module.exports = {
1260
1327
  withdrawBoardItem,
1261
1328
  getIdeaCompleteness,
1262
1329
  getExpiredOpenBoardItems,
1330
+ msUntilSoonestOpenDeadline,
1331
+ onBoardDeadline,
1263
1332
  getLatestReturnedItemWithObjection,
1264
1333
  getLatestPassedItem,
1265
1334
  listPassedSubjectIds,
@@ -1272,6 +1341,9 @@ module.exports = {
1272
1341
  applyLaws,
1273
1342
  listLeads,
1274
1343
  listActiveArchons,
1344
+ listAccessHolders,
1345
+ grantAccess,
1346
+ revokeAccess,
1275
1347
  getActiveBuilder,
1276
1348
  applyLeadAct,
1277
1349
  listBuildouts,
@@ -85,6 +85,8 @@ const docketFloors = require('./docket-floors');
85
85
  const governmentConfig = require('./config');
86
86
  const asksDb = require('./asks-db');
87
87
  const routing = require('./routing');
88
+ const waiting = require('./waiting');
89
+ const access = require('./access');
88
90
  const { ownerOf } = require('./leads');
89
91
 
90
92
  const log = api.logger('government');
@@ -839,14 +841,57 @@ async function docketFor(builder) {
839
841
  // nothing — the docket then says `routed: false` rather than guess whose it is.
840
842
  async function loadSeats(government) {
841
843
  try {
842
- const [leads, archons] = await Promise.all([db.listLeads(), db.listActiveArchons()]);
843
- return { leads, owner: ownerOf(archons), holders: (government && government.holders) || {} };
844
+ // task 1005000: who holds each outside access. Its own read fails OPEN to nobody
845
+ // recorded (every access seat then falls to the owner), so a missing table on an
846
+ // instance that has not migrated yet never stops the City routing. Started inside
847
+ // a .then so a db layer without the function (an older stub, the walk's store)
848
+ // fails open too rather than throwing before the catch is attached.
849
+ const accessRows = Promise.resolve().then(() => db.listAccessHolders()).catch((err) => {
850
+ log.warn({ err: err && err.message }, 'governor docket: access holders unreadable, access seats fall to the owner');
851
+ return [];
852
+ });
853
+ const [leads, archons, held] = await Promise.all([db.listLeads(), db.listActiveArchons(), accessRows]);
854
+ return { leads, owner: ownerOf(archons), holders: (government && government.holders) || {}, access: access.holdersByKey(held) };
844
855
  } catch (err) {
845
856
  log.warn({ err: err && err.message }, 'governor docket: seats unreadable, nothing routed');
846
857
  return null;
847
858
  }
848
859
  }
849
860
 
861
+ // OPEN BLOCKERS UNDER THE PEOPLE THEY WAIT ON (task 1004999). The blockers source is
862
+ // read exactly as the docket reads it (the ideas module's contribution, under its own
863
+ // `anyBuilder` gate — the list is public), each one carries its stored ask, and the
864
+ // seats are read once, uncached (ADR 0016). waiting.js groups them with routing.js's
865
+ // rule, so this answers "whose is it" exactly as the City does. A board seat's holders
866
+ // are read only when some blocker names one. → { available, people, nobody, yours }
867
+ // `available: false` names why nothing could be grouped — never an empty list
868
+ // standing in for "nobody is waiting".
869
+ async function blockersByPerson(builder, { contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS } = {}) {
870
+ const c = contributions.sources.get('blockers');
871
+ if (!c) return { available: false, reason: 'off', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
872
+ const permissions = await resolver.resolveBuilderPermissions(builder.id);
873
+ const held = new Set(permissions || []);
874
+ const ctx = Object.freeze({ builderId: String(builder.id), rank: builder.rank || null, has: (p) => held.has(p) });
875
+ if (!gateAllows(c.gate, ctx)) return { available: false, reason: 'not_yours', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
876
+ let matters;
877
+ try {
878
+ const raw = await withTimeout(Promise.resolve(c.read(ctx, { now })), timeoutMs);
879
+ matters = (Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source: 'blockers', building: c.building || 'planning' })).filter(Boolean);
880
+ } catch (err) {
881
+ log.warn({ err: err && err.message }, 'blockers by person: blockers unreadable');
882
+ return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
883
+ }
884
+ await attachAsks(matters, asksDb.asksFor, timeoutMs);
885
+ const boardSeat = matters.some((m) => /^board:/.test(routing.seatOf(m)));
886
+ const government = boardSeat
887
+ ? await loadGovernment({ decisions: await board.decisionRulesView().catch(() => null) })
888
+ : { holders: {} };
889
+ const seats = await loadSeats(government);
890
+ if (!seats) return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
891
+ const grouped = waiting.groupByPerson(matters, seats);
892
+ return { available: true, ...grouped, yours: waiting.forViewer(grouped, builder.id) };
893
+ }
894
+
850
895
  // One matter with its stored ask and the seats, for passing it on to `toSeat`
851
896
  // (task 1004705): → { matter, seats } | { reason } as findMatter answers. A
852
897
  // board's holders are read only when a board seat is involved.
@@ -927,5 +972,6 @@ module.exports = {
927
972
  findMatter,
928
973
  findMatterFor,
929
974
  loadSeats,
975
+ blockersByPerson,
930
976
  matterWithSeats,
931
977
  };
@@ -0,0 +1,29 @@
1
+ -- government_035_payout_approve.sql — grant the new `payout.approve` atom to
2
+ -- Archon (task 1004913, goal 1000092 N9; ADR 0385 D4).
3
+ --
4
+ -- WHAT IT IS. The project's authorised person for money: signs a standing payout
5
+ -- rule, narrows or revokes it, holds a run in its pause window, approves a single
6
+ -- payout, signs an equity grant or a contract, and reads the approval log
7
+ -- (core_283_payout_approvals). The routes also demand the person's own web
8
+ -- session, so holding the atom on a CLI token is not enough to sign.
9
+ --
10
+ -- WHY THIS FILE EXISTS. RANK_SEED is derived from the catalog's floors, and the
11
+ -- drift guard (tests/government_seed.mjs) compares it against the union of the
12
+ -- run-once seed plus every later grant migration, per rank. An archon-floor key
13
+ -- is held by archon only.
14
+ --
15
+ -- Additive only: one new permission key granted to one rank; nothing is revoked.
16
+ -- Idempotent (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
17
+ --
18
+ -- The `SELECT '<rank>', unnest(ARRAY[…])` shape is load-bearing: the drift guard
19
+ -- scans for it literally.
20
+
21
+ BEGIN;
22
+
23
+ INSERT INTO government_rank_permissions (rank_key, permission_key)
24
+ SELECT 'archon', unnest(ARRAY[
25
+ 'payout.approve'
26
+ ])
27
+ ON CONFLICT (rank_key, permission_key) DO NOTHING;
28
+
29
+ COMMIT;
@@ -0,0 +1,33 @@
1
+ -- government_036_access_holders.sql — who holds the keys OUTSIDE Bongos, and an ask
2
+ -- that waits on them (task 1005000, goal 1000111).
3
+ --
4
+ -- WHAT THIS ADDS
5
+ --
6
+ -- government_access_holders — one row per (outside access, builder): the GitHub
7
+ -- repository's admin, the server, the domain's DNS, the hosting account, package
8
+ -- publishing, payments. The vocabulary is modules/government/access.js `ACCESS`,
9
+ -- held equal to the CHECK below by tests/access_holders.mjs. Written by the owner
10
+ -- only (the routes gate government.manage); who granted it and when ride the row.
11
+ --
12
+ -- It is a RECORD OF WHO TO ASK, never an authority grant: no permission atom, rank
13
+ -- check or gate reads this table, and it stores no credential — only who holds one.
14
+ --
15
+ -- government_asks.waits_on gains the seat `access:<key>`, so an ask can wait on
16
+ -- whoever holds an outside access (routing.js; nobody recorded → the owner).
17
+
18
+ BEGIN;
19
+
20
+ CREATE TABLE IF NOT EXISTS government_access_holders (
21
+ access_key text NOT NULL
22
+ CHECK (access_key IN ('github-admin', 'server-ssh', 'dns', 'hosting', 'npm-publish', 'payments')),
23
+ builder_id bigint NOT NULL REFERENCES builders(id) ON DELETE CASCADE,
24
+ granted_by bigint REFERENCES builders(id) ON DELETE SET NULL,
25
+ granted_at timestamptz NOT NULL DEFAULT now(),
26
+ PRIMARY KEY (access_key, builder_id)
27
+ );
28
+
29
+ ALTER TABLE government_asks DROP CONSTRAINT IF EXISTS government_asks_waits_on_check;
30
+ ALTER TABLE government_asks ADD CONSTRAINT government_asks_waits_on_check
31
+ CHECK (waits_on ~ '^(owner|(lead|board):(charter|rules|disputes|people|planning|automation|review|release|treasury)|committee:(charter|rules|disputes|people|planning|automation|review|release|treasury)\.[a-z][a-z0-9_]{0,31}|access:(github-admin|server-ssh|dns|hosting|npm-publish|payments))$');
32
+
33
+ COMMIT;
@@ -19,17 +19,32 @@
19
19
  // minute-cadence poller logging every tick would bury the journal (the
20
20
  // reconciler's own warn-once lesson).
21
21
  //
22
- // CADENCE: windows are minutes-granular (the recommended sitting is 90
23
- // minutes), so a one-minute sweep bounds lateness at ~1 minute — tight enough
24
- // that "closes on time" is honest, cheap enough that the query (one indexed
25
- // read; government_board_items_open_by_deadline exists for exactly this, per
26
- // government_003's own comment) is negligible. Concurrent windows are normal
22
+ // CADENCE (task 1004960): the poller SLEEPS UNTIL THE SOONEST DEADLINE instead
23
+ // of asking every minute. After each sweep it makes one MIN(closes_at) read
24
+ // (msUntilSoonestOpenDeadline, the same partial index) and arms its next run
25
+ // for just after that deadline — capped at MAX_WAIT_MS (5 minutes) so an item
26
+ // opened by some other process is still caught. A window opened in THIS process
27
+ // wakes it sooner through noteDeadline (db.openBoardItem calls it), so a
28
+ // one-minute window still closes on time. An idle instance with no open clock
29
+ // therefore reads twice per 5 minutes, not once a minute; a vote still closes
30
+ // within ~1 second of its deadline. While something is OVERDUE after a sweep
31
+ // (its close threw and rolled back) the retry waits RETRY_MS (the old one-minute
32
+ // cadence), never a hot loop. Runs never overlap: a tick that fires while the
33
+ // previous sweep is still in flight is skipped. Concurrent windows are normal
27
34
  // (owner decision 10): the sweep closes every due item, oldest deadline first.
28
35
 
29
36
  const board = require('../board');
30
37
  const db = require('../db');
31
38
 
32
- const INTERVAL_MS = Number(process.env.BOARD_EXPIRY_SWEEP_MS) || 60_000;
39
+ // The longest the poller ever sleeps (BOARD_EXPIRY_SWEEP_MS overrides it). It
40
+ // only bounds an item opened by ANOTHER process; this process's opens wake it.
41
+ const MAX_WAIT_MS = Number(process.env.BOARD_EXPIRY_SWEEP_MS) || 5 * 60_000;
42
+ // Kept as an alias: INTERVAL_MS was the fixed cadence before task 1004960.
43
+ const INTERVAL_MS = MAX_WAIT_MS;
44
+ // While an item is overdue after a sweep (its close failed and rolled back).
45
+ const RETRY_MS = 60_000;
46
+ // Wake just AFTER a deadline, since the sweep reads closes_at < now().
47
+ const GRACE_MS = 1_000;
33
48
  // A first sweep shortly after boot, so windows that expired while the process
34
49
  // was down (a deploy restart mid-sitting) close promptly rather than one full
35
50
  // interval late.
@@ -38,8 +53,21 @@ const FIRST_SWEEP_DELAY_MS = Number(process.env.BOARD_EXPIRY_FIRST_MS) || 15_000
38
53
  // handful of open windows (the board's subjects are rare by construction).
39
54
  const SWEEP_LIMIT = 100;
40
55
 
41
- let intervalTimer = null;
42
- let firstTimer = null;
56
+ let timer = null;
57
+ let dueAt = null; // when the armed timer fires (process clock, ms)
58
+ let started = false;
59
+ let running = false;
60
+ let hintAt = null; // soonest deadline (process clock) noted mid-sweep
61
+ let startDeps = {};
62
+ let unlisten = null;
63
+
64
+ // When to run next, given the ms until the soonest open deadline (null: none).
65
+ function nextDelayMs(msUntilSoonest) {
66
+ if (msUntilSoonest == null || !Number.isFinite(Number(msUntilSoonest))) return MAX_WAIT_MS;
67
+ const ms = Number(msUntilSoonest);
68
+ if (ms <= 0) return RETRY_MS; // still overdue after a sweep: retry, never spin
69
+ return Math.min(MAX_WAIT_MS, ms + GRACE_MS);
70
+ }
43
71
 
44
72
  // One sweep. Injectable deps keep it DB-free testable: listFn yields the due
45
73
  // items, closeFn closes one, log receives the disposition lines.
@@ -82,20 +110,80 @@ function start(deps = {}) {
82
110
  console.log('[board-expiry] disabled (BOARD_EXPIRY_DISABLED=1)');
83
111
  return;
84
112
  }
85
- if (intervalTimer) return; // already started
86
- const tick = () => {
87
- runSweep(deps).catch((e) => console.error(`[board-expiry] sweep crashed: ${e && e.message}`));
88
- };
89
- firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
90
- if (firstTimer.unref) firstTimer.unref();
91
- intervalTimer = setInterval(tick, INTERVAL_MS);
92
- if (intervalTimer.unref) intervalTimer.unref(); // never hold the process open on a restart
93
- console.log(`[board-expiry] started (every ${Math.round(INTERVAL_MS / 1000)}s; first sweep in ${Math.round(FIRST_SWEEP_DELAY_MS / 1000)}s)`);
113
+ if (started) return; // already started
114
+ started = true;
115
+ startDeps = deps;
116
+ unlisten = db.onBoardDeadline(noteDeadline); // a window opened here wakes us sooner
117
+ arm(FIRST_SWEEP_DELAY_MS);
118
+ console.log(`[board-expiry] started (sleeps until the soonest deadline, at most ${Math.round(MAX_WAIT_MS / 1000)}s; first sweep in ${Math.round(FIRST_SWEEP_DELAY_MS / 1000)}s)`);
119
+ }
120
+
121
+ function arm(delay) {
122
+ if (timer) clearTimeout(timer);
123
+ dueAt = Date.now() + delay;
124
+ timer = setTimeout(() => {
125
+ timer = null;
126
+ dueAt = null;
127
+ tick().catch((e) => console.error(`[board-expiry] sweep crashed: ${e && e.message}`));
128
+ }, delay);
129
+ if (timer.unref) timer.unref(); // never hold the process open on a restart
130
+ }
131
+
132
+ // One run: the sweep, then the one MIN(closes_at) read that sets the alarm.
133
+ // A run that starts while the previous one is still in flight is skipped —
134
+ // the running one re-arms when it finishes, so nothing is lost.
135
+ async function tick(deps = startDeps) {
136
+ if (running) return { skipped: true };
137
+ running = true;
138
+ let soonest = null;
139
+ try {
140
+ const out = await runSweep(deps);
141
+ try {
142
+ soonest = await (deps.soonestFn || db.msUntilSoonestOpenDeadline)();
143
+ } catch (e) {
144
+ (deps.error || console.error)(`[board-expiry] soonest-deadline read failed: ${e && e.message}`);
145
+ soonest = 0; // unknown: fall back to the one-minute retry, not the 5-minute cap
146
+ }
147
+ return out;
148
+ } finally {
149
+ running = false;
150
+ if (hintAt != null) {
151
+ const hint = hintAt - Date.now();
152
+ soonest = soonest == null ? hint : Math.min(soonest, hint);
153
+ hintAt = null;
154
+ }
155
+ if (started) arm(nextDelayMs(soonest));
156
+ }
157
+ }
158
+
159
+ // A window was opened that closes msUntilClose from now: wake sooner if the
160
+ // armed alarm is later than that deadline. Mid-sweep, remember it for the re-arm.
161
+ function noteDeadline(msUntilClose) {
162
+ if (!started || msUntilClose == null || !Number.isFinite(Number(msUntilClose))) return;
163
+ const ms = Math.max(0, Number(msUntilClose));
164
+ if (running) {
165
+ const at = Date.now() + ms;
166
+ hintAt = hintAt == null ? at : Math.min(hintAt, at);
167
+ return;
168
+ }
169
+ const delay = Math.min(MAX_WAIT_MS, ms + GRACE_MS);
170
+ if (dueAt == null || Date.now() + delay < dueAt) arm(delay);
94
171
  }
95
172
 
96
173
  function stop() {
97
- if (firstTimer) { clearTimeout(firstTimer); firstTimer = null; }
98
- if (intervalTimer) { clearInterval(intervalTimer); intervalTimer = null; }
174
+ started = false;
175
+ hintAt = null;
176
+ if (unlisten) { unlisten(); unlisten = null; }
177
+ if (timer) { clearTimeout(timer); timer = null; }
178
+ dueAt = null;
179
+ }
180
+
181
+ // For tests: what is armed right now.
182
+ function _state() {
183
+ return { started, running, msUntilNextRun: dueAt == null ? null : dueAt - Date.now() };
99
184
  }
100
185
 
101
- module.exports = { start, stop, runSweep, INTERVAL_MS, SWEEP_LIMIT };
186
+ module.exports = {
187
+ start, stop, runSweep, tick, noteDeadline, nextDelayMs, _state,
188
+ INTERVAL_MS, MAX_WAIT_MS, RETRY_MS, GRACE_MS, SWEEP_LIMIT,
189
+ };
@@ -30,6 +30,7 @@
30
30
  { "glob": "src/bongos/auth-config.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
31
31
  { "glob": "src/bongos/auth-discord.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
32
32
  { "glob": "src/bongos/auth-github.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
33
+ { "glob": "src/bongos/auth-google.js", "group": "authz", "why": "task 1004993 (ADR 0396). The Google sign-in beside auth-github.js: it checks Google's ID token, runs the admission gate for a Google person and mints their web session, so it is the same trust surface as the GitHub flow." },
33
34
  { "glob": "src/bongos/auth-idp.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
34
35
  { "glob": "src/bongos/db.js", "group": "authz", "why": "rank tiers, REQUIRES_RANK_TIER — the CASE edited in the SEC #863 incident" },
35
36
  { "glob": "src/bongos/db-kernel.js", "group": "authz", "why": "task 1004024. The kernel db slice carved out of db.js (BV1.R69 / ADR 0091): builder identity and the rank reads the SEC #863 entry above exists for." },