@bongos/core 1.23.22 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.bongos-core.json +933 -413
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +96 -0
  27. package/clients/bongos-client/index.cjs +96 -0
  28. package/clients/bongos-client/index.d.ts +155 -4
  29. package/clients/bongos-client/index.mjs +96 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
  33. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  34. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  35. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  36. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  37. package/docs/adr/README.md +6 -2
  38. package/docs/api/openapi.json +8651 -5833
  39. package/docs/api-reference.md +66 -10
  40. package/docs/architecture.md +37 -1
  41. package/docs/canonical-permissions.md +1 -1
  42. package/docs/copy-inventory.md +427 -339
  43. package/docs/copy-registry.json +1376 -500
  44. package/docs/file-map.md +1 -0
  45. package/docs/module-api-changelog.md +7 -0
  46. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  47. package/docs/onboarding/diagrams/README.md +1 -1
  48. package/docs/onboarding/drachmae-and-karma.md +15 -15
  49. package/docs/onboarding/primer.md +2 -2
  50. package/docs/page-inventory.json +57 -7
  51. package/docs/page-readings.json +1210 -1027
  52. package/docs/recipes/google-sign-in.md +60 -0
  53. package/docs/recipes/ops-gotchas.md +1 -1
  54. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  55. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  56. package/migrations/core_283_payout_approvals.sql +183 -0
  57. package/migrations/core_284_payout_gates.sql +179 -0
  58. package/migrations/core_285_task_needs.sql +74 -0
  59. package/modules/ai-studio/billing.js +67 -0
  60. package/modules/ai-studio/context.js +29 -0
  61. package/modules/ai-studio/db.js +172 -0
  62. package/modules/ai-studio/gemini.js +75 -0
  63. package/modules/ai-studio/generate.js +119 -0
  64. package/modules/ai-studio/keys.js +73 -0
  65. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  66. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  67. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  68. package/modules/ai-studio/openrouter.js +90 -0
  69. package/modules/ai-studio/payments.js +105 -0
  70. package/modules/ai-studio/pricing.js +92 -0
  71. package/modules/ai-studio/routes/ai-studio.js +235 -5
  72. package/modules/ai-studio/studio.js +121 -0
  73. package/modules/autonomy/fence.js +64 -1
  74. package/modules/builder-settings/builder-needs.js +27 -1
  75. package/modules/copy-desk/routes/copy-desk.js +5 -0
  76. package/modules/economy/achievements.js +22 -10
  77. package/modules/economy/cost-classify.js +9 -0
  78. package/modules/economy/cost-write-guard.js +52 -0
  79. package/modules/economy/cost.js +2 -1
  80. package/modules/economy/credits.js +18 -15
  81. package/modules/economy/goal-reward.js +161 -0
  82. package/modules/economy/reward.js +4 -0
  83. package/modules/economy/routes/cost.js +33 -7
  84. package/modules/government/access-routes.js +70 -0
  85. package/modules/government/access.js +88 -0
  86. package/modules/government/asks.js +5 -2
  87. package/modules/government/catalog.js +6 -0
  88. package/modules/government/db.js +72 -0
  89. package/modules/government/docket.js +48 -2
  90. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  91. package/modules/government/migrations/government_036_access_holders.sql +33 -0
  92. package/modules/government/pollers/board-expiry.js +108 -20
  93. package/modules/government/protected-surfaces.json +1 -0
  94. package/modules/government/routes/government.js +31 -0
  95. package/modules/government/routing.js +49 -7
  96. package/modules/government/waiting.js +78 -0
  97. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  98. package/modules/hall-ui/public/ai-studio.css +193 -0
  99. package/modules/hall-ui/public/ai-studio.html +98 -0
  100. package/modules/hall-ui/public/ai-studio.js +556 -0
  101. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  102. package/modules/hall-ui/public/blockers-by-person.js +158 -0
  103. package/modules/hall-ui/public/blockers-lib.js +15 -1
  104. package/modules/hall-ui/public/blockers.html +8 -0
  105. package/modules/hall-ui/public/blockers.js +1 -1
  106. package/modules/hall-ui/public/claim-action.js +3 -0
  107. package/modules/hall-ui/public/collab.html +12 -0
  108. package/modules/hall-ui/public/craft-mode.js +4 -1
  109. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  110. package/modules/hall-ui/public/drachmae.html +1 -1
  111. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  115. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  120. package/modules/hall-ui/public/goal-inbox.js +1 -1
  121. package/modules/hall-ui/public/goals-render.js +4 -0
  122. package/modules/hall-ui/public/goals.html +8 -0
  123. package/modules/hall-ui/public/gov-living.js +6 -2
  124. package/modules/hall-ui/public/government-access.js +108 -0
  125. package/modules/hall-ui/public/government.html +21 -0
  126. package/modules/hall-ui/public/hall-ask.css +5 -0
  127. package/modules/hall-ui/public/hall-ask.js +39 -3
  128. package/modules/hall-ui/public/notes-panel.css +20 -0
  129. package/modules/hall-ui/public/notes-panel.js +146 -0
  130. package/modules/hall-ui/public/project-settings.html +1 -1
  131. package/modules/hall-ui/public/project-settings.js +7 -5
  132. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  133. package/modules/hall-ui/public/settings.html +15 -0
  134. package/modules/hall-ui/public/street-deck.js +12 -3
  135. package/modules/hall-ui/public/studio.css +90 -8
  136. package/modules/hall-ui/public/studio.html +39 -10
  137. package/modules/hall-ui/public/studio.js +148 -18
  138. package/modules/hall-ui/public/task.html +2 -0
  139. package/modules/hall-ui/public/task.js +4 -0
  140. package/modules/hall-ui/public/tweak-editor.js +12 -3
  141. package/modules/hall-ui/public/welcome-boot.css +42 -6
  142. package/modules/hall-ui/public/welcome-boot.js +20 -6
  143. package/modules/hall-ui/public/work.js +20 -0
  144. package/modules/hall-ui/records/ai-studio.md +36 -0
  145. package/modules/hall-ui/records/blockers-people.md +38 -0
  146. package/modules/lifecycle/db-claims.js +32 -4
  147. package/modules/lifecycle/db-goals.js +18 -5
  148. package/modules/lifecycle/db-tasks.js +15 -6
  149. package/modules/lifecycle/done-when.js +23 -1
  150. package/modules/lifecycle/lifecycle.js +5 -0
  151. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  152. package/modules/lifecycle/module.json +2 -1
  153. package/modules/lifecycle/notes.js +219 -0
  154. package/modules/lifecycle/page-notice.js +124 -0
  155. package/modules/lifecycle/page-tweak-artist.js +18 -1
  156. package/modules/lifecycle/publish-reconciler.js +67 -20
  157. package/modules/lifecycle/routes/claims.js +5 -2
  158. package/modules/lifecycle/routes/goals.js +3 -1
  159. package/modules/lifecycle/routes/notes.js +91 -0
  160. package/modules/lifecycle/routes/project-settings.js +43 -35
  161. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  162. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  163. package/modules/lifecycle/routes/tasks.js +12 -12
  164. package/modules/lifecycle/task-needs-gate.js +70 -0
  165. package/modules/market-watch/CLAUDE.md +15 -0
  166. package/modules/market-watch/db.js +340 -0
  167. package/modules/market-watch/key.js +58 -0
  168. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  169. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  170. package/modules/market-watch/module.json +26 -0
  171. package/modules/market-watch/pollers/hub-seed.js +54 -0
  172. package/modules/market-watch/pollers/weekly-run.js +100 -0
  173. package/modules/market-watch/research.js +350 -0
  174. package/modules/market-watch/run.js +245 -0
  175. package/modules/market-watch/seed/market-data.json +38 -0
  176. package/modules/market-watch/seed/partners-data.json +497 -0
  177. package/modules/market-watch/seed.js +252 -0
  178. package/modules/market-watch/store.js +230 -0
  179. package/modules/memory/routes/memory.js +14 -2
  180. package/modules/memory/routes/search.js +8 -2
  181. package/modules/platform-identity/account-logins.js +192 -0
  182. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  183. package/modules/platform-identity/platform-identity.js +5 -4
  184. package/modules/platform-identity/routes/my-projects.js +6 -0
  185. package/modules/platform-identity/routes/profile.js +18 -0
  186. package/modules/platform-identity/routes/public-profile.js +10 -1
  187. package/modules/platform-identity/routes/sso.js +26 -1
  188. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  189. package/modules/provisioning/credential-preflight.js +13 -3
  190. package/modules/provisioning/pollers/app-liveness.js +25 -5
  191. package/modules/provisioning/routes/provisioning.js +1 -1
  192. package/modules/provisioning/starter-bundles.js +6 -2
  193. package/modules/public-landing/public/account.html +10 -2
  194. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  195. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  196. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  197. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  198. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  199. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  200. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  201. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  202. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  203. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  205. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  209. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  210. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  213. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  214. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  221. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  222. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  223. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  224. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  225. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  226. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  227. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  228. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  229. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  230. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  233. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  244. package/modules/public-landing/public/community.html +1 -1
  245. package/modules/public-landing/public/connections.html +2 -2
  246. package/modules/public-landing/public/guild.html +1 -1
  247. package/modules/public-landing/public/invitations.html +2 -2
  248. package/modules/public-landing/public/projects.html +5 -5
  249. package/modules/raise/views/nda.html +2 -0
  250. package/modules/specialities/db.js +32 -1
  251. package/modules/specialities/routes/specialities.js +10 -6
  252. package/modules/status-ui/public/index.html +1 -1
  253. package/package-lock.json +2 -2
  254. package/package.json +1 -1
  255. package/release-notes.json +230 -0
  256. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  257. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  258. package/scripts/bongos/claim.js +22 -1
  259. package/scripts/bongos/db-backup-nightly.sh +29 -0
  260. package/scripts/bongos/deploy-divergence.js +7 -0
  261. package/scripts/bongos/doc-cli-guard.js +6 -0
  262. package/scripts/bongos/fitness.js +3 -2
  263. package/scripts/bongos/gen-diagrams.js +3 -3
  264. package/scripts/bongos/government-walk.js +3 -0
  265. package/scripts/bongos/lib.sh +4 -5
  266. package/scripts/bongos/migration-namespace.js +13 -1
  267. package/scripts/bongos/plain-cards.js +4 -0
  268. package/scripts/bongos/provision-github-setup.js +3 -1
  269. package/scripts/bongos/provision-units.js +6 -1
  270. package/scripts/bongos/publish-manifest.js +3 -0
  271. package/scripts/bongos/release.js +2 -0
  272. package/scripts/bongos/run-unit-tests.js +268 -82
  273. package/scripts/bongos/seed-tasks.example.json +56 -0
  274. package/scripts/bongos/seed-tasks.js +222 -0
  275. package/scripts/bongos/seed-verify-criteria.js +1 -2
  276. package/scripts/bongos/ship-finish.js +2 -0
  277. package/scripts/bongos/ship-resume.js +2 -0
  278. package/scripts/bongos/strand-lease.js +250 -0
  279. package/scripts/bongos/strand-watch.js +26 -5
  280. package/scripts/bongos/version-literal-guard.js +2 -2
  281. package/scripts/discord/post.js +4 -3
  282. package/scripts/gds/seed-tasks.js +7 -0
  283. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  284. package/scripts/hall-preview/server.js +21 -0
  285. package/src/bongos/auth-config.js +29 -1
  286. package/src/bongos/auth-discord.js +3 -3
  287. package/src/bongos/auth-github.js +3 -6
  288. package/src/bongos/auth-google.js +217 -0
  289. package/src/bongos/device-approval.js +140 -0
  290. package/src/bongos/llm-pricing.js +17 -1
  291. package/src/bongos/metrics.js +48 -2
  292. package/src/bongos/middleware/rate-limit.js +30 -11
  293. package/src/bongos/module-scope-map.js +12 -1
  294. package/src/bongos/optimizer.js +15 -1
  295. package/src/bongos/payout-approvals.js +342 -0
  296. package/src/bongos/payout-gates.js +364 -0
  297. package/src/bongos/prelaunch-gate.js +7 -9
  298. package/src/bongos/prelaunch-switch.js +20 -0
  299. package/src/bongos/route-rank-check.js +7 -0
  300. package/src/bongos/routes/auth-google.js +246 -0
  301. package/src/bongos/routes/auth.js +38 -1
  302. package/src/bongos/routes/backup.js +33 -9
  303. package/src/bongos/routes/builders.js +57 -30
  304. package/src/bongos/routes/github.js +9 -0
  305. package/src/bongos/routes/me.js +18 -0
  306. package/src/bongos/routes/my-payouts.js +133 -0
  307. package/src/bongos/routes/payout-approvals.js +312 -0
  308. package/src/bongos/routes/payout-gates.js +179 -0
  309. package/src/bongos/routes.js +15 -1
  310. package/src/bongos/secret-box.js +18 -3
  311. package/src/bongos/serve-internal.js +47 -19
  312. package/src/bongos/sign-in-pages.js +138 -0
  313. package/src/bongos/task-needs.js +146 -0
  314. package/src/bongos-downloads.js +18 -1
  315. package/src/instance-config.js +28 -0
  316. package/src/module-api.js +14 -1
  317. package/tests/access_holders.mjs +214 -0
  318. package/tests/account_logins.mjs +326 -0
  319. package/tests/account_logins_db.mjs +161 -0
  320. package/tests/ai_studio_billing.mjs +253 -0
  321. package/tests/ai_studio_generate.mjs +282 -0
  322. package/tests/ai_studio_keys.mjs +159 -0
  323. package/tests/ai_studio_requests.mjs +319 -0
  324. package/tests/auth_backchannel_logout_route.mjs +201 -0
  325. package/tests/auto_satisfy_criteria.mjs +10 -1
  326. package/tests/autobongos_tweak_lane.mjs +144 -0
  327. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  328. package/tests/backfill_subagent_rewards.mjs +17 -1
  329. package/tests/backup_trigger_lock.mjs +119 -0
  330. package/tests/blockers_people.mjs +198 -0
  331. package/tests/bounded_rate_maps.mjs +184 -0
  332. package/tests/claimable_blocker_status.mjs +1 -1
  333. package/tests/claims_batch_routes.mjs +421 -0
  334. package/tests/config_read_cache.mjs +171 -0
  335. package/tests/copy_desk_page_editor.mjs +12 -0
  336. package/tests/cost_write_guard.mjs +200 -0
  337. package/tests/craft_achievements.mjs +33 -10
  338. package/tests/criterion_goal_attach.mjs +1 -1
  339. package/tests/db_backup_nightly.mjs +73 -1
  340. package/tests/deploy_divergence_line.mjs +7 -0
  341. package/tests/effective_visibility_predicate.mjs +12 -0
  342. package/tests/fitness.mjs +19 -0
  343. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  344. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  345. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  346. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  347. package/tests/github_one_trip.mjs +34 -0
  348. package/tests/github_repo_visibility.mjs +20 -0
  349. package/tests/goal_achievement.mjs +30 -1
  350. package/tests/goal_achievement_reward.mjs +205 -0
  351. package/tests/goal_sort_staleness.mjs +2 -1
  352. package/tests/goal_tier.mjs +11 -1
  353. package/tests/google_fonts_nonblocking.mjs +84 -0
  354. package/tests/google_sign_in.mjs +401 -0
  355. package/tests/government_board_expiry.mjs +94 -0
  356. package/tests/government_board_membership.mjs +4 -1
  357. package/tests/government_protected_surfaces.mjs +1 -1
  358. package/tests/government_routes.mjs +1 -1
  359. package/tests/government_seed.mjs +1 -0
  360. package/tests/hall_ai_studio.mjs +347 -0
  361. package/tests/hall_page_gate_map.mjs +3 -0
  362. package/tests/hall_studio_home.mjs +61 -2
  363. package/tests/home_attention_cards.mjs +1 -1
  364. package/tests/human_signal.mjs +141 -0
  365. package/tests/install_script_reader.mjs +51 -0
  366. package/tests/integration_fixture_ids.mjs +82 -0
  367. package/tests/invite_expiry_db.mjs +13 -1
  368. package/tests/lib_sh_resolution.mjs +0 -4
  369. package/tests/market_watch_data.mjs +244 -0
  370. package/tests/market_watch_data_db.mjs +308 -0
  371. package/tests/market_watch_module.mjs +114 -0
  372. package/tests/market_watch_run.mjs +503 -0
  373. package/tests/metrics.mjs +124 -0
  374. package/tests/migration_namespace.mjs +14 -0
  375. package/tests/module_api.mjs +2 -0
  376. package/tests/module_loader.mjs +1 -1
  377. package/tests/notes.mjs +182 -0
  378. package/tests/own_scope_guard.mjs +5 -0
  379. package/tests/page_tweak_marker_sql.mjs +76 -0
  380. package/tests/page_tweak_no_block.mjs +135 -0
  381. package/tests/page_tweak_notice.mjs +103 -0
  382. package/tests/payout_approvals.mjs +253 -0
  383. package/tests/payout_approvals_db.mjs +106 -0
  384. package/tests/payout_gates.mjs +322 -0
  385. package/tests/payout_gates_db.mjs +113 -0
  386. package/tests/planning_session_skill.mjs +13 -3
  387. package/tests/project_settings_page.mjs +29 -5
  388. package/tests/project_settings_spend_cap_route.mjs +235 -0
  389. package/tests/promotion_sweep.mjs +29 -1
  390. package/tests/provision.mjs +3 -2
  391. package/tests/provisioning_app_status.mjs +18 -0
  392. package/tests/provisioning_credential_preflight.mjs +53 -0
  393. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  394. package/tests/publish_reconciler_overlap.mjs +60 -0
  395. package/tests/rank_change_broadcast_port.mjs +151 -0
  396. package/tests/seed_tasks.mjs +176 -0
  397. package/tests/seed_verify_criteria.mjs +2 -6
  398. package/tests/site_classified_once.mjs +142 -0
  399. package/tests/speciality_db_shape.mjs +23 -0
  400. package/tests/speciality_session_skills.mjs +48 -2
  401. package/tests/strand_lease.mjs +249 -0
  402. package/tests/strand_watch.mjs +10 -0
  403. package/tests/task_criteria_routes.mjs +275 -0
  404. package/tests/task_criteria_unlink_db.mjs +123 -0
  405. package/tests/task_needs.mjs +233 -0
  406. package/tests/task_needs_db.mjs +110 -0
  407. package/tests/unit_runner_serial.mjs +266 -43
  408. package/tests/version_literals.mjs +5 -7
  409. package/tests/welcome_boot.mjs +16 -5
  410. package/tests/welcome_boot_browser.mjs +36 -4
  411. package/docs/recipes/bongos-cli-release.md +0 -66
  412. package/scripts/bongos/backfill-disciplines.js +0 -144
  413. package/scripts/bongos/backfill-session-earnings.js +0 -123
  414. package/scripts/bongos/backfill-session-types.js +0 -87
  415. package/scripts/bongos/build-bongos.js +0 -94
  416. package/scripts/bongos/cleanup-orphans.js +0 -74
  417. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  418. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  419. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  420. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  421. package/scripts/bongos/smoke-batch-release.sh +0 -316
  422. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  423. package/scripts/bongos/smoke-dependencies.sh +0 -143
  424. package/scripts/gds/backfill-disciplines.js +0 -7
  425. package/scripts/gds/backfill-session-earnings.js +0 -7
  426. package/scripts/gds/backfill-session-types.js +0 -7
  427. package/scripts/gds/cleanup-orphans.js +0 -7
  428. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  429. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  430. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  431. package/scripts/gds/smoke-batch-claim.sh +0 -8
  432. package/scripts/gds/smoke-batch-release.sh +0 -8
  433. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  434. package/scripts/gds/smoke-dependencies.sh +0 -8
  435. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ // scripts/bongos/seed-tasks.js — file one planning session's tasks from a JSON spec (task 1004979).
3
+ //
4
+ // WHY. Every planning session used to copy a whole seed SCRIPT (seed-bv1-profiles-tasks.js,
5
+ // seed-bongos-v1-idea-routing-tasks.js, seed-bv2-ideator-surface-tasks.js) and edit its TASKS
6
+ // array: about 700 lines of three near-identical HTTP loops, each with its own drift — one
7
+ // tallied replays twice, one never verified its criterion links, one never promoted anything.
8
+ // The only part that differs per session is DATA, so this is the one seeder and a session now
9
+ // writes a spec. The three old scripts were replaced by their specs, and
10
+ // tests/seed_tasks.mjs proves this filer sends exactly the same writes they did.
11
+ //
12
+ // THE SPEC (see scripts/bongos/seed-tasks.example.json):
13
+ // version_id, goal_id where every task lands
14
+ // source_ref_prefix each task's source_ref is `<prefix>-<ref>`; POST /tasks is
15
+ // idempotent on (source, source_ref), so a re-run replays
16
+ // source default 'planning-session'
17
+ // tasks[] { ref, title, description, kind, discipline, priority,
18
+ // criterion_ids, deps?, est_minutes?, touches?, requires_rank?,
19
+ // manual_degree?, credits_reward?, security_sensitive? }
20
+ // promote[] refs to POST /tasks/:id/promote after the edges are wired
21
+ // links[] { method, path, body } follow-up writes (an idea marked
22
+ // promoted, a meta task moved into the goal); a body value
23
+ // { "$task": "R11" } becomes that ref's task id, and a link
24
+ // whose ref did not land is skipped
25
+ // verify_criteria default true — see below
26
+ // allow_positional_criteria default false — only the historical bv1-profiles spec sets it
27
+ //
28
+ // THE ORDER is the old scripts' order: baseline the goal, create every task, wire every
29
+ // dependency edge, promote, follow-up links, then verify. Verification is
30
+ // seed-verify-criteria.js's contract (task 1003076, task 1004668): baseline BEFORE the first
31
+ // write, tally only the criterion links this run CREATED (a replay is already in the baseline),
32
+ // and exit 1 loudly on any mismatch.
33
+ //
34
+ // Positional "Cn" criterion refs are refused at validation. They rank over the whole VERSION's
35
+ // criteria and mislinked all 15 tasks of goal 1000071 (learning 1000188); the server now
36
+ // refuses them on a multi-goal version too, but a spec should fail before its first write.
37
+ //
38
+ // Usage:
39
+ // node scripts/bongos/seed-tasks.js <spec.json> --dry-run # validate + print the plan; files nothing
40
+ // node scripts/bongos/seed-tasks.js <spec.json> # file it
41
+
42
+ 'use strict';
43
+
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ const { sleep } = require('../../src/sleep');
47
+ const { baselineSeedCriteria, expectedCriterionCounts, verifySeedCriteriaLinks } = require('./seed-verify-criteria');
48
+
49
+ // Optional per-task fields that POST /tasks accepts and a spec may carry through as-is.
50
+ const OPTIONAL_FIELDS = ['est_minutes', 'touches', 'requires_rank', 'manual_degree', 'credits_reward', 'security_sensitive'];
51
+ const POSITIONAL_CRITERION = /^C\d+$/;
52
+
53
+ /** Every problem with a spec, as plain sentences. An empty list means it can be filed. */
54
+ function validateSpec(spec) {
55
+ const errs = [];
56
+ if (!spec || typeof spec !== 'object') return ['the spec is not a JSON object'];
57
+ if (typeof spec.version_id !== 'string' || !spec.version_id) errs.push('version_id must be a non-empty string');
58
+ if (!Number.isInteger(Number(spec.goal_id)) || Number(spec.goal_id) <= 0) errs.push('goal_id must be a positive integer');
59
+ if (typeof spec.source_ref_prefix !== 'string' || !spec.source_ref_prefix) errs.push('source_ref_prefix must be a non-empty string');
60
+ if (!Array.isArray(spec.tasks) || !spec.tasks.length) { errs.push('tasks must be a non-empty array'); return errs; }
61
+ const refs = new Set();
62
+ for (const [i, t] of spec.tasks.entries()) {
63
+ const at = `tasks[${i}]${t && t.ref ? ` (${t.ref})` : ''}`;
64
+ if (!t || typeof t.ref !== 'string' || !t.ref) { errs.push(`${at}: ref must be a non-empty string`); continue; }
65
+ if (refs.has(t.ref)) errs.push(`${at}: duplicate ref`);
66
+ refs.add(t.ref);
67
+ for (const f of ['title', 'description', 'kind', 'discipline']) {
68
+ if (typeof t[f] !== 'string' || !t[f]) errs.push(`${at}: ${f} must be a non-empty string`);
69
+ }
70
+ if (!Number.isInteger(t.priority)) errs.push(`${at}: priority must be an integer`);
71
+ if (!Array.isArray(t.criterion_ids) || !t.criterion_ids.length) errs.push(`${at}: criterion_ids must name at least one criterion`);
72
+ else if (!spec.allow_positional_criteria) {
73
+ for (const c of t.criterion_ids) {
74
+ if (POSITIONAL_CRITERION.test(String(c))) errs.push(`${at}: "${c}" is a positional criterion ref — use the criterion_id slug (learning 1000188)`);
75
+ }
76
+ }
77
+ if (t.deps !== undefined && !Array.isArray(t.deps)) errs.push(`${at}: deps must be an array of refs`);
78
+ }
79
+ for (const t of spec.tasks) {
80
+ for (const d of (t && Array.isArray(t.deps) ? t.deps : [])) {
81
+ if (d === t.ref) errs.push(`${t.ref}: depends on itself`);
82
+ else if (!refs.has(d)) errs.push(`${t.ref}: depends on unknown ref ${d}`);
83
+ }
84
+ }
85
+ for (const r of spec.promote || []) if (!refs.has(r)) errs.push(`promote: unknown ref ${r}`);
86
+ for (const [i, l] of (spec.links || []).entries()) {
87
+ if (!l || !['PATCH', 'POST'].includes(l.method)) errs.push(`links[${i}]: method must be PATCH or POST`);
88
+ if (!l || typeof l.path !== 'string' || !l.path.startsWith('/')) errs.push(`links[${i}]: path must start with /`);
89
+ for (const r of linkRefs(l && l.body)) if (!refs.has(r)) errs.push(`links[${i}]: unknown ref ${r}`);
90
+ }
91
+ return errs;
92
+ }
93
+
94
+ function linkRefs(body) {
95
+ return Object.values(body || {}).filter((v) => v && typeof v === 'object' && typeof v.$task === 'string').map((v) => v.$task);
96
+ }
97
+
98
+ /** The POST /tasks body for one spec task — the field order the old scripts sent. */
99
+ function taskBody(spec, t) {
100
+ const body = {
101
+ version_id: spec.version_id, goal_id: Number(spec.goal_id), title: t.title, description: t.description,
102
+ kind: t.kind, discipline: t.discipline, priority: t.priority,
103
+ };
104
+ for (const f of OPTIONAL_FIELDS) if (t[f] !== undefined) body[f] = t[f];
105
+ body.criterion_ids = t.criterion_ids;
106
+ body.source = spec.source || 'planning-session';
107
+ body.source_ref = `${spec.source_ref_prefix}-${t.ref}`;
108
+ return body;
109
+ }
110
+
111
+ /** The dry run: what would be filed, in order. Sends nothing. */
112
+ function describePlan(spec, log = console.log) {
113
+ log(`DRY RUN — would seed ${spec.tasks.length} task(s) into goal ${spec.goal_id} (${spec.version_id}), source_ref ${spec.source_ref_prefix}-<ref>:`);
114
+ for (const t of spec.tasks) {
115
+ const deps = t.deps && t.deps.length ? ` after ${t.deps.join(',')}` : '';
116
+ log(` ${t.ref} ${t.kind}/${t.discipline} P${t.priority} [${t.criterion_ids.join(',')}]${deps} ${t.title}`);
117
+ }
118
+ if ((spec.promote || []).length) log(` then promote: ${spec.promote.join(', ')}`);
119
+ for (const l of spec.links || []) log(` then ${l.method} ${l.path}`);
120
+ log(` then ${spec.verify_criteria === false ? 'skip' : 'verify'} the criterion links.`);
121
+ log('Nothing was filed.');
122
+ }
123
+
124
+ /**
125
+ * File a spec. `api(method, path, body)` resolves to { status, ok, json } for a path under
126
+ * /api/bongos; it is injected so the equivalence test can point this at a fake server.
127
+ */
128
+ async function seedTasks(spec, { api, log = console.log, exit = process.exit } = {}) {
129
+ const goalId = Number(spec.goal_id);
130
+ const verify = spec.verify_criteria !== false;
131
+ const fetchGoal = (gid) => api('GET', `/goals/${gid}`);
132
+ const baseline = verify ? await baselineSeedCriteria({ goalId, fetchGoal, log, exit }) : null;
133
+
134
+ const idByRef = {};
135
+ const criterionIdListsPerTask = [];
136
+ let created = 0, replayed = 0, failed = 0;
137
+ log(`Seeding ${spec.tasks.length} task(s) into goal ${goalId} (${spec.version_id})…`);
138
+ for (const t of spec.tasks) {
139
+ const r = await api('POST', '/tasks', taskBody(spec, t));
140
+ const task = r.ok && r.json ? (r.json.task || r.json) : null;
141
+ if (!task || task.id == null) { failed++; log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.json).slice(0, 300)}`); continue; }
142
+ idByRef[t.ref] = task.id;
143
+ if (r.json.idempotent) replayed++;
144
+ else { created++; criterionIdListsPerTask.push(t.criterion_ids); }
145
+ log(` ${t.ref} → task ${task.id}${r.json.idempotent ? ' (already there)' : ''} ${t.title.slice(0, 80)}`);
146
+ }
147
+
148
+ log('Wiring dependencies…');
149
+ let edges = 0;
150
+ for (const t of spec.tasks) {
151
+ if (!t.deps || !t.deps.length) continue;
152
+ const id = idByRef[t.ref];
153
+ const depIds = t.deps.map((d) => idByRef[d]).filter((x) => x != null);
154
+ if (id == null || depIds.length !== t.deps.length) { log(` ${t.ref}: a task in this edge did not land — skipped`); continue; }
155
+ const r = await api('POST', `/tasks/${id}/dependencies`, { depends_on_task_ids: depIds });
156
+ if (!r.ok) { failed++; log(` ${t.ref}: dependency FAILED ${r.status} ${JSON.stringify(r.json).slice(0, 180)}`); continue; }
157
+ edges += (r.json && r.json.added) || depIds.length;
158
+ log(` ${t.ref} depends on ${t.deps.join(',')}`);
159
+ }
160
+
161
+ if ((spec.promote || []).length) log('Promoting…');
162
+ for (const ref of spec.promote || []) {
163
+ const id = idByRef[ref];
164
+ if (id == null) continue;
165
+ const r = await api('POST', `/tasks/${id}/promote`, {});
166
+ log(` ${ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.json).slice(0, 120)}`);
167
+ }
168
+
169
+ if ((spec.links || []).length) log('Follow-up links…');
170
+ for (const l of spec.links || []) {
171
+ const missing = linkRefs(l.body).filter((ref) => idByRef[ref] == null);
172
+ if (missing.length) { log(` ${l.method} ${l.path}: skipped, ${missing.join(',')} did not land`); continue; }
173
+ const body = {};
174
+ for (const [k, v] of Object.entries(l.body || {})) body[k] = v && typeof v === 'object' && typeof v.$task === 'string' ? Number(idByRef[v.$task]) : v;
175
+ const r = await api(l.method, l.path, body);
176
+ log(` ${l.method} ${l.path}: ${r.ok ? 'OK' : 'note ' + JSON.stringify(r.json).slice(0, 160)}`);
177
+ }
178
+
179
+ log(`\nDONE. created=${created} replayed=${replayed} failed=${failed} depEdges=${edges}.`);
180
+ log('ref→id: ' + JSON.stringify(idByRef));
181
+ if (verify) {
182
+ log('Verifying criterion links against the plan…');
183
+ await verifySeedCriteriaLinks({ goalId, expectedCounts: expectedCriterionCounts(criterionIdListsPerTask), baseline, fetchGoal, log, exit });
184
+ }
185
+ return { idByRef, created, replayed, failed };
186
+ }
187
+
188
+ /** The real transport: the CLI session, a base throttle, and the 429 back-off every seed needs. */
189
+ function cliApi({ throttleMs = 400 } = {}) {
190
+ const { apiCall } = require('./cli-lib');
191
+ return async function api(method, p, body, tries = 0) {
192
+ await sleep(throttleMs);
193
+ const r = await apiCall(method, `/api/bongos${p}`, body || null);
194
+ const json = r.data || {};
195
+ if (r.status === 429 && tries < 8) {
196
+ const wait = ((json.error && json.error.details && json.error.details.retry_after_seconds) || 30) + 1;
197
+ console.log(` …rate-limited, waiting ${wait}s`);
198
+ await sleep(wait * 1000);
199
+ return api(method, p, body, tries + 1);
200
+ }
201
+ return { status: r.status, ok: r.ok, json };
202
+ };
203
+ }
204
+
205
+ async function main(argv) {
206
+ const file = argv.find((a) => !a.startsWith('--'));
207
+ if (!file) { console.error('usage: node scripts/bongos/seed-tasks.js <spec.json> [--dry-run]'); return 2; }
208
+ let spec;
209
+ try { spec = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8')); }
210
+ catch (e) { console.error(`seed-tasks: cannot read ${file}: ${e.message}`); return 2; }
211
+ const errs = validateSpec(spec);
212
+ if (errs.length) { console.error(`seed-tasks: ${file} is not a valid spec:\n ${errs.join('\n ')}`); return 1; }
213
+ if (argv.includes('--dry-run')) { describePlan(spec); return 0; }
214
+ const out = await seedTasks(spec, { api: cliApi() });
215
+ return out.failed ? 1 : 0;
216
+ }
217
+
218
+ if (require.main === module) {
219
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (e) => { console.error(e); process.exitCode = 1; });
220
+ }
221
+
222
+ module.exports = { validateSpec, taskBody, describePlan, seedTasks, cliApi, main };
@@ -27,8 +27,7 @@
27
27
  // names. A seed is resumable (POST /tasks is idempotent on source_ref and replays
28
28
  // an existing task with `idempotent: true`), and a replayed task's links are
29
29
  // already inside the baseline — tally it again and a correct re-run expects them
30
- // twice. So a baselined seed skips replays when it tallies (see
31
- // seed-bv2-ideator-surface-tasks.js).
30
+ // twice. So a baselined seed skips replays when it tallies (see seed-tasks.js).
32
31
 
33
32
  // countsByCriterion — { criterion_id: linked_tasks } from GET /goals/:id rows.
34
33
  function countsByCriterion(criteriaRows) {
@@ -217,6 +217,8 @@ async function finishShip({ taskId, summary, advanceToMerge, skipMerge, postGrad
217
217
  // task 1719: pass the reconciled status so a just-landed ship shows SHIPPED (and
218
218
  // writes no breadcrumb), not the stale pre-merge 'confirmed'.
219
219
  await printCompletionCard({ taskId, postGradeStatus: landedStatus, card, usage, land });
220
+ // task 1004956: a landed strand needs no fixer — clear whoever held its lease.
221
+ if (landedStatus === 'shipped') require('./strand-lease.js').clearOnExit(taskId);
220
222
  // task 932: reconcile the working tree — ONLY on a successful land. A
221
223
  // grade-failed ship stays at 'completed'; leave that tree untouched so the
222
224
  // builder can fix findings + re-ship.
@@ -186,6 +186,8 @@ async function resumeMerge(taskId, { summary, skipMerge, force, task, dbOnly = f
186
186
  try {
187
187
  const t = await (await cliClient()).tasks.getTasksId({ id: taskId });
188
188
  const status = (t.ok && t.data && t.data.task) ? t.data.task.status : 'shipped';
189
+ // task 1004956: a landed strand needs no fixer — clear whoever held its lease.
190
+ if (t.ok && status === 'shipped') require('./strand-lease.js').clearOnExit(taskId);
189
191
  await printCompletionCard({
190
192
  taskId, postGradeStatus: status, usage, land,
191
193
  card: { title: task?.title, valueSummary: effSummary, notes: '', creditsAwarded: null },
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ //
4
+ // scripts/bongos/strand-lease.js — ONE session fixes a stuck ship; the others are told who
5
+ // has it (task 1004956).
6
+ //
7
+ // THE GAP. A builder runs several sessions at once, one worktree each. When a ship strands,
8
+ // every session start prints the same strand note, and that note told every one of them to
9
+ // fix it. So N sessions ran /strand-fix on the SAME task: duplicated paid work, sessions
10
+ // colliding on one branch, and the owner watching all of them recommend the same fix
11
+ // (owner, 2026-10-29: "maybe all sessions point to one session to fix").
12
+ //
13
+ // THE LEASE. Fixing a strand takes a short-lived mark: "this worktree is on task N until T".
14
+ // /strand-fix takes it first and stops if another live session holds it; the strand note and
15
+ // the claim refusal name the holder, so every other session carries on with its own work.
16
+ //
17
+ // WHY A FILE IN THE COMMON GIT DIR, NOT A SERVER ROW. Every session a builder runs on one
18
+ // machine is a worktree of one repo, and they all share one common git dir — the same reason
19
+ // merge-lock.js keeps the auto-merge mutex there. That is exactly the set that collides, it
20
+ // needs no migration and no route, and it still works when the API is unreachable. A session
21
+ // on another machine cannot see it; that machine's sessions share their own.
22
+ //
23
+ // KEYED ON THE WORKTREE, NOT THE BUILDER. One builder, many sessions, is the normal case here,
24
+ // so a builder-keyed lease would make every one of their sessions look like the owner. The
25
+ // claim gate keys on the worktree for the same reason (routes/claims.js,
26
+ // gateOneActiveClaimPerSession).
27
+ //
28
+ // EXPIRY IS THE CRASH PATH. A session that just stops never releases. The lease lives
29
+ // LEASE_TTL_MS from its last take; /strand-fix re-takes it at each step to renew it, so a live
30
+ // fixer keeps it and a dead one loses it on its own. Landing (ship.js) and releasing
31
+ // (release.js) clear it at once.
32
+ //
33
+ // Usage:
34
+ // node scripts/bongos/strand-lease.js take <task-id> # take or renew; exit 3 if another session holds it
35
+ // node scripts/bongos/strand-lease.js release <task-id> # drop this worktree's lease
36
+ // node scripts/bongos/strand-lease.js show [<task-id>] # who holds what
37
+
38
+ const fs = require('node:fs');
39
+ const path = require('node:path');
40
+
41
+ const LEASE_TTL_MS = 45 * 60 * 1000;
42
+ const HELD_EXIT = 3;
43
+ const DIR_NAME = 'otb-strand-leases';
44
+
45
+ const safeId = (v) => (/^\d{1,19}$/.test(String(v)) ? String(v) : null);
46
+
47
+ // One spelling per folder. Windows can hand the cwd in 8.3 short form (GMK1_P~1), so the same
48
+ // folder reached two ways would otherwise read as two holders.
49
+ function canonical(p) {
50
+ try { return fs.realpathSync.native(p); } catch { return path.resolve(p); }
51
+ }
52
+
53
+ // Find this checkout WITHOUT spawning git. strand-watch --line calls this on the SessionStart
54
+ // path, whose cost was cut from 8.2 s by caching (task 1004928), so no subprocess belongs here.
55
+ // Walk up to `.git`: a directory is a main checkout (it IS the common dir); a file is a linked
56
+ // worktree, `gitdir: <common>/worktrees/<name>`, whose `commondir` file names the common dir.
57
+ // Memoised per cwd for the life of the process.
58
+ const located = new Map();
59
+ function locate(cwd) {
60
+ const start = path.resolve(cwd);
61
+ if (located.has(start)) return located.get(start);
62
+ let found = null;
63
+ for (let d = start; ; d = path.dirname(d)) {
64
+ const g = path.join(d, '.git');
65
+ let st = null;
66
+ try { st = fs.statSync(g); } catch { /* keep walking */ }
67
+ if (st && st.isDirectory()) { found = { top: d, common: g }; break; }
68
+ if (st && st.isFile()) {
69
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(g, 'utf8'));
70
+ if (!m) break;
71
+ const gitdir = path.resolve(d, m[1]);
72
+ let common = gitdir;
73
+ try { common = path.resolve(gitdir, fs.readFileSync(path.join(gitdir, 'commondir'), 'utf8').trim()); } catch { /* not a linked worktree's gitdir */ }
74
+ found = { top: d, common };
75
+ break;
76
+ }
77
+ if (path.dirname(d) === d) break;
78
+ }
79
+ if (found) found = { top: canonical(found.top), common: canonical(found.common) };
80
+ located.set(start, found);
81
+ return found;
82
+ }
83
+
84
+ // The lease folder: shared by every worktree of this repo. Null when not in a git checkout.
85
+ function leaseDir({ cwd = process.cwd() } = {}) {
86
+ const at = locate(cwd);
87
+ return at ? path.join(at.common, DIR_NAME) : null;
88
+ }
89
+
90
+ // Who "this session" is: its worktree. The label is what other sessions are shown.
91
+ function currentHolder({ cwd = process.cwd() } = {}) {
92
+ const at = locate(cwd);
93
+ if (!at) return null;
94
+ return { holder: at.top.toLowerCase().replace(/\\/g, '/'), label: path.basename(at.top) };
95
+ }
96
+
97
+ function leaseFile(dir, taskId) { return path.join(dir, `${taskId}.json`); }
98
+
99
+ function readRaw(dir, taskId) {
100
+ try { return JSON.parse(fs.readFileSync(leaseFile(dir, taskId), 'utf8')); } catch { return null; }
101
+ }
102
+
103
+ function isLive(lease, now) {
104
+ return !!lease && Number.isFinite(lease.expires_at) && lease.expires_at > now;
105
+ }
106
+
107
+ // The live lease on a task, or null (none, expired, or unreadable).
108
+ function read(taskId, { dir, now = Date.now() } = {}) {
109
+ const id = safeId(taskId);
110
+ if (!id || !dir) return null;
111
+ const l = readRaw(dir, id);
112
+ return isLive(l, now) ? l : null;
113
+ }
114
+
115
+ // A temp file older than this was left by a process that died mid-write; nothing will rename it.
116
+ const TMP_STALE_MS = 60 * 1000;
117
+ function sweepTmp(dir, now) {
118
+ let names = [];
119
+ try { names = fs.readdirSync(dir); } catch { return; }
120
+ for (const n of names) {
121
+ if (!n.endsWith('.tmp')) continue;
122
+ const f = path.join(dir, n);
123
+ try { if (now - fs.statSync(f).mtimeMs > TMP_STALE_MS) fs.unlinkSync(f); } catch { /* raced */ }
124
+ }
125
+ }
126
+
127
+ // Take or renew. { ok: true, lease, renewed } when this holder has it now; { ok: false, lease }
128
+ // naming the live holder otherwise. A FREE lease is created exclusively (flag 'wx'), so of two
129
+ // sessions taking it at the same instant the filesystem lets exactly one in. Renewing, and taking
130
+ // over an EXPIRED lease, replace the file atomically (temp + rename) and read it back: a takeover
131
+ // race can only happen after the holder has already been gone for the whole TTL.
132
+ function take(taskId, { dir, holder, label, now = Date.now(), ttlMs = LEASE_TTL_MS } = {}) {
133
+ const id = safeId(taskId);
134
+ if (!id) throw new Error(`not a task id: ${taskId}`);
135
+ if (!dir || !holder) throw new Error('no lease folder or holder (not inside a git checkout?)');
136
+ fs.mkdirSync(dir, { recursive: true });
137
+ sweepTmp(dir, now);
138
+ let cur = readRaw(dir, id);
139
+ const lease = (taken) => ({
140
+ task_id: id, holder, label: label || path.basename(holder), taken_at: taken, expires_at: now + ttlMs,
141
+ });
142
+ if (!cur) {
143
+ try {
144
+ const fresh = lease(now);
145
+ fs.writeFileSync(leaseFile(dir, id), JSON.stringify(fresh), { flag: 'wx' });
146
+ return { ok: true, lease: fresh, renewed: false };
147
+ } catch (e) {
148
+ if (e.code !== 'EEXIST') throw e;
149
+ cur = readRaw(dir, id); // another session got there first
150
+ }
151
+ }
152
+ if (isLive(cur, now) && cur.holder !== holder) return { ok: false, lease: cur };
153
+ const renewed = isLive(cur, now) && cur.holder === holder;
154
+ const tmp = `${leaseFile(dir, id)}.${process.pid}.${now}.tmp`;
155
+ try {
156
+ fs.writeFileSync(tmp, JSON.stringify(lease(renewed ? cur.taken_at : now)));
157
+ fs.renameSync(tmp, leaseFile(dir, id));
158
+ } finally {
159
+ try { fs.unlinkSync(tmp); } catch { /* renamed away: the normal case */ }
160
+ }
161
+ const back = readRaw(dir, id);
162
+ if (!back || back.holder !== holder) return { ok: false, lease: back };
163
+ return { ok: true, lease: back, renewed };
164
+ }
165
+
166
+ // Drop the lease if this holder has it (or always, with force — landing clears it whoever
167
+ // held it). Returns true when a lease file was removed.
168
+ function release(taskId, { dir, holder, force = false } = {}) {
169
+ const id = safeId(taskId);
170
+ if (!id || !dir) return false;
171
+ const cur = readRaw(dir, id);
172
+ if (!cur) return false;
173
+ if (!force && cur.holder !== holder) return false;
174
+ try { fs.unlinkSync(leaseFile(dir, id)); return true; } catch { return false; }
175
+ }
176
+
177
+ // Every live lease, oldest first.
178
+ function liveLeases({ dir, now = Date.now() } = {}) {
179
+ if (!dir) return [];
180
+ let names;
181
+ try { names = fs.readdirSync(dir); } catch { return []; }
182
+ return names
183
+ .map((n) => /^(\d{1,19})\.json$/.exec(n))
184
+ .filter(Boolean)
185
+ .map((m) => readRaw(dir, m[1]))
186
+ .filter((l) => isLive(l, now))
187
+ .sort((a, b) => a.taken_at - b.taken_at);
188
+ }
189
+
190
+ function minutes(ms) { return `${Math.max(1, Math.round(ms / 60000))}m`; }
191
+
192
+ // "session <label> (on it 12m)" — the words every other session is shown. The label is a
193
+ // worktree folder name, so it is safe to inject (no builder-authored text).
194
+ function describe(lease, now = Date.now()) {
195
+ const label = String(lease.label || '').replace(/[^\w.-]/g, '').slice(0, 80) || 'another session';
196
+ return `session ${label} (on it ${minutes(now - lease.taken_at)})`;
197
+ }
198
+
199
+ // The clause the strand note appends: the strands OTHER sessions hold, so this one leaves
200
+ // them alone. Null when nobody else holds one.
201
+ function othersClause(leases, holder, now = Date.now()) {
202
+ const others = (leases || []).filter((l) => l.holder !== holder);
203
+ if (!others.length) return null;
204
+ const list = others.map((l) => `task ${safeId(l.task_id)} by ${describe(l, now)}`).join('; ');
205
+ return `Already being fixed: ${list}. Leave ${others.length === 1 ? 'that one' : 'those'} to that session and carry on with your own work.`;
206
+ }
207
+
208
+ // Drop the lease for a task that has left the strand board (landed or released). Never throws:
209
+ // a cleanup that fails must not fail the ship or release it rides on.
210
+ function clearOnExit(taskId, { cwd = process.cwd() } = {}) {
211
+ try { return release(taskId, { dir: leaseDir({ cwd }), force: true }); } catch { return false; }
212
+ }
213
+
214
+ function main(argv = process.argv.slice(2)) {
215
+ const [cmd, rawId] = argv;
216
+ const dir = leaseDir();
217
+ const me = currentHolder();
218
+ if (!dir || !me) { console.error('strand-lease: not inside a git checkout'); return 2; }
219
+ if (cmd === 'take') {
220
+ const r = take(rawId, { dir, holder: me.holder, label: me.label });
221
+ if (r.ok) {
222
+ console.log(`strand-lease: ${r.renewed ? 'renewed' : 'took'} task ${r.lease.task_id} for this session until ${new Date(r.lease.expires_at).toISOString()}.`);
223
+ return 0;
224
+ }
225
+ console.log(`strand-lease: task ${safeId(rawId)} is already being fixed by ${describe(r.lease)}.`);
226
+ console.log(' Leave it to that session and carry on with your own work. The hold runs out on its own if that session stops.');
227
+ return HELD_EXIT;
228
+ }
229
+ if (cmd === 'release') {
230
+ console.log(release(rawId, { dir, holder: me.holder })
231
+ ? `strand-lease: released task ${safeId(rawId)}.`
232
+ : `strand-lease: this session held no lease on task ${safeId(rawId)}.`);
233
+ return 0;
234
+ }
235
+ if (cmd === 'show') {
236
+ const leases = rawId ? [read(rawId, { dir })].filter(Boolean) : liveLeases({ dir });
237
+ if (!leases.length) { console.log('strand-lease: nobody is fixing a stuck ship right now.'); return 0; }
238
+ for (const l of leases) console.log(` task ${l.task_id} ${describe(l)}${l.holder === me.holder ? ' ← this session' : ''}`);
239
+ return 0;
240
+ }
241
+ console.error('usage: node scripts/bongos/strand-lease.js take|release|show [<task-id>]');
242
+ return 2;
243
+ }
244
+
245
+ if (require.main === module) process.exitCode = main();
246
+
247
+ module.exports = {
248
+ LEASE_TTL_MS, HELD_EXIT, leaseDir, currentHolder, take, release, read, liveLeases,
249
+ describe, othersClause, clearOnExit,
250
+ };
@@ -241,7 +241,28 @@ function line(r, { max = 3 } = {}) {
241
241
  // Name the skill, not just the diagnosis: 19 of the last 30 days' session logs mention a strand,
242
242
  // and the next claim is refused REBASE_REQUIRED until one is landed or released (task 1003548).
243
243
  + `Run /strand-fix ${worst.id} to walk it. `
244
- + 'A strand produces no signal of its own; this line is the only one.';
244
+ + 'A strand produces no signal of its own; this line is the only one. '
245
+ // Agents kept relaying this to the owner on every session (owner, 2026-10-28; task 1004952).
246
+ + RELAY_RULE;
247
+ }
248
+
249
+ // This line is context for the AGENT. Fixed text only — it is injected every session.
250
+ // "Fix it yourself" sent EVERY session at the same strand (task 1004956): /strand-fix now takes a
251
+ // lease first, so the rule says one session fixes it and the others are told who has it.
252
+ const RELAY_RULE = 'This note is for you, not the builder: do not repeat it to them. ONE session fixes a strand — '
253
+ + '/strand-fix takes it first and stops if another session already has it; tell the builder only if it needs a person or they ask.';
254
+
255
+ // The lease clause, appended at PRINT time, never cached: the line text is shared across
256
+ // sessions for up to two hours (linePlan), while who holds a strand changes by the minute and
257
+ // differs per worktree. Fail-open: no lease folder, no clause.
258
+ function withLeases(text, { now = Date.now() } = {}) {
259
+ if (!text) return text;
260
+ try {
261
+ const lease = require('./strand-lease.js');
262
+ const me = lease.currentHolder();
263
+ const clause = lease.othersClause(lease.liveLeases({ dir: lease.leaseDir(), now }), me && me.holder, now);
264
+ return clause ? `${text} ${clause}` : text;
265
+ } catch { return text; }
245
266
  }
246
267
 
247
268
  function report(r) {
@@ -364,10 +385,10 @@ async function main() {
364
385
  const rec = readLineRecord();
365
386
  const plan = linePlan(rec, Date.now());
366
387
  if (plan === 'fresh') {
367
- if (rec.value) console.log(rec.value);
388
+ if (rec.value) console.log(withLeases(rec.value));
368
389
  } else if (plan === 'stale') {
369
390
  const one = labelStale(rec.value, Date.now() - rec.at);
370
- if (one) console.log(one);
391
+ if (one) console.log(withLeases(one));
371
392
  spawnRefresh();
372
393
  } else {
373
394
  const { cached } = require('./probe-cache.js');
@@ -376,7 +397,7 @@ async function main() {
376
397
  // say how old, so a long-expired line is never passed off as current.
377
398
  const shown = from === 'stale-while-locked' && rec && typeof rec.at === 'number'
378
399
  ? labelStale(one, Date.now() - rec.at) : one;
379
- if (shown) console.log(shown);
400
+ if (shown) console.log(withLeases(shown));
380
401
  }
381
402
  } catch { /* no session, offline, unreadable, past the deadline — all silence */ }
382
403
  process.exitCode = 0;
@@ -405,7 +426,7 @@ if (require.main === module) {
405
426
  }
406
427
 
407
428
  module.exports = {
408
- check, line, report, classifyStrand, rankStrands, humanAge,
429
+ check, line, withLeases, report, classifyStrand, rankStrands, humanAge,
409
430
  linePlan, labelStale, produceLine, LINE_FRESH_MS, LINE_STALE_MAX_MS, LINE_DEADLINE_MS,
410
431
  CONFIRMED_STALE_MS, COMPLETED_STALE_MS, STRAND_REASON_PREFIX, WATCHED, PAGE_LIMIT,
411
432
  };
@@ -11,8 +11,8 @@
11
11
  //
12
12
  // WHAT IT CHECKS, narrowly: a version-shaped literal ASSIGNED TO A VERSION FIELD
13
13
  // or used as a version DEFAULT, in an executable script. Not any mention of a
14
- // version anywhere — smoke-claim-consistency.sh legitimately says "PMS-V2 P6 not
15
- // live yet" in a comment and an echo, and flagging that would be the over-broad
14
+ // version anywhere — a script may legitimately say "PMS-V2 P6 not live yet" in a
15
+ // comment or an echo, and flagging that would be the over-broad
16
16
  // source assertion this repo has learned to avoid (learning 1000196). Comment
17
17
  // lines are stripped before matching for the same reason.
18
18
  //
@@ -16,10 +16,11 @@
16
16
  // modules/discord/ship-broadcast.js, whose kind filter decides which ships are
17
17
  // news. What does pass through here is the project's own news in plain words,
18
18
  // which is what #ship-news promises: a builder graduating
19
- // (modules/onboarding/onboarding-state.js), a rank change
20
- // (src/bongos/routes/builders.js), an outage heads-up
19
+ // (modules/onboarding/onboarding-state.js), an outage heads-up
21
20
  // (modules/provisioning/pollers/liveness-notify.js) and the style-guide audit
22
- // (scripts/cron/style_guide_audit.sh). Builder housekeeping names its channel:
21
+ // (scripts/cron/style_guide_audit.sh). A rank change posts the same #ship-news line
22
+ // in-process through the discord.post port instead (task 1004962,
23
+ // src/bongos/routes/builders.js). Builder housekeeping names its channel:
23
24
  // scripts/bongos/pulse.js's reclaim nudge passes --channel ship_feed.
24
25
 
25
26
  const fs = require('node:fs');
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/seed-tasks.js — @bongos-forwarder (task 1004848). Generated by
3
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
4
+ // The real script is scripts/bongos/seed-tasks.js. This forwarder keeps callers that still name
5
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
6
+ 'use strict';
7
+ require('../bongos/forwarder.js').forward(module, 'seed-tasks.js');
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // scripts/gds/build-bongos.js — @bongos-forwarder (task 1004848). Generated by
2
+ // scripts/gds/strand-lease.js — @bongos-forwarder (task 1004848). Generated by
3
3
  // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
4
- // The real script is scripts/bongos/build-bongos.js. This forwarder keeps callers that still name
4
+ // The real script is scripts/bongos/strand-lease.js. This forwarder keeps callers that still name
5
5
  // the old path working, and warns once so they get repointed. Task 1003706 removes it.
6
6
  'use strict';
7
- require('../bongos/forwarder.js').forward(module, 'build-bongos.js');
7
+ require('../bongos/forwarder.js').forward(module, 'strand-lease.js');
@@ -156,6 +156,15 @@ const FIXTURE_FALLBACKS = [
156
156
  // a store version's how-to page (task 1004366): one reading serves any key and
157
157
  // version, since the Modules tab's how-to view shows one document.
158
158
  [/^store\/modules\/[a-z0-9-]+\/versions\/[^/]+\/howto$/, 'store__modules__howto'],
159
+ // the studio's AI pages (task 1004921): one request per STATE, picked by id,
160
+ // so the waiting page, a result on the balance, a result on a Gemini key and a
161
+ // failed run each render: 1002 is running, 1003 failed, 1004 ran on the
162
+ // builder's own Gemini key, and any other id is the finished result on the
163
+ // balance.
164
+ [/^ai-studio\/requests\/1002$/, 'ai-studio__requests__running'],
165
+ [/^ai-studio\/requests\/1003$/, 'ai-studio__requests__failed'],
166
+ [/^ai-studio\/requests\/1004$/, 'ai-studio__requests__own'],
167
+ [/^ai-studio\/requests\/\d+$/, 'ai-studio__requests__done'],
159
168
  ];
160
169
 
161
170
  // CANNED WRITES (task 1004321). The tweak editor's whole flow is writes (the
@@ -186,6 +195,11 @@ const CANNED_WRITES = [
186
195
  // Settings → Software update's "Ask for this upgrade" (task 1004595), so the
187
196
  // pending line the panel draws after the press renders here too.
188
197
  ['POST', /^software-update\/request$/, 'software-update__request.post'],
198
+ // the studio's AI pages (task 1004921): a file uploads, a request is sent
199
+ // (it answers as 1002, the running one), and money is added.
200
+ ['POST', /^ai-studio\/uploads$/, 'ai-studio__uploads.post'],
201
+ ['POST', /^ai-studio\/requests$/, 'ai-studio__requests.post'],
202
+ ['POST', /^ai-studio\/topup$/, 'ai-studio__topup.post'],
189
203
  ];
190
204
 
191
205
  // A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
@@ -316,6 +330,13 @@ app.use((req, res, next) => {
316
330
  // Studio's review card, where the picture IS the card — previews as a broken
317
331
  // frame, so the one thing being reviewed is the one thing you cannot see.
318
332
  // A generated stand-in; see scripts/hall-preview/task-visual.js.
333
+ // The studio's AI files (task 1004921) are images too: an upload's thumbnail
334
+ // and the pictures a request made. The same generated stand-in.
335
+ if (/^ai-studio\/files\/\d+$/.test(apiPath) && req.method === 'GET') {
336
+ const png = require('./task-visual').placeholderPng();
337
+ res.set({ 'content-type': 'image/png', 'cache-control': 'no-store' });
338
+ return res.end(png);
339
+ }
319
340
  if (/^task-visuals\/[A-Za-z0-9._-]+$/.test(apiPath) && req.method === 'GET') {
320
341
  // HALL_PREVIEW_VISUALS=<dir> serves a REAL image of that name from <dir>
321
342
  // instead (task 1004323): the Approval queue's review sheet shows the renders