@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
@@ -0,0 +1,146 @@
1
+ // modules/hall-ui/public/notes-panel.js — the Notes panel on a task or goal page
2
+ // (task 1004745, goal 1000095).
3
+ //
4
+ // Notes are information that helps scope a task or goal or helps finish it. They are
5
+ // shown GROUPED BY PURPOSE (scope, decisions, open questions, risks, progress, links)
6
+ // with the newest few open in each group and the rest folded behind "Show more", so a
7
+ // long history stays scannable instead of becoming one endless list. The grouping is
8
+ // the server's (GET /notes); this file only draws it and posts new notes.
9
+ //
10
+ // OTBNotes.mount(element, { task_id: 12 }) // or { goal_id: 3 }
11
+ //
12
+ // Dependency-free apart from window.BongosClient and window.OTB.escapeHtml, so both
13
+ // the task page and the goals page can load it.
14
+
15
+ (function () {
16
+ 'use strict';
17
+
18
+ const API = '/api/bongos';
19
+ const esc = (s) => window.OTB.escapeHtml(s == null ? '' : String(s));
20
+ const client = () => window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
21
+
22
+ // A builder's words are quoted data: a long dash inside them is normalised the way the
23
+ // public feed does (cleanTitle, goal-inbox.js), then escaped.
24
+ const clean = (raw) => esc(String(raw == null ? '' : raw).replace(/\s*[—–]\s*/g, ', ').replace(/,\s*,/g, ',').trim());
25
+
26
+ function rel(iso) {
27
+ if (!iso) return '';
28
+ const sec = (Date.now() - new Date(iso).getTime()) / 1000;
29
+ if (sec < 3600) return Math.max(1, Math.floor(sec / 60)) + 'm ago';
30
+ if (sec < 86400) return Math.floor(sec / 3600) + 'h ago';
31
+ return Math.floor(sec / 86400) + 'd ago';
32
+ }
33
+
34
+ function noteHtml(n, kinds) {
35
+ const who = n.author_name || n.author_login || 'Someone';
36
+ const moves = kinds
37
+ .filter((k) => k.key !== n.kind)
38
+ .map((k) => `<option value="${esc(k.key)}">${esc(k.label)}</option>`).join('');
39
+ return `
40
+ <li class="np-note" data-note="${esc(n.id)}">
41
+ <p class="np-note__body">${clean(n.body)}</p>
42
+ <p class="np-note__meta">
43
+ <span>${esc(who)} · ${esc(rel(n.created_at))}${n.kind_source === 'auto' ? ' · sorted automatically' : ''}</span>
44
+ <label class="np-move">Move to
45
+ <select data-move="${esc(n.id)}" aria-label="Move this note to another group"><option value="">…</option>${moves}</select>
46
+ </label>
47
+ <button type="button" class="np-link" data-del="${esc(n.id)}">Delete</button>
48
+ </p>
49
+ </li>`;
50
+ }
51
+
52
+ function groupHtml(g, kinds, expanded) {
53
+ const shown = expanded.has(g.kind) ? g.open.concat(g.folded) : g.open;
54
+ const more = expanded.has(g.kind) || !g.folded.length
55
+ ? ''
56
+ : `<button type="button" class="np-link" data-more="${esc(g.kind)}">Show ${esc(g.folded.length)} more</button>`;
57
+ return `
58
+ <section class="np-group" data-kind="${esc(g.kind)}">
59
+ <h3 class="np-group__h">${esc(g.label)} <span class="np-count">${esc(g.count)}</span>
60
+ <span class="np-group__hint">${esc(g.hint)}</span></h3>
61
+ <ul class="np-list">${shown.map((n) => noteHtml(n, kinds)).join('')}</ul>
62
+ ${more}
63
+ </section>`;
64
+ }
65
+
66
+ function formHtml(kinds) {
67
+ const opts = kinds.map((k) => `<option value="${esc(k.key)}">${esc(k.label)}</option>`).join('');
68
+ return `
69
+ <form class="np-form" autocomplete="off">
70
+ <label class="np-field"><span>What would help someone scope this or finish it?</span>
71
+ <textarea name="body" rows="3" maxlength="4000" required
72
+ placeholder="The mobile layout can wait until the desktop one ships."></textarea>
73
+ </label>
74
+ <div class="np-actions">
75
+ <label class="np-field np-field--inline"><span>Group</span>
76
+ <select name="kind"><option value="">Sort it for me</option>${opts}</select>
77
+ </label>
78
+ <button type="submit" class="td-btn">Add note</button>
79
+ </div>
80
+ </form>`;
81
+ }
82
+
83
+ async function mount(el, subject) {
84
+ if (!el || !subject) return;
85
+ const qs = subject.task_id != null ? `task_id=${encodeURIComponent(subject.task_id)}` : `goal_id=${encodeURIComponent(subject.goal_id)}`;
86
+ // Mounting again on the same element (the goals page reuses one slot across goals)
87
+ // must not stack a second set of listeners: the previous set is dropped first.
88
+ if (el.npAbort) el.npAbort.abort();
89
+ const ctl = new AbortController();
90
+ el.npAbort = ctl;
91
+ const on = (type, fn) => el.addEventListener(type, fn, { signal: ctl.signal });
92
+ const expanded = new Set();
93
+ let data = null;
94
+
95
+ async function load() {
96
+ const res = await client().request('GET', `${API}/notes?${qs}`);
97
+ data = res.ok ? res.data : null;
98
+ }
99
+
100
+ function draw() {
101
+ if (!data) { el.innerHTML = '<p class="np-empty">Notes could not be loaded.</p>'; return; }
102
+ const body = data.groups.length
103
+ ? data.groups.map((g) => groupHtml(g, data.kinds, expanded)).join('')
104
+ : '<p class="np-empty">No notes yet. Add what helps scope this or get it done.</p>';
105
+ el.innerHTML = `<div class="np">${body}${formHtml(data.kinds)}</div>`;
106
+ }
107
+
108
+ // A read that lands after the panel was re-mounted for another task or goal is
109
+ // dropped, so one subject's notes never paint over the next one's.
110
+ async function refresh() { await load(); if (!ctl.signal.aborted) draw(); }
111
+
112
+ on('click', async (ev) => {
113
+ const t = ev.target;
114
+ if (!(t instanceof Element)) return;
115
+ if (t.dataset.more) { expanded.add(t.dataset.more); draw(); return; }
116
+ if (t.dataset.del) {
117
+ const res = await client().request('DELETE', `${API}/notes/${encodeURIComponent(t.dataset.del)}`);
118
+ if (res.status === 403) window.OTB.toast('Only the author or a Metic+ can delete that note.');
119
+ await refresh();
120
+ }
121
+ });
122
+ on('change', async (ev) => {
123
+ const t = ev.target;
124
+ if (!(t instanceof HTMLSelectElement) || !t.dataset.move || !t.value) return;
125
+ const res = await client().request('PATCH', `${API}/notes/${encodeURIComponent(t.dataset.move)}`, { body: { kind: t.value } });
126
+ if (res.status === 403) window.OTB.toast('Only the author or a Metic+ can move that note.');
127
+ await refresh();
128
+ });
129
+ on('submit', async (ev) => {
130
+ ev.preventDefault();
131
+ const form = ev.target;
132
+ if (!(form instanceof HTMLFormElement)) return;
133
+ const text = form.elements.body.value.trim();
134
+ if (!text) return;
135
+ const body = Object.assign({ body: text }, subject);
136
+ if (form.elements.kind.value) body.kind = form.elements.kind.value;
137
+ const res = await client().request('POST', `${API}/notes`, { body });
138
+ if (!res.ok) { window.OTB.toast('That note could not be added.'); return; }
139
+ await refresh();
140
+ });
141
+
142
+ await refresh();
143
+ }
144
+
145
+ window.OTBNotes = { mount };
146
+ })();
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Project settings — {{worldName}}</title>
7
- <meta name="description" content="The fundamentals of this project in one place — how tasks ship, the stale timer, the category set, how credits are calculated, whether equity is real, and what share of upmarket revenue is paid on credits.">
7
+ <meta name="description" content="The fundamentals of this project in one place — how tasks ship, the stale timer, the category set, how credits are calculated, why credits are not equity, and whether a credit-weighted pool can pay out.">
8
8
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // WHAT THIS PAGE IS FOR. Goal 1000072's thesis: the project is legible to Claude and
5
5
  // illegible to a person. Answering "how long before work is called stale", "what
6
- // categories do we use", "how is a credit calculated", "is equity real" or "what
7
- // share of upmarket is paid on credits" each meant reading source. This page is the
6
+ // categories do we use", "how is a credit calculated", "are credits equity" or "can
7
+ // a credit-weighted pool pay out" each meant reading source. This page is the
8
8
  // translation layer — it invents no value and stores nothing of its own.
9
9
  //
10
10
  // THE RULE THAT SHAPES EVERY ROW: the SERVER decides what each row says and where it
@@ -14,9 +14,10 @@
14
14
  // describes, one release later and on the screen the owner trusts most.
15
15
  //
16
16
  // A ROW THE READER CANNOT TYPE INTO MUST LOOK UNWRITABLE AND SAY WHY. Three of the
17
- // five are read-only, and two of those are read-only for different reasons — equity
18
- // is a settled owner decision that it is not real, upmarket is a knob that does not
19
- // exist yet. Both render as themselves rather than as a plausible zero.
17
+ // five are read-only, and two of those are read-only for different reasons — credits
18
+ // are a settled owner decision that they are not equity (ADR 0387), and the
19
+ // credit-weighted pool is a pay type gated shut (ADR 0385). Both render as themselves
20
+ // rather than as a plausible zero.
20
21
  (() => {
21
22
  'use strict';
22
23
 
@@ -69,6 +70,7 @@
69
70
  'read-only': ['ps-pill--readonly', 'committed config'],
70
71
  'not-yet-real': ['ps-pill--readonly', 'not yet real'],
71
72
  absent: ['ps-pill--absent', 'not configured'],
73
+ gated: ['ps-pill--absent', 'not available (gated)'],
72
74
  unavailable: ['ps-pill--absent', 'unavailable'],
73
75
  };
74
76
 
@@ -0,0 +1,124 @@
1
+ // modules/hall-ui/public/settings-ai-keys.js — the "Your AI keys" panel on
2
+ // /builders/settings (task 1004919, goal 1000136, ADR 0392). A builder links
3
+ // their own OpenRouter or Gemini key here so AI studio generations run on their
4
+ // account and cost them nothing with us. The ONLY place a key is ever typed.
5
+ //
6
+ // Write-only: save, replace, remove. The server never sends a key back, only
7
+ // its last four characters, so once saved the full key is gone from the page.
8
+ // The panel stays hidden when the AI studio is not switched on (the read 404s).
9
+ (() => {
10
+ 'use strict';
11
+
12
+ const API = '/api/bongos';
13
+ const { escapeHtml, $, fmtDate } = window.OTB;
14
+ const toast = (msg) => window.OTB.toast(msg);
15
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
16
+ const toAuth = () => window.location.assign(`${API}/auth/web/start`);
17
+
18
+ let storageConfigured = true;
19
+
20
+ function entryHtml(k) {
21
+ return `<div class="artkey-entry-row">
22
+ <input type="password" class="artkey-input aikeys-input" autocomplete="off" spellcheck="false"
23
+ aria-label="Your ${escapeHtml(k.label)} key" placeholder="Paste your ${escapeHtml(k.label)} key">
24
+ <button type="button" class="settings-btn craft-save aikeys-save">Save key</button>
25
+ </div>`;
26
+ }
27
+
28
+ function rowHtml(k) {
29
+ const get = `<a href="${escapeHtml(k.learn_more_url)}" target="_blank" rel="noopener">Get a key ↗</a>`;
30
+ const head = `<strong>${escapeHtml(k.label)}</strong>`;
31
+ if (!storageConfigured) {
32
+ return `<div class="aikeys-row">${head}<p class="settings-card__hint">Key storage is not set up on this project yet.</p></div>`;
33
+ }
34
+ if (k.set) {
35
+ const added = k.set_at ? ` · added ${escapeHtml(fmtDate(k.set_at))}` : '';
36
+ return `<div class="aikeys-row" data-provider="${escapeHtml(k.provider)}">${head}
37
+ <p class="settings-card__ok">Key on file ending …${escapeHtml(k.last4 || '????')}${added}. Your generations run on it and we charge nothing.</p>
38
+ <div class="artkey-actions">
39
+ <button type="button" class="settings-btn aikeys-replace">Replace</button>
40
+ <button type="button" class="settings-btn settings-btn--quiet aikeys-remove">Remove</button>
41
+ </div>
42
+ <div class="aikeys-entry" hidden></div>
43
+ </div>`;
44
+ }
45
+ return `<div class="aikeys-row" data-provider="${escapeHtml(k.provider)}">${head}
46
+ ${entryHtml(k)}
47
+ <p class="settings-card__hint">${get} · Optional. Without one, generations run on ours and are charged to your balance.</p>
48
+ </div>`;
49
+ }
50
+
51
+ function wire(keys) {
52
+ for (const row of document.querySelectorAll('#aikeys-list .aikeys-row[data-provider]')) {
53
+ const provider = row.getAttribute('data-provider');
54
+ const k = keys.find((x) => x.provider === provider);
55
+ const bindEntry = (scope) => {
56
+ const input = scope.querySelector('.aikeys-input');
57
+ const save = scope.querySelector('.aikeys-save');
58
+ if (!input || !save) return;
59
+ save.addEventListener('click', () => saveKey(provider, input, save));
60
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); saveKey(provider, input, save); } });
61
+ };
62
+ bindEntry(row);
63
+ const replace = row.querySelector('.aikeys-replace');
64
+ if (replace) replace.addEventListener('click', () => {
65
+ const entry = row.querySelector('.aikeys-entry');
66
+ entry.innerHTML = entryHtml(k);
67
+ entry.hidden = false;
68
+ bindEntry(entry);
69
+ const i = entry.querySelector('.aikeys-input'); if (i) i.focus();
70
+ });
71
+ const remove = row.querySelector('.aikeys-remove');
72
+ if (remove) remove.addEventListener('click', () => removeKey(provider, k.label));
73
+ }
74
+ }
75
+
76
+ async function saveKey(provider, input, btn) {
77
+ const key = (input.value || '').trim();
78
+ if (!key) { toast('Paste your key first.'); return; }
79
+ btn.disabled = true;
80
+ try {
81
+ const res = await api.request('PUT', `${API}/ai-studio/keys/${encodeURIComponent(provider)}`, { body: { key } });
82
+ input.value = '';
83
+ if (res.status === 401) { toAuth(); return; }
84
+ if (!res.ok) { toast((res.data && res.data.message) || `Could not save (HTTP ${res.status}).`); btn.disabled = false; return; }
85
+ toast('Key saved.');
86
+ await render();
87
+ } catch (err) {
88
+ toast('Could not save your key. Try again.');
89
+ btn.disabled = false;
90
+ }
91
+ }
92
+
93
+ async function removeKey(provider, label) {
94
+ if (!window.confirm(`Remove your ${label} key? Generations will run on ours and be charged to your balance.`)) return;
95
+ const res = await api.request('DELETE', `${API}/ai-studio/keys/${encodeURIComponent(provider)}`);
96
+ if (res.status === 401) { toAuth(); return; }
97
+ if (!res.ok) { toast('Could not remove your key. Try again.'); return; }
98
+ toast('Key removed.');
99
+ await render();
100
+ }
101
+
102
+ async function render() {
103
+ const res = await api.request('GET', `${API}/ai-studio/keys`);
104
+ if (res.status === 401) { toAuth(); return false; }
105
+ if (res.status === 404) return false; // the AI studio is not switched on here
106
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
107
+ storageConfigured = res.data.storage_configured !== false;
108
+ const keys = res.data.keys || [];
109
+ $('#aikeys-list').innerHTML = keys.map(rowHtml).join('');
110
+ wire(keys);
111
+ return true;
112
+ }
113
+
114
+ async function setup() {
115
+ try {
116
+ if (await render()) $('#ai-keys').hidden = false;
117
+ } catch (err) {
118
+ $('#ai-keys').hidden = false;
119
+ $('#aikeys-list').innerHTML = '<p class="voices-empty">Your AI keys could not be loaded. Reload the page to try again.</p>';
120
+ }
121
+ }
122
+
123
+ setup();
124
+ })();
@@ -226,6 +226,20 @@
226
226
  <p class="section-note">Stored encrypted; never shown again after saving — only its last four digits.</p>
227
227
  </section>
228
228
 
229
+ <!-- YOUR AI KEYS — your own OpenRouter / Gemini keys for the AI studio
230
+ (task 1004919, ADR 0392). Rendered by settings-ai-keys.js against
231
+ /api/bongos/ai-studio/keys. Write-only: a key is never sent back,
232
+ only its last four characters. Hidden where the AI studio is off. -->
233
+ <section class="scroll" id="ai-keys" aria-labelledby="aikeys-h" data-group="access" hidden>
234
+ <h2 id="aikeys-h" class="scroll__h">Your AI keys</h2>
235
+ <p class="scroll__lede">
236
+ Link your own AI account so studio generations run on it and we charge
237
+ you nothing. Without a key, generations run on ours and come out of your balance.
238
+ </p>
239
+ <div class="settings-card" id="aikeys-list" aria-live="polite"></div>
240
+ <p class="section-note">Stored encrypted; never shown again after saving, only its last four characters.</p>
241
+ </section>
242
+
229
243
  <!-- ============ group: preferences ============ -->
230
244
 
231
245
  <!-- INTERACTION PROFILE — how replies are pitched for you (task 2010,
@@ -487,6 +501,7 @@
487
501
  settings.js, which sits close to the 1500-line size ratchet. -->
488
502
  <script src="/builders/settings-sessions.js?v=2026-08-29-sessions"></script>
489
503
  <script src="/builders/settings-passkeys.js?v=2026-10-15-passkeys"></script>
504
+ <script src="/builders/settings-ai-keys.js?v=2026-10-28-ai-keys"></script>
490
505
  <!-- task 1002514: the interaction-profile panel ("How Claude talks to you"), on
491
506
  the same precedent and for the same reason — settings.js is against the
492
507
  1500-line ratchet, and this panel shares nothing with the rest of the page. -->
@@ -123,12 +123,21 @@
123
123
  // ---------- one card ----------
124
124
  // A seat the server routed (task 1004705, `waits_on.real`) is named; an ask
125
125
  // the docket did not route waits on you, or on others, unnamed.
126
+ // Who sits in the seat, by name (task 1004999): "Lars, the owner", "Ana and Ben, the
127
+ // board". Falls back to the seat alone when the server named nobody.
128
+ function whoWords(w) {
129
+ const label = str(w && w.label) || 'others';
130
+ const names = (Array.isArray(w && w.people) ? w.people : []).map((p) => str(p && (p.name || p.login))).filter(Boolean);
131
+ if (!names.length) return label;
132
+ const who = names.length === 1 ? names[0] : names.length === 2 ? names[0] + ' and ' + names[1] : names[0] + ' and ' + (names.length - 1) + ' others';
133
+ return who + ', ' + label;
134
+ }
126
135
  function seatWords(ask) {
127
136
  const w = ask.waits_on || {};
128
137
  if (!w.real) return ask.needs_you ? 'waits on you' : 'waits on others';
129
138
  const label = str(w.label) || 'someone';
130
139
  if (ask.needs_you) return label === 'you' || label === 'the owner' || /^the .* Lead$/.test(label) ? 'waits on you' : 'waits on you · ' + label;
131
- return 'waits on ' + label;
140
+ return 'waits on ' + whoWords(w);
132
141
  }
133
142
  // ---------- the words, in the asker's own voice (task 1004702) ----------
134
143
  // The owner's pick (2026-10-13): a card speaks as "I", the agent that asked.
@@ -159,7 +168,7 @@
159
168
  else if (/^(board|committee):/.test(seat) && w.real && str(w.label)) you = upperFirst(w.label) + ' answers ' + (ask.batch ? 'these' : 'this') + ', and you sit in it.';
160
169
  else you = 'Only you can answer ' + (ask.batch ? 'these' : 'this') + '.';
161
170
  } else if (ask) {
162
- rest = 'It waits on ' + (str(w.label) || 'others') + '.';
171
+ rest = 'It waits on ' + (w.real ? whoWords(w) : (str(w.label) || 'others')) + '.';
163
172
  }
164
173
  const left = str(silent).trim() ? 'Left alone, ' + lowerFirst(stop(silent)) : '';
165
174
  return { you: you, rest: [rest, left].filter(Boolean).join(' ') };
@@ -363,7 +372,7 @@
363
372
 
364
373
  const model = {
365
374
  actFor: actFor, optionPlan: optionPlan, cardView: cardView, queueOf: queueOf, scopesOf: scopesOf, likesOf: likesOf, sessionCounts: sessionCounts,
366
- batchAsk: batchAsk, seatsFor: seatsFor, deckModel: deckModel, saidOf: saidOf, footOf: footOf, holdWords: holdWords, waitWords: waitWords,
375
+ batchAsk: batchAsk, seatsFor: seatsFor, deckModel: deckModel, saidOf: saidOf, footOf: footOf, whoWords: whoWords, holdWords: holdWords, waitWords: waitWords,
367
376
  BATCH: BATCH, ALIAS: ALIAS,
368
377
  };
369
378
 
@@ -30,6 +30,7 @@
30
30
  1003117), which still answers the reviews filed before Tweak Mode (TW14) in
31
31
  its own quiet dialog, in the hall's own tokens as it always was. */
32
32
 
33
+ body[data-page="ai-studio"],
33
34
  body[data-page="studio"] {
34
35
  --st-room: #07080d;
35
36
  --st-frame: #14151b;
@@ -85,6 +86,7 @@ body[data-page="studio"] {
85
86
  inks are set darker than the mockup's so they hold AA on the glass over a
86
87
  light wall (records/studio.md): the link, and the credits figure and focus
87
88
  ring, which by night are the accent. */
89
+ body[data-page="ai-studio"][data-sky="day"],
88
90
  body[data-page="studio"][data-sky="day"] {
89
91
  --st-room: #efe7dc;
90
92
  --st-frame: #d6c9b7;
@@ -136,6 +138,7 @@ body[data-page="studio"][data-sky="day"] .st-overlay {
136
138
  --st-scrim: rgba(239, 231, 220, .9);
137
139
  color-scheme: light;
138
140
  }
141
+ body[data-page="ai-studio"][data-sky="day"] .studio,
139
142
  body[data-page="studio"][data-sky="day"] .studio { color-scheme: light; }
140
143
  /* The Approval queue (approval-queue.css) is night glass on its own root; over
141
144
  the day room it wears the same day glass as the studio's own panels. Its
@@ -163,7 +166,9 @@ body[data-page="studio"][data-sky="day"] .aq-overlay :focus-visible { outline-co
163
166
 
164
167
  /* The stage fills the content box: the hall's content column is a reading
165
168
  width, and the artboard is the whole window. */
169
+ body[data-page="ai-studio"] .app-content,
166
170
  body[data-page="studio"] .app-content { max-width: none; padding: 0; }
171
+ body[data-page="ai-studio"] .app-foot,
167
172
  body[data-page="studio"] .app-foot { display: none; }
168
173
 
169
174
  .studio {
@@ -184,6 +189,7 @@ html[data-fullscreen="on"] .studio { min-height: 100vh; }
184
189
 
185
190
  /* In full screen the shell's one button floats over the night room in either
186
191
  theme, so on this page it wears the room's glass, not the hall's ink. */
192
+ html[data-fullscreen="on"] body[data-page="ai-studio"] .fullscreen-toggle,
187
193
  html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
188
194
  color: var(--st-ink);
189
195
  background: var(--st-glass);
@@ -451,36 +457,112 @@ body[data-sky="day"] .st-sunpatch { opacity: .7; }
451
457
  flex-direction: column;
452
458
  gap: 12px;
453
459
  }
460
+ /* The things to do (Resume and the two doors): the accent tint and an arrow, so
461
+ they read as buttons to press. The overview below (.st-glance) stays quiet. */
454
462
  .st-resume,
455
463
  .st-action {
464
+ position: relative;
456
465
  display: flex;
457
466
  flex-direction: column;
458
467
  align-items: flex-start;
459
468
  justify-content: center;
460
469
  gap: 3px;
461
470
  min-height: 72px;
462
- padding: 10px 24px;
471
+ padding: 10px 52px 10px 24px;
463
472
  box-sizing: border-box;
464
473
  border-radius: 22px;
465
474
  text-align: left;
475
+ background: var(--st-resume);
476
+ border: 1px solid var(--st-resume-edge);
466
477
  backdrop-filter: blur(20px);
467
478
  -webkit-backdrop-filter: blur(20px);
468
479
  }
469
- .st-resume {
470
- text-decoration: none;
471
- background: var(--st-resume);
472
- border: 1px solid var(--st-resume-edge);
480
+ .st-resume::after,
481
+ .st-action::after {
482
+ content: "\2192";
483
+ position: absolute;
484
+ right: 22px;
485
+ top: 50%;
486
+ transform: translateY(-50%);
487
+ font-size: 20px;
488
+ color: var(--st-link);
473
489
  }
490
+ .st-resume { text-decoration: none; }
474
491
  .st-resume[hidden] { display: none; }
492
+ /* The AI door (task 1004921) is a link, not a button: same row, no underline. */
493
+ a.st-action { text-decoration: none; }
494
+ a.st-action[hidden], a.st-glance__row[hidden] { display: none; }
495
+ a.st-glance__row { text-decoration: none; }
475
496
  .st-action {
476
497
  cursor: pointer;
477
- background: var(--st-glass);
478
- border: 1px solid var(--st-glass-edge);
479
498
  color: var(--st-ink);
480
499
  }
481
- .st-action:hover { border-color: var(--st-rule); background: var(--st-close); }
500
+ .st-resume:hover,
501
+ .st-action:hover { border-color: var(--st-focus); background: var(--st-close); }
482
502
  .st-resume__label,
483
503
  .st-action__label { font-size: 17px; color: var(--st-ink); }
504
+
505
+ /* The full-page reader: in place of the room, never over it. */
506
+ html.st-reading .st-words,
507
+ html.st-reading .st-actions,
508
+ html.st-reading .st-glance { display: none; }
509
+ .st-doc { position: relative; z-index: 1; width: min(720px, 100%); margin-top: 32px; }
510
+ .st-doc[hidden] { display: none; }
511
+ .st-doc__back { display: inline-block; min-height: 24px; font-size: 14px; color: var(--st-link); }
512
+ .st-doc__title { margin: 16px 0 8px; font-size: 28px; font-weight: 400; color: var(--st-ink); }
513
+ .st-doc__meta { margin: 0 0 20px; font-size: 13px; color: var(--st-ink-faint); }
514
+ .st-doc__text { font-size: 17px; line-height: 1.65; color: var(--st-ink); }
515
+ .st-doc__text h3, .st-doc__h { margin: 28px 0 8px; font-size: 19px; font-weight: 500; color: var(--st-ink); }
516
+ .st-doc__text p, .st-doc__text ul { margin: 0 0 14px; }
517
+ .st-doc__text ul, .st-doc__ideas { padding-left: 22px; }
518
+ .st-doc__ideas a { color: var(--st-link); }
519
+ .st-doc__img { display: block; max-width: 100%; margin: 12px 0; border-radius: 12px; }
520
+ .st-doc-list { list-style: none; margin: 16px 0 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
521
+ .st-doc-list__row {
522
+ display: flex; flex-direction: column; gap: 4px; padding: 16px 20px; border-radius: 18px;
523
+ background: var(--st-glass); border: 1px solid var(--st-glass-edge); text-decoration: none; color: var(--st-ink);
524
+ }
525
+ .st-doc-list__row:hover { border-color: var(--st-rule); }
526
+ .st-doc-list__page { font-size: 17px; }
527
+ .st-doc-list__meta { font-size: 12px; color: var(--st-ink-faint); }
528
+ .st-doc-list__text { font-size: 14px; color: var(--st-ink-faint); }
529
+ .st-doc__back:focus-visible, .st-doc-list__row:focus-visible { outline: 2px solid var(--st-focus); outline-offset: 3px; }
530
+
531
+ /* Overview: small text rows, no glass, set apart under the actions. */
532
+ .st-glance {
533
+ margin-top: 40px;
534
+ width: min(420px, 100%);
535
+ display: flex;
536
+ flex-direction: column;
537
+ gap: 2px;
538
+ padding-top: 16px;
539
+ border-top: 1px solid var(--st-glass-edge);
540
+ }
541
+ .st-glance__head {
542
+ font-family: var(--st-mono);
543
+ font-size: 11px;
544
+ letter-spacing: .08em;
545
+ text-transform: uppercase;
546
+ color: var(--st-ink-faint);
547
+ margin-bottom: 4px;
548
+ }
549
+ .st-glance__row {
550
+ display: flex;
551
+ flex-wrap: wrap;
552
+ align-items: baseline;
553
+ gap: 2px 12px;
554
+ min-height: 32px;
555
+ padding: 4px 0;
556
+ border: 0;
557
+ background: none;
558
+ cursor: pointer;
559
+ text-align: left;
560
+ font: inherit;
561
+ }
562
+ .st-glance__label { font-size: 14px; color: var(--st-ink-faint); text-decoration: underline; text-underline-offset: 3px; }
563
+ .st-glance__detail { font-size: 12px; color: var(--st-ink-faint); }
564
+ .st-glance__row:hover .st-glance__label { color: var(--st-ink); }
565
+ .st-glance__row:focus-visible { outline: 2px solid var(--st-focus); outline-offset: 3px; }
484
566
  .st-resume__detail { font-size: 13px; color: var(--st-link); }
485
567
  .st-action__detail { font-size: 13px; color: var(--st-ink-faint); }
486
568
  .st-resume:focus-visible,
@@ -127,16 +127,13 @@
127
127
  <span class="st-action__label">Approve and confirm tweaks</span>
128
128
  <span class="st-action__detail" id="studio-approvals-detail">tweaks waiting for your sign-off</span>
129
129
  </button>
130
- <button type="button" class="st-action" id="st-status" aria-haspopup="dialog">
131
- <span class="st-action__label">Needs to be reviewed</span>
132
- <span class="st-action__detail" id="st-status-detail">pages waiting on you, and how far each surface has come</span>
133
- </button>
134
- <!-- Task 1004499: the reviews builders wrote on the public site with
135
- its Artist review button. Each opens here, in the palette's panel. -->
136
- <button type="button" class="st-action" id="st-site" aria-haspopup="dialog">
137
- <span class="st-action__label">From the live site</span>
138
- <span class="st-action__detail" id="st-site-detail">reviews written on the live site</span>
139
- </button>
130
+ <!-- Task 1004921 (goal 1000136): the door to the studio's AI pages,
131
+ shown only where the ai-studio module is on. Its line points at
132
+ the newest request while it is being made or just finished. -->
133
+ <a class="st-action" id="st-ai" href="/builders/ai-studio" hidden>
134
+ <span class="st-action__label">Make something with AI</span>
135
+ <span class="st-action__detail" id="st-ai-detail">sketches in, ideas and pictures out</span>
136
+ </a>
140
137
  <!-- The retired review queue (task 1004325 / TW14), a quiet door that
141
138
  shows only while reviews filed before Tweak Mode still wait. -->
142
139
  <button type="button" class="st-older" id="st-older" aria-haspopup="dialog" hidden>
@@ -144,6 +141,37 @@
144
141
  </button>
145
142
  </div>
146
143
 
144
+ <!-- Overview, not actions: where things stand. Quiet text rows under the
145
+ actions so the three above read as the things to do. -->
146
+ <div class="st-glance" role="group" aria-label="Overview">
147
+ <span class="st-glance__head">Overview</span>
148
+ <button type="button" class="st-glance__row" id="st-status" aria-haspopup="dialog">
149
+ <span class="st-glance__label">Needs to be reviewed</span>
150
+ <span class="st-glance__detail" id="st-status-detail">pages waiting on you, and how far each surface has come</span>
151
+ </button>
152
+ <!-- Task 1004499: the reviews builders wrote on the public site with
153
+ its Artist review button. Each opens here, in the palette's panel. -->
154
+ <button type="button" class="st-glance__row" id="st-site" aria-haspopup="dialog">
155
+ <span class="st-glance__label">From the live site</span>
156
+ <span class="st-glance__detail" id="st-site-detail">reviews written on the live site</span>
157
+ </button>
158
+ <!-- Task 1004921: what is left of the AI balance, where paying from a
159
+ balance is set up. -->
160
+ <a class="st-glance__row" id="st-ai-balance" href="/builders/ai-studio?view=balance" hidden>
161
+ <span class="st-glance__label">AI balance</span>
162
+ <span class="st-glance__detail" id="st-ai-balance-detail"></span>
163
+ </a>
164
+ </div>
165
+
166
+ <!-- The full-page reader (not a dialog): the live site's reviews as a list
167
+ (?view=site) or one doc (?note=N). Shown in place of the room's
168
+ greeting and doors while html.st-reading is set. -->
169
+ <section class="st-doc" id="st-doc" aria-labelledby="st-doc-title" hidden>
170
+ <a class="st-doc__back" href="/builders/studio">← Back to the studio</a>
171
+ <h2 class="st-doc__title" id="st-doc-title" tabindex="-1">Review</h2>
172
+ <div class="st-doc__body" id="st-doc-body"></div>
173
+ </section>
174
+
147
175
  <!-- The two small glass panels: the Tweak/CopyWrite Flow and Artist
148
176
  Review Status. The Approval queue is its own component
149
177
  (approval-queue.js), mounted by studio.js. -->
@@ -206,6 +234,7 @@
206
234
  the studio's own. -->
207
235
  <script src="/builders/tweak-editor-lib.js?v=2026-09-28-tw23"></script>
208
236
  <script src="/builders/studio-lib.js?v=2026-10-09-day"></script>
237
+ <script src="/builders/ai-studio-lib.js?v=2026-10-29-ais1"></script>
209
238
  <script src="/builders/studio.js?v=2026-10-09-day"></script>
210
239
  </body>
211
240
  </html>