@bongos/core 1.23.22 → 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 (424) hide show
  1. package/.bongos-core.json +902 -407
  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 +90 -0
  27. package/clients/bongos-client/index.cjs +90 -0
  28. package/clients/bongos-client/index.d.ts +149 -4
  29. package/clients/bongos-client/index.mjs +90 -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/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  33. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  34. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  35. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  36. package/docs/adr/README.md +5 -1
  37. package/docs/api/openapi.json +8555 -5876
  38. package/docs/api-reference.md +63 -10
  39. package/docs/architecture.md +37 -1
  40. package/docs/canonical-permissions.md +1 -1
  41. package/docs/copy-inventory.md +420 -337
  42. package/docs/copy-registry.json +1321 -500
  43. package/docs/file-map.md +1 -0
  44. package/docs/module-api-changelog.md +5 -0
  45. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  46. package/docs/onboarding/diagrams/README.md +1 -1
  47. package/docs/onboarding/drachmae-and-karma.md +15 -15
  48. package/docs/onboarding/primer.md +2 -2
  49. package/docs/page-inventory.json +55 -6
  50. package/docs/page-readings.json +1185 -1011
  51. package/docs/recipes/google-sign-in.md +60 -0
  52. package/docs/recipes/ops-gotchas.md +1 -1
  53. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  54. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  55. package/migrations/core_283_payout_approvals.sql +183 -0
  56. package/migrations/core_284_payout_gates.sql +179 -0
  57. package/migrations/core_285_task_needs.sql +74 -0
  58. package/modules/ai-studio/billing.js +67 -0
  59. package/modules/ai-studio/context.js +29 -0
  60. package/modules/ai-studio/db.js +172 -0
  61. package/modules/ai-studio/gemini.js +75 -0
  62. package/modules/ai-studio/generate.js +119 -0
  63. package/modules/ai-studio/keys.js +73 -0
  64. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  65. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  66. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  67. package/modules/ai-studio/openrouter.js +90 -0
  68. package/modules/ai-studio/payments.js +105 -0
  69. package/modules/ai-studio/pricing.js +92 -0
  70. package/modules/ai-studio/routes/ai-studio.js +235 -5
  71. package/modules/ai-studio/studio.js +121 -0
  72. package/modules/autonomy/fence.js +64 -1
  73. package/modules/builder-settings/builder-needs.js +27 -1
  74. package/modules/copy-desk/routes/copy-desk.js +5 -0
  75. package/modules/economy/achievements.js +22 -10
  76. package/modules/economy/cost-classify.js +9 -0
  77. package/modules/economy/cost-write-guard.js +52 -0
  78. package/modules/economy/cost.js +2 -1
  79. package/modules/economy/credits.js +18 -15
  80. package/modules/economy/goal-reward.js +161 -0
  81. package/modules/economy/reward.js +4 -0
  82. package/modules/economy/routes/cost.js +33 -7
  83. package/modules/government/catalog.js +6 -0
  84. package/modules/government/db.js +34 -0
  85. package/modules/government/docket.js +36 -0
  86. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  87. package/modules/government/pollers/board-expiry.js +108 -20
  88. package/modules/government/protected-surfaces.json +1 -0
  89. package/modules/government/routes/government.js +22 -0
  90. package/modules/government/routing.js +39 -5
  91. package/modules/government/waiting.js +74 -0
  92. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  93. package/modules/hall-ui/public/ai-studio.css +193 -0
  94. package/modules/hall-ui/public/ai-studio.html +98 -0
  95. package/modules/hall-ui/public/ai-studio.js +556 -0
  96. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  97. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  98. package/modules/hall-ui/public/blockers-lib.js +15 -1
  99. package/modules/hall-ui/public/blockers.html +8 -0
  100. package/modules/hall-ui/public/blockers.js +1 -1
  101. package/modules/hall-ui/public/claim-action.js +3 -0
  102. package/modules/hall-ui/public/collab.html +12 -0
  103. package/modules/hall-ui/public/craft-mode.js +4 -1
  104. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  105. package/modules/hall-ui/public/drachmae.html +1 -1
  106. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  107. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  108. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  109. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  110. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  111. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  115. package/modules/hall-ui/public/goal-inbox.js +1 -1
  116. package/modules/hall-ui/public/goals-render.js +4 -0
  117. package/modules/hall-ui/public/goals.html +8 -0
  118. package/modules/hall-ui/public/gov-living.js +6 -2
  119. package/modules/hall-ui/public/hall-ask.css +5 -0
  120. package/modules/hall-ui/public/hall-ask.js +39 -3
  121. package/modules/hall-ui/public/notes-panel.css +20 -0
  122. package/modules/hall-ui/public/notes-panel.js +146 -0
  123. package/modules/hall-ui/public/project-settings.html +1 -1
  124. package/modules/hall-ui/public/project-settings.js +7 -5
  125. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  126. package/modules/hall-ui/public/settings.html +15 -0
  127. package/modules/hall-ui/public/street-deck.js +12 -3
  128. package/modules/hall-ui/public/studio.css +90 -8
  129. package/modules/hall-ui/public/studio.html +39 -10
  130. package/modules/hall-ui/public/studio.js +148 -18
  131. package/modules/hall-ui/public/task.html +2 -0
  132. package/modules/hall-ui/public/task.js +4 -0
  133. package/modules/hall-ui/public/tweak-editor.js +12 -3
  134. package/modules/hall-ui/public/welcome-boot.css +42 -6
  135. package/modules/hall-ui/public/welcome-boot.js +20 -6
  136. package/modules/hall-ui/public/work.js +20 -0
  137. package/modules/hall-ui/records/ai-studio.md +36 -0
  138. package/modules/hall-ui/records/blockers-people.md +24 -0
  139. package/modules/lifecycle/db-claims.js +32 -4
  140. package/modules/lifecycle/db-goals.js +18 -5
  141. package/modules/lifecycle/db-tasks.js +15 -6
  142. package/modules/lifecycle/done-when.js +23 -1
  143. package/modules/lifecycle/lifecycle.js +5 -0
  144. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  145. package/modules/lifecycle/module.json +2 -1
  146. package/modules/lifecycle/notes.js +219 -0
  147. package/modules/lifecycle/page-notice.js +124 -0
  148. package/modules/lifecycle/page-tweak-artist.js +18 -1
  149. package/modules/lifecycle/publish-reconciler.js +67 -20
  150. package/modules/lifecycle/routes/claims.js +5 -2
  151. package/modules/lifecycle/routes/goals.js +3 -1
  152. package/modules/lifecycle/routes/notes.js +91 -0
  153. package/modules/lifecycle/routes/project-settings.js +43 -35
  154. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  155. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  156. package/modules/lifecycle/routes/tasks.js +12 -12
  157. package/modules/lifecycle/task-needs-gate.js +70 -0
  158. package/modules/market-watch/CLAUDE.md +15 -0
  159. package/modules/market-watch/db.js +340 -0
  160. package/modules/market-watch/key.js +58 -0
  161. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  162. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  163. package/modules/market-watch/module.json +26 -0
  164. package/modules/market-watch/pollers/hub-seed.js +54 -0
  165. package/modules/market-watch/pollers/weekly-run.js +100 -0
  166. package/modules/market-watch/research.js +350 -0
  167. package/modules/market-watch/run.js +245 -0
  168. package/modules/market-watch/seed/market-data.json +38 -0
  169. package/modules/market-watch/seed/partners-data.json +497 -0
  170. package/modules/market-watch/seed.js +252 -0
  171. package/modules/market-watch/store.js +230 -0
  172. package/modules/memory/routes/memory.js +14 -2
  173. package/modules/memory/routes/search.js +8 -2
  174. package/modules/platform-identity/account-logins.js +192 -0
  175. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  176. package/modules/platform-identity/platform-identity.js +5 -4
  177. package/modules/platform-identity/routes/my-projects.js +6 -0
  178. package/modules/platform-identity/routes/profile.js +18 -0
  179. package/modules/platform-identity/routes/public-profile.js +10 -1
  180. package/modules/platform-identity/routes/sso.js +26 -1
  181. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  182. package/modules/provisioning/credential-preflight.js +13 -3
  183. package/modules/provisioning/pollers/app-liveness.js +25 -5
  184. package/modules/provisioning/routes/provisioning.js +1 -1
  185. package/modules/provisioning/starter-bundles.js +6 -2
  186. package/modules/public-landing/public/account.html +10 -2
  187. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  188. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  189. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  190. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  191. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  192. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  193. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  194. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  195. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  196. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  197. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  198. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  199. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  200. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  201. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  202. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  203. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  204. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  205. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  206. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  207. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  208. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  209. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  210. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  211. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  212. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  213. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  214. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  215. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  216. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  217. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  218. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  219. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  220. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  221. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  222. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  223. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  224. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  225. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  226. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  227. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  228. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  229. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  230. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  231. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  232. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  233. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  234. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  235. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  236. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  237. package/modules/public-landing/public/community.html +1 -1
  238. package/modules/public-landing/public/connections.html +2 -2
  239. package/modules/public-landing/public/guild.html +1 -1
  240. package/modules/public-landing/public/invitations.html +2 -2
  241. package/modules/public-landing/public/projects.html +5 -5
  242. package/modules/raise/views/nda.html +2 -0
  243. package/modules/specialities/db.js +32 -1
  244. package/modules/specialities/routes/specialities.js +10 -6
  245. package/modules/status-ui/public/index.html +1 -1
  246. package/package-lock.json +2 -2
  247. package/package.json +1 -1
  248. package/release-notes.json +220 -0
  249. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  250. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  251. package/scripts/bongos/claim.js +22 -1
  252. package/scripts/bongos/db-backup-nightly.sh +29 -0
  253. package/scripts/bongos/deploy-divergence.js +7 -0
  254. package/scripts/bongos/doc-cli-guard.js +6 -0
  255. package/scripts/bongos/fitness.js +3 -2
  256. package/scripts/bongos/gen-diagrams.js +3 -3
  257. package/scripts/bongos/lib.sh +4 -5
  258. package/scripts/bongos/migration-namespace.js +13 -1
  259. package/scripts/bongos/plain-cards.js +4 -0
  260. package/scripts/bongos/provision-github-setup.js +3 -1
  261. package/scripts/bongos/provision-units.js +6 -1
  262. package/scripts/bongos/publish-manifest.js +3 -0
  263. package/scripts/bongos/release.js +2 -0
  264. package/scripts/bongos/run-unit-tests.js +268 -82
  265. package/scripts/bongos/seed-tasks.example.json +56 -0
  266. package/scripts/bongos/seed-tasks.js +222 -0
  267. package/scripts/bongos/seed-verify-criteria.js +1 -2
  268. package/scripts/bongos/ship-finish.js +2 -0
  269. package/scripts/bongos/ship-resume.js +2 -0
  270. package/scripts/bongos/strand-lease.js +250 -0
  271. package/scripts/bongos/strand-watch.js +26 -5
  272. package/scripts/bongos/version-literal-guard.js +2 -2
  273. package/scripts/discord/post.js +4 -3
  274. package/scripts/gds/seed-tasks.js +7 -0
  275. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  276. package/scripts/hall-preview/server.js +21 -0
  277. package/src/bongos/auth-config.js +29 -1
  278. package/src/bongos/auth-discord.js +3 -3
  279. package/src/bongos/auth-github.js +3 -6
  280. package/src/bongos/auth-google.js +217 -0
  281. package/src/bongos/device-approval.js +140 -0
  282. package/src/bongos/llm-pricing.js +17 -1
  283. package/src/bongos/metrics.js +48 -2
  284. package/src/bongos/middleware/rate-limit.js +30 -11
  285. package/src/bongos/module-scope-map.js +12 -1
  286. package/src/bongos/optimizer.js +15 -1
  287. package/src/bongos/payout-approvals.js +342 -0
  288. package/src/bongos/payout-gates.js +364 -0
  289. package/src/bongos/prelaunch-gate.js +7 -9
  290. package/src/bongos/prelaunch-switch.js +20 -0
  291. package/src/bongos/route-rank-check.js +3 -0
  292. package/src/bongos/routes/auth-google.js +246 -0
  293. package/src/bongos/routes/auth.js +38 -1
  294. package/src/bongos/routes/backup.js +33 -9
  295. package/src/bongos/routes/builders.js +57 -30
  296. package/src/bongos/routes/github.js +9 -0
  297. package/src/bongos/routes/me.js +18 -0
  298. package/src/bongos/routes/my-payouts.js +133 -0
  299. package/src/bongos/routes/payout-approvals.js +312 -0
  300. package/src/bongos/routes/payout-gates.js +179 -0
  301. package/src/bongos/routes.js +15 -1
  302. package/src/bongos/secret-box.js +18 -3
  303. package/src/bongos/serve-internal.js +47 -19
  304. package/src/bongos/sign-in-pages.js +138 -0
  305. package/src/bongos/task-needs.js +146 -0
  306. package/src/bongos-downloads.js +18 -1
  307. package/src/instance-config.js +28 -0
  308. package/src/module-api.js +14 -1
  309. package/tests/account_logins.mjs +326 -0
  310. package/tests/account_logins_db.mjs +161 -0
  311. package/tests/ai_studio_billing.mjs +253 -0
  312. package/tests/ai_studio_generate.mjs +282 -0
  313. package/tests/ai_studio_keys.mjs +159 -0
  314. package/tests/ai_studio_requests.mjs +319 -0
  315. package/tests/auth_backchannel_logout_route.mjs +201 -0
  316. package/tests/auto_satisfy_criteria.mjs +10 -1
  317. package/tests/autobongos_tweak_lane.mjs +144 -0
  318. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  319. package/tests/backfill_subagent_rewards.mjs +17 -1
  320. package/tests/backup_trigger_lock.mjs +119 -0
  321. package/tests/blockers_people.mjs +198 -0
  322. package/tests/bounded_rate_maps.mjs +184 -0
  323. package/tests/claimable_blocker_status.mjs +1 -1
  324. package/tests/claims_batch_routes.mjs +421 -0
  325. package/tests/config_read_cache.mjs +171 -0
  326. package/tests/copy_desk_page_editor.mjs +12 -0
  327. package/tests/cost_write_guard.mjs +200 -0
  328. package/tests/craft_achievements.mjs +33 -10
  329. package/tests/criterion_goal_attach.mjs +1 -1
  330. package/tests/db_backup_nightly.mjs +73 -1
  331. package/tests/deploy_divergence_line.mjs +7 -0
  332. package/tests/effective_visibility_predicate.mjs +12 -0
  333. package/tests/fitness.mjs +19 -0
  334. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  335. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  336. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  337. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  338. package/tests/github_one_trip.mjs +34 -0
  339. package/tests/github_repo_visibility.mjs +20 -0
  340. package/tests/goal_achievement.mjs +30 -1
  341. package/tests/goal_achievement_reward.mjs +205 -0
  342. package/tests/goal_sort_staleness.mjs +2 -1
  343. package/tests/goal_tier.mjs +11 -1
  344. package/tests/google_fonts_nonblocking.mjs +84 -0
  345. package/tests/google_sign_in.mjs +401 -0
  346. package/tests/government_board_expiry.mjs +94 -0
  347. package/tests/government_protected_surfaces.mjs +1 -1
  348. package/tests/government_routes.mjs +1 -1
  349. package/tests/hall_ai_studio.mjs +347 -0
  350. package/tests/hall_page_gate_map.mjs +3 -0
  351. package/tests/hall_studio_home.mjs +61 -2
  352. package/tests/home_attention_cards.mjs +1 -1
  353. package/tests/human_signal.mjs +141 -0
  354. package/tests/install_script_reader.mjs +51 -0
  355. package/tests/integration_fixture_ids.mjs +82 -0
  356. package/tests/invite_expiry_db.mjs +13 -1
  357. package/tests/lib_sh_resolution.mjs +0 -4
  358. package/tests/market_watch_data.mjs +244 -0
  359. package/tests/market_watch_data_db.mjs +308 -0
  360. package/tests/market_watch_module.mjs +114 -0
  361. package/tests/market_watch_run.mjs +503 -0
  362. package/tests/metrics.mjs +124 -0
  363. package/tests/migration_namespace.mjs +14 -0
  364. package/tests/module_api.mjs +2 -0
  365. package/tests/module_loader.mjs +1 -1
  366. package/tests/notes.mjs +182 -0
  367. package/tests/own_scope_guard.mjs +5 -0
  368. package/tests/page_tweak_marker_sql.mjs +76 -0
  369. package/tests/page_tweak_no_block.mjs +135 -0
  370. package/tests/page_tweak_notice.mjs +103 -0
  371. package/tests/payout_approvals.mjs +253 -0
  372. package/tests/payout_approvals_db.mjs +106 -0
  373. package/tests/payout_gates.mjs +322 -0
  374. package/tests/payout_gates_db.mjs +113 -0
  375. package/tests/planning_session_skill.mjs +13 -3
  376. package/tests/project_settings_page.mjs +29 -5
  377. package/tests/project_settings_spend_cap_route.mjs +235 -0
  378. package/tests/promotion_sweep.mjs +29 -1
  379. package/tests/provision.mjs +3 -2
  380. package/tests/provisioning_app_status.mjs +18 -0
  381. package/tests/provisioning_credential_preflight.mjs +53 -0
  382. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  383. package/tests/publish_reconciler_overlap.mjs +60 -0
  384. package/tests/rank_change_broadcast_port.mjs +151 -0
  385. package/tests/seed_tasks.mjs +176 -0
  386. package/tests/seed_verify_criteria.mjs +2 -6
  387. package/tests/site_classified_once.mjs +142 -0
  388. package/tests/speciality_db_shape.mjs +23 -0
  389. package/tests/speciality_session_skills.mjs +48 -2
  390. package/tests/strand_lease.mjs +249 -0
  391. package/tests/strand_watch.mjs +10 -0
  392. package/tests/task_criteria_routes.mjs +275 -0
  393. package/tests/task_criteria_unlink_db.mjs +123 -0
  394. package/tests/task_needs.mjs +233 -0
  395. package/tests/task_needs_db.mjs +110 -0
  396. package/tests/unit_runner_serial.mjs +266 -43
  397. package/tests/version_literals.mjs +5 -7
  398. package/tests/welcome_boot.mjs +16 -5
  399. package/tests/welcome_boot_browser.mjs +36 -4
  400. package/docs/recipes/bongos-cli-release.md +0 -66
  401. package/scripts/bongos/backfill-disciplines.js +0 -144
  402. package/scripts/bongos/backfill-session-earnings.js +0 -123
  403. package/scripts/bongos/backfill-session-types.js +0 -87
  404. package/scripts/bongos/build-bongos.js +0 -94
  405. package/scripts/bongos/cleanup-orphans.js +0 -74
  406. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  407. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  408. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  409. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  410. package/scripts/bongos/smoke-batch-release.sh +0 -316
  411. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  412. package/scripts/bongos/smoke-dependencies.sh +0 -143
  413. package/scripts/gds/backfill-disciplines.js +0 -7
  414. package/scripts/gds/backfill-session-earnings.js +0 -7
  415. package/scripts/gds/backfill-session-types.js +0 -7
  416. package/scripts/gds/cleanup-orphans.js +0 -7
  417. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  418. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  419. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  420. package/scripts/gds/smoke-batch-claim.sh +0 -8
  421. package/scripts/gds/smoke-batch-release.sh +0 -8
  422. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  423. package/scripts/gds/smoke-dependencies.sh +0 -8
  424. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,200 @@
1
+ // tests/cost_write_guard.mjs — POST /cost refuses what a builder may not book
2
+ // (task 1004959, security report 1000029).
3
+ //
4
+ // The real route runs over a stubbed doorway: requireBuilder admits a builder
5
+ // whose id and rank the test sets, the pool answers the claims lookup from a
6
+ // set of (task, builder) pairs, and logCost is a recorder injected through the
7
+ // router's deps. So every case goes through the route's own wiring (the guard,
8
+ // the claims SQL, the error envelope), not a hand-built input to the guard.
9
+ //
10
+ // The legitimate callers are pinned by the payloads they really send:
11
+ // session-cost.js (session:* rows, and cron:* rows on a scheduled run),
12
+ // ship-grade-steps.js (source 'grader' on the caller's own task), cost.js
13
+ // (manual, optional --task-id) and backfill-cost.js (art_pipeline, no task).
14
+ //
15
+ // Run: node --test tests/cost_write_guard.mjs
16
+
17
+ import assert from 'node:assert/strict';
18
+ import { test, before, after, beforeEach } from 'node:test';
19
+ import http from 'node:http';
20
+ import { createRequire } from 'node:module';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const express = require('express');
24
+
25
+ const apiPath = require.resolve('../src/module-api.js');
26
+ const realApi = require(apiPath);
27
+
28
+ let caller = { id: 7, rank: 'xenos' };
29
+ let claimed = new Set(); // "taskId:builderId"
30
+ let logged = [];
31
+ const queries = [];
32
+
33
+ const pool = {
34
+ async query(sql, params) {
35
+ queries.push({ sql, params });
36
+ if (/FROM claims/.test(sql)) {
37
+ return { rows: claimed.has(`${params[0]}:${params[1]}`) ? [{ '?column?': 1 }] : [] };
38
+ }
39
+ throw new Error(`unscripted query: ${sql.trim().slice(0, 60)}`);
40
+ },
41
+ };
42
+ require.cache[apiPath].exports = {
43
+ ...realApi,
44
+ pool,
45
+ requireBuilder: (req, _res, next) => { req.builder = { ...caller }; next(); },
46
+ };
47
+
48
+ const buildCostRouter = require('../modules/economy/routes/cost.js');
49
+ const { MAX_ROW_USD } = require('../modules/economy/cost-write-guard.js');
50
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
51
+
52
+ const app = express();
53
+ app.use(express.json());
54
+ app.use(attachFail);
55
+ app.use('/api/bongos', buildCostRouter({
56
+ logCost: async (row) => { logged.push(row); return { id: logged.length, recorded_at: new Date().toISOString() }; },
57
+ }));
58
+
59
+ let server, port;
60
+ before(async () => {
61
+ server = http.createServer(app);
62
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
63
+ port = server.address().port;
64
+ });
65
+ after(async () => { await new Promise((r) => server.close(r)); });
66
+ beforeEach(() => { caller = { id: 7, rank: 'xenos' }; claimed = new Set(); logged = []; queries.length = 0; });
67
+
68
+ function post(body) {
69
+ return new Promise((resolve, reject) => {
70
+ const payload = JSON.stringify(body);
71
+ const req = http.request({
72
+ host: '127.0.0.1', port, path: '/api/bongos/cost', method: 'POST',
73
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
74
+ }, (res) => {
75
+ let data = '';
76
+ res.on('data', (c) => { data += c; });
77
+ res.on('end', () => resolve({ status: res.statusCode, body: data ? JSON.parse(data) : null }));
78
+ });
79
+ req.on('error', reject);
80
+ req.end(payload);
81
+ });
82
+ }
83
+
84
+ const errCode = (r) => r.body && (r.body.error?.code || r.body.error || r.body.code);
85
+
86
+ // --- the three attacks from the report -------------------------------------
87
+
88
+ test('an agent: row is refused from every rank, Archon included (only the server writes them)', async () => {
89
+ for (const rank of ['xenos', 'metic', 'archon']) {
90
+ caller = { id: 7, rank };
91
+ const r = await post({ amount_usd: 50, category: 'api', source: 'agent:anything', source_ref: `x-${rank}` });
92
+ assert.equal(r.status, 403, `${rank}: agent spend must not come through HTTP`);
93
+ assert.equal(errCode(r), 'reserved_source');
94
+ }
95
+ assert.equal(logged.length, 0, 'nothing reached the ledger');
96
+ });
97
+
98
+ test('a market-watch: row is refused from every rank too (the weekly run writes it in-process, task 1004937)', async () => {
99
+ for (const rank of ['xenos', 'metic', 'archon']) {
100
+ caller = { id: 7, rank };
101
+ const r = await post({ amount_usd: 5, category: 'api', source: 'Market-Watch:12', source_ref: `item-${rank}` });
102
+ assert.equal(r.status, 403, `${rank}: Market Watch spend must not come through HTTP`);
103
+ assert.equal(errCode(r), 'reserved_source');
104
+ }
105
+ assert.equal(logged.length, 0, 'nothing reached the ledger');
106
+ });
107
+
108
+ test('the prefix check ignores case and leading spaces', async () => {
109
+ const r = await post({ amount_usd: 50, category: 'api', source: ' AGENT:x', source_ref: 'x1' });
110
+ assert.equal(r.status, 403);
111
+ assert.equal(logged.length, 0);
112
+ });
113
+
114
+ test('a cron: row is refused below Metic and accepted from Metic+', async () => {
115
+ for (const rank of ['xenos', 'thetes']) {
116
+ caller = { id: 7, rank };
117
+ const r = await post({ amount_usd: 2, category: 'api', source: 'cron:nightly:interactive', source_ref: `s-${rank}` });
118
+ assert.equal(r.status, 403, `${rank} may not book scheduled-run spend`);
119
+ assert.equal(errCode(r), 'reserved_source');
120
+ }
121
+ assert.equal(logged.length, 0);
122
+ for (const rank of ['metic', 'archon']) {
123
+ caller = { id: 7, rank };
124
+ const r = await post({ amount_usd: 2, category: 'api', source: 'cron:nightly:interactive', source_ref: `s-${rank}` });
125
+ assert.equal(r.status, 201, `${rank} runs the scheduled jobs and must still log them`);
126
+ }
127
+ assert.equal(logged.length, 2);
128
+ });
129
+
130
+ test('a task the caller never claimed is refused; one they claimed (even released) is booked', async () => {
131
+ const r1 = await post({ task_id: 555, amount_usd: 1, category: 'api', source: 'grader', source_ref: 'g1' });
132
+ assert.equal(r1.status, 403);
133
+ assert.equal(errCode(r1), 'task_not_yours');
134
+ assert.equal(logged.length, 0);
135
+ const q = queries.find((x) => /FROM claims/.test(x.sql));
136
+ assert.deepEqual(q.params, [555, 7], 'the lookup is keyed on the task AND the authed caller');
137
+
138
+ claimed.add('555:7');
139
+ const r2 = await post({ task_id: 555, amount_usd: 1, category: 'api', source: 'grader', source_ref: 'g2' });
140
+ assert.equal(r2.status, 201);
141
+ assert.equal(logged[0].taskId, 555);
142
+ });
143
+
144
+ test('someone else\'s claim on the task does not let this caller charge it', async () => {
145
+ claimed.add('555:8');
146
+ const r = await post({ task_id: 555, amount_usd: 1, category: 'other', source_ref: 'm1' });
147
+ assert.equal(r.status, 403);
148
+ assert.equal(errCode(r), 'task_not_yours');
149
+ });
150
+
151
+ test('an Archon may log against any task (backfills) without the claims lookup', async () => {
152
+ caller = { id: 3, rank: 'archon' };
153
+ const r = await post({ task_id: 555, amount_usd: 1, category: 'other', source: 'manual', source_ref: 'a1' });
154
+ assert.equal(r.status, 201);
155
+ assert.equal(queries.filter((x) => /FROM claims/.test(x.sql)).length, 0);
156
+ });
157
+
158
+ test('one row is capped at MAX_ROW_USD, on the total and on every tier', async () => {
159
+ const over = MAX_ROW_USD + 1;
160
+ for (const field of ['amount_usd', 'fresh_input_usd', 'cache_read_usd', 'cache_write_usd', 'output_usd']) {
161
+ const body = { amount_usd: 1, category: 'api', source: 'session:interactive', source_ref: `c-${field}` };
162
+ body[field] = over;
163
+ const r = await post(body);
164
+ assert.equal(r.status, 400, `${field} over the cap must be refused`);
165
+ }
166
+ const ok = await post({ amount_usd: MAX_ROW_USD, category: 'api', source: 'session:interactive', source_ref: 'c-at' });
167
+ assert.equal(ok.status, 201, 'exactly the cap is allowed');
168
+ assert.ok(MAX_ROW_USD >= 1000, 'the cap stays far above a real session');
169
+ });
170
+
171
+ test('the claims lookup leads on task_id, which a full (not partial) index serves', async () => {
172
+ // The only partial claims index is WHERE released_at IS NULL, which this lookup
173
+ // cannot use (a released claim must count). core_263's idx_claims_task_all is the
174
+ // one that keeps it off a table scan; if either side drifts, this fails.
175
+ const fs = require('node:fs');
176
+ const route = fs.readFileSync(new URL('../modules/economy/routes/cost.js', import.meta.url), 'utf8');
177
+ assert.match(route, /FROM claims WHERE task_id = \$1 AND builder_id = \$2/);
178
+ const mig = fs.readFileSync(new URL('../migrations/core_263_closeness_history_indexes.sql', import.meta.url), 'utf8');
179
+ assert.match(mig, /idx_claims_task_all\s+ON claims \(task_id\);/);
180
+ });
181
+
182
+ // --- the real callers keep working ----------------------------------------
183
+
184
+ test('session-cost.js rows (no task, session:* sources) still book for any rank', async () => {
185
+ for (const source of ['session:interactive', 'session:subagent']) {
186
+ const r = await post({ amount_usd: 12.5, category: 'api', source, source_ref: `sess-${source}`,
187
+ fresh_input_usd: 1, cache_read_usd: 10, cache_write_usd: 1, output_usd: 0.5 });
188
+ assert.equal(r.status, 201, source);
189
+ }
190
+ assert.equal(logged.length, 2);
191
+ assert.equal(logged[0].builderId, 7, 'attributed to the authed caller');
192
+ });
193
+
194
+ test('the art backfill and a plain manual row (no task) still book', async () => {
195
+ const art = await post({ amount_usd: 0.04, category: 'art', source: 'art_pipeline', source_ref: 'art-1', recorded_at: '2026-10-01T00:00:00Z' });
196
+ assert.equal(art.status, 201);
197
+ const manual = await post({ amount_usd: 3, category: 'other', source_ref: 'man-1' });
198
+ assert.equal(manual.status, 201);
199
+ assert.equal(logged[1].source, 'manual');
200
+ });
@@ -114,10 +114,16 @@ function fakeLedger({ failReconcile = false } = {}) {
114
114
  rowCount: 1,
115
115
  };
116
116
  }
117
- if (q.startsWith('SELECT DISTINCT builder_id FROM credit_log')) {
117
+ if (q.startsWith('SELECT builder_id, MAX(id) AS last_id FROM credit_log')) {
118
+ // the backfill read: craft-paid builders with a row newer than the watermark ($5)
118
119
  const pats = params.slice(0, 4).map(likeRe);
119
- const ids = [...new Set(ledger.filter((r) => r.delta > 0 && pats.some((re) => re.test(r.reason))).map((r) => r.builder_id))].sort();
120
- return { rows: ids.map((builder_id) => ({ builder_id })) };
120
+ const since = Number(params[4]);
121
+ const last = new Map();
122
+ for (const r of ledger) {
123
+ if (r.id > since && r.delta > 0 && pats.some((re) => re.test(r.reason))) last.set(r.builder_id, Math.max(last.get(r.builder_id) || 0, r.id));
124
+ }
125
+ const ids = [...last.keys()].sort().slice(0, Number(params[5]));
126
+ return { rows: ids.map((builder_id) => ({ builder_id, last_id: String(last.get(builder_id)) })) };
121
127
  }
122
128
  if (q.startsWith('SELECT COUNT(*)::int AS n FROM credit_log')) return { rows: [{ n: 0 }], rowCount: 1 };
123
129
  if (q.startsWith('SELECT total_credits FROM builders')) return { rows: [{ total_credits: 0 }], rowCount: 1 };
@@ -274,7 +280,7 @@ await test('a builder holding every craft badge costs one read and nothing more'
274
280
 
275
281
  // ---- 4. the backfill ------------------------------------------------------------------
276
282
 
277
- await test('A Whole Surface is backfilled by the once-per-start sweep, for the artists the ledger has paid', async () => {
283
+ await test('A Whole Surface is backfilled by the hourly incremental sweep, for the artists the ledger has paid', async () => {
278
284
  seams._reset();
279
285
  let asked = [];
280
286
  seams.registerProvider('copy-desk.artistStats', async (id) => { asked.push(String(id)); return { surfaces: [{ tweaked: 4, total: 4 }] }; });
@@ -289,17 +295,34 @@ await test('A Whole Surface is backfilled by the once-per-start sweep, for the a
289
295
  assert.deepEqual(asked, ['2001'], 'the studio is asked about the artist only');
290
296
  const again = await achievements.backfillCraftAchievements({ client: c });
291
297
  assert.equal(again.unlocked, 0, 'a second run unlocks nothing');
298
+ assert.equal(out.watermark, 1001, 'the watermark is the highest CRAFT credit_log id covered (pay() numbers rows from 1000; the engineer row 1002 is not craft)');
299
+
300
+ // task 1004965: INCREMENTAL — a caught-up run reads only past the watermark and checks nobody
301
+ const reads = () => c.sql.filter((q) => q.startsWith('SELECT builder_id, MAX(id) AS last_id FROM credit_log'));
302
+ const before = c.sql.length;
303
+ const caughtUp = await achievements.backfillCraftAchievements({ client: c, sinceId: out.watermark });
304
+ assert.deepEqual(caughtUp, { checked: 0, unlocked: 0, watermark: out.watermark }, 'caught up: nobody re-checked, watermark held');
305
+ assert.equal(c.sql.length - before, 1, 'one statement in all — no per-builder reads');
306
+ assert.match(reads().at(-1), /WHERE id > \$5 AND/, 'bounded by the watermark, never a full scan');
307
+ pay(c, '2003', 'idea.credit.task_ship:8:102');
308
+ const fresh = await achievements.backfillCraftAchievements({ client: c, sinceId: out.watermark });
309
+ assert.equal(fresh.checked, 1, 'only the builder paid since the watermark');
310
+ assert.ok(fresh.watermark > out.watermark, 'and the watermark moves past them');
311
+ const capped = await achievements.backfillCraftAchievements({ client: c, limit: 1 });
312
+ assert.equal(capped.watermark, 0, 'a run that hit its limit keeps the old watermark, skipping nobody past it');
292
313
 
293
314
  const reconciler = require('../modules/lifecycle/publish-reconciler.js');
294
- const calls = [];
295
- const reward = { backfillCraftAchievements: async () => { calls.push(1); return { checked: 1, unlocked: 1 }; } };
315
+ const sinceAsked = [];
316
+ const reward = { backfillCraftAchievements: async (o) => { sinceAsked.push(o.sinceId); return { checked: 1, unlocked: 1, watermark: 42 }; } };
296
317
  const quiet = { logFn: () => {}, errFn: () => {} };
297
- await reconciler.runCraftAchievementBackfill({ reward, ...quiet, forceCraftBackfill: true });
298
318
  await reconciler.runCraftAchievementBackfill({ reward, ...quiet });
299
- assert.equal(calls.length, 1, 'once per process start, not on every tick');
300
- const failing = await reconciler.runCraftAchievementBackfill({ reward: { backfillCraftAchievements: async () => { throw new Error('db down'); } }, ...quiet, forceCraftBackfill: true });
319
+ await reconciler.runCraftAchievementBackfill({ reward, ...quiet });
320
+ assert.deepEqual(sinceAsked, [0, 42], 'the first run scans from 0; the next starts at the watermark it was handed');
321
+ const failing = await reconciler.runCraftAchievementBackfill({ reward: { backfillCraftAchievements: async () => { throw new Error('db down'); } }, ...quiet });
301
322
  assert.equal(failing.skipped, 'error', 'a failure is logged and swallowed');
302
- assert.match(read('modules/lifecycle/publish-reconciler.js'), /runCraftAchievementBackfill\(deps\)\.catch\(crashHandler\('craft achievement backfill'\)\)/, 'the tick runs it');
323
+ await reconciler.runCraftAchievementBackfill({ reward, ...quiet });
324
+ assert.equal(sinceAsked.at(-1), 42, 'a failed run keeps the watermark');
325
+ assert.match(read('modules/lifecycle/publish-reconciler.js'), /\['craft achievement backfill', runCraftAchievementBackfill, HOURLY_MS\]/, 'the tick runs it HOURLY (an entry in the guarded SWEEPS list, tasks 1004924 + 1004965)');
303
326
  seams._reset();
304
327
  });
305
328
 
@@ -562,7 +562,7 @@ await test('setGoalStatus writes on the transaction client (task 1003840)', asyn
562
562
  // reintroduce the very pool-vs-transaction deadlock this test guards.
563
563
  assert.equal(seen.length, 2, 'the UPDATE and the request sweep reached the injected client');
564
564
  assert.match(seen[0].sql, /UPDATE goals/);
565
- assert.deepEqual(seen[0].params, [3, 'achieved']);
565
+ assert.deepEqual(seen[0].params, [3, 'achieved', null]);
566
566
  assert.match(seen[1].sql, /UPDATE goal_membership_requests/);
567
567
  });
568
568
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  import assert from 'node:assert/strict';
15
15
  import { test } from 'node:test';
16
- import { spawnSync } from 'node:child_process';
16
+ import { spawn, spawnSync } from 'node:child_process';
17
17
  import fs from 'node:fs';
18
18
  import os from 'node:os';
19
19
  import path from 'node:path';
@@ -111,6 +111,78 @@ test('it runs the same on macOS, where stat refuses -c and wc pads its numbers (
111
111
  assert.match(r.stdout, /ok · 1 dump\(s\) retained/);
112
112
  });
113
113
 
114
+ // task 1004966 — the nightly unit and the hall's "Back up now" both run this script, and
115
+ // the script takes a per-database flock under the backup dir, so a second run while one
116
+ // is dumping exits 0 with a SKIPPED line instead of starting a concurrent pg_dump. Needs
117
+ // flock (util-linux): skipped on Windows and wherever flock is not on PATH.
118
+ const hasFlock = process.platform !== 'win32' && spawnSync('bash', ['-c', 'command -v flock'], { encoding: 'utf8' }).status === 0;
119
+ const lockSkip = skip || (hasFlock ? false : 'needs flock (util-linux; runs in CI)');
120
+
121
+ // A slow fake pg_dump that counts its starts, so a test can see whether a second run
122
+ // began a dump while the first was still inside one.
123
+ function slowBackupDir() {
124
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'db-backup-lock-'));
125
+ const bin = path.join(tmp, 'bin');
126
+ const dir = path.join(tmp, 'backups');
127
+ fs.mkdirSync(bin);
128
+ fs.mkdirSync(dir);
129
+ const starts = path.join(tmp, 'starts');
130
+ fs.writeFileSync(path.join(bin, 'pg_dump'), [
131
+ '#!/bin/bash',
132
+ `echo started >> "${starts}"`,
133
+ 'sleep 3',
134
+ "printf -- '--\\n-- PostgreSQL database dump\\n--\\n--\\n-- PostgreSQL database dump complete\\n--\\n'",
135
+ '',
136
+ ].join('\n'), { mode: 0o755 });
137
+ const env = (db) => ({ ...process.env, PATH: `${bin}:${process.env.PATH}`, [`${ENV}DB`]: db, [`${ENV}DIR`]: dir });
138
+ const startCount = () => (fs.existsSync(starts) ? fs.readFileSync(starts, 'utf8').trim().split('\n').length : 0);
139
+ return { dir, env, startCount };
140
+ }
141
+
142
+ async function waitFor(pred, ms = 5000) {
143
+ const until = Date.now() + ms;
144
+ while (!pred()) {
145
+ if (Date.now() > until) throw new Error('timed out waiting for the first run to start its dump');
146
+ await new Promise((r) => setTimeout(r, 50));
147
+ }
148
+ }
149
+
150
+ function runAsync(env) {
151
+ const child = spawn('bash', [SCRIPT], { env });
152
+ let stdout = '';
153
+ child.stdout.on('data', (c) => { stdout += c; });
154
+ child.stderr.resume();
155
+ return new Promise((resolve) => child.on('close', (status) => resolve({ status, stdout })));
156
+ }
157
+
158
+ test('a second run while one is dumping does not start a concurrent dump (task 1004966)', { skip: lockSkip }, async () => {
159
+ const { dir, env, startCount } = slowBackupDir();
160
+ const first = runAsync(env('smallproj'));
161
+ await waitFor(() => startCount() === 1);
162
+ const second = spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env('smallproj') });
163
+ assert.equal(second.status, 0, 'the second run exits cleanly');
164
+ assert.match(second.stdout, /gds-db-backup: SKIPPED another backup of smallproj is already running/);
165
+ assert.equal(startCount(), 1, 'the second run never started pg_dump');
166
+ const r = await first;
167
+ assert.equal(r.status, 0);
168
+ assert.equal(fs.readdirSync(dir).filter((f) => f.endsWith('.sql.gz')).length, 1, 'exactly one dump landed');
169
+ // Released on exit: the next run dumps normally.
170
+ const third = spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env('smallproj') });
171
+ assert.equal(third.status, 0, third.stderr);
172
+ assert.equal(startCount(), 2);
173
+ });
174
+
175
+ test('the lock is per database: another project sharing the backup dir still dumps', { skip: lockSkip }, async () => {
176
+ const { env, startCount } = slowBackupDir();
177
+ const first = runAsync(env('smallproj'));
178
+ await waitFor(() => startCount() === 1);
179
+ const other = spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env('otherproj') });
180
+ assert.equal(other.status, 0, other.stderr);
181
+ assert.doesNotMatch(other.stdout, /SKIPPED/);
182
+ assert.equal(startCount(), 2);
183
+ assert.equal((await first).status, 0);
184
+ });
185
+
114
186
  test('no fixed size floor is baked in for every instance', () => {
115
187
  const src = fs.readFileSync(SCRIPT, 'utf8');
116
188
  assert.doesNotMatch(src, new RegExp(`${ENV}MIN_BYTES:-[1-9]`), 'a default floor fails every database smaller than it');
@@ -84,6 +84,13 @@ test('the line says who sees a merge, not a blanket rule in either direction', (
84
84
  assert.match(s, /Never call work live until that instance's \/version shows it/);
85
85
  });
86
86
 
87
+ test('the line tells the agent not to relay it to the builder (task 1004952)', () => {
88
+ // Owner, 2026-10-28: agents repeated this note in replies every session.
89
+ const s = line(behind);
90
+ assert.match(s, /This note is for you, not the builder: do not repeat it to them unless they ask whether work is live/);
91
+ assert.ok(!s.includes('\n'), 'still one line');
92
+ });
93
+
87
94
  test('the line says whether main\'s latest merge is in a release yet', () => {
88
95
  // task 1004873: the first question behind "is it live?" is "is it released?".
89
96
  const none = line({ ...behind, behind: false, hasUnreleased: true, unreleased: { count: 4 }, tip: { short: '0f39e344', version: null } });
@@ -160,6 +160,18 @@ const NON_PUBLIC_READS = [
160
160
  fn: 'getAccountByLogin',
161
161
  why: 'resolves a github_login for the Archon-gated invite path (POST /projects/invite). Inviting a person is not publishing them: the caller already knows the login and the result never reaches a public body.',
162
162
  },
163
+ {
164
+ fn: 'findPersonByLogin',
165
+ why: 'the sign-in lookup (task 1004992, ADR 0393): which account a provider identity reaches, so a sign-in through Google lands on the right person. It returns only the account id and person key to the sign-in route that minted the session; nothing it returns reaches a public body, and gating it on visibility would lock a private account out of its own sign-in.',
166
+ },
167
+ {
168
+ fn: 'signInWithLogin',
169
+ why: 'the Google sign-in (task 1004993, ADR 0396): after finding or creating the person by the provider id, it reads their handle and any linked GitHub login for the core sign-in route that mints their session. Nothing it returns reaches a public body, and a private account must still be able to sign in.',
170
+ },
171
+ {
172
+ fn: 'listLoginsForPerson',
173
+ why: 'the own-scope read behind GET /me/sign-in-methods (task 1004992): which ways in the signed-in person has, keyed on their authenticated key only. It returns their own providers and addresses to themselves; a private account must still see its own sign-in methods.',
174
+ },
163
175
  {
164
176
  fn: 'hiddenGithubIds',
165
177
  why: 'the closeness-graph consent gate — it answers only "which of these accounts set hide_stats", the OPPOSITE question to visibility. Its callers are requireBuilder-gated and it must stay fail-closed on hide alone (ADR 0171 D4); joining addressability would silently empty the graph of every private account for readers who are allowed to see the edge.',
package/tests/fitness.mjs CHANGED
@@ -74,6 +74,25 @@ test('the live CLAUDE.md delegates the charter to the rule file', () => {
74
74
  assert.ok(kernel.includes(CHARTER_RULE_REL), 'the heading must point somewhere, or it is decoration');
75
75
  });
76
76
 
77
+ // task 1004933 trimmed quick-nav rows that only repeated a section's own pointer.
78
+ // The pointer itself must survive somewhere in the kernel, and still resolve.
79
+ test('the kernel still points at every detail file a trimmed quick-nav row named', () => {
80
+ const kernel = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8');
81
+ for (const rel of ['docs/architecture.md', 'docs/file-map.md', 'docs/canonical-permissions.md',
82
+ 'docs/handoff-template.md', 'docs/project-context.template.md', 'config/branding.neutral.json', 'docs/branding-contract.md']) {
83
+ assert.ok(kernel.includes(`](${rel})`), `CLAUDE.md lost its link to ${rel}`);
84
+ assert.ok(fs.existsSync(path.join(ROOT, rel)), `${rel} is linked but missing`);
85
+ }
86
+ });
87
+
88
+ test('every section rule in the kernel sits after a blank line (a trim must not make a heading)', () => {
89
+ // Text directly above a `---` is a Setext H2 in CommonMark: the paragraph becomes a
90
+ // heading and the section break disappears. A substring check on links stays green.
91
+ const lines = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8').split(/\r?\n/);
92
+ const glued = lines.flatMap((l, i) => (l === '---' && i > 0 && lines[i - 1].trim() !== '' ? [i + 1] : []));
93
+ assert.deepEqual(glued, [], `CLAUDE.md has a --- with text directly above it at line(s) ${glued.join(', ')}`);
94
+ });
95
+
77
96
  test('a hollow charter heading hard-fails (the heading alone is not the charter)', () => {
78
97
  const r = fitness.checkClaudeMdBudget({ text: `${CHARTER_HEADING}\n` });
79
98
  assert.equal(r.hardFail, true, 'a heading with neither inline rules nor a pointer must fail');