@bongos/core 1.23.22 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.bongos-core.json +933 -413
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +96 -0
  27. package/clients/bongos-client/index.cjs +96 -0
  28. package/clients/bongos-client/index.d.ts +155 -4
  29. package/clients/bongos-client/index.mjs +96 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
  33. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  34. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  35. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  36. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  37. package/docs/adr/README.md +6 -2
  38. package/docs/api/openapi.json +8651 -5833
  39. package/docs/api-reference.md +66 -10
  40. package/docs/architecture.md +37 -1
  41. package/docs/canonical-permissions.md +1 -1
  42. package/docs/copy-inventory.md +427 -339
  43. package/docs/copy-registry.json +1376 -500
  44. package/docs/file-map.md +1 -0
  45. package/docs/module-api-changelog.md +7 -0
  46. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  47. package/docs/onboarding/diagrams/README.md +1 -1
  48. package/docs/onboarding/drachmae-and-karma.md +15 -15
  49. package/docs/onboarding/primer.md +2 -2
  50. package/docs/page-inventory.json +57 -7
  51. package/docs/page-readings.json +1210 -1027
  52. package/docs/recipes/google-sign-in.md +60 -0
  53. package/docs/recipes/ops-gotchas.md +1 -1
  54. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  55. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  56. package/migrations/core_283_payout_approvals.sql +183 -0
  57. package/migrations/core_284_payout_gates.sql +179 -0
  58. package/migrations/core_285_task_needs.sql +74 -0
  59. package/modules/ai-studio/billing.js +67 -0
  60. package/modules/ai-studio/context.js +29 -0
  61. package/modules/ai-studio/db.js +172 -0
  62. package/modules/ai-studio/gemini.js +75 -0
  63. package/modules/ai-studio/generate.js +119 -0
  64. package/modules/ai-studio/keys.js +73 -0
  65. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  66. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  67. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  68. package/modules/ai-studio/openrouter.js +90 -0
  69. package/modules/ai-studio/payments.js +105 -0
  70. package/modules/ai-studio/pricing.js +92 -0
  71. package/modules/ai-studio/routes/ai-studio.js +235 -5
  72. package/modules/ai-studio/studio.js +121 -0
  73. package/modules/autonomy/fence.js +64 -1
  74. package/modules/builder-settings/builder-needs.js +27 -1
  75. package/modules/copy-desk/routes/copy-desk.js +5 -0
  76. package/modules/economy/achievements.js +22 -10
  77. package/modules/economy/cost-classify.js +9 -0
  78. package/modules/economy/cost-write-guard.js +52 -0
  79. package/modules/economy/cost.js +2 -1
  80. package/modules/economy/credits.js +18 -15
  81. package/modules/economy/goal-reward.js +161 -0
  82. package/modules/economy/reward.js +4 -0
  83. package/modules/economy/routes/cost.js +33 -7
  84. package/modules/government/access-routes.js +70 -0
  85. package/modules/government/access.js +88 -0
  86. package/modules/government/asks.js +5 -2
  87. package/modules/government/catalog.js +6 -0
  88. package/modules/government/db.js +72 -0
  89. package/modules/government/docket.js +48 -2
  90. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  91. package/modules/government/migrations/government_036_access_holders.sql +33 -0
  92. package/modules/government/pollers/board-expiry.js +108 -20
  93. package/modules/government/protected-surfaces.json +1 -0
  94. package/modules/government/routes/government.js +31 -0
  95. package/modules/government/routing.js +49 -7
  96. package/modules/government/waiting.js +78 -0
  97. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  98. package/modules/hall-ui/public/ai-studio.css +193 -0
  99. package/modules/hall-ui/public/ai-studio.html +98 -0
  100. package/modules/hall-ui/public/ai-studio.js +556 -0
  101. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  102. package/modules/hall-ui/public/blockers-by-person.js +158 -0
  103. package/modules/hall-ui/public/blockers-lib.js +15 -1
  104. package/modules/hall-ui/public/blockers.html +8 -0
  105. package/modules/hall-ui/public/blockers.js +1 -1
  106. package/modules/hall-ui/public/claim-action.js +3 -0
  107. package/modules/hall-ui/public/collab.html +12 -0
  108. package/modules/hall-ui/public/craft-mode.js +4 -1
  109. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  110. package/modules/hall-ui/public/drachmae.html +1 -1
  111. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  115. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  120. package/modules/hall-ui/public/goal-inbox.js +1 -1
  121. package/modules/hall-ui/public/goals-render.js +4 -0
  122. package/modules/hall-ui/public/goals.html +8 -0
  123. package/modules/hall-ui/public/gov-living.js +6 -2
  124. package/modules/hall-ui/public/government-access.js +108 -0
  125. package/modules/hall-ui/public/government.html +21 -0
  126. package/modules/hall-ui/public/hall-ask.css +5 -0
  127. package/modules/hall-ui/public/hall-ask.js +39 -3
  128. package/modules/hall-ui/public/notes-panel.css +20 -0
  129. package/modules/hall-ui/public/notes-panel.js +146 -0
  130. package/modules/hall-ui/public/project-settings.html +1 -1
  131. package/modules/hall-ui/public/project-settings.js +7 -5
  132. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  133. package/modules/hall-ui/public/settings.html +15 -0
  134. package/modules/hall-ui/public/street-deck.js +12 -3
  135. package/modules/hall-ui/public/studio.css +90 -8
  136. package/modules/hall-ui/public/studio.html +39 -10
  137. package/modules/hall-ui/public/studio.js +148 -18
  138. package/modules/hall-ui/public/task.html +2 -0
  139. package/modules/hall-ui/public/task.js +4 -0
  140. package/modules/hall-ui/public/tweak-editor.js +12 -3
  141. package/modules/hall-ui/public/welcome-boot.css +42 -6
  142. package/modules/hall-ui/public/welcome-boot.js +20 -6
  143. package/modules/hall-ui/public/work.js +20 -0
  144. package/modules/hall-ui/records/ai-studio.md +36 -0
  145. package/modules/hall-ui/records/blockers-people.md +38 -0
  146. package/modules/lifecycle/db-claims.js +32 -4
  147. package/modules/lifecycle/db-goals.js +18 -5
  148. package/modules/lifecycle/db-tasks.js +15 -6
  149. package/modules/lifecycle/done-when.js +23 -1
  150. package/modules/lifecycle/lifecycle.js +5 -0
  151. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  152. package/modules/lifecycle/module.json +2 -1
  153. package/modules/lifecycle/notes.js +219 -0
  154. package/modules/lifecycle/page-notice.js +124 -0
  155. package/modules/lifecycle/page-tweak-artist.js +18 -1
  156. package/modules/lifecycle/publish-reconciler.js +67 -20
  157. package/modules/lifecycle/routes/claims.js +5 -2
  158. package/modules/lifecycle/routes/goals.js +3 -1
  159. package/modules/lifecycle/routes/notes.js +91 -0
  160. package/modules/lifecycle/routes/project-settings.js +43 -35
  161. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  162. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  163. package/modules/lifecycle/routes/tasks.js +12 -12
  164. package/modules/lifecycle/task-needs-gate.js +70 -0
  165. package/modules/market-watch/CLAUDE.md +15 -0
  166. package/modules/market-watch/db.js +340 -0
  167. package/modules/market-watch/key.js +58 -0
  168. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  169. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  170. package/modules/market-watch/module.json +26 -0
  171. package/modules/market-watch/pollers/hub-seed.js +54 -0
  172. package/modules/market-watch/pollers/weekly-run.js +100 -0
  173. package/modules/market-watch/research.js +350 -0
  174. package/modules/market-watch/run.js +245 -0
  175. package/modules/market-watch/seed/market-data.json +38 -0
  176. package/modules/market-watch/seed/partners-data.json +497 -0
  177. package/modules/market-watch/seed.js +252 -0
  178. package/modules/market-watch/store.js +230 -0
  179. package/modules/memory/routes/memory.js +14 -2
  180. package/modules/memory/routes/search.js +8 -2
  181. package/modules/platform-identity/account-logins.js +192 -0
  182. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  183. package/modules/platform-identity/platform-identity.js +5 -4
  184. package/modules/platform-identity/routes/my-projects.js +6 -0
  185. package/modules/platform-identity/routes/profile.js +18 -0
  186. package/modules/platform-identity/routes/public-profile.js +10 -1
  187. package/modules/platform-identity/routes/sso.js +26 -1
  188. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  189. package/modules/provisioning/credential-preflight.js +13 -3
  190. package/modules/provisioning/pollers/app-liveness.js +25 -5
  191. package/modules/provisioning/routes/provisioning.js +1 -1
  192. package/modules/provisioning/starter-bundles.js +6 -2
  193. package/modules/public-landing/public/account.html +10 -2
  194. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  195. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  196. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  197. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  198. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  199. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  200. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  201. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  202. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  203. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  205. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  209. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  210. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  213. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  214. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  221. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  222. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  223. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  224. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  225. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  226. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  227. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  228. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  229. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  230. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  233. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  244. package/modules/public-landing/public/community.html +1 -1
  245. package/modules/public-landing/public/connections.html +2 -2
  246. package/modules/public-landing/public/guild.html +1 -1
  247. package/modules/public-landing/public/invitations.html +2 -2
  248. package/modules/public-landing/public/projects.html +5 -5
  249. package/modules/raise/views/nda.html +2 -0
  250. package/modules/specialities/db.js +32 -1
  251. package/modules/specialities/routes/specialities.js +10 -6
  252. package/modules/status-ui/public/index.html +1 -1
  253. package/package-lock.json +2 -2
  254. package/package.json +1 -1
  255. package/release-notes.json +230 -0
  256. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  257. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  258. package/scripts/bongos/claim.js +22 -1
  259. package/scripts/bongos/db-backup-nightly.sh +29 -0
  260. package/scripts/bongos/deploy-divergence.js +7 -0
  261. package/scripts/bongos/doc-cli-guard.js +6 -0
  262. package/scripts/bongos/fitness.js +3 -2
  263. package/scripts/bongos/gen-diagrams.js +3 -3
  264. package/scripts/bongos/government-walk.js +3 -0
  265. package/scripts/bongos/lib.sh +4 -5
  266. package/scripts/bongos/migration-namespace.js +13 -1
  267. package/scripts/bongos/plain-cards.js +4 -0
  268. package/scripts/bongos/provision-github-setup.js +3 -1
  269. package/scripts/bongos/provision-units.js +6 -1
  270. package/scripts/bongos/publish-manifest.js +3 -0
  271. package/scripts/bongos/release.js +2 -0
  272. package/scripts/bongos/run-unit-tests.js +268 -82
  273. package/scripts/bongos/seed-tasks.example.json +56 -0
  274. package/scripts/bongos/seed-tasks.js +222 -0
  275. package/scripts/bongos/seed-verify-criteria.js +1 -2
  276. package/scripts/bongos/ship-finish.js +2 -0
  277. package/scripts/bongos/ship-resume.js +2 -0
  278. package/scripts/bongos/strand-lease.js +250 -0
  279. package/scripts/bongos/strand-watch.js +26 -5
  280. package/scripts/bongos/version-literal-guard.js +2 -2
  281. package/scripts/discord/post.js +4 -3
  282. package/scripts/gds/seed-tasks.js +7 -0
  283. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  284. package/scripts/hall-preview/server.js +21 -0
  285. package/src/bongos/auth-config.js +29 -1
  286. package/src/bongos/auth-discord.js +3 -3
  287. package/src/bongos/auth-github.js +3 -6
  288. package/src/bongos/auth-google.js +217 -0
  289. package/src/bongos/device-approval.js +140 -0
  290. package/src/bongos/llm-pricing.js +17 -1
  291. package/src/bongos/metrics.js +48 -2
  292. package/src/bongos/middleware/rate-limit.js +30 -11
  293. package/src/bongos/module-scope-map.js +12 -1
  294. package/src/bongos/optimizer.js +15 -1
  295. package/src/bongos/payout-approvals.js +342 -0
  296. package/src/bongos/payout-gates.js +364 -0
  297. package/src/bongos/prelaunch-gate.js +7 -9
  298. package/src/bongos/prelaunch-switch.js +20 -0
  299. package/src/bongos/route-rank-check.js +7 -0
  300. package/src/bongos/routes/auth-google.js +246 -0
  301. package/src/bongos/routes/auth.js +38 -1
  302. package/src/bongos/routes/backup.js +33 -9
  303. package/src/bongos/routes/builders.js +57 -30
  304. package/src/bongos/routes/github.js +9 -0
  305. package/src/bongos/routes/me.js +18 -0
  306. package/src/bongos/routes/my-payouts.js +133 -0
  307. package/src/bongos/routes/payout-approvals.js +312 -0
  308. package/src/bongos/routes/payout-gates.js +179 -0
  309. package/src/bongos/routes.js +15 -1
  310. package/src/bongos/secret-box.js +18 -3
  311. package/src/bongos/serve-internal.js +47 -19
  312. package/src/bongos/sign-in-pages.js +138 -0
  313. package/src/bongos/task-needs.js +146 -0
  314. package/src/bongos-downloads.js +18 -1
  315. package/src/instance-config.js +28 -0
  316. package/src/module-api.js +14 -1
  317. package/tests/access_holders.mjs +214 -0
  318. package/tests/account_logins.mjs +326 -0
  319. package/tests/account_logins_db.mjs +161 -0
  320. package/tests/ai_studio_billing.mjs +253 -0
  321. package/tests/ai_studio_generate.mjs +282 -0
  322. package/tests/ai_studio_keys.mjs +159 -0
  323. package/tests/ai_studio_requests.mjs +319 -0
  324. package/tests/auth_backchannel_logout_route.mjs +201 -0
  325. package/tests/auto_satisfy_criteria.mjs +10 -1
  326. package/tests/autobongos_tweak_lane.mjs +144 -0
  327. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  328. package/tests/backfill_subagent_rewards.mjs +17 -1
  329. package/tests/backup_trigger_lock.mjs +119 -0
  330. package/tests/blockers_people.mjs +198 -0
  331. package/tests/bounded_rate_maps.mjs +184 -0
  332. package/tests/claimable_blocker_status.mjs +1 -1
  333. package/tests/claims_batch_routes.mjs +421 -0
  334. package/tests/config_read_cache.mjs +171 -0
  335. package/tests/copy_desk_page_editor.mjs +12 -0
  336. package/tests/cost_write_guard.mjs +200 -0
  337. package/tests/craft_achievements.mjs +33 -10
  338. package/tests/criterion_goal_attach.mjs +1 -1
  339. package/tests/db_backup_nightly.mjs +73 -1
  340. package/tests/deploy_divergence_line.mjs +7 -0
  341. package/tests/effective_visibility_predicate.mjs +12 -0
  342. package/tests/fitness.mjs +19 -0
  343. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  344. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  345. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  346. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  347. package/tests/github_one_trip.mjs +34 -0
  348. package/tests/github_repo_visibility.mjs +20 -0
  349. package/tests/goal_achievement.mjs +30 -1
  350. package/tests/goal_achievement_reward.mjs +205 -0
  351. package/tests/goal_sort_staleness.mjs +2 -1
  352. package/tests/goal_tier.mjs +11 -1
  353. package/tests/google_fonts_nonblocking.mjs +84 -0
  354. package/tests/google_sign_in.mjs +401 -0
  355. package/tests/government_board_expiry.mjs +94 -0
  356. package/tests/government_board_membership.mjs +4 -1
  357. package/tests/government_protected_surfaces.mjs +1 -1
  358. package/tests/government_routes.mjs +1 -1
  359. package/tests/government_seed.mjs +1 -0
  360. package/tests/hall_ai_studio.mjs +347 -0
  361. package/tests/hall_page_gate_map.mjs +3 -0
  362. package/tests/hall_studio_home.mjs +61 -2
  363. package/tests/home_attention_cards.mjs +1 -1
  364. package/tests/human_signal.mjs +141 -0
  365. package/tests/install_script_reader.mjs +51 -0
  366. package/tests/integration_fixture_ids.mjs +82 -0
  367. package/tests/invite_expiry_db.mjs +13 -1
  368. package/tests/lib_sh_resolution.mjs +0 -4
  369. package/tests/market_watch_data.mjs +244 -0
  370. package/tests/market_watch_data_db.mjs +308 -0
  371. package/tests/market_watch_module.mjs +114 -0
  372. package/tests/market_watch_run.mjs +503 -0
  373. package/tests/metrics.mjs +124 -0
  374. package/tests/migration_namespace.mjs +14 -0
  375. package/tests/module_api.mjs +2 -0
  376. package/tests/module_loader.mjs +1 -1
  377. package/tests/notes.mjs +182 -0
  378. package/tests/own_scope_guard.mjs +5 -0
  379. package/tests/page_tweak_marker_sql.mjs +76 -0
  380. package/tests/page_tweak_no_block.mjs +135 -0
  381. package/tests/page_tweak_notice.mjs +103 -0
  382. package/tests/payout_approvals.mjs +253 -0
  383. package/tests/payout_approvals_db.mjs +106 -0
  384. package/tests/payout_gates.mjs +322 -0
  385. package/tests/payout_gates_db.mjs +113 -0
  386. package/tests/planning_session_skill.mjs +13 -3
  387. package/tests/project_settings_page.mjs +29 -5
  388. package/tests/project_settings_spend_cap_route.mjs +235 -0
  389. package/tests/promotion_sweep.mjs +29 -1
  390. package/tests/provision.mjs +3 -2
  391. package/tests/provisioning_app_status.mjs +18 -0
  392. package/tests/provisioning_credential_preflight.mjs +53 -0
  393. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  394. package/tests/publish_reconciler_overlap.mjs +60 -0
  395. package/tests/rank_change_broadcast_port.mjs +151 -0
  396. package/tests/seed_tasks.mjs +176 -0
  397. package/tests/seed_verify_criteria.mjs +2 -6
  398. package/tests/site_classified_once.mjs +142 -0
  399. package/tests/speciality_db_shape.mjs +23 -0
  400. package/tests/speciality_session_skills.mjs +48 -2
  401. package/tests/strand_lease.mjs +249 -0
  402. package/tests/strand_watch.mjs +10 -0
  403. package/tests/task_criteria_routes.mjs +275 -0
  404. package/tests/task_criteria_unlink_db.mjs +123 -0
  405. package/tests/task_needs.mjs +233 -0
  406. package/tests/task_needs_db.mjs +110 -0
  407. package/tests/unit_runner_serial.mjs +266 -43
  408. package/tests/version_literals.mjs +5 -7
  409. package/tests/welcome_boot.mjs +16 -5
  410. package/tests/welcome_boot_browser.mjs +36 -4
  411. package/docs/recipes/bongos-cli-release.md +0 -66
  412. package/scripts/bongos/backfill-disciplines.js +0 -144
  413. package/scripts/bongos/backfill-session-earnings.js +0 -123
  414. package/scripts/bongos/backfill-session-types.js +0 -87
  415. package/scripts/bongos/build-bongos.js +0 -94
  416. package/scripts/bongos/cleanup-orphans.js +0 -74
  417. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  418. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  419. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  420. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  421. package/scripts/bongos/smoke-batch-release.sh +0 -316
  422. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  423. package/scripts/bongos/smoke-dependencies.sh +0 -143
  424. package/scripts/gds/backfill-disciplines.js +0 -7
  425. package/scripts/gds/backfill-session-earnings.js +0 -7
  426. package/scripts/gds/backfill-session-types.js +0 -7
  427. package/scripts/gds/cleanup-orphans.js +0 -7
  428. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  429. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  430. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  431. package/scripts/gds/smoke-batch-claim.sh +0 -8
  432. package/scripts/gds/smoke-batch-release.sh +0 -8
  433. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  434. package/scripts/gds/smoke-dependencies.sh +0 -8
  435. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -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);
@@ -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
  }
@@ -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,
@@ -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"
@@ -0,0 +1,219 @@
1
+ // modules/lifecycle/notes.js — notes on a task or goal, grouped by purpose
2
+ // (task 1004745, goal 1000095).
3
+ //
4
+ // A note is a short piece of information that helps scope a task or goal or helps
5
+ // finish it. A long thread of them reads badly, so they are shown GROUPED BY
6
+ // PURPOSE — what is decided, what is still a question, what to read, what is
7
+ // risky — with the newest few open in each group and the rest folded away.
8
+ //
9
+ // THE PURPOSE IS CHOSEN BY THE AUTHOR, AND GUESSED WHEN THEY DO NOT. Guessing by
10
+ // text likeness would need a model call per note and could regroup a reader's page
11
+ // under them; a stored purpose is stable, cheap and the author's own word. The
12
+ // guess (`suggestKind`) is a few plain rules (a link, a question mark, "decided",
13
+ // "risk") used only when no purpose was given, and is stored with
14
+ // kind_source 'auto' so it can be told from a deliberate choice.
15
+ //
16
+ // Reaches core only through the doorway (ADR 0083).
17
+
18
+ 'use strict';
19
+
20
+ const api = require('../../src/module-api');
21
+
22
+ const { pool } = api;
23
+
24
+ const MAX_BODY = 4000;
25
+
26
+ // The most notes one task or goal carries, so no one can bury a page in them.
27
+ const MAX_NOTES_PER_SUBJECT = 500;
28
+
29
+ // Display order is the order a person scoping work wants to read: what the work is,
30
+ // what was decided, what is still open, what is risky, how far along it is, what to
31
+ // read, and everything else last.
32
+ const KINDS = Object.freeze([
33
+ { key: 'scope', label: 'Scope', hint: 'What is in and out' },
34
+ { key: 'decision', label: 'Decisions', hint: 'What was settled, and why' },
35
+ { key: 'question', label: 'Open questions', hint: 'What still needs an answer' },
36
+ { key: 'risk', label: 'Risks', hint: 'What could go wrong' },
37
+ { key: 'progress', label: 'Progress', hint: 'How far along it is' },
38
+ { key: 'reference', label: 'Links & reading', hint: 'Where to look' },
39
+ { key: 'other', label: 'Other', hint: 'Anything else' },
40
+ ]);
41
+ const KIND_KEYS = Object.freeze(KINDS.map((k) => k.key));
42
+
43
+ // How many notes a group shows before folding the rest behind "show more".
44
+ const OPEN_PER_GROUP = 3;
45
+
46
+ // A purpose from the words, for a note written without one. Order matters: the
47
+ // first rule that matches wins, and 'other' is the honest answer when none does.
48
+ function suggestKind(body) {
49
+ const t = String(body == null ? '' : body).trim().toLowerCase();
50
+ if (!t) return 'other';
51
+ if (/\b(decided|decision|we will|we'll go with|going with|agreed|chose|chosen)\b/.test(t)) return 'decision';
52
+ if (/\b(risk|danger|could break|might break|breaks|careful|watch out|blocker|blocked)\b/.test(t)) return 'risk';
53
+ if (t.includes('?') || /^(how|what|why|should|can|could|does|do|is|are|who|when|where)\b/.test(t)) return 'question';
54
+ if (/\b(in scope|out of scope|scope|must|should include|needs to|acceptance|done when)\b/.test(t)) return 'scope';
55
+ if (/\b(done|finished|shipped|merged|started|working on|in progress|so far|next step)\b/.test(t)) return 'progress';
56
+ if (/https?:\/\/|\bsee (the )?(docs?|adr|spec|page|link)\b|\bdocs?\b|\badr\b|\bspec\b/.test(t)) return 'reference';
57
+ return 'other';
58
+ }
59
+
60
+ // Which of the two subjects a request names: { taskId } or { goalId }, else null.
61
+ // Exactly one id must be present and a positive integer.
62
+ function subjectOf(src) {
63
+ const num = (v) => (v == null || v === '' ? null : (Number.isInteger(Number(v)) && Number(v) > 0 ? Number(v) : NaN));
64
+ const taskId = num(src.task_id);
65
+ const goalId = num(src.goal_id);
66
+ if (Number.isNaN(taskId) || Number.isNaN(goalId)) return null;
67
+ if ((taskId == null) === (goalId == null)) return null;
68
+ return taskId != null ? { taskId } : { goalId };
69
+ }
70
+
71
+ // Validate an incoming note; `kind` may be absent (then guessed).
72
+ function validateNote(input) {
73
+ const errors = [];
74
+ const body = input && typeof input.body === 'string' ? input.body.trim() : '';
75
+ if (!body) errors.push({ field: 'body', reason: 'required' });
76
+ else if (body.length > MAX_BODY) errors.push({ field: 'body', reason: `at most ${MAX_BODY} characters` });
77
+ const kind = input ? input.kind : undefined;
78
+ if (kind != null && !KIND_KEYS.includes(kind)) errors.push({ field: 'kind', reason: `pick a group: ${KINDS.map((k) => k.label).join(', ')}` });
79
+ if (!subjectOf(input || {})) errors.push({ field: 'subject', reason: 'say whether the note is on a task or on a goal, not both' });
80
+ if (errors.length) return { ok: false, errors };
81
+ return {
82
+ ok: true,
83
+ body,
84
+ kind: kind == null ? suggestKind(body) : kind,
85
+ kindSource: kind == null ? 'auto' : 'author',
86
+ };
87
+ }
88
+
89
+ // Rows (newest first, or any order) -> the grouped view the page renders. Empty
90
+ // groups are left out; within a group newest comes first; `open` is what shows
91
+ // at once and `folded` is what waits behind "show more".
92
+ function groupNotes(rows, { openPerGroup = OPEN_PER_GROUP } = {}) {
93
+ const byKind = new Map(KIND_KEYS.map((k) => [k, []]));
94
+ for (const r of rows || []) {
95
+ const k = byKind.has(r.kind) ? r.kind : 'other';
96
+ byKind.get(k).push(r);
97
+ }
98
+ const groups = [];
99
+ for (const meta of KINDS) {
100
+ const list = byKind.get(meta.key).slice().sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
101
+ if (!list.length) continue;
102
+ groups.push({
103
+ kind: meta.key,
104
+ label: meta.label,
105
+ hint: meta.hint,
106
+ count: list.length,
107
+ latest_at: list[0].created_at,
108
+ open: list.slice(0, openPerGroup),
109
+ folded: list.slice(openPerGroup),
110
+ });
111
+ }
112
+ return groups;
113
+ }
114
+
115
+ const COLS = 'n.id, n.task_id, n.goal_id, n.author_id, n.body, n.kind, n.kind_source, n.created_at, n.updated_at, '
116
+ + 'b.github_login AS author_login, b.display_name AS author_name, b.avatar_url AS author_avatar_url';
117
+
118
+ // Every note on one task or goal, newest first. `subject` is { taskId } or { goalId }.
119
+ async function listNotes(subject, deps = {}) {
120
+ const db = deps.pool || pool;
121
+ const col = subject.taskId != null ? 'n.task_id' : 'n.goal_id';
122
+ const id = subject.taskId != null ? subject.taskId : subject.goalId;
123
+ const { rows } = await db.query(
124
+ `SELECT ${COLS} FROM lifecycle_notes n JOIN builders b ON b.id = n.author_id
125
+ WHERE ${col} = $1 ORDER BY n.created_at DESC, n.id DESC LIMIT $2`,
126
+ [id, MAX_NOTES_PER_SUBJECT]
127
+ );
128
+ return rows;
129
+ }
130
+
131
+ // How many notes a task or goal already carries (for the per-subject cap).
132
+ async function countNotes(subject, deps = {}) {
133
+ const db = deps.pool || pool;
134
+ const col = subject.taskId != null ? 'task_id' : 'goal_id';
135
+ const id = subject.taskId != null ? subject.taskId : subject.goalId;
136
+ const { rows } = await db.query(`SELECT count(*)::int AS n FROM lifecycle_notes WHERE ${col} = $1`, [id]);
137
+ return rows[0].n;
138
+ }
139
+
140
+ async function addNote({ taskId = null, goalId = null, authorId, body, kind, kindSource }, deps = {}) {
141
+ const db = deps.pool || pool;
142
+ const { rows } = await db.query(
143
+ `WITH ins AS (
144
+ INSERT INTO lifecycle_notes (task_id, goal_id, author_id, body, kind, kind_source)
145
+ VALUES ($1, $2, $3, $4, $5, $6) RETURNING *
146
+ )
147
+ SELECT ${COLS} FROM ins n JOIN builders b ON b.id = n.author_id`,
148
+ [taskId, goalId, authorId, body, kind, kindSource]
149
+ );
150
+ return rows[0];
151
+ }
152
+
153
+ // Re-file a note under another purpose. Only its author or a Metic+ may (the route
154
+ // decides who; this just writes), and the choice is now the author's.
155
+ async function setKind(id, kind, deps = {}) {
156
+ const db = deps.pool || pool;
157
+ const { rows } = await db.query(
158
+ `UPDATE lifecycle_notes SET kind = $2, kind_source = 'author', updated_at = now()
159
+ WHERE id = $1 RETURNING id, kind, kind_source, author_id`,
160
+ [id, kind]
161
+ );
162
+ return rows[0] || null;
163
+ }
164
+
165
+ async function getNote(id, deps = {}) {
166
+ const db = deps.pool || pool;
167
+ const { rows } = await db.query('SELECT id, author_id, kind, task_id, goal_id FROM lifecycle_notes WHERE id = $1', [id]);
168
+ return rows[0] || null;
169
+ }
170
+
171
+ async function deleteNote(id, deps = {}) {
172
+ const db = deps.pool || pool;
173
+ const { rowCount } = await db.query('DELETE FROM lifecycle_notes WHERE id = $1', [id]);
174
+ return rowCount > 0;
175
+ }
176
+
177
+ // May this builder see notes on this task or goal? A private goal's notes (and those of
178
+ // its tasks) are for its members and an Archon only, the same wall the goal's own title
179
+ // and prose sit behind (ADR 0112). Anything else is open to every signed-in builder.
180
+ // A task or goal that does not exist answers false too, so the route gives the same
181
+ // 404 for "missing" and "not yours to see" from ONE read.
182
+ async function canSee(builder, subject, deps = {}) {
183
+ const db = deps.pool || pool;
184
+ const { rows } = subject.taskId != null
185
+ ? await db.query('SELECT g.id AS goal_id, g.visibility FROM tasks t LEFT JOIN goals g ON g.id = t.goal_id WHERE t.id = $1', [subject.taskId])
186
+ : await db.query('SELECT g.id AS goal_id, g.visibility FROM goals g WHERE g.id = $1', [subject.goalId]);
187
+ const g = rows[0];
188
+ if (!g) return false;
189
+ if (g.visibility !== 'private' || api.rankMeetsThreshold(builder.rank, ['archon'])) return true;
190
+ const m = await db.query('SELECT 1 FROM goal_members WHERE goal_id = $1 AND builder_id = $2', [g.goal_id, builder.id]);
191
+ return m.rows.length > 0;
192
+ }
193
+
194
+ // Who may re-file or delete a note: its author, or a Metic or above. The rank is the live
195
+ // one on the authenticated request (ADR 0016), compared through the kernel's own ladder.
196
+ function canChange(builder, note) {
197
+ if (String(builder.id) === String(note.author_id)) return true;
198
+ return api.rankMeetsThreshold(builder.rank, ['metic', 'archon']);
199
+ }
200
+
201
+ module.exports = {
202
+ KINDS,
203
+ MAX_NOTES_PER_SUBJECT,
204
+ subjectOf,
205
+ canSee,
206
+ canChange,
207
+ countNotes,
208
+ KIND_KEYS,
209
+ MAX_BODY,
210
+ suggestKind,
211
+ validateNote,
212
+ groupNotes,
213
+ listNotes,
214
+
215
+ addNote,
216
+ setKind,
217
+ getNote,
218
+ deleteNote,
219
+ };