@bongos/core 1.23.21 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/.bongos-core.json +983 -428
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +6 -3
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +94 -0
  27. package/clients/bongos-client/index.cjs +94 -0
  28. package/clients/bongos-client/index.d.ts +154 -4
  29. package/clients/bongos-client/index.mjs +94 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
  33. package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
  34. package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
  35. package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
  36. package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
  37. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  38. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  39. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  40. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  41. package/docs/adr/README.md +10 -1
  42. package/docs/api/openapi.json +8619 -5885
  43. package/docs/api-reference.md +68 -9
  44. package/docs/architecture.md +37 -1
  45. package/docs/canonical-permissions.md +1 -1
  46. package/docs/copy-inventory.md +420 -337
  47. package/docs/copy-registry.json +1321 -500
  48. package/docs/file-map.md +4 -0
  49. package/docs/module-api-changelog.md +7 -0
  50. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  51. package/docs/onboarding/diagrams/README.md +1 -1
  52. package/docs/onboarding/drachmae-and-karma.md +15 -15
  53. package/docs/onboarding/primer.md +2 -2
  54. package/docs/page-inventory.json +55 -6
  55. package/docs/page-readings.json +1185 -1011
  56. package/docs/recipes/google-sign-in.md +60 -0
  57. package/docs/recipes/ops-gotchas.md +1 -1
  58. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  59. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  60. package/migrations/core_283_payout_approvals.sql +183 -0
  61. package/migrations/core_284_payout_gates.sql +179 -0
  62. package/migrations/core_285_task_needs.sql +74 -0
  63. package/modules/ai-studio/CLAUDE.md +15 -0
  64. package/modules/ai-studio/billing.js +67 -0
  65. package/modules/ai-studio/context.js +29 -0
  66. package/modules/ai-studio/db.js +172 -0
  67. package/modules/ai-studio/gemini.js +75 -0
  68. package/modules/ai-studio/generate.js +119 -0
  69. package/modules/ai-studio/keys.js +73 -0
  70. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  71. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  72. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  73. package/modules/ai-studio/module.json +26 -0
  74. package/modules/ai-studio/openrouter.js +90 -0
  75. package/modules/ai-studio/payments.js +105 -0
  76. package/modules/ai-studio/pricing.js +92 -0
  77. package/modules/ai-studio/routes/ai-studio.js +252 -0
  78. package/modules/ai-studio/studio.js +121 -0
  79. package/modules/autonomy/fence.js +64 -1
  80. package/modules/builder-settings/builder-needs.js +27 -1
  81. package/modules/copy-desk/routes/copy-desk.js +5 -0
  82. package/modules/economy/achievements.js +22 -10
  83. package/modules/economy/cost-classify.js +9 -0
  84. package/modules/economy/cost-write-guard.js +52 -0
  85. package/modules/economy/cost.js +2 -1
  86. package/modules/economy/credits.js +18 -15
  87. package/modules/economy/goal-reward.js +161 -0
  88. package/modules/economy/reward.js +4 -0
  89. package/modules/economy/routes/cost.js +33 -7
  90. package/modules/government/catalog.js +6 -0
  91. package/modules/government/db.js +108 -0
  92. package/modules/government/docket.js +36 -0
  93. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  94. package/modules/government/pollers/board-expiry.js +108 -20
  95. package/modules/government/protected-surfaces.json +1 -0
  96. package/modules/government/routes/government.js +50 -18
  97. package/modules/government/routing.js +39 -5
  98. package/modules/government/waiting.js +74 -0
  99. package/modules/grading/grader-subagent.js +30 -11
  100. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  101. package/modules/hall-ui/public/ai-studio.css +193 -0
  102. package/modules/hall-ui/public/ai-studio.html +98 -0
  103. package/modules/hall-ui/public/ai-studio.js +556 -0
  104. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  105. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  106. package/modules/hall-ui/public/blockers-lib.js +15 -1
  107. package/modules/hall-ui/public/blockers.html +8 -0
  108. package/modules/hall-ui/public/blockers.js +1 -1
  109. package/modules/hall-ui/public/city.html +15 -15
  110. package/modules/hall-ui/public/claim-action.js +3 -0
  111. package/modules/hall-ui/public/collab.html +12 -0
  112. package/modules/hall-ui/public/craft-mode.js +4 -1
  113. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  114. package/modules/hall-ui/public/drachmae.html +1 -1
  115. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  120. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  121. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  122. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  123. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  124. package/modules/hall-ui/public/goal-inbox.js +1 -1
  125. package/modules/hall-ui/public/goals-render.js +4 -0
  126. package/modules/hall-ui/public/goals.html +8 -0
  127. package/modules/hall-ui/public/gov-living.js +6 -2
  128. package/modules/hall-ui/public/hall-ask.css +5 -0
  129. package/modules/hall-ui/public/hall-ask.js +39 -3
  130. package/modules/hall-ui/public/notes-panel.css +20 -0
  131. package/modules/hall-ui/public/notes-panel.js +146 -0
  132. package/modules/hall-ui/public/project-settings.html +1 -1
  133. package/modules/hall-ui/public/project-settings.js +7 -5
  134. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  135. package/modules/hall-ui/public/settings.html +15 -0
  136. package/modules/hall-ui/public/street-deck.js +12 -3
  137. package/modules/hall-ui/public/studio.css +90 -8
  138. package/modules/hall-ui/public/studio.html +39 -10
  139. package/modules/hall-ui/public/studio.js +148 -18
  140. package/modules/hall-ui/public/task.html +2 -0
  141. package/modules/hall-ui/public/task.js +4 -0
  142. package/modules/hall-ui/public/tweak-editor.js +12 -3
  143. package/modules/hall-ui/public/welcome-boot.css +42 -6
  144. package/modules/hall-ui/public/welcome-boot.js +20 -6
  145. package/modules/hall-ui/public/work.js +20 -0
  146. package/modules/hall-ui/records/ai-studio.md +36 -0
  147. package/modules/hall-ui/records/blockers-people.md +24 -0
  148. package/modules/lifecycle/db-analytics.js +10 -3
  149. package/modules/lifecycle/db-claims.js +32 -4
  150. package/modules/lifecycle/db-goals.js +48 -18
  151. package/modules/lifecycle/db-shared.js +9 -0
  152. package/modules/lifecycle/db-tasks.js +15 -6
  153. package/modules/lifecycle/db-versions.js +21 -8
  154. package/modules/lifecycle/done-when.js +23 -1
  155. package/modules/lifecycle/lifecycle.js +5 -0
  156. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  157. package/modules/lifecycle/module.json +2 -1
  158. package/modules/lifecycle/notes.js +219 -0
  159. package/modules/lifecycle/page-notice.js +124 -0
  160. package/modules/lifecycle/page-tweak-artist.js +18 -1
  161. package/modules/lifecycle/publish-reconciler.js +67 -20
  162. package/modules/lifecycle/routes/claims.js +5 -2
  163. package/modules/lifecycle/routes/goals.js +3 -1
  164. package/modules/lifecycle/routes/notes.js +91 -0
  165. package/modules/lifecycle/routes/project-settings.js +43 -35
  166. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  167. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  168. package/modules/lifecycle/routes/tasks.js +12 -12
  169. package/modules/lifecycle/ship-assurance.js +4 -1
  170. package/modules/lifecycle/ship-card.js +8 -2
  171. package/modules/lifecycle/task-needs-gate.js +70 -0
  172. package/modules/market-watch/CLAUDE.md +15 -0
  173. package/modules/market-watch/db.js +340 -0
  174. package/modules/market-watch/key.js +58 -0
  175. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  176. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  177. package/modules/market-watch/module.json +26 -0
  178. package/modules/market-watch/pollers/hub-seed.js +54 -0
  179. package/modules/market-watch/pollers/weekly-run.js +100 -0
  180. package/modules/market-watch/research.js +350 -0
  181. package/modules/market-watch/run.js +245 -0
  182. package/modules/market-watch/seed/market-data.json +38 -0
  183. package/modules/market-watch/seed/partners-data.json +497 -0
  184. package/modules/market-watch/seed.js +252 -0
  185. package/modules/market-watch/store.js +230 -0
  186. package/modules/memory/routes/memory.js +14 -2
  187. package/modules/memory/routes/search.js +8 -2
  188. package/modules/onboarding/howto-moments.js +8 -1
  189. package/modules/platform-identity/account-logins.js +192 -0
  190. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  191. package/modules/platform-identity/platform-identity.js +5 -4
  192. package/modules/platform-identity/routes/my-projects.js +6 -0
  193. package/modules/platform-identity/routes/profile.js +18 -0
  194. package/modules/platform-identity/routes/public-profile.js +10 -1
  195. package/modules/platform-identity/routes/sso.js +26 -1
  196. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  197. package/modules/provisioning/credential-preflight.js +13 -3
  198. package/modules/provisioning/pollers/app-liveness.js +25 -5
  199. package/modules/provisioning/routes/provisioning.js +1 -1
  200. package/modules/provisioning/starter-bundles.js +8 -2
  201. package/modules/public-landing/public/account.html +10 -2
  202. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  203. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  205. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  209. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  210. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  213. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  214. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  221. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  222. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  223. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  224. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  225. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  226. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  227. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  228. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  229. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  230. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  233. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  244. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  245. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  246. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  247. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  248. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  249. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  250. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  251. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  252. package/modules/public-landing/public/community.html +1 -1
  253. package/modules/public-landing/public/connections.html +2 -2
  254. package/modules/public-landing/public/guild.html +1 -1
  255. package/modules/public-landing/public/invitations.html +2 -2
  256. package/modules/public-landing/public/projects.html +5 -5
  257. package/modules/raise/views/nda.html +2 -0
  258. package/modules/specialities/db.js +32 -1
  259. package/modules/specialities/routes/specialities.js +10 -6
  260. package/modules/status-ui/public/index.html +1 -1
  261. package/package-lock.json +2 -2
  262. package/package.json +1 -1
  263. package/release-notes.json +286 -0
  264. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  265. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  266. package/scripts/bongos/claim.js +22 -1
  267. package/scripts/bongos/db-backup-nightly.sh +29 -0
  268. package/scripts/bongos/deploy-divergence.js +7 -0
  269. package/scripts/bongos/doc-cli-guard.js +6 -0
  270. package/scripts/bongos/fitness.js +3 -2
  271. package/scripts/bongos/gen-diagrams.js +3 -3
  272. package/scripts/bongos/lib.sh +4 -5
  273. package/scripts/bongos/migration-namespace.js +13 -1
  274. package/scripts/bongos/plain-cards.js +4 -0
  275. package/scripts/bongos/provision-github-setup.js +3 -1
  276. package/scripts/bongos/provision-units.js +6 -1
  277. package/scripts/bongos/publish-manifest.js +4 -0
  278. package/scripts/bongos/release.js +2 -0
  279. package/scripts/bongos/run-unit-tests.js +268 -82
  280. package/scripts/bongos/seed-tasks.example.json +56 -0
  281. package/scripts/bongos/seed-tasks.js +222 -0
  282. package/scripts/bongos/seed-verify-criteria.js +1 -2
  283. package/scripts/bongos/ship-finish.js +2 -0
  284. package/scripts/bongos/ship-resume.js +2 -0
  285. package/scripts/bongos/strand-lease.js +250 -0
  286. package/scripts/bongos/strand-watch.js +129 -12
  287. package/scripts/bongos/version-literal-guard.js +2 -2
  288. package/scripts/discord/post.js +4 -3
  289. package/scripts/gds/seed-tasks.js +7 -0
  290. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  291. package/scripts/hall-preview/server.js +21 -0
  292. package/src/bongos/auth-config.js +29 -1
  293. package/src/bongos/auth-discord.js +3 -3
  294. package/src/bongos/auth-github.js +3 -6
  295. package/src/bongos/auth-google.js +217 -0
  296. package/src/bongos/device-approval.js +140 -0
  297. package/src/bongos/llm-pricing.js +17 -1
  298. package/src/bongos/metrics.js +48 -2
  299. package/src/bongos/middleware/rate-limit.js +30 -11
  300. package/src/bongos/module-scope-map.js +16 -1
  301. package/src/bongos/optimizer.js +15 -1
  302. package/src/bongos/payout-approvals.js +342 -0
  303. package/src/bongos/payout-gates.js +364 -0
  304. package/src/bongos/prelaunch-gate.js +7 -9
  305. package/src/bongos/prelaunch-switch.js +20 -0
  306. package/src/bongos/route-rank-check.js +3 -0
  307. package/src/bongos/routes/auth-google.js +246 -0
  308. package/src/bongos/routes/auth.js +38 -1
  309. package/src/bongos/routes/backup.js +33 -9
  310. package/src/bongos/routes/builders.js +57 -30
  311. package/src/bongos/routes/github.js +9 -0
  312. package/src/bongos/routes/me.js +18 -0
  313. package/src/bongos/routes/my-payouts.js +133 -0
  314. package/src/bongos/routes/payout-approvals.js +312 -0
  315. package/src/bongos/routes/payout-gates.js +179 -0
  316. package/src/bongos/routes.js +15 -1
  317. package/src/bongos/secret-box.js +18 -3
  318. package/src/bongos/serve-internal.js +47 -19
  319. package/src/bongos/sign-in-pages.js +138 -0
  320. package/src/bongos/task-needs.js +146 -0
  321. package/src/bongos-downloads.js +18 -1
  322. package/src/instance-config.js +28 -0
  323. package/src/module-api.js +14 -1
  324. package/src/platform-server.js +27 -1
  325. package/tests/account_logins.mjs +326 -0
  326. package/tests/account_logins_db.mjs +161 -0
  327. package/tests/ai_studio_billing.mjs +253 -0
  328. package/tests/ai_studio_generate.mjs +282 -0
  329. package/tests/ai_studio_keys.mjs +159 -0
  330. package/tests/ai_studio_requests.mjs +319 -0
  331. package/tests/auth_backchannel_logout_route.mjs +201 -0
  332. package/tests/auto_satisfy_criteria.mjs +10 -1
  333. package/tests/autobongos_tweak_lane.mjs +144 -0
  334. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  335. package/tests/backfill_subagent_rewards.mjs +17 -1
  336. package/tests/backup_trigger_lock.mjs +119 -0
  337. package/tests/batched_writes_parity.mjs +340 -0
  338. package/tests/blockers_people.mjs +198 -0
  339. package/tests/bounded_rate_maps.mjs +184 -0
  340. package/tests/claimable_blocker_status.mjs +1 -1
  341. package/tests/claims_batch_routes.mjs +421 -0
  342. package/tests/conductor_main_e2e.mjs +4 -0
  343. package/tests/config_read_cache.mjs +171 -0
  344. package/tests/copy_desk_page_editor.mjs +12 -0
  345. package/tests/cost_write_guard.mjs +200 -0
  346. package/tests/craft_achievements.mjs +33 -10
  347. package/tests/criterion_goal_attach.mjs +1 -1
  348. package/tests/db_backup_nightly.mjs +73 -1
  349. package/tests/deploy_divergence_line.mjs +7 -0
  350. package/tests/effective_visibility_predicate.mjs +12 -0
  351. package/tests/fitness.mjs +19 -0
  352. package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
  353. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  354. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  355. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  356. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  357. package/tests/github_one_trip.mjs +34 -0
  358. package/tests/github_repo_visibility.mjs +20 -0
  359. package/tests/goal_achievement.mjs +30 -1
  360. package/tests/goal_achievement_reward.mjs +205 -0
  361. package/tests/goal_archive_disposition.mjs +5 -5
  362. package/tests/goal_sort_staleness.mjs +2 -1
  363. package/tests/goal_tier.mjs +11 -1
  364. package/tests/google_fonts_nonblocking.mjs +84 -0
  365. package/tests/google_sign_in.mjs +401 -0
  366. package/tests/government_board_expiry.mjs +94 -0
  367. package/tests/government_protected_surfaces.mjs +1 -1
  368. package/tests/government_routes.mjs +146 -23
  369. package/tests/grader.mjs +31 -0
  370. package/tests/hall_ai_studio.mjs +347 -0
  371. package/tests/hall_city.mjs +8 -0
  372. package/tests/hall_page_gate_map.mjs +3 -0
  373. package/tests/hall_studio_home.mjs +61 -2
  374. package/tests/home_attention_cards.mjs +1 -1
  375. package/tests/hook_shell_tools.mjs +8 -0
  376. package/tests/hooks_drain.mjs +1 -1
  377. package/tests/host_topology_skips.mjs +2 -0
  378. package/tests/howto_moments.mjs +17 -0
  379. package/tests/human_signal.mjs +141 -0
  380. package/tests/install_script_reader.mjs +51 -0
  381. package/tests/integration_fixture_ids.mjs +82 -0
  382. package/tests/invite_expiry_db.mjs +13 -1
  383. package/tests/lib_sh_resolution.mjs +0 -4
  384. package/tests/market_watch_data.mjs +244 -0
  385. package/tests/market_watch_data_db.mjs +308 -0
  386. package/tests/market_watch_module.mjs +114 -0
  387. package/tests/market_watch_run.mjs +503 -0
  388. package/tests/metrics.mjs +124 -0
  389. package/tests/migration_namespace.mjs +14 -0
  390. package/tests/module_api.mjs +2 -0
  391. package/tests/module_loader.mjs +1 -1
  392. package/tests/module_skills_hook.mjs +69 -0
  393. package/tests/notes.mjs +182 -0
  394. package/tests/own_scope_guard.mjs +5 -0
  395. package/tests/page_tweak_marker_sql.mjs +76 -0
  396. package/tests/page_tweak_no_block.mjs +135 -0
  397. package/tests/page_tweak_notice.mjs +103 -0
  398. package/tests/payout_approvals.mjs +253 -0
  399. package/tests/payout_approvals_db.mjs +106 -0
  400. package/tests/payout_gates.mjs +322 -0
  401. package/tests/payout_gates_db.mjs +113 -0
  402. package/tests/planning_session_skill.mjs +13 -3
  403. package/tests/project_settings_page.mjs +29 -5
  404. package/tests/project_settings_spend_cap_route.mjs +235 -0
  405. package/tests/promotion_sweep.mjs +29 -1
  406. package/tests/provision.mjs +3 -2
  407. package/tests/provisioning_app_status.mjs +18 -0
  408. package/tests/provisioning_credential_preflight.mjs +53 -0
  409. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  410. package/tests/public_landing_projects.mjs +37 -0
  411. package/tests/publish_reconciler_overlap.mjs +60 -0
  412. package/tests/rank_change_broadcast_port.mjs +151 -0
  413. package/tests/seed_tasks.mjs +176 -0
  414. package/tests/seed_verify_criteria.mjs +2 -6
  415. package/tests/session_start_freshness.mjs +66 -1
  416. package/tests/shell_hooks.mjs +209 -0
  417. package/tests/ship_assurance.mjs +28 -0
  418. package/tests/ship_card.mjs +20 -5
  419. package/tests/site_classified_once.mjs +142 -0
  420. package/tests/speciality_db_shape.mjs +23 -0
  421. package/tests/speciality_session_skills.mjs +48 -2
  422. package/tests/strand_lease.mjs +249 -0
  423. package/tests/strand_watch.mjs +58 -0
  424. package/tests/task_criteria_routes.mjs +275 -0
  425. package/tests/task_criteria_unlink_db.mjs +123 -0
  426. package/tests/task_needs.mjs +233 -0
  427. package/tests/task_needs_db.mjs +110 -0
  428. package/tests/unit_runner_serial.mjs +266 -43
  429. package/tests/version_close_route.mjs +1 -1
  430. package/tests/version_literals.mjs +5 -7
  431. package/tests/welcome_boot.mjs +16 -5
  432. package/tests/welcome_boot_browser.mjs +36 -4
  433. package/docs/recipes/bongos-cli-release.md +0 -66
  434. package/scripts/bongos/backfill-disciplines.js +0 -144
  435. package/scripts/bongos/backfill-session-earnings.js +0 -123
  436. package/scripts/bongos/backfill-session-types.js +0 -87
  437. package/scripts/bongos/build-bongos.js +0 -94
  438. package/scripts/bongos/cleanup-orphans.js +0 -74
  439. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  440. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  441. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  442. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  443. package/scripts/bongos/smoke-batch-release.sh +0 -316
  444. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  445. package/scripts/bongos/smoke-dependencies.sh +0 -143
  446. package/scripts/gds/backfill-disciplines.js +0 -7
  447. package/scripts/gds/backfill-session-earnings.js +0 -7
  448. package/scripts/gds/backfill-session-types.js +0 -7
  449. package/scripts/gds/cleanup-orphans.js +0 -7
  450. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  451. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  452. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  453. package/scripts/gds/smoke-batch-claim.sh +0 -8
  454. package/scripts/gds/smoke-batch-release.sh +0 -8
  455. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  456. package/scripts/gds/smoke-dependencies.sh +0 -8
  457. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,46 @@
1
+ -- market_watch_002_run.sql - what the weekly run needs beyond the data model (task 1004937,
2
+ -- goal 1000137, ADR 0394).
3
+ --
4
+ -- 1. The project's Anthropic key. ADR 0394 §1: the run calls the Messages API on the
5
+ -- PROJECT's own key, which the owner sets in Market Watch settings. It is stored the
6
+ -- way ai-studio stores a builder's provider keys (task 1004919): the kernel secret
7
+ -- box's "v1.<iv>.<tag>.<ct>" blob, the master key only in server env, so a database
8
+ -- dump decrypts nothing. last4 is what the settings page shows. modules/market-watch/
9
+ -- key.js is the only reader, and it decrypts only inside a run.
10
+ -- 2. One open run per project. The poller creates a run with INSERT ... ON CONFLICT DO
11
+ -- NOTHING against this index, so two processes on one database that both find the
12
+ -- watch due cannot start two runs (ADR 0394 §2 "one run at a time").
13
+ -- 3. A run's own proposals. A 'new candidate' the run finds becomes a proposal, never a
14
+ -- tracked item (the task's rule), and it was not a member's act nor a weekly-ask tap,
15
+ -- so the origin enum gains 'run'. Widening a CHECK enum is the one DROP CONSTRAINT a
16
+ -- module migration may carry (fitness isConstraintWidening): it is re-added, wider.
17
+ --
18
+ -- 4. A score's one-line reason. The run re-scores each dimension "with a one-line
19
+ -- reason" (the task); the reason is stored beside the value it explains.
20
+ --
21
+ -- Additive and idempotent, like 001.
22
+
23
+ BEGIN;
24
+
25
+ ALTER TABLE market_watch_settings ADD COLUMN IF NOT EXISTS anthropic_key_enc text;
26
+ ALTER TABLE market_watch_settings ADD COLUMN IF NOT EXISTS anthropic_key_last4 text;
27
+ ALTER TABLE market_watch_settings ADD COLUMN IF NOT EXISTS anthropic_key_set_at timestamptz;
28
+ ALTER TABLE market_watch_settings DROP CONSTRAINT IF EXISTS market_watch_settings_key_chk;
29
+ ALTER TABLE market_watch_settings ADD CONSTRAINT market_watch_settings_key_chk CHECK (
30
+ (anthropic_key_enc IS NULL AND anthropic_key_last4 IS NULL)
31
+ OR (anthropic_key_enc LIKE 'v1.%' AND length(anthropic_key_last4) = 4));
32
+
33
+ CREATE UNIQUE INDEX IF NOT EXISTS market_watch_runs_one_open
34
+ ON market_watch_runs (project) WHERE status IN ('queued', 'running');
35
+
36
+ ALTER TABLE market_watch_proposals DROP CONSTRAINT IF EXISTS market_watch_proposals_origin_check;
37
+ ALTER TABLE market_watch_proposals ADD CONSTRAINT market_watch_proposals_origin_check
38
+ CHECK (origin IN ('member', 'weekly_ask', 'run'));
39
+ ALTER TABLE market_watch_proposals ADD COLUMN IF NOT EXISTS run_id bigint REFERENCES market_watch_runs(id);
40
+ -- One proposal per candidate name per run, so a resumed run cannot propose it twice.
41
+ CREATE UNIQUE INDEX IF NOT EXISTS market_watch_proposals_one_per_run_candidate
42
+ ON market_watch_proposals (run_id, lower(payload->>'name')) WHERE origin = 'run';
43
+
44
+ ALTER TABLE market_watch_score_snapshots ADD COLUMN IF NOT EXISTS reason text;
45
+
46
+ COMMIT;
@@ -0,0 +1,26 @@
1
+ {
2
+ "key": "market-watch",
3
+ "title": "Market Watch (a weekly AI watch on a project's market)",
4
+ "description": "A weekly AI watch on a project's competitors, partners and topics, with a sourced log per tracked item and a weekly question to members (goal 1000137). Its data model (tables, read queries and the hub seed of the 2026-10-28 research) and its weekly run (an hourly poller that researches each tracked item on the project's own Anthropic key, under the monthly cap) are in; the pages and weekly ask are not yet, so it mounts no routes. Off by default, and it cannot be turned on behind an open join door with no payer, because every run will spend the project's AI budget.",
5
+ "version": "0.2.0",
6
+ "coreVersion": "^1.19.0",
7
+ "category": {
8
+ "parent": "knowledge"
9
+ },
10
+ "default": false,
11
+ "maintenance": {
12
+ "status": "core-maintained"
13
+ },
14
+ "contributes": {
15
+ "migrations": true,
16
+ "pollers": [
17
+ "hub-seed",
18
+ "weekly-run"
19
+ ]
20
+ },
21
+ "spend": {
22
+ "requiresPayer": true
23
+ },
24
+ "provides": [],
25
+ "consumes": []
26
+ }
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ // modules/market-watch/pollers/hub-seed.js — load the 2026-10-28 research into the hub's
4
+ // Market Watch, once, shortly after boot (task 1004936).
5
+ //
6
+ // WHY A POLLER. The seed needs to know whether this instance IS the hub, which a SQL
7
+ // migration cannot ask, and module migrations only apply once the module is enabled. A
8
+ // poller runs exactly then: the loader starts it only on an instance with Market Watch
9
+ // switched on, after migrations. It fires one time per boot; every boot after the first
10
+ // load is a no-op by constraint (seed.js), so a redeploy on every merge costs a few
11
+ // SELECTs and never a duplicate row. Off the hub it returns 'not_hub' and writes nothing.
12
+ //
13
+ // Never throws into the server: a failure logs and waits for the next boot, and a
14
+ // database that is not reachable (a builder laptop) is a quiet no-op.
15
+
16
+ const api = require('../../../src/module-api');
17
+ const { seedHub } = require('../seed');
18
+
19
+ const log = api.logger('market-watch');
20
+ const FIRST_MS = Number(process.env.MARKET_WATCH_SEED_DELAY_MS) || 2 * 60_000;
21
+
22
+ let timer = null;
23
+
24
+ async function runOnce(deps = {}) {
25
+ try {
26
+ const r = await (deps.seedHub || seedHub)(deps);
27
+ if (r.seeded) {
28
+ const n = r.inserted;
29
+ log.info(`[hub-seed] ${r.project}: ${n.items} items, ${n.scores} scores, ${n.entries} entries, ${n.rules} rules, ${n.runs} runs inserted`);
30
+ } else {
31
+ log.info(`[hub-seed] skipped (${r.skipped})`);
32
+ }
33
+ return r;
34
+ } catch (e) {
35
+ log.warn(`[hub-seed] not loaded: ${e && e.message}`);
36
+ if (typeof deps.reportError === 'function') deps.reportError();
37
+ return { seeded: false, skipped: 'error', error: e && e.message };
38
+ }
39
+ }
40
+
41
+ // The loader passes its own deps plus reportError; only reportError is taken, so a
42
+ // loader dep can never be read as a seed seam (isHub, project, pool).
43
+ function start({ reportError } = {}) {
44
+ if (timer) return;
45
+ timer = setTimeout(() => { timer = null; runOnce({ reportError }); }, FIRST_MS);
46
+ if (timer.unref) timer.unref();
47
+ }
48
+
49
+ function stop() {
50
+ if (timer) clearTimeout(timer);
51
+ timer = null;
52
+ }
53
+
54
+ module.exports = { start, stop, runOnce };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ // modules/market-watch/pollers/weekly-run.js — fire the weekly Market Watch run from the
4
+ // server itself (task 1004937, ADR 0394 §2).
5
+ //
6
+ // A COPY OF THE daily-checks SHAPE, not an import of it (modules may not require each
7
+ // other): one unref'd HOURLY tick, the first a few minutes after boot plus jitter,
8
+ // kill-switched by ROUTINE_TIMERS_DISABLED=1, and it never throws into the server.
9
+ //
10
+ // DUE BY THE LEDGER, NOT BY THE BOOT. The hub redeploys on every merge, so a boot-anchored
11
+ // timer would start (and charge) a run once per deploy. Each tick instead asks
12
+ // market_watch_runs whether the newest run is a cadence old (run.js isDue), and a cut-off
13
+ // run is resumed by whichever tick finds its lease lapsed. A deploy restarts this timer
14
+ // and changes nothing about when the next run is due.
15
+ //
16
+ // The loader starts pollers only on an instance with Market Watch enabled, after its
17
+ // migrations. A database that is not reachable (a builder laptop) is a quiet no-op.
18
+
19
+ const api = require('../../../src/module-api');
20
+ const { runDue } = require('../run');
21
+ const { createStore } = require('../store');
22
+ const { readProjectKey } = require('../key');
23
+ const { projectKey } = require('../seed');
24
+
25
+ const log = api.logger('market-watch');
26
+
27
+ const TICK_MS = Number(process.env.MARKET_WATCH_TICK_MS) || 60 * 60_000;
28
+ // After hub-seed's 2 minutes, so the hub's run zero is in before the first due check.
29
+ const FIRST_MS = Number(process.env.MARKET_WATCH_FIRST_MS) || 4 * 60_000;
30
+ const JITTER_MS = 60_000;
31
+
32
+ function defaultProject() {
33
+ try { return projectKey(api.branding().domains.publicOrigin); } catch { return null; }
34
+ }
35
+
36
+ let running = false;
37
+ let warnedUnreachable = false;
38
+
39
+ // tick(deps) — one look at the ledger, and a run if one is due. NEVER throws.
40
+ async function tick(deps = {}) {
41
+ if (running) return { skipped: 'busy' };
42
+ running = true;
43
+ try {
44
+ const project = deps.project !== undefined ? deps.project : defaultProject();
45
+ if (!project) return { skipped: 'no_project_origin' };
46
+ const pool = deps.pool || api.pool;
47
+ const store = deps.store || createStore(pool);
48
+ const r = await runDue({
49
+ ...deps,
50
+ project,
51
+ store,
52
+ readKey: deps.readKey || ((p) => readProjectKey(pool, p)),
53
+ });
54
+ if (r.outcome) {
55
+ log.info(`[weekly-run] ${project}: run ${r.runId} ${r.outcome}${r.reason ? ` (${r.reason})` : ''}`
56
+ + (r.done !== undefined ? ` — ${r.done} done, ${r.errors} errors, ${r.entries} entries, $${(r.costUsd || 0).toFixed(4)}` : ''));
57
+ }
58
+ return r;
59
+ } catch (e) {
60
+ const msg = (e && e.message) || String(e);
61
+ if (/ECONNREFUSED|ENOTFOUND|does not exist|connect/i.test(msg)) {
62
+ if (!warnedUnreachable) { warnedUnreachable = true; log.info(`[weekly-run] skipped — database unreachable (${msg})`); }
63
+ return { skipped: 'db_unreachable' };
64
+ }
65
+ log.error(`[weekly-run] tick failed: ${msg}`);
66
+ if (typeof deps.reportError === 'function') deps.reportError();
67
+ return { skipped: 'error', error: msg };
68
+ } finally {
69
+ running = false;
70
+ }
71
+ }
72
+
73
+ let firstTimer = null;
74
+ let interval = null;
75
+
76
+ // The loader passes its own deps plus reportError; only reportError is taken, so a loader
77
+ // dep can never be read as a run seam (store, readKey, project).
78
+ function start({ reportError } = {}) {
79
+ if (process.env.ROUTINE_TIMERS_DISABLED === '1') {
80
+ log.info('[weekly-run] disabled (ROUTINE_TIMERS_DISABLED=1)');
81
+ return;
82
+ }
83
+ if (firstTimer || interval) return;
84
+ const fire = () => tick({ reportError });
85
+ firstTimer = setTimeout(() => {
86
+ firstTimer = null;
87
+ fire();
88
+ interval = setInterval(fire, TICK_MS);
89
+ if (interval.unref) interval.unref();
90
+ }, FIRST_MS + Math.floor(Math.random() * JITTER_MS));
91
+ if (firstTimer.unref) firstTimer.unref();
92
+ }
93
+
94
+ function stop() {
95
+ if (firstTimer) clearTimeout(firstTimer);
96
+ if (interval) clearInterval(interval);
97
+ firstTimer = interval = null;
98
+ }
99
+
100
+ module.exports = { start, stop, tick, TICK_MS };
@@ -0,0 +1,350 @@
1
+ 'use strict';
2
+
3
+ // modules/market-watch/research.js — research ONE tracked item with ONE Messages API call
4
+ // (task 1004937, ADR 0394 §1).
5
+ //
6
+ // THE CALL. Model claude-sonnet-5, Anthropic's server-side web tools (web_search_20260209
7
+ // capped at 3 uses, web_fetch_20260209 capped at 2), a bounded max_tokens, and a JSON
8
+ // schema for the reply. The watch rules and both rubrics go first, in the system prompt,
9
+ // under cache_control: they are the same for every item of a run, so items 2..n read them
10
+ // from the cache. The item itself, and today's date, come after the cached prefix.
11
+ //
12
+ // NOT THE GRADE PORT, deliberately (ADR 0394 "Rejected options"): that port spawns a CLI
13
+ // the server image does not have, and it replays cached answers, which for news means
14
+ // finding nothing. This is the server's second direct LLM caller after discord-support.
15
+ // The model id is a single constant here so the ModelProvider port (ADR 0089 §3) can
16
+ // take it over later.
17
+ //
18
+ // TRUST NOTHING THE MODEL SAYS ABOUT ITS SOURCES. The reply is parsed and validated in
19
+ // code, never trusted from the prompt:
20
+ // - an entry survives only if its source_url is a URL that actually appeared in a web
21
+ // search result or a fetched page in THIS response. An invented or remembered link
22
+ // is dropped, so no entry is ever written without a real source (the task's rule);
23
+ // - scores survive only on the item's own rubric dimensions (or 'overall'), 0..10;
24
+ // - a 'new candidate' is returned as a candidate, and the run turns it into a
25
+ // proposal. Nothing here can add a tracked item.
26
+ // A refusal, a max_tokens or pause_turn stop, a tool error, or a reply that is not the
27
+ // JSON asked for ends the item with a recorded error. All of these arrive with HTTP 200
28
+ // and do not raise, so they are checked here, before any content is read.
29
+ //
30
+ // THE COST of each call is priced here from the response's own usage: tokens through
31
+ // llm-pricing.priceUsage plus web searches through llm-pricing.priceWebSearches (the one
32
+ // place the search fee is spelled). An item that errored after the model ran still cost
33
+ // money, so its cost is returned with the error.
34
+ //
35
+ // Pure apart from the injected client: tests pass a fake `messages.create` and read the
36
+ // request it was handed. Nothing here touches the network on its own.
37
+
38
+ const api = require('../../src/module-api');
39
+
40
+ const MODEL = 'claude-sonnet-5';
41
+ const SEARCH_MAX_USES = 3;
42
+ const FETCH_MAX_USES = 2;
43
+ const FETCH_MAX_CONTENT_TOKENS = 10_000;
44
+ const MAX_TOKENS = 8_000;
45
+ const EFFORT = 'low';
46
+ const CALL_TIMEOUT_MS = 5 * 60_000;
47
+
48
+ // AN ITEM'S CEILING (ADR 0394 §3): the most one item may cost, so no item starts unless
49
+ // all of it fits under the cap and the cap is never overshot by more than the estimate's
50
+ // error. Built from the bounds above at Sonnet 5's list rates ($2 in / $10 out per MTok):
51
+ // searches: 3 × $0.01 = $0.03
52
+ // output: 8,000 max_tokens × $10/MTok (thinking incl.)= $0.08
53
+ // input: an 80,000-token budget × $2/MTok = $0.16
54
+ // The input line is an ASSUMED bound, not one the API enforces: fetches are hard-capped at
55
+ // 2 × 10,000 tokens, but search results and the server loop's re-reads are not. The two
56
+ // measured items in ADR 0394 read 23k and 35k tokens of search content, so 80k leaves
57
+ // room. ≈ $0.27 per item against an expected ≈ $0.09.
58
+ const ASSUMED_MAX_INPUT_TOKENS = 80_000;
59
+ function itemCeilingUsd() {
60
+ const p = api.llmPricing;
61
+ const rate = p.PRICES[p.familyFor(MODEL)];
62
+ return p.priceWebSearches(SEARCH_MAX_USES)
63
+ + (MAX_TOKENS * rate.output) / 1e6
64
+ + (ASSUMED_MAX_INPUT_TOKENS * rate.input) / 1e6;
65
+ }
66
+
67
+ const ENTRY_TYPES = new Set(['insight', 'news', 'datapoint']);
68
+ const ITEM_KINDS = new Set(['competitor', 'partner', 'topic']);
69
+ const CONFIDENCE = Object.freeze({ low: 1, medium: 2, high: 3 });
70
+
71
+ // The reply's shape. Bounds that JSON schema strict mode cannot express (lengths, 0..10)
72
+ // are enforced in parseReply below.
73
+ const REPLY_SCHEMA = Object.freeze({
74
+ type: 'object',
75
+ additionalProperties: false,
76
+ required: ['entries', 'scores', 'candidates'],
77
+ properties: {
78
+ entries: {
79
+ type: 'array',
80
+ items: {
81
+ type: 'object',
82
+ additionalProperties: false,
83
+ required: ['type', 'takeaway', 'source_url', 'source_label', 'confidence', 'dimension'],
84
+ properties: {
85
+ type: { type: 'string', enum: ['insight', 'news', 'datapoint'] },
86
+ takeaway: { type: 'string' },
87
+ source_url: { type: 'string' },
88
+ source_label: { type: 'string' },
89
+ confidence: { type: 'string', enum: ['low', 'medium', 'high'] },
90
+ dimension: { type: ['string', 'null'] },
91
+ },
92
+ },
93
+ },
94
+ scores: {
95
+ type: 'array',
96
+ items: {
97
+ type: 'object',
98
+ additionalProperties: false,
99
+ required: ['dimension', 'value', 'reason'],
100
+ properties: {
101
+ dimension: { type: 'string' },
102
+ value: { type: 'number' },
103
+ reason: { type: 'string' },
104
+ },
105
+ },
106
+ },
107
+ candidates: {
108
+ type: 'array',
109
+ items: {
110
+ type: 'object',
111
+ additionalProperties: false,
112
+ required: ['name', 'url', 'kind', 'why'],
113
+ properties: {
114
+ name: { type: 'string' },
115
+ url: { type: 'string' },
116
+ kind: { type: 'string', enum: ['competitor', 'partner', 'topic'] },
117
+ why: { type: 'string' },
118
+ },
119
+ },
120
+ },
121
+ },
122
+ });
123
+
124
+ const oneLine = (s, max) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, max);
125
+ const isHttp = (u) => typeof u === 'string' && /^https?:\/\/[^\s]+$/.test(u);
126
+ const normUrl = (u) => String(u || '').trim().replace(/#.*$/, '').replace(/\/+$/, '');
127
+
128
+ // The system prompt: the rules and the rubrics. Stable for a whole run (the cache prefix),
129
+ // so it must not carry the date, the item, or anything else that varies per call.
130
+ function systemPrompt(rules) {
131
+ const r = rules || {};
132
+ const rubric = r.rubric || {};
133
+ const lines = [
134
+ 'You are the weekly market watch for a software project. You research ONE tracked item per request and answer only with the JSON the response format asks for.',
135
+ '',
136
+ '## The watch rules (approved by the project)',
137
+ oneLine(r.instructions, 4000) || 'Find what changed this week for the item.',
138
+ ];
139
+ if (Array.isArray(r.search_queries) && r.search_queries.length) {
140
+ lines.push('', 'Search themes:', ...r.search_queries.map((q) => `- ${oneLine(q, 300)}`));
141
+ }
142
+ if (Array.isArray(r.sources) && r.sources.length) {
143
+ lines.push('', 'Preferred sources:', ...r.sources.map((s) => `- ${oneLine(s, 300)}`));
144
+ }
145
+ for (const kind of Object.keys(rubric).sort()) {
146
+ const dims = Array.isArray(rubric[kind]) ? rubric[kind] : [];
147
+ if (!dims.length) continue;
148
+ lines.push('', `## How a ${kind} is scored (0 to 10 per dimension, plus "overall")`,
149
+ ...dims.map((d) => `- ${d.key}: ${oneLine(d.definition || d.label, 300)}`));
150
+ }
151
+ lines.push('',
152
+ '## How to answer',
153
+ `- Use at most ${SEARCH_MAX_USES} web searches and ${FETCH_MAX_USES} page fetches.`,
154
+ '- entries: only what is new or changed. Every entry needs source_url copied exactly from a search result or fetched page you saw in this request; an entry with any other link is thrown away. confidence: high only if two sources agree, low if one source and unconfirmed.',
155
+ '- scores: re-score each of the item\'s rubric dimensions and "overall", each with a one-line reason. Leave a dimension out if you found nothing that bears on it.',
156
+ '- candidates: companies, partners or topics this item\'s news points to that are not the item itself and look worth tracking. They are proposed to the team, never added by you.',
157
+ '- If nothing changed, return empty entries.');
158
+ return lines.join('\n');
159
+ }
160
+
161
+ function userPrompt(item, { today, since } = {}) {
162
+ const it = item || {};
163
+ return [
164
+ `Today is ${today}. Research what changed${since ? ` since ${since}` : ' in the last week'}.`,
165
+ '',
166
+ `Item: ${oneLine(it.name, 200)}`,
167
+ `Kind: ${it.kind}`,
168
+ it.url ? `Website: ${it.url}` : null,
169
+ it.description ? `What it is: ${oneLine(it.description, 500)}` : null,
170
+ it.why_tracked ? `Why we track it: ${oneLine(it.why_tracked, 500)}` : null,
171
+ ].filter((l) => l !== null).join('\n');
172
+ }
173
+
174
+ // buildRequest — the exact Messages API body for one item. Exported so the test asserts
175
+ // the model, the tool caps and the cache placement without a network.
176
+ function buildRequest({ item, rules, today, since }) {
177
+ return {
178
+ model: MODEL,
179
+ max_tokens: MAX_TOKENS,
180
+ system: [{ type: 'text', text: systemPrompt(rules), cache_control: { type: 'ephemeral' } }],
181
+ tools: [
182
+ { type: 'web_search_20260209', name: 'web_search', max_uses: SEARCH_MAX_USES },
183
+ { type: 'web_fetch_20260209', name: 'web_fetch', max_uses: FETCH_MAX_USES, max_content_tokens: FETCH_MAX_CONTENT_TOKENS },
184
+ ],
185
+ output_config: { effort: EFFORT, format: { type: 'json_schema', schema: REPLY_SCHEMA } },
186
+ messages: [{ role: 'user', content: userPrompt(item, { today, since }) }],
187
+ };
188
+ }
189
+
190
+ // The URLs this response actually saw: every web_search result and every fetched page.
191
+ function seenUrls(content) {
192
+ const seen = new Set();
193
+ for (const b of content || []) {
194
+ if (!b) continue;
195
+ if (b.type === 'web_search_tool_result' && Array.isArray(b.content)) {
196
+ for (const r of b.content) if (r && isHttp(r.url)) seen.add(normUrl(r.url));
197
+ } else if (b.type === 'web_fetch_tool_result' && b.content && !Array.isArray(b.content)) {
198
+ if (isHttp(b.content.url)) seen.add(normUrl(b.content.url));
199
+ }
200
+ }
201
+ return seen;
202
+ }
203
+
204
+ // A tool error block (HTTP 200, no exception). max_uses_exceeded is the cap doing its job
205
+ // (the model asked for one search too many and was refused), so it does not end the item.
206
+ function toolError(content) {
207
+ for (const b of content || []) {
208
+ if (!b) continue;
209
+ if (b.type === 'web_search_tool_result' || b.type === 'web_fetch_tool_result') {
210
+ const c = b.content;
211
+ const code = c && !Array.isArray(c) ? c.error_code : null;
212
+ if (code && code !== 'max_uses_exceeded') return String(code);
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+
218
+ // The reply text: the text blocks after the last server tool block (the final answer),
219
+ // or every text block if there were no tools.
220
+ function replyText(content) {
221
+ const blocks = content || [];
222
+ let start = 0;
223
+ blocks.forEach((b, i) => {
224
+ if (b && /tool_(use|result)$/.test(String(b.type))) start = i + 1;
225
+ });
226
+ const after = blocks.slice(start).filter((b) => b && b.type === 'text').map((b) => b.text);
227
+ const all = blocks.filter((b) => b && b.type === 'text').map((b) => b.text);
228
+ return (after.length ? after : all).join('');
229
+ }
230
+
231
+ // parseReply(text, { seen, dimensions }) → { ok: true, entries, scores, candidates, dropped }
232
+ // | { ok: false, error: 'malformed_reply' }. Pure.
233
+ function parseReply(text, { seen = new Set(), dimensions = [] } = {}) {
234
+ let obj = null;
235
+ const s = String(text || '').trim();
236
+ try { obj = JSON.parse(s); } catch (_) {
237
+ // A reply wrapped in a code fence or a sentence: take the outermost object, once.
238
+ const a = s.indexOf('{');
239
+ const z = s.lastIndexOf('}');
240
+ if (a !== -1 && z > a) { try { obj = JSON.parse(s.slice(a, z + 1)); } catch (_) { obj = null; } }
241
+ }
242
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)
243
+ || !Array.isArray(obj.entries) || !Array.isArray(obj.scores) || !Array.isArray(obj.candidates)) {
244
+ return { ok: false, error: 'malformed_reply' };
245
+ }
246
+ const dims = new Set([...dimensions, 'overall']);
247
+ const dropped = { entries: 0, scores: 0, candidates: 0 };
248
+
249
+ const entries = [];
250
+ for (const e of obj.entries) {
251
+ const url = e && typeof e.source_url === 'string' ? e.source_url.trim() : '';
252
+ const takeaway = oneLine(e && e.takeaway, 1000);
253
+ const conf = e && CONFIDENCE[String(e.confidence).toLowerCase()];
254
+ if (!isHttp(url) || !seen.has(normUrl(url)) || !takeaway || !conf || !ENTRY_TYPES.has(e.type)) {
255
+ dropped.entries += 1;
256
+ continue;
257
+ }
258
+ let host = '';
259
+ try { host = new URL(url).hostname.replace(/^www\./, ''); } catch (_) { /* isHttp passed */ }
260
+ entries.push({
261
+ type: e.type,
262
+ takeaway,
263
+ sourceUrl: url,
264
+ sourceLabel: oneLine(e.source_label, 200) || host,
265
+ confidence: conf,
266
+ dimension: typeof e.dimension === 'string' && dims.has(e.dimension) && e.dimension !== 'overall' ? e.dimension : null,
267
+ });
268
+ }
269
+
270
+ const scores = [];
271
+ const scored = new Set();
272
+ for (const sc of obj.scores) {
273
+ const v = sc && typeof sc.value === 'number' ? sc.value : NaN;
274
+ const reason = oneLine(sc && sc.reason, 300);
275
+ if (!sc || !dims.has(sc.dimension) || scored.has(sc.dimension) || !Number.isFinite(v) || v < 0 || v > 10 || !reason) {
276
+ dropped.scores += 1;
277
+ continue;
278
+ }
279
+ scored.add(sc.dimension);
280
+ scores.push({ dimension: sc.dimension, value: Math.round(v * 10) / 10, reason });
281
+ }
282
+
283
+ const candidates = [];
284
+ const named = new Set();
285
+ for (const c of obj.candidates) {
286
+ const name = oneLine(c && c.name, 200);
287
+ const key = name.toLowerCase();
288
+ if (!name || named.has(key) || !c || !ITEM_KINDS.has(c.kind) || !(c.url === '' || isHttp(c.url))) {
289
+ dropped.candidates += 1;
290
+ continue;
291
+ }
292
+ named.add(key);
293
+ candidates.push({ name, url: c.url, kind: c.kind, why: oneLine(c.why, 500) });
294
+ }
295
+ return { ok: true, entries, scores, candidates, dropped };
296
+ }
297
+
298
+ // priceResponse(usage) → USD for one response, tokens + searches, 4 decimals (the
299
+ // numeric(10,4) the run_items row stores).
300
+ function priceResponse(usage) {
301
+ const p = api.llmPricing;
302
+ const u = usage || {};
303
+ const tokens = p.priceUsage(MODEL, u).usd;
304
+ const searches = p.priceWebSearches(u.server_tool_use && u.server_tool_use.web_search_requests);
305
+ return Math.round((tokens + searches) * 10000) / 10000;
306
+ }
307
+
308
+ // createClient(apiKey) — the live SDK client on the PROJECT's key. Required lazily, so an
309
+ // instance with Market Watch off never loads the SDK.
310
+ function createClient(apiKey, { sdk = null } = {}) {
311
+ const mod = sdk || require('@anthropic-ai/sdk');
312
+ const Anthropic = mod.Anthropic || mod.default || mod;
313
+ return new Anthropic({ apiKey, timeout: CALL_TIMEOUT_MS, maxRetries: 1 });
314
+ }
315
+
316
+ // researchItem({ client, item, rules, today, since }) →
317
+ // { ok: true, entries, scores, candidates, dropped, costUsd, usage }
318
+ // { ok: false, error, costUsd, usage }
319
+ // Never throws: a thrown SDK error (network, 4xx, 5xx) is an item error that cost nothing.
320
+ async function researchItem({ client, item, rules, today, since }) {
321
+ const req = buildRequest({ item, rules, today, since });
322
+ let res;
323
+ try {
324
+ res = await client.messages.create(req);
325
+ } catch (e) {
326
+ const status = e && e.status ? ` ${e.status}` : '';
327
+ return { ok: false, error: `api_error${status}`, costUsd: 0, usage: null };
328
+ }
329
+ const usage = (res && res.usage) || {};
330
+ const costUsd = priceResponse(usage);
331
+ const stop = res && res.stop_reason;
332
+ if (stop === 'refusal' || stop === 'max_tokens' || stop === 'pause_turn') {
333
+ return { ok: false, error: stop, costUsd, usage };
334
+ }
335
+ const content = (res && res.content) || [];
336
+ const tErr = toolError(content);
337
+ if (tErr) return { ok: false, error: `tool_error:${tErr}`, costUsd, usage };
338
+ const rubric = (rules && rules.rubric && rules.rubric[item.kind]) || [];
339
+ const parsed = parseReply(replyText(content), {
340
+ seen: seenUrls(content),
341
+ dimensions: rubric.map((d) => d.key),
342
+ });
343
+ if (!parsed.ok) return { ok: false, error: parsed.error, costUsd, usage };
344
+ return { ...parsed, costUsd, usage };
345
+ }
346
+
347
+ module.exports = {
348
+ researchItem, buildRequest, parseReply, seenUrls, priceResponse, createClient, itemCeilingUsd,
349
+ MODEL, REPLY_SCHEMA,
350
+ };