@bongos/core 1.23.22 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.bongos-core.json +933 -413
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +96 -0
  27. package/clients/bongos-client/index.cjs +96 -0
  28. package/clients/bongos-client/index.d.ts +155 -4
  29. package/clients/bongos-client/index.mjs +96 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
  33. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  34. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  35. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  36. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  37. package/docs/adr/README.md +6 -2
  38. package/docs/api/openapi.json +8651 -5833
  39. package/docs/api-reference.md +66 -10
  40. package/docs/architecture.md +37 -1
  41. package/docs/canonical-permissions.md +1 -1
  42. package/docs/copy-inventory.md +427 -339
  43. package/docs/copy-registry.json +1376 -500
  44. package/docs/file-map.md +1 -0
  45. package/docs/module-api-changelog.md +7 -0
  46. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  47. package/docs/onboarding/diagrams/README.md +1 -1
  48. package/docs/onboarding/drachmae-and-karma.md +15 -15
  49. package/docs/onboarding/primer.md +2 -2
  50. package/docs/page-inventory.json +57 -7
  51. package/docs/page-readings.json +1210 -1027
  52. package/docs/recipes/google-sign-in.md +60 -0
  53. package/docs/recipes/ops-gotchas.md +1 -1
  54. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  55. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  56. package/migrations/core_283_payout_approvals.sql +183 -0
  57. package/migrations/core_284_payout_gates.sql +179 -0
  58. package/migrations/core_285_task_needs.sql +74 -0
  59. package/modules/ai-studio/billing.js +67 -0
  60. package/modules/ai-studio/context.js +29 -0
  61. package/modules/ai-studio/db.js +172 -0
  62. package/modules/ai-studio/gemini.js +75 -0
  63. package/modules/ai-studio/generate.js +119 -0
  64. package/modules/ai-studio/keys.js +73 -0
  65. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  66. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  67. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  68. package/modules/ai-studio/openrouter.js +90 -0
  69. package/modules/ai-studio/payments.js +105 -0
  70. package/modules/ai-studio/pricing.js +92 -0
  71. package/modules/ai-studio/routes/ai-studio.js +235 -5
  72. package/modules/ai-studio/studio.js +121 -0
  73. package/modules/autonomy/fence.js +64 -1
  74. package/modules/builder-settings/builder-needs.js +27 -1
  75. package/modules/copy-desk/routes/copy-desk.js +5 -0
  76. package/modules/economy/achievements.js +22 -10
  77. package/modules/economy/cost-classify.js +9 -0
  78. package/modules/economy/cost-write-guard.js +52 -0
  79. package/modules/economy/cost.js +2 -1
  80. package/modules/economy/credits.js +18 -15
  81. package/modules/economy/goal-reward.js +161 -0
  82. package/modules/economy/reward.js +4 -0
  83. package/modules/economy/routes/cost.js +33 -7
  84. package/modules/government/access-routes.js +70 -0
  85. package/modules/government/access.js +88 -0
  86. package/modules/government/asks.js +5 -2
  87. package/modules/government/catalog.js +6 -0
  88. package/modules/government/db.js +72 -0
  89. package/modules/government/docket.js +48 -2
  90. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  91. package/modules/government/migrations/government_036_access_holders.sql +33 -0
  92. package/modules/government/pollers/board-expiry.js +108 -20
  93. package/modules/government/protected-surfaces.json +1 -0
  94. package/modules/government/routes/government.js +31 -0
  95. package/modules/government/routing.js +49 -7
  96. package/modules/government/waiting.js +78 -0
  97. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  98. package/modules/hall-ui/public/ai-studio.css +193 -0
  99. package/modules/hall-ui/public/ai-studio.html +98 -0
  100. package/modules/hall-ui/public/ai-studio.js +556 -0
  101. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  102. package/modules/hall-ui/public/blockers-by-person.js +158 -0
  103. package/modules/hall-ui/public/blockers-lib.js +15 -1
  104. package/modules/hall-ui/public/blockers.html +8 -0
  105. package/modules/hall-ui/public/blockers.js +1 -1
  106. package/modules/hall-ui/public/claim-action.js +3 -0
  107. package/modules/hall-ui/public/collab.html +12 -0
  108. package/modules/hall-ui/public/craft-mode.js +4 -1
  109. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  110. package/modules/hall-ui/public/drachmae.html +1 -1
  111. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  115. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  120. package/modules/hall-ui/public/goal-inbox.js +1 -1
  121. package/modules/hall-ui/public/goals-render.js +4 -0
  122. package/modules/hall-ui/public/goals.html +8 -0
  123. package/modules/hall-ui/public/gov-living.js +6 -2
  124. package/modules/hall-ui/public/government-access.js +108 -0
  125. package/modules/hall-ui/public/government.html +21 -0
  126. package/modules/hall-ui/public/hall-ask.css +5 -0
  127. package/modules/hall-ui/public/hall-ask.js +39 -3
  128. package/modules/hall-ui/public/notes-panel.css +20 -0
  129. package/modules/hall-ui/public/notes-panel.js +146 -0
  130. package/modules/hall-ui/public/project-settings.html +1 -1
  131. package/modules/hall-ui/public/project-settings.js +7 -5
  132. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  133. package/modules/hall-ui/public/settings.html +15 -0
  134. package/modules/hall-ui/public/street-deck.js +12 -3
  135. package/modules/hall-ui/public/studio.css +90 -8
  136. package/modules/hall-ui/public/studio.html +39 -10
  137. package/modules/hall-ui/public/studio.js +148 -18
  138. package/modules/hall-ui/public/task.html +2 -0
  139. package/modules/hall-ui/public/task.js +4 -0
  140. package/modules/hall-ui/public/tweak-editor.js +12 -3
  141. package/modules/hall-ui/public/welcome-boot.css +42 -6
  142. package/modules/hall-ui/public/welcome-boot.js +20 -6
  143. package/modules/hall-ui/public/work.js +20 -0
  144. package/modules/hall-ui/records/ai-studio.md +36 -0
  145. package/modules/hall-ui/records/blockers-people.md +38 -0
  146. package/modules/lifecycle/db-claims.js +32 -4
  147. package/modules/lifecycle/db-goals.js +18 -5
  148. package/modules/lifecycle/db-tasks.js +15 -6
  149. package/modules/lifecycle/done-when.js +23 -1
  150. package/modules/lifecycle/lifecycle.js +5 -0
  151. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  152. package/modules/lifecycle/module.json +2 -1
  153. package/modules/lifecycle/notes.js +219 -0
  154. package/modules/lifecycle/page-notice.js +124 -0
  155. package/modules/lifecycle/page-tweak-artist.js +18 -1
  156. package/modules/lifecycle/publish-reconciler.js +67 -20
  157. package/modules/lifecycle/routes/claims.js +5 -2
  158. package/modules/lifecycle/routes/goals.js +3 -1
  159. package/modules/lifecycle/routes/notes.js +91 -0
  160. package/modules/lifecycle/routes/project-settings.js +43 -35
  161. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  162. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  163. package/modules/lifecycle/routes/tasks.js +12 -12
  164. package/modules/lifecycle/task-needs-gate.js +70 -0
  165. package/modules/market-watch/CLAUDE.md +15 -0
  166. package/modules/market-watch/db.js +340 -0
  167. package/modules/market-watch/key.js +58 -0
  168. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  169. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  170. package/modules/market-watch/module.json +26 -0
  171. package/modules/market-watch/pollers/hub-seed.js +54 -0
  172. package/modules/market-watch/pollers/weekly-run.js +100 -0
  173. package/modules/market-watch/research.js +350 -0
  174. package/modules/market-watch/run.js +245 -0
  175. package/modules/market-watch/seed/market-data.json +38 -0
  176. package/modules/market-watch/seed/partners-data.json +497 -0
  177. package/modules/market-watch/seed.js +252 -0
  178. package/modules/market-watch/store.js +230 -0
  179. package/modules/memory/routes/memory.js +14 -2
  180. package/modules/memory/routes/search.js +8 -2
  181. package/modules/platform-identity/account-logins.js +192 -0
  182. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  183. package/modules/platform-identity/platform-identity.js +5 -4
  184. package/modules/platform-identity/routes/my-projects.js +6 -0
  185. package/modules/platform-identity/routes/profile.js +18 -0
  186. package/modules/platform-identity/routes/public-profile.js +10 -1
  187. package/modules/platform-identity/routes/sso.js +26 -1
  188. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  189. package/modules/provisioning/credential-preflight.js +13 -3
  190. package/modules/provisioning/pollers/app-liveness.js +25 -5
  191. package/modules/provisioning/routes/provisioning.js +1 -1
  192. package/modules/provisioning/starter-bundles.js +6 -2
  193. package/modules/public-landing/public/account.html +10 -2
  194. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  195. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  196. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  197. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  198. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  199. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  200. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  201. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  202. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  203. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  205. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  209. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  210. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  213. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  214. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  221. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  222. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  223. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  224. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  225. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  226. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  227. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  228. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  229. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  230. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  233. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  244. package/modules/public-landing/public/community.html +1 -1
  245. package/modules/public-landing/public/connections.html +2 -2
  246. package/modules/public-landing/public/guild.html +1 -1
  247. package/modules/public-landing/public/invitations.html +2 -2
  248. package/modules/public-landing/public/projects.html +5 -5
  249. package/modules/raise/views/nda.html +2 -0
  250. package/modules/specialities/db.js +32 -1
  251. package/modules/specialities/routes/specialities.js +10 -6
  252. package/modules/status-ui/public/index.html +1 -1
  253. package/package-lock.json +2 -2
  254. package/package.json +1 -1
  255. package/release-notes.json +230 -0
  256. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  257. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  258. package/scripts/bongos/claim.js +22 -1
  259. package/scripts/bongos/db-backup-nightly.sh +29 -0
  260. package/scripts/bongos/deploy-divergence.js +7 -0
  261. package/scripts/bongos/doc-cli-guard.js +6 -0
  262. package/scripts/bongos/fitness.js +3 -2
  263. package/scripts/bongos/gen-diagrams.js +3 -3
  264. package/scripts/bongos/government-walk.js +3 -0
  265. package/scripts/bongos/lib.sh +4 -5
  266. package/scripts/bongos/migration-namespace.js +13 -1
  267. package/scripts/bongos/plain-cards.js +4 -0
  268. package/scripts/bongos/provision-github-setup.js +3 -1
  269. package/scripts/bongos/provision-units.js +6 -1
  270. package/scripts/bongos/publish-manifest.js +3 -0
  271. package/scripts/bongos/release.js +2 -0
  272. package/scripts/bongos/run-unit-tests.js +268 -82
  273. package/scripts/bongos/seed-tasks.example.json +56 -0
  274. package/scripts/bongos/seed-tasks.js +222 -0
  275. package/scripts/bongos/seed-verify-criteria.js +1 -2
  276. package/scripts/bongos/ship-finish.js +2 -0
  277. package/scripts/bongos/ship-resume.js +2 -0
  278. package/scripts/bongos/strand-lease.js +250 -0
  279. package/scripts/bongos/strand-watch.js +26 -5
  280. package/scripts/bongos/version-literal-guard.js +2 -2
  281. package/scripts/discord/post.js +4 -3
  282. package/scripts/gds/seed-tasks.js +7 -0
  283. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  284. package/scripts/hall-preview/server.js +21 -0
  285. package/src/bongos/auth-config.js +29 -1
  286. package/src/bongos/auth-discord.js +3 -3
  287. package/src/bongos/auth-github.js +3 -6
  288. package/src/bongos/auth-google.js +217 -0
  289. package/src/bongos/device-approval.js +140 -0
  290. package/src/bongos/llm-pricing.js +17 -1
  291. package/src/bongos/metrics.js +48 -2
  292. package/src/bongos/middleware/rate-limit.js +30 -11
  293. package/src/bongos/module-scope-map.js +12 -1
  294. package/src/bongos/optimizer.js +15 -1
  295. package/src/bongos/payout-approvals.js +342 -0
  296. package/src/bongos/payout-gates.js +364 -0
  297. package/src/bongos/prelaunch-gate.js +7 -9
  298. package/src/bongos/prelaunch-switch.js +20 -0
  299. package/src/bongos/route-rank-check.js +7 -0
  300. package/src/bongos/routes/auth-google.js +246 -0
  301. package/src/bongos/routes/auth.js +38 -1
  302. package/src/bongos/routes/backup.js +33 -9
  303. package/src/bongos/routes/builders.js +57 -30
  304. package/src/bongos/routes/github.js +9 -0
  305. package/src/bongos/routes/me.js +18 -0
  306. package/src/bongos/routes/my-payouts.js +133 -0
  307. package/src/bongos/routes/payout-approvals.js +312 -0
  308. package/src/bongos/routes/payout-gates.js +179 -0
  309. package/src/bongos/routes.js +15 -1
  310. package/src/bongos/secret-box.js +18 -3
  311. package/src/bongos/serve-internal.js +47 -19
  312. package/src/bongos/sign-in-pages.js +138 -0
  313. package/src/bongos/task-needs.js +146 -0
  314. package/src/bongos-downloads.js +18 -1
  315. package/src/instance-config.js +28 -0
  316. package/src/module-api.js +14 -1
  317. package/tests/access_holders.mjs +214 -0
  318. package/tests/account_logins.mjs +326 -0
  319. package/tests/account_logins_db.mjs +161 -0
  320. package/tests/ai_studio_billing.mjs +253 -0
  321. package/tests/ai_studio_generate.mjs +282 -0
  322. package/tests/ai_studio_keys.mjs +159 -0
  323. package/tests/ai_studio_requests.mjs +319 -0
  324. package/tests/auth_backchannel_logout_route.mjs +201 -0
  325. package/tests/auto_satisfy_criteria.mjs +10 -1
  326. package/tests/autobongos_tweak_lane.mjs +144 -0
  327. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  328. package/tests/backfill_subagent_rewards.mjs +17 -1
  329. package/tests/backup_trigger_lock.mjs +119 -0
  330. package/tests/blockers_people.mjs +198 -0
  331. package/tests/bounded_rate_maps.mjs +184 -0
  332. package/tests/claimable_blocker_status.mjs +1 -1
  333. package/tests/claims_batch_routes.mjs +421 -0
  334. package/tests/config_read_cache.mjs +171 -0
  335. package/tests/copy_desk_page_editor.mjs +12 -0
  336. package/tests/cost_write_guard.mjs +200 -0
  337. package/tests/craft_achievements.mjs +33 -10
  338. package/tests/criterion_goal_attach.mjs +1 -1
  339. package/tests/db_backup_nightly.mjs +73 -1
  340. package/tests/deploy_divergence_line.mjs +7 -0
  341. package/tests/effective_visibility_predicate.mjs +12 -0
  342. package/tests/fitness.mjs +19 -0
  343. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  344. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  345. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  346. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  347. package/tests/github_one_trip.mjs +34 -0
  348. package/tests/github_repo_visibility.mjs +20 -0
  349. package/tests/goal_achievement.mjs +30 -1
  350. package/tests/goal_achievement_reward.mjs +205 -0
  351. package/tests/goal_sort_staleness.mjs +2 -1
  352. package/tests/goal_tier.mjs +11 -1
  353. package/tests/google_fonts_nonblocking.mjs +84 -0
  354. package/tests/google_sign_in.mjs +401 -0
  355. package/tests/government_board_expiry.mjs +94 -0
  356. package/tests/government_board_membership.mjs +4 -1
  357. package/tests/government_protected_surfaces.mjs +1 -1
  358. package/tests/government_routes.mjs +1 -1
  359. package/tests/government_seed.mjs +1 -0
  360. package/tests/hall_ai_studio.mjs +347 -0
  361. package/tests/hall_page_gate_map.mjs +3 -0
  362. package/tests/hall_studio_home.mjs +61 -2
  363. package/tests/home_attention_cards.mjs +1 -1
  364. package/tests/human_signal.mjs +141 -0
  365. package/tests/install_script_reader.mjs +51 -0
  366. package/tests/integration_fixture_ids.mjs +82 -0
  367. package/tests/invite_expiry_db.mjs +13 -1
  368. package/tests/lib_sh_resolution.mjs +0 -4
  369. package/tests/market_watch_data.mjs +244 -0
  370. package/tests/market_watch_data_db.mjs +308 -0
  371. package/tests/market_watch_module.mjs +114 -0
  372. package/tests/market_watch_run.mjs +503 -0
  373. package/tests/metrics.mjs +124 -0
  374. package/tests/migration_namespace.mjs +14 -0
  375. package/tests/module_api.mjs +2 -0
  376. package/tests/module_loader.mjs +1 -1
  377. package/tests/notes.mjs +182 -0
  378. package/tests/own_scope_guard.mjs +5 -0
  379. package/tests/page_tweak_marker_sql.mjs +76 -0
  380. package/tests/page_tweak_no_block.mjs +135 -0
  381. package/tests/page_tweak_notice.mjs +103 -0
  382. package/tests/payout_approvals.mjs +253 -0
  383. package/tests/payout_approvals_db.mjs +106 -0
  384. package/tests/payout_gates.mjs +322 -0
  385. package/tests/payout_gates_db.mjs +113 -0
  386. package/tests/planning_session_skill.mjs +13 -3
  387. package/tests/project_settings_page.mjs +29 -5
  388. package/tests/project_settings_spend_cap_route.mjs +235 -0
  389. package/tests/promotion_sweep.mjs +29 -1
  390. package/tests/provision.mjs +3 -2
  391. package/tests/provisioning_app_status.mjs +18 -0
  392. package/tests/provisioning_credential_preflight.mjs +53 -0
  393. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  394. package/tests/publish_reconciler_overlap.mjs +60 -0
  395. package/tests/rank_change_broadcast_port.mjs +151 -0
  396. package/tests/seed_tasks.mjs +176 -0
  397. package/tests/seed_verify_criteria.mjs +2 -6
  398. package/tests/site_classified_once.mjs +142 -0
  399. package/tests/speciality_db_shape.mjs +23 -0
  400. package/tests/speciality_session_skills.mjs +48 -2
  401. package/tests/strand_lease.mjs +249 -0
  402. package/tests/strand_watch.mjs +10 -0
  403. package/tests/task_criteria_routes.mjs +275 -0
  404. package/tests/task_criteria_unlink_db.mjs +123 -0
  405. package/tests/task_needs.mjs +233 -0
  406. package/tests/task_needs_db.mjs +110 -0
  407. package/tests/unit_runner_serial.mjs +266 -43
  408. package/tests/version_literals.mjs +5 -7
  409. package/tests/welcome_boot.mjs +16 -5
  410. package/tests/welcome_boot_browser.mjs +36 -4
  411. package/docs/recipes/bongos-cli-release.md +0 -66
  412. package/scripts/bongos/backfill-disciplines.js +0 -144
  413. package/scripts/bongos/backfill-session-earnings.js +0 -123
  414. package/scripts/bongos/backfill-session-types.js +0 -87
  415. package/scripts/bongos/build-bongos.js +0 -94
  416. package/scripts/bongos/cleanup-orphans.js +0 -74
  417. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  418. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  419. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  420. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  421. package/scripts/bongos/smoke-batch-release.sh +0 -316
  422. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  423. package/scripts/bongos/smoke-dependencies.sh +0 -143
  424. package/scripts/gds/backfill-disciplines.js +0 -7
  425. package/scripts/gds/backfill-session-earnings.js +0 -7
  426. package/scripts/gds/backfill-session-types.js +0 -7
  427. package/scripts/gds/cleanup-orphans.js +0 -7
  428. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  429. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  430. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  431. package/scripts/gds/smoke-batch-claim.sh +0 -8
  432. package/scripts/gds/smoke-batch-release.sh +0 -8
  433. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  434. package/scripts/gds/smoke-dependencies.sh +0 -8
  435. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,124 @@
1
+ // modules/lifecycle/page-notice.js — WHO IS UPDATING A PAGE, in one read (task 1004948).
2
+ //
3
+ // An artist's page claim is a marker (task 1004947), so nothing blocks anyone;
4
+ // this is the other half: telling people. `heldPagesForTouches` answers, for the
5
+ // files a task touches, which pages an artist is updating right now, who, and
6
+ // since when. `engineeringActiveOnPage` answers the reverse for an artist: whose
7
+ // engineering claims are open on files that page is made of. Information only:
8
+ // nothing here refuses, gates or writes, and a failed read answers "no notice"
9
+ // rather than throwing, because a heads-up must never break a claim.
10
+ //
11
+ // Every notice is `{ page_id, holder, since, task_id, line }`, the same shape on
12
+ // every surface (claim output, GET /me, the Conductor). Names and ids that reach a
13
+ // session as text are cleaned first: a display name is somebody else's words.
14
+ // Engineering work is reported by holder only, never by title: a task's title can
15
+ // belong to a goal the asking artist may not read (ADR 0112).
16
+ //
17
+ // Reads through the caller's executor (a pool or a transaction client) and the
18
+ // doorway's path matcher, like the rest of the claim ladder.
19
+
20
+ 'use strict';
21
+
22
+ const api = require('../../src/module-api');
23
+ const { touchesOverlap } = api.pathMatch;
24
+ const { PAGE_TWEAK_SOURCE, webClaimPredicate } = require('./page-tweak-artist.js');
25
+
26
+ // Open page markers / claims are bounded by concurrent people; the cap is a backstop.
27
+ const ROW_CAP = 500;
28
+
29
+ // 'page-tweak/builders:studio/r2' -> 'builders:studio' ('' when malformed).
30
+ function pageIdOf(sourceRef) {
31
+ const m = /^page-tweak\/([a-z0-9:-]{1,80})\/r[1-9][0-9]*$/.exec(String(sourceRef || ''));
32
+ return m ? m[1] : '';
33
+ }
34
+
35
+ // A name that is safe to print into a session's context: no control characters,
36
+ // no newlines, at most 60 characters.
37
+ function cleanName(raw, fallback) {
38
+ const s = String(raw == null ? '' : raw).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 60);
39
+ return s || fallback;
40
+ }
41
+
42
+ // One line a person or an AI session reads: gentle, never a refusal.
43
+ function noticeLine(n) {
44
+ const since = n.since ? new Date(n.since).toISOString().slice(0, 10) : 'recently';
45
+ return `${n.page_id} is being updated by ${n.holder} (since ${since}) — expect copy changes on it.`;
46
+ }
47
+
48
+ // Every open page marker, with its holder: ONE read for any number of tasks.
49
+ async function openMarkers(exec) {
50
+ const { rows } = await exec.query(
51
+ `SELECT t.id AS task_id, t.source_ref, t.touches, c.claimed_at,
52
+ COALESCE(b.display_name, b.github_login) AS holder
53
+ FROM claims c
54
+ JOIN tasks t ON t.id = c.task_id
55
+ JOIN builders b ON b.id = c.builder_id
56
+ WHERE c.released_at IS NULL
57
+ AND t.source = '${PAGE_TWEAK_SOURCE}'
58
+ AND ${webClaimPredicate('c')}
59
+ ORDER BY c.claimed_at
60
+ LIMIT ${ROW_CAP}`
61
+ );
62
+ return rows;
63
+ }
64
+
65
+ function noticesFor(touches, markers, exceptTaskId) {
66
+ if (!Array.isArray(touches) || touches.length === 0) return [];
67
+ return markers
68
+ .filter((r) => String(r.task_id) !== String(exceptTaskId) && touchesOverlap(touches, r.touches))
69
+ .map((r) => {
70
+ const n = { page_id: pageIdOf(r.source_ref), holder: cleanName(r.holder, 'an artist'), since: r.claimed_at, task_id: String(r.task_id) };
71
+ return { ...n, line: noticeLine(n) };
72
+ })
73
+ .filter((n) => n.page_id);
74
+ }
75
+
76
+ // heldPagesForTouches(touches, exec, { exceptTaskId }) -> notices (empty on no overlap or any failure)
77
+ async function heldPagesForTouches(touches, exec, { exceptTaskId = null } = {}) {
78
+ if (!Array.isArray(touches) || touches.length === 0) return [];
79
+ try {
80
+ return noticesFor(touches, await openMarkers(exec), exceptTaskId);
81
+ } catch (_) {
82
+ return [];
83
+ }
84
+ }
85
+
86
+ // noticesForClaims(claims, exec) -> Map(task_id -> notices): ONE read for all of a
87
+ // builder's claims (GET /me), instead of one per claim. A claim with no touches or
88
+ // no overlap maps to [].
89
+ async function noticesForClaims(claims, exec = api.pool) {
90
+ const out = new Map();
91
+ const list = Array.isArray(claims) ? claims : [];
92
+ for (const c of list) out.set(String(c.task_id), []);
93
+ if (list.length === 0) return out;
94
+ try {
95
+ const markers = await openMarkers(exec);
96
+ for (const c of list) out.set(String(c.task_id), noticesFor(c.touches, markers, c.task_id));
97
+ } catch (_) { /* heads-up only */ }
98
+ return out;
99
+ }
100
+
101
+ // engineeringActiveOnPage(pageTouches, exec) -> [{ task_id, holder }]
102
+ // Open worktree claims on real code work (never a page round, whoever holds it)
103
+ // whose files overlap the page's files. No title: see the header.
104
+ async function engineeringActiveOnPage(pageTouches, exec = api.pool) {
105
+ if (!Array.isArray(pageTouches) || pageTouches.length === 0) return [];
106
+ try {
107
+ const { rows } = await exec.query(
108
+ `SELECT t.id AS task_id, t.touches, COALESCE(b.display_name, b.github_login) AS holder
109
+ FROM claims c
110
+ JOIN tasks t ON t.id = c.task_id
111
+ JOIN builders b ON b.id = c.builder_id
112
+ WHERE c.released_at IS NULL
113
+ AND t.source IS DISTINCT FROM '${PAGE_TWEAK_SOURCE}'
114
+ LIMIT ${ROW_CAP}`
115
+ );
116
+ return rows
117
+ .filter((r) => touchesOverlap(pageTouches, r.touches))
118
+ .map((r) => ({ task_id: String(r.task_id), holder: cleanName(r.holder, 'an engineer') }));
119
+ } catch (_) {
120
+ return [];
121
+ }
122
+ }
123
+
124
+ module.exports = { heldPagesForTouches, noticesForClaims, engineeringActiveOnPage };
@@ -24,6 +24,23 @@ function webClaimPredicate(alias = 'c') {
24
24
  return `${alias}.worktree_name IS NULL AND ${alias}.creator_session_id IS NULL`;
25
25
  }
26
26
 
27
+ // A PAGE MARKER is an artist's web claim on a page-tweak round: it says "this
28
+ // page is being updated" and nothing more. The file-overlap checks that guard
29
+ // engineering work against engineering work (the claimable feed, claimTask,
30
+ // the batch claim and its dry-run) leave markers out, so an artist writing a
31
+ // page never hides or refuses coding work on the same files. That includes
32
+ // claimTask's module-disjointness gate, which reads the same active-claim rows:
33
+ // a page round carries no module_key, and a marker must never hold a module.
34
+ // Returns a SQL condition that is true for every claim that is NOT a marker.
35
+ // COALESCE keeps it TRUE (counted) when tasks.source is NULL: without it a
36
+ // both-NULL claim on a source-less task would evaluate to NULL and vanish.
37
+ // src/bongos/optimizer.js carries a hand copy (core cannot require a module);
38
+ // tests/page_tweak_marker_sql.mjs pins the two together.
39
+ // Aliases are code literals at every call site; never pass request input.
40
+ function notPageMarkerSql(claimAlias = 'c', taskAlias = 't') {
41
+ return `NOT (COALESCE(${taskAlias}.source, '') = '${PAGE_TWEAK_SOURCE}' AND ${webClaimPredicate(claimAlias)})`;
42
+ }
43
+
27
44
  // The newest claim first; the id breaks a same-instant tie the same way in
28
45
  // every reader.
29
46
  function latestFirst(alias = 'c') {
@@ -83,4 +100,4 @@ function helperJoinSql(alias = 't') {
83
100
  ) helper ON true`;
84
101
  }
85
102
 
86
- module.exports = { PAGE_TWEAK_SOURCE, webClaimPredicate, latestFirst, artistIdSql, shippedBySql, helperJoinSql };
103
+ module.exports = { PAGE_TWEAK_SOURCE, webClaimPredicate, notPageMarkerSql, latestFirst, artistIdSql, shippedBySql, helperJoinSql };
@@ -852,8 +852,6 @@ function loggers(deps = {}) {
852
852
  return { logFn: deps.log || console.log, errFn: deps.error || console.error };
853
853
  }
854
854
 
855
- // A single crash handler for the tick's fire-and-forget sweeps — same reason.
856
- const crashHandler = (name) => (e) => loggers().errFn(`[publish-reconciler] ${name} crashed: ${e && e.message}`);
857
855
 
858
856
  async function runCostSweep(deps = {}) {
859
857
  const { logFn, errFn } = loggers(deps);
@@ -958,19 +956,23 @@ async function runPromotionSweep(deps = {}) {
958
956
 
959
957
  // The craft-achievement backfill (task 1004438, WA6.RP08): the one badge a
960
958
  // migration cannot backfill, "A Whole Surface", needs the page inventory, which is
961
- // a file the database never sees. Run ONCE per process start, on the first tick,
962
- // through the reward port (economy is optional; no port, no sweep). Pure DB and
963
- // idempotent, so a restart that runs it again unlocks nothing twice; a failure is
964
- // logged and never touches the other sweeps.
965
- let craftBackfillDone = false;
959
+ // a file the database never sees. Runs HOURLY (task 1004965) through the reward
960
+ // port (economy is optional; no port, no sweep), and INCREMENTALLY: the first run
961
+ // of a process scans every craft-paid builder and hands back a watermark (the
962
+ // highest credit_log id it covered); every later run asks only about builders
963
+ // paid since that id, so a caught-up backfill is one indexed read that finds
964
+ // nothing. Pure DB and idempotent, so a restart that rescans unlocks nothing
965
+ // twice; a failure keeps the watermark (the next run retries the same span) and
966
+ // never touches the other sweeps.
967
+ let craftWatermark = 0;
966
968
  async function runCraftAchievementBackfill(deps = {}) {
967
969
  const { logFn, errFn } = loggers(deps);
968
- if (craftBackfillDone && !deps.forceCraftBackfill) return { skipped: 'already_ran' };
969
- craftBackfillDone = true;
970
970
  const reward = deps.reward || seams.resolveOptional('reward');
971
971
  if (!reward || typeof reward.backfillCraftAchievements !== 'function') return { skipped: 'unsupported' };
972
972
  try {
973
- const out = await reward.backfillCraftAchievements({});
973
+ const out = await reward.backfillCraftAchievements({ sinceId: craftWatermark });
974
+ const next = out ? Number(out.watermark) : NaN;
975
+ if (Number.isFinite(next) && next > craftWatermark) craftWatermark = next;
974
976
  if (out && out.unlocked) logFn(`[publish-reconciler] craft achievements: ${out.unlocked} unlocked across ${out.checked} builder(s)`);
975
977
  return out;
976
978
  } catch (e) {
@@ -979,6 +981,57 @@ async function runCraftAchievementBackfill(deps = {}) {
979
981
  }
980
982
  }
981
983
 
984
+ // The sweeps one tick fires. Each is INDEPENDENT on purpose: the cost (task 1366)
985
+ // and criterion (task 1002069) sweeps are pure DB — no push credential — and a
986
+ // failure in either must never touch the confirmed→shipped work.
987
+ //
988
+ // CADENCE PER SWEEP (task 1004965): an entry's third element is its minimum gap
989
+ // between runs; none means every tick (INTERVAL_MS). The publish, cost and
990
+ // criterion sweeps keep the tick — a strand, an unpriced session and a newly
991
+ // satisfiable criterion are all things someone is waiting on. The promotion sweep
992
+ // and the craft backfill run HOURLY: the ship-time triggers already promote and
993
+ // unlock on the event, so these are backlog drains, and a drain an hour late
994
+ // costs nobody anything while running it every 5 minutes cost every instance
995
+ // ~288 scans a day.
996
+ const HOURLY_MS = 60 * 60_000;
997
+ const SWEEPS = [
998
+ ['sweep', runSweep],
999
+ ['cost sweep', runCostSweep],
1000
+ ['criterion sweep', runCriterionSweep],
1001
+ ['promotion sweep', runPromotionSweep, HOURLY_MS],
1002
+ ['craft achievement backfill', runCraftAchievementBackfill, HOURLY_MS],
1003
+ ];
1004
+
1005
+ // buildTick — one interval round, with a per-sweep overlap guard (task 1004924).
1006
+ // A sweep whose previous run has not settled is SKIPPED this tick, so a slow DB
1007
+ // or GitHub call can never stack rounds and hold several pool connections at
1008
+ // once. The flag is per sweep, so one slow sweep never starves the others, and
1009
+ // it clears whether the run resolves or rejects. `sweeps` is the test seam.
1010
+ //
1011
+ // A sweep with a cadence runs on the first tick, then again once its gap has
1012
+ // passed. Half a tick of slack, so timer jitter (a tick landing a few ms early)
1013
+ // never pushes an hourly sweep a whole tick late. `now` is the clock seam.
1014
+ function buildTick(deps = {}, sweeps = SWEEPS, { now = Date.now, tickMs = INTERVAL_MS } = {}) {
1015
+ const inFlight = new Set();
1016
+ const lastRun = new Map();
1017
+ return () => {
1018
+ const t = now();
1019
+ for (const [name, fn, everyMs] of sweeps) {
1020
+ if (everyMs && lastRun.has(name) && t - lastRun.get(name) < everyMs - tickMs / 2) continue; // not due yet
1021
+ if (inFlight.has(name)) {
1022
+ loggers(deps).logFn(`[publish-reconciler] ${name} still running from the last tick — skipped`);
1023
+ continue;
1024
+ }
1025
+ lastRun.set(name, t);
1026
+ inFlight.add(name);
1027
+ Promise.resolve()
1028
+ .then(() => fn(deps))
1029
+ .catch((e) => loggers(deps).errFn(`[publish-reconciler] ${name} crashed: ${e && e.message}`))
1030
+ .finally(() => inFlight.delete(name));
1031
+ }
1032
+ };
1033
+ }
1034
+
982
1035
  // Start the periodic sweep. Idempotent (a second call is a no-op). Kill-switched
983
1036
  // by PUBLISH_RECONCILER_DISABLED=1 (purely additive bookkeeping, so it's ON by
984
1037
  // default whenever a push credential is configured; the route's getPublishStatus
@@ -991,16 +1044,7 @@ function start(deps = {}) {
991
1044
  }
992
1045
  if (intervalTimer) return; // already started
993
1046
 
994
- // Each sweep is INDEPENDENT on purpose: the cost (task 1366) and criterion
995
- // (task 1002069) sweeps are pure DB — no push credential — and a failure in
996
- // either must never touch the confirmed→shipped work.
997
- const tick = () => {
998
- runSweep(deps).catch(crashHandler('sweep'));
999
- runCostSweep(deps).catch(crashHandler('cost sweep'));
1000
- runCriterionSweep(deps).catch(crashHandler('criterion sweep'));
1001
- runPromotionSweep(deps).catch(crashHandler('promotion sweep'));
1002
- runCraftAchievementBackfill(deps).catch(crashHandler('craft achievement backfill'));
1003
- };
1047
+ const tick = buildTick(deps);
1004
1048
 
1005
1049
  firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
1006
1050
  if (firstTimer.unref) firstTimer.unref();
@@ -1039,7 +1083,10 @@ module.exports = {
1039
1083
  runPromotionSweep,
1040
1084
  runCriterionSweep,
1041
1085
  runCraftAchievementBackfill,
1086
+ buildTick,
1042
1087
  couldBeStaleGenerated,
1043
1088
  describeTarget,
1044
1089
  INTERVAL_MS,
1090
+ HOURLY_MS,
1091
+ SWEEPS,
1045
1092
  };
@@ -305,7 +305,7 @@ module.exports = function buildClaimsRouter() {
305
305
  enforceOverlap: enforceTouches === true,
306
306
  autoPickup: autoPickup === true,
307
307
  });
308
- const { just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingOverlapWarning, ...claim } = claimResult;
308
+ const { just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingOverlapWarning, page_notice: pageNotice, ...claim } = claimResult;
309
309
  const task = await db.getTask(Number(taskId));
310
310
  // task 1168: keep the evergreen newcomer-chore shelf full. If this claim
311
311
  // took a chore instance, stamp a replacement NOW (before responding) so
@@ -339,7 +339,7 @@ module.exports = function buildClaimsRouter() {
339
339
  const p = rw && typeof rw.rewardConfig === 'function' ? rw.rewardConfig() : null;
340
340
  if (p && typeof p.mode === 'string') rewardMode = { mode: p.mode, perTaskCredit: p.perTaskCredit !== false };
341
341
  } catch (_) { /* display-only: a claim must never fail over this */ }
342
- res.status(201).json({ claim, task, just_unlocked: justUnlocked ?? [], overlap_warning: overlapWarning ?? null, sibling_overlap_warning: siblingOverlapWarning ?? null, reward: rewardMode });
342
+ res.status(201).json({ claim, task, just_unlocked: justUnlocked ?? [], overlap_warning: overlapWarning ?? null, sibling_overlap_warning: siblingOverlapWarning ?? null, page_notice: pageNotice ?? [], reward: rewardMode });
343
343
  } catch (err) {
344
344
  if (err && err.code) {
345
345
  const status =
@@ -388,6 +388,9 @@ module.exports = function buildClaimsRouter() {
388
388
  // block, not an authorization refusal). The refusal object already
389
389
  // carries `message` + `hint`, so no extra hint-building step needed.
390
390
  err.code === 'RELEVANCE_REVIEW_PENDING' ? 409 :
391
+ // task 1004950 / ADR 0395: the task needs hands on the live server and
392
+ // the claimer holds no server grant — an authorization refusal, like rank.
393
+ err.code === 'NEEDS_SERVER_ACCESS' ? 403 :
391
394
  // ADR 0371 D5/D6: a marked auto-pickup claim the project's switch, monthly
392
395
  // cap or per-round limit refuses. The round waits for a person.
393
396
  err.code === 'AUTO_PICKUP_NOT_A_TWEAK' ? 409 :
@@ -1333,6 +1333,7 @@ module.exports = function buildGoalsRouter() {
1333
1333
  discipline: { type: 'string', maxLength: 64 },
1334
1334
  value_summary: { type: 'string', maxLength: LIMITS.VALUE_SUMMARY },
1335
1335
  parent_task_id: { type: 'integer', min: 1 },
1336
+ needs: {}, // task 1004950 / ADR 0395 — ['owner'|'server']; a need narrows who is shown the task, it grants nothing
1336
1337
  // Deliberately NOT read from the client (validateOrRespond ignores unknown
1337
1338
  // fields, so any of these a client smuggles in are simply never used):
1338
1339
  // • version_id — derived from the goal (a member can't author cross-version);
@@ -1343,6 +1344,7 @@ module.exports = function buildGoalsRouter() {
1343
1344
  })) return;
1344
1345
 
1345
1346
  const body = req.body || {};
1347
+ const goalNeeds = api.taskNeeds.normalizeNeeds(body.needs); if (!goalNeeds.ok) return require('./task-route-helpers.js').badNeeds(res, goalNeeds.bad);
1346
1348
  const touches = Array.isArray(body.touches) ? [...body.touches] : [];
1347
1349
 
1348
1350
  try {
@@ -1425,7 +1427,7 @@ module.exports = function buildGoalsRouter() {
1425
1427
  kind,
1426
1428
  discipline,
1427
1429
  source: 'goal-authored',
1428
- goalId: goal.id,
1430
+ goalId: goal.id, needs: goalNeeds.needs,
1429
1431
  // task 1003278 — attribution from the authenticated request (never the body).
1430
1432
  createdBy: req.builder.id,
1431
1433
  });
@@ -0,0 +1,91 @@
1
+ // modules/lifecycle/routes/notes.js — notes on a task or a goal, read grouped by
2
+ // purpose (task 1004745, goal 1000095).
3
+ //
4
+ // Four routes: read the notes on one task or goal (already grouped), add one, re-file
5
+ // one under another purpose, and delete one. The grouping, the subject rule, who may
6
+ // see a private goal's notes and who may change a note are all notes.js's; this surface
7
+ // only gathers the request and renders the verdict.
8
+ //
9
+ // A note nobody may see answers 404, exactly as a missing one does, so the answer
10
+ // never tells a non-member that a private goal has notes. Every write here is recorded
11
+ // by the server's audit middleware, so a Metic's delete leaves a trail.
12
+ //
13
+ // Reaches core only through the doorway (ADR 0083).
14
+
15
+ 'use strict';
16
+
17
+ const express = require('express');
18
+ const api = require('../../../src/module-api');
19
+ const notes = require('../notes');
20
+
21
+ const { asyncHandler, parseId, validateOrRespond } = api;
22
+
23
+ module.exports = function buildNotesRouter() {
24
+ const router = express.Router();
25
+
26
+ // rank: any-builder — notes are shared working information, readable by anyone
27
+ // signed in; a private goal's notes are narrowed to its members by notes.canSee.
28
+ router.get('/notes', api.requireBuilder, asyncHandler('GET /notes', async (req, res) => {
29
+ const subject = notes.subjectOf(req.query || {});
30
+ if (!subject) return res.fail('invalid_subject', 400, { hint: 'give exactly one of task_id or goal_id' });
31
+ if (!(await notes.canSee(req.builder, subject))) return res.fail('not_found', 404);
32
+ const rows = await notes.listNotes(subject);
33
+ return res.json({ count: rows.length, groups: notes.groupNotes(rows), kinds: notes.KINDS });
34
+ }, { errorCode: 'notes_read_failed' }));
35
+
36
+ // rank: any-builder — adding information is not a privileged act; the author is
37
+ // the caller and is never taken from the body.
38
+ router.post('/notes', api.requireBuilder, asyncHandler('POST /notes', async (req, res) => {
39
+ if (validateOrRespond(req, res, {
40
+ body: { required: true, type: 'string', minLength: 1, maxLength: notes.MAX_BODY },
41
+ kind: { type: 'string', maxLength: 16 },
42
+ task_id: { type: 'integer', min: 1 },
43
+ goal_id: { type: 'integer', min: 1 },
44
+ })) return;
45
+ const body = req.body || {};
46
+ const v = notes.validateNote(body);
47
+ if (!v.ok) return res.fail('invalid_note', 400, { errors: v.errors, kinds: notes.KIND_KEYS });
48
+ const subject = notes.subjectOf(body);
49
+ if (!(await notes.canSee(req.builder, subject))) return res.fail('not_found', 404);
50
+ if ((await notes.countNotes(subject)) >= notes.MAX_NOTES_PER_SUBJECT) {
51
+ return res.fail('note_limit', 409, { limit: notes.MAX_NOTES_PER_SUBJECT });
52
+ }
53
+ const note = await notes.addNote({
54
+ taskId: subject.taskId ?? null,
55
+ goalId: subject.goalId ?? null,
56
+ authorId: req.builder.id,
57
+ body: v.body,
58
+ kind: v.kind,
59
+ kindSource: v.kindSource,
60
+ });
61
+ return res.status(201).json({ ok: true, note });
62
+ }, { errorCode: 'note_create_failed' }));
63
+
64
+ // rank: any-builder, narrowed in the handler — the author re-files their own note;
65
+ // a Metic or Archon may re-file anyone's (the grouping is a shared view).
66
+ router.patch('/notes/:id', api.requireBuilder, asyncHandler('PATCH /notes/:id', async (req, res) => {
67
+ if (validateOrRespond(req, res, { kind: { required: true, type: 'string', maxLength: 16 } })) return;
68
+ const id = parseId(req.params.id);
69
+ const kind = req.body.kind;
70
+ if (id == null) return res.fail('invalid_id', 400);
71
+ if (!notes.KIND_KEYS.includes(kind)) return res.fail('invalid_note', 400, { errors: [{ field: 'kind', reason: 'pick one of the listed groups' }], kinds: notes.KIND_KEYS });
72
+ const existing = await notes.getNote(id);
73
+ if (!existing || !(await notes.canSee(req.builder, notes.subjectOf(existing)))) return res.fail('not_found', 404);
74
+ if (!notes.canChange(req.builder, existing)) return res.fail('not_yours', 403);
75
+ const row = await notes.setKind(id, kind);
76
+ return res.json({ ok: true, note: row });
77
+ }, { errorCode: 'note_update_failed' }));
78
+
79
+ // rank: any-builder, narrowed in the handler exactly as the re-file above.
80
+ router.delete('/notes/:id', api.requireBuilder, asyncHandler('DELETE /notes/:id', async (req, res) => {
81
+ const id = parseId(req.params.id);
82
+ if (id == null) return res.fail('invalid_id', 400);
83
+ const existing = await notes.getNote(id);
84
+ if (!existing || !(await notes.canSee(req.builder, notes.subjectOf(existing)))) return res.fail('not_found', 404);
85
+ if (!notes.canChange(req.builder, existing)) return res.fail('not_yours', 403);
86
+ await notes.deleteNote(id);
87
+ return res.json({ ok: true });
88
+ }, { errorCode: 'note_delete_failed' }));
89
+
90
+ return router;
91
+ };
@@ -7,8 +7,9 @@
7
7
  // WHY THIS ROUTE EXISTS. Goal 1000072's thesis is that this project is legible to
8
8
  // Claude and illegible to a human: the system knows everything and exposes almost
9
9
  // none of it in a shape a person can hold. The five values here — the rot timer, the
10
- // category set, how credits are calculated, whether equity is real, and what share of
11
- // upmarket revenue is paid on credits — were each answerable only by reading source.
10
+ // category set, how credits are calculated, what credits are (and are not) next to
11
+ // equity, and whether a credit-weighted pool can pay out — were each answerable only
12
+ // by reading source.
12
13
  // This route is the TRANSLATION LAYER, not a new capability: it invents no value and
13
14
  // stores nothing of its own.
14
15
  //
@@ -20,10 +21,11 @@
20
21
  // and say why.
21
22
  //
22
23
  // AND THE ONE THAT KEEPS IT HONEST: a fundamental with no knob behind it is reported
23
- // as ABSENT, never as a plausible zero. `upmarket_percent` is that row today — the
24
- // task asked for it, and no such knob exists anywhere in this codebase. Rendering it
25
- // as "0%" would be a fabricated fact about the project's economics; saying "there is
26
- // no such setting yet, and here is where it would live" is the legible answer.
24
+ // as ABSENT or GATED, never as a plausible zero. `upmarket_percent` (shown as the
25
+ // credit-weighted pool) is that row today: no such knob exists, and ADR 0385 gates
26
+ // the pay type behind counsel and a signed rule. Rendering it as "0%" would be a
27
+ // fabricated fact about the project's economics; saying "not available, and here is
28
+ // what gates it" is the legible answer.
27
29
  //
28
30
  // MODULE BOUNDARY: reaches core only through the published doorway
29
31
  // `../../../src/module-api`; `../goal-category` is an intra-module sibling.
@@ -118,58 +120,64 @@ function creditRow() {
118
120
  // (ADR 0146). A page that showed the estimate without that sentence would be telling
119
121
  // a builder they will earn a number this instance has decided not to pay.
120
122
  const estimate = policy.perTaskCredit
121
- ? 'A task’s own credit estimate is also paid, scaled by its kind.'
122
- : 'A task’s credit estimate is an advertised guide only — this instance pays cost-plus and nothing else.';
123
+ ? 'A task’s own credit estimate is also awarded, scaled by its kind.'
124
+ : 'A task’s credit estimate is an advertised guide only — this instance awards cost-plus credits and nothing else.';
125
+ // Wording per ADR 0387: the formula's INPUT is USD, so the credits are "sized by"
126
+ // the session's cost — never "worth" dollars, and never money.
123
127
  return {
124
128
  key: 'credit_calculation',
125
129
  title: 'Credit calculation',
126
130
  editable: false,
127
131
  state: 'live',
128
132
  value: { marginPct: policy.marginPct, usdPeg: policy.usdPeg, mode: policy.mode, perTaskCredit: policy.perTaskCredit },
129
- sentence: `A shipped session pays round(true cost in USD × ${mult}${pegNote}). ${estimate}`,
133
+ sentence: `A shipped session earns round(true cost in USD × ${mult}${pegNote}) credits, sized by the session’s cost in USD. Credits measure contribution; they are not money. ${estimate}`,
130
134
  source: 'branding pack → `reward`',
131
135
  reads: { file: 'config/branding.json → reward', adr: 'ADR 0054, ADR 0146' },
132
136
  };
133
137
  }
134
138
 
135
- // Equity. THE OWNER DECIDED THIS (2026-08-25) and the decision is that it is NOT REAL:
136
- // credits ARE the ownership stream today, and no legal equity is issued. The page says
137
- // that plainly rather than implying a cap table exists — an owner reading their own
138
- // settings page must not come away believing shares are being issued. If issuance ever
139
- // becomes real it gets its own decision record; the Board Room spec already parks
140
- // Howey / Subchapter T explicitly out of scope.
139
+ // Equity. Bongos issues none, and credits are NOT equity. The owner's 2026-08-25
140
+ // decision (no legal equity issued) stands; the older ADR 0054 framing that a credit
141
+ // IS a unit of ownership was superseded on 2026-10-29 (ADR 0385 decisions 2-3, ADR
142
+ // 0387): credits are a project-scoped contribution meter, and equity, if a project
143
+ // offers any, is that project's own grant, one signature each. The page says that
144
+ // plainly — an owner reading their own settings page must not come away believing
145
+ // credits are shares or that a cap table exists. The key stays `equity_calculation`
146
+ // so existing readers keep working.
141
147
  function equityRow() {
142
148
  return {
143
149
  key: 'equity_calculation',
144
- title: 'Equity calculation',
150
+ title: 'Equity',
145
151
  editable: false,
146
152
  state: 'not-yet-real',
147
- sentence: 'No legal equity is issued. Credits are the ownership stream this project actually runs — '
148
- + 'the owner’s framing in ADR 0054 is that a credit IS a unit of ownership. There is no cap table, '
149
- + 'no share class and no issuance behind this row.',
150
- source: 'owner decision, 2026-08-25',
151
- reads: { file: 'docs/adr/0054-drachmae-token-reward.md', adr: 'ADR 0054' },
153
+ sentence: 'Credits are a contribution meter, not ownership: they measure work on this project '
154
+ + 'and are never money, never redeemable and never equity. No legal equity is issued here. '
155
+ + 'Equity, if any, is granted by the project separately, one signed grant at a time — '
156
+ + 'there is no cap table and no share class behind this row.',
157
+ source: 'owner decisions, 2026-08-25 and 2026-10-29',
158
+ reads: { file: 'docs/adr/0054-session-token-reward-cost-plus.md', adr: 'ADR 0054 (superseded in part), ADR 0385, ADR 0387' },
152
159
  };
153
160
  }
154
161
 
155
- // The share of upmarket revenue paid out on credits. NO SUCH KNOB EXISTS — searched
156
- // across code, config, migrations and docs on this task and found only prose in an
157
- // unrelated competitive-analysis file. So this row reports ABSENCE and names where the
158
- // setting would live if it became real, which is the actionable fact. A rendered "0%"
159
- // would be a fabricated economic claim; an omitted row would leave the owner to
160
- // rediscover the gap by grep.
162
+ // The credit-weighted pool: a project dividing a pool of its OWN money by credit
163
+ // share (ADR 0387 decision 3). The row key stays `upmarket_percent` (its first name,
164
+ // "share of upmarket revenue paid on credits") so existing readers keep working.
165
+ // NO SUCH KNOB EXISTS, and ADR 0385's pay-type gates hold this one shut until the
166
+ // wording rule is in force, the model's inputs are visible and a signed payout rule
167
+ // exists — with counsel's answer on held item 2 first. So the row reports GATED,
168
+ // never a rendered "0%", which would be a fabricated economic claim.
161
169
  function upmarketRow() {
162
170
  return {
163
171
  key: 'upmarket_percent',
164
- title: 'Percent of upmarket revenue paid on credits',
172
+ title: 'Credit-weighted pool',
165
173
  editable: false,
166
- state: 'absent',
167
- sentence: 'This project has no such setting yet — nothing in the code, config or database '
168
- + 'defines it, so there is no number to show. It would belong beside the other reward '
169
- + 'terms in the branding pack’s `reward` block, and paying against it would need its own '
170
- + 'decision record first.',
171
- source: 'not configured anywhere',
172
- reads: { file: 'config/branding.json → reward (where it would live)', adr: null },
174
+ state: 'gated',
175
+ sentence: 'Not available (gated). A project could one day divide a pool of its own money by '
176
+ + 'credit share, but that pay type stays switched off until counsel clears it and the '
177
+ + 'project signs a payout rule. Bongos sets no share and no rate, and no dollar value is '
178
+ + 'ever read off a credit.',
179
+ source: 'gated by ADR 0385 (pay-type gates)',
180
+ reads: { file: 'docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md', adr: 'ADR 0385, ADR 0387' },
173
181
  };
174
182
  }
175
183
 
@@ -251,10 +251,31 @@ function badKind(res) {
251
251
  }
252
252
 
253
253
 
254
+ // task 1004950 / ADR 0395: a task's needs are a subset of TASK_NEEDS. Shared by
255
+ // POST /tasks, PATCH /tasks/:id and POST /goals/:id/tasks so all three refuse alike.
256
+ function badNeeds(res, bad) {
257
+ const valid = api.taskNeeds.TASK_NEEDS;
258
+ return res.fail('bad_needs', { status: 400, message: `needs must list only: ${valid.join(', ')} (unknown: ${bad.join(', ')}). Most tasks need neither.`, details: { valid } });
259
+ }
260
+
261
+ // ?needs= on GET /tasks and GET /tasks/claimable (task 1004950): one need
262
+ // ('owner' | 'server') or 'none'. Shared so the two lists agree on the vocabulary.
263
+ const NEEDS_FILTERS = new Set([...api.taskNeeds.TASK_NEEDS, 'none']);
264
+ function parseNeedsFilter(raw) {
265
+ if (raw === undefined || raw === '') return undefined;
266
+ return NEEDS_FILTERS.has(raw) ? raw : { bad: 'bad_needs' };
267
+ }
268
+ function badNeedsFilter(res) {
269
+ return res.fail('bad_needs', { status: 400, message: `needs must be one of ${[...NEEDS_FILTERS].join(' | ')}.` });
270
+ }
271
+
254
272
  module.exports = {
273
+ badNeedsFilter,
274
+ parseNeedsFilter,
255
275
  MIGRATION_KEYWORD_RE,
256
276
  VALID_FITS,
257
277
  badKind,
278
+ badNeeds,
258
279
  deHashRefs,
259
280
  detectsMigration,
260
281
  gateTaskOwnership,