@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,60 @@
1
+ // tests/publish_reconciler_overlap.mjs
2
+ //
3
+ // task 1004924 — the reconciler's interval tick fired five sweeps every 5 minutes
4
+ // with no check that the previous round had finished, so a slow DB or GitHub call
5
+ // stacked rounds and held several pool connections at once. buildTick now keeps a
6
+ // per-sweep in-flight flag: a sweep still running from the last tick is skipped.
7
+ //
8
+ // Its own file on purpose: tests/publish_reconciler.mjs exits on a fixed 100 ms
9
+ // timer, so a case appended there that waits on a few event-loop turns is cut off
10
+ // before it reports — it neither passes nor fails, it just vanishes.
11
+ //
12
+ // Run: node tests/publish_reconciler_overlap.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import { createRequire } from 'node:module';
16
+ import { makeRunner } from './helpers.mjs';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const reconciler = require('../modules/lifecycle/publish-reconciler.js');
20
+ const { test, summary } = makeRunner();
21
+
22
+ const settle = () => new Promise((r) => setImmediate(r));
23
+
24
+ await test('buildTick skips a sweep still in flight, and runs it again once it settles', async () => {
25
+ const calls = { slow: 0, fast: 0, failing: 0 };
26
+ let releaseSlow;
27
+ const sweeps = [
28
+ ['slow', () => { calls.slow++; return new Promise((r) => { releaseSlow = r; }); }],
29
+ ['fast', async () => { calls.fast++; }],
30
+ ['failing', async () => { calls.failing++; throw new Error('boom'); }],
31
+ ];
32
+ const logs = [];
33
+ const errs = [];
34
+ const tick = reconciler.buildTick({ log: (m) => logs.push(m), error: (m) => errs.push(m) }, sweeps);
35
+
36
+ tick(); await settle();
37
+ tick(); await settle();
38
+ tick(); await settle();
39
+ assert.equal(calls.slow, 1, 'the slow sweep is never stacked while its first run is pending');
40
+ assert.equal(calls.fast, 3, 'a slow sweep never starves the others');
41
+ assert.equal(calls.failing, 3, 'a rejected run clears its flag too');
42
+ assert.equal(logs.filter((m) => /slow still running/.test(m)).length, 2, 'each skip is logged');
43
+
44
+ releaseSlow(); await settle();
45
+ tick(); await settle();
46
+ assert.equal(calls.slow, 2, 'once the run settles the next tick runs it again');
47
+ });
48
+
49
+ await test('the default tick covers all five sweeps', () => {
50
+ // A sweep left out of the guarded list would simply never run again.
51
+ const src = require('node:fs').readFileSync(require.resolve('../modules/lifecycle/publish-reconciler.js'), 'utf8');
52
+ // task 1004965: read the exported list, since an entry may now carry a cadence.
53
+ const fns = reconciler.SWEEPS.map(([, fn]) => fn);
54
+ for (const fn of ['runSweep', 'runCostSweep', 'runCriterionSweep', 'runPromotionSweep', 'runCraftAchievementBackfill']) {
55
+ assert.ok(fns.includes(reconciler[fn]), `${fn} is in the tick`);
56
+ }
57
+ assert.match(src, /const tick = buildTick\(deps\);/, 'start() uses the guarded tick');
58
+ });
59
+
60
+ summary();
@@ -0,0 +1,151 @@
1
+ // tests/rank_change_broadcast_port.mjs
2
+ //
3
+ // Task 1004962 — the rank-change Discord announcement posts IN-PROCESS through
4
+ // the discord module's `discord.post` port, never by spawning
5
+ // `node scripts/discord/post.js` (50-100 MB + ~300 ms CPU per rank change, with
6
+ // nothing bounding a burst). Drives the real PATCH /builders/:id/rank route with
7
+ // auth + db stubbed, and pins the three cases the task names:
8
+ //
9
+ // 1. posted — the port is called once with the #ship-news line, and no
10
+ // child process is started.
11
+ // 2. module off — no `discord.post` provider: the rank change still 200s and
12
+ // nothing is posted.
13
+ // 3. post throws — a provider that throws (sync) or rejects (async) never fails
14
+ // the rank change.
15
+ //
16
+ // Run: node tests/rank_change_broadcast_port.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { createRequire } from 'node:module';
20
+ import http from 'node:http';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const express = require('express');
24
+
25
+ delete process.env.CHAT_DRY_RUN; // the dry-run guard would hide the port call
26
+
27
+ // Record any child process the route starts. Patched BEFORE the router loads, so
28
+ // a destructured `execFile`/`spawn` picks up the recorder.
29
+ const cp = require('node:child_process');
30
+ const spawned = [];
31
+ for (const fn of ['execFile', 'spawn', 'exec', 'fork']) {
32
+ cp[fn] = (...args) => { spawned.push([fn, args[0], args[1]]); throw new Error(`test: ${fn} must not be called`); };
33
+ }
34
+
35
+ const authPath = require.resolve('../src/bongos/auth.js');
36
+ require.cache[authPath] = {
37
+ id: authPath, filename: authPath, loaded: true,
38
+ exports: {
39
+ requireBuilder: (req, _res, next) => {
40
+ req.builder = { id: '1', rank: 'archon', github_login: 'archon1', display_name: 'Archon One' };
41
+ next();
42
+ },
43
+ requireRank: () => (_req, _res, next) => next(),
44
+ requirePermission: () => (_req, _res, next) => next(),
45
+ },
46
+ };
47
+
48
+ const fakeDb = {
49
+ RANKS: ['xenos', 'thetes', 'metic', 'archon'],
50
+ setBuilderRank: async ({ builderId, newRank }) => ({
51
+ builder: { id: builderId, rank: newRank, display_name: 'Ada', github_login: 'ada' },
52
+ previous_rank: 'thetes',
53
+ direction: 'promotion',
54
+ credit_delta: 5,
55
+ }),
56
+ };
57
+ const dbPath = require.resolve('../src/bongos/db.js');
58
+ require.cache[dbPath] = { id: dbPath, filename: dbPath, loaded: true, exports: fakeDb };
59
+
60
+ const seams = require('../src/module-seams.js');
61
+ const buildBuildersRouter = require('../src/bongos/routes/builders.js');
62
+ const { fireDiscordBroadcast } = buildBuildersRouter;
63
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
64
+
65
+ const app = express();
66
+ app.use(express.json());
67
+ app.use(attachFail);
68
+ app.use('/api/gds', buildBuildersRouter());
69
+ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
70
+ const { port } = server.address();
71
+
72
+ function patchRank(id, rank) {
73
+ return new Promise((resolve, reject) => {
74
+ const data = JSON.stringify({ rank });
75
+ const req = http.request({
76
+ host: '127.0.0.1', port, method: 'PATCH', path: `/api/gds/builders/${id}/rank`,
77
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) },
78
+ }, (res) => {
79
+ const chunks = [];
80
+ res.on('data', (c) => chunks.push(c));
81
+ res.on('end', () => {
82
+ let json = null;
83
+ try { json = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch (_) {}
84
+ resolve({ status: res.statusCode, json });
85
+ });
86
+ });
87
+ req.on('error', reject);
88
+ req.write(data);
89
+ req.end();
90
+ });
91
+ }
92
+
93
+ const settle = () => new Promise((r) => setTimeout(r, 20));
94
+ let passed = 0;
95
+ let failed = 0;
96
+ async function t(name, fn) {
97
+ seams._reset();
98
+ spawned.length = 0;
99
+ try { await fn(); passed++; console.log(`ok ${name}`); } catch (e) { failed++; console.log(`FAIL ${name}\n ${e && e.stack}`); }
100
+ }
101
+
102
+ await t('posted: the route calls discord.post once with the #ship-news line, no child process', async () => {
103
+ const posts = [];
104
+ seams.registerProvider('discord.post', async (msg) => { posts.push(msg); return { posted: true, channel: msg.channel }; });
105
+ const r = await patchRank(7, 'metic');
106
+ assert.equal(r.status, 200);
107
+ await settle();
108
+ assert.equal(posts.length, 1);
109
+ assert.equal(posts[0].channel, 'ship_news');
110
+ assert.equal(posts[0].text, r.json.broadcast);
111
+ assert.match(posts[0].text, /Ada has been raised to the rank of Metic/);
112
+ assert.deepEqual(spawned, []);
113
+ });
114
+
115
+ await t('module off: no discord.post provider — the rank change succeeds and nothing is posted', async () => {
116
+ const r = await patchRank(7, 'metic');
117
+ assert.equal(r.status, 200);
118
+ assert.equal(r.json.builder.rank, 'metic');
119
+ await settle();
120
+ assert.deepEqual(spawned, []);
121
+ assert.deepEqual(await fireDiscordBroadcast('hello'), { posted: false, reason: 'no_discord' });
122
+ });
123
+
124
+ await t('post throws synchronously: the rank change still succeeds', async () => {
125
+ let calls = 0;
126
+ seams.registerProvider('discord.post', () => { calls++; throw new Error('webhook exploded'); });
127
+ const r = await patchRank(7, 'metic');
128
+ assert.equal(r.status, 200);
129
+ assert.equal(r.json.builder.rank, 'metic');
130
+ await settle();
131
+ assert.equal(calls, 1);
132
+ assert.deepEqual(await fireDiscordBroadcast('hello'), { posted: false, reason: 'post_failed' });
133
+ });
134
+
135
+ await t('post rejects: the rank change still succeeds and no rejection escapes', async () => {
136
+ const unhandled = [];
137
+ const onUnhandled = (e) => unhandled.push(e);
138
+ process.on('unhandledRejection', onUnhandled);
139
+ let calls = 0;
140
+ seams.registerProvider('discord.post', async () => { calls++; throw new Error('discord 500'); });
141
+ const r = await patchRank(7, 'metic');
142
+ assert.equal(r.status, 200);
143
+ await settle();
144
+ process.off('unhandledRejection', onUnhandled);
145
+ assert.equal(calls, 1);
146
+ assert.deepEqual(unhandled, []);
147
+ });
148
+
149
+ server.close();
150
+ console.log(`\n${passed} passed, ${failed} failed`);
151
+ process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,176 @@
1
+ // tests/seed_tasks.mjs — scripts/bongos/seed-tasks.js, the one data-driven task seeder (task 1004979).
2
+ //
3
+ // The load-bearing case is EQUIVALENCE: the three per-version seed scripts it replaced
4
+ // (seed-bongos-v1-idea-routing-tasks.js, seed-bv1-profiles-tasks.js,
5
+ // seed-bv2-ideator-surface-tasks.js) each became a spec under tests/fixtures/seed-tasks/, and
6
+ // seed-tasks.js must send exactly the writes the old script sent — every POST /tasks body,
7
+ // dependency edge, promotion and follow-up PATCH, in order. The proof ran LIVE first: the first
8
+ // version of this test (task 1004979's first commit) ran each old script and seed-tasks.js
9
+ // against the same fake API and compared both the writes and the resulting task rows.
10
+ // The old scripts are gone now, so OLD_WRITE_HASHES pins what that run recorded: the sha256 of
11
+ // the canonical write log each old script produced. Any change to what seed-tasks.js sends for
12
+ // these specs moves the hash.
13
+ //
14
+ // Run: node tests/seed_tasks.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import crypto from 'node:crypto';
21
+ import { spawn } from 'node:child_process';
22
+ import { createRequire } from 'node:module';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { startFakeApi, fetchApi, canonical } from './fixtures/seed-tasks/fake-api.mjs';
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+ const SEEDER = path.join(ROOT, 'scripts/bongos/seed-tasks.js');
29
+ const EXAMPLE = path.join(ROOT, 'scripts/bongos/seed-tasks.example.json');
30
+ const FIX = path.join(ROOT, 'tests/fixtures/seed-tasks');
31
+ const { validateSpec, seedTasks } = require(SEEDER);
32
+
33
+ // spec fixture -> the old script it replaced, and the sha256 of that script's write log.
34
+ const HISTORICAL = [
35
+ { spec: 'bongos-v1-idea-routing.json', old: 'seed-bongos-v1-idea-routing-tasks.js', hash: 'f5bc176890077a62754812cf69666bd49ba0da441dafa84702e1883156c302f8' },
36
+ { spec: 'bv1-profiles.json', old: 'seed-bv1-profiles-tasks.js', hash: 'cf81ee14131a20fe5a07c05b7c5fe3362bc940d5c5592031ad911b709db129a7' },
37
+ { spec: 'bv2-ideator-surface.json', old: 'seed-bv2-ideator-surface-tasks.js', hash: '5f8ed981593dfa4738b6dd97208ef0b0aec01a4d7e3eca8abc76caa5d8901f5c' },
38
+ ];
39
+
40
+ let passed = 0, failed = 0;
41
+ async function test(name, fn) {
42
+ try { await fn(); passed++; console.log(` ok ${name}`); }
43
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.stack || err.message}`); }
44
+ }
45
+ const readSpec = (f) => JSON.parse(fs.readFileSync(path.join(FIX, f), 'utf8'));
46
+ const sha = (writes) => crypto.createHash('sha256').update(canonical(writes)).digest('hex');
47
+ const quiet = () => {};
48
+ const noExit = (code) => { throw new Error(`seeder exited ${code}`); };
49
+
50
+ function run(cmd, args, env) {
51
+ return new Promise((resolve) => {
52
+ const p = spawn(cmd, args, { cwd: ROOT, env: { ...process.env, ...env } });
53
+ let out = '';
54
+ p.stdout.on('data', (d) => { out += d; });
55
+ p.stderr.on('data', (d) => { out += d; });
56
+ p.on('close', (code) => resolve({ code, out }));
57
+ });
58
+ }
59
+
60
+ async function runNew(spec) {
61
+ const api = await startFakeApi();
62
+ try {
63
+ await seedTasks(spec, { api: fetchApi(api.base), log: quiet, exit: noExit });
64
+ return { writes: api.writes(), tasks: api.tasks };
65
+ } finally { await api.close(); }
66
+ }
67
+
68
+ // ---- equivalence with the three scripts it replaced -------------------------------------
69
+
70
+ for (const h of HISTORICAL) {
71
+ await test(`${h.spec} sends exactly the writes ${h.old} sent`, async () => {
72
+ const spec = readSpec(h.spec);
73
+ assert.deepEqual(validateSpec(spec), [], 'the historical spec is valid');
74
+ const next = await runNew(spec);
75
+ assert.equal(next.tasks.length, spec.tasks.length, 'every task was filed');
76
+ assert.equal(sha(next.writes), h.hash, 'same writes, same order, as the old script sent');
77
+ });
78
+ }
79
+
80
+ // ---- the example spec and the dry run ----------------------------------------------------
81
+
82
+ await test('the example spec is valid', async () => {
83
+ assert.deepEqual(validateSpec(JSON.parse(fs.readFileSync(EXAMPLE, 'utf8'))), []);
84
+ });
85
+
86
+ await test('--dry-run prints the plan and files nothing', async () => {
87
+ const api = await startFakeApi();
88
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-dry-'));
89
+ try {
90
+ const r = await run(process.execPath, [SEEDER, EXAMPLE, '--dry-run'], { GDS_API_BASE: api.base, BONGOS_API_BASE: api.base, HOME: home, USERPROFILE: home });
91
+ assert.equal(r.code, 0, r.out);
92
+ const spec = JSON.parse(fs.readFileSync(EXAMPLE, 'utf8'));
93
+ for (const t of spec.tasks) assert.ok(r.out.includes(`${t.ref} `) && r.out.includes(t.title), `plan lists ${t.ref}`);
94
+ assert.match(r.out, /Nothing was filed\./);
95
+ assert.equal(api.requests.length, 0, 'not one request reached the API');
96
+ } finally { await api.close(); fs.rmSync(home, { recursive: true, force: true }); }
97
+ });
98
+
99
+ await test('an invalid spec is refused before any write', async () => {
100
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-bad-'));
101
+ const file = path.join(dir, 'bad.json');
102
+ fs.writeFileSync(file, JSON.stringify({ version_id: 'V', goal_id: 1, source_ref_prefix: 'p', tasks: [] }));
103
+ const r = await run(process.execPath, [SEEDER, file, '--dry-run'], {});
104
+ fs.rmSync(dir, { recursive: true, force: true });
105
+ assert.equal(r.code, 1);
106
+ assert.match(r.out, /tasks must be a non-empty array/);
107
+ });
108
+
109
+ // ---- validation ---------------------------------------------------------------------------
110
+
111
+ const base = () => ({
112
+ version_id: 'V', goal_id: 7, source_ref_prefix: 'p',
113
+ tasks: [
114
+ { ref: 'R1', title: 't1', description: 'd', kind: 'feature', discipline: 'engineer', priority: 3, criterion_ids: ['slug-a'] },
115
+ { ref: 'R2', title: 't2', description: 'd', kind: 'verify', discipline: 'engineer', priority: 3, criterion_ids: ['slug-a'], deps: ['R1'] },
116
+ ],
117
+ });
118
+
119
+ await test('validation names each broken thing', async () => {
120
+ const s = base();
121
+ s.tasks[1].ref = 'R1';
122
+ assert.ok(validateSpec(s).some((e) => /duplicate ref/.test(e)));
123
+ const u = base(); u.tasks[1].deps = ['R9'];
124
+ assert.ok(validateSpec(u).some((e) => /unknown ref R9/.test(e)));
125
+ const self = base(); self.tasks[1].deps = ['R2'];
126
+ assert.ok(validateSpec(self).some((e) => /depends on itself/.test(e)));
127
+ const p = base(); p.promote = ['R7'];
128
+ assert.ok(validateSpec(p).some((e) => /promote: unknown ref R7/.test(e)));
129
+ });
130
+
131
+ await test('a positional "Cn" criterion ref is refused unless the spec opts in (learning 1000188)', async () => {
132
+ const s = base(); s.tasks[0].criterion_ids = ['C1'];
133
+ assert.ok(validateSpec(s).some((e) => /positional criterion ref/.test(e)));
134
+ s.allow_positional_criteria = true;
135
+ assert.deepEqual(validateSpec(s), []);
136
+ });
137
+
138
+ // ---- the seeding contract: baseline, replay, verify ---------------------------------------
139
+
140
+ await test('a re-run replays every task and still verifies (only created links are tallied)', async () => {
141
+ const api = await startFakeApi({ goalCriteria: { 7: { 'slug-a': 4 } } });
142
+ try {
143
+ const first = await seedTasks(base(), { api: fetchApi(api.base), log: quiet, exit: noExit });
144
+ assert.equal(first.created, 2);
145
+ const again = await seedTasks(base(), { api: fetchApi(api.base), log: quiet, exit: noExit });
146
+ assert.equal(again.replayed, 2);
147
+ assert.equal(api.tasks.length, 2, 'the replay filed nothing new');
148
+ } finally { await api.close(); }
149
+ });
150
+
151
+ await test('a criterion link that did not land exits 1', async () => {
152
+ const api = await startFakeApi();
153
+ const lying = fetchApi(api.base);
154
+ // A server that files the task but drops its criterion link: the GET shows zero.
155
+ const transport = async (m, p, b) => (m === 'GET' ? { status: 200, ok: true, json: { criteria: [] } } : lying(m, p, b));
156
+ let code = null; const lines = [];
157
+ try {
158
+ await seedTasks(base(), { api: transport, log: (l) => lines.push(l), exit: (c) => { code = c; } });
159
+ } finally { await api.close(); }
160
+ assert.equal(code, 1);
161
+ assert.ok(lines.some((l) => /VERIFY FAILED/.test(l)));
162
+ });
163
+
164
+ await test('a follow-up link waits for the task it names', async () => {
165
+ const s = base();
166
+ s.links = [{ method: 'PATCH', path: '/inbox/55', body: { status: 'promoted', promoted_to_task_id: { $task: 'R2' } } }];
167
+ const api = await startFakeApi();
168
+ try {
169
+ await seedTasks(s, { api: fetchApi(api.base), log: quiet, exit: noExit });
170
+ const patch = api.writes().find((w) => w.method === 'PATCH');
171
+ assert.deepEqual(patch.body, { status: 'promoted', promoted_to_task_id: Number(api.tasks[1].id) });
172
+ } finally { await api.close(); }
173
+ });
174
+
175
+ console.log(`\n${passed} passed, ${failed} failed`);
176
+ if (failed) process.exitCode = 1;
@@ -15,7 +15,6 @@
15
15
 
16
16
  import { strict as assert } from 'node:assert';
17
17
  import { createRequire } from 'node:module';
18
- import { readFileSync } from 'node:fs';
19
18
 
20
19
  const require = createRequire(import.meta.url);
21
20
  const {
@@ -225,11 +224,8 @@ test('a fully re-run seed (every create a replay) plans nothing and verifies OK'
225
224
  assert.deepEqual(diffCriterionLinks(tallyCreated(responses), [{ criterion_id: 'ours', linked_tasks: 2 }], { baseline: { ours: 2 } }), []);
226
225
  });
227
226
 
228
- test('the baselined reference seed skips replays when it tallies', () => {
229
- const src = readFileSync(new URL('../scripts/bongos/seed-bv2-ideator-surface-tasks.js', import.meta.url), 'utf8');
230
- assert.match(src, /baselineSeedCriteria\(/, 'it takes a baseline');
231
- assert.match(src, /if \(!r\.json\.idempotent\) criterionIdListsPerTask\.push\(t\.crit\);/, 'and tallies only created tasks');
232
- });
227
+ // That the real seeder baselines and skips replays when it tallies is proven by behaviour, not
228
+ // by its source text: tests/seed_tasks.mjs re-runs a spec against a fake API (task 1004979).
233
229
 
234
230
  await testAsync('baselineSeedCriteria snapshots the goal before any write', async () => {
235
231
  const logs = [];
@@ -16,10 +16,15 @@
16
16
 
17
17
  import { strict as assert } from 'node:assert';
18
18
  import { createRequire } from 'node:module';
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
19
22
  import { requireHostModule } from './helpers.mjs';
20
23
 
21
24
  const require = createRequire(import.meta.url);
22
- const { decideFreshness } = requireHostModule(require, '../.claude/hooks/session-start.js');
25
+ const {
26
+ decideFreshness, planFetch, viewNote, staleViewLine, lastMainFetchMs, FETCH_FRESH_MS, FETCH_OLD_NOTE_MS,
27
+ } = requireHostModule(require, '../.claude/hooks/session-start.js');
23
28
 
24
29
  let passed = 0;
25
30
  let failed = 0;
@@ -120,5 +125,65 @@ test('dirty and ahead → still just warn (no pull)', () => {
120
125
  assert.equal(d.action, 'warn');
121
126
  });
122
127
 
128
+ // ---------- the fetch is off the critical path (task 1004928) ----------
129
+ // The hook used to block on `git fetch` (4 s timeout). It now judges against the local
130
+ // origin/main, fetches in the background when that view is old, and says how old it is.
131
+
132
+ const NOW = Date.parse('2026-10-28T12:00:00Z');
133
+ const MIN = 60000;
134
+
135
+ test('planFetch: a view fetched under 10 min ago is used as-is, no fetch', () => {
136
+ const p = planFetch({ lastFetchMs: NOW - 9 * MIN, nowMs: NOW });
137
+ assert.equal(p.background, false);
138
+ assert.equal(p.ageMs, 9 * MIN);
139
+ });
140
+
141
+ test('planFetch: an older view starts a background fetch', () => {
142
+ assert.equal(planFetch({ lastFetchMs: NOW - FETCH_FRESH_MS, nowMs: NOW }).background, true);
143
+ });
144
+
145
+ test('planFetch: never fetched (null) → background fetch, age unknown', () => {
146
+ const p = planFetch({ lastFetchMs: null, nowMs: NOW });
147
+ assert.equal(p.background, true);
148
+ assert.equal(p.ageMs, null);
149
+ });
150
+
151
+ test('viewNote: the behind-count names the age of its origin/main', () => {
152
+ assert.equal(viewNote({ ageMs: 4 * MIN, background: false }), 'as fetched 4m ago');
153
+ assert.equal(viewNote({ ageMs: 3 * 60 * MIN, background: true }), 'as fetched 3h ago; a fresh fetch is running in the background');
154
+ assert.match(viewNote({ ageMs: null, background: true }), /time unknown/);
155
+ });
156
+
157
+ test('staleViewLine: silent when the view is fresh or merely minutes old', () => {
158
+ assert.equal(staleViewLine({ ageMs: 2 * MIN, background: false }), null);
159
+ assert.equal(staleViewLine({ ageMs: 20 * MIN, background: true }), null);
160
+ });
161
+
162
+ test('staleViewLine: an old or unknown view says "up to date" is unconfirmed instead of staying silent', () => {
163
+ assert.match(staleViewLine({ ageMs: FETCH_OLD_NOTE_MS, background: true }), /last fetched 1h ago.*unconfirmed/);
164
+ assert.match(staleViewLine({ ageMs: null, background: true }), /never been fetched/);
165
+ });
166
+
167
+ test('lastMainFetchMs: the newest FETCH_HEAD across worktrees that recorded main wins', () => {
168
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ss-fetch-'));
169
+ try {
170
+ fs.mkdirSync(path.join(dir, 'worktrees', 'a'), { recursive: true });
171
+ fs.mkdirSync(path.join(dir, 'worktrees', 'b'), { recursive: true });
172
+ const main = "abc\t\tbranch 'main' of https://example.invalid/repo\n";
173
+ const other = "def\t\tbranch 'feature' of https://example.invalid/repo\n";
174
+ const put = (rel, body, t) => { const f = path.join(dir, rel); fs.writeFileSync(f, body); fs.utimesSync(f, t / 1000, t / 1000); };
175
+ put('FETCH_HEAD', main, NOW - 60 * MIN);
176
+ put(path.join('worktrees', 'a', 'FETCH_HEAD'), main, NOW - 5 * MIN);
177
+ put(path.join('worktrees', 'b', 'FETCH_HEAD'), other, NOW - 1 * MIN); // newest, but not main
178
+ assert.equal(Math.round(lastMainFetchMs(dir) / 1000), Math.round((NOW - 5 * MIN) / 1000));
179
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
180
+ });
181
+
182
+ test('lastMainFetchMs: no FETCH_HEAD anywhere → null (never fetched)', () => {
183
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ss-fetch-'));
184
+ try { assert.equal(lastMainFetchMs(dir), null); } finally { fs.rmSync(dir, { recursive: true, force: true }); }
185
+ assert.equal(lastMainFetchMs(null), null);
186
+ });
187
+
123
188
  console.log(`\n${passed} passed, ${failed} failed`);
124
189
  process.exit(failed === 0 ? 0 : 1);