@bongos/core 1.24.79 → 1.24.80

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 (366) hide show
  1. package/.bongos-core.json +495 -350
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  4. package/.claude/skills/design/SKILL.md +1 -1
  5. package/.claude/skills/goal-create/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +1 -1
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +8 -0
  9. package/clients/bongos-client/index.cjs +8 -0
  10. package/clients/bongos-client/index.d.ts +11 -0
  11. package/clients/bongos-client/index.mjs +8 -0
  12. package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
  13. package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
  14. package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
  15. package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +195 -3
  18. package/docs/api-reference.md +8 -4
  19. package/docs/architecture.md +7 -3
  20. package/docs/copy-inventory.md +390 -370
  21. package/docs/copy-registry.json +719 -518
  22. package/docs/design/characters/character-sprites-source.webp +0 -0
  23. package/docs/file-map.md +7 -8
  24. package/docs/module-api-changelog.md +2 -0
  25. package/docs/modules-contract.md +5 -0
  26. package/docs/onboarding/slash-commands.md +1 -1
  27. package/docs/packs/{engineer.md → artisan.md} +5 -5
  28. package/docs/packs/artist.md +50 -4
  29. package/docs/page-inventory.json +15 -4
  30. package/docs/page-readings.json +3182 -3336
  31. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  32. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  33. package/migrations/core_297_builder_character.sql +30 -0
  34. package/modules/copy-desk/proposals.js +1 -1
  35. package/modules/economy/credits-by-craft.js +29 -15
  36. package/modules/hall-ui/public/ai-studio.html +4 -4
  37. package/modules/hall-ui/public/atlas.html +5 -5
  38. package/modules/hall-ui/public/atlas.js +3 -1
  39. package/modules/hall-ui/public/blockers.html +5 -5
  40. package/modules/hall-ui/public/board-room.html +4 -4
  41. package/modules/hall-ui/public/characters/artisan.png +0 -0
  42. package/modules/hall-ui/public/characters/artist.png +0 -0
  43. package/modules/hall-ui/public/characters/patron.png +0 -0
  44. package/modules/hall-ui/public/city.css +2 -1
  45. package/modules/hall-ui/public/city.html +9 -6
  46. package/modules/hall-ui/public/city.js +7 -1
  47. package/modules/hall-ui/public/collab.html +4 -4
  48. package/modules/hall-ui/public/commands.html +4 -4
  49. package/modules/hall-ui/public/copy-desk.html +4 -4
  50. package/modules/hall-ui/public/craft-mode.js +258 -64
  51. package/modules/hall-ui/public/deploy.html +4 -4
  52. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  53. package/modules/hall-ui/public/dom-utils.js +1 -1
  54. package/modules/hall-ui/public/goal-map.html +9 -6
  55. package/modules/hall-ui/public/goals.html +5 -5
  56. package/modules/hall-ui/public/government.html +4 -4
  57. package/modules/hall-ui/public/hall-render.js +3 -1
  58. package/modules/hall-ui/public/home-city-draw.js +3 -3
  59. package/modules/hall-ui/public/home-city.css +4 -0
  60. package/modules/hall-ui/public/home-city.js +17 -9
  61. package/modules/hall-ui/public/home-gallery.js +8 -1
  62. package/modules/hall-ui/public/home-idea-box.css +2 -2
  63. package/modules/hall-ui/public/home-idea-box.js +3 -3
  64. package/modules/hall-ui/public/idea.html +5 -5
  65. package/modules/hall-ui/public/ideas.html +5 -5
  66. package/modules/hall-ui/public/index.html +11 -7
  67. package/modules/hall-ui/public/market-watch-item.html +4 -4
  68. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  69. package/modules/hall-ui/public/market-watch.html +6 -9
  70. package/modules/hall-ui/public/market-watch.js +22 -45
  71. package/modules/hall-ui/public/modules.html +4 -4
  72. package/modules/hall-ui/public/money.html +4 -4
  73. package/modules/hall-ui/public/my-agents.html +4 -4
  74. package/modules/hall-ui/public/options-menu.js +409 -0
  75. package/modules/hall-ui/public/paintbrush.js +3 -3
  76. package/modules/hall-ui/public/palette.js +11 -13
  77. package/modules/hall-ui/public/primer.html +5 -5
  78. package/modules/hall-ui/public/profile-crafts.js +1 -1
  79. package/modules/hall-ui/public/profile-roles.js +23 -19
  80. package/modules/hall-ui/public/profile.css +1 -1
  81. package/modules/hall-ui/public/profile.html +19 -18
  82. package/modules/hall-ui/public/profile.js +19 -18
  83. package/modules/hall-ui/public/profile.states.json +4 -4
  84. package/modules/hall-ui/public/project-settings.html +4 -4
  85. package/modules/hall-ui/public/roster.html +4 -4
  86. package/modules/hall-ui/public/roster.js +3 -3
  87. package/modules/hall-ui/public/settings.css +24 -4
  88. package/modules/hall-ui/public/settings.html +21 -17
  89. package/modules/hall-ui/public/settings.js +44 -44
  90. package/modules/hall-ui/public/shell-game.css +65 -0
  91. package/modules/hall-ui/public/shell-game.js +283 -3
  92. package/modules/hall-ui/public/shell.js +7 -26
  93. package/modules/hall-ui/public/sky-interaction.js +7 -0
  94. package/modules/hall-ui/public/speciality-tools.js +1 -2
  95. package/modules/hall-ui/public/street-inside.js +3 -4
  96. package/modules/hall-ui/public/street.html +4 -4
  97. package/modules/hall-ui/public/studio.html +5 -5
  98. package/modules/hall-ui/public/style.css +73 -47
  99. package/modules/hall-ui/public/task-detail.js +5 -4
  100. package/modules/hall-ui/public/task.html +5 -5
  101. package/modules/hall-ui/public/thinking.css +10 -0
  102. package/modules/hall-ui/public/thinking.html +9 -6
  103. package/modules/hall-ui/public/tweak-editor.html +5 -5
  104. package/modules/hall-ui/public/tweak-editor.js +5 -1
  105. package/modules/hall-ui/public/update-pill.js +91 -2
  106. package/modules/hall-ui/public/visor.css +80 -0
  107. package/modules/hall-ui/public/visor.js +316 -0
  108. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  109. package/modules/hall-ui/public/watch.html +6 -5
  110. package/modules/hall-ui/public/watch.js +17 -2
  111. package/modules/hall-ui/public/welcome-boot.js +15 -6
  112. package/modules/hall-ui/public/work.html +5 -5
  113. package/modules/hall-ui/records/builders-roster.md +2 -2
  114. package/modules/hall-ui/records/craft-mode.md +13 -11
  115. package/modules/hall-ui/records/game-mode.md +2 -2
  116. package/modules/hall-ui/records/governor-street.md +1 -1
  117. package/modules/hall-ui/records/home-city.md +3 -1
  118. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  119. package/modules/hall-ui/records/options-menu.md +15 -0
  120. package/modules/hall-ui/records/profile-roles.md +11 -11
  121. package/modules/hall-ui/records/sky.md +3 -3
  122. package/modules/hall-ui/records/start-screen.md +21 -0
  123. package/modules/hall-ui/records/tweak-editor.md +1 -1
  124. package/modules/hall-ui/records/update-pill.md +12 -0
  125. package/modules/hall-ui/records/visor.md +23 -0
  126. package/modules/hall-ui/records/welcome-tower.md +5 -5
  127. package/modules/ideas/ideator-stats.js +3 -1
  128. package/modules/ideas/inbox.js +2 -2
  129. package/modules/lifecycle/activity.js +1 -1
  130. package/modules/lifecycle/cascade.js +4 -4
  131. package/modules/lifecycle/goal-spec-task.js +1 -1
  132. package/modules/lifecycle/help-requests.js +19 -15
  133. package/modules/lifecycle/kickoff-checklist.js +3 -3
  134. package/modules/lifecycle/kickoff-seed.js +1 -1
  135. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  136. package/modules/lifecycle/mingle.js +4 -4
  137. package/modules/lifecycle/page-tweak-write.js +1 -1
  138. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  139. package/modules/lifecycle/routes/tasks.js +3 -3
  140. package/modules/lifecycle/ship-card.js +2 -3
  141. package/modules/lifecycle/task-classifier.js +39 -29
  142. package/modules/lifecycle/tests/cascade.mjs +7 -7
  143. package/modules/onboarding/founding-mode.js +3 -0
  144. package/modules/onboarding/genesis-home.js +21 -1
  145. package/modules/onboarding/routes/onboarding.js +2 -0
  146. package/modules/page-notes/notes.js +3 -2
  147. package/modules/page-notes/split.js +1 -1
  148. package/modules/platform-identity/craft-rollup.js +35 -7
  149. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  150. package/modules/platform-identity/platform-identity.js +2 -2
  151. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  152. package/modules/platform-identity/routes/community-search.js +2 -1
  153. package/modules/platform-identity/routes/sso.js +2 -1
  154. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  155. package/modules/public-landing/public/community.html +3 -5
  156. package/modules/public-landing/public/index.html +1 -1
  157. package/modules/public-landing/public/projects.html +1 -1
  158. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  159. package/modules/specialities/routes/specialities.js +2 -2
  160. package/modules/status-ui/public/status.js +1 -1
  161. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  162. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  163. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  164. package/package-lock.json +2 -2
  165. package/package.json +1 -1
  166. package/release-notes.json +70 -0
  167. package/scripts/bongos/autobongos-loop.js +1 -1
  168. package/scripts/bongos/discipline-modes.json +7 -12
  169. package/scripts/bongos/fitness.js +1 -1
  170. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  171. package/scripts/bongos/grade-smoke.js +2 -2
  172. package/scripts/bongos/init.js +2 -2
  173. package/scripts/bongos/newcomer-floor.config.json +1 -2
  174. package/scripts/bongos/newcomer-floor.js +4 -3
  175. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  176. package/scripts/bongos/newcomer-floor.test.js +47 -38
  177. package/scripts/bongos/role-pack-guard.js +5 -4
  178. package/scripts/bongos/seed-tasks.example.json +3 -3
  179. package/scripts/bongos/setup.js +20 -107
  180. package/scripts/bongos/start.js +2 -2
  181. package/scripts/bongos/task.js +5 -4
  182. package/scripts/bongos/triage.js +1 -1
  183. package/scripts/bongos/worktree.js +16 -4
  184. package/scripts/setup-builder.js +6 -46
  185. package/src/bongos/auth-admission.js +8 -8
  186. package/src/bongos/craft-mode.js +2 -0
  187. package/src/bongos/db.js +39 -1
  188. package/src/bongos/github-repos.js +78 -0
  189. package/src/bongos/module-scope-map.js +22 -0
  190. package/src/bongos/role-stats.js +27 -11
  191. package/src/bongos/routes/github.js +100 -2
  192. package/src/bongos/routes/me.js +33 -0
  193. package/src/bongos/serve-internal.js +26 -9
  194. package/src/crafts.js +66 -0
  195. package/src/module-api.js +6 -1
  196. package/src/module-loader/manifest-schema.js +16 -0
  197. package/src/modules.js +2 -2
  198. package/src/role-responsibilities.js +6 -5
  199. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  200. package/tests/activity_rollup_as_of.mjs +1 -1
  201. package/tests/api_alias_caller_ratchet.mjs +3 -3
  202. package/tests/application_lifecycle.mjs +1 -1
  203. package/tests/artist_review.mjs +1 -1
  204. package/tests/artist_review_goal.mjs +1 -1
  205. package/tests/blocker_hall_live_proof.mjs +1 -1
  206. package/tests/blocker_tasks_named_db.mjs +2 -2
  207. package/tests/canonical_profile_url.mjs +1 -1
  208. package/tests/cap_newcomer_tasks.mjs +13 -13
  209. package/tests/character_creation.mjs +294 -0
  210. package/tests/claim_gate_open_blocker.mjs +2 -2
  211. package/tests/claim_gate_rebase.mjs +6 -6
  212. package/tests/claude_materialize.mjs +1 -1
  213. package/tests/clawback_confirmed_backlog.mjs +3 -3
  214. package/tests/collab_page.mjs +8 -9
  215. package/tests/collab_self_settling.mjs +2 -2
  216. package/tests/community_leaderboard_route.mjs +13 -5
  217. package/tests/community_proof_db.mjs +3 -0
  218. package/tests/community_search_route.mjs +7 -6
  219. package/tests/conductor.mjs +1 -1
  220. package/tests/conductor_main_e2e.mjs +8 -8
  221. package/tests/copy_desk_flags_db.mjs +1 -1
  222. package/tests/copy_desk_page_approve.mjs +2 -2
  223. package/tests/copy_desk_page_claim.mjs +2 -2
  224. package/tests/copy_desk_page_next.mjs +1 -1
  225. package/tests/copy_desk_page_release.mjs +1 -1
  226. package/tests/copy_desk_page_submit.mjs +2 -2
  227. package/tests/core_258_box_session_claims_db.mjs +1 -1
  228. package/tests/craft_mode.mjs +48 -34
  229. package/tests/craft_mode_home.mjs +20 -20
  230. package/tests/craft_mode_looks.mjs +16 -15
  231. package/tests/craft_mode_nav.mjs +31 -29
  232. package/tests/craft_review_mode.mjs +46 -41
  233. package/tests/crafts_vocabulary.mjs +70 -0
  234. package/tests/cross_discipline_priority_direction.mjs +5 -5
  235. package/tests/fitness.mjs +7 -11
  236. package/tests/funded_project_pays_proof_db.mjs +1 -1
  237. package/tests/genesis_home.mjs +21 -1
  238. package/tests/github_chip.mjs +3 -3
  239. package/tests/github_repo_invite.mjs +379 -0
  240. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  241. package/tests/goal_invite_consent.mjs +1 -1
  242. package/tests/goal_map_page.mjs +2 -2
  243. package/tests/goal_sort_staleness.mjs +1 -1
  244. package/tests/goal_spec_task.mjs +1 -1
  245. package/tests/goal_spec_task_db.mjs +3 -3
  246. package/tests/grade_engine.mjs +2 -2
  247. package/tests/grader_no_artifact.mjs +1 -1
  248. package/tests/grader_prompt_fencing.mjs +1 -1
  249. package/tests/grader_prompt_window_bound.mjs +1 -1
  250. package/tests/grader_reader_lens.mjs +2 -2
  251. package/tests/grader_severity_gate.mjs +1 -1
  252. package/tests/grader_unbacked_score.mjs +1 -1
  253. package/tests/hall_approval_queue.mjs +1 -1
  254. package/tests/hall_board_world.mjs +2 -2
  255. package/tests/hall_builders_page_pass.mjs +1 -1
  256. package/tests/hall_city.mjs +1 -1
  257. package/tests/hall_fullscreen.mjs +27 -3
  258. package/tests/hall_kit_charts.mjs +1 -1
  259. package/tests/hall_landing_boot.mjs +1 -1
  260. package/tests/hall_my_agents.mjs +16 -16
  261. package/tests/hall_nav.mjs +22 -15
  262. package/tests/hall_options_menu.mjs +247 -0
  263. package/tests/hall_oversight_world.mjs +1 -1
  264. package/tests/hall_palette.mjs +8 -9
  265. package/tests/hall_profile_world.mjs +3 -3
  266. package/tests/hall_record_world.mjs +2 -2
  267. package/tests/hall_settings_world.mjs +2 -1
  268. package/tests/hall_sitemap.mjs +6 -5
  269. package/tests/hall_start_screen.mjs +244 -0
  270. package/tests/hall_street.mjs +2 -2
  271. package/tests/hall_street_inside.mjs +17 -9
  272. package/tests/hall_studio_world.mjs +3 -3
  273. package/tests/hall_visor.mjs +224 -0
  274. package/tests/help_requests.mjs +47 -15
  275. package/tests/helpers.mjs +4 -4
  276. package/tests/home_city.mjs +6 -4
  277. package/tests/home_city_widget.mjs +33 -14
  278. package/tests/home_gallery_widget.mjs +4 -4
  279. package/tests/home_idea_box.mjs +6 -6
  280. package/tests/hub_community_page.mjs +3 -3
  281. package/tests/hub_craft_rollup.mjs +75 -36
  282. package/tests/hub_recent_ships.mjs +3 -2
  283. package/tests/idea_cascade.mjs +2 -2
  284. package/tests/idea_credit_streams_db.mjs +1 -1
  285. package/tests/idea_detail_page.mjs +1 -1
  286. package/tests/idea_ideate_full.mjs +5 -2
  287. package/tests/idea_spark_hall.mjs +1 -1
  288. package/tests/init.mjs +3 -3
  289. package/tests/instance_manifest.mjs +2 -2
  290. package/tests/kickoff_checklist.mjs +1 -1
  291. package/tests/leaderboard_roles.mjs +34 -25
  292. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  293. package/tests/lightbulb.mjs +14 -10
  294. package/tests/main_discipline.mjs +8 -8
  295. package/tests/markdown_line_endings.mjs +1 -1
  296. package/tests/market_watch_pages.mjs +52 -74
  297. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  298. package/tests/migration_allocation.mjs +1 -1
  299. package/tests/mingle.mjs +36 -21
  300. package/tests/module-scope-map.mjs +22 -2
  301. package/tests/module_api.mjs +1 -0
  302. package/tests/module_contributions.mjs +13 -13
  303. package/tests/module_manifest.mjs +29 -0
  304. package/tests/module_skills_hook.mjs +2 -2
  305. package/tests/modules.mjs +14 -7
  306. package/tests/nav_permission_atoms.mjs +2 -1
  307. package/tests/needs_rebase.mjs +1 -1
  308. package/tests/newcomer_restock.mjs +13 -13
  309. package/tests/page_notes_panel.mjs +22 -22
  310. package/tests/page_tweak_no_block.mjs +1 -1
  311. package/tests/paintbrush.mjs +7 -7
  312. package/tests/privacy_matrix.mjs +1 -0
  313. package/tests/profile_activity.mjs +20 -23
  314. package/tests/profile_earnings.mjs +1 -1
  315. package/tests/profile_nudge_prefs.mjs +4 -4
  316. package/tests/profile_role_headline.mjs +73 -46
  317. package/tests/profile_role_stats.mjs +55 -45
  318. package/tests/profile_rollup_consent.mjs +19 -6
  319. package/tests/profile_route.mjs +12 -12
  320. package/tests/profile_ui_cross_project.mjs +4 -4
  321. package/tests/project_invite_suggestions.mjs +2 -2
  322. package/tests/promote_goal_id.mjs +1 -1
  323. package/tests/publish_manifest.mjs +1 -1
  324. package/tests/rank_floor_wiring.mjs +1 -1
  325. package/tests/regrade_actual_minutes.mjs +1 -1
  326. package/tests/removal_proposal.mjs +2 -2
  327. package/tests/reward_gate.mjs +4 -4
  328. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  329. package/tests/role_responsibilities.mjs +18 -12
  330. package/tests/role_stats_db.mjs +17 -16
  331. package/tests/scouting_rollup_fanout.mjs +5 -5
  332. package/tests/seed_tasks.mjs +5 -2
  333. package/tests/session_earnings_mirror_db.mjs +1 -1
  334. package/tests/session_reward_on_land_db.mjs +1 -1
  335. package/tests/session_reward_reconcile_db.mjs +2 -2
  336. package/tests/sessions_kit_pager.mjs +1 -1
  337. package/tests/settings_art_key_absent.mjs +2 -2
  338. package/tests/settings_main_role.mjs +21 -20
  339. package/tests/setup_flow.mjs +3 -3
  340. package/tests/setup_smoke.mjs +20 -0
  341. package/tests/ship_card.mjs +6 -6
  342. package/tests/specialities.mjs +16 -4
  343. package/tests/speciality_db_shape.mjs +6 -6
  344. package/tests/speciality_injection.mjs +17 -17
  345. package/tests/speciality_offer_ui.mjs +5 -5
  346. package/tests/speciality_offered.mjs +6 -5
  347. package/tests/speciality_session_skills.mjs +7 -7
  348. package/tests/speciality_skills.mjs +11 -11
  349. package/tests/speciality_walkthrough_ui.mjs +2 -2
  350. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  351. package/tests/task_classifier_vocab.mjs +10 -0
  352. package/tests/task_cli.mjs +11 -3
  353. package/tests/task_criteria_unlink_db.mjs +2 -2
  354. package/tests/task_discipline_required.mjs +38 -16
  355. package/tests/task_goal_required.mjs +4 -4
  356. package/tests/task_needs_db.mjs +1 -1
  357. package/tests/tweak_replaces_artist_review.mjs +1 -1
  358. package/tests/update_pill.mjs +153 -2
  359. package/tests/upkeep_home.mjs +1 -1
  360. package/tests/v3_c1_options_proof.mjs +145 -0
  361. package/tests/visual_review.mjs +3 -3
  362. package/tests/watch_applications_queue.mjs +8 -5
  363. package/tests/watch_sealed_floor.mjs +3 -1
  364. package/tests/welcome_boot.mjs +12 -9
  365. package/tests/welcome_boot_browser.mjs +13 -13
  366. package/tests/worktree_helper.mjs +16 -1
@@ -0,0 +1,80 @@
1
+ /* visor.css — the first-person visor (task 1005148, visor.js; ADR 0399). Glass over the scene:
2
+ the frame lines, the place, three pills and the actions sheet. It shows in game mode only, and
3
+ it is an overlay, never layout, so the stage under it never changes size (ADR 0399 §2.1).
4
+ Colours are the hall's tokens; the frame is hairline ink, never a glow (DESIGN.md: no
5
+ near-black-plus-neon HUD). Record: modules/hall-ui/records/visor.md. */
6
+ .visor { display: none; }
7
+ html[data-game="on"] .visor:not([hidden]) {
8
+ display: block; position: fixed; inset: 0; z-index: 4; pointer-events: none;
9
+ /* where the glass sits, so a scene's pop-up can keep clear of it (ADR 0399 §2.3) */
10
+ --visor-inset-top: 64px; --visor-inset-right: 40px; --visor-inset-bottom: 76px; --visor-inset-left: 40px;
11
+ color: var(--ink); font-family: var(--font-body);
12
+ }
13
+
14
+ /* the frame: stretched over the stage, every stroke one hairline at any size */
15
+ .visor__frame { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
16
+ .visor__frame path { vector-effect: non-scaling-stroke; }
17
+ .visor__line { fill: none; stroke: var(--rule); stroke-width: var(--ds-line); }
18
+ .visor__line--side { stroke: var(--rule-soft); }
19
+ .visor__cell { fill: color-mix(in srgb, var(--ink) 12%, transparent); stroke: var(--rule); stroke-width: var(--ds-line); }
20
+ .visor__mark { fill: var(--accent); stroke: none; }
21
+
22
+ /* the place, under the compass */
23
+ .visor__place {
24
+ position: absolute; left: 50%; top: max(9.5%, 84px); transform: translateX(-50%); margin: 0;
25
+ font-family: var(--font-mono); font-size: var(--ds-text-2xs); letter-spacing: .14em; text-transform: uppercase;
26
+ color: var(--ink-soft); white-space: nowrap;
27
+ }
28
+
29
+ /* the pills: the only parts of the glass that take a pointer */
30
+ .visor__pill, .visor__me {
31
+ position: absolute; pointer-events: auto; box-sizing: border-box;
32
+ display: inline-flex; align-items: center; gap: var(--ds-space-xs);
33
+ min-height: 46px; padding: 0 var(--ds-space-sm);
34
+ background: var(--bg-card); color: var(--ink); border: var(--ds-line) solid var(--rule);
35
+ border-radius: var(--ds-radius-pill); font: inherit; font-size: var(--ds-text-sm); text-decoration: none;
36
+ }
37
+ .visor__pill { cursor: pointer; }
38
+ .visor__pill:hover { border-color: var(--ink-faint); }
39
+ .visor__pill:focus-visible, .visor__row:focus-visible, .visor__close:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
40
+ .visor__pill--main { left: var(--visor-inset-left); bottom: var(--visor-inset-bottom); background: var(--accent); color: var(--accent-on); border-color: var(--accent); font-weight: 500; }
41
+ .visor__pill--main:hover { border-color: var(--accent-deep); }
42
+ .visor__pill--actions { right: var(--visor-inset-right); top: var(--visor-inset-top); }
43
+ .visor__pill[hidden] { display: none; }
44
+ .visor__dots { width: 14px; height: 4px; background: radial-gradient(circle, currentColor 1.5px, transparent 2px) 0 0 / 5px 4px repeat-x; }
45
+ .visor__me { right: var(--visor-inset-right); bottom: var(--visor-inset-bottom); padding: 4px var(--ds-space-sm) 4px 4px; pointer-events: none; }
46
+ .visor__sprite { width: 38px; height: 38px; border-radius: var(--ds-radius-pill); object-fit: cover; object-position: 50% 12%; background: var(--bg-deep); }
47
+ .visor__who { display: flex; flex-direction: column; line-height: 1.2; }
48
+ .visor__name { font-weight: 500; }
49
+ .visor__credits { font-size: var(--ds-text-2xs); color: var(--ink-soft); font-variant-numeric: tabular-nums; }
50
+ .visor__key {
51
+ font-family: var(--font-mono); font-size: var(--ds-text-2xs); line-height: 1; padding: 3px 5px;
52
+ border: var(--ds-line) solid currentColor; border-radius: var(--ds-radius-pill); opacity: .75;
53
+ }
54
+
55
+ /* the sheet: above the scene's pop-ups, under the modals (ADR 0399 §2.3 layer 4) */
56
+ .visor__sheet {
57
+ position: absolute; right: var(--visor-inset-right); top: var(--visor-inset-top); z-index: 1;
58
+ pointer-events: auto; box-sizing: border-box; width: min(18rem, calc(100vw - 2 * var(--ds-space-gutter-narrow)));
59
+ max-height: calc(100% - var(--visor-inset-top) - var(--visor-inset-bottom)); overflow-y: auto;
60
+ padding: var(--ds-space-sm); background: var(--bg-card); border: var(--ds-line) solid var(--rule);
61
+ border-radius: var(--ds-radius-panel); box-shadow: var(--ds-shadow-pop);
62
+ }
63
+ .visor__sheet-head { display: flex; align-items: center; justify-content: space-between; }
64
+ .visor__sheet-title { margin: 0; font-size: var(--ds-text-md); font-weight: 500; }
65
+ .visor__close { min-width: 32px; min-height: 32px; background: none; border: 0; color: var(--ink-soft); cursor: pointer; }
66
+ .visor__cap { margin: var(--ds-space-xs) 0 2px; font-family: var(--font-mono); font-size: var(--ds-text-2xs); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }
67
+ .visor__rows { list-style: none; margin: 0; padding: 0; }
68
+ .visor__rows + .visor__rows { margin-top: var(--ds-space-xs); }
69
+ .visor__row {
70
+ display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-xs);
71
+ box-sizing: border-box; width: 100%; min-height: 36px; padding: 0 2px;
72
+ background: none; border: 0; border-top: var(--ds-line) solid var(--rule-soft);
73
+ color: var(--ink); font: inherit; font-size: var(--ds-text-sm); text-align: left; text-decoration: none; cursor: pointer;
74
+ }
75
+ .visor__row:hover { color: var(--accent-deep); }
76
+ .visor__row--hint { cursor: default; color: var(--ink-soft); }
77
+ .visor__row--hint:hover { color: var(--ink-soft); }
78
+
79
+ /* a phone (640px and narrower): the rules live in style.css’s shared phone block, so the
80
+ visor adds no breakpoint of its own (the design-count ratchet). */
@@ -0,0 +1,316 @@
1
+ // visor.js — the first-person visor: glass over the scene (task 1005148, BONGOS-V3 R08,
2
+ // goal 1000142, criterion `you-are-a-character-in-first-person`). Built to ADR 0399.
3
+ //
4
+ // What is on the glass (the design the owner agreed on 2026-11-10, task 1005148):
5
+ // - the frame: thin curved visor lines at the top, the bottom and both sides, a small
6
+ // centre cross, and a compass of nine slanted cells along the top line with the one
7
+ // salmon marker under its centre. Drawn once as SVG; it is ornament, never a control.
8
+ // - the place, in a mono line under the compass ("THE SKY", "GOVERNOR CITY", …).
9
+ // - bottom-left: your character's main action, the one accent pill, with its key.
10
+ // - bottom-right: your sprite (third person) with your character's name and your credits
11
+ // (builders.total_credits — credits only, never a payout; the spec's "does not exist").
12
+ // - top-right: "Actions" (key A), which opens the sheet: your character's actions, then
13
+ // the keys of the place you are in, then Options on Escape.
14
+ //
15
+ // The rules it keeps (ADR 0399):
16
+ // - one overlay, #visor, with pointer-events: none; only its pills take a pointer, so a
17
+ // drag, a wheel or a click on the world reaches the scene unchanged (§2.1).
18
+ // - no camera: a scene registers itself once with OTBVisor.scene({ name, place, actions,
19
+ // keys }) and a movement action calls the scene's own function (§2.2). A page with no
20
+ // scene registered shows no visor.
21
+ // - Escape (§2.4): an OPEN SHEET takes the press in capture and stops it (row 4), so the
22
+ // star under it stays picked; the ambient glass never takes one, so the last press
23
+ // still reaches the options menu (shell-game.js onEscape).
24
+ // - it hides nothing and gates nothing (§2.5): every action is a link or tool a page
25
+ // already offers, and the server's rank checks answer exactly as before.
26
+ // - game mode only: html[data-game="on"] shows it (visor.css). Outside it, the hall is
27
+ // exactly as it was.
28
+ //
29
+ // The character comes from craft-mode.js (task 1005147): the held character, or this
30
+ // session's mode on a core that has no characters yet. With neither, no visor.
31
+ // R09 (task 1005149) turns the keys below into the one rebindable registry.
32
+ (function () {
33
+ 'use strict';
34
+
35
+ // ---------- each character's actions ----------
36
+ // `href` opens a page the hall already has; `tool` opens one of craft-mode's tools on
37
+ // this page. The first is the character's main action (the accent pill).
38
+ var ACTIONS = {
39
+ artisan: [
40
+ { id: 'claim', label: 'Claim a task', key: 'c', href: '/work' },
41
+ { id: 'blockers', label: 'Blockers', key: 'b', href: '/blockers' },
42
+ { id: 'sky', label: 'Goal sky', key: 'g', href: '/goal-map' },
43
+ { id: 'thinking', label: 'Your thinking', key: 't', href: '/thinking' },
44
+ ],
45
+ artist: [
46
+ { id: 'review', label: 'Review this page', key: 'r', tool: 'palette' },
47
+ { id: 'words', label: 'Edit its words', key: 'e', tool: 'paintbrush' },
48
+ { id: 'idea', label: 'Write an idea', key: 'i', tool: 'lightbulb' },
49
+ { id: 'studio', label: 'Studio', key: 's', href: '/studio' },
50
+ ],
51
+ patron: [
52
+ { id: 'board', label: 'Board room', key: 'o', href: '/board-room' },
53
+ { id: 'city', label: 'Governor city', key: 'v', href: '/city' },
54
+ { id: 'blockers', label: 'Blockers', key: 'b', href: '/blockers' },
55
+ { id: 'watch', label: 'Watch', key: 'w', href: '/watch' },
56
+ ],
57
+ };
58
+ var SHEET_KEY = 'a';
59
+ var NAMES = { artisan: 'Artisan', artist: 'Artist', patron: 'Patron' };
60
+
61
+ function esc(v) { return window.OTB && window.OTB.escapeHtml ? window.OTB.escapeHtml(v) : String(v == null ? '' : v).replace(/[^\w .,:'-]/g, ''); }
62
+ function craft() { return window.OTBCraftMode || null; }
63
+ // The character on the glass: the held one, else this session's mode, else none.
64
+ function character() {
65
+ var C = craft();
66
+ if (!C) return null;
67
+ var held = typeof C.character === 'function' ? C.character() : undefined;
68
+ if (typeof held === 'string' && ACTIONS[held]) return held;
69
+ var mode = typeof C.current === 'function' ? C.current() : null;
70
+ return ACTIONS[mode] ? mode : null;
71
+ }
72
+ function actionsFor(id) { return ACTIONS[id] ? ACTIONS[id].slice() : []; }
73
+ function credits() {
74
+ var C = craft();
75
+ var me = C && typeof C.me === 'function' ? C.me() : null;
76
+ var n = me && me.builder ? Number(me.builder.total_credits) : NaN;
77
+ return isFinite(n) ? n : null;
78
+ }
79
+ function creditsText(n) {
80
+ if (n == null) return '';
81
+ var v = Math.round(n);
82
+ var s = String(Math.abs(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
83
+ return (v < 0 ? '−' : '') + s + (Math.abs(v) === 1 ? ' credit' : ' credits');
84
+ }
85
+ // A link on the hall's own host: '/work' on the builders host, '/builders/work' on the apex.
86
+ function hallHref(p) {
87
+ var here = String((window.location && window.location.pathname) || '');
88
+ return (here.indexOf('/builders/') === 0 || here === '/builders' ? '/builders' : '') + p;
89
+ }
90
+
91
+ // ---------- the scene ----------
92
+ var scene = null;
93
+ // A scene says what it is and, if it has a camera, the actions that move it (ADR 0399
94
+ // §2.2). `keys` are lines for the sheet's "this place" list: keys the scene already binds.
95
+ function registerScene(s) {
96
+ if (!s || typeof s !== 'object') return;
97
+ scene = {
98
+ name: String(s.name || ''),
99
+ place: String(s.place || ''),
100
+ actions: (Array.isArray(s.actions) ? s.actions : []).filter(function (a) { return a && a.label && typeof a.run === 'function'; }),
101
+ keys: (Array.isArray(s.keys) ? s.keys : []).filter(function (k) { return k && k.label && k.key; }),
102
+ };
103
+ render();
104
+ }
105
+
106
+ // ---------- drawing ----------
107
+ var root = null, sheet = null, returnFocus = null;
108
+ var NS = 'http://www.w3.org/2000/svg';
109
+ // The frame is drawn in a 1000×1000 box stretched over the stage (preserveAspectRatio
110
+ // none), with non-scaling strokes so every line stays one hairline at any size.
111
+ function frameSvg() {
112
+ var cells = '';
113
+ for (var i = -4; i <= 4; i++) {
114
+ var x = 500 + i * 38, y = 30 + Math.pow(i / 4, 2) * 14, s = i * 2;
115
+ cells += '<path class="visor__cell" d="M' + (x - 15) + ' ' + (y + 4 - s / 2) + 'l30 ' + s + 'l-6 15l-30 ' + (-s) + 'z"/>';
116
+ }
117
+ return '<svg class="visor__frame" viewBox="0 0 1000 1000" preserveAspectRatio="none" aria-hidden="true" focusable="false" xmlns="' + NS + '">' +
118
+ '<path class="visor__line" d="M0 120Q500 -10 1000 120"/>' +
119
+ '<path class="visor__line" d="M0 880Q500 1010 1000 880"/>' +
120
+ '<path class="visor__line visor__line--side" d="M18 120Q4 500 18 880M982 120Q996 500 982 880"/>' +
121
+ '<path class="visor__line" d="M490 500h-12M510 500h12M500 490v-14M500 510v14"/>' +
122
+ cells + '<path class="visor__mark" d="M494 76l6 9l6 -9z"/>' +
123
+ '</svg>';
124
+ }
125
+ function keyHtml(k) { k = String(k); return '<kbd class="visor__key">' + esc(k.length === 1 ? k.toUpperCase() : k) + '</kbd>'; }
126
+ function actionAttrs(a) {
127
+ return a.href ? 'href="' + esc(hallHref(a.href)) + '"' : 'href="#" role="button"';
128
+ }
129
+ function glassHtml(id) {
130
+ var acts = actionsFor(id), main = acts[0], C = craft();
131
+ var sprite = C && typeof C.spriteSrc === 'function' ? C.spriteSrc(id) : '';
132
+ var n = credits();
133
+ return frameSvg() +
134
+ (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') +
135
+ '<a class="visor__pill visor__pill--main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
136
+ '<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>' +
137
+ '<button class="visor__pill visor__pill--actions" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
138
+ '<span class="visor__dots" aria-hidden="true"></span><span>Actions</span>' + keyHtml(SHEET_KEY) + '</button>' +
139
+ '<div class="visor__me">' +
140
+ (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="250" height="290" decoding="async">' : '') +
141
+ '<span class="visor__who"><span class="visor__name">' + esc(NAMES[id]) + '</span>' +
142
+ '<span class="visor__credits" data-visor-credits>' + esc(creditsText(n)) + '</span></span>' +
143
+ '</div>';
144
+ }
145
+ function sheetHtml(id) {
146
+ var rows = actionsFor(id).map(function (a) {
147
+ return '<li><a class="visor__row" data-visor-act="' + esc(a.id) + '" ' + actionAttrs(a) + '><span>' + esc(a.label) + '</span>' + keyHtml(a.key) + '</a></li>';
148
+ }).join('');
149
+ var place = scene.actions.map(function (a, i) {
150
+ return '<li><button class="visor__row" type="button" data-visor-scene="' + i + '"><span>' + esc(a.label) + '</span>' + (a.key ? keyHtml(a.key) : '') + '</button></li>';
151
+ }).concat(scene.keys.map(function (k) {
152
+ return '<li><span class="visor__row visor__row--hint"><span>' + esc(k.label) + '</span>' + keyHtml(k.key) + '</span></li>';
153
+ })).join('');
154
+ return '<div class="visor__sheet-head"><h2 class="visor__sheet-title" id="visor-sheet-title">Actions</h2>' +
155
+ '<button class="visor__close" type="button" data-visor-close aria-label="Close actions">' + keyHtml('Esc') + '</button></div>' +
156
+ '<p class="visor__cap">' + esc(NAMES[id]) + '</p><ul class="visor__rows">' + rows + '</ul>' +
157
+ (place ? '<p class="visor__cap">This place</p><ul class="visor__rows">' + place + '</ul>' : '') +
158
+ '<ul class="visor__rows"><li><a class="visor__row" href="' + esc(hallHref('/settings')) + '"><span>Options</span>' + keyHtml('Esc') + '</a></li></ul>';
159
+ }
160
+ function shown() { return !!(root && !root.hidden); }
161
+ function render() {
162
+ if (!document.body) return;
163
+ var id = scene && !embedded() ? character() : null;
164
+ var html = document.documentElement;
165
+ if (!id) { if (html.classList) html.classList.remove('visor-on'); if (root) { closeSheet(false); root.hidden = true; } return; }
166
+ // A scene's own layers read this to keep clear of the glass (thinking.css), in game mode only.
167
+ if (html.classList) html.classList.toggle('visor-on', html.dataset.game === 'on');
168
+ if (!root) {
169
+ root = document.createElement('div');
170
+ root.id = 'visor';
171
+ root.className = 'visor';
172
+ root.setAttribute('role', 'region');
173
+ root.setAttribute('aria-label', 'Visor');
174
+ root.addEventListener('click', onClick);
175
+ document.body.appendChild(root);
176
+ }
177
+ var open = !!sheet;
178
+ closeSheet(false);
179
+ root.hidden = false;
180
+ root.setAttribute('data-character', id);
181
+ root.innerHTML = glassHtml(id);
182
+ if (open) openSheet();
183
+ }
184
+ function embedded() {
185
+ var G = window.OTBShellGame;
186
+ return !!(G && typeof G.embedded === 'function' && G.embedded());
187
+ }
188
+
189
+ // ---------- the sheet ----------
190
+ function openSheet() {
191
+ var id = character();
192
+ if (!shown() || sheet || !id) return;
193
+ returnFocus = document.activeElement;
194
+ sheet = document.createElement('div');
195
+ sheet.id = 'visor-sheet';
196
+ sheet.className = 'visor__sheet';
197
+ sheet.setAttribute('role', 'dialog');
198
+ sheet.setAttribute('aria-labelledby', 'visor-sheet-title');
199
+ sheet.innerHTML = sheetHtml(id);
200
+ sheet.addEventListener('click', onClick);
201
+ root.appendChild(sheet);
202
+ var btn = root.querySelector('[data-visor-sheet]');
203
+ if (btn) { btn.setAttribute('aria-expanded', 'true'); btn.hidden = true; }
204
+ document.addEventListener('keydown', onSheetKey, true);
205
+ var first = sheet.querySelector('.visor__rows a, .visor__rows button');
206
+ if (first && first.focus) first.focus();
207
+ }
208
+ function closeSheet(refocus) {
209
+ if (!sheet) return;
210
+ document.removeEventListener('keydown', onSheetKey, true);
211
+ if (sheet.parentNode) sheet.parentNode.removeChild(sheet);
212
+ sheet = null;
213
+ var btn = root && root.querySelector('[data-visor-sheet]');
214
+ if (btn) { btn.setAttribute('aria-expanded', 'false'); btn.hidden = false; }
215
+ if (refocus !== false) {
216
+ var to = returnFocus && returnFocus.isConnected !== false && returnFocus !== document.body ? returnFocus : btn;
217
+ if (to && to.focus) { try { to.focus(); } catch (_) { /* gone */ } }
218
+ }
219
+ returnFocus = null;
220
+ }
221
+ // ADR 0399 §2.4 row 4: capture on document, and stop the press, so no scene listener
222
+ // below (the sky's unpick) and not the options menu also spends it.
223
+ function onSheetKey(e) {
224
+ if (!sheet || e.key !== 'Escape') return;
225
+ e.preventDefault();
226
+ e.stopPropagation();
227
+ closeSheet(true);
228
+ }
229
+
230
+ // ---------- running an action ----------
231
+ function run(a) {
232
+ if (!a) return;
233
+ if (a.tool) {
234
+ var C = craft();
235
+ if (C && typeof C.openTool === 'function') C.openTool(a.tool);
236
+ return;
237
+ }
238
+ if (a.href) window.location.assign(hallHref(a.href));
239
+ }
240
+ function find(id) {
241
+ var acts = actionsFor(character());
242
+ for (var i = 0; i < acts.length; i++) if (acts[i].id === id) return acts[i];
243
+ return null;
244
+ }
245
+ function onClick(e) {
246
+ var t = e.target && e.target.closest ? e.target : null;
247
+ if (!t) return;
248
+ if (t.closest('[data-visor-close]')) { closeSheet(true); return; }
249
+ if (t.closest('[data-visor-sheet]')) { openSheet(); return; }
250
+ var sc = t.closest('[data-visor-scene]');
251
+ if (sc && scene) { var a = scene.actions[Number(sc.getAttribute('data-visor-scene'))]; closeSheet(false); if (a) a.run(); return; }
252
+ var act = t.closest('[data-visor-act]');
253
+ if (!act) return;
254
+ var found = find(act.getAttribute('data-visor-act'));
255
+ if (found && found.tool) { e.preventDefault(); closeSheet(false); run(found); }
256
+ else closeSheet(false); // a link: the browser follows it
257
+ }
258
+
259
+ // ---------- the keys ----------
260
+ function inField(el) {
261
+ var tag = el && el.tagName ? String(el.tagName).toLowerCase() : '';
262
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || !!(el && el.isContentEditable);
263
+ }
264
+ // Something above the glass has the keyboard: the character picker, a modal dialog, the
265
+ // palette, the start screen. The visor's keys wait while any of them is up.
266
+ function busy() {
267
+ var html = document.documentElement;
268
+ if (html.classList && (html.classList.contains('craft-picker-open') || html.classList.contains('gs-open'))) return true;
269
+ return !!document.querySelector('dialog[open], [aria-modal="true"]');
270
+ }
271
+ // Which action (or the sheet) a key press means, or null. Pure, for the tests.
272
+ function keyFor(id, e) {
273
+ if (!e || e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey || typeof e.key !== 'string' || e.key.length !== 1) return null;
274
+ var k = e.key.toLowerCase();
275
+ if (k === SHEET_KEY) return 'sheet';
276
+ var acts = actionsFor(id);
277
+ for (var i = 0; i < acts.length; i++) if (acts[i].key === k) return acts[i];
278
+ return null;
279
+ }
280
+ // Bubble, on window: a scene or pop-up that used the key has called preventDefault by now.
281
+ function onKey(e) {
282
+ if (!shown() || inField(e.target) || busy()) return;
283
+ var hit = keyFor(character(), e);
284
+ if (!hit) return;
285
+ e.preventDefault();
286
+ if (hit === 'sheet') { if (sheet) closeSheet(true); else openSheet(); return; }
287
+ closeSheet(false);
288
+ run(hit);
289
+ }
290
+
291
+ function start() {
292
+ window.addEventListener('keydown', onKey);
293
+ // A scene whose script ran before this file left its registration here.
294
+ if (window.__visorScene && !scene) registerScene(window.__visorScene);
295
+ // The character and the credits settle when /me answers (craft-mode.js), after this file.
296
+ document.addEventListener('otbcraft:me', render);
297
+ document.addEventListener('otb:craft', render);
298
+ render();
299
+ }
300
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
301
+ else start();
302
+
303
+ window.OTBVisor = {
304
+ scene: registerScene,
305
+ render: render,
306
+ open: openSheet,
307
+ close: function () { closeSheet(true); },
308
+ isOpen: function () { return !!sheet; },
309
+ // pure parts, for the tests
310
+ ACTIONS: ACTIONS,
311
+ actionsFor: actionsFor,
312
+ keyFor: keyFor,
313
+ creditsText: creditsText,
314
+ character: character,
315
+ };
316
+ })();
@@ -0,0 +1,159 @@
1
+ /* watch-repo-invite.js — the GitHub repository half of an invite, for the Gate on /watch.
2
+ *
3
+ * A hall invite admits someone to THIS hall. It never puts the project's repository on
4
+ * their GitHub: that is a separate invite, sent with the owner's own GitHub account and
5
+ * accepted by the person on GitHub. This file holds the pieces of that second door so
6
+ * watch.js (at its line cap) only wires them:
7
+ * • formFieldsHtml() / explainerHtml() / readForm() — the "also invite to the
8
+ * repository" tick in the invite box, and the words under it
9
+ * • inviteToRepo() — POST /github/repo-invite; the form runs it AFTER the hall invite
10
+ * landed, so a GitHub refusal never undoes the admission
11
+ * • showRepoNote() — the form's result, kept where the owner can read it after the
12
+ * queue redraws
13
+ * • rowButtonHtml() / gateGithubStep() — one admitted row's GitHub status, in place:
14
+ * on the repository, waiting for them to accept, or not on it
15
+ *
16
+ * Access is always read-only (owner decision, 2026-11-04): a builder ships through the
17
+ * server's own publish and needs no write access, so there is no access choice at all.
18
+ * The server holds the same line (POST /github/repo-invite allows only 'pull').
19
+ *
20
+ * It is a factory over the page's own helpers, so it makes no request of its own that the
21
+ * page's sign-in bounce and error envelope do not already cover.
22
+ */
23
+ (function () {
24
+ 'use strict';
25
+
26
+ // A real GitHub username (the rule the server applies), so the repository button never
27
+ // shows for a Google-only member's stand-in name.
28
+ const GH_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
29
+
30
+ // The one word for this access level, so every sentence says it the same way.
31
+ const READ_ONLY = 'read-only';
32
+
33
+ function make({ api, API, postJson, $ }) {
34
+ function formFieldsHtml() {
35
+ return (
36
+ '<div class="ov-field">' +
37
+ `<label class="ov-field__l"><input type="checkbox" id="gate-invite-repo" checked> Also invite them to the project’s GitHub repository (${READ_ONLY} — they can see the code, not change it)</label>` +
38
+ '</div>'
39
+ );
40
+ }
41
+
42
+ // The sentences under the form: what each invite does, and where the result lands.
43
+ function explainerHtml() {
44
+ return (
45
+ '<p class="ov-form__why">The hall invite lets them sign in here. The GitHub invite is separate: GitHub emails them, and the repository only shows on their GitHub after they accept it (github.com/notifications). Neither changes their rank.</p>' +
46
+ '<p class="ov-form__why" id="gate-invite-note" role="status"></p>'
47
+ );
48
+ }
49
+
50
+ // Reads the form's tick: { want }. There is no access choice; it is always read-only.
51
+ function readForm(form) {
52
+ const tick = form.querySelector('#gate-invite-repo');
53
+ return { want: !!(tick && tick.checked) };
54
+ }
55
+
56
+ // Resolves { ok, text, signin } and never throws except for a signed-out 401, which
57
+ // postJson has already bounced to sign-in. The words say nothing about the hall
58
+ // invite: this runs from the form (after one) and from a row (with none).
59
+ async function inviteToRepo(login) {
60
+ try {
61
+ // No access level is sent: the server offers read-only and nothing else.
62
+ const out = await postJson('/github/repo-invite', { github_login: login });
63
+ if (out.state === 'already_collaborator') {
64
+ return { ok: true, text: `@${login} already has access to the GitHub repository.` };
65
+ }
66
+ return { ok: true, text: `GitHub repository invite sent to @${login} (${READ_ONLY}). They must accept it: GitHub emails them, or they can open github.com/notifications.` };
67
+ } catch (err) {
68
+ if (err && err.kind === '401') throw err;
69
+ return {
70
+ ok: false,
71
+ signin: !!(err && err.code === 'repo_scope_required'),
72
+ text: `The GitHub repository invite did not go through: ${(err && err.message) || err}`,
73
+ };
74
+ }
75
+ }
76
+
77
+ // Writes a result into an element. A sign-in link is added only when GitHub access is
78
+ // what is missing; it opens the same GitHub sign-in with the repository permission asked
79
+ // for. Used by the form's note and by a row's status line alike.
80
+ function paint(el, res) {
81
+ if (!el) return;
82
+ el.textContent = res.text;
83
+ if (res.signin) {
84
+ const a = document.createElement('a');
85
+ a.href = `${API}/auth/web/start?return=${encodeURIComponent('/watch')}&repos=1`;
86
+ a.textContent = ' Reconnect GitHub with repository access, then try again.';
87
+ el.appendChild(a);
88
+ }
89
+ }
90
+
91
+ // The form's result. When the repository step failed, the owner is told the hall
92
+ // invite still stands, because the form sent that one first.
93
+ function showRepoNote(res) {
94
+ paint($('#gate-invite-note'), res.ok ? res : { ...res, text: `The hall invite went through. ${res.text}` });
95
+ }
96
+
97
+ // The button an admitted row offers, and the span its answer is written into.
98
+ function rowButtonHtml(r, ghost) {
99
+ return GH_LOGIN_RE.test(String(r.github_login || ''))
100
+ ? ghost(`data-gate-gh data-gh-mode="check" data-login="${String(r.github_login).replace(/[^A-Za-z0-9-]/g, '')}"`, 'GitHub repository')
101
+ : '';
102
+ }
103
+ // The span a row's answer is written into: drawn only where the button is.
104
+ function rowStateHtml(r) {
105
+ return r.status === 'invited' && !r.expired && GH_LOGIN_RE.test(String(r.github_login || ''))
106
+ ? '<span class="ov-row__when" data-gh-state role="status"></span>'
107
+ : '';
108
+ }
109
+
110
+ // One row's GitHub status, in place; and a button to send (or send again) read-only access.
111
+ async function gateGithubStep(btn) {
112
+ const login = btn.getAttribute('data-login');
113
+ const mode = btn.getAttribute('data-gh-mode') || 'check';
114
+ const row = btn.closest('.ov-row');
115
+ const out = row && row.querySelector('[data-gh-state]');
116
+ const say = (t) => paint(out, { ok: true, text: t });
117
+ try {
118
+ if (mode === 'send') {
119
+ const sent = await inviteToRepo(login);
120
+ paint(out, sent);
121
+ if (sent.ok) { // a failed send stays a send, so the retry is one click
122
+ btn.textContent = 'Check GitHub again';
123
+ btn.setAttribute('data-gh-mode', 'check');
124
+ }
125
+ return;
126
+ }
127
+ const res = await api.request('GET', `${API}/github/repo-invite?github_login=${encodeURIComponent(login)}`);
128
+ if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw { kind: '401' }; }
129
+ const s = res.data || {};
130
+ if (!res.ok) {
131
+ const env = s.error && typeof s.error === 'object' ? s.error : null;
132
+ paint(out, { ok: false, signin: !!(env && env.code === 'repo_scope_required'), text: (env && env.message) || 'Could not read the GitHub repository. Try again in a moment.' });
133
+ return;
134
+ }
135
+ if (s.state === 'collaborator') {
136
+ say('On the GitHub repository.');
137
+ btn.textContent = 'Check GitHub again';
138
+ } else if (s.state === 'pending') {
139
+ say('GitHub invite sent, waiting for them to accept it.');
140
+ btn.textContent = 'Send the invite again';
141
+ btn.setAttribute('data-gh-mode', 'send');
142
+ } else {
143
+ say('Not on the GitHub repository.');
144
+ btn.textContent = `Send ${READ_ONLY} repository invite`;
145
+ btn.setAttribute('data-gh-mode', 'send');
146
+ }
147
+ } catch (err) {
148
+ if (err && err.kind === '401') throw err;
149
+ say((err && err.message) || 'Could not reach GitHub. Try again in a moment.');
150
+ } finally {
151
+ btn.disabled = false;
152
+ }
153
+ }
154
+
155
+ return { formFieldsHtml, explainerHtml, readForm, inviteToRepo, showRepoNote, rowButtonHtml, rowStateHtml, gateGithubStep };
156
+ }
157
+
158
+ window.OTBWatchRepoInvite = { make };
159
+ })();
@@ -11,7 +11,7 @@
11
11
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
14
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-10-options">
15
15
  <!-- The oversight family's sheet (task 1003469): the figure band, the group
16
16
  head, the row, the form. Then the page's own: severities, the report
17
17
  reader, the grade trend. The attention tiles + tabs come from style.css's
@@ -269,20 +269,21 @@
269
269
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
270
270
  chrome exists before the page script runs. -->
271
271
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
272
- <script src="/builders/shell-game.js?v=2026-11-09-game-mode"></script>
272
+ <script src="/builders/shell-game.js?v=2026-11-10-options"></script>
273
273
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
274
- <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
275
- <script src="/builders/shell.js?v=2026-11-09-game-mode"></script>
274
+ <script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
275
+ <script src="/builders/shell.js?v=2026-11-10-character"></script>
276
276
  <script src="/builders/hall-dialog.js"></script>
277
277
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
278
278
  <script src="/builders/palette.js" defer></script>
279
279
  <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
280
280
  <script src="/builders/hall-kit.js"></script>
281
- <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
281
+ <script src="/builders/task-detail.js?v=2026-11-10-options"></script>
282
282
  <script src="/builders/brand-apply.js?v=2026-06-19-r56"></script>
283
283
  <!-- task 1005049: the two tabs' scripts, booted by watch.js when their tab first opens. -->
284
284
  <script src="/builders/sessions.js?v=2026-11-05-tab"></script>
285
285
  <script src="/builders/gate.js?v=2026-11-05-tab"></script>
286
+ <script src="/builders/watch-repo-invite.js?v=2026-11-04-repoinvite"></script>
286
287
  <script src="/builders/watch.js?v=2026-11-05-tabs"></script>
287
288
  </body>
288
289
  </html>