@bongos/core 1.23.22 → 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 (424) hide show
  1. package/.bongos-core.json +902 -407
  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 +90 -0
  27. package/clients/bongos-client/index.cjs +90 -0
  28. package/clients/bongos-client/index.d.ts +149 -4
  29. package/clients/bongos-client/index.mjs +90 -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/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  33. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  34. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  35. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  36. package/docs/adr/README.md +5 -1
  37. package/docs/api/openapi.json +8555 -5876
  38. package/docs/api-reference.md +63 -10
  39. package/docs/architecture.md +37 -1
  40. package/docs/canonical-permissions.md +1 -1
  41. package/docs/copy-inventory.md +420 -337
  42. package/docs/copy-registry.json +1321 -500
  43. package/docs/file-map.md +1 -0
  44. package/docs/module-api-changelog.md +5 -0
  45. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  46. package/docs/onboarding/diagrams/README.md +1 -1
  47. package/docs/onboarding/drachmae-and-karma.md +15 -15
  48. package/docs/onboarding/primer.md +2 -2
  49. package/docs/page-inventory.json +55 -6
  50. package/docs/page-readings.json +1185 -1011
  51. package/docs/recipes/google-sign-in.md +60 -0
  52. package/docs/recipes/ops-gotchas.md +1 -1
  53. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  54. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  55. package/migrations/core_283_payout_approvals.sql +183 -0
  56. package/migrations/core_284_payout_gates.sql +179 -0
  57. package/migrations/core_285_task_needs.sql +74 -0
  58. package/modules/ai-studio/billing.js +67 -0
  59. package/modules/ai-studio/context.js +29 -0
  60. package/modules/ai-studio/db.js +172 -0
  61. package/modules/ai-studio/gemini.js +75 -0
  62. package/modules/ai-studio/generate.js +119 -0
  63. package/modules/ai-studio/keys.js +73 -0
  64. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  65. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  66. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  67. package/modules/ai-studio/openrouter.js +90 -0
  68. package/modules/ai-studio/payments.js +105 -0
  69. package/modules/ai-studio/pricing.js +92 -0
  70. package/modules/ai-studio/routes/ai-studio.js +235 -5
  71. package/modules/ai-studio/studio.js +121 -0
  72. package/modules/autonomy/fence.js +64 -1
  73. package/modules/builder-settings/builder-needs.js +27 -1
  74. package/modules/copy-desk/routes/copy-desk.js +5 -0
  75. package/modules/economy/achievements.js +22 -10
  76. package/modules/economy/cost-classify.js +9 -0
  77. package/modules/economy/cost-write-guard.js +52 -0
  78. package/modules/economy/cost.js +2 -1
  79. package/modules/economy/credits.js +18 -15
  80. package/modules/economy/goal-reward.js +161 -0
  81. package/modules/economy/reward.js +4 -0
  82. package/modules/economy/routes/cost.js +33 -7
  83. package/modules/government/catalog.js +6 -0
  84. package/modules/government/db.js +34 -0
  85. package/modules/government/docket.js +36 -0
  86. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  87. package/modules/government/pollers/board-expiry.js +108 -20
  88. package/modules/government/protected-surfaces.json +1 -0
  89. package/modules/government/routes/government.js +22 -0
  90. package/modules/government/routing.js +39 -5
  91. package/modules/government/waiting.js +74 -0
  92. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  93. package/modules/hall-ui/public/ai-studio.css +193 -0
  94. package/modules/hall-ui/public/ai-studio.html +98 -0
  95. package/modules/hall-ui/public/ai-studio.js +556 -0
  96. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  97. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  98. package/modules/hall-ui/public/blockers-lib.js +15 -1
  99. package/modules/hall-ui/public/blockers.html +8 -0
  100. package/modules/hall-ui/public/blockers.js +1 -1
  101. package/modules/hall-ui/public/claim-action.js +3 -0
  102. package/modules/hall-ui/public/collab.html +12 -0
  103. package/modules/hall-ui/public/craft-mode.js +4 -1
  104. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  105. package/modules/hall-ui/public/drachmae.html +1 -1
  106. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  107. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  108. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  109. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  110. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  111. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  115. package/modules/hall-ui/public/goal-inbox.js +1 -1
  116. package/modules/hall-ui/public/goals-render.js +4 -0
  117. package/modules/hall-ui/public/goals.html +8 -0
  118. package/modules/hall-ui/public/gov-living.js +6 -2
  119. package/modules/hall-ui/public/hall-ask.css +5 -0
  120. package/modules/hall-ui/public/hall-ask.js +39 -3
  121. package/modules/hall-ui/public/notes-panel.css +20 -0
  122. package/modules/hall-ui/public/notes-panel.js +146 -0
  123. package/modules/hall-ui/public/project-settings.html +1 -1
  124. package/modules/hall-ui/public/project-settings.js +7 -5
  125. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  126. package/modules/hall-ui/public/settings.html +15 -0
  127. package/modules/hall-ui/public/street-deck.js +12 -3
  128. package/modules/hall-ui/public/studio.css +90 -8
  129. package/modules/hall-ui/public/studio.html +39 -10
  130. package/modules/hall-ui/public/studio.js +148 -18
  131. package/modules/hall-ui/public/task.html +2 -0
  132. package/modules/hall-ui/public/task.js +4 -0
  133. package/modules/hall-ui/public/tweak-editor.js +12 -3
  134. package/modules/hall-ui/public/welcome-boot.css +42 -6
  135. package/modules/hall-ui/public/welcome-boot.js +20 -6
  136. package/modules/hall-ui/public/work.js +20 -0
  137. package/modules/hall-ui/records/ai-studio.md +36 -0
  138. package/modules/hall-ui/records/blockers-people.md +24 -0
  139. package/modules/lifecycle/db-claims.js +32 -4
  140. package/modules/lifecycle/db-goals.js +18 -5
  141. package/modules/lifecycle/db-tasks.js +15 -6
  142. package/modules/lifecycle/done-when.js +23 -1
  143. package/modules/lifecycle/lifecycle.js +5 -0
  144. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  145. package/modules/lifecycle/module.json +2 -1
  146. package/modules/lifecycle/notes.js +219 -0
  147. package/modules/lifecycle/page-notice.js +124 -0
  148. package/modules/lifecycle/page-tweak-artist.js +18 -1
  149. package/modules/lifecycle/publish-reconciler.js +67 -20
  150. package/modules/lifecycle/routes/claims.js +5 -2
  151. package/modules/lifecycle/routes/goals.js +3 -1
  152. package/modules/lifecycle/routes/notes.js +91 -0
  153. package/modules/lifecycle/routes/project-settings.js +43 -35
  154. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  155. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  156. package/modules/lifecycle/routes/tasks.js +12 -12
  157. package/modules/lifecycle/task-needs-gate.js +70 -0
  158. package/modules/market-watch/CLAUDE.md +15 -0
  159. package/modules/market-watch/db.js +340 -0
  160. package/modules/market-watch/key.js +58 -0
  161. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  162. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  163. package/modules/market-watch/module.json +26 -0
  164. package/modules/market-watch/pollers/hub-seed.js +54 -0
  165. package/modules/market-watch/pollers/weekly-run.js +100 -0
  166. package/modules/market-watch/research.js +350 -0
  167. package/modules/market-watch/run.js +245 -0
  168. package/modules/market-watch/seed/market-data.json +38 -0
  169. package/modules/market-watch/seed/partners-data.json +497 -0
  170. package/modules/market-watch/seed.js +252 -0
  171. package/modules/market-watch/store.js +230 -0
  172. package/modules/memory/routes/memory.js +14 -2
  173. package/modules/memory/routes/search.js +8 -2
  174. package/modules/platform-identity/account-logins.js +192 -0
  175. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  176. package/modules/platform-identity/platform-identity.js +5 -4
  177. package/modules/platform-identity/routes/my-projects.js +6 -0
  178. package/modules/platform-identity/routes/profile.js +18 -0
  179. package/modules/platform-identity/routes/public-profile.js +10 -1
  180. package/modules/platform-identity/routes/sso.js +26 -1
  181. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  182. package/modules/provisioning/credential-preflight.js +13 -3
  183. package/modules/provisioning/pollers/app-liveness.js +25 -5
  184. package/modules/provisioning/routes/provisioning.js +1 -1
  185. package/modules/provisioning/starter-bundles.js +6 -2
  186. package/modules/public-landing/public/account.html +10 -2
  187. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  188. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  189. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  190. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  191. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  192. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  193. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  194. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  195. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  196. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  197. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  198. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  199. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  200. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  201. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  202. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  203. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  204. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  205. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  206. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  207. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  208. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  209. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  210. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  211. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  212. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  213. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  214. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  215. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  216. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  217. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  218. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  219. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  220. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  221. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  222. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  223. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  224. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  225. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  226. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  227. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  228. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  229. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  230. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  231. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  232. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  233. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  234. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  235. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  236. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  237. package/modules/public-landing/public/community.html +1 -1
  238. package/modules/public-landing/public/connections.html +2 -2
  239. package/modules/public-landing/public/guild.html +1 -1
  240. package/modules/public-landing/public/invitations.html +2 -2
  241. package/modules/public-landing/public/projects.html +5 -5
  242. package/modules/raise/views/nda.html +2 -0
  243. package/modules/specialities/db.js +32 -1
  244. package/modules/specialities/routes/specialities.js +10 -6
  245. package/modules/status-ui/public/index.html +1 -1
  246. package/package-lock.json +2 -2
  247. package/package.json +1 -1
  248. package/release-notes.json +220 -0
  249. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  250. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  251. package/scripts/bongos/claim.js +22 -1
  252. package/scripts/bongos/db-backup-nightly.sh +29 -0
  253. package/scripts/bongos/deploy-divergence.js +7 -0
  254. package/scripts/bongos/doc-cli-guard.js +6 -0
  255. package/scripts/bongos/fitness.js +3 -2
  256. package/scripts/bongos/gen-diagrams.js +3 -3
  257. package/scripts/bongos/lib.sh +4 -5
  258. package/scripts/bongos/migration-namespace.js +13 -1
  259. package/scripts/bongos/plain-cards.js +4 -0
  260. package/scripts/bongos/provision-github-setup.js +3 -1
  261. package/scripts/bongos/provision-units.js +6 -1
  262. package/scripts/bongos/publish-manifest.js +3 -0
  263. package/scripts/bongos/release.js +2 -0
  264. package/scripts/bongos/run-unit-tests.js +268 -82
  265. package/scripts/bongos/seed-tasks.example.json +56 -0
  266. package/scripts/bongos/seed-tasks.js +222 -0
  267. package/scripts/bongos/seed-verify-criteria.js +1 -2
  268. package/scripts/bongos/ship-finish.js +2 -0
  269. package/scripts/bongos/ship-resume.js +2 -0
  270. package/scripts/bongos/strand-lease.js +250 -0
  271. package/scripts/bongos/strand-watch.js +26 -5
  272. package/scripts/bongos/version-literal-guard.js +2 -2
  273. package/scripts/discord/post.js +4 -3
  274. package/scripts/gds/seed-tasks.js +7 -0
  275. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  276. package/scripts/hall-preview/server.js +21 -0
  277. package/src/bongos/auth-config.js +29 -1
  278. package/src/bongos/auth-discord.js +3 -3
  279. package/src/bongos/auth-github.js +3 -6
  280. package/src/bongos/auth-google.js +217 -0
  281. package/src/bongos/device-approval.js +140 -0
  282. package/src/bongos/llm-pricing.js +17 -1
  283. package/src/bongos/metrics.js +48 -2
  284. package/src/bongos/middleware/rate-limit.js +30 -11
  285. package/src/bongos/module-scope-map.js +12 -1
  286. package/src/bongos/optimizer.js +15 -1
  287. package/src/bongos/payout-approvals.js +342 -0
  288. package/src/bongos/payout-gates.js +364 -0
  289. package/src/bongos/prelaunch-gate.js +7 -9
  290. package/src/bongos/prelaunch-switch.js +20 -0
  291. package/src/bongos/route-rank-check.js +3 -0
  292. package/src/bongos/routes/auth-google.js +246 -0
  293. package/src/bongos/routes/auth.js +38 -1
  294. package/src/bongos/routes/backup.js +33 -9
  295. package/src/bongos/routes/builders.js +57 -30
  296. package/src/bongos/routes/github.js +9 -0
  297. package/src/bongos/routes/me.js +18 -0
  298. package/src/bongos/routes/my-payouts.js +133 -0
  299. package/src/bongos/routes/payout-approvals.js +312 -0
  300. package/src/bongos/routes/payout-gates.js +179 -0
  301. package/src/bongos/routes.js +15 -1
  302. package/src/bongos/secret-box.js +18 -3
  303. package/src/bongos/serve-internal.js +47 -19
  304. package/src/bongos/sign-in-pages.js +138 -0
  305. package/src/bongos/task-needs.js +146 -0
  306. package/src/bongos-downloads.js +18 -1
  307. package/src/instance-config.js +28 -0
  308. package/src/module-api.js +14 -1
  309. package/tests/account_logins.mjs +326 -0
  310. package/tests/account_logins_db.mjs +161 -0
  311. package/tests/ai_studio_billing.mjs +253 -0
  312. package/tests/ai_studio_generate.mjs +282 -0
  313. package/tests/ai_studio_keys.mjs +159 -0
  314. package/tests/ai_studio_requests.mjs +319 -0
  315. package/tests/auth_backchannel_logout_route.mjs +201 -0
  316. package/tests/auto_satisfy_criteria.mjs +10 -1
  317. package/tests/autobongos_tweak_lane.mjs +144 -0
  318. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  319. package/tests/backfill_subagent_rewards.mjs +17 -1
  320. package/tests/backup_trigger_lock.mjs +119 -0
  321. package/tests/blockers_people.mjs +198 -0
  322. package/tests/bounded_rate_maps.mjs +184 -0
  323. package/tests/claimable_blocker_status.mjs +1 -1
  324. package/tests/claims_batch_routes.mjs +421 -0
  325. package/tests/config_read_cache.mjs +171 -0
  326. package/tests/copy_desk_page_editor.mjs +12 -0
  327. package/tests/cost_write_guard.mjs +200 -0
  328. package/tests/craft_achievements.mjs +33 -10
  329. package/tests/criterion_goal_attach.mjs +1 -1
  330. package/tests/db_backup_nightly.mjs +73 -1
  331. package/tests/deploy_divergence_line.mjs +7 -0
  332. package/tests/effective_visibility_predicate.mjs +12 -0
  333. package/tests/fitness.mjs +19 -0
  334. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  335. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  336. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  337. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  338. package/tests/github_one_trip.mjs +34 -0
  339. package/tests/github_repo_visibility.mjs +20 -0
  340. package/tests/goal_achievement.mjs +30 -1
  341. package/tests/goal_achievement_reward.mjs +205 -0
  342. package/tests/goal_sort_staleness.mjs +2 -1
  343. package/tests/goal_tier.mjs +11 -1
  344. package/tests/google_fonts_nonblocking.mjs +84 -0
  345. package/tests/google_sign_in.mjs +401 -0
  346. package/tests/government_board_expiry.mjs +94 -0
  347. package/tests/government_protected_surfaces.mjs +1 -1
  348. package/tests/government_routes.mjs +1 -1
  349. package/tests/hall_ai_studio.mjs +347 -0
  350. package/tests/hall_page_gate_map.mjs +3 -0
  351. package/tests/hall_studio_home.mjs +61 -2
  352. package/tests/home_attention_cards.mjs +1 -1
  353. package/tests/human_signal.mjs +141 -0
  354. package/tests/install_script_reader.mjs +51 -0
  355. package/tests/integration_fixture_ids.mjs +82 -0
  356. package/tests/invite_expiry_db.mjs +13 -1
  357. package/tests/lib_sh_resolution.mjs +0 -4
  358. package/tests/market_watch_data.mjs +244 -0
  359. package/tests/market_watch_data_db.mjs +308 -0
  360. package/tests/market_watch_module.mjs +114 -0
  361. package/tests/market_watch_run.mjs +503 -0
  362. package/tests/metrics.mjs +124 -0
  363. package/tests/migration_namespace.mjs +14 -0
  364. package/tests/module_api.mjs +2 -0
  365. package/tests/module_loader.mjs +1 -1
  366. package/tests/notes.mjs +182 -0
  367. package/tests/own_scope_guard.mjs +5 -0
  368. package/tests/page_tweak_marker_sql.mjs +76 -0
  369. package/tests/page_tweak_no_block.mjs +135 -0
  370. package/tests/page_tweak_notice.mjs +103 -0
  371. package/tests/payout_approvals.mjs +253 -0
  372. package/tests/payout_approvals_db.mjs +106 -0
  373. package/tests/payout_gates.mjs +322 -0
  374. package/tests/payout_gates_db.mjs +113 -0
  375. package/tests/planning_session_skill.mjs +13 -3
  376. package/tests/project_settings_page.mjs +29 -5
  377. package/tests/project_settings_spend_cap_route.mjs +235 -0
  378. package/tests/promotion_sweep.mjs +29 -1
  379. package/tests/provision.mjs +3 -2
  380. package/tests/provisioning_app_status.mjs +18 -0
  381. package/tests/provisioning_credential_preflight.mjs +53 -0
  382. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  383. package/tests/publish_reconciler_overlap.mjs +60 -0
  384. package/tests/rank_change_broadcast_port.mjs +151 -0
  385. package/tests/seed_tasks.mjs +176 -0
  386. package/tests/seed_verify_criteria.mjs +2 -6
  387. package/tests/site_classified_once.mjs +142 -0
  388. package/tests/speciality_db_shape.mjs +23 -0
  389. package/tests/speciality_session_skills.mjs +48 -2
  390. package/tests/strand_lease.mjs +249 -0
  391. package/tests/strand_watch.mjs +10 -0
  392. package/tests/task_criteria_routes.mjs +275 -0
  393. package/tests/task_criteria_unlink_db.mjs +123 -0
  394. package/tests/task_needs.mjs +233 -0
  395. package/tests/task_needs_db.mjs +110 -0
  396. package/tests/unit_runner_serial.mjs +266 -43
  397. package/tests/version_literals.mjs +5 -7
  398. package/tests/welcome_boot.mjs +16 -5
  399. package/tests/welcome_boot_browser.mjs +36 -4
  400. package/docs/recipes/bongos-cli-release.md +0 -66
  401. package/scripts/bongos/backfill-disciplines.js +0 -144
  402. package/scripts/bongos/backfill-session-earnings.js +0 -123
  403. package/scripts/bongos/backfill-session-types.js +0 -87
  404. package/scripts/bongos/build-bongos.js +0 -94
  405. package/scripts/bongos/cleanup-orphans.js +0 -74
  406. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  407. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  408. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  409. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  410. package/scripts/bongos/smoke-batch-release.sh +0 -316
  411. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  412. package/scripts/bongos/smoke-dependencies.sh +0 -143
  413. package/scripts/gds/backfill-disciplines.js +0 -7
  414. package/scripts/gds/backfill-session-earnings.js +0 -7
  415. package/scripts/gds/backfill-session-types.js +0 -7
  416. package/scripts/gds/cleanup-orphans.js +0 -7
  417. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  418. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  419. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  420. package/scripts/gds/smoke-batch-claim.sh +0 -8
  421. package/scripts/gds/smoke-batch-release.sh +0 -8
  422. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  423. package/scripts/gds/smoke-dependencies.sh +0 -8
  424. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -687,6 +687,32 @@
687
687
  setText('st-status-detail', lib.statusDetail(home.pages));
688
688
  }
689
689
 
690
+ // The AI door and the AI balance line (task 1004921, goal 1000136). Only where
691
+ // the ai-studio module is on: it is off by default, so a missing flag means
692
+ // off here (not moduleOn's show-when-unsure). The door's line follows the
693
+ // newest request (ai-studio-lib.js homeLine); the balance line shows only
694
+ // where paying from a balance is set up.
695
+ async function loadAi() {
696
+ const mods = window.__MODULES__;
697
+ const aiLib = window.OTBAiStudioLib;
698
+ if (!aiLib || !mods || !mods.enabled || mods.enabled['ai-studio'] !== true) return;
699
+ const [reqs, bal] = await Promise.all([
700
+ api.request('GET', `${API}/ai-studio/requests`),
701
+ api.request('GET', `${API}/ai-studio/balance`),
702
+ ]);
703
+ if (!reqs.ok && !bal.ok) return;
704
+ const door = byId('st-ai');
705
+ const line = aiLib.homeLine(reqs.ok && reqs.data ? reqs.data.requests : [], new Date());
706
+ if (door) { door.href = line.href; door.hidden = false; }
707
+ setText('st-ai-detail', line.detail);
708
+ const b = bal.ok && bal.data ? bal.data.balance : null;
709
+ const row = byId('st-ai-balance');
710
+ if (row && b) {
711
+ setText('st-ai-balance-detail', `${aiLib.balanceUsd(b.available_micro)} left`);
712
+ row.hidden = false;
713
+ }
714
+ }
715
+
690
716
  // The public site's reviews (task 1004499): docs written with its Artist
691
717
  // review button, newest first. Each row is the doc's own link, which opens it
692
718
  // in the palette's panel right here (craft-mode.js reads ?note=).
@@ -760,25 +786,33 @@
760
786
  return `<ul class="st-surfaces">${rows}</ul>${asked ? `<p class="st-asked">${escapeHtml(asked)}</p>` : ''}${inFlight}${worstList}`;
761
787
  }
762
788
 
789
+ // What the two views of the live site's reviews (the panel and the full page)
790
+ // say in common, so they cannot drift apart.
791
+ const SITE_FAILED = 'The reviews could not be read just now. Try again in a moment.';
792
+ const SITE_NONE = 'None yet. A signed-in builder on the live site sees a Review button in its top bar; what they write lands here.';
793
+ const siteWhen = (iso, withYear) => {
794
+ const d = iso ? new Date(iso) : null;
795
+ return d && !isNaN(d) ? d.toLocaleDateString(undefined, withYear ? { month: 'short', day: 'numeric', year: 'numeric' } : { month: 'short', day: 'numeric' }) : '';
796
+ };
797
+ const siteIdeas = (r) => (r.status === 'filed' ? `${r.idea_count} ${r.idea_count === 1 ? 'idea' : 'ideas'}` : 'splitting');
798
+ const siteExcerpt = (r, max) => {
799
+ const whole = String(r.excerpt || '').replace(/!\[[^\]\n]*\]\([^)\n]*\)/g, '[image]').replace(/\s+/g, ' ').trim();
800
+ return whole.length > max ? `${whole.slice(0, max - 1).replace(/\s+\S*$/, '')}…` : whole;
801
+ };
802
+ // A review's link comes from the server; only a path on this site is followed.
803
+ const siteHref = (link) => (/^\/(?!\/)/.test(String(link || '')) ? String(link) : '#');
804
+
763
805
  function siteHtml() {
764
806
  if (!home.site) {
765
- return `<p class="st-panel__line">${home.siteFailed ? 'The reviews could not be read just now. Try again in a moment.' : 'Reading the reviews…'}</p>`;
807
+ return `<p class="st-panel__line">${home.siteFailed ? SITE_FAILED : 'Reading the reviews…'}</p>`;
766
808
  }
767
- if (!home.site.length) {
768
- return '<p class="st-panel__line">None yet. A signed-in builder on the live site sees a Review button in its top bar; what they write lands here.</p>';
769
- }
770
- const day = (iso) => { const d = iso ? new Date(iso) : null; return d && !isNaN(d) ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''; };
771
- const rows = home.site.map((r) => {
772
- const ideas = r.status === 'filed' ? `${r.idea_count} ${r.idea_count === 1 ? 'idea' : 'ideas'}` : 'splitting';
773
- const whole = String(r.excerpt || '').replace(/!\[[^\]\n]*\]\([^)\n]*\)/g, '[image]').replace(/\s+/g, ' ').trim();
774
- const text = whole.length > 140 ? `${whole.slice(0, 139).replace(/\s+\S*$/, '')}…` : whole;
775
- return `
776
- <li><a class="st-site" href="${escapeHtml(r.link)}">
777
- <span class="st-site__head"><span class="st-site__page">${escapeHtml(r.page_title || r.page_path || r.page_id)}</span><span class="st-site__meta">${escapeHtml(r.author_name)} · ${escapeHtml(day(r.submitted_at))} · ${escapeHtml(ideas)}</span></span>
778
- <span class="st-site__text">${escapeHtml(text)}</span>
779
- </a></li>`;
780
- }).join('');
781
- return `<ul class="st-sites">${rows}</ul>`;
809
+ if (!home.site.length) return `<p class="st-panel__line">${SITE_NONE}</p>`;
810
+ const rows = home.site.map((r) => `
811
+ <li><a class="st-site" href="${escapeHtml(siteHref(r.link))}">
812
+ <span class="st-site__head"><span class="st-site__page">${escapeHtml(r.page_title || r.page_path || r.page_id)}</span><span class="st-site__meta">${escapeHtml(r.author_name)} · ${escapeHtml(siteWhen(r.submitted_at))} · ${escapeHtml(siteIdeas(r))}</span></span>
813
+ <span class="st-site__text">${escapeHtml(siteExcerpt(r, 140))}</span>
814
+ </a></li>`).join('');
815
+ return `<ul class="st-sites">${rows}</ul><p class="st-panel__line"><a href="/builders/studio?view=site">Read all the live-site reviews</a></p>`;
782
816
  }
783
817
 
784
818
  function paintPanel() {
@@ -1092,11 +1126,107 @@
1092
1126
  window.addEventListener('pagehide', () => { if (wedit.editing) saveWords(); });
1093
1127
  }
1094
1128
 
1129
+ // ---- the full-page reader --------------------------------------------------
1130
+ // The live site's reviews as a page of their own, not a panel over the room:
1131
+ // /studio?view=site lists them, /studio?note=N reads one review. Both replace the
1132
+ // room's greeting and doors in place (html.st-reading) and nothing is modal.
1133
+
1134
+ const IMAGE_RE = /!\[([^\]\n]{0,120})\]\(\/api\/bongos\/page-notes\/images\/(\d{1,18})\)/g;
1135
+ const HEADING_RE = /^(#{1,3})\s+(.*)$/;
1136
+ const ITEM_RE = /^\s*[-*]\s+(.*)$/;
1137
+ const NOTE_RE = /[?&]note=([1-9]\d{0,17})(?:&|$)/;
1138
+ const SITE_VIEW_RE = /[?&]view=site(?:&|$)/;
1139
+
1140
+ // A review's markdown as headings, lists and paragraphs. Deliberately not
1141
+ // md-reader.js: that one follows links and draws tables and code, and a review
1142
+ // may show only this module's own pictures; anything else stays escaped text.
1143
+ // Same subset as the side sheet's reader (page-notes.js), which this replaces
1144
+ // on the Studio.
1145
+ function docBodyHtml(md) {
1146
+ const out = [];
1147
+ let para = [];
1148
+ let list = [];
1149
+ const inline = (line) => escapeHtml(line).replace(IMAGE_RE, (_m, alt, id) => `<img class="st-doc__img" src="${API}/page-notes/images/${id}" alt="${alt}" loading="lazy">`);
1150
+ const flush = () => {
1151
+ if (para.length) out.push(`<p>${para.join('<br>')}</p>`);
1152
+ if (list.length) out.push(`<ul>${list.map((x) => `<li>${x}</li>`).join('')}</ul>`);
1153
+ para = []; list = [];
1154
+ };
1155
+ for (const raw of String(md || '').split('\n')) {
1156
+ const line = raw.replace(/\s+$/, '');
1157
+ const h = HEADING_RE.exec(line);
1158
+ const li = ITEM_RE.exec(line);
1159
+ if (!line.trim()) flush();
1160
+ else if (h) { flush(); out.push(`<h3>${inline(h[2])}</h3>`); }
1161
+ else if (li) { if (para.length) flush(); list.push(inline(li[1])); }
1162
+ else { if (list.length) flush(); para.push(inline(line)); }
1163
+ }
1164
+ flush();
1165
+ return out.join('');
1166
+ }
1167
+
1168
+ function showReader(html, title) {
1169
+ const box = byId('st-doc');
1170
+ if (!box) return;
1171
+ setText('st-doc-title', title);
1172
+ const body = byId('st-doc-body');
1173
+ if (body) body.innerHTML = html;
1174
+ box.hidden = false;
1175
+ document.documentElement.classList.add('st-reading');
1176
+ const t = byId('st-doc-title');
1177
+ if (t) t.focus();
1178
+ }
1179
+
1180
+ function siteListPage() {
1181
+ if (!home.site) return `<p class="st-panel__line">${home.siteFailed ? SITE_FAILED : 'Reading the reviews…'}</p>`;
1182
+ if (!home.site.length) return `<p class="st-panel__line">${SITE_NONE}</p>`;
1183
+ return `<ul class="st-doc-list">${home.site.map((r) => `<li><a class="st-doc-list__row" href="${escapeHtml(siteHref(r.link))}">
1184
+ <span class="st-doc-list__page">${escapeHtml(r.page_title || r.page_path || r.page_id)}</span>
1185
+ <span class="st-doc-list__meta">${escapeHtml(r.author_name)} · ${escapeHtml(siteWhen(r.submitted_at, true))} · ${escapeHtml(siteIdeas(r))}</span>
1186
+ <span class="st-doc-list__text">${escapeHtml(siteExcerpt(r, 260))}</span>
1187
+ </a></li>`).join('')}</ul>`;
1188
+ }
1189
+
1190
+ async function openDocPage(id) {
1191
+ showReader('<p class="st-panel__line">Opening the review…</p>', 'Review');
1192
+ const res = await api.request('GET', `${API}/page-notes/${id}`);
1193
+ if (!res.ok || !res.data || !res.data.note) {
1194
+ showReader('<p class="st-panel__err" role="alert">That review could not be opened. It may have been removed, or you may not have access.</p>', 'Review');
1195
+ return;
1196
+ }
1197
+ const n = res.data.note;
1198
+ const ideas = res.data.ideas || [];
1199
+ const filed = n.status === 'filed';
1200
+ const became = filed
1201
+ ? `<h3 class="st-doc__h">It became ${ideas.length} ${ideas.length === 1 ? 'idea' : 'ideas'}</h3><ul class="st-doc__ideas">${ideas.map((i) => `<li><a href="/#/idea/${escapeHtml(String(i.id))}">${escapeHtml(i.title)}</a></li>`).join('')}</ul>`
1202
+ : '<p class="st-panel__line">Being split into ideas…</p>';
1203
+ showReader(
1204
+ `<p class="st-doc__meta">${escapeHtml(n.author_name)} · ${escapeHtml(siteWhen(n.submitted_at, true))}</p><div class="st-doc__text">${docBodyHtml(n.body_md)}</div>${became}`,
1205
+ n.page_title || n.page_id || 'Review',
1206
+ );
1207
+ }
1208
+
1209
+ // Which reader the address asks for: one review (?note=N) wins over the list
1210
+ // (?view=site). Read once, so the page makes one request for it.
1211
+ function readerRoute() {
1212
+ const q = (window.location && window.location.search) || '';
1213
+ const note = NOTE_RE.exec(q);
1214
+ if (note) return { note: note[1] };
1215
+ return SITE_VIEW_RE.test(q) ? { site: true } : null;
1216
+ }
1217
+
1095
1218
  async function bootHome(signedIn) {
1096
1219
  if (!hasDom || !lib) return;
1097
1220
  wireHome();
1098
- if (!signedIn) return;
1099
- await Promise.all([loadTally(), loadNext(), loadPages(), loadSite()]);
1221
+ const route = readerRoute();
1222
+ if (!signedIn) {
1223
+ // The side sheet stays shut on this page (craft-mode.js), so say why here.
1224
+ if (route) showReader('<p class="st-panel__line">Sign in to read page reviews.</p>', 'Review');
1225
+ return;
1226
+ }
1227
+ if (route && route.note) openDocPage(route.note);
1228
+ await Promise.all([loadTally(), loadNext(), loadPages(), loadSite(), loadAi()]);
1229
+ if (route && route.site) showReader(siteListPage(), 'From the live site');
1100
1230
  if (home.panel) paintPanel();
1101
1231
  }
1102
1232
 
@@ -15,6 +15,7 @@
15
15
  <link rel="stylesheet" href="/builders/record.css">
16
16
  <link rel="stylesheet" href="/builders/hall-ask.css">
17
17
  <link rel="stylesheet" href="/builders/task.css">
18
+ <link rel="stylesheet" href="/builders/notes-panel.css">
18
19
  </head>
19
20
  <body data-page="task" data-page-title="Task" data-back-fallback="/work">
20
21
 
@@ -72,6 +73,7 @@
72
73
  page's Claim it button consumes it rather than a second hand-rolled POST. -->
73
74
  <script src="/builders/claim-action.js?v=2026-09-26-claimaction1" defer></script>
74
75
  <script src="/builders/hall-ask.js" defer></script>
76
+ <script src="/builders/notes-panel.js" defer></script>
75
77
  <script src="/builders/task.js?v=2026-09-28-taskclaim" defer></script>
76
78
  </body>
77
79
  </html>
@@ -815,6 +815,9 @@
815
815
  : '<button class="td-btn" type="button" id="abandon-open-btn">Retire this task…</button>';
816
816
  }
817
817
 
818
+ // Notes (task 1004745): grouped by purpose, filled by notes-panel.js after the body is set.
819
+ parts.push('<section class="rec-sec"><p class="rec-sec__h">Notes</p><div id="task-notes"></div></section>');
820
+
818
821
  // Copy prompt (the "ship link") — the same claim/build/ship prompt the Work
819
822
  // Board hands an agent. Purely client-side (clipboard write, no server call).
820
823
  parts.push(`
@@ -833,6 +836,7 @@
833
836
  if (body) body.innerHTML = parts.join('');
834
837
  fillExits(body);
835
838
  announceShown(t);
839
+ if (window.OTBNotes) window.OTBNotes.mount(document.getElementById('task-notes'), { task_id: t.id });
836
840
 
837
841
  const copyBtn = $('#copy-prompt-btn');
838
842
  if (copyBtn) {
@@ -296,10 +296,19 @@
296
296
  $('tw-submit').disabled = !st.writable;
297
297
  }
298
298
 
299
+ // Information only (task 1004948): engineering work is open on this page's files.
300
+ function engineeringLine() {
301
+ const list = st.ed && Array.isArray(st.ed.engineering_active) ? st.ed.engineering_active : [];
302
+ if (!list.length) return '';
303
+ const who = list.slice(0, 2).map((e) => escapeHtml(e.holder)).join(' and ');
304
+ return `<p>Engineering work is open on this page (${who}), so code may change around your words. You can keep writing.</p>`;
305
+ }
306
+
299
307
  function notice(html) {
300
308
  const n = $('tw-notice');
301
- if (!html) { n.hidden = true; n.innerHTML = ''; return; }
302
- n.innerHTML = html;
309
+ const all = (html || '') + engineeringLine();
310
+ if (!all) { n.hidden = true; n.innerHTML = ''; return; }
311
+ n.innerHTML = all;
303
312
  n.hidden = false;
304
313
  }
305
314
 
@@ -309,7 +318,7 @@
309
318
  if (st.ed.draft_on_older_reading) {
310
319
  notice('<p>The page changed after your draft was saved, so the draft is not shown over its new words. Rewrite the lines again here.</p>');
311
320
  }
312
- if (st.writable) return;
321
+ if (st.writable) { if (!st.ed.draft_on_older_reading) notice(''); return; }
313
322
  if (!r || r.state === 'draft' || (r.state === 'writing' && !r.holder)) {
314
323
  notice('<p>Nobody is writing this page. Take it, and every line here is yours to type over.</p>'
315
324
  + '<button class="tw-notice__act" id="tw-claim" type="button">take this page</button>');
@@ -56,20 +56,56 @@ html.wb-open { overflow: hidden; }
56
56
  .wb__skip:hover, .wb__skip:focus-visible { border-color: rgba(255, 255, 255, .6); outline: none; }
57
57
  .wb--day .wb__skip { background: rgba(255, 255, 255, .55); border-color: rgba(20, 22, 30, .2); color: #14161c; }
58
58
 
59
- /* the banner: what is being built */
59
+ /* the banner: the logo, animated (task 1004953). The rest state below IS the finished logo; the
60
+ motion lives only in keyframes that run once the veil lifts, so reduced motion (animation: none)
61
+ and a page that never lights both show the whole mark. */
60
62
  .wb__banner {
61
63
  position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 4;
62
- display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 10px 22px;
64
+ display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 10px 24px 10px 16px;
63
65
  max-width: calc(100% - 140px); text-align: center;
64
- background: rgba(10, 14, 32, .55); border: 1px solid rgba(94, 234, 212, .35); border-radius: 999px;
66
+ background: rgba(10, 14, 32, .5); border: 1px solid rgba(255, 255, 255, .14); border-radius: 999px;
65
67
  backdrop-filter: blur(6px);
66
68
  transition: transform 2.2s cubic-bezier(.45, 0, .25, 1);
67
69
  }
68
- .wb__eyebrow { font-family: var(--font-display); font-size: clamp(15px, 2.2vw, 22px); font-weight: 500; letter-spacing: .01em; }
70
+ .wb__lockup { display: flex; align-items: center; gap: 12px; }
71
+ .wb__mark, .wb__logo { width: clamp(34px, 4.4vw, 46px); height: clamp(34px, 4.4vw, 46px); flex: none; overflow: visible; }
72
+ .wb__logo { object-fit: contain; }
73
+ .wb__name {
74
+ font-family: var(--font-display); font-size: clamp(18px, 2.6vw, 26px); font-weight: 300;
75
+ letter-spacing: -.01em; text-transform: lowercase; white-space: nowrap;
76
+ }
77
+ /* the mark's ink follows the painting: pale by night, dark by day; the moon stays salmon */
78
+ .wb__ring { stroke: #e2e1e4; }
79
+ .wb__planet { fill: #e2e1e4; transform-box: fill-box; transform-origin: center; }
80
+ .wb__moonlet { transform-box: view-box; transform-origin: 100px 100px; }
81
+ .wb--day .wb__ring { stroke: #17181a; }
82
+ .wb--day .wb__planet { fill: #17181a; }
83
+ .wb--lit .wb__ring { stroke-dasharray: 100; animation: wb-ring 1.5s cubic-bezier(.6, 0, .2, 1) .5s both; }
84
+ .wb--lit .wb__planet { animation: wb-planet 1s cubic-bezier(.3, 1.4, .5, 1) .3s both; }
85
+ .wb--lit .wb__moonlet { animation: wb-orbit 1.6s cubic-bezier(.25, .8, .3, 1) 1.1s both, wb-glow 3.2s ease-in-out 2.7s infinite alternate; }
86
+ .wb--lit .wb__name { animation: wb-name 1.2s ease-out 1.5s both; }
87
+ @keyframes wb-ring { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
88
+ @keyframes wb-planet { from { transform: scale(.35); opacity: 0; } to { transform: none; opacity: 1; } }
89
+ @keyframes wb-orbit { from { transform: rotate(-200deg); opacity: 0; } 25% { opacity: 1; } to { transform: none; opacity: 1; } }
90
+ @keyframes wb-glow { from { filter: drop-shadow(0 0 0 rgba(251, 149, 105, 0)); } to { filter: drop-shadow(0 0 7px rgba(251, 149, 105, .95)); } }
91
+ @keyframes wb-name { from { opacity: 0; letter-spacing: .18em; } to { opacity: 1; letter-spacing: -.01em; } }
69
92
  .wb__version { font-family: var(--font-mono); font-size: 12px; color: #a5b4cf; }
70
- .wb--day .wb__banner { background: rgba(255, 255, 255, .7); border-color: rgba(15, 159, 143, .4); }
93
+ .wb--day .wb__banner { background: rgba(255, 255, 255, .7); border-color: rgba(20, 22, 30, .14); }
71
94
  .wb--day .wb__version { color: #475569; }
72
95
 
96
+ /* the credit, bottom right (task 1004953): under the veil, so it arrives with the scene */
97
+ .wb__built {
98
+ position: absolute; right: 16px; bottom: 14px; z-index: 5;
99
+ display: inline-flex; align-items: center; min-height: 24px; padding: 2px 10px; border-radius: 999px;
100
+ font-size: 12px; color: rgba(244, 244, 246, .78); text-decoration: none;
101
+ background: rgba(10, 14, 32, .4); backdrop-filter: blur(4px);
102
+ }
103
+ .wb__built b { font-weight: 600; margin-left: .3em; color: #f4f4f6; }
104
+ .wb__built:hover, .wb__built:focus-visible { color: #fff; outline: 1px solid rgba(255, 255, 255, .5); text-decoration: none; }
105
+ .wb--day .wb__built { color: rgba(20, 22, 28, .7); background: rgba(255, 255, 255, .6); }
106
+ .wb--day .wb__built b { color: #14161c; }
107
+ .wb--leaving .wb__built { opacity: 0; transition: opacity .4s; }
108
+
73
109
  /* the greeting and the choice */
74
110
  .wb__after {
75
111
  position: absolute; left: 0; right: 0; bottom: 8vh; z-index: 5;
@@ -124,7 +160,7 @@ html.wb-open { overflow: hidden; }
124
160
  }
125
161
  @media (max-width: 600px) {
126
162
  .wb__banner { top: 16px; max-width: calc(100% - 110px); border-radius: 18px; }
127
- .wb__eyebrow { font-size: 14px; }
163
+ .wb__name { font-size: 17px; }
128
164
  .wb__choices { grid-template-columns: 1fr; }
129
165
  .wb__roles, .wb__roles:has(> :nth-child(3):last-child) { grid-template-columns: 1fr 1fr; }
130
166
  }
@@ -3,8 +3,8 @@
3
3
  // Home only. The scene is the owner's own painting of a circuit city (welcome-day.webp and
4
4
  // welcome-night.webp, 2026-10-23), brought to life: a slow drift across the skyline, data pulses
5
5
  // climbing the circuit towers, hover cars crossing, and by night blinking aircraft lights, a breathing
6
- // moon and twinkling stars. A banner at the top says what is being built ("Working on the core of
7
- // <project>"). The picture follows the local time of day: the day painting from 06:00 to 17:59, the
6
+ // moon and twinkling stars. The banner at the top is the logo, animated, and a "Built on
7
+ // cloudbongos.com" credit sits in the bottom-right corner (task 1004953). The picture follows the local time of day: the day painting from 06:00 to 17:59, the
8
8
  // night one otherwise. It opens from a white (day) or black (night) veil, then "Good morning, <name>"
9
9
  // comes up with two choices. The way out is slow: the view sinks past the rooftops and fades into the
10
10
  // hall underneath.
@@ -22,7 +22,7 @@
22
22
  // - A painting that will not load leaves the sky gradient behind it, and the welcome works the same.
23
23
  // - prefers-reduced-motion: no motion; the still painting and the greeting at once, leaving is immediate.
24
24
  //
25
- // The pure parts (the day rule, the banner, the roles, the motion layout) are exported for
25
+ // The pure parts (the day rule, the logo, the roles, the motion layout) are exported for
26
26
  // tests/welcome_boot.mjs; tests/welcome_boot_browser.mjs runs the overlay itself in a real browser.
27
27
  (function () {
28
28
  'use strict';
@@ -54,7 +54,20 @@
54
54
  var id = branding && branding.identity;
55
55
  return String((id && (id.worldName || id.productName)) || 'this project');
56
56
  }
57
- function bannerText(branding) { return 'Working on the core of ' + projectName(branding); }
57
+ // The banner is the logo, animated (task 1004953; it replaced "Working on the core of <project>"):
58
+ // the ring draws in, the planet settles, the moon glides into the gap and glows. A project with its
59
+ // own logo (theme.logo, ADR 0358) shows that instead, as the sidebar's brand mark does.
60
+ var MARK_SVG = '<svg class="wb__mark" viewBox="0 0 200 200" aria-hidden="true">' +
61
+ '<path class="wb__ring" d="M137 31 A78 78 0 1 0 170 66" pathLength="100" fill="none" stroke-width="15" stroke-linecap="round"/>' +
62
+ '<circle class="wb__planet" cx="100" cy="100" r="55"/>' +
63
+ '<circle class="wb__moonlet" cx="156" cy="46" r="12" fill="#fb9569"/></svg>';
64
+ function logoHtml(branding) {
65
+ var logo = branding && branding.theme && branding.theme.logo;
66
+ var mark = logo ? '<img class="wb__logo" src="' + esc(logo) + '" alt="" width="40" height="40" decoding="async">' : MARK_SVG;
67
+ return '<span class="wb__lockup">' + mark + '<span class="wb__name">' + esc(projectName(branding)) + '</span></span>';
68
+ }
69
+ // The credit in the bottom-right corner (task 1004953).
70
+ var BUILT_ON = { href: 'https://cloudbongos.com', label: 'cloudbongos.com' };
58
71
 
59
72
  // ---------- the roles (the owner's mapping) ----------
60
73
  // `mode` is the craft mode the pick sets; `href` null means stay on Home.
@@ -159,10 +172,11 @@
159
172
  '<div class="wb__fx" id="wb-fx"></div>' +
160
173
  '</div></div>' +
161
174
  '<div class="wb__cloud wb__cloud--a" aria-hidden="true"></div><div class="wb__cloud wb__cloud--b" aria-hidden="true"></div>' +
162
- '<div class="wb__banner"><span class="wb__eyebrow">' + esc(bannerText(window.__BRANDING__)) + '</span>' +
175
+ '<div class="wb__banner">' + logoHtml(window.__BRANDING__) +
163
176
  '<span class="wb__version" id="wb-version" hidden></span></div>' +
164
177
  '<div class="wb__veil" aria-hidden="true"></div>' +
165
178
  '<button class="wb__skip" type="button" data-wb-skip>Skip</button>' +
179
+ '<a class="wb__built" href="' + BUILT_ON.href + '" target="_blank" rel="noopener">Built on <b>' + BUILT_ON.label + '</b></a>' +
166
180
  '<div class="wb__after">' + (builder ? signedInHtml(builder, now) : signedOutHtml()) + '</div>';
167
181
  }
168
182
  function signedInHtml(builder, now) {
@@ -374,7 +388,7 @@
374
388
  var api = {
375
389
  DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, ART: ART, TOWERS: TOWERS, BEACONS: BEACONS, LANES: LANES, AW: AW, AH: AH,
376
390
  localDay: localDay, firstVisitToday: firstVisitToday, greetingFor: greetingFor, nameOf: nameOf, timeOfDay: timeOfDay,
377
- artFor: artFor, projectName: projectName, bannerText: bannerText, rolesFor: rolesFor, pulseRuns: pulseRuns,
391
+ artFor: artFor, projectName: projectName, logoHtml: logoHtml, BUILT_ON: BUILT_ON, rolesFor: rolesFor, pulseRuns: pulseRuns,
378
392
  onAccess: onAccess, finish: finish, close: close,
379
393
  };
380
394
  if (typeof window !== 'undefined') { window.OTBWelcome = api; watchSignInLinks(); }
@@ -88,6 +88,8 @@
88
88
  const parts = [];
89
89
  const creator = $('#f-creator') ? $('#f-creator').value : '';
90
90
  const stale = $('#f-stale') ? $('#f-stale').value : '';
91
+ const needs = $('#f-needs') ? $('#f-needs').value : '';
92
+ if (needs) parts.push(`needs=${encodeURIComponent(needs)}`); // task 1004950
91
93
  if (creator) parts.push(`created_by=${encodeURIComponent(creator === '__none__' ? 'none' : creator)}`);
92
94
  if (stale) parts.push(`stale_days=${encodeURIComponent(stale)}`);
93
95
  return parts.length ? `?${parts.join('&')}` : '';
@@ -115,6 +117,7 @@
115
117
  const parts = [];
116
118
  if (f.creator) parts.push(`created_by=${encodeURIComponent(f.creator === '__none__' ? 'none' : f.creator)}`);
117
119
  if (f.stale) parts.push(`stale_days=${encodeURIComponent(f.stale)}`);
120
+ if (f.needs) parts.push(`needs=${encodeURIComponent(f.needs)}`);
118
121
  if (f.version) parts.push(`version=${encodeURIComponent(f.version)}`);
119
122
  if (f.kind) parts.push(`kind=${encodeURIComponent(f.kind)}`);
120
123
  if (f.discipline) parts.push(`discipline=${encodeURIComponent(f.discipline)}`);
@@ -347,6 +350,7 @@
347
350
  newcomer: val('#f-newcomer'),
348
351
  creator: val('#f-creator'),
349
352
  stale: val('#f-stale'),
353
+ needs: val('#f-needs'),
350
354
  };
351
355
  }
352
356
 
@@ -368,6 +372,9 @@
368
372
  if (f.creator === '__none__' && t.created_by != null) return false;
369
373
  if (f.creator && f.creator !== '__none__' && String(t.created_by) !== f.creator) return false;
370
374
  if (f.stale && !isStale(taskActivityMs(t), f.stale)) return false;
375
+ const tNeeds = Array.isArray(t.needs) ? t.needs : [];
376
+ if (f.needs === 'none' && tNeeds.length > 0) return false;
377
+ if (f.needs && f.needs !== 'none' && !tNeeds.includes(f.needs)) return false;
371
378
  return true;
372
379
  }
373
380
 
@@ -456,6 +463,8 @@
456
463
  : `<span class="task__tag">${escapeHtml(window.OTB.roleLabel(t.discipline))}</span>`;
457
464
  // A kind nobody set is no tag at all, like the role above (task 1004038).
458
465
  const kindTag = window.OTB.kindIsUnset(t.kind) ? '' : `<span class="task__tag">${escapeHtml(t.kind)}</span>`;
466
+ // task 1004950 — what the task needs beyond the build loop, said in words.
467
+ const needsTags = (Array.isArray(t.needs) ? t.needs : []).map((n) => `<span class="task__tag task__tag--needs">${n === 'owner' ? 'needs the project owner' : n === 'server' ? 'needs server access' : escapeHtml(n)}</span>`).join('');
459
468
  const newcomerTag = t.newcomer_friendly
460
469
  ? '<span class="task__tag task__tag--newcomer">newcomer</span>'
461
470
  : '';
@@ -489,6 +498,7 @@
489
498
  ${kindTag}
490
499
  ${disciplineTag}
491
500
  ${newcomerTag}
501
+ ${needsTags}
492
502
  ${rankTag}
493
503
  ${t.priority ? `<span class="task__tag">${escapeHtml(t.priority)}</span>` : ''}
494
504
  <span>${minutesText}</span>
@@ -1162,6 +1172,16 @@
1162
1172
  // shared STALE_CHOICES so both tiers offer the same lenses.
1163
1173
  { id: 'f-creator', label: 'Creator', options: [{ value: '', label: '— any —' }] },
1164
1174
  { id: 'f-stale', label: 'Age', options: STALE_CHOICES },
1175
+ // task 1004950 — what a task needs beyond the build loop. Answered by the
1176
+ // server (both feeds), like Creator and Age.
1177
+ {
1178
+ id: 'f-needs',
1179
+ label: 'Needs',
1180
+ options: [{ value: '', label: '— any —' },
1181
+ { value: 'owner', label: 'needs the project owner' },
1182
+ { value: 'server', label: 'needs server access' },
1183
+ { value: 'none', label: 'needs neither' }],
1184
+ },
1165
1185
  {
1166
1186
  id: 'f-fit',
1167
1187
  label: 'Run mode',
@@ -0,0 +1,36 @@
1
+ # The studio's AI pages — /builders/ai-studio (task 1004921)
2
+
3
+ **Goal 1000136, [ADR 0392](../../../docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md).** Built 1:1 on the owner's pick, option C of the five-option "AI Studio options" canvas ("Three steps: pick, describe, make"), plus the owner's addition (2026-10-29): *"when the image is made it needs the option to edit/update the prompt"*. The server half is the `ai-studio` module (`modules/ai-studio/CLAUDE.md`); this page is its only face besides the key panel in Settings.
4
+
5
+ Files: `ai-studio.html` (the room, drawn as studio.html draws it, and one view mount), `ai-studio.css` (spends only studio.css's `--st-*` tokens), `ai-studio-lib.js` (the pure rules, `window.OTBAiStudioLib`, node-require-safe) and `ai-studio.js` (the reads, the writes, the painting). The page's own id is `data-page="ai-studio"` (page-inventory.js keys pages on it, so it cannot share `studio`); studio.css names `ai-studio` beside `studio` on the room's rules (the tokens, the day sky, the full-width stage, the full-screen button), so the room and its two skies apply unchanged.
6
+
7
+ ## The views (one page, the address names the view)
8
+
9
+ - `?step=pick` — "What are you starting from?" Each file uploads as it is picked, one per call; the page says no to a wrong type or size before the upload starts, in the server's words.
10
+ - `?step=describe` — "What should it make?"
11
+ - `?step=make` — "How do you want to pay?" Two cards: *My own key* (only when one is saved; OpenRouter first, as `keys.chooseKey` prefers it) and *My balance* (only where payments are set up; disabled under the run ceiling, with a link to add money). A card press sends the request.
12
+ - `?request=N` — the waiting page (a clock, three lines, the lamp on), polled every 2.5 s, then the result: the picture big with thumbnails and *Save picture*, the reply, **Your request** in an editable box with *Make it again* / *Change files* / *Start something new*, and the receipt.
13
+ - `?view=balance` — what is left, *Add how much?* with the card charge shown before paying, this month's summary, and every charge and refund (`&all=1`).
14
+
15
+ The draft (files and words) lives in sessionStorage, so the steps survive a reload and the back button.
16
+
17
+ ## The rules worth knowing
18
+
19
+ - **A file goes with one request only** (the server's `createRequest`). So *Make it again* and *Change files* carry the earlier request's UPLOADS by address and upload a fresh copy of each when the new request is sent; outputs are never copied. *Make it again* pays the same way the first run did (`key_source`).
20
+ - **No price is spelled in the browser.** The card charge comes from `GET /ai-studio/topup/quote` and the "up to $0.50 is set aside" from `max_cost_micro` on `GET /ai-studio/balance`, both added in this task, so the fee and the ceiling stay named only in `pricing.js` / `studio.js`. The file limits ARE repeated in the lib (named, with a pointer), only to spare a doomed 4 MB upload; the server still checks.
21
+ - **Which own key ran is inferred**: the stored request says `own`, not which provider. A reported cost means OpenRouter, none means Gemini (Google reports no price). A failed run shows no receipt; its error line already says what was charged.
22
+ - **A key is never asked for here.** The "add your own key" line links to `/builders/settings#ai-keys`. `tests/hall_ai_studio.mjs` pins the three writes (`upload`, `request`, `topup`) and the absence of a key field.
23
+ - **The studio home's door** ("Make something with AI") and its **AI balance** line show only where `window.__MODULES__.enabled['ai-studio'] === true`: the module is off by default, so a missing flag means off (not `moduleOn`'s show-when-unsure). The door's line follows the newest request while it runs or for a day after it finished.
24
+
25
+ ## Where it differs from the canvas, and why
26
+
27
+ - **The column is 580 px**, not the canvas's 680: the studio's window never slides left of 680 px, and a wider column ran under it on a narrower window.
28
+ - **The waiting checklist is paced by the clock** ("Wrote the answer" ticks at 25 s): the server reports queued or running, not which call it is on.
29
+ - **By day the lamp lights while a request runs** (by day the room's lamp is otherwise off), so "the lamp stays on until it's done" holds at noon.
30
+ - **On a phone** the words start below the scaled window and end above the plant (`padding-bottom: 240px`), so no line is read over the drawing.
31
+ - **The PDF badge reads 10 px**, not 8: the kit flags 8 px as tiny type.
32
+ - **Not on the canvas at all:** the no-key and no-payments lines on step 3, the short-balance line, the failed-run result, *Test mode: no real money moves.*, the other-amount box, the module-off and signed-out lines.
33
+
34
+ ## The proof
35
+
36
+ `ai-studio.states.json` renders ten states (pick, describe, make, waiting, result, result-own, failed, balance, balance-all, out) against the hall-preview harness with the module on (`node scripts/hall-preview/server.js --port 4637 --fixture-me --modules-on ai-studio`, the `hall-preview-ai-studio` launch entry). All clean at 1440, 390 and 320, dark and light. The harness answers from `ai-studio__*.json` (request 1002 running, 1003 failed, 1004 own Gemini key, any other id the finished result on the balance) and serves every file as a stand-in picture. Behaviour: `tests/hall_ai_studio.mjs` (the lib, and the real page in a vm over those fixtures: the steps, the request sent on the card pressed, *Make it again* with the changed words on fresh copies, *Change files*, a failed run, the balance and top-up, the module off).
@@ -0,0 +1,24 @@
1
+ # Blockers under people — Blockers, Collab and the City say WHO a blocker waits on (task 1004999)
2
+
3
+ Goal 1000111 (working area 7, Government). A blocker has no assignee: who it waits on is the seat its ask
4
+ names (the owner, a building's Lead, a board), and the person in that seat changes. Every surface used to
5
+ show the seat alone — or, on /blockers, every blocker under "Requires project owner fix".
6
+
7
+ - **One rule, the server's.** `GET /government/blockers/people` and `/government/blockers/for-me` group
8
+ open blockers with `modules/government/waiting.js`, which resolves each one through `routing.js` — the
9
+ City's own rule. A page never decides whose a blocker is; it draws the answer.
10
+ - **/blockers** gains a *Waiting on people* section above the queue (`blockers-by-person.js`): the viewer's
11
+ own group first as **You**, then each person with their count, then **Nobody yet** for a seat nobody
12
+ sits in. Each row says the seat in words ("as the owner, since Planning has no Lead"). Hidden until the
13
+ read answers, so a signed-out visitor still gets the public queue and nothing else.
14
+ - **Collab** gains *Blockers waiting on you* beside *Asked of you*, from the same file. Collab loads no
15
+ `hall-kit.js`, so the file falls back to the shell's `OTB.escapeHtml` and a local date format.
16
+ - **The City** names the people on every routed card: `route.people` rides the docket, `gov-living.js`
17
+ keeps it on `waits_on.people`, and `street-deck.js` `whoWords` says "It waits on Ana, the Planning Lead."
18
+ - **The heading stays "Requires project owner fix"** — Home names its summary that way and
19
+ `tests/blockers_page_ui.mjs` pins the match; renaming both is a separate copy call.
20
+ - **Harness fixtures are DERIVED, not hand-written**: `government__docket.json`'s routes and the two
21
+ `government__blockers__*.json` reads are computed with `routing.js` + `waiting.js` over the leads
22
+ fixture, because `tests/hall_preview_governor_fixtures.mjs` re-derives every route and fails on a guess.
23
+
24
+ Tests: `tests/blockers_people.mjs`.
@@ -15,6 +15,8 @@ const { pool, withTx } = api;
15
15
  const log = api.logger('lifecycle');
16
16
  const seams = api;
17
17
  const { touchesOverlap } = api.pathMatch;
18
+ const { notPageMarkerSql } = require('./page-tweak-artist.js');
19
+ const { heldPagesForTouches } = require('./page-notice.js');
18
20
  // CLAIM_ANY / CLAIM_NEWCOMER name the two atoms in the refusal message. The
19
21
  // decision itself moved to db-rank-authz.claimGate (task 1003196), so this file
20
22
  // no longer calls claimEligibilityAllows / rankAllowsTask / xenosClaimAllowed
@@ -27,6 +29,9 @@ const { goalMembershipFailures } = require('./claim-feed.js');
27
29
  // called from all three claim surfaces below, so a flagged task can't be
28
30
  // claimed via the batch path as a bypass of the single-claim gate.
29
31
  const { relevanceFrictionFailures } = require('./db-relevance-flags.js');
32
+ // task 1004950 / ADR 0395: a task that needs server access refuses a claimer
33
+ // without the grant — the third extracted gate in the same shape.
34
+ const { taskNeedsFailures } = require('./task-needs-gate.js');
30
35
  const { unsatisfiedDeps } = require('./db-deps-criteria.js');
31
36
  // depRefusalRow: the ONE shape a DEPS_NOT_SHIPPED refusal carries. All three
32
37
  // claim paths below use it — hand-building the object here is what made task
@@ -251,6 +256,13 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
251
256
  const relevanceRefusals = await relevanceFrictionFailures({ client, tasks: [task] });
252
257
  if (relevanceRefusals.length > 0) throw relevanceRefusals[0].refusal;
253
258
 
259
+ // task 1004950 / ADR 0395: a task that needs the live server refuses a
260
+ // claimer without the server grant. Same extracted check on all three
261
+ // surfaces (task-needs-gate.js). `owner` never refuses here — the feed is
262
+ // where owner work narrows to the owner.
263
+ const needsRefusals = await taskNeedsFailures({ client, tasks: [task], builderId });
264
+ if (needsRefusals.length > 0) throw needsRefusals[0].refusal;
265
+
254
266
  // The claim gate (ADR 0015, generalized in task 1516 / ADR 0086 §5): a task
255
267
  // can't be claimed while any dependency is un-done — its OWN edges plus those
256
268
  // inherited from its goal/criteria, where "done" = task shipped | criterion
@@ -300,7 +312,7 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
300
312
  const { rows: activeClaimRows } = await client.query(
301
313
  `SELECT c.id, t.id AS task_id, t.title, t.touches, t.module_key FROM claims c
302
314
  JOIN tasks t ON t.id = c.task_id
303
- WHERE c.released_at IS NULL AND t.id <> $1`,
315
+ WHERE c.released_at IS NULL AND t.id <> $1 AND ${notPageMarkerSql('c', 't')}`,
304
316
  [taskId]
305
317
  );
306
318
  // BV1.R53 (task 1422): module-disjointness is a HARD gate (not advisory) when
@@ -460,7 +472,10 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
460
472
  console.log('[gds] graduation broadcast post-claim failed:', e && e.message);
461
473
  }
462
474
  }
463
- return { ...insertClaim.rows[0], just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingWarning };
475
+ // task 1004948: a gentle heads-up, never a refusal, when this task's files belong
476
+ // to a page an artist is updating. Read after the commit; a failed read is "none".
477
+ const pageNotice = await heldPagesForTouches(task.touches, client, { exceptTaskId: taskId });
478
+ return { ...insertClaim.rows[0], just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingWarning, page_notice: pageNotice };
464
479
  } catch (err) {
465
480
  await client.query('ROLLBACK').catch(() => {});
466
481
  if (err && err.code === '23505') {
@@ -658,6 +673,12 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
658
673
  failures.push({ task_id: f.task_id, reason: code, ...rest });
659
674
  }
660
675
 
676
+ // task 1004950: the SAME extracted server-access gate as claimTask.
677
+ for (const f of await taskNeedsFailures({ client, tasks: taskRows, builderId })) {
678
+ const { code, ...rest } = f.refusal;
679
+ failures.push({ task_id: f.task_id, reason: code, ...rest });
680
+ }
681
+
661
682
  // Per-task dependency check (ADR 0015, generalized task 1516). An EXTERNAL dep
662
683
  // (not in this batch) must already be done; a batch-internal dep isn't done yet
663
684
  // at claim time but doesn't block — the batch is worked serially. "Internal"
@@ -724,7 +745,8 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
724
745
  FROM claims c
725
746
  JOIN tasks t ON t.id = c.task_id
726
747
  WHERE c.released_at IS NULL
727
- AND t.id <> ALL($1::bigint[])`,
748
+ AND t.id <> ALL($1::bigint[])
749
+ AND ${notPageMarkerSql('c', 't')}`,
728
750
  [uniqIds]
729
751
  );
730
752
  for (const t of taskRows) {
@@ -966,7 +988,7 @@ async function validateClaimBatch({ taskIds, builderId }) {
966
988
  const { rows: activeClaims } = await pool.query(
967
989
  `SELECT c.id AS claim_id, t.title, t.touches, t.module_key
968
990
  FROM claims c JOIN tasks t ON t.id = c.task_id
969
- WHERE c.released_at IS NULL AND t.id <> ALL($1::bigint[])`,
991
+ WHERE c.released_at IS NULL AND t.id <> ALL($1::bigint[]) AND ${notPageMarkerSql('c', 't')}`,
970
992
  [uniqIds]
971
993
  );
972
994
 
@@ -1014,6 +1036,12 @@ async function validateClaimBatch({ taskIds, builderId }) {
1014
1036
  push(f.task_id, { reason: code, ...rest });
1015
1037
  }
1016
1038
 
1039
+ // task 1004950: server-access, kept in sync with claimTask + claimTasksBatch.
1040
+ for (const f of await taskNeedsFailures({ client: pool, tasks: taskRows, builderId })) {
1041
+ const { code, ...rest } = f.refusal;
1042
+ push(f.task_id, { reason: code, ...rest });
1043
+ }
1044
+
1017
1045
  // Dependencies — external to the batch only (a batch-internal dep is fine;
1018
1046
  // the batch is worked serially, same rule as claimTasksBatch).
1019
1047
  const batchIdSet = new Set(uniqIds);