@bongos/core 1.24.79 → 1.24.81

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 (413) hide show
  1. package/.bongos-core.json +554 -394
  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/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
  17. package/docs/adr/README.md +4 -0
  18. package/docs/api/openapi.json +195 -3
  19. package/docs/api-reference.md +8 -4
  20. package/docs/architecture.md +7 -3
  21. package/docs/copy-inventory.md +393 -378
  22. package/docs/copy-registry.json +722 -578
  23. package/docs/design/characters/character-sprites-source.webp +0 -0
  24. package/docs/file-map.md +7 -8
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +5 -0
  27. package/docs/onboarding/slash-commands.md +1 -1
  28. package/docs/packs/{engineer.md → artisan.md} +5 -5
  29. package/docs/packs/artist.md +50 -4
  30. package/docs/page-inventory.json +16 -7
  31. package/docs/page-readings.json +3184 -3338
  32. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  33. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  34. package/migrations/core_297_builder_character.sql +30 -0
  35. package/modules/copy-desk/proposals.js +1 -1
  36. package/modules/economy/credits-by-craft.js +29 -15
  37. package/modules/hall-ui/public/ai-studio.html +6 -4
  38. package/modules/hall-ui/public/ai-studio.states.json +1 -0
  39. package/modules/hall-ui/public/atlas.html +5 -5
  40. package/modules/hall-ui/public/atlas.js +3 -1
  41. package/modules/hall-ui/public/atlas.states.json +1 -0
  42. package/modules/hall-ui/public/blockers.html +5 -5
  43. package/modules/hall-ui/public/board-room.html +4 -4
  44. package/modules/hall-ui/public/board-room.states.json +1 -0
  45. package/modules/hall-ui/public/characters/artisan.png +0 -0
  46. package/modules/hall-ui/public/characters/artist.png +0 -0
  47. package/modules/hall-ui/public/characters/patron.png +0 -0
  48. package/modules/hall-ui/public/city.css +3 -4
  49. package/modules/hall-ui/public/city.html +9 -9
  50. package/modules/hall-ui/public/city.js +17 -56
  51. package/modules/hall-ui/public/city.states.json +7 -32
  52. package/modules/hall-ui/public/collab.html +4 -4
  53. package/modules/hall-ui/public/collab.states.json +1 -0
  54. package/modules/hall-ui/public/commands.html +4 -4
  55. package/modules/hall-ui/public/commands.states.json +1 -0
  56. package/modules/hall-ui/public/controls.css +63 -11
  57. package/modules/hall-ui/public/copy-desk.html +4 -4
  58. package/modules/hall-ui/public/copy-desk.states.json +1 -0
  59. package/modules/hall-ui/public/craft-mode.js +258 -64
  60. package/modules/hall-ui/public/deploy.html +4 -4
  61. package/modules/hall-ui/public/deploy.states.json +1 -0
  62. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/goal-map.html +9 -6
  65. package/modules/hall-ui/public/goals.html +5 -5
  66. package/modules/hall-ui/public/goals.states.json +1 -0
  67. package/modules/hall-ui/public/government.html +4 -4
  68. package/modules/hall-ui/public/government.states.json +1 -0
  69. package/modules/hall-ui/public/hall-render.js +3 -1
  70. package/modules/hall-ui/public/home-city-draw.js +3 -3
  71. package/modules/hall-ui/public/home-city.css +4 -0
  72. package/modules/hall-ui/public/home-city.js +17 -9
  73. package/modules/hall-ui/public/home-gallery.js +8 -1
  74. package/modules/hall-ui/public/home-idea-box.css +2 -2
  75. package/modules/hall-ui/public/home-idea-box.js +3 -3
  76. package/modules/hall-ui/public/idea.html +5 -5
  77. package/modules/hall-ui/public/idea.states.json +1 -0
  78. package/modules/hall-ui/public/ideas.html +5 -5
  79. package/modules/hall-ui/public/ideas.states.json +1 -0
  80. package/modules/hall-ui/public/index-founding.states.json +1 -0
  81. package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
  82. package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
  83. package/modules/hall-ui/public/index-genesis.states.json +1 -0
  84. package/modules/hall-ui/public/index.html +11 -7
  85. package/modules/hall-ui/public/index.states.json +1 -0
  86. package/modules/hall-ui/public/market-watch-item.html +4 -4
  87. package/modules/hall-ui/public/market-watch-item.states.json +1 -0
  88. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  89. package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
  90. package/modules/hall-ui/public/market-watch.html +6 -9
  91. package/modules/hall-ui/public/market-watch.js +22 -45
  92. package/modules/hall-ui/public/market-watch.states.json +1 -0
  93. package/modules/hall-ui/public/modules.html +4 -4
  94. package/modules/hall-ui/public/modules.states.json +1 -0
  95. package/modules/hall-ui/public/money.html +4 -4
  96. package/modules/hall-ui/public/money.states.json +1 -0
  97. package/modules/hall-ui/public/my-agents.html +4 -4
  98. package/modules/hall-ui/public/my-agents.states.json +1 -0
  99. package/modules/hall-ui/public/options-menu.js +409 -0
  100. package/modules/hall-ui/public/paintbrush.js +3 -3
  101. package/modules/hall-ui/public/palette.js +11 -13
  102. package/modules/hall-ui/public/primer.html +5 -5
  103. package/modules/hall-ui/public/primer.states.json +1 -0
  104. package/modules/hall-ui/public/profile-crafts.js +1 -1
  105. package/modules/hall-ui/public/profile-roles.js +23 -19
  106. package/modules/hall-ui/public/profile.css +1 -1
  107. package/modules/hall-ui/public/profile.html +19 -18
  108. package/modules/hall-ui/public/profile.js +19 -18
  109. package/modules/hall-ui/public/profile.states.json +5 -4
  110. package/modules/hall-ui/public/project-settings.html +4 -4
  111. package/modules/hall-ui/public/roster.html +4 -4
  112. package/modules/hall-ui/public/roster.js +3 -3
  113. package/modules/hall-ui/public/roster.states.json +1 -0
  114. package/modules/hall-ui/public/settings.css +24 -4
  115. package/modules/hall-ui/public/settings.html +21 -17
  116. package/modules/hall-ui/public/settings.js +44 -44
  117. package/modules/hall-ui/public/settings.states.json +1 -0
  118. package/modules/hall-ui/public/shell-game.css +110 -0
  119. package/modules/hall-ui/public/shell-game.js +316 -3
  120. package/modules/hall-ui/public/shell.js +15 -66
  121. package/modules/hall-ui/public/sky-interaction.js +7 -0
  122. package/modules/hall-ui/public/speciality-tools.js +1 -2
  123. package/modules/hall-ui/public/street-card.states.json +1 -0
  124. package/modules/hall-ui/public/street-inside.js +3 -4
  125. package/modules/hall-ui/public/street.css +1 -5
  126. package/modules/hall-ui/public/street.html +6 -10
  127. package/modules/hall-ui/public/street.js +8 -59
  128. package/modules/hall-ui/public/street.states.json +7 -32
  129. package/modules/hall-ui/public/studio.html +5 -5
  130. package/modules/hall-ui/public/studio.states.json +1 -0
  131. package/modules/hall-ui/public/style.css +197 -112
  132. package/modules/hall-ui/public/task-detail.js +5 -4
  133. package/modules/hall-ui/public/task.html +5 -5
  134. package/modules/hall-ui/public/task.states.json +1 -0
  135. package/modules/hall-ui/public/thinking.css +10 -0
  136. package/modules/hall-ui/public/thinking.html +9 -6
  137. package/modules/hall-ui/public/thinking.states.json +1 -0
  138. package/modules/hall-ui/public/tweak-editor.html +5 -5
  139. package/modules/hall-ui/public/tweak-editor.js +5 -1
  140. package/modules/hall-ui/public/tweak-editor.states.json +1 -0
  141. package/modules/hall-ui/public/update-pill.js +91 -2
  142. package/modules/hall-ui/public/visor.css +80 -0
  143. package/modules/hall-ui/public/visor.js +316 -0
  144. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  145. package/modules/hall-ui/public/watch.html +6 -5
  146. package/modules/hall-ui/public/watch.js +17 -2
  147. package/modules/hall-ui/public/watch.states.json +1 -0
  148. package/modules/hall-ui/public/welcome-boot.js +15 -6
  149. package/modules/hall-ui/public/work-founding.states.json +1 -0
  150. package/modules/hall-ui/public/work.html +5 -5
  151. package/modules/hall-ui/public/work.states.json +1 -0
  152. package/modules/hall-ui/records/builders-roster.md +2 -2
  153. package/modules/hall-ui/records/craft-mode.md +13 -11
  154. package/modules/hall-ui/records/game-mode.md +2 -2
  155. package/modules/hall-ui/records/governor-street.md +1 -1
  156. package/modules/hall-ui/records/home-city.md +3 -1
  157. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  158. package/modules/hall-ui/records/options-menu.md +15 -0
  159. package/modules/hall-ui/records/profile-roles.md +11 -11
  160. package/modules/hall-ui/records/sky.md +3 -3
  161. package/modules/hall-ui/records/start-screen.md +25 -0
  162. package/modules/hall-ui/records/tweak-editor.md +1 -1
  163. package/modules/hall-ui/records/update-pill.md +12 -0
  164. package/modules/hall-ui/records/visor.md +23 -0
  165. package/modules/hall-ui/records/welcome-tower.md +5 -5
  166. package/modules/hall-ui/specimens/components.states.json +2 -1
  167. package/modules/ideas/ideator-stats.js +3 -1
  168. package/modules/ideas/inbox.js +2 -2
  169. package/modules/lifecycle/activity.js +1 -1
  170. package/modules/lifecycle/cascade.js +4 -4
  171. package/modules/lifecycle/goal-spec-task.js +1 -1
  172. package/modules/lifecycle/help-requests.js +19 -15
  173. package/modules/lifecycle/kickoff-checklist.js +3 -3
  174. package/modules/lifecycle/kickoff-seed.js +1 -1
  175. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  176. package/modules/lifecycle/mingle.js +4 -4
  177. package/modules/lifecycle/page-tweak-write.js +1 -1
  178. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  179. package/modules/lifecycle/routes/tasks.js +3 -3
  180. package/modules/lifecycle/ship-card.js +2 -3
  181. package/modules/lifecycle/task-classifier.js +39 -29
  182. package/modules/lifecycle/tests/cascade.mjs +7 -7
  183. package/modules/onboarding/founding-mode.js +3 -0
  184. package/modules/onboarding/genesis-home.js +21 -1
  185. package/modules/onboarding/routes/onboarding.js +2 -0
  186. package/modules/page-notes/notes.js +3 -2
  187. package/modules/page-notes/split.js +1 -1
  188. package/modules/platform-identity/craft-rollup.js +35 -7
  189. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  190. package/modules/platform-identity/platform-identity.js +2 -2
  191. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  192. package/modules/platform-identity/routes/community-search.js +2 -1
  193. package/modules/platform-identity/routes/sso.js +2 -1
  194. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  195. package/modules/public-landing/public/community.html +3 -5
  196. package/modules/public-landing/public/index.html +1 -1
  197. package/modules/public-landing/public/projects.html +1 -1
  198. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  199. package/modules/specialities/routes/specialities.js +2 -2
  200. package/modules/status-ui/public/status.js +1 -1
  201. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  202. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  203. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  204. package/modules/ui-design/public/brand.html +2 -2
  205. package/package-lock.json +2 -2
  206. package/package.json +1 -1
  207. package/release-notes.json +76 -0
  208. package/scripts/bongos/autobongos-loop.js +1 -1
  209. package/scripts/bongos/discipline-modes.json +7 -12
  210. package/scripts/bongos/fitness.js +1 -1
  211. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  212. package/scripts/bongos/grade-smoke.js +2 -2
  213. package/scripts/bongos/init.js +2 -2
  214. package/scripts/bongos/newcomer-floor.config.json +1 -2
  215. package/scripts/bongos/newcomer-floor.js +4 -3
  216. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  217. package/scripts/bongos/newcomer-floor.test.js +47 -38
  218. package/scripts/bongos/role-pack-guard.js +5 -4
  219. package/scripts/bongos/seed-tasks.example.json +3 -3
  220. package/scripts/bongos/setup.js +20 -107
  221. package/scripts/bongos/start.js +2 -2
  222. package/scripts/bongos/task.js +5 -4
  223. package/scripts/bongos/triage.js +1 -1
  224. package/scripts/bongos/worktree.js +16 -4
  225. package/scripts/hall-preview/README.md +2 -3
  226. package/scripts/setup-builder.js +6 -46
  227. package/src/bongos/auth-admission.js +8 -8
  228. package/src/bongos/craft-mode.js +2 -0
  229. package/src/bongos/db.js +39 -1
  230. package/src/bongos/github-repos.js +78 -0
  231. package/src/bongos/module-scope-map.js +22 -0
  232. package/src/bongos/role-stats.js +27 -11
  233. package/src/bongos/routes/github.js +100 -2
  234. package/src/bongos/routes/me.js +33 -0
  235. package/src/bongos/serve-internal.js +33 -11
  236. package/src/crafts.js +66 -0
  237. package/src/hall-helm.js +79 -0
  238. package/src/module-api.js +6 -1
  239. package/src/module-loader/manifest-schema.js +16 -0
  240. package/src/modules.js +2 -2
  241. package/src/role-responsibilities.js +6 -5
  242. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  243. package/tests/activity_rollup_as_of.mjs +1 -1
  244. package/tests/api_alias_caller_ratchet.mjs +3 -3
  245. package/tests/application_lifecycle.mjs +1 -1
  246. package/tests/artist_review.mjs +1 -1
  247. package/tests/artist_review_goal.mjs +1 -1
  248. package/tests/blocker_hall_live_proof.mjs +1 -1
  249. package/tests/blocker_tasks_named_db.mjs +2 -2
  250. package/tests/canonical_profile_url.mjs +1 -1
  251. package/tests/cap_newcomer_tasks.mjs +13 -13
  252. package/tests/character_creation.mjs +294 -0
  253. package/tests/claim_gate_open_blocker.mjs +2 -2
  254. package/tests/claim_gate_rebase.mjs +6 -6
  255. package/tests/claude_materialize.mjs +1 -1
  256. package/tests/clawback_confirmed_backlog.mjs +3 -3
  257. package/tests/collab_page.mjs +8 -9
  258. package/tests/collab_self_settling.mjs +2 -2
  259. package/tests/community_leaderboard_route.mjs +13 -5
  260. package/tests/community_proof_db.mjs +3 -0
  261. package/tests/community_search_route.mjs +7 -6
  262. package/tests/conductor.mjs +1 -1
  263. package/tests/conductor_main_e2e.mjs +8 -8
  264. package/tests/copy_desk_flags_db.mjs +1 -1
  265. package/tests/copy_desk_page_approve.mjs +2 -2
  266. package/tests/copy_desk_page_claim.mjs +2 -2
  267. package/tests/copy_desk_page_next.mjs +1 -1
  268. package/tests/copy_desk_page_release.mjs +1 -1
  269. package/tests/copy_desk_page_submit.mjs +2 -2
  270. package/tests/core_258_box_session_claims_db.mjs +1 -1
  271. package/tests/craft_mode.mjs +48 -34
  272. package/tests/craft_mode_home.mjs +20 -20
  273. package/tests/craft_mode_looks.mjs +16 -15
  274. package/tests/craft_mode_nav.mjs +31 -29
  275. package/tests/craft_review_mode.mjs +46 -41
  276. package/tests/crafts_vocabulary.mjs +70 -0
  277. package/tests/cross_discipline_priority_direction.mjs +5 -5
  278. package/tests/design_reference.mjs +1 -1
  279. package/tests/fitness.mjs +7 -11
  280. package/tests/funded_project_pays_proof_db.mjs +1 -1
  281. package/tests/genesis_home.mjs +21 -1
  282. package/tests/github_chip.mjs +3 -3
  283. package/tests/github_repo_invite.mjs +379 -0
  284. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  285. package/tests/goal_invite_consent.mjs +1 -1
  286. package/tests/goal_map_page.mjs +2 -2
  287. package/tests/goal_sort_staleness.mjs +1 -1
  288. package/tests/goal_spec_task.mjs +1 -1
  289. package/tests/goal_spec_task_db.mjs +3 -3
  290. package/tests/grade_engine.mjs +2 -2
  291. package/tests/grader_no_artifact.mjs +1 -1
  292. package/tests/grader_prompt_fencing.mjs +1 -1
  293. package/tests/grader_prompt_window_bound.mjs +1 -1
  294. package/tests/grader_reader_lens.mjs +2 -2
  295. package/tests/grader_severity_gate.mjs +1 -1
  296. package/tests/grader_unbacked_score.mjs +1 -1
  297. package/tests/hall_approval_queue.mjs +1 -1
  298. package/tests/hall_board_world.mjs +2 -2
  299. package/tests/hall_builders_page_pass.mjs +3 -2
  300. package/tests/hall_city.mjs +16 -30
  301. package/tests/hall_controls.mjs +52 -4
  302. package/tests/hall_fullscreen.mjs +27 -3
  303. package/tests/hall_helm.mjs +187 -0
  304. package/tests/hall_kit_charts.mjs +1 -1
  305. package/tests/hall_landing_boot.mjs +1 -1
  306. package/tests/hall_my_agents.mjs +16 -16
  307. package/tests/hall_nav.mjs +22 -15
  308. package/tests/hall_options_menu.mjs +247 -0
  309. package/tests/hall_oversight_world.mjs +1 -1
  310. package/tests/hall_palette.mjs +8 -9
  311. package/tests/hall_profile_world.mjs +3 -3
  312. package/tests/hall_record_world.mjs +6 -4
  313. package/tests/hall_settings_world.mjs +2 -1
  314. package/tests/hall_sitemap.mjs +6 -5
  315. package/tests/hall_start_screen.mjs +307 -0
  316. package/tests/hall_street.mjs +15 -28
  317. package/tests/hall_street_inside.mjs +17 -9
  318. package/tests/hall_studio_world.mjs +3 -3
  319. package/tests/hall_topbar_320.mjs +4 -2
  320. package/tests/hall_visor.mjs +224 -0
  321. package/tests/help_requests.mjs +47 -15
  322. package/tests/helpers.mjs +4 -4
  323. package/tests/home_city.mjs +6 -4
  324. package/tests/home_city_widget.mjs +33 -14
  325. package/tests/home_gallery_widget.mjs +4 -4
  326. package/tests/home_idea_box.mjs +6 -6
  327. package/tests/hub_community_page.mjs +3 -3
  328. package/tests/hub_craft_rollup.mjs +75 -36
  329. package/tests/hub_recent_ships.mjs +3 -2
  330. package/tests/idea_cascade.mjs +2 -2
  331. package/tests/idea_credit_streams_db.mjs +1 -1
  332. package/tests/idea_detail_page.mjs +1 -1
  333. package/tests/idea_ideate_full.mjs +5 -2
  334. package/tests/idea_spark_hall.mjs +1 -1
  335. package/tests/init.mjs +3 -3
  336. package/tests/instance_manifest.mjs +2 -2
  337. package/tests/kickoff_checklist.mjs +1 -1
  338. package/tests/leaderboard_roles.mjs +34 -25
  339. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  340. package/tests/lightbulb.mjs +14 -10
  341. package/tests/main_discipline.mjs +8 -8
  342. package/tests/markdown_line_endings.mjs +1 -1
  343. package/tests/market_watch_pages.mjs +52 -74
  344. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  345. package/tests/migration_allocation.mjs +1 -1
  346. package/tests/mingle.mjs +36 -21
  347. package/tests/module-scope-map.mjs +22 -2
  348. package/tests/module_api.mjs +1 -0
  349. package/tests/module_contributions.mjs +13 -13
  350. package/tests/module_manifest.mjs +29 -0
  351. package/tests/module_skills_hook.mjs +2 -2
  352. package/tests/modules.mjs +14 -7
  353. package/tests/nav_permission_atoms.mjs +2 -1
  354. package/tests/needs_rebase.mjs +1 -1
  355. package/tests/newcomer_restock.mjs +13 -13
  356. package/tests/page_notes_panel.mjs +22 -22
  357. package/tests/page_tweak_no_block.mjs +1 -1
  358. package/tests/paintbrush.mjs +7 -7
  359. package/tests/privacy_matrix.mjs +1 -0
  360. package/tests/profile_activity.mjs +20 -23
  361. package/tests/profile_earnings.mjs +1 -1
  362. package/tests/profile_nudge_prefs.mjs +4 -4
  363. package/tests/profile_role_headline.mjs +73 -46
  364. package/tests/profile_role_stats.mjs +55 -45
  365. package/tests/profile_rollup_consent.mjs +19 -6
  366. package/tests/profile_route.mjs +12 -12
  367. package/tests/profile_ui_cross_project.mjs +4 -4
  368. package/tests/project_invite_suggestions.mjs +2 -2
  369. package/tests/promote_goal_id.mjs +1 -1
  370. package/tests/publish_manifest.mjs +1 -1
  371. package/tests/rank_floor_wiring.mjs +1 -1
  372. package/tests/regrade_actual_minutes.mjs +1 -1
  373. package/tests/removal_proposal.mjs +2 -2
  374. package/tests/reward_gate.mjs +4 -4
  375. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  376. package/tests/role_responsibilities.mjs +18 -12
  377. package/tests/role_stats_db.mjs +17 -16
  378. package/tests/scouting_rollup_fanout.mjs +5 -5
  379. package/tests/seed_tasks.mjs +5 -2
  380. package/tests/session_earnings_mirror_db.mjs +1 -1
  381. package/tests/session_reward_on_land_db.mjs +1 -1
  382. package/tests/session_reward_reconcile_db.mjs +2 -2
  383. package/tests/sessions_kit_pager.mjs +1 -1
  384. package/tests/settings_art_key_absent.mjs +2 -2
  385. package/tests/settings_main_role.mjs +21 -20
  386. package/tests/setup_flow.mjs +3 -3
  387. package/tests/setup_smoke.mjs +20 -0
  388. package/tests/ship_card.mjs +6 -6
  389. package/tests/specialities.mjs +16 -4
  390. package/tests/speciality_db_shape.mjs +6 -6
  391. package/tests/speciality_injection.mjs +17 -17
  392. package/tests/speciality_offer_ui.mjs +5 -5
  393. package/tests/speciality_offered.mjs +6 -5
  394. package/tests/speciality_session_skills.mjs +7 -7
  395. package/tests/speciality_skills.mjs +11 -11
  396. package/tests/speciality_walkthrough_ui.mjs +2 -2
  397. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  398. package/tests/task_classifier_vocab.mjs +10 -0
  399. package/tests/task_cli.mjs +11 -3
  400. package/tests/task_criteria_unlink_db.mjs +2 -2
  401. package/tests/task_discipline_required.mjs +38 -16
  402. package/tests/task_goal_required.mjs +4 -4
  403. package/tests/task_needs_db.mjs +1 -1
  404. package/tests/tweak_replaces_artist_review.mjs +1 -1
  405. package/tests/update_pill.mjs +153 -2
  406. package/tests/upkeep_home.mjs +1 -1
  407. package/tests/v3_c1_options_proof.mjs +145 -0
  408. package/tests/visual_review.mjs +3 -3
  409. package/tests/watch_applications_queue.mjs +8 -5
  410. package/tests/watch_sealed_floor.mjs +3 -1
  411. package/tests/welcome_boot.mjs +12 -9
  412. package/tests/welcome_boot_browser.mjs +13 -13
  413. package/tests/worktree_helper.mjs +16 -1
@@ -301,7 +301,8 @@ test('sidebar groups read by audience, in order', () => {
301
301
  // how your own Claude works for you, the personal half of "the city steers
302
302
  // everyone, My agents steers yours". Its subject is the viewer's own agents.
303
303
  // task 1005050: 'standing' (Accomplishments) left — your numbers are the one Profile page.
304
- assert.deepEqual(nav[3].items.map((i) => i.id), ['profile', 'my-agents'], 'You = your own numbers and your own agents');
304
+ // task 1005144 (ADR 0401): 'my-agents' LEFT for the one options menu (the profile chip).
305
+ assert.deepEqual(nav[3].items.map((i) => i.id), ['profile'], 'You = your own numbers');
305
306
  // task 1003444: 'people' LEFT the group — the owner deleted the hall's
306
307
  // cross-project directory (ADR 0337 D5); its home is the hub, and /people
307
308
  // redirects to /roster. Builders is the hall's directory now.
@@ -324,7 +325,7 @@ test('sidebar groups read by audience, in order', () => {
324
325
  // Economy and Diagrams explained what the Primer explains; they are its chapters
325
326
  // now, so Docs is the Primer and the four references that are not a document.
326
327
  assert.deepEqual(nav[6].items.map((i) => i.id),
327
- ['primer', 'commands', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
328
+ ['primer', 'commands', 'atlas', 'apidocs'], 'Docs = reference material (Modules is an options menu entry since task 1005144)');
328
329
  // 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
329
330
  // 'project-settings' joins the Government group at the metic floor (task 1003280,
330
331
  // goal 1000072) — it sits before 'government', which is the one archon-only item.
@@ -342,7 +343,12 @@ test('sidebar groups read by audience, in order', () => {
342
343
  // idea 1001310). The settings are an owner-only Settings tab on /market-watch now.
343
344
  // 'gate' (Autobongos) and 'sessions' LEFT the group in task 1005049 (owner decision
344
345
  // 2026-11-03, idea 1001315): they are tabs on Watch, and /gate + /sessions redirect there.
345
- assert.deepEqual(nav[5].items.map((i) => i.id), ['watch', 'project-settings', 'money', 'deploy', 'government']);
346
+ // task 1005144 (ADR 0401): 'project-settings' and 'government' (Permissions) LEFT for the
347
+ // one options menu; their URLs open it (serve-internal.js RETIRED_TO_OPTIONS).
348
+ assert.deepEqual(nav[5].items.map((i) => i.id), ['watch', 'money', 'deploy']);
349
+ for (const gone of ['/my-agents', '/project-settings', '/government', '/modules']) {
350
+ assert.ok(!nav.flatMap((g) => g.items).some((i) => i.href === gone), `no item points at ${gone}, an options menu entry now`);
351
+ }
346
352
  assert.ok(!nav.flatMap((g) => g.items).some((i) => i.href === '/gate' || i.href === '/sessions'), 'no item points at a page that became a Watch tab');
347
353
  // task 1005045: and no label is left twice anywhere in the sidebar, so the next pair
348
354
  // fails here instead of in front of the owner.
@@ -792,7 +798,7 @@ test('no glyph path data reads as an IP address to the release leak scan', () =>
792
798
  // block (task 1004713). These drive the real shell and the real gov-positions.js.
793
799
 
794
800
  const tick = () => new Promise((resolve) => setImmediate(resolve));
795
- const GOVERNING = ['city', 'board-room', 'watch', 'project-settings', 'money', 'deploy', 'government'];
801
+ const GOVERNING = ['city', 'board-room', 'watch', 'money', 'deploy'];
796
802
  const lead = (building, title) => ({ kind: 'lead', seat: 'lead:' + building, building, title });
797
803
  const block = (held) => ({ held, title: held.length ? held[0].title : null, has_position: held.length > 0 });
798
804
 
@@ -821,7 +827,7 @@ test('POSITION: a Lead keeps every governing surface, and the seat names it', as
821
827
  const b = await seated({ builder: { id: '424242', rank: 'thetes' }, positions: block([lead('planning', 'Planning Lead')]) });
822
828
  assert.equal(b.shell.positions().title, 'Planning Lead');
823
829
  // a thetes Lead: the seat opens the governing tabs their rank and permissions allow
824
- for (const id of ['board-room', 'city', 'my-agents']) assert.equal(b.navHidden(id), false, `${id} shows`);
830
+ for (const id of ['board-room', 'city', 'profile']) assert.equal(b.navHidden(id), false, `${id} shows`);
825
831
  });
826
832
 
827
833
  test('POSITION: the fixture viewer holds what the fixture /me says', async () => {
@@ -833,10 +839,10 @@ test('POSITION: the fixture viewer holds what the fixture /me says', async () =>
833
839
 
834
840
  test('POSITION: a builder with no seat sees no governing surface, and only those go', async () => {
835
841
  // an archon the server says holds no seat: rank, craft and permissions do not make one
836
- const b = await seated({ builder: { id: '424242', rank: 'archon', preferred_disciplines: ['engineer'] }, positions: block([]) });
842
+ const b = await seated({ builder: { id: '424242', rank: 'archon', preferred_disciplines: ['artisan'] }, positions: block([]) });
837
843
  assert.equal(b.shell.positions().has_position, false);
838
- for (const id of ['board-room', 'city', 'watch', 'project-settings', 'government']) assert.equal(b.navHidden(id), true, `${id} hides`);
839
- for (const id of ['home', 'work', 'goals', 'my-agents', 'profile', 'primer']) assert.equal(b.navHidden(id), false, `${id} still shows`);
844
+ for (const id of ['board-room', 'city', 'watch', 'money']) assert.equal(b.navHidden(id), true, `${id} hides`);
845
+ for (const id of ['home', 'work', 'goals', 'profile', 'primer']) assert.equal(b.navHidden(id), false, `${id} still shows`);
840
846
  });
841
847
 
842
848
  test('POSITION: unknown is today\'s nav — a /me with no positions block hides nothing', async () => {
@@ -859,18 +865,19 @@ test('POSITION: ?as=nobody views the hall with no seat, and can only hide', asyn
859
865
  const b = await seated({ builder: me.builder, positions: me.positions, search: '?as=nobody' });
860
866
  assert.equal(b.shell.positions().as, 'nobody');
861
867
  for (const id of GOVERNING) assert.equal(b.navHidden(id), true, `${id} hides for the stand-in`);
862
- assert.equal(b.navHidden('my-agents'), false);
868
+ assert.equal(b.navHidden('profile'), false);
863
869
  // a xenos stand-in stays a xenos: the pretend rank never rises above the real one
864
870
  const x = await seated({ builder: { id: '9', rank: 'xenos' }, positions: block([lead('planning', 'Planning Lead')]), search: '?as=nobody' });
865
871
  assert.equal(x.navHidden('goal-map'), true, 'a member page stays hidden from a real xenos');
866
872
  });
867
873
 
868
- test('My agents draws its own glyph and lands on a real page', () => {
869
- const item = parseNav(boot().sidebarHtml).flatMap((g) => g.items).find((i) => i.id === 'my-agents');
870
- assert.ok(item, 'the item renders');
871
- assert.equal(item.href, '/my-agents');
872
- assert.equal(item.gate, 'signed');
873
- assert.ok(hrefResolves(item), 'my-agents.html exists');
874
+ // task 1005144 (ADR 0401): My agents, Project settings, Permissions and Modules are entries
875
+ // in the one options menu, reached by the profile chip, which now says so.
876
+ test('the settings pages left the sidebar for the options menu, and the profile chip names it', () => {
877
+ const html = boot().sidebarHtml;
878
+ const ids = parseNav(html).flatMap((g) => g.items).map((i) => i.id);
879
+ for (const gone of ['my-agents', 'project-settings', 'government', 'modules']) assert.ok(!ids.includes(gone), `${gone} is not a nav item`);
880
+ assert.ok(html.includes('<a class="shell-user" id="shell-user" href="/settings" title="Options" aria-label="Options"'), 'the chip opens the menu and says so');
874
881
  });
875
882
 
876
883
  // task 1005050 (owner decision 2026-11-03, idea 1001315): Accomplishments
@@ -0,0 +1,247 @@
1
+ // tests/hall_options_menu.mjs — the ONE options menu (task 1005144, BONGOS-V3 R04,
2
+ // goal 1000141, criterion game-opens-not-navigates; ADR 0401).
3
+ //
4
+ // Every setting and configuration is reached from one vertical, grouped menu with a
5
+ // search box at the top: the Settings page. The screens that were pages of their own
6
+ // (My agents, Project settings, Modules, Permissions, Market Watch's settings and rules)
7
+ // open INSIDE it, in a frame asking for the old address with ?embed=1, and a plain visit
8
+ // to an old address is sent to the menu with that screen open. These run the real
9
+ // options-menu.js (its pure half), the real shell-game.js in a DOM stub, and read the
10
+ // server's redirect map, so a menu entry, its old URL and its frame cannot drift apart.
11
+ import { test } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import vm from 'node:vm';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { createRequire } from 'node:module';
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
21
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
22
+ const Menu = createRequire(import.meta.url)(path.join(HALL, 'options-menu.js'));
23
+ const SERVE = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8');
24
+
25
+ const ids = (menu) => menu.flatMap((g) => g.items.map((i) => i.id));
26
+ const signed = (rank) => ({ authed: true, rank });
27
+
28
+ test('the menu holds every settings surface: Settings\' own groups and the six screens that were pages', () => {
29
+ const all = Menu.entries();
30
+ const embeds = all.filter((e) => e.embed).map((e) => e.embed).sort();
31
+ assert.deepEqual(embeds, ['/government', '/market-watch', '/market-watch-rules', '/modules', '/my-agents', '/project-settings']);
32
+ for (const e of all.filter((x) => x.embed)) {
33
+ assert.ok(fs.existsSync(path.join(HALL, e.embed.slice(1) + '.html')), `${e.embed}.html exists for the frame`);
34
+ }
35
+ // every native entry is a real group on the Settings page, and every group has an entry
36
+ const html = read('settings.html');
37
+ const groups = new Set([...html.matchAll(/data-group="([a-z-]+)"/g)].map((m) => m[1]));
38
+ const natives = all.filter((e) => e.native).map((e) => e.id);
39
+ for (const id of natives) assert.ok(groups.has(id), `#${id} is a Settings group`);
40
+ for (const g of groups) assert.ok(natives.includes(g), `the ${g} group is reachable from the menu`);
41
+ // ids are unique, so one address opens one thing
42
+ assert.equal(new Set(all.map((e) => e.id)).size, all.length);
43
+ });
44
+
45
+ test('the proof\'s three settings are all in the one menu: a sound, a CLI token and a module toggle', () => {
46
+ const menu = Menu.visibleMenu(signed('archon'));
47
+ const hit = (q) => Menu.search(q, menu)[0];
48
+ assert.equal(hit('sound').id, 'sound');
49
+ assert.deepEqual([hit('cli token').id, hit('cli token').anchor], ['access', 'cli']);
50
+ assert.equal(hit('module toggle').id, 'modules');
51
+ });
52
+
53
+ test('who sees what: the same rank keys the sidebar used, and module and contribution gates', () => {
54
+ const xenos = ids(Menu.visibleMenu(signed('xenos')));
55
+ assert.ok(xenos.includes('account') && xenos.includes('my-agents') && xenos.includes('access') && xenos.includes('market-watch-rules'));
56
+ for (const id of ['modules', 'project-settings', 'permissions', 'market-watch']) assert.ok(!xenos.includes(id), `a xenos does not see ${id}`);
57
+ const thetes = ids(Menu.visibleMenu(signed('thetes')));
58
+ assert.ok(thetes.includes('modules') && !thetes.includes('project-settings'), 'Modules from member, as its sidebar item was');
59
+ const metic = ids(Menu.visibleMenu(signed('metic')));
60
+ assert.ok(metic.includes('project-settings') && metic.includes('permissions') && !metic.includes('market-watch'));
61
+ const archon = ids(Menu.visibleMenu(signed('archon')));
62
+ assert.ok(archon.includes('market-watch'), 'Market Watch settings: the owner, as its tab was');
63
+ assert.deepEqual(ids(Menu.visibleMenu({ authed: false, rank: 'archon' })), [], 'signed out: nothing');
64
+ const off = ids(Menu.visibleMenu(signed('archon'), { moduleOn: (k) => k !== 'market-watch' }));
65
+ assert.ok(!off.includes('market-watch') && !off.includes('market-watch-rules'), 'a module that is off has no entries');
66
+ assert.ok(!archon.includes('extensions'), 'Extensions only when a module contributed a panel');
67
+ assert.ok(ids(Menu.visibleMenu(signed('archon'), { contributed: true })).includes('extensions'));
68
+ // a group with nothing in it is not drawn
69
+ for (const g of Menu.visibleMenu(signed('xenos'))) assert.ok(g.items.length > 0, `${g.head} has entries`);
70
+ });
71
+
72
+ test('the search: every word must match, label hits first, and a sub-place says where it is', () => {
73
+ const menu = Menu.visibleMenu(signed('archon'));
74
+ assert.deepEqual(Menu.search('', menu), []);
75
+ assert.deepEqual(Menu.search('zzzz', menu), []);
76
+ const talk = Menu.search('talks', menu)[0];
77
+ assert.deepEqual([talk.id, talk.anchor, talk.where], ['my-agents', 'talk', 'Your agents']);
78
+ assert.equal(Menu.search('PERMISSIONS', menu)[0].id, 'permissions', 'case does not matter');
79
+ // Enter opens the first line, so the first line must be the place meant
80
+ assert.equal(Menu.search('token', menu)[0].label, 'CLI access', 'the exact place above the group that holds it');
81
+ assert.equal(Menu.search('access', menu)[0].label, 'Access and keys', 'but a label hit on the group still leads');
82
+ assert.equal(Menu.search('danger zone', menu)[0].id, 'project-settings', 'a word only the entry\'s words carry still finds it');
83
+ // a viewer never finds what they cannot open
84
+ assert.ok(!Menu.search('permissions', Menu.visibleMenu(signed('xenos'))).some((h) => h.id === 'permissions'));
85
+ });
86
+
87
+ test('what an address opens: ?open=<entry> with its #anchor, a bare #<entry>, and nothing else', () => {
88
+ assert.deepEqual(Menu.target('?open=my-agents', '#talk'), { id: 'my-agents', anchor: 'talk' });
89
+ assert.deepEqual(Menu.target('?open=market-watch', ''), { id: 'market-watch', anchor: 'settings' }, 'an entry\'s own anchor');
90
+ assert.deepEqual(Menu.target('', '#sound'), { id: 'sound', anchor: null });
91
+ assert.equal(Menu.target('', '#cli'), null, 'a panel anchor is the page\'s own to land');
92
+ assert.equal(Menu.target('?open=nope', ''), null);
93
+ assert.equal(Menu.frameSrc(Menu.entry('my-agents'), 'talk', ''), '/my-agents?embed=1#talk');
94
+ assert.equal(Menu.frameSrc(Menu.entry('permissions'), null, '/builders'), '/builders/government?embed=1');
95
+ assert.equal(Menu.frameSrc(Menu.entry('sound'), null, ''), null, 'a native group has no frame');
96
+ });
97
+
98
+ test('closing goes back only to a same-site path', () => {
99
+ assert.equal(Menu.closeTo('?from=' + encodeURIComponent('/goals#/goal/7')), '/goals#/goal/7');
100
+ for (const bad of ['//evil.example/x', 'https://evil.example', '/\\evil', '']) {
101
+ assert.equal(Menu.closeTo('?from=' + encodeURIComponent(bad)), null, `${bad || 'nothing'} is not a way back`);
102
+ }
103
+ });
104
+
105
+ test('the server sends every old address to its entry, lets the frame through, and the two maps agree', () => {
106
+ const m = SERVE.match(/const RETIRED_TO_OPTIONS = Object\.freeze\((\{[\s\S]*?\})\);/);
107
+ assert.ok(m, 'the server map is a literal');
108
+ const server = vm.runInNewContext('(' + m[1] + ')');
109
+ const client = Object.fromEntries(Object.entries(Menu.RETIRED).map(([p, id]) => [p.slice(1), id]));
110
+ assert.deepEqual({ ...server }, client, 'serve-internal.js RETIRED_TO_OPTIONS and options-menu.js RETIRED are one map');
111
+ for (const id of Object.values(client)) assert.ok(Menu.entry(id), `${id} is a menu entry`);
112
+ const reDecl = 'const RETIRED_OPTIONS_RE = ';
113
+ const i = SERVE.indexOf(reDecl);
114
+ const rx = vm.runInNewContext(SERVE.slice(i + reDecl.length, SERVE.indexOf(';', i)));
115
+ for (const p of Object.keys(server)) {
116
+ for (const v of [`/builders/${p}`, `/builders/${p}/`, `/builders/${p}.html`]) assert.ok(rx.test(v), `${v} is redirected`);
117
+ }
118
+ for (const p of ['/builders/settings', '/builders/market-watch', '/builders/modulesx', '/modules/x/public/a.js']) assert.ok(!rx.test(p), `${p} is not`);
119
+ const block = SERVE.slice(i, SERVE.indexOf('});', i));
120
+ assert.match(block, /if \(embed === '1' && m\[1\] !== 'market-watch-settings'\) return next\(\);/, 'the frame passes through to the page and its own gate');
121
+ assert.match(block, /\/settings\?open=\$\{RETIRED_TO_OPTIONS\[m\[1\]\]\}/);
122
+ // it runs BEFORE the page gates, which still guard the framed request
123
+ assert.ok(i < SERVE.indexOf('PROJECT_SETTINGS_PAGE_RE.test(norm)'), 'ahead of the page-gate dispatch');
124
+ assert.match(SERVE, /GOVERNMENT_PAGE_RE\.test\(norm\)\) \{\s*return gdsAuth\.requireGovernmentPage/, 'the Permissions frame keeps its page gate');
125
+ });
126
+
127
+ test('the Settings page IS the menu: one search over the groups, loaded before settings.js', () => {
128
+ const html = read('settings.html');
129
+ assert.match(html, /<nav class="opt-rail" id="settings-rail" aria-label="Options"><\/nav>/);
130
+ assert.ok(html.indexOf('options-menu.js') > 0 && html.indexOf('options-menu.js') < html.indexOf('/builders/settings.js'));
131
+ assert.match(html, /<title>Options — /);
132
+ const js = read('settings.js');
133
+ assert.match(js, /menu\.mount\(\{/, 'settings.js hands the menu the viewer');
134
+ assert.doesNotMatch(js, /makeSectionRail/, 'the old rail is gone, not drawn beside the menu');
135
+ const css = read('settings.css');
136
+ assert.match(css, /\.settings-groups > \.opt-embed\.opt-embed--on \{ display: block; \}/, 'one framed screen shows at a time');
137
+ });
138
+
139
+ // ---- shell-game.js: the embedded page and game mode's Escape --------------------
140
+
141
+ function bootGame({ search = '', framed = true, game = false, path: p = '/my-agents' } = {}) {
142
+ const docListeners = {};
143
+ const winListeners = {};
144
+ const timers = [];
145
+ const assigned = [];
146
+ const root = { dataset: {} };
147
+ const bodyAttrs = {};
148
+ const parent = { OTBOptions: { escapes: 0, escape() { this.escapes += 1; } } };
149
+ const win = {
150
+ location: { pathname: p, search, hash: '', origin: 'https://h', href: 'https://h' + p + search, assign(u) { assigned.push(u); } },
151
+ localStorage: { getItem: (k) => (k === 'hall-game' && game ? 'on' : null), setItem() {} },
152
+ sessionStorage: { getItem: () => null, setItem() {} },
153
+ addEventListener(t, fn) { (winListeners[t] ||= []).push(fn); },
154
+ setTimeout(fn) { timers.push(fn); },
155
+ };
156
+ win.parent = framed ? parent : win;
157
+ const document = {
158
+ documentElement: root,
159
+ body: { setAttribute(k, v) { bodyAttrs[k] = v; } },
160
+ addEventListener(t, fn) { (docListeners[t] ||= []).push(fn); },
161
+ };
162
+ const ctx = vm.createContext({ window: win, document, URLSearchParams, URL });
163
+ vm.runInContext(read('shell-game.js'), ctx);
164
+ return {
165
+ root, bodyAttrs, parent, assigned, game: win.OTBShellGame,
166
+ press(key, target = { tagName: 'DIV' }) {
167
+ const ev = { key, target, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
168
+ for (const fn of winListeners.keydown || []) fn(ev);
169
+ while (timers.length) timers.shift()();
170
+ return ev;
171
+ },
172
+ click(href, target = null) {
173
+ const attrs = { href };
174
+ if (target) attrs.target = target;
175
+ const a = { getAttribute: (k) => (k in attrs ? attrs[k] : null), setAttribute: (k, v) => { attrs[k] = v; } };
176
+ for (const fn of docListeners.click || []) fn({ target: { closest: () => a } });
177
+ return attrs.target || null;
178
+ },
179
+ };
180
+ }
181
+
182
+ test('EMBEDDED: only a framed ?embed=1 page drops its chrome, and the switch is off', () => {
183
+ const framed = bootGame({ search: '?embed=1' });
184
+ assert.equal(framed.root.dataset.embed, 'on');
185
+ assert.equal(framed.bodyAttrs['data-fullscreen-switch'], 'off', 'no corner button, no Escape-to-leave');
186
+ assert.equal(framed.game.embedded(), true);
187
+ const alone = bootGame({ search: '?embed=1', framed: false });
188
+ assert.equal(alone.root.dataset.embed, undefined, 'the same URL opened on its own keeps its chrome');
189
+ assert.equal(bootGame({ search: '' }).root.dataset.embed, undefined);
190
+ const css = read('style.css');
191
+ assert.match(css, /html\[data-embed="on"\] \.app-sidebar,\s*html\[data-embed="on"\] \.app-scrim,\s*html\[data-embed="on"\] \.app-foot,\s*html\[data-embed="on"\] \.app-topbar \{ display: none; \}/);
192
+ });
193
+
194
+ test('EMBEDDED: an Escape nobody used goes to the menu; a link to another page leaves the frame', () => {
195
+ const p = bootGame({ search: '?embed=1' });
196
+ p.press('Escape');
197
+ assert.equal(p.parent.OTBOptions.escapes, 1, 'the menu got it');
198
+ p.press('Escape', { tagName: 'INPUT' });
199
+ assert.equal(p.parent.OTBOptions.escapes, 1, 'a field keeps its own Escape');
200
+ assert.equal(p.click('/task/42'), '_top', 'another page opens in the whole window');
201
+ assert.equal(p.click('#talk'), null, 'a place in the same page stays in the frame');
202
+ assert.equal(p.click('/goals', '_blank'), '_blank', 'a link that names its own target keeps it');
203
+ });
204
+
205
+ test('GAME MODE: on the menu itself the last Escape closes it; outside game mode it is not the menu\'s', () => {
206
+ // the menu page registers window.OTBOptions; shell-game hands it the press
207
+ const menu = bootGame({ game: true, framed: false, path: '/settings' });
208
+ menu.parent.OTBOptions.escapes = 0;
209
+ const ev = menu.press('Escape');
210
+ assert.equal(ev.defaultPrevented, true, 'marked used, so the shell does not also bring the bars back');
211
+ assert.deepEqual(menu.assigned, ['/settings?from=%2Fsettings'], 'with no menu script loaded it opens the menu page');
212
+ const off = bootGame({ game: false, framed: false, path: '/goals' });
213
+ off.press('Escape');
214
+ assert.deepEqual(off.assigned, [], 'outside game mode the press stays the shell\'s');
215
+ });
216
+
217
+ test('options-menu.js gives the menu its Escape: close in game mode, nothing outside it', () => {
218
+ const src = read('options-menu.js');
219
+ assert.match(src, /function escape\(\) \{\s*var game = window\.OTBShellGame;\s*if \(game && game\.on && game\.on\(\)\) close\(\);/);
220
+ assert.match(src, /root\.OTBOptions = \{/, 'the menu registers itself where shell-game.js looks');
221
+ });
222
+
223
+ // In game mode an Escape nobody used opens the options menu, so a pop-up that closes on
224
+ // Escape must say it used the press (ADR 0399 §2.4 row 5). These seven closed something and
225
+ // let the press fall through: before task 1005144 that also brought the bars back (the city
226
+ // card was task 1005188); after it, it would also have left the page for the menu.
227
+ test('every pop-up that closes on Escape spends the press, so the menu never opens from under it', () => {
228
+ const spent = {
229
+ 'atlas.js': /ev\.key === 'Escape' && popup\.classList\.contains\('show'\)\) \{ ev\.preventDefault\(\); closePopup\(\);/,
230
+ 'city.js': /if \(e\.key === 'Escape'\) \{ if \(e\.preventDefault\) e\.preventDefault\(\); closeCard\(\);/,
231
+ 'home-gallery.js': /if \(isFs\(\) \|\| reading\.length\) e\.preventDefault\(\);/,
232
+ 'task-detail.js': /e\.key === 'Escape' && document\.getElementById\('deeplink-card'\)\) \{ e\.preventDefault\(\); clearLink\(\);/,
233
+ 'tweak-editor.js': /\$\('tw-openwrap'\)\.classList\.contains\('is-open'\)\) ev\.preventDefault\(\);/,
234
+ 'update-pill.js': /state\.card && !state\.card\.hidden\) \{ e\.preventDefault\(\); closeCard\(\);/,
235
+ 'diagram-viewer.js': /&& fullscreen\) \{ e\.preventDefault\(\); toggleFullscreen\(fullscreen\);/,
236
+ };
237
+ for (const [f, re] of Object.entries(spent)) assert.match(read(f), re, `${f} spends the Escape it used`);
238
+ // and a framed screen never asks for a craft: the menu page around it does
239
+ assert.match(read('craft-mode.js'), /if \(window\.OTBShellGame && window\.OTBShellGame\.embedded && window\.OTBShellGame\.embedded\(\)\) return;/);
240
+ });
241
+
242
+ test('the start screen\'s Options is the menu, and its first tip says what Escape does now', () => {
243
+ const src = read('shell-game.js');
244
+ assert.match(src, /\{ id: 'options', label: 'Options', href: '\/settings' \}/, 'Options opens the one options menu');
245
+ assert.match(src, /'Escape opens the options menu, where every setting lives\.'/);
246
+ assert.doesNotMatch(src, /Escape brings the navigation back/, 'no tip promises what Escape no longer does');
247
+ });
@@ -206,7 +206,7 @@ async function bootWatchLedger(overrides = {}) {
206
206
  };
207
207
  sandbox.globalThis = sandbox;
208
208
  vm.createContext(sandbox);
209
- for (const f of ['dom-utils.js', 'hall-kit.js', 'watch.js']) {
209
+ for (const f of ['dom-utils.js', 'hall-kit.js', 'watch-repo-invite.js', 'watch.js']) {
210
210
  vm.runInContext(read(f), sandbox, { filename: f });
211
211
  }
212
212
  // load() is async: let every section's promise settle before reading the DOM.
@@ -156,11 +156,10 @@ test('Government section anchors exist in government.js makeTabs', () => {
156
156
  const ids = [...read('government.js').matchAll(/\{ id: '([a-z-]+)', label: '[^']*', panel:/g)].map((m) => m[1]);
157
157
  assert.ok(ids.length === 2, `sanity: found the two government tabs (${ids})`);
158
158
  assert.ok(!ids.includes('board-room'), 'the Board Room is a page, not a tab (ADR 0266)');
159
- const declared = declaredHrefs((x) => x.startsWith('/government#'));
160
- assert.ok(declared.length === 2, 'palette declares the two Government sections');
161
- for (const h of declared) {
162
- assert.ok(ids.includes(h.split('#')[1]), `${h} must be a real government.js tab id`);
163
- }
159
+ // task 1005144: Permissions and the Constitution are an options menu entry, rank-gated
160
+ // in the menu, so the palette declares no /government row — one offered to every
161
+ // signed-in reader would land a sub-Metic one on a menu without that entry.
162
+ assert.equal(declaredHrefs((x) => x.startsWith('/government')).length, 0, 'no palette row points at the old page');
164
163
  });
165
164
 
166
165
  test('Settings panel anchors exist in settings.js RAIL_GROUPS', () => {
@@ -182,8 +181,8 @@ test('My agents anchors are ones my-agents.js lands, and the moved Settings pane
182
181
  const block = (src.match(/const ANCHORS = \{([\s\S]*?)\n \};/) || [, ''])[1];
183
182
  const keys = [...block.matchAll(/^\s*([a-z-]+): \{/gm)].map((m) => m[1]);
184
183
  assert.ok(keys.length >= 5, `sanity: my-agents.js ANCHORS (${keys})`);
185
- const declared = declaredHrefs((x) => x.startsWith('/my-agents#'));
186
- assert.ok(declared.length >= 5, 'the palette offers the agent settings where they live now');
184
+ const declared = declaredHrefs((x) => x.startsWith('/settings?open=my-agents#'));
185
+ assert.ok(declared.length >= 5, 'the palette offers the agent settings where they live now (the options menu, task 1005144)');
187
186
  for (const h of declared) assert.ok(keys.includes(h.split('#')[1]), `${h} must be a my-agents.js anchor`);
188
187
  assert.equal(declaredHrefs((x) => /\/settings#(interaction|wandering|rendering|voices|specialities)/.test(x)).length, 0,
189
188
  'no palette row points at a Settings panel that left');
@@ -207,10 +206,10 @@ test('pages come from the sidebar, and a gated-off nav item is not a destination
207
206
  test('section groups appear only when their owning nav item is visible', () => {
208
207
  const all = bootPalette({ navItems: NAV_FIXTURE }).compute('');
209
208
  assert.ok(all.some((r) => r.group === 'Profile'), 'Profile sections offered when nav-profile shows');
210
- assert.ok(all.some((r) => r.group === 'Settings'), 'Settings panels offered when the profile chip shows');
209
+ assert.ok(all.some((r) => r.group === 'Options'), 'Options (the one menu, task 1005144) offered when the profile chip shows');
211
210
 
212
211
  const gatedOut = bootPalette({ navItems: NAV_FIXTURE, visibleIds: [] }).compute('');
213
- assert.ok(!gatedOut.some((r) => ['Profile', 'Tasks', 'Settings'].includes(r.group)),
212
+ assert.ok(!gatedOut.some((r) => ['Profile', 'Tasks', 'Options'].includes(r.group)),
214
213
  'no sub-destinations when their owners are hidden');
215
214
  });
216
215
 
@@ -164,7 +164,7 @@ function bootCharts() {
164
164
  // the same reason categoryChip is: an undefined helper throws mid-render.
165
165
  roleAxisLabel: () => 'Role',
166
166
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
167
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
167
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
168
168
  roleIsUnset: (d) => !d || d === 'unclassified',
169
169
  ROLE_UNSET: 'unclassified',
170
170
  escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
@@ -308,7 +308,7 @@ function bootRender({ rows, status = 200, analytics = null, earnings = [] }) {
308
308
  // the same reason categoryChip is: an undefined helper throws mid-render.
309
309
  roleAxisLabel: () => 'Role',
310
310
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
311
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
311
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
312
312
  roleIsUnset: (d) => !d || d === 'unclassified',
313
313
  ROLE_UNSET: 'unclassified',
314
314
  escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
@@ -328,7 +328,7 @@ function bootRender({ rows, status = 200, analytics = null, earnings = [] }) {
328
328
  return { C: sandbox.window.OTBProfileCharts, api, requests, mount, controls };
329
329
  }
330
330
 
331
- const ship = (d, extra = {}) => ({ shipped_at: `${d}T10:00:00Z`, discipline: 'engineer', credits_awarded: 10, credits_reward: 12, ...extra });
331
+ const ship = (d, extra = {}) => ({ shipped_at: `${d}T10:00:00Z`, discipline: 'artisan', credits_awarded: 10, credits_reward: 12, ...extra });
332
332
  // an earnings row: what the ledger paid on one day, for one kind of credit
333
333
  const pay = (d, credits, kind = 'session') => ({ day: d, kind, credits });
334
334
 
@@ -152,14 +152,16 @@ test('.btn and .btn--ghost are defined, and say what .btn-accent / .btn-ghost sa
152
152
  assert.match(btn, /background:\s*var\(--accent\)/, '.btn is the accent door');
153
153
  assert.match(btn, /color:\s*var\(--accent-on\)/, '--accent-on is the dark ink for the salmon');
154
154
  const shape = ruleHolding(controlsCss, '.btn', 'border-radius');
155
- assert.match(shape, /border-radius:\s*var\(--radius-pill\)/, 'pill or nothing (DESIGN.md, Shapes)');
155
+ // Helm (task 1005192, ADR 0402): the hall's corner is the cut; the pill is the fallback where the hall's tier is not loaded
156
+ assert.match(shape, /border-radius:\s*var\(--press-radius, var\(--radius-pill\)\)/, 'the cut corner in the hall, the pill elsewhere (DESIGN.md, The hall)');
157
+ assert.match(shape, /clip-path:\s*var\(--cut-path, none\)/, 'anything you can press takes the cut corner');
156
158
  // collab.css's .btn--quiet sets only a border-COLOUR; a modifier cannot paint
157
159
  // an edge that was never reserved, so the base has to carry a transparent one.
158
160
  assert.match(shape, /border:\s*var\(--line\)\s+solid\s+transparent/,
159
161
  'the edge is reserved so a modifier that sets only border-color still draws');
160
162
  const ghost = ruleHolding(controlsCss, '.btn--ghost', 'background');
161
163
  assert.ok(ghost, '.btn--ghost has a rule');
162
- assert.match(ghost, /background:\s*transparent/, 'the quiet sibling is a hairline pill, not a fill');
164
+ assert.match(ghost, /background:\s*var\(--press-fill, transparent\)/, 'the quiet sibling is the press fill inside a hairline, never the accent');
163
165
  assert.equal(ghost, ruleHolding(controlsCss, '.btn-ghost', 'background'), 'and .btn-ghost says the same, in the same rule');
164
166
  assert.match(ghost, /border-color:\s*var\(--rule\)/, 'and it takes the reserved edge');
165
167
  });
@@ -395,7 +397,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
395
397
  // the same reason categoryChip is: an undefined helper throws mid-render.
396
398
  roleAxisLabel: () => 'Role',
397
399
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
398
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
400
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
399
401
  roleIsUnset: (d) => !d || d === 'unclassified',
400
402
  ROLE_UNSET: 'unclassified',
401
403
  // task 1004038 — the kind axis asks the same question. Stubbed for the
@@ -446,7 +448,7 @@ const settle = () => new Promise((r) => setTimeout(r, 0));
446
448
 
447
449
  const BASE_TASK = {
448
450
  id: '1003305', title: 'A record in the world', status: 'shipped', version_id: 'BONGOS-V1',
449
- kind: 'feature', discipline: 'engineer', priority: 1, requires_rank: 'metic',
451
+ kind: 'feature', discipline: 'artisan', priority: 1, requires_rank: 'metic',
450
452
  credits_reward: 60, est_minutes: 240, actual_minutes: 37,
451
453
  created_at: '2026-08-28T00:00:00Z', description: 'the description', touches: ['a.js', 'b.js'],
452
454
  dependencies: [], dependents: [],
@@ -222,7 +222,8 @@ test('every old link to those sections lands on My agents', () => {
222
222
  // settings.js asks before it draws anything, and on every later hash
223
223
  const html = read('settings.html');
224
224
  assert.ok(html.indexOf('settings-moved.js') > 0 && html.indexOf('settings-moved.js') < html.indexOf('/builders/settings.js'), 'loaded before settings.js');
225
- assert.match(settingsJs, /location\.replace\(/, 'a replace, so Back does not bounce through Settings');
225
+ // task 1005144: My agents is an options menu entry now, so a moved link opens it in place.
226
+ assert.match(settingsJs, /OTBOptions\.activate\('my-agents', anchor\)/, 'opened in the menu, no page load');
226
227
  assert.doesNotMatch(settingsJs, /'(interaction|wandering|rendering|voices)': '[a-z-]+-scroll'/, 'no PANEL_ANCHORS entry for a section that left');
227
228
  });
228
229
 
@@ -21,9 +21,10 @@ const model = gen.build();
21
21
  test('the parse found the hall, so nothing below passes against an empty map', () => {
22
22
  const files = fs.readdirSync(path.join(ROOT, 'modules/hall-ui/public')).filter((f) => f.endsWith('.html'));
23
23
  assert.equal(model.pages.length, files.length, 'one row per page file');
24
- // 24 since task 1005051 folded the Roadmap into Goals.
25
- assert.ok(model.nav.length >= 24, `expected the shell's nav, found ${model.nav.length} items`);
26
- for (const c of ['building', 'artistry', 'ideating', 'governing']) {
24
+ // 20 since task 1005144 moved four settings pages into the options menu.
25
+ assert.ok(model.nav.length >= 20, `expected the shell's nav, found ${model.nav.length} items`);
26
+ // task 1005141: three crafts — Artisan, Artist (Artistry + Ideating), Patron
27
+ for (const c of ['artisan', 'artist', 'patron']) {
27
28
  assert.ok(model.crafts.tabs[c].length > model.crafts.always.length, `${c} lists tabs of its own`);
28
29
  }
29
30
  assert.ok(model.chrome.includes('shell.js') && model.chrome.includes('craft-mode.js'), 'the chrome is the shell');
@@ -87,10 +88,10 @@ test('the Primer links to Repo Atlas and Commands, and in the hall they open tho
87
88
  assert.equal(gen.classify('/no-such-page', urls), false);
88
89
  });
89
90
 
90
- test('the street is Governing\'s home: no nav item, reached by clicking from the city', () => {
91
+ test('the street is Patron\'s home: no nav item, reached by clicking from the city', () => {
91
92
  const street = model.pages.find((p) => p.url === '/street');
92
93
  assert.ok(street, 'the street is a hall page');
93
94
  assert.deepEqual(street.nav, []);
94
- assert.ok(street.reachIn.includes('governing'));
95
+ assert.ok(street.reachIn.includes('patron'));
95
96
  assert.ok(street.in.includes('/city'), 'the city links to the street');
96
97
  });