@bongos/core 1.23.21 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/.bongos-core.json +983 -428
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +6 -3
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +94 -0
  27. package/clients/bongos-client/index.cjs +94 -0
  28. package/clients/bongos-client/index.d.ts +154 -4
  29. package/clients/bongos-client/index.mjs +94 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
  33. package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
  34. package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
  35. package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
  36. package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
  37. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  38. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  39. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  40. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  41. package/docs/adr/README.md +10 -1
  42. package/docs/api/openapi.json +8619 -5885
  43. package/docs/api-reference.md +68 -9
  44. package/docs/architecture.md +37 -1
  45. package/docs/canonical-permissions.md +1 -1
  46. package/docs/copy-inventory.md +420 -337
  47. package/docs/copy-registry.json +1321 -500
  48. package/docs/file-map.md +4 -0
  49. package/docs/module-api-changelog.md +7 -0
  50. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  51. package/docs/onboarding/diagrams/README.md +1 -1
  52. package/docs/onboarding/drachmae-and-karma.md +15 -15
  53. package/docs/onboarding/primer.md +2 -2
  54. package/docs/page-inventory.json +55 -6
  55. package/docs/page-readings.json +1185 -1011
  56. package/docs/recipes/google-sign-in.md +60 -0
  57. package/docs/recipes/ops-gotchas.md +1 -1
  58. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  59. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  60. package/migrations/core_283_payout_approvals.sql +183 -0
  61. package/migrations/core_284_payout_gates.sql +179 -0
  62. package/migrations/core_285_task_needs.sql +74 -0
  63. package/modules/ai-studio/CLAUDE.md +15 -0
  64. package/modules/ai-studio/billing.js +67 -0
  65. package/modules/ai-studio/context.js +29 -0
  66. package/modules/ai-studio/db.js +172 -0
  67. package/modules/ai-studio/gemini.js +75 -0
  68. package/modules/ai-studio/generate.js +119 -0
  69. package/modules/ai-studio/keys.js +73 -0
  70. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  71. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  72. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  73. package/modules/ai-studio/module.json +26 -0
  74. package/modules/ai-studio/openrouter.js +90 -0
  75. package/modules/ai-studio/payments.js +105 -0
  76. package/modules/ai-studio/pricing.js +92 -0
  77. package/modules/ai-studio/routes/ai-studio.js +252 -0
  78. package/modules/ai-studio/studio.js +121 -0
  79. package/modules/autonomy/fence.js +64 -1
  80. package/modules/builder-settings/builder-needs.js +27 -1
  81. package/modules/copy-desk/routes/copy-desk.js +5 -0
  82. package/modules/economy/achievements.js +22 -10
  83. package/modules/economy/cost-classify.js +9 -0
  84. package/modules/economy/cost-write-guard.js +52 -0
  85. package/modules/economy/cost.js +2 -1
  86. package/modules/economy/credits.js +18 -15
  87. package/modules/economy/goal-reward.js +161 -0
  88. package/modules/economy/reward.js +4 -0
  89. package/modules/economy/routes/cost.js +33 -7
  90. package/modules/government/catalog.js +6 -0
  91. package/modules/government/db.js +108 -0
  92. package/modules/government/docket.js +36 -0
  93. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  94. package/modules/government/pollers/board-expiry.js +108 -20
  95. package/modules/government/protected-surfaces.json +1 -0
  96. package/modules/government/routes/government.js +50 -18
  97. package/modules/government/routing.js +39 -5
  98. package/modules/government/waiting.js +74 -0
  99. package/modules/grading/grader-subagent.js +30 -11
  100. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  101. package/modules/hall-ui/public/ai-studio.css +193 -0
  102. package/modules/hall-ui/public/ai-studio.html +98 -0
  103. package/modules/hall-ui/public/ai-studio.js +556 -0
  104. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  105. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  106. package/modules/hall-ui/public/blockers-lib.js +15 -1
  107. package/modules/hall-ui/public/blockers.html +8 -0
  108. package/modules/hall-ui/public/blockers.js +1 -1
  109. package/modules/hall-ui/public/city.html +15 -15
  110. package/modules/hall-ui/public/claim-action.js +3 -0
  111. package/modules/hall-ui/public/collab.html +12 -0
  112. package/modules/hall-ui/public/craft-mode.js +4 -1
  113. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  114. package/modules/hall-ui/public/drachmae.html +1 -1
  115. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  120. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  121. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  122. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  123. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  124. package/modules/hall-ui/public/goal-inbox.js +1 -1
  125. package/modules/hall-ui/public/goals-render.js +4 -0
  126. package/modules/hall-ui/public/goals.html +8 -0
  127. package/modules/hall-ui/public/gov-living.js +6 -2
  128. package/modules/hall-ui/public/hall-ask.css +5 -0
  129. package/modules/hall-ui/public/hall-ask.js +39 -3
  130. package/modules/hall-ui/public/notes-panel.css +20 -0
  131. package/modules/hall-ui/public/notes-panel.js +146 -0
  132. package/modules/hall-ui/public/project-settings.html +1 -1
  133. package/modules/hall-ui/public/project-settings.js +7 -5
  134. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  135. package/modules/hall-ui/public/settings.html +15 -0
  136. package/modules/hall-ui/public/street-deck.js +12 -3
  137. package/modules/hall-ui/public/studio.css +90 -8
  138. package/modules/hall-ui/public/studio.html +39 -10
  139. package/modules/hall-ui/public/studio.js +148 -18
  140. package/modules/hall-ui/public/task.html +2 -0
  141. package/modules/hall-ui/public/task.js +4 -0
  142. package/modules/hall-ui/public/tweak-editor.js +12 -3
  143. package/modules/hall-ui/public/welcome-boot.css +42 -6
  144. package/modules/hall-ui/public/welcome-boot.js +20 -6
  145. package/modules/hall-ui/public/work.js +20 -0
  146. package/modules/hall-ui/records/ai-studio.md +36 -0
  147. package/modules/hall-ui/records/blockers-people.md +24 -0
  148. package/modules/lifecycle/db-analytics.js +10 -3
  149. package/modules/lifecycle/db-claims.js +32 -4
  150. package/modules/lifecycle/db-goals.js +48 -18
  151. package/modules/lifecycle/db-shared.js +9 -0
  152. package/modules/lifecycle/db-tasks.js +15 -6
  153. package/modules/lifecycle/db-versions.js +21 -8
  154. package/modules/lifecycle/done-when.js +23 -1
  155. package/modules/lifecycle/lifecycle.js +5 -0
  156. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  157. package/modules/lifecycle/module.json +2 -1
  158. package/modules/lifecycle/notes.js +219 -0
  159. package/modules/lifecycle/page-notice.js +124 -0
  160. package/modules/lifecycle/page-tweak-artist.js +18 -1
  161. package/modules/lifecycle/publish-reconciler.js +67 -20
  162. package/modules/lifecycle/routes/claims.js +5 -2
  163. package/modules/lifecycle/routes/goals.js +3 -1
  164. package/modules/lifecycle/routes/notes.js +91 -0
  165. package/modules/lifecycle/routes/project-settings.js +43 -35
  166. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  167. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  168. package/modules/lifecycle/routes/tasks.js +12 -12
  169. package/modules/lifecycle/ship-assurance.js +4 -1
  170. package/modules/lifecycle/ship-card.js +8 -2
  171. package/modules/lifecycle/task-needs-gate.js +70 -0
  172. package/modules/market-watch/CLAUDE.md +15 -0
  173. package/modules/market-watch/db.js +340 -0
  174. package/modules/market-watch/key.js +58 -0
  175. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  176. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  177. package/modules/market-watch/module.json +26 -0
  178. package/modules/market-watch/pollers/hub-seed.js +54 -0
  179. package/modules/market-watch/pollers/weekly-run.js +100 -0
  180. package/modules/market-watch/research.js +350 -0
  181. package/modules/market-watch/run.js +245 -0
  182. package/modules/market-watch/seed/market-data.json +38 -0
  183. package/modules/market-watch/seed/partners-data.json +497 -0
  184. package/modules/market-watch/seed.js +252 -0
  185. package/modules/market-watch/store.js +230 -0
  186. package/modules/memory/routes/memory.js +14 -2
  187. package/modules/memory/routes/search.js +8 -2
  188. package/modules/onboarding/howto-moments.js +8 -1
  189. package/modules/platform-identity/account-logins.js +192 -0
  190. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  191. package/modules/platform-identity/platform-identity.js +5 -4
  192. package/modules/platform-identity/routes/my-projects.js +6 -0
  193. package/modules/platform-identity/routes/profile.js +18 -0
  194. package/modules/platform-identity/routes/public-profile.js +10 -1
  195. package/modules/platform-identity/routes/sso.js +26 -1
  196. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  197. package/modules/provisioning/credential-preflight.js +13 -3
  198. package/modules/provisioning/pollers/app-liveness.js +25 -5
  199. package/modules/provisioning/routes/provisioning.js +1 -1
  200. package/modules/provisioning/starter-bundles.js +8 -2
  201. package/modules/public-landing/public/account.html +10 -2
  202. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  203. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  205. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  209. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  210. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  213. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  214. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  221. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  222. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  223. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  224. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  225. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  226. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  227. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  228. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  229. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  230. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  233. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  244. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  245. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  246. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  247. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  248. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  249. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  250. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  251. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  252. package/modules/public-landing/public/community.html +1 -1
  253. package/modules/public-landing/public/connections.html +2 -2
  254. package/modules/public-landing/public/guild.html +1 -1
  255. package/modules/public-landing/public/invitations.html +2 -2
  256. package/modules/public-landing/public/projects.html +5 -5
  257. package/modules/raise/views/nda.html +2 -0
  258. package/modules/specialities/db.js +32 -1
  259. package/modules/specialities/routes/specialities.js +10 -6
  260. package/modules/status-ui/public/index.html +1 -1
  261. package/package-lock.json +2 -2
  262. package/package.json +1 -1
  263. package/release-notes.json +286 -0
  264. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  265. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  266. package/scripts/bongos/claim.js +22 -1
  267. package/scripts/bongos/db-backup-nightly.sh +29 -0
  268. package/scripts/bongos/deploy-divergence.js +7 -0
  269. package/scripts/bongos/doc-cli-guard.js +6 -0
  270. package/scripts/bongos/fitness.js +3 -2
  271. package/scripts/bongos/gen-diagrams.js +3 -3
  272. package/scripts/bongos/lib.sh +4 -5
  273. package/scripts/bongos/migration-namespace.js +13 -1
  274. package/scripts/bongos/plain-cards.js +4 -0
  275. package/scripts/bongos/provision-github-setup.js +3 -1
  276. package/scripts/bongos/provision-units.js +6 -1
  277. package/scripts/bongos/publish-manifest.js +4 -0
  278. package/scripts/bongos/release.js +2 -0
  279. package/scripts/bongos/run-unit-tests.js +268 -82
  280. package/scripts/bongos/seed-tasks.example.json +56 -0
  281. package/scripts/bongos/seed-tasks.js +222 -0
  282. package/scripts/bongos/seed-verify-criteria.js +1 -2
  283. package/scripts/bongos/ship-finish.js +2 -0
  284. package/scripts/bongos/ship-resume.js +2 -0
  285. package/scripts/bongos/strand-lease.js +250 -0
  286. package/scripts/bongos/strand-watch.js +129 -12
  287. package/scripts/bongos/version-literal-guard.js +2 -2
  288. package/scripts/discord/post.js +4 -3
  289. package/scripts/gds/seed-tasks.js +7 -0
  290. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  291. package/scripts/hall-preview/server.js +21 -0
  292. package/src/bongos/auth-config.js +29 -1
  293. package/src/bongos/auth-discord.js +3 -3
  294. package/src/bongos/auth-github.js +3 -6
  295. package/src/bongos/auth-google.js +217 -0
  296. package/src/bongos/device-approval.js +140 -0
  297. package/src/bongos/llm-pricing.js +17 -1
  298. package/src/bongos/metrics.js +48 -2
  299. package/src/bongos/middleware/rate-limit.js +30 -11
  300. package/src/bongos/module-scope-map.js +16 -1
  301. package/src/bongos/optimizer.js +15 -1
  302. package/src/bongos/payout-approvals.js +342 -0
  303. package/src/bongos/payout-gates.js +364 -0
  304. package/src/bongos/prelaunch-gate.js +7 -9
  305. package/src/bongos/prelaunch-switch.js +20 -0
  306. package/src/bongos/route-rank-check.js +3 -0
  307. package/src/bongos/routes/auth-google.js +246 -0
  308. package/src/bongos/routes/auth.js +38 -1
  309. package/src/bongos/routes/backup.js +33 -9
  310. package/src/bongos/routes/builders.js +57 -30
  311. package/src/bongos/routes/github.js +9 -0
  312. package/src/bongos/routes/me.js +18 -0
  313. package/src/bongos/routes/my-payouts.js +133 -0
  314. package/src/bongos/routes/payout-approvals.js +312 -0
  315. package/src/bongos/routes/payout-gates.js +179 -0
  316. package/src/bongos/routes.js +15 -1
  317. package/src/bongos/secret-box.js +18 -3
  318. package/src/bongos/serve-internal.js +47 -19
  319. package/src/bongos/sign-in-pages.js +138 -0
  320. package/src/bongos/task-needs.js +146 -0
  321. package/src/bongos-downloads.js +18 -1
  322. package/src/instance-config.js +28 -0
  323. package/src/module-api.js +14 -1
  324. package/src/platform-server.js +27 -1
  325. package/tests/account_logins.mjs +326 -0
  326. package/tests/account_logins_db.mjs +161 -0
  327. package/tests/ai_studio_billing.mjs +253 -0
  328. package/tests/ai_studio_generate.mjs +282 -0
  329. package/tests/ai_studio_keys.mjs +159 -0
  330. package/tests/ai_studio_requests.mjs +319 -0
  331. package/tests/auth_backchannel_logout_route.mjs +201 -0
  332. package/tests/auto_satisfy_criteria.mjs +10 -1
  333. package/tests/autobongos_tweak_lane.mjs +144 -0
  334. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  335. package/tests/backfill_subagent_rewards.mjs +17 -1
  336. package/tests/backup_trigger_lock.mjs +119 -0
  337. package/tests/batched_writes_parity.mjs +340 -0
  338. package/tests/blockers_people.mjs +198 -0
  339. package/tests/bounded_rate_maps.mjs +184 -0
  340. package/tests/claimable_blocker_status.mjs +1 -1
  341. package/tests/claims_batch_routes.mjs +421 -0
  342. package/tests/conductor_main_e2e.mjs +4 -0
  343. package/tests/config_read_cache.mjs +171 -0
  344. package/tests/copy_desk_page_editor.mjs +12 -0
  345. package/tests/cost_write_guard.mjs +200 -0
  346. package/tests/craft_achievements.mjs +33 -10
  347. package/tests/criterion_goal_attach.mjs +1 -1
  348. package/tests/db_backup_nightly.mjs +73 -1
  349. package/tests/deploy_divergence_line.mjs +7 -0
  350. package/tests/effective_visibility_predicate.mjs +12 -0
  351. package/tests/fitness.mjs +19 -0
  352. package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
  353. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  354. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  355. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  356. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  357. package/tests/github_one_trip.mjs +34 -0
  358. package/tests/github_repo_visibility.mjs +20 -0
  359. package/tests/goal_achievement.mjs +30 -1
  360. package/tests/goal_achievement_reward.mjs +205 -0
  361. package/tests/goal_archive_disposition.mjs +5 -5
  362. package/tests/goal_sort_staleness.mjs +2 -1
  363. package/tests/goal_tier.mjs +11 -1
  364. package/tests/google_fonts_nonblocking.mjs +84 -0
  365. package/tests/google_sign_in.mjs +401 -0
  366. package/tests/government_board_expiry.mjs +94 -0
  367. package/tests/government_protected_surfaces.mjs +1 -1
  368. package/tests/government_routes.mjs +146 -23
  369. package/tests/grader.mjs +31 -0
  370. package/tests/hall_ai_studio.mjs +347 -0
  371. package/tests/hall_city.mjs +8 -0
  372. package/tests/hall_page_gate_map.mjs +3 -0
  373. package/tests/hall_studio_home.mjs +61 -2
  374. package/tests/home_attention_cards.mjs +1 -1
  375. package/tests/hook_shell_tools.mjs +8 -0
  376. package/tests/hooks_drain.mjs +1 -1
  377. package/tests/host_topology_skips.mjs +2 -0
  378. package/tests/howto_moments.mjs +17 -0
  379. package/tests/human_signal.mjs +141 -0
  380. package/tests/install_script_reader.mjs +51 -0
  381. package/tests/integration_fixture_ids.mjs +82 -0
  382. package/tests/invite_expiry_db.mjs +13 -1
  383. package/tests/lib_sh_resolution.mjs +0 -4
  384. package/tests/market_watch_data.mjs +244 -0
  385. package/tests/market_watch_data_db.mjs +308 -0
  386. package/tests/market_watch_module.mjs +114 -0
  387. package/tests/market_watch_run.mjs +503 -0
  388. package/tests/metrics.mjs +124 -0
  389. package/tests/migration_namespace.mjs +14 -0
  390. package/tests/module_api.mjs +2 -0
  391. package/tests/module_loader.mjs +1 -1
  392. package/tests/module_skills_hook.mjs +69 -0
  393. package/tests/notes.mjs +182 -0
  394. package/tests/own_scope_guard.mjs +5 -0
  395. package/tests/page_tweak_marker_sql.mjs +76 -0
  396. package/tests/page_tweak_no_block.mjs +135 -0
  397. package/tests/page_tweak_notice.mjs +103 -0
  398. package/tests/payout_approvals.mjs +253 -0
  399. package/tests/payout_approvals_db.mjs +106 -0
  400. package/tests/payout_gates.mjs +322 -0
  401. package/tests/payout_gates_db.mjs +113 -0
  402. package/tests/planning_session_skill.mjs +13 -3
  403. package/tests/project_settings_page.mjs +29 -5
  404. package/tests/project_settings_spend_cap_route.mjs +235 -0
  405. package/tests/promotion_sweep.mjs +29 -1
  406. package/tests/provision.mjs +3 -2
  407. package/tests/provisioning_app_status.mjs +18 -0
  408. package/tests/provisioning_credential_preflight.mjs +53 -0
  409. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  410. package/tests/public_landing_projects.mjs +37 -0
  411. package/tests/publish_reconciler_overlap.mjs +60 -0
  412. package/tests/rank_change_broadcast_port.mjs +151 -0
  413. package/tests/seed_tasks.mjs +176 -0
  414. package/tests/seed_verify_criteria.mjs +2 -6
  415. package/tests/session_start_freshness.mjs +66 -1
  416. package/tests/shell_hooks.mjs +209 -0
  417. package/tests/ship_assurance.mjs +28 -0
  418. package/tests/ship_card.mjs +20 -5
  419. package/tests/site_classified_once.mjs +142 -0
  420. package/tests/speciality_db_shape.mjs +23 -0
  421. package/tests/speciality_session_skills.mjs +48 -2
  422. package/tests/strand_lease.mjs +249 -0
  423. package/tests/strand_watch.mjs +58 -0
  424. package/tests/task_criteria_routes.mjs +275 -0
  425. package/tests/task_criteria_unlink_db.mjs +123 -0
  426. package/tests/task_needs.mjs +233 -0
  427. package/tests/task_needs_db.mjs +110 -0
  428. package/tests/unit_runner_serial.mjs +266 -43
  429. package/tests/version_close_route.mjs +1 -1
  430. package/tests/version_literals.mjs +5 -7
  431. package/tests/welcome_boot.mjs +16 -5
  432. package/tests/welcome_boot_browser.mjs +36 -4
  433. package/docs/recipes/bongos-cli-release.md +0 -66
  434. package/scripts/bongos/backfill-disciplines.js +0 -144
  435. package/scripts/bongos/backfill-session-earnings.js +0 -123
  436. package/scripts/bongos/backfill-session-types.js +0 -87
  437. package/scripts/bongos/build-bongos.js +0 -94
  438. package/scripts/bongos/cleanup-orphans.js +0 -74
  439. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  440. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  441. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  442. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  443. package/scripts/bongos/smoke-batch-release.sh +0 -316
  444. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  445. package/scripts/bongos/smoke-dependencies.sh +0 -143
  446. package/scripts/gds/backfill-disciplines.js +0 -7
  447. package/scripts/gds/backfill-session-earnings.js +0 -7
  448. package/scripts/gds/backfill-session-types.js +0 -7
  449. package/scripts/gds/cleanup-orphans.js +0 -7
  450. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  451. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  452. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  453. package/scripts/gds/smoke-batch-claim.sh +0 -8
  454. package/scripts/gds/smoke-batch-release.sh +0 -8
  455. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  456. package/scripts/gds/smoke-dependencies.sh +0 -8
  457. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -15,6 +15,8 @@ const { pool, withTx } = api;
15
15
  const log = api.logger('lifecycle');
16
16
  const seams = api;
17
17
  const { touchesOverlap } = api.pathMatch;
18
+ const { notPageMarkerSql } = require('./page-tweak-artist.js');
19
+ const { heldPagesForTouches } = require('./page-notice.js');
18
20
  // CLAIM_ANY / CLAIM_NEWCOMER name the two atoms in the refusal message. The
19
21
  // decision itself moved to db-rank-authz.claimGate (task 1003196), so this file
20
22
  // no longer calls claimEligibilityAllows / rankAllowsTask / xenosClaimAllowed
@@ -27,6 +29,9 @@ const { goalMembershipFailures } = require('./claim-feed.js');
27
29
  // called from all three claim surfaces below, so a flagged task can't be
28
30
  // claimed via the batch path as a bypass of the single-claim gate.
29
31
  const { relevanceFrictionFailures } = require('./db-relevance-flags.js');
32
+ // task 1004950 / ADR 0395: a task that needs server access refuses a claimer
33
+ // without the grant — the third extracted gate in the same shape.
34
+ const { taskNeedsFailures } = require('./task-needs-gate.js');
30
35
  const { unsatisfiedDeps } = require('./db-deps-criteria.js');
31
36
  // depRefusalRow: the ONE shape a DEPS_NOT_SHIPPED refusal carries. All three
32
37
  // claim paths below use it — hand-building the object here is what made task
@@ -251,6 +256,13 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
251
256
  const relevanceRefusals = await relevanceFrictionFailures({ client, tasks: [task] });
252
257
  if (relevanceRefusals.length > 0) throw relevanceRefusals[0].refusal;
253
258
 
259
+ // task 1004950 / ADR 0395: a task that needs the live server refuses a
260
+ // claimer without the server grant. Same extracted check on all three
261
+ // surfaces (task-needs-gate.js). `owner` never refuses here — the feed is
262
+ // where owner work narrows to the owner.
263
+ const needsRefusals = await taskNeedsFailures({ client, tasks: [task], builderId });
264
+ if (needsRefusals.length > 0) throw needsRefusals[0].refusal;
265
+
254
266
  // The claim gate (ADR 0015, generalized in task 1516 / ADR 0086 §5): a task
255
267
  // can't be claimed while any dependency is un-done — its OWN edges plus those
256
268
  // inherited from its goal/criteria, where "done" = task shipped | criterion
@@ -300,7 +312,7 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
300
312
  const { rows: activeClaimRows } = await client.query(
301
313
  `SELECT c.id, t.id AS task_id, t.title, t.touches, t.module_key FROM claims c
302
314
  JOIN tasks t ON t.id = c.task_id
303
- WHERE c.released_at IS NULL AND t.id <> $1`,
315
+ WHERE c.released_at IS NULL AND t.id <> $1 AND ${notPageMarkerSql('c', 't')}`,
304
316
  [taskId]
305
317
  );
306
318
  // BV1.R53 (task 1422): module-disjointness is a HARD gate (not advisory) when
@@ -460,7 +472,10 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
460
472
  console.log('[gds] graduation broadcast post-claim failed:', e && e.message);
461
473
  }
462
474
  }
463
- return { ...insertClaim.rows[0], just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingWarning };
475
+ // task 1004948: a gentle heads-up, never a refusal, when this task's files belong
476
+ // to a page an artist is updating. Read after the commit; a failed read is "none".
477
+ const pageNotice = await heldPagesForTouches(task.touches, client, { exceptTaskId: taskId });
478
+ return { ...insertClaim.rows[0], just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingWarning, page_notice: pageNotice };
464
479
  } catch (err) {
465
480
  await client.query('ROLLBACK').catch(() => {});
466
481
  if (err && err.code === '23505') {
@@ -658,6 +673,12 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
658
673
  failures.push({ task_id: f.task_id, reason: code, ...rest });
659
674
  }
660
675
 
676
+ // task 1004950: the SAME extracted server-access gate as claimTask.
677
+ for (const f of await taskNeedsFailures({ client, tasks: taskRows, builderId })) {
678
+ const { code, ...rest } = f.refusal;
679
+ failures.push({ task_id: f.task_id, reason: code, ...rest });
680
+ }
681
+
661
682
  // Per-task dependency check (ADR 0015, generalized task 1516). An EXTERNAL dep
662
683
  // (not in this batch) must already be done; a batch-internal dep isn't done yet
663
684
  // at claim time but doesn't block — the batch is worked serially. "Internal"
@@ -724,7 +745,8 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
724
745
  FROM claims c
725
746
  JOIN tasks t ON t.id = c.task_id
726
747
  WHERE c.released_at IS NULL
727
- AND t.id <> ALL($1::bigint[])`,
748
+ AND t.id <> ALL($1::bigint[])
749
+ AND ${notPageMarkerSql('c', 't')}`,
728
750
  [uniqIds]
729
751
  );
730
752
  for (const t of taskRows) {
@@ -966,7 +988,7 @@ async function validateClaimBatch({ taskIds, builderId }) {
966
988
  const { rows: activeClaims } = await pool.query(
967
989
  `SELECT c.id AS claim_id, t.title, t.touches, t.module_key
968
990
  FROM claims c JOIN tasks t ON t.id = c.task_id
969
- WHERE c.released_at IS NULL AND t.id <> ALL($1::bigint[])`,
991
+ WHERE c.released_at IS NULL AND t.id <> ALL($1::bigint[]) AND ${notPageMarkerSql('c', 't')}`,
970
992
  [uniqIds]
971
993
  );
972
994
 
@@ -1014,6 +1036,12 @@ async function validateClaimBatch({ taskIds, builderId }) {
1014
1036
  push(f.task_id, { reason: code, ...rest });
1015
1037
  }
1016
1038
 
1039
+ // task 1004950: server-access, kept in sync with claimTask + claimTasksBatch.
1040
+ for (const f of await taskNeedsFailures({ client: pool, tasks: taskRows, builderId })) {
1041
+ const { code, ...rest } = f.refusal;
1042
+ push(f.task_id, { reason: code, ...rest });
1043
+ }
1044
+
1017
1045
  // Dependencies — external to the batch only (a batch-internal dep is fine;
1018
1046
  // the batch is worked serially, same rule as claimTasksBatch).
1019
1047
  const batchIdSet = new Set(uniqIds);
@@ -16,7 +16,7 @@ const doneWhen = require('./done-when.js');
16
16
  const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
17
17
  const { nonTerminalSql, isTerminalStatus } = require('./task-status');
18
18
  const { maintenanceGoalTitle } = require('./goal-advisory.js');
19
- const { SMOKE_NOT_LIKE } = require('./db-shared.js');
19
+ const { SMOKE_NOT_LIKE, idKey } = require('./db-shared.js');
20
20
  const { executorFor, inTransaction } = require('./db-executor.js');
21
21
  // Task 1003660 — closing a goal cancels its pending invites + join requests.
22
22
  const { cancelPendingMembershipRequests } = require('./goal-close-sweep.js');
@@ -487,14 +487,19 @@ async function reorderGoals({ versionId, orderedIds }, deps = {}) {
487
487
  // FOR UPDATE on its transaction client, and this UPDATE on a pool connection
488
488
  // blocks on that lock — while the transaction is awaiting this UPDATE before it
489
489
  // can COMMIT and release it. Neither side can move.
490
- async function setGoalStatus({ goalId, status }, deps = {}) {
490
+ //
491
+ // `fromStatus` (task 1001693) makes the move conditional: the row flips only if it
492
+ // still reads that status, and null comes back otherwise. The achieve path uses it
493
+ // so exactly one caller wins a goal's flip to `achieved`, and only the winner books
494
+ // the goal-achievement karma.
495
+ async function setGoalStatus({ goalId, status, fromStatus = null }, deps = {}) {
491
496
  const activePool = executorFor(deps, pool);
492
497
  const { rows } = await activePool.query(
493
498
  `UPDATE goals
494
499
  SET status = $2, updated_at = now()
495
- WHERE id = $1
500
+ WHERE id = $1 AND ($3::text IS NULL OR status = $3::text)
496
501
  RETURNING ${GOAL_COLS}`,
497
- [goalId, status]
502
+ [goalId, status, fromStatus]
498
503
  );
499
504
  // A goal leaving `open` takes no new members, so its pending asks are dead.
500
505
  if (rows[0] && status !== 'open') await cancelPendingMembershipRequests(activePool, goalId);
@@ -547,7 +552,16 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
547
552
  // This never blocks a SHIP; it declines only the goal's flip to achieved, and
548
553
  // the goal simply stays open until its last task lands or is abandoned.
549
554
  if (unfinishedTasks > 0) return { achieved: false, goal, unfinishedTasks };
550
- const updated = await setGoalStatus({ goalId, status: 'achieved' }, deps);
555
+ // Conditional on still reading `open` (task 1001693): two overrides closing a
556
+ // goal's last criteria at once both reach here, and only one may flip it and
557
+ // book its karma. The loser reports the goal as achieved, which it now is, and
558
+ // leaves the reward and the version close to the winner.
559
+ const updated = await setGoalStatus({ goalId, status: 'achieved', fromStatus: 'open' }, deps);
560
+ if (!updated) return { achieved: true, goal: await getGoal(goalId, { ...deps, forUpdate: false }), versionsClosed: [] };
561
+ const exec = executorFor(deps, pool);
562
+ // The same reward the ship-time flip books, through the same helper, so a failed
563
+ // booking is logged the same way on both doors.
564
+ await doneWhen.rewardAchievedGoals(exec, [updated]);
551
565
 
552
566
  // BV1.R16 (task 1003603, ADR 0263 §§3–4): the SECOND achievement writer. A goal
553
567
  // reaching `achieved` may have been the version's last non-maintenance goal, and
@@ -559,7 +573,6 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
559
573
  //
560
574
  // Runs on the caller's exec so it sees the flip above, which in the ship path is
561
575
  // still uncommitted. Never throws: autoCloseVersionsForGoals catches per version.
562
- const exec = executorFor(deps, pool);
563
576
  const versionsClosed = await autoCloseVersionsForGoals(exec, [updated]);
564
577
  return { achieved: true, goal: updated, versionsClosed };
565
578
  }
@@ -733,28 +746,43 @@ async function archiveGoalWithDispositions({ goalId, plan = [], reason }, deps =
733
746
  return { goal: goalRows[0], applied: [], alreadyArchived: true };
734
747
  }
735
748
 
736
- const applied = [];
737
- for (const step of plan) {
749
+ // task 1004929: every plan task is locked and read in ONE statement, and the
750
+ // active claims on them in one more, instead of two round trips per step. The
751
+ // locks are taken in id order (no deadlock against another batch), then the
752
+ // plan is walked in memory in PLAN order, so the applied list and the first
753
+ // error are what the per-step loop produced. `inGoal` tracks this call's own
754
+ // writes, so a task named twice reads the state the first step left behind.
755
+ const inGoal = new Map();
756
+ const claimedIds = new Set();
757
+ if (plan.length > 0) {
758
+ const ids = plan.map((step) => step.taskId);
738
759
  const { rows: tRows } = await client.query(
739
- `SELECT id, title, status FROM tasks WHERE id = $1 AND goal_id = $2 FOR UPDATE`,
740
- [step.taskId, goalId]
760
+ `SELECT id, title, status FROM tasks WHERE id = ANY($1::bigint[]) AND goal_id = $2 ORDER BY id FOR UPDATE`,
761
+ [ids, goalId]
762
+ );
763
+ for (const row of tRows) inGoal.set(idKey(row.id), row);
764
+ const { rows: claimed } = await client.query(
765
+ `SELECT DISTINCT task_id FROM claims WHERE task_id = ANY($1::bigint[]) AND released_at IS NULL`, [ids]
741
766
  );
767
+ for (const row of claimed) claimedIds.add(idKey(row.task_id));
768
+ }
769
+
770
+ const applied = [];
771
+ for (const step of plan) {
772
+ const task = inGoal.get(idKey(step.taskId));
742
773
  // The task left this goal (or shipped) while the caller was deciding. Not an
743
774
  // error: the disposition is moot, and the final re-count is what actually
744
775
  // guards the invariant. Recorded as 'skipped' so the response tells the
745
776
  // truth about what this call did rather than claiming a write it never made.
746
- if (!tRows[0]) { applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_not_in_goal' }); continue; }
747
- if (isTerminalStatus(tRows[0].status)) {
748
- applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_already_terminal', status: tRows[0].status });
777
+ if (!task) { applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_not_in_goal' }); continue; }
778
+ if (isTerminalStatus(task.status)) {
779
+ applied.push({ task_id: String(step.taskId), verb: step.verb, result: 'skipped_already_terminal', status: task.status });
749
780
  continue;
750
781
  }
751
782
 
752
- const { rows: claimed } = await client.query(
753
- `SELECT id FROM claims WHERE task_id = $1 AND released_at IS NULL LIMIT 1`, [step.taskId]
754
- );
755
- if (claimed[0]) {
783
+ if (claimedIds.has(idKey(step.taskId))) {
756
784
  throw Object.assign(new Error('task has an active claim'), {
757
- code: 'TASK_HAS_ACTIVE_CLAIM', taskId: String(step.taskId), taskTitle: tRows[0].title,
785
+ code: 'TASK_HAS_ACTIVE_CLAIM', taskId: String(step.taskId), taskTitle: task.title,
758
786
  });
759
787
  }
760
788
 
@@ -766,6 +794,7 @@ async function archiveGoalWithDispositions({ goalId, plan = [], reason }, deps =
766
794
  WHERE id = $1`,
767
795
  [step.taskId, `Abandoned: ${reason}`]
768
796
  );
797
+ task.status = 'abandoned';
769
798
  applied.push({ task_id: String(step.taskId), verb: 'abandon', result: 'abandoned' });
770
799
  } else {
771
800
  const { rows: dest } = await client.query(
@@ -780,6 +809,7 @@ async function archiveGoalWithDispositions({ goalId, plan = [], reason }, deps =
780
809
  });
781
810
  }
782
811
  await client.query(`UPDATE tasks SET goal_id = $2, updated_at = now() WHERE id = $1`, [step.taskId, step.toGoalId]);
812
+ if (idKey(step.toGoalId) !== idKey(goalId)) inGoal.delete(idKey(step.taskId));
783
813
  applied.push({ task_id: String(step.taskId), verb: 'move', result: 'moved', to_goal_id: String(step.toGoalId) });
784
814
  }
785
815
  }
@@ -104,10 +104,19 @@ async function mostRecentClaimHolderId(client, taskId) {
104
104
  return rows[0]?.builder_id ?? null;
105
105
  }
106
106
 
107
+ // One spelling of a bigint id as a Map key (task 1004929). pg returns bigint
108
+ // columns as strings while a plan may carry numbers, so a batched locked read
109
+ // (the goal archive, the version close) matches its rows back to the plan steps
110
+ // through this on both sides. A value BigInt cannot read keeps its own string.
111
+ function idKey(v) {
112
+ try { return BigInt(v).toString(); } catch { return String(v); }
113
+ }
114
+
107
115
  module.exports = {
108
116
  SMOKE_NOT_LIKE,
109
117
  gradePassCount,
110
118
  gradeQualityWhere,
119
+ idKey,
111
120
  median,
112
121
  mostRecentClaimHolderId,
113
122
  sealSessionLog,
@@ -22,6 +22,8 @@ const { isReservedCriterionSlug } = require('./criterion-ref.js');
22
22
  const { resolveWriteDiscipline, WRITABLE_DISCIPLINES } = require('./task-classifier.js');
23
23
  const { gateOnSpecTask } = require('./goal-spec-task.js');
24
24
  const { ensureArtistReviewGoal } = require('./artist-review-goal.js');
25
+ const { notPageMarkerSql } = require('./page-tweak-artist.js');
26
+ const { needsFilterWhere, capabilityFeedWhere } = require('./task-needs-gate.js');
25
27
 
26
28
  // -------------------------------------------------------------------------
27
29
  // Tasks
@@ -147,6 +149,7 @@ async function createTask({
147
149
  // (requiresRank stays null for the goal vector → derived purely from touches +
148
150
  // sensitivity), so goal_id never influences the requires_rank clamp.
149
151
  goalId = null,
152
+ needs = [], // task 1004950 / ADR 0395 — ⊆ ['owner','server']; routes normalize, the CHECK backstops
150
153
  // task 1003278: WHO created it — the builder id from the authenticated request.
151
154
  // Nullable, appended at the end (the goals bind-order convention): a system
152
155
  // path with no builder in the request (a seed, a migration backfill) passes
@@ -267,13 +270,13 @@ async function createTask({
267
270
  est_minutes, est_cost_usd, manual_degree, priority,
268
271
  automation_tag, credits_reward, source, source_ref, value_summary,
269
272
  kind, discipline, parallel_safe, newcomer_friendly, needs_migration,
270
- security_sensitive, requires_rank, module_key, goal_id, created_by, promoted_at)
273
+ security_sensitive, requires_rank, module_key, goal_id, created_by, promoted_at, needs)
271
274
  VALUES ($1, $2, $3, $4, $5,
272
275
  $6, $7, $8, $9,
273
276
  $10, $11, $12, $13, $14,
274
277
  $15, $16, $17, $18, $19,
275
278
  $20, $21, $22, $23, $24,
276
- CASE WHEN $4 IN ('ready','active') THEN now() ELSE NULL END)
279
+ CASE WHEN $4 IN ('ready','active') THEN now() ELSE NULL END, $25)
277
280
  RETURNING *`,
278
281
  [
279
282
  versionId,
@@ -300,6 +303,7 @@ async function createTask({
300
303
  moduleKey ?? null,
301
304
  goalIdEffective ?? null,
302
305
  createdBy ?? null,
306
+ Array.isArray(needs) ? needs : [],
303
307
  ]
304
308
  );
305
309
  // task 1677 / ADR 0096: audit each creation-time auto-assignment so the trail is
@@ -357,7 +361,7 @@ async function findTaskBySourceRef({ source, sourceRef } = {}, deps = {}) {
357
361
 
358
362
  const TASK_SETTER_COLUMNS = new Set([
359
363
  'kind', 'discipline', 'newcomer_friendly', 'security_sensitive', 'parallel_safe', 'automation_tag',
360
- 'needs_migration', 'module_key', 'description', 'title', 'priority', 'value_summary',
364
+ 'needs_migration', 'module_key', 'description', 'title', 'priority', 'value_summary', 'needs',
361
365
  ]);
362
366
 
363
367
  async function updateTaskColumn(id, column, value) {
@@ -647,6 +651,7 @@ async function updateTaskParallelSafe(id, parallelSafe) { return updateTaskColum
647
651
  // and leave the tag unset, then later judge that a task is safe to delegate.
648
652
 
649
653
  async function updateTaskAutomationTag(id, automationTag) { return updateTaskColumn(id, 'automation_tag', automationTag); }
654
+ async function updateTaskNeeds(id, needs) { return updateTaskColumn(id, 'needs', needs); }
650
655
 
651
656
  // task 1002648: correct the create-time migration heuristic's verdict — a false
652
657
  // flag makes claiming reserve a migration number that goes unused (task 1002614
@@ -777,7 +782,7 @@ async function getTask(id) {
777
782
  }
778
783
 
779
784
 
780
- async function listTasks({ versionId, status, kind, discipline, goalId, createdBy, staleDays, limit, offset } = {}) {
785
+ async function listTasks({ versionId, status, kind, discipline, goalId, createdBy, staleDays, limit, offset, needs } = {}) {
781
786
  const params = [];
782
787
  const where = [];
783
788
  if (versionId) {
@@ -800,6 +805,7 @@ async function listTasks({ versionId, status, kind, discipline, goalId, createdB
800
805
  params.push(goalId);
801
806
  where.push(`goal_id = $${params.length}`);
802
807
  }
808
+ const needsWhere = needsFilterWhere(needs, '', params); if (needsWhere) where.push(needsWhere); // task 1004950
803
809
  // task 1003278 — the creator + staleness axes, server-side. They exist HERE and
804
810
  // not only in the browser because the board was reducing the whole table to
805
811
  // answer them: GET /tasks?status=backlog measured 690,625 bytes on 2026-08-25,
@@ -1015,6 +1021,7 @@ const CLAIMABLE_SQL = `
1015
1021
  SELECT 1 FROM claims c
1016
1022
  JOIN tasks t2 ON t2.id = c.task_id
1017
1023
  WHERE c.released_at IS NULL
1024
+ AND ${notPageMarkerSql('c', 't2')}
1018
1025
  AND t2.touches && t.touches
1019
1026
  )`;
1020
1027
  const SESSION_FIT_SQL = `
@@ -1028,7 +1035,7 @@ const SESSION_FIT_SQL = `
1028
1035
  END AS fits_session_modes
1029
1036
  FROM tasks t`;
1030
1037
 
1031
- async function listClaimableTasks({ versionId = null, builderId = null, discipline = null, fit = null, createdBy = null, staleDays = null } = {}) {
1038
+ async function listClaimableTasks({ versionId = null, builderId = null, discipline = null, fit = null, createdBy = null, staleDays = null, needs = null } = {}) {
1032
1039
  const params = [];
1033
1040
  const where = [];
1034
1041
  let goalJoin = '';
@@ -1037,6 +1044,7 @@ async function listClaimableTasks({ versionId = null, builderId = null, discipli
1037
1044
  params.push(versionId);
1038
1045
  where.push(`v.version_id = $${params.length}`);
1039
1046
  }
1047
+ const needsWhere = needsFilterWhere(needs, 'v', params); if (needsWhere) where.push(needsWhere); // task 1004950: the 'waiting on you' queue
1040
1048
  if (discipline) {
1041
1049
  params.push(discipline);
1042
1050
  where.push(`v.discipline = $${params.length}`);
@@ -1110,6 +1118,7 @@ async function listClaimableTasks({ versionId = null, builderId = null, discipli
1110
1118
  selectCols = `v.*, g.visibility AS g_visibility, g.scope_modules AS g_scope_modules, g.title AS g_title,
1111
1119
  (gm.builder_id IS NOT NULL
1112
1120
  OR (SELECT rank FROM builders WHERE id = $${params.length}) = 'archon') AS g_is_member`;
1121
+ where.push(await capabilityFeedWhere(builderId, fit, 'v', params)); // task 1004950 / ADR 0395: only what the viewer can do
1113
1122
  }
1114
1123
  // Autonomy / parallel-eligibility filter. Joining the session-fit relation
1115
1124
  // (also a SELECT t.* shape) makes bare column names like priority/created_at
@@ -1470,7 +1479,7 @@ module.exports = {
1470
1479
  suggestCreditsReward,
1471
1480
  taskTouchesProtectedPath,
1472
1481
  updateCriterionGoal,
1473
- updateTaskAutomationTag,
1482
+ updateTaskAutomationTag, updateTaskNeeds,
1474
1483
  updateTaskDescription,
1475
1484
  updateTaskTitle,
1476
1485
  updateTaskDiscipline,
@@ -14,7 +14,7 @@ const api = require('../../src/module-api');
14
14
  const { pool, withTx } = api;
15
15
  const doneWhen = require('./done-when.js');
16
16
  const { nonTerminalSql } = require('./task-status');
17
- const { SMOKE_NOT_LIKE } = require('./db-shared.js');
17
+ const { SMOKE_NOT_LIKE, idKey } = require('./db-shared.js');
18
18
  const { executorFor, inTransaction } = require('./db-executor.js');
19
19
  // db-goals.js does NOT require this file, so this direction adds no require cycle
20
20
  // (fitness pins the count). createVersion needs it to seed a version's FIRST goal
@@ -372,18 +372,29 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
372
372
  });
373
373
  }
374
374
 
375
- const applied = [];
376
- for (const step of plan) {
375
+ // task 1004929: every plan goal is locked and read in ONE statement instead of
376
+ // one round trip per step. Locks are taken in id order (no deadlock against
377
+ // another batch); the plan is then walked in memory in PLAN order, so the
378
+ // applied list is what the per-step loop produced. Each step's own archive is
379
+ // mirrored into `onVersion`, so a goal named twice reads 'archived' second time.
380
+ const onVersion = new Map();
381
+ if (plan.length > 0) {
377
382
  const { rows: gRows } = await client.query(
378
- `SELECT id, title, status FROM goals WHERE id = $1 AND version_id = $2 FOR UPDATE`,
379
- [step.goalId, versionId]
383
+ `SELECT id, title, status FROM goals WHERE id = ANY($1::bigint[]) AND version_id = $2 ORDER BY id FOR UPDATE`,
384
+ [plan.map((step) => step.goalId), versionId]
380
385
  );
386
+ for (const row of gRows) onVersion.set(idKey(row.id), row);
387
+ }
388
+
389
+ const applied = [];
390
+ for (const step of plan) {
391
+ const goal = onVersion.get(idKey(step.goalId));
381
392
  // The goal moved or closed itself while the caller was deciding. Not an
382
393
  // error — the final re-count is what guards the invariant — but recorded so
383
394
  // the response says what this call actually did.
384
- if (!gRows[0]) { applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_not_on_version' }); continue; }
385
- if (gRows[0].status !== 'open') {
386
- applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_already_closed', status: gRows[0].status });
395
+ if (!goal) { applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_not_on_version' }); continue; }
396
+ if (goal.status !== 'open') {
397
+ applied.push({ goal_id: String(step.goalId), verb: step.verb, result: 'skipped_already_closed', status: goal.status });
387
398
  continue;
388
399
  }
389
400
 
@@ -410,6 +421,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
410
421
  [step.goalId, `Abandoned: ${reason}`]
411
422
  );
412
423
  await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
424
+ Object.assign(goal, { status: 'archived' }); // mirror the write above for a goal named twice
413
425
  await cancelPendingMembershipRequests(client, step.goalId);
414
426
  applied.push({ goal_id: String(step.goalId), verb: 'abandon', result: 'archived', tasks_abandoned: killed.length });
415
427
  } else {
@@ -502,6 +514,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
502
514
  // on the way out rather than being special-cased around it (the brief's
503
515
  // explicit ask).
504
516
  await client.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [step.goalId]);
517
+ Object.assign(goal, { status: 'archived' }); // mirror the write above for a goal named twice
505
518
  await cancelPendingMembershipRequests(client, step.goalId);
506
519
 
507
520
  applied.push({
@@ -23,7 +23,7 @@
23
23
  // POST /api/bongos/done-when/:criterionId/satisfy — auth
24
24
  // POST /api/bongos/done-when/:criterionId/unsatisfy — auth
25
25
 
26
- const { pool } = require('../../src/module-api');
26
+ const { pool, resolveOptional } = require('../../src/module-api');
27
27
  const { nonTerminalSql } = require('./task-status');
28
28
  const { SMOKE_NOT_LIKE } = require('./db-shared.js');
29
29
  const { executorFor } = require('./db-executor.js');
@@ -618,9 +618,30 @@ async function achieveCompletedGoals(exec, goalIds) {
618
618
  // Task 1003660: an achieved goal takes no new members — close its pending asks
619
619
  // on the same executor, so the cancel rides the ship transaction.
620
620
  await cancelPendingMembershipRequests(exec, rows.map((g) => g.id));
621
+ await rewardAchievedGoals(exec, rows);
621
622
  return rows;
622
623
  }
623
624
 
625
+ // rewardAchievedGoals — the goal-achievement karma (task 1001693 / BV1.P5.3, ADR
626
+ // 0101 Part 3), booked through the economy's `reward` port for each goal THIS
627
+ // statement flipped. Only the caller whose UPDATE ... WHERE status = 'open'
628
+ // returned the row gets here, so a concurrent ship or sweep cannot award twice.
629
+ // Optional and best-effort: no economy module means no karma, and the award runs
630
+ // under its own SAVEPOINT (goal-reward.js) so a failed karma write cannot abort
631
+ // the ship that achieved the goal. The catch here is for a provider that throws.
632
+ async function rewardAchievedGoals(exec, goals) {
633
+ const reward = resolveOptional('reward');
634
+ if (!reward || typeof reward.awardGoalAchievementKarma !== 'function') return;
635
+ for (const g of goals) {
636
+ try {
637
+ const r = await reward.awardGoalAchievementKarma(exec, { goalId: g.id });
638
+ if (r && r.reason === 'failed') console.warn(`[lifecycle] goal ${g.id} achieved; its karma was not booked: ${r.error}`);
639
+ } catch (e) {
640
+ console.warn(`[lifecycle] goal ${g.id} achieved; its karma was not booked: ${e && e.message}`);
641
+ }
642
+ }
643
+ }
644
+
624
645
  // describeAutoClose — the one-line summary of what a close actually closed, or
625
646
  // null on a no-op. Pure + here rather than in db.js because db.js is over the
626
647
  // fitness oversize threshold: growth there has to be paid for, and formatting is
@@ -674,6 +695,7 @@ module.exports = {
674
695
  autoSatisfyShippedCriteria,
675
696
  describeAutoClose,
676
697
  closeCompletedWorkForShip,
698
+ rewardAchievedGoals,
677
699
  markCriterion,
678
700
  unmarkCriterion,
679
701
  };
@@ -26,6 +26,7 @@ const activity = require('./activity');
26
26
  const mingle = require('./mingle');
27
27
  const pageTweakReads = require('./page-tweak-reads');
28
28
  const pageTweakClaim = require('./page-tweak-claim');
29
+ const pageNotice = require('./page-notice');
29
30
  const pageTweakWrite = require('./page-tweak-write');
30
31
  const pageTweakApprove = require('./page-tweak-approve');
31
32
 
@@ -165,6 +166,10 @@ module.exports = {
165
166
  // read the part-3 claim gate uses, surfaced to me.js so the CLI + hall can
166
167
  // show the owed-rebase warning that gates new claims (reward lands on ship).
167
168
  needsRebaseTasksForBuilder: (...a) => db.needsRebaseTasksForBuilder(...a),
169
+ // task 1004948: the page-update heads-up lines for a claim's files, for GET /me.
170
+ noticesForClaims: (...a) => pageNotice.noticesForClaims(...a),
171
+ // task 1004948: engineering claims open on a page's files, for the artist's editor read.
172
+ engineeringActiveOnPage: (...a) => pageNotice.engineeringActiveOnPage(...a),
168
173
  // builders.js (the Archon roster): the roster + stale-claim + per-builder grade.
169
174
  getBuildersRoster: (...a) => db.getBuildersRoster(...a),
170
175
  getActiveClaimsForBuilderWithStale: (...a) => db.getActiveClaimsForBuilderWithStale(...a),
@@ -0,0 +1,46 @@
1
+ -- lifecycle_017_notes.sql — notes on a task or a goal (task 1004745, goal 1000095).
2
+ --
3
+ -- WHAT IT IS. A place to put information that helps scope a task or goal, or helps
4
+ -- finish it: a decision, an open question, a link, a risk, progress. Each note is
5
+ -- short, dated and has an author.
6
+ --
7
+ -- WHY `kind` IS A COLUMN. Notes are shown GROUPED BY PURPOSE (scope, decision,
8
+ -- question, reference, progress, risk, other) instead of as one growing list, so the
9
+ -- purpose has to be stored, not re-guessed on every read. `kind_source` records
10
+ -- whether the author chose it or the heuristic did, so a wrong guess can be told
11
+ -- apart from a deliberate choice and a later smarter grouper knows which to revisit.
12
+ --
13
+ -- A note is about a task OR a goal, never both, and never neither.
14
+ --
15
+ -- MODULE-OWNED and `lifecycle_`-prefixed, so fitness check 11 permits it: a NEW
16
+ -- table the lifecycle module owns outright (ADR 0083 §Decision #5).
17
+
18
+ CREATE TABLE IF NOT EXISTS lifecycle_notes (
19
+ id bigserial PRIMARY KEY,
20
+ task_id bigint REFERENCES tasks(id),
21
+ goal_id bigint REFERENCES goals(id),
22
+ author_id bigint NOT NULL REFERENCES builders(id),
23
+ body text NOT NULL,
24
+ kind text NOT NULL DEFAULT 'other',
25
+ kind_source text NOT NULL DEFAULT 'auto',
26
+ created_at timestamptz NOT NULL DEFAULT now(),
27
+ updated_at timestamptz NOT NULL DEFAULT now(),
28
+
29
+ CONSTRAINT lifecycle_notes_subject_chk
30
+ CHECK ((task_id IS NOT NULL) <> (goal_id IS NOT NULL)),
31
+ CONSTRAINT lifecycle_notes_body_chk
32
+ CHECK (length(btrim(body)) BETWEEN 1 AND 4000),
33
+ CONSTRAINT lifecycle_notes_kind_chk
34
+ CHECK (kind IN ('scope', 'decision', 'question', 'reference', 'progress', 'risk', 'other')),
35
+ CONSTRAINT lifecycle_notes_kind_source_chk
36
+ CHECK (kind_source IN ('author', 'auto'))
37
+ );
38
+
39
+ -- The two reads: everything on one task, everything on one goal, newest first.
40
+ CREATE INDEX IF NOT EXISTS lifecycle_notes_task_idx
41
+ ON lifecycle_notes (task_id, created_at DESC) WHERE task_id IS NOT NULL;
42
+ CREATE INDEX IF NOT EXISTS lifecycle_notes_goal_idx
43
+ ON lifecycle_notes (goal_id, created_at DESC) WHERE goal_id IS NOT NULL;
44
+
45
+ INSERT INTO schema_migrations (version) VALUES ('lifecycle_017_notes')
46
+ ON CONFLICT (version) DO NOTHING;
@@ -34,7 +34,8 @@
34
34
  "sky",
35
35
  "goal-task-relevance",
36
36
  "moving",
37
- "task-votes"
37
+ "task-votes",
38
+ "notes"
38
39
  ],
39
40
  "pollers": [
40
41
  "publish-reconciler"