@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
@@ -129,7 +129,7 @@ function boot({
129
129
  permissions = { 'core.pin.move': true }, update = BEHIND, held = null, status = 200, coreFails = false,
130
130
  sw = SW({ kind: 'local', url: '/deploy' }), preview = { instance_id: '41', recommended_version: '1.19.871' },
131
131
  post = { ok: true, status: 200, data: { ok: true } }, run = null, version = () => '1.19.861',
132
- pathname = '/goals', hash = '', openResult = 'popup',
132
+ pathname = '/goals', hash = '', openResult = 'popup', dialog = null, later = null, handoff = false,
133
133
  } = {}) {
134
134
  const ids = new Map();
135
135
  const inserted = [];
@@ -138,6 +138,7 @@ function boot({
138
138
  const posted = [];
139
139
  const store = new Map();
140
140
  if (run) store.set('bongos.update.run', JSON.stringify(run));
141
+ if (later) store.set('bongos.update.later', JSON.stringify(later));
141
142
  let pillLoads = 0;
142
143
  const listeners = {};
143
144
 
@@ -181,7 +182,7 @@ function boot({
181
182
  if (tag.onload) tag.onload();
182
183
  },
183
184
  },
184
- documentElement: { dataset: {}, style: {}, setAttribute() {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
185
+ documentElement: { dataset: handoff ? { handoff: 'on' } : {}, style: {}, setAttribute() {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
185
186
  getElementById(id) { if (!ids.has(id)) ids.set(id, el('div')); return ids.get(id); },
186
187
  querySelector(sel) { return sel === '#app-topbar .topbar-side' ? side : null; },
187
188
  querySelectorAll() { return []; },
@@ -214,6 +215,7 @@ function boot({
214
215
  matchMedia: () => ({ matches: false, addEventListener() {} }),
215
216
  localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) },
216
217
  requestAnimationFrame() {}, scrollTo() {}, location, innerWidth: 1280,
218
+ ...(dialog ? { OTBDialog: dialog } : {}),
217
219
  ...timers,
218
220
  };
219
221
  windowObj.document = documentObj;
@@ -433,3 +435,152 @@ test('a run whose version this page already runs has landed: nothing to say', as
433
435
  assert.equal(b.pill(), undefined);
434
436
  assert.equal(b.store.has('bongos.update.run'), false);
435
437
  });
438
+
439
+ // ── the pop-up (task 1005191) ─────────────────────────────────────────────────
440
+ // The owner, 2026-11-10: the pill alone was not seen (game mode opens every page without its
441
+ // bars), so an update that is ready also asks, once, in the hall's own dialog.
442
+
443
+ // A stand-in for OTBDialog.open that keeps what it was handed and lets a case press a button
444
+ // or dismiss it. The real one is run by tests/hall_dialog.mjs.
445
+ function fakeDialog() {
446
+ const opens = [];
447
+ return {
448
+ opens,
449
+ open(opts) {
450
+ let resolve;
451
+ const result = new Promise((r) => { resolve = r; });
452
+ const on = {};
453
+ const d = { opts, el: { addEventListener(ev, fn) { (on[ev] ||= []).push(fn); } }, result, close: (v) => resolve(v === undefined ? null : v) };
454
+ d.press = (i) => {
455
+ const btn = { getAttribute: (k) => (k === 'data-dialog-value' ? String(i) : null) };
456
+ for (const fn of on.click || []) fn({ target: { closest: () => btn } });
457
+ resolve(opts.actions[i].value);
458
+ };
459
+ opens.push(d);
460
+ return d;
461
+ },
462
+ };
463
+ }
464
+
465
+ test('the pop-up is due for a ready update, and not for a version said "later" to inside a day', () => {
466
+ const now = 10 * P.LATER_MS;
467
+ assert.equal(P.askDue({ face: 'ready', offered: '1.19.871', later: null, now }), true);
468
+ assert.equal(P.askDue({ face: 'ready', offered: '1.19.871', later: { v: '1.19.871', at: now - 1000 }, now }), false);
469
+ assert.equal(P.askDue({ face: 'ready', offered: '1.19.871', later: { v: '1.19.871', at: now - P.LATER_MS }, now }), true, 'a day later it asks again');
470
+ assert.equal(P.askDue({ face: 'ready', offered: '1.19.872', later: { v: '1.19.871', at: now - 1000 }, now }), true, 'a newer version asks at once');
471
+ assert.equal(P.askDue({ face: 'ready', offered: '1.19.871', later: { v: '1.19.871', at: now + 5000 }, now }), true, 'a clock that went backwards does not silence it');
472
+ for (const face of ['held', 'running', 'done', 'none']) assert.equal(P.askDue({ face, offered: '1.19.871', later: null, now }), false, face);
473
+ assert.equal(P.askDue({ face: 'ready', offered: 'next', later: null, now }), false);
474
+ const box = new Map();
475
+ const storage = { getItem: (k) => (box.has(k) ? box.get(k) : null), setItem: (k, v) => box.set(k, v) };
476
+ P.keepLater(storage, '1.19.871', 5);
477
+ assert.deepEqual(P.readLater(storage), { v: '1.19.871', at: 5 });
478
+ box.set(P.LATER_KEY, '{"v":"soon","at":1}');
479
+ assert.equal(P.readLater(storage), null);
480
+ assert.doesNotThrow(() => P.keepLater({ setItem() { throw new Error('private'); } }, '1.0.0', 1));
481
+ });
482
+
483
+ test('an owner who is behind is asked, unprompted, in the hall dialog: what it brings, both versions, two buttons', async () => {
484
+ const dialog = fakeDialog();
485
+ const b = boot({ dialog });
486
+ await settle();
487
+ assert.equal(dialog.opens.length, 1, 'asked once, without a press');
488
+ const { opts } = dialog.opens[0];
489
+ assert.equal(opts.title, 'Update the builders hall?');
490
+ assert.deepEqual(plain(opts.actions.map((a) => a.label)), ['Later', 'Update now']);
491
+ const html = opts.body.innerHTML;
492
+ assert.match(html, /latest changes and bug fixes/);
493
+ assert.match(html, /Faster boards, and fewer clicks/);
494
+ assert.match(html, /1\.19\.861<\/span> → <span class="upd-ver">1\.19\.871/);
495
+ assert.ok(b.pill(), 'the pill is still there');
496
+ assert.equal(b.requests.filter((r) => r.path === '/api/bongos/software-update').length, 1);
497
+ });
498
+
499
+ test('Later is remembered for that version, and the next page does not ask again', async () => {
500
+ const dialog = fakeDialog();
501
+ const b = boot({ dialog });
502
+ await settle();
503
+ dialog.opens[0].press(0);
504
+ await settle();
505
+ const kept = JSON.parse(b.store.get('bongos.update.later'));
506
+ assert.equal(kept.v, '1.19.871');
507
+ assert.equal(b.requests.some((r) => /core-upgrade$/.test(r.path)), false, 'Later moves nothing');
508
+ const again = fakeDialog();
509
+ const next = boot({ dialog: again, later: kept });
510
+ await settle();
511
+ assert.equal(again.opens.length, 0);
512
+ assert.ok(next.pill(), 'the pill still offers it');
513
+ assert.equal(next.requests.some((r) => r.path === '/api/bongos/software-update'), false, 'and nothing is read for a pop-up that will not open');
514
+ });
515
+
516
+ test('closing the pop-up any other way is "later" too', async () => {
517
+ const dialog = fakeDialog();
518
+ const b = boot({ dialog });
519
+ await settle();
520
+ dialog.opens[0].close();
521
+ await settle();
522
+ assert.equal(JSON.parse(b.store.get('bongos.update.later')).v, '1.19.871');
523
+ });
524
+
525
+ test('Update now in the pop-up starts the same update as the card, with the version it showed', async () => {
526
+ const dialog = fakeDialog();
527
+ const b = boot({ dialog });
528
+ await settle();
529
+ dialog.opens[0].press(1);
530
+ await settle(5);
531
+ const post = b.requests.find((r) => /core-upgrade$/.test(r.path));
532
+ assert.ok(post, 'the platform door was called');
533
+ assert.deepEqual(plain(post.body), { to: '1.19.871' });
534
+ assert.equal(b.store.has('bongos.update.later'), false, 'an update taken is not snoozed');
535
+ assert.match(b.card().innerHTML, /Updating Bongos/, 'and the card opens to say where it has got to');
536
+ });
537
+
538
+ test('a hosted project: Update now in the pop-up opens the hub window inside the press', async () => {
539
+ const dialog = fakeDialog();
540
+ const b = boot({ dialog, sw: SW({ kind: 'hub', url: HUB }) });
541
+ await settle();
542
+ assert.equal(b.opened.length, 0);
543
+ dialog.opens[0].press(1);
544
+ assert.equal(b.opened.length, 1, 'opened synchronously, so the browser allows it');
545
+ assert.match(b.opened[0].url, /handoff=1$/);
546
+ });
547
+
548
+ test('who is not asked: a viewer the door would refuse, the handoff window, the door page, a hall with no dialog', async () => {
549
+ const refused = fakeDialog();
550
+ boot({ dialog: refused, sw: SW({ kind: 'local', url: '/deploy' }, { can_update: false }) });
551
+ const handoff = fakeDialog();
552
+ boot({ dialog: handoff, handoff: true });
553
+ const door = fakeDialog();
554
+ boot({ dialog: door, pathname: '/deploy' });
555
+ const b = boot();
556
+ await settle();
557
+ assert.equal(refused.opens.length, 0);
558
+ assert.equal(handoff.opens.length, 0);
559
+ assert.equal(door.opens.length, 0);
560
+ assert.ok(b.pill(), 'no dialog script: the pill alone, as before');
561
+ });
562
+
563
+ test('a self-hosted project is told the command, with one button and no Update now', async () => {
564
+ const dialog = fakeDialog();
565
+ boot({ dialog, sw: SW({ kind: 'cli' }) });
566
+ await settle();
567
+ assert.equal(dialog.opens.length, 1);
568
+ assert.deepEqual(plain(dialog.opens[0].opts.actions.map((a) => a.label)), ['Close']);
569
+ assert.match(dialog.opens[0].opts.body.innerHTML, /bongos upgrade/);
570
+ });
571
+
572
+ test('a held version, or an update already running, is not asked about', async () => {
573
+ const held = fakeDialog();
574
+ boot({ dialog: held, held: { version: '1.19.871', message: 'rolled back' } });
575
+ const running = fakeDialog();
576
+ boot({ dialog: running, run: { to: '1.19.871', from: '1.19.861', at: Date.now(), via: 'local' } });
577
+ await settle();
578
+ assert.equal(held.opens.length, 0);
579
+ assert.equal(running.opens.length, 0);
580
+ });
581
+
582
+ test('with the bars away the pill stays on screen and can be pressed', () => {
583
+ const css = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/style.css'), 'utf8');
584
+ assert.match(css, /html\[data-fullscreen="on"\] \.topbar-side > :not\(\.fullscreen-toggle\):not\(\.upd-pill\) \{ display: none; \}/);
585
+ assert.match(css, /html\[data-fullscreen="on"\] \.upd-pill \{\s*pointer-events: auto;/);
586
+ });
@@ -102,7 +102,7 @@ function loadRoute({ versionStatus = 'building', maintenance = { id: '900', vers
102
102
  return { call, created, ensured };
103
103
  }
104
104
 
105
- const UPGRADE = { version_id: 'V2', title: 'Upgrade the Bongos core to 1.21.70', description: 'Platform upkeep.', discipline: 'engineer' };
105
+ const UPGRADE = { version_id: 'V2', title: 'Upgrade the Bongos core to 1.21.70', description: 'Platform upkeep.', discipline: 'artisan' };
106
106
 
107
107
  test('an upgrade filed with kind infra and NO goal lands in the maintenance goal', async () => {
108
108
  const { call, created, ensured } = loadRoute();
@@ -0,0 +1,145 @@
1
+ // tests/v3_c1_options_proof.mjs — the proof for BONGOS-V3 criterion C1
2
+ // `game-opens-not-navigates` (task 1005145, R05; docs/specs/bongos-v3-game-conversion.md;
3
+ // ADR 0401).
4
+ //
5
+ // The criterion's proof: "a builder signs in, never sees a sidebar, and changes a sound
6
+ // preference, a CLI token and a module toggle without leaving the options menu. A build
7
+ // that kept any old settings page fails this."
8
+ //
9
+ // The walk itself was done by hand in the hall preview on 2026-11-11 (recorded in task
10
+ // 1005145's ship notes). This file pins the parts of that walk a later change could
11
+ // quietly undo, each against the REAL code that does it:
12
+ // 1. The old settings pages are gone: the real server (serve-internal.js, mounted on a
13
+ // throwaway express app and hit over loopback) answers every old address with a 302
14
+ // into the menu, and only the menu's own frame (?embed=1) still reaches the old file.
15
+ // 2. No sidebar: the start screen's Start turns game mode on, and every hall page loads
16
+ // the game-mode file and none opts out of the full-screen switch.
17
+ // 3. The three settings are inside the one menu, each a control on the menu's own screen
18
+ // or in its frame: the CLI token's button, the module toggle, and the Sound entry.
19
+ // Sounds are switched off (task 1004554), so Sound shows its note, not a toggle;
20
+ // ADR 0401 accepted that for this proof until sounds come back.
21
+ //
22
+ // Run: node --test tests/v3_c1_options_proof.mjs
23
+
24
+ import { test } from 'node:test';
25
+ import assert from 'node:assert/strict';
26
+ import { createRequire } from 'node:module';
27
+ import fs from 'node:fs';
28
+ import http from 'node:http';
29
+ import path from 'node:path';
30
+ import { fileURLToPath } from 'node:url';
31
+
32
+ const require = createRequire(import.meta.url);
33
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
35
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
36
+ const Menu = require(path.join(HALL, 'options-menu.js'));
37
+
38
+ // The six screens that were pages of their own, and the menu entry each now opens.
39
+ const OLD_TO_ENTRY = {
40
+ 'my-agents': 'my-agents',
41
+ 'project-settings': 'project-settings',
42
+ modules: 'modules',
43
+ government: 'permissions',
44
+ 'market-watch-rules': 'market-watch-rules',
45
+ 'market-watch-settings': 'market-watch',
46
+ };
47
+
48
+ // GET over loopback as the hall's own host (the tests/hall_asset_cache.mjs pattern).
49
+ function get(base, urlPath) {
50
+ const { port } = new URL(base);
51
+ return new Promise((resolve, reject) => {
52
+ const req = http.request(
53
+ { host: '127.0.0.1', port, path: urlPath, method: 'GET', headers: { host: 'builders.example.test' } },
54
+ (res) => { res.resume(); res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || '' })); },
55
+ );
56
+ req.on('error', reject);
57
+ req.end();
58
+ });
59
+ }
60
+
61
+ async function withServer(fn) {
62
+ const express = require('express');
63
+ const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
64
+ const app = express();
65
+ mountInternalSurfaces(app);
66
+ const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
67
+ try { return await fn(`http://127.0.0.1:${server.address().port}`); }
68
+ finally { await new Promise((resolve) => server.close(resolve)); }
69
+ }
70
+
71
+ test('PROOF 1 — no old settings page survives: every old address is sent into the menu by the real server', async () => {
72
+ await withServer(async (base) => {
73
+ for (const [old, entry] of Object.entries(OLD_TO_ENTRY)) {
74
+ for (const p of [`/${old}`, `/${old}.html`]) {
75
+ const r = await get(base, p);
76
+ assert.equal(r.status, 302, `${p} must redirect, got ${r.status}`);
77
+ assert.equal(r.location, `/settings?open=${entry}`, `${p} must open the "${entry}" entry of the menu`);
78
+ }
79
+ // the menu's own frame still reaches the screen — except Market Watch settings, which has no page left
80
+ const framed = await get(base, `/${old}?embed=1`);
81
+ if (old === 'market-watch-settings') {
82
+ assert.equal(framed.location, `/settings?open=${entry}`, 'Market Watch settings has no page to frame');
83
+ } else {
84
+ assert.ok(!framed.location.startsWith('/settings?open='), `${old}?embed=1 is the menu's frame and must not bounce back to the menu`);
85
+ }
86
+ }
87
+ });
88
+ });
89
+
90
+ test('PROOF 1 — every one of those entries is a line in the menu an Archon sees, opening that same screen in a frame', () => {
91
+ const visible = Menu.visibleMenu({ authed: true, rank: 'archon' }, { moduleOn: () => true })
92
+ .flatMap((g) => g.items);
93
+ for (const [old, entry] of Object.entries(OLD_TO_ENTRY)) {
94
+ const line = visible.find((i) => i.id === entry);
95
+ assert.ok(line, `the menu lists "${entry}" (was /${old})`);
96
+ if (old !== 'market-watch-settings') assert.equal(line.embed, `/${old}`, `"${entry}" frames /${old}`);
97
+ }
98
+ });
99
+
100
+ test('PROOF 2 — no sidebar: Start turns game mode on, and every hall page is in it with no way to opt out', () => {
101
+ const game = read('shell-game.js');
102
+ assert.match(game, /set\(true\)/, 'the start screen\'s Start turns game mode on');
103
+ const pages = fs.readdirSync(HALL).filter((f) => f.endsWith('.html'));
104
+ const shelled = pages.filter((f) => read(f).includes('id="app-sidebar"') || read(f).includes('id="app-topbar"'));
105
+ assert.ok(shelled.length >= 25, `expected the hall's shelled pages, saw ${shelled.length}`);
106
+ for (const f of shelled) {
107
+ const html = read(f);
108
+ assert.ok(html.includes('shell-game.js'), `${f} must load shell-game.js, or it never enters game mode and keeps its sidebar`);
109
+ assert.ok(!/data-fullscreen-switch="off"/.test(html), `${f} opts out of the full-screen switch, so it would keep its sidebar in game mode`);
110
+ }
111
+ });
112
+
113
+ test('PROOF 3 — the CLI token: its button is on the menu\'s own Access and keys screen', () => {
114
+ const menu = Menu.visibleMenu({ authed: true, rank: 'xenos' });
115
+ const [hit] = Menu.search('cli token', menu);
116
+ assert.deepEqual([hit.id, hit.anchor], ['access', 'cli'], 'searching "cli token" lands on Access and keys, CLI access');
117
+ const access = Menu.entries().find((e) => e.id === 'access');
118
+ assert.equal(access.native, true, 'Access and keys is one of the menu\'s own screens, not a frame of another page');
119
+ const html = read('settings.html');
120
+ const section = html.slice(html.indexOf('id="cli-scroll"'), html.indexOf('</section>', html.indexOf('id="cli-scroll"')));
121
+ assert.match(section, /data-group="access"/, 'the CLI section belongs to the Access and keys group');
122
+ assert.match(section, /id="cli-reissue-btn"/, 'the re-issue button is in it');
123
+ });
124
+
125
+ test('PROOF 3 — the module toggle: the Modules entry frames /modules, whose catalogue draws an enable/disable toggle', () => {
126
+ const thetes = Menu.visibleMenu({ authed: true, rank: 'thetes' });
127
+ assert.equal(Menu.search('module toggle', thetes)[0].id, 'modules');
128
+ assert.equal(Menu.entries().find((e) => e.id === 'modules').embed, '/modules');
129
+ const js = read('modules.js');
130
+ assert.match(js, /class="ov-toggle mod-toggle" aria-pressed=/, 'the catalogue draws a pressable toggle per module');
131
+ });
132
+
133
+ test('PROOF 3 — sound: the Sound entry is in the menu, and while sounds are off it says so (ADR 0401)', () => {
134
+ const menu = Menu.visibleMenu({ authed: true, rank: 'xenos' });
135
+ assert.equal(Menu.search('sound', menu)[0].id, 'sound');
136
+ assert.equal(Menu.entries().find((e) => e.id === 'sound').native, true);
137
+ const html = read('settings.html');
138
+ for (const id of ['sounds-off-scroll', 'sounds-scroll']) {
139
+ const at = html.indexOf(`id="${id}"`);
140
+ assert.ok(at > 0, `#${id} is on the Settings page`);
141
+ assert.match(html.slice(at, at + 200), /data-group="sound"/, `#${id} is in the Sound group`);
142
+ }
143
+ // The toggles come back in the same place: when task 1004554 is undone, #sounds-scroll is
144
+ // what shows, and it is already inside the menu's Sound entry.
145
+ });
@@ -64,7 +64,7 @@ await test('the CLI writes the page and prints its path; a missing image is an e
64
64
 
65
65
  // ---- the claim-time nudge ----------------------------------------------------
66
66
 
67
- const task = (over = {}) => ({ id: '1', discipline: 'engineer', module_key: null, source: 'manual', touches: [], description: '', ...over });
67
+ const task = (over = {}) => ({ id: '1', discipline: 'artisan', module_key: null, source: 'manual', touches: [], description: '', ...over });
68
68
 
69
69
  await test('a task that names a page file in its description is told to use /design and get an OK', () => {
70
70
  const t = task({ description: 'Where: modules/hall-ui/public/goal-forest-lib.js — NODE_W = 168. Drawing is modules/hall-ui/public/goal-forest-render.js.' });
@@ -102,12 +102,12 @@ await test('claim.js prints the nudge after the craft role line, and cannot fail
102
102
  // ---- the rule, where every builder's session reads it ------------------------
103
103
 
104
104
  await test('all three playbooks carry the rule and the way to show a CLI user the shots', () => {
105
- for (const rel of ['docs/packs/engineer.md', 'docs/packs/artist.md', '.claude/skills/design/SKILL.md']) {
105
+ for (const rel of ['docs/packs/artisan.md', 'docs/packs/artist.md', '.claude/skills/design/SKILL.md']) {
106
106
  const doc = read(rel);
107
107
  assert.match(doc, /review-sheet\.js/, `${rel} names the helper`);
108
108
  assert.match(doc, /2026-09-23/, `${rel} dates the owner decision`);
109
109
  }
110
- assert.doesNotMatch(read('docs/packs/engineer.md'), /rather than asking a human to look/, 'the old contrary instruction is gone');
110
+ assert.doesNotMatch(read('docs/packs/artisan.md'), /rather than asking a human to look/, 'the old contrary instruction is gone');
111
111
  });
112
112
 
113
113
  summary();
@@ -167,7 +167,7 @@ async function boot(opts = {}) {
167
167
  // the same reason categoryChip is: an undefined helper throws mid-render.
168
168
  roleAxisLabel: () => 'Role',
169
169
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
170
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
170
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
171
171
  roleIsUnset: (d) => !d || d === 'unclassified',
172
172
  ROLE_UNSET: 'unclassified',
173
173
  escapeHtml,
@@ -196,6 +196,9 @@ async function boot(opts = {}) {
196
196
  };
197
197
  sandbox.globalThis = sandbox;
198
198
  vm.createContext(sandbox);
199
+ // The GitHub-repository half of the invite box is its own file, loaded before watch.js
200
+ // exactly as watch.html does.
201
+ vm.runInContext(fs.readFileSync(path.join(HALL, 'watch-repo-invite.js'), 'utf8'), sandbox, { filename: 'watch-repo-invite.js' });
199
202
  vm.runInContext(SRC, sandbox, { filename: 'watch.js' });
200
203
  await settle();
201
204
 
@@ -421,13 +424,13 @@ const PUBLIC_PROFILE = {
421
424
  },
422
425
  cross_project: {
423
426
  totals: { projects: 3, credits: 4200, works_shipped: 41, karma: 7 },
424
- disciplines: ['engineer', 'artist'], last_active: '2026-08-30T00:00:00Z',
427
+ disciplines: ['artisan', 'artist'], last_active: '2026-08-30T00:00:00Z',
425
428
  projects: [], more_projects: 0,
426
429
  },
427
430
  };
428
431
  const SLIVER_PROFILE = {
429
432
  view: 'sliver',
430
- sliver: { handle: 'quiet', display_name: 'Quiet Person', avatar_url: null, disciplines: ['engineer'], active_recently: true },
433
+ sliver: { handle: 'quiet', display_name: 'Quiet Person', avatar_url: null, disciplines: ['artisan'], active_recently: true },
431
434
  };
432
435
  const withProfile = (profile, over = {}) => [{ ...WAITING[0], profile, ...over }];
433
436
 
@@ -439,7 +442,7 @@ await test('a PUBLIC applicant renders their record beside the ask', async () =>
439
442
  assert.match(text, /I draw maps\./, 'the bio is part of what the public page already shows');
440
443
  assert.match(text, /3 projects/);
441
444
  assert.match(text, /41 shipped/);
442
- assert.match(text, /engineer/);
445
+ assert.match(text, /artisan/);
443
446
  });
444
447
 
445
448
  await test('a PRIVATE applicant renders the sliver AND says why it is thin', async () => {
@@ -449,7 +452,7 @@ await test('a PRIVATE applicant renders the sliver AND says why it is thin', asy
449
452
  const text = block_.textContent;
450
453
  assert.match(text, /Private account/,
451
454
  'a reviewer must not read the thinness as a poor record — it is a boundary, and applying is what opened even this much');
452
- assert.match(text, /engineer/);
455
+ assert.match(text, /artisan/);
453
456
  assert.match(text, /Active recently/);
454
457
  });
455
458
 
@@ -119,7 +119,7 @@ async function boot({ answers = {}, fallback = ok({}) } = {}) {
119
119
  // the same reason categoryChip is: an undefined helper throws mid-render.
120
120
  roleAxisLabel: () => 'Role',
121
121
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
122
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
122
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
123
123
  roleIsUnset: (d) => !d || d === 'unclassified',
124
124
  ROLE_UNSET: 'unclassified',
125
125
  escapeHtml,
@@ -147,6 +147,8 @@ async function boot({ answers = {}, fallback = ok({}) } = {}) {
147
147
  };
148
148
  sandbox.globalThis = sandbox;
149
149
  vm.createContext(sandbox);
150
+ // watch-repo-invite.js loads before watch.js, as watch.html has it.
151
+ vm.runInContext(fs.readFileSync(path.join(HALL, 'watch-repo-invite.js'), 'utf8'), sandbox, { filename: 'watch-repo-invite.js' });
150
152
  vm.runInContext(SRC, sandbox, { filename: 'watch.js' });
151
153
  await settle();
152
154
 
@@ -98,23 +98,25 @@ test('the credit links to cloudbongos.com (task 1004953)', () => {
98
98
  });
99
99
 
100
100
  test('the owner\'s role mapping: destination and craft mode', () => {
101
+ // task 1005141: the game's three characters — Owner became Patron, and the ideator's
102
+ // door folded into the artist's (one Artist door, to the studio).
103
+ assert.equal(W.ROLES.length, 3, 'three doors, no separate ideator door');
101
104
  const map = Object.fromEntries(W.ROLES.map((r) => [r.id, [r.label, r.room, r.href, r.mode]]));
102
105
  assert.deepEqual(map, {
103
- owner: ['Owner', 'City', '/city', 'governing'],
104
- ideator: ['Ideator', 'Your thinking', '/thinking', 'ideating'],
105
- artist: ['Artist', 'Studio', '/studio', 'artistry'],
106
- engineer: ['Engineer', 'Home', null, 'building'],
106
+ patron: ['Patron', 'City', '/city', 'patron'],
107
+ artist: ['Artist', 'Studio', '/studio', 'artist'],
108
+ artisan: ['Artisan', 'Home', null, 'artisan'],
107
109
  });
108
110
  // Every mode is one craft-mode.js knows, so OTBCraftMode.choose can never be handed a stranger.
109
111
  const craft = fs.readFileSync(path.join(HALL, 'craft-mode.js'), 'utf8');
110
112
  for (const r of W.ROLES) assert.match(craft, new RegExp(`id: '${r.mode}'`), r.mode);
111
113
  });
112
114
 
113
- test('the Owner role shows only where Governing is offered', () => {
115
+ test('the Patron role shows only where the Patron mode is offered', () => {
114
116
  const ids = (c) => W.rolesFor(c).map((r) => r.id);
115
- assert.deepEqual(ids({ offered: (m) => m !== 'governing' }), ['ideator', 'artist', 'engineer'], 'no seat: no Owner');
116
- assert.deepEqual(ids({ offered: () => true }), ['owner', 'ideator', 'artist', 'engineer']);
117
- assert.deepEqual(ids(null), ['owner', 'ideator', 'artist', 'engineer'], 'no craft mode at all: the four, the pages gate themselves');
117
+ assert.deepEqual(ids({ offered: (m) => m !== 'patron' }), ['artist', 'artisan'], 'no seat: no Patron');
118
+ assert.deepEqual(ids({ offered: () => true }), ['patron', 'artist', 'artisan']);
119
+ assert.deepEqual(ids(null), ['patron', 'artist', 'artisan'], 'no craft mode at all: the three, the pages gate themselves');
118
120
  });
119
121
 
120
122
  test('Home loads the welcome before the shell, and the shell asks it before the craft picker', () => {
@@ -126,6 +128,7 @@ test('Home loads the welcome before the shell, and the shell asks it before the
126
128
  assert.ok(at('welcome-boot.css') > 0, 'and its sheet');
127
129
  const shell = fs.readFileSync(path.join(HALL, 'shell.js'), 'utf8');
128
130
  // task 1005076: and neither is asked while the setup screen holds the builder (tests/setup_flow.mjs).
129
- assert.match(shell, /if \(!holdForSetup\(state && state\.builder\) && !\(window\.OTBWelcome && window\.OTBWelcome\.onAccess\(access, state && state\.builder\)\) && CRAFT\) CRAFT\.onAccess\(/,
131
+ // task 1005147: character creation comes first of all — a builder with no character picks one.
132
+ assert.match(shell, /if \(!holdForSetup\(state && state\.builder\) && !\(CRAFT && CRAFT\.characterGate && CRAFT\.characterGate\(access, state\)\) && !\(window\.OTBWelcome && window\.OTBWelcome\.onAccess\(access, state && state\.builder\)\) && CRAFT\) CRAFT\.onAccess\(/,
130
133
  'the picker is asked only when setup is done and the welcome did not show');
131
134
  });
@@ -1,6 +1,6 @@
1
1
  // tests/welcome_boot_browser.mjs — the welcome in a real browser (tasks 1004785, 1004818).
2
2
  //
3
- // WHY. "Once a day", "Skip", "the craft picker does not also open", "Owner only with a seat", the day
3
+ // WHY. "Once a day", "Skip", "the craft picker does not also open", "Patron only with a seat", the day
4
4
  // and night paintings and the slow way out are behaviour across the real shell.js, craft-mode.js and
5
5
  // welcome-boot.js together — the shell is what decides which of the two greets a builder — so this
6
6
  // serves the REAL Home markup, the REAL hall scripts and the REAL paintings from a routed origin
@@ -44,7 +44,7 @@ async function hall({ art = true, reducedMotion = 'no-preference', time = null }
44
44
  const p = new URL(route.request().url()).pathname;
45
45
  if (p === '/' || p === '/index.html') return route.fulfill({ contentType: 'text/html', body: PAGE });
46
46
  if (p === '/api/bongos/auth/web/start') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>github</title><p id="auth">sign in</p>' });
47
- if (p === '/thinking' || p === '/city') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>room</title><p id="room">' + p + '</p>' });
47
+ if (p === '/studio' || p === '/city') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>room</title><p id="room">' + p + '</p>' });
48
48
  const ext = path.extname(p);
49
49
  if (ext === '.webp' && !art) return route.fulfill({ status: 404, body: '' });
50
50
  const f = path.join(HALL, p.replace(/^\/builders\//, ''));
@@ -70,9 +70,9 @@ const signIn = (page, positions) => page.evaluate((pos) => {
70
70
  window.OTBShell.applyAccess(me);
71
71
  window.OTBShell.applyAccess(me);
72
72
  }, positions || null);
73
- // The seats are settled once the top bar's Governing option says so (hidden, or named by the seat).
73
+ // The seats are settled once the top bar's Patron option (Governing until task 1005141) says so (hidden, or named by the seat).
74
74
  const seatsSettled = (page, want) => page.waitForFunction((w) => {
75
- const opt = document.querySelector('#craft-switch [data-craft-opt="governing"]');
75
+ const opt = document.querySelector('#craft-switch [data-craft-opt="patron"]');
76
76
  return !!opt && (w === null ? opt.hidden : !opt.hidden && opt.textContent.indexOf(w) !== -1);
77
77
  }, want);
78
78
  const state = (page) => page.evaluate(() => ({
@@ -190,7 +190,7 @@ test('Escape leaves the welcome at any point', { skip }, async () => {
190
190
  await ctx.close();
191
191
  });
192
192
 
193
- test('roles: no seat hides Owner; Ideator sets the mode and goes to Your thinking', { skip }, async () => {
193
+ test('roles: no seat hides Patron; Artist sets the mode and goes to the Studio', { skip }, async () => {
194
194
  const ctx = await hall();
195
195
  const { page } = await open(ctx);
196
196
  await signIn(page, { held: [] });
@@ -200,16 +200,16 @@ test('roles: no seat hides Owner; Ideator sets the mode and goes to Your thinkin
200
200
  await page.click('[data-wb-roles]');
201
201
  await page.waitForSelector('#wb-roles:not([hidden]) [data-wb-role]');
202
202
  const roles = await page.evaluate(() => [...document.querySelectorAll('[data-wb-role]')].map((b) => b.getAttribute('data-wb-role')));
203
- assert.deepEqual(roles, ['ideator', 'artist', 'engineer']);
203
+ assert.deepEqual(roles, ['artist', 'artisan'], 'the ideator door folded into Artist (task 1005141)');
204
204
  const stored = await Promise.all([
205
- page.waitForURL(ORIGIN + '/thinking'),
206
- page.click('[data-wb-role="ideator"]'),
205
+ page.waitForURL(ORIGIN + '/studio'),
206
+ page.click('[data-wb-role="artist"]'),
207
207
  ]).then(() => page.evaluate(() => sessionStorage.getItem('hall-craft')));
208
- assert.equal(stored, 'ideating', 'the hall wears Ideating on arrival');
208
+ assert.equal(stored, 'artist', 'the hall wears Artist on arrival');
209
209
  await ctx.close();
210
210
  });
211
211
 
212
- test('roles: a seat holder sees Owner; Engineer stays on Home in Building', { skip }, async () => {
212
+ test('roles: a seat holder sees Patron; Artisan stays on Home in Artisan', { skip }, async () => {
213
213
  const ctx = await hall();
214
214
  const { page } = await open(ctx);
215
215
  await signIn(page, { held: [{ kind: 'lead', seat: 'planning', title: 'Planning Lead' }] });
@@ -219,10 +219,10 @@ test('roles: a seat holder sees Owner; Engineer stays on Home in Building', { sk
219
219
  await page.click('[data-wb-roles]');
220
220
  await page.waitForSelector('#wb-roles:not([hidden]) [data-wb-role]');
221
221
  const roles = await page.evaluate(() => [...document.querySelectorAll('[data-wb-role]')].map((b) => b.getAttribute('data-wb-role')));
222
- assert.deepEqual(roles, ['owner', 'ideator', 'artist', 'engineer']);
223
- await page.click('[data-wb-role="engineer"]');
222
+ assert.deepEqual(roles, ['patron', 'artist', 'artisan']);
223
+ await page.click('[data-wb-role="artisan"]');
224
224
  await page.waitForSelector('#welcome-boot', { state: 'detached' });
225
- assert.deepEqual(await state(page), { welcome: false, picker: false, craft: 'building', asked: '1' });
225
+ assert.deepEqual(await state(page), { welcome: false, picker: false, craft: 'artisan', asked: '1' });
226
226
  assert.equal(new URL(page.url()).pathname, '/');
227
227
  await ctx.close();
228
228
  });
@@ -27,7 +27,7 @@ import os from 'node:os';
27
27
  import path from 'node:path';
28
28
 
29
29
  const require = createRequire(import.meta.url);
30
- const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules, huskDisposition, unlinkNodeModulesLink } = require('../scripts/bongos/worktree.js');
30
+ const { worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules, huskDisposition, unlinkNodeModulesLink, nextStepLines } = require('../scripts/bongos/worktree.js');
31
31
 
32
32
  const HOUR = 60 * 60 * 1000;
33
33
 
@@ -285,6 +285,21 @@ test('in-use outranks every other rule', () => {
285
285
  'in use — this session is inside it');
286
286
  });
287
287
 
288
+ // task 1005185 - feedback bug scripts: on a project that installs the core from npm the
289
+ // worktree has no scripts/ of its own (it is a git-ignored link into node_modules), so the
290
+ // "Next:" hint must name `npx bongos claim`, not a node path that points at nothing.
291
+ test('installed core: the Next hint names npx bongos claim, never a node scripts path', () => {
292
+ const out = nextStepLines('/p/.claude/worktrees/task-9', { installed: true }).join('\n');
293
+ eq(/npx bongos claim <task-id>/.test(out), true, 'names npx bongos claim');
294
+ eq(/node scripts\//.test(out), false, 'no node scripts/ path');
295
+ eq(out.includes('cd "/p/.claude/worktrees/task-9"'), true, 'still says where to cd');
296
+ });
297
+
298
+ test('core checkout: the Next hint keeps the node scripts/bongos/claim.js path', () => {
299
+ const out = nextStepLines('/p/.claude/worktrees/task-9', { installed: false }).join('\n');
300
+ eq(/node scripts\/bongos\/claim\.js <task-id>/.test(out), true);
301
+ });
302
+
288
303
  console.log('');
289
304
  console.log(`worktree_helper: ${passed} passed, ${failed} failed`);
290
305
  process.exit(failed > 0 ? 1 : 0);