@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
@@ -0,0 +1,379 @@
1
+ // tests/github_repo_invite.mjs — inviting a person to the project's GitHub repo from
2
+ // inside Bongos: POST + GET /github/repo-invite, and the two github-repos helpers.
3
+ //
4
+ // The load-bearing assertions:
5
+ // • the repo is the PROJECT's own (repo-info), never read from the request, so a
6
+ // typed name cannot point the owner's token at another repository
7
+ // • the access offered is always READ ('pull'): 'push', 'admin' and anything else is
8
+ // refused with a 400 before GitHub is touched (owner decision: no write access)
9
+ // • the status read is strict too: a stray repo= is refused, never ignored
10
+ // • no repo connected -> 409 no_project_repo, and GitHub is never called
11
+ // • a bad GitHub name is a clean 400 before any token or GitHub touch
12
+ // • GitHub's own refusals come back as sentences: 403 -> not_repo_admin (409, never
13
+ // a 403, which the page reads as "sign in again"), 401 drops the stored token
14
+ // • the helper sends PUT /collaborators/{login} with the permission, and reads
15
+ // 'pending' from the open invitations when the person is not yet a collaborator
16
+ //
17
+ // DB-free + network-free: the token, repo-info and the helpers are stubbed on their
18
+ // cached module objects (the github_repo_visibility.mjs pattern).
19
+
20
+ import { strict as assert } from 'node:assert';
21
+ import { test } from 'node:test';
22
+ import { createRequire } from 'node:module';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ process.env.BUILDER_SECRET_KEY = 'ab'.repeat(32);
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
+ const db = require(path.join(ROOT, 'src/bongos/db.js'));
31
+ const githubRepos = require(path.join(ROOT, 'src/bongos/github-repos.js'));
32
+ const repoInfo = require(path.join(ROOT, 'src/bongos/repo-info.js'));
33
+ const buildGithubRouter = require(path.join(ROOT, 'src/bongos/routes/github.js'));
34
+ const { attachFail } = require(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
35
+
36
+ const { GithubApiError } = githubRepos;
37
+
38
+ function fakeRes() {
39
+ const res = { _status: 200, _body: null };
40
+ res.status = (c) => { res._status = c; return res; };
41
+ res.json = (b) => { res._body = b; return res; };
42
+ attachFail({}, res, () => {});
43
+ return res;
44
+ }
45
+ function finalHandler(router, method, pth) {
46
+ const layer = router.stack.find((l) => l.route && l.route.path === pth && l.route.methods[method]);
47
+ const stack = layer.route.stack;
48
+ return stack[stack.length - 1].handle;
49
+ }
50
+
51
+ // The two repo-invite helpers reach the router through its deps seam (see routes/github.js).
52
+ let routerDeps = {};
53
+
54
+ function stub({ repo = { owner: 'jaxri', repo: 'mercury' }, invite, state } = {}) {
55
+ const orig = {
56
+ get: db.getGithubToken, del: db.deleteGithubToken,
57
+ info: repoInfo.loadRepoInfo,
58
+ };
59
+ const calls = { invite: [], state: [], del: 0 };
60
+ db.getGithubToken = async () => ({ token: 'tok', scope: 'repo read:user' });
61
+ db.deleteGithubToken = async () => { calls.del++; };
62
+ repoInfo.loadRepoInfo = () => repo;
63
+ routerDeps = {
64
+ inviteRepoCollaborator: async (_t, a) => { calls.invite.push(a); return invite(); },
65
+ repoInviteState: async (_t, a) => { calls.state.push(a); return state(); },
66
+ };
67
+ const restore = () => {
68
+ db.getGithubToken = orig.get; db.deleteGithubToken = orig.del;
69
+ routerDeps = {};
70
+ repoInfo.loadRepoInfo = orig.info;
71
+ };
72
+ return { calls, restore };
73
+ }
74
+
75
+ const post = () => finalHandler(buildGithubRouter(routerDeps), 'post', '/github/repo-invite');
76
+ const get = () => finalHandler(buildGithubRouter(routerDeps), 'get', '/github/repo-invite');
77
+ const reqOf = (body) => ({ builder: { id: 1 }, body });
78
+
79
+ test('POST: invites to the PROJECT repo, read-only by default', async () => {
80
+ const s = stub({ invite: () => ({ state: 'invited', invitation_id: 9, permission: 'pull' }) });
81
+ try {
82
+ const res = fakeRes();
83
+ await post()(reqOf({ github_login: '@octocat' }), res);
84
+ assert.equal(res._status, 201);
85
+ assert.equal(res._body.repo, 'jaxri/mercury');
86
+ assert.deepEqual(s.calls.invite, [{ owner: 'jaxri', name: 'mercury', login: 'octocat', permission: 'pull' }]);
87
+ } finally { s.restore(); }
88
+ });
89
+
90
+ test('POST: a repo named in the request is refused — the project repo is the only target', async () => {
91
+ const s = stub({ invite: () => ({ state: 'invited', permission: 'pull' }) });
92
+ try {
93
+ const res = fakeRes();
94
+ await post()(reqOf({ github_login: 'octocat', repo: 'someone-else/secret', owner: 'x', name: 'y' }), res);
95
+ assert.equal(res._status, 400, 'unknown fields are refused, so no field can aim the token elsewhere');
96
+ assert.equal(s.calls.invite.length, 0);
97
+ } finally { s.restore(); }
98
+ });
99
+
100
+ test('POST: write access can never be granted — push, admin and the rest are refused before GitHub', async () => {
101
+ const s = stub({ invite: () => ({ state: 'invited', permission: 'pull' }) });
102
+ try {
103
+ const ok = fakeRes();
104
+ await post()(reqOf({ github_login: 'octocat', permission: 'pull' }), ok);
105
+ assert.equal(s.calls.invite[0].permission, 'pull');
106
+ for (const bad of ['push', 'admin', 'maintain', 'triage', 'write', '']) {
107
+ const res = fakeRes();
108
+ await post()(reqOf({ github_login: 'octocat', permission: bad }), res);
109
+ assert.equal(res._status, 400, `permission ${JSON.stringify(bad)} must 400`);
110
+ }
111
+ assert.equal(s.calls.invite.length, 1, 'GitHub is only called for the one read-only request');
112
+ } finally { s.restore(); }
113
+ });
114
+
115
+ test('POST: already a collaborator answers 200, not a second invitation', async () => {
116
+ const s = stub({ invite: () => ({ state: 'already_collaborator', permission: 'pull' }) });
117
+ try {
118
+ const res = fakeRes();
119
+ await post()(reqOf({ github_login: 'octocat' }), res);
120
+ assert.equal(res._status, 200);
121
+ assert.equal(res._body.state, 'already_collaborator');
122
+ } finally { s.restore(); }
123
+ });
124
+
125
+ test('POST: a malformed GitHub name is a clean 400 before any GitHub touch', async () => {
126
+ const s = stub({ invite: () => ({}) });
127
+ try {
128
+ for (const bad of ['', '-lead', 'a b', 'a/b', 'x'.repeat(40), 'javascript:alert(1)']) {
129
+ const res = fakeRes();
130
+ await post()(reqOf({ github_login: bad }), res);
131
+ assert.equal(res._status, 400, `login ${JSON.stringify(bad)} must 400`);
132
+ }
133
+ assert.equal(s.calls.invite.length, 0);
134
+ } finally { s.restore(); }
135
+ });
136
+
137
+ test('POST: a project with no repository is a 409 and GitHub is never called', async () => {
138
+ const s = stub({ repo: { error: 'no_repo_info' }, invite: () => ({}) });
139
+ try {
140
+ const res = fakeRes();
141
+ await post()(reqOf({ github_login: 'octocat' }), res);
142
+ assert.equal(res._status, 409);
143
+ assert.equal(res._body.error.code, 'no_project_repo');
144
+ assert.equal(s.calls.invite.length, 0);
145
+ } finally { s.restore(); }
146
+ });
147
+
148
+ test('POST: GitHub 403 is a 409 not_repo_admin (never a 403); 401 drops the token and re-prompts', async () => {
149
+ const denied = stub({ invite: () => { throw new GithubApiError('x', { status: 403, code: 'not_repo_admin' }); } });
150
+ try {
151
+ const res = fakeRes();
152
+ await post()(reqOf({ github_login: 'octocat' }), res);
153
+ assert.equal(res._status, 409);
154
+ assert.equal(res._body.error.code, 'not_repo_admin');
155
+ } finally { denied.restore(); }
156
+ const expired = stub({ invite: () => { throw new GithubApiError('x', { status: 401, code: 'github_unauthorized' }); } });
157
+ try {
158
+ const res = fakeRes();
159
+ await post()(reqOf({ github_login: 'octocat' }), res);
160
+ assert.equal(res._status, 403);
161
+ assert.equal(res._body.error.code, 'repo_scope_required');
162
+ assert.equal(expired.calls.del, 1);
163
+ } finally { expired.restore(); }
164
+ });
165
+
166
+ test('GET: reports where one person stands on the project repo', async () => {
167
+ const s = stub({ state: () => ({ state: 'pending', invitation_id: 9 }) });
168
+ try {
169
+ const res = fakeRes();
170
+ await get()({ builder: { id: 1 }, query: { github_login: 'octocat' } }, res);
171
+ assert.equal(res._status, 200);
172
+ assert.equal(res._body.state, 'pending');
173
+ assert.deepEqual(s.calls.state, [{ owner: 'jaxri', name: 'mercury', login: 'octocat' }]);
174
+ } finally { s.restore(); }
175
+ });
176
+
177
+ // ---- the helpers, against a fake fetch ----
178
+
179
+ function fakeFetch(map) {
180
+ const seen = [];
181
+ const f = async (url, opts = {}) => {
182
+ seen.push({ url, method: opts.method || 'GET', body: opts.body || null });
183
+ const hit = map.find((m) => url.includes(m.match) && (m.method || 'GET') === (opts.method || 'GET'));
184
+ const r = hit || { status: 500 };
185
+ return { status: r.status, ok: r.status >= 200 && r.status < 300, json: async () => r.body };
186
+ };
187
+ f.seen = seen;
188
+ return f;
189
+ }
190
+
191
+ test('inviteRepoCollaborator: PUTs the permission, 201 = invited, 204 = already in', async () => {
192
+ const f1 = fakeFetch([{ match: '/collaborators/octocat', method: 'PUT', status: 201, body: { id: 5 } }]);
193
+ const a = await githubRepos.inviteRepoCollaborator('t', { owner: 'o', name: 'n', login: 'octocat', fetchImpl: f1 });
194
+ assert.deepEqual(a, { state: 'invited', invitation_id: 5, permission: 'pull' });
195
+ assert.equal(f1.seen[0].method, 'PUT');
196
+ assert.deepEqual(JSON.parse(f1.seen[0].body), { permission: 'pull' });
197
+ const f2 = fakeFetch([{ match: '/collaborators/octocat', method: 'PUT', status: 204 }]);
198
+ const b = await githubRepos.inviteRepoCollaborator('t', { owner: 'o', name: 'n', login: 'octocat', permission: 'pull', fetchImpl: f2 });
199
+ assert.equal(b.state, 'already_collaborator');
200
+ for (const bad of ['push', 'admin', 'maintain', 'triage']) {
201
+ await assert.rejects(
202
+ githubRepos.inviteRepoCollaborator('t', { owner: 'o', name: 'n', login: 'octocat', permission: bad, fetchImpl: f2 }),
203
+ (e) => e.code === 'bad_permission',
204
+ `${bad} must be refused by the helper itself, before any request`,
205
+ );
206
+ }
207
+ assert.equal(f2.seen.length, 1, 'only the one read-only request reached GitHub');
208
+ });
209
+
210
+ test('repoInviteState: collaborator, pending (case-insensitive) or none', async () => {
211
+ const yes = fakeFetch([{ match: '/collaborators/octocat', status: 204 }]);
212
+ assert.equal((await githubRepos.repoInviteState('t', { owner: 'o', name: 'n', login: 'octocat', fetchImpl: yes })).state, 'collaborator');
213
+ const pending = fakeFetch([
214
+ { match: '/collaborators/octocat', status: 404 },
215
+ { match: '/invitations', status: 200, body: [{ id: 3, invitee: { login: 'OctoCat' }, permissions: 'read' }] },
216
+ ]);
217
+ const p = await githubRepos.repoInviteState('t', { owner: 'o', name: 'n', login: 'octocat', fetchImpl: pending });
218
+ assert.equal(p.state, 'pending');
219
+ assert.equal(p.invitation_id, 3);
220
+ const none = fakeFetch([
221
+ { match: '/collaborators/octocat', status: 404 },
222
+ { match: '/invitations', status: 200, body: [{ id: 4, invitee: { login: 'someone' } }] },
223
+ ]);
224
+ assert.equal((await githubRepos.repoInviteState('t', { owner: 'o', name: 'n', login: 'octocat', fetchImpl: none })).state, 'none');
225
+ });
226
+
227
+ // ---- the hall's invite box (modules/hall-ui/public/watch-repo-invite.js), run in a vm ----
228
+
229
+ import fs from 'node:fs';
230
+ import vm from 'node:vm';
231
+
232
+ function loadBox(calls = []) {
233
+ const win = {};
234
+ const sandbox = { window: win, document: {}, encodeURIComponent, console };
235
+ vm.createContext(sandbox);
236
+ vm.runInContext(fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/watch-repo-invite.js'), 'utf8'), sandbox);
237
+ const postJson = async (p, b) => { calls.push([p, b]); return { state: 'invited' }; };
238
+ return win.OTBWatchRepoInvite.make({ api: {}, API: '/api/bongos', postJson, $: () => null });
239
+ }
240
+
241
+ test('hall box: the invite is read-only and there is no access choice to make', () => {
242
+ const box = loadBox();
243
+ const html = box.formFieldsHtml();
244
+ assert.match(html, /id="gate-invite-repo" checked/, 'the repository invite is on by default');
245
+ assert.match(html, /read-only/, 'the label says what they get, in the one word every sentence uses');
246
+ assert.doesNotMatch(html, /<select|<option|push|admin|maintain|triage/i, 'no write or admin choice is drawn');
247
+ const form = { querySelector: (s) => (s === '#gate-invite-repo' ? { checked: true } : { value: 'push' }) };
248
+ assert.equal(JSON.stringify(box.readForm(form)), JSON.stringify({ want: true }), 'only the tick is read; there is no access level to pick');
249
+ });
250
+
251
+ test('hall box: the repository button shows for a GitHub name, never a stand-in name', () => {
252
+ const box = loadBox();
253
+ const ghost = (attrs, label) => `<b ${attrs}>${label}</b>`;
254
+ assert.match(box.rowButtonHtml({ github_login: 'octocat' }, ghost), /data-gate-gh/);
255
+ assert.equal(box.rowButtonHtml({ github_login: '~google-person' }, ghost), '');
256
+ assert.equal(box.rowButtonHtml({ github_login: '' }, ghost), '');
257
+ });
258
+
259
+ test('hall box: inviteToRepo posts to the repository route, and a refusal never throws', async () => {
260
+ const calls = [];
261
+ const box = loadBox(calls);
262
+ const ok = await box.inviteToRepo('octocat');
263
+ assert.equal(JSON.stringify(calls[0]), JSON.stringify(['/github/repo-invite', { github_login: 'octocat' }]));
264
+ assert.match(ok.text, /must accept it/);
265
+ const win = {};
266
+ const sandbox = { window: win, document: {}, encodeURIComponent, console };
267
+ vm.createContext(sandbox);
268
+ vm.runInContext(fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/watch-repo-invite.js'), 'utf8'), sandbox);
269
+ const refused = win.OTBWatchRepoInvite.make({
270
+ api: {}, API: '/api/bongos', $: () => null,
271
+ postJson: async () => { const e = new Error('Reconnect GitHub'); e.code = 'repo_scope_required'; throw e; },
272
+ });
273
+ const r = await refused.inviteToRepo('octocat');
274
+ assert.equal(r.ok, false);
275
+ assert.equal(r.signin, true, 'a missing GitHub permission offers the reconnect link');
276
+ assert.match(r.text, /did not go through/);
277
+ assert.doesNotMatch(r.text, /hall invite/, 'the helper says nothing of a hall invite: a row has none');
278
+ // The FORM sends the hall invite first, so only its note says the admission stands.
279
+ const note = { textContent: '', appendChild() {} };
280
+ const winF = {};
281
+ const sbF = { window: winF, document: { createElement: () => ({}) }, encodeURIComponent, console };
282
+ vm.createContext(sbF);
283
+ vm.runInContext(fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/watch-repo-invite.js'), 'utf8'), sbF);
284
+ winF.OTBWatchRepoInvite.make({ api: {}, API: '/api/bongos', $: () => note, postJson: async () => ({}) }).showRepoNote(r);
285
+ assert.match(note.textContent, /^The hall invite went through\. The GitHub repository invite did not go through/);
286
+ });
287
+
288
+ test('GET: a stray repo= (or any other parameter) is refused, never ignored', async () => {
289
+ const s = stub({ state: () => ({ state: 'none' }) });
290
+ try {
291
+ const res = fakeRes();
292
+ await get()({ builder: { id: 1 }, query: { github_login: 'octocat', repo: 'someone-else/secret' } }, res);
293
+ assert.equal(res._status, 400);
294
+ assert.equal(s.calls.state.length, 0, 'GitHub is never asked');
295
+ const noName = fakeRes();
296
+ await get()({ builder: { id: 1 }, query: {} }, noName);
297
+ assert.equal(noName._status, 400);
298
+ } finally { s.restore(); }
299
+ });
300
+
301
+ test('POST: the helper\'s own bad_permission is a 400, never a 502', async () => {
302
+ const s = stub({ invite: () => { throw new GithubApiError('x', { status: 400, code: 'bad_permission' }); } });
303
+ try {
304
+ const res = fakeRes();
305
+ await post()(reqOf({ github_login: 'octocat' }), res);
306
+ assert.equal(res._status, 400);
307
+ assert.equal(res._body.error.code, 'bad_permission');
308
+ } finally { s.restore(); }
309
+ });
310
+
311
+ // ---- the row button (gateGithubStep) and the message that follows it ----
312
+
313
+ function fakeRow() {
314
+ const out = { textContent: '', children: [], appendChild(c) { this.children.push(c); } };
315
+ const row = { querySelector: (s) => (s === '[data-gh-state]' ? out : null) };
316
+ const attrs = { 'data-login': 'octocat', 'data-gh-mode': 'check' };
317
+ const btn = {
318
+ textContent: 'GitHub repository', disabled: true,
319
+ getAttribute: (k) => attrs[k], setAttribute: (k, v) => { attrs[k] = v; },
320
+ closest: () => row, attrs,
321
+ };
322
+ return { btn, out };
323
+ }
324
+ function loadStep({ get: getAnswer, post: postAnswer }) {
325
+ const win = { location: { assign() {} } };
326
+ const doc = { createElement: () => ({ set href(v) { this._h = v; }, get href() { return this._h; }, textContent: '' }) };
327
+ const sandbox = { window: win, document: doc, encodeURIComponent, console };
328
+ vm.createContext(sandbox);
329
+ vm.runInContext(fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/watch-repo-invite.js'), 'utf8'), sandbox);
330
+ return win.OTBWatchRepoInvite.make({
331
+ api: { request: async () => getAnswer() }, API: '/api/bongos', $: () => null,
332
+ postJson: async () => postAnswer(),
333
+ }).gateGithubStep;
334
+ }
335
+
336
+ test('row button: check reads pending, then Send again invites read-only and says nothing of a hall invite', async () => {
337
+ const step = loadStep({
338
+ get: () => ({ status: 200, ok: true, data: { state: 'pending' } }),
339
+ post: () => { const e = new Error('GitHub could not be reached.'); e.code = 'github_error'; throw e; },
340
+ });
341
+ const { btn, out } = fakeRow();
342
+ await step(btn);
343
+ assert.match(out.textContent, /waiting for them to accept/);
344
+ assert.equal(btn.attrs['data-gh-mode'], 'send');
345
+ assert.equal(btn.disabled, false, 'the button is usable again');
346
+ await step(btn); // now the send, which fails
347
+ assert.match(out.textContent, /did not go through/);
348
+ assert.doesNotMatch(out.textContent, /hall invite went through/, 'a row never claims a hall invite just happened');
349
+ assert.equal(btn.attrs['data-gh-mode'], 'send', 'a failed send stays a send, so the retry is one click');
350
+ });
351
+
352
+ test('row button: not invited, collaborator, and a missing GitHub permission offers the reconnect link', async () => {
353
+ const none = fakeRow();
354
+ await loadStep({ get: () => ({ status: 200, ok: true, data: { state: 'none' } }), post: () => ({}) })(none.btn);
355
+ assert.match(none.out.textContent, /Not on the GitHub repository/);
356
+ assert.match(none.btn.textContent, /read-only repository invite/);
357
+ const yes = fakeRow();
358
+ await loadStep({ get: () => ({ status: 200, ok: true, data: { state: 'collaborator' } }), post: () => ({}) })(yes.btn);
359
+ assert.match(yes.out.textContent, /On the GitHub repository/);
360
+ const scope = fakeRow();
361
+ await loadStep({
362
+ get: () => ({ status: 403, ok: false, data: { error: { code: 'repo_scope_required', message: 'Grant repository access.' } } }),
363
+ post: () => ({}),
364
+ })(scope.btn);
365
+ assert.equal(scope.out.textContent, 'Grant repository access.');
366
+ assert.equal(scope.out.children.length, 1, 'the reconnect link is appended');
367
+ assert.match(scope.out.children[0].href, /auth\/web\/start\?return=.*repos=1/);
368
+ });
369
+
370
+ test('hall box: a row only gets its status span where it has the button', () => {
371
+ const box = loadBox();
372
+ assert.match(box.rowStateHtml({ status: 'invited', expired: false, github_login: 'octocat' }), /data-gh-state/);
373
+ for (const r of [
374
+ { status: 'pending', github_login: 'octocat' },
375
+ { status: 'dismissed', github_login: 'octocat' },
376
+ { status: 'invited', expired: true, github_login: 'octocat' },
377
+ { status: 'invited', github_login: '~google-person' },
378
+ ]) assert.equal(box.rowStateHtml(r), '', `no span for ${JSON.stringify(r)}`);
379
+ });
@@ -129,7 +129,7 @@ async function render({ rank = 'metic', goalStatus = 'open', criteria = Object.v
129
129
  // the same reason categoryChip is: an undefined helper throws mid-render.
130
130
  roleAxisLabel: () => 'Role',
131
131
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
132
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
132
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
133
133
  roleIsUnset: (d) => !d || d === 'unclassified',
134
134
  ROLE_UNSET: 'unclassified',
135
135
  escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch])),
@@ -146,7 +146,7 @@ function harness({ pendingRequests = [] } = {}) {
146
146
  // the same reason categoryChip is: an undefined helper throws mid-render.
147
147
  roleAxisLabel: () => 'Role',
148
148
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
149
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
149
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
150
150
  roleIsUnset: (d) => !d || d === 'unclassified',
151
151
  ROLE_UNSET: 'unclassified',
152
152
  escapeHtml: (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
@@ -434,9 +434,9 @@ await test('the page is the sky\'s own surface — the same sheet and the same s
434
434
  assert.match(html, /<main id="hall-main" class="sky-stage">/, 'the sky is drawn in the stage, the skip link\'s target');
435
435
  const scripts = (h) => [...h.matchAll(/<script src="\/builders\/([^"?]+)\?v=([^"]+)"/g)].map((m) => [m[1], m[2]]);
436
436
  // criterion-uat-ui.js (task 1004402): the rings' UAT state words, shared with the goal page.
437
- assert.deepEqual(scripts(html).filter((s) => !['dom-utils.js', 'shell-game.js', 'shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0])).map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
437
+ assert.deepEqual(scripts(html).filter((s) => !['dom-utils.js', 'shell-game.js', 'shell-trail.js', 'craft-mode.js', 'shell.js', 'visor.js'].includes(s[0])).map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
438
438
  // Both doors load the shell (tasks 1004327, 1004430); the SKY's files are the same.
439
- const skyScripts = (h) => scripts(h).filter((s) => !['dom-utils.js', 'shell-game.js', 'shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0]));
439
+ const skyScripts = (h) => scripts(h).filter((s) => !['dom-utils.js', 'shell-game.js', 'shell-trail.js', 'craft-mode.js', 'shell.js', 'visor.js'].includes(s[0]));
440
440
  assert.deepEqual(skyScripts(html), skyScripts(thinking), 'the two doors load the same sky files at the same stamp');
441
441
  assert.equal(html.match(/thinking\.css\?v=([^"]+)/)[1], thinking.match(/thinking\.css\?v=([^"]+)/)[1]);
442
442
  assert.match(read('thinking.css'), /body\[data-page\] \.sky-stage \{/, 'the sheet dresses the one stage both doors draw in inside the hall');
@@ -277,7 +277,7 @@ await test('the board pipeline survives the real corpus size (~1,400 rows), meas
277
277
  title: `Task number ${i} of the corpus`,
278
278
  version_id: 'BONGOS-V1',
279
279
  kind: ['feature', 'bug', 'infra'][i % 3],
280
- discipline: ['engineer', 'artist', 'ideator'][i % 3],
280
+ discipline: ['artisan', 'artist', 'artist'][i % 3],
281
281
  created_by: i % 5 === 0 ? null : String(1000000 + (i % 7)),
282
282
  goal_id: String(1000000 + (i % 40)),
283
283
  updated_at: new Date(now - (i % 90) * DAY).toISOString(),
@@ -98,7 +98,7 @@ test('createTask itself adds the edge — every create vector inherits it', asyn
98
98
  if (/INSERT INTO tasks/.test(sql)) return { rows: [{ id: '777', goal_id: params[22], source: params[11], source_ref: params[12] }] };
99
99
  return inner(sql, params);
100
100
  };
101
- const row = await db.createTask({ versionId: 'V9', goalId: 42, title: 'Build the page', kind: 'feature', discipline: 'engineer', creditsReward: 5 }, { client: c });
101
+ const row = await db.createTask({ versionId: 'V9', goalId: 42, title: 'Build the page', kind: 'feature', discipline: 'artisan', creditsReward: 5 }, { client: c });
102
102
  assert.equal(row.id, '777');
103
103
  assert.deepEqual(c.edges, [[777, 500]]);
104
104
  });
@@ -60,7 +60,7 @@ try {
60
60
  ok((await edges(specTask.id)).length === 0, 'the spec task does not wait on itself');
61
61
 
62
62
  // 1) A task filed into the goal waits on the spec task, and the claim gate says so.
63
- const work = await db.createTask({ versionId: VER, goalId: goal.id, title: 'Build the first page', kind: 'feature', discipline: 'engineer', creditsReward: 10 });
63
+ const work = await db.createTask({ versionId: VER, goalId: goal.id, title: 'Build the first page', kind: 'feature', discipline: 'artisan', creditsReward: 10 });
64
64
  ok((await edges(work.id)).includes(Number(specTask.id)), 'a filed task carries an edge to the spec task');
65
65
  // unsatisfiedDeps is the read the claim gate refuses on (db-claims.js: a non-empty
66
66
  // answer throws DEPS_NOT_SHIPPED). The full claim path is not driven here — its own
@@ -74,12 +74,12 @@ try {
74
74
  ok((await statusOf(work.id)) === 'ready', `the waiting task auto-promotes to ready (got ${await statusOf(work.id)})`);
75
75
 
76
76
  // 3) A task filed after the spec shipped waits on nothing.
77
- const later = await db.createTask({ versionId: VER, goalId: goal.id, title: 'Build the second page', kind: 'feature', discipline: 'engineer' });
77
+ const later = await db.createTask({ versionId: VER, goalId: goal.id, title: 'Build the second page', kind: 'feature', discipline: 'artisan' });
78
78
  ok((await edges(later.id)).length === 0, 'a shipped spec gates nothing new');
79
79
 
80
80
  // 4) A goal made WITHOUT a spec task behaves exactly as before.
81
81
  const plain = await db.createGoal({ versionId: VER, title: 'CLI goal', createdBy: WORKER });
82
- const plainTask = await db.createTask({ versionId: VER, goalId: plain.id, title: 'Build the third page', kind: 'feature', discipline: 'engineer' });
82
+ const plainTask = await db.createTask({ versionId: VER, goalId: plain.id, title: 'Build the third page', kind: 'feature', discipline: 'artisan' });
83
83
  ok((await edges(plainTask.id)).length === 0, 'a goal with no spec task adds no edge');
84
84
 
85
85
  // 5) A task MOVED into a goal with an open spec task waits on it too.
@@ -25,7 +25,7 @@ const engine = grader.gradeEngine;
25
25
  const api = require('../src/module-api');
26
26
  const llmCache = require('../src/bongos/llm-cache.js');
27
27
 
28
- const TASK = { id: 4242, title: 'Fixture task', description: 'Add helper X. Acceptance: X exported.', kind: 'feature', discipline: 'engineer', version_id: 'BONGOS-V2', est_minutes: 60 };
28
+ const TASK = { id: 4242, title: 'Fixture task', description: 'Add helper X. Acceptance: X exported.', kind: 'feature', discipline: 'artisan', version_id: 'BONGOS-V2', est_minutes: 60 };
29
29
  const DIFF = 'diff --git a/modules/example/a.js b/modules/example/a.js\n--- a/modules/example/a.js\n+++ b/modules/example/a.js\n@@ -1 +1 @@\n-const a = 1;\n+const a = 2; // changed value here\n';
30
30
  const DS = { files_changed: 1, lines_added: 1, lines_removed: 1 };
31
31
 
@@ -79,7 +79,7 @@ test('the code panel is registered as work-type #1 and is the default', () => {
79
79
  // throwing inside the grade step (the task 1002659 posture).
80
80
  assert.equal(engine.resolveWorkType({}), code);
81
81
  assert.equal(engine.resolveWorkType({ workType: 'no-such-type' }), code);
82
- assert.equal(engine.resolveWorkType({ task: { discipline: 'engineer', kind: 'feature' } }), code);
82
+ assert.equal(engine.resolveWorkType({ task: { discipline: 'artisan', kind: 'feature' } }), code);
83
83
  });
84
84
 
85
85
  test('registerWorkType refuses a malformed or conflicting work-type at registration', () => {
@@ -33,7 +33,7 @@ const TASK = {
33
33
  id: '1004011',
34
34
  title: 'Close the thing',
35
35
  kind: 'bug',
36
- discipline: 'engineer',
36
+ discipline: 'artisan',
37
37
  version_id: 'BONGOS-V2',
38
38
  description: 'Acceptance: scripts/x.js must read the configured value, not a literal.',
39
39
  };
@@ -203,7 +203,7 @@ test('computeVerdict still passes a clean grade (fence change is presentation-on
203
203
  const noArtifact = require('../modules/grading/grader-no-artifact.js');
204
204
 
205
205
  const naArgs = () => ({
206
- task: { id: '1', title: 'T', kind: 'bug', discipline: 'engineer', description: 'Acceptance: X.' },
206
+ task: { id: '1', title: 'T', kind: 'bug', discipline: 'artisan', description: 'Acceptance: X.' },
207
207
  valueSummary: 'nothing was needed',
208
208
  notes: 'the premise was already false',
209
209
  });
@@ -53,7 +53,7 @@ const ONE_FILE = [{ file: 'a.js', content: `// ${BUNDLE_MARKER}\n${'x'.repeat(50
53
53
  async function run({ contextBundle, outcomes }) {
54
54
  const prompts = [];
55
55
  const r = await runWorker(quality, {
56
- task: { id: 1003648, title: 't', description: 'd', kind: 'bug', discipline: 'engineer' },
56
+ task: { id: 1003648, title: 't', description: 'd', kind: 'bug', discipline: 'artisan' },
57
57
  valueSummary: 'v', diff: DIFF_MARKER, changedFiles: ['a.js'], diffStats: {},
58
58
  model: 'claude-haiku-4-5-20251001', runOpts: {}, contextBundle,
59
59
  runSubagentFn: async ({ prompt }) => {
@@ -189,7 +189,7 @@ test('scope keys off the ONE existing predicate, not a second copy', () => {
189
189
  // Sanity: the predicate really does discriminate.
190
190
  assert.equal(hasCopyOrVisualWork({ discipline: 'artist' }), true);
191
191
  assert.equal(hasCopyOrVisualWork({ module_key: 'hall-ui' }), true);
192
- assert.equal(hasCopyOrVisualWork({ module_key: 'grading', discipline: 'engineer' }), false);
192
+ assert.equal(hasCopyOrVisualWork({ module_key: 'grading', discipline: 'artisan' }), false);
193
193
  });
194
194
 
195
195
  // ---- 6. STANDARD -----------------------------------------------------------
@@ -291,7 +291,7 @@ test('the copy lens is scoped by the files the work changed', () => {
291
291
  // Task 1004201: the grader hands the predicate the files the work changed, so
292
292
  // an unlabelled UI ship gets the lens at grade time.
293
293
  assert.match(shipSrc, /hasCopyOrVisualWork\(task, changedFiles\)/, 'the grader passes its own diff');
294
- const unlabelled = { module_key: null, discipline: 'engineer' };
294
+ const unlabelled = { module_key: null, discipline: 'artisan' };
295
295
  assert.equal(hasCopyOrVisualWork(unlabelled, ['modules/hall-ui/public/goal-forest-lib.js']), true);
296
296
  assert.equal(hasCopyOrVisualWork(unlabelled, ['modules/hall-ui/module.json', 'scripts/bongos/claim.js']), false);
297
297
  });
@@ -144,7 +144,7 @@ test('narc: ff=5 with only nits → concerns (not fail)', () => {
144
144
  // ---- prompt carries the policy -------------------------------------------
145
145
 
146
146
  test('every worker prompt carries the STYLE_ADVISORY paragraph', () => {
147
- const args = { task: { id: 1, title: 't', description: 'd', kind: 'bug', discipline: 'engineer' }, valueSummary: 'v', diff: '+ x', changedFiles: ['x.js'] };
147
+ const args = { task: { id: 1, title: 't', description: 'd', kind: 'bug', discipline: 'artisan' }, valueSummary: 'v', diff: '+ x', changedFiles: ['x.js'] };
148
148
  for (const w of [narc, quality, hacker, efficiency]) {
149
149
  const p = w.buildPrompt(args);
150
150
  assert.match(p, /Severity policy — style findings are ADVISORY/, `${w.KIND || 'worker'} prompt must carry the policy`);
@@ -43,7 +43,7 @@ async function test(name, fn) {
43
43
  // hand-built row would have hidden it.
44
44
  async function row(worker, output) {
45
45
  return runWorker(worker, {
46
- task: { id: 1002903, title: 't', description: 'd', kind: 'bug', discipline: 'engineer' },
46
+ task: { id: 1002903, title: 't', description: 'd', kind: 'bug', discipline: 'artisan' },
47
47
  valueSummary: 'v', diff: '+ const x = 1;', changedFiles: ['a.js'], diffStats: {},
48
48
  model: 'claude-sonnet-5', runOpts: {},
49
49
  runSubagentFn: async () => ({ stdout: JSON.stringify(output), run_ms: 5, cost_usd: 0 }),
@@ -304,7 +304,7 @@ await test('a refusal is the server\'s words, and nothing is marked decided', as
304
304
  });
305
305
 
306
306
  await test('a builder who may not decide reads the queue with a sentence, not two dead buttons', async () => {
307
- const b = bootPanel({ answers: { [APPROVALS]: queueFor({ crafts: ['engineer'], tasks: TASKS.map((t) => ({ ...t, artist_id: '99' })) }), [vis(41)]: slotsFor(41) } });
307
+ const b = bootPanel({ answers: { [APPROVALS]: queueFor({ crafts: ['artisan'], tasks: TASKS.map((t) => ({ ...t, artist_id: '99' })) }), [vis(41)]: slotsFor(41) } });
308
308
  await b.panel.open(); await settle(); await settle();
309
309
  assert.equal(b.q('[data-approve]'), null);
310
310
  assert.equal(b.q('[data-open-note]'), null);
@@ -399,8 +399,8 @@ test('RUN: the Standing card carries the three facts, the place as "2nd of 4", a
399
399
  assert.match(html, /<dt>Preferred work<\/dt><dd>Not set yet<\/dd>/, 'the mock\'s copy for an empty preference');
400
400
  assert.doesNotMatch(html, /profile__(needs|budget|rebase)/, 'the banners are not in the card');
401
401
  const withPrefs = bootHallRender({ me: ME, leaderboard: [] });
402
- withPrefs.api.renderProfile({ ...ME.builder, preferred_disciplines: ['engineer', 'ui'] });
403
- assert.match(withPrefs.el('home-standing-body').innerHTML, /<dd>engineer, ui<\/dd>/);
402
+ withPrefs.api.renderProfile({ ...ME.builder, preferred_disciplines: ['artisan', 'ui'] });
403
+ assert.match(withPrefs.el('home-standing-body').innerHTML, /<dd>artisan, ui<\/dd>/);
404
404
  assert.match(withPrefs.el('home-standing-body').innerHTML, /<dt>Leaderboard place<\/dt><dd>—<\/dd>/, 'no roll yet: a dash, not a wrong place');
405
405
  });
406
406
 
@@ -69,7 +69,7 @@ function hallRender(joinability) {
69
69
  // the same reason categoryChip is: an undefined helper throws mid-render.
70
70
  roleAxisLabel: () => 'Role',
71
71
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
72
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
72
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
73
73
  roleIsUnset: (d) => !d || d === 'unclassified',
74
74
  ROLE_UNSET: 'unclassified',
75
75
  escapeHtml,
@@ -152,7 +152,8 @@ test('A: the quiet fill is a defined variant, not an inline colour', () => {
152
152
  const m = css.match(/([^{}]*\.landing__btn--quiet[\s,][^{}]*)\{([^{}]*)\}/);
153
153
  assert.ok(m, 'controls.css defines .landing__btn--quiet');
154
154
  const block = m[2];
155
- assert.match(block, /background:\s*transparent/, 'the quiet variant is the ghost');
155
+ // Helm (task 1005192, ADR 0402): the ghost takes the hall's press fill, and nothing without the hall's tier
156
+ assert.match(block, /background:\s*var\(--press-fill, transparent\)/, 'the quiet variant is the ghost');
156
157
  assert.deepEqual(block.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'the variant carries no literal colour');
157
158
  // Same geometry as the request button means it inherits .landing__btn's
158
159
  // min-height and padding rather than restating them.