@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
@@ -291,9 +291,21 @@ function cmdAdd(root, argv) {
291
291
  }
292
292
  }
293
293
  console.log('');
294
- console.log('Next:');
295
- console.log(` cd "${wtPath}"`);
296
- console.log(' node scripts/bongos/claim.js <task-id> (worktree auto-detected)');
294
+ for (const line of nextStepLines(wtPath)) console.log(line);
295
+ }
296
+
297
+ // task 1005185 - feedback bug scripts: the "Next:" hint names the claim command that
298
+ // exists in the new tree. A project that installs the core from npm has no scripts/ of
299
+ // its own — scripts is a git-ignored link into node_modules/@bongos/core, so a fresh
300
+ // worktree has none and `node scripts/...` dies MODULE_NOT_FOUND. There the CLI is
301
+ // `npx bongos <verb>` (npx walks up to the main checkout's node_modules). Same check
302
+ // claim.js's finishHintLines uses (task 1005113).
303
+ function nextStepLines(wtPath, { installed } = {}) {
304
+ if (installed === undefined) {
305
+ try { installed = require('./fitness-lib').isInstalledCore(); } catch { installed = false; }
306
+ }
307
+ const claim = installed ? 'npx bongos claim <task-id>' : 'node scripts/bongos/claim.js <task-id>';
308
+ return ['Next:', ` cd "${wtPath}"`, ` ${claim} (worktree auto-detected)`];
297
309
  }
298
310
 
299
311
  function cmdRemove(root, argv) {
@@ -445,5 +457,5 @@ if (require.main === module) main();
445
457
 
446
458
  module.exports = {
447
459
  worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules, huskDisposition,
448
- unlinkNodeModulesLink,
460
+ unlinkNodeModulesLink, nextStepLines,
449
461
  };
@@ -135,15 +135,14 @@ overlay). A concept no table holds is not drawn: never add sample data for one.
135
135
 
136
136
  ## The evidence matrix (per page-redesign ship)
137
137
 
138
- Screenshot every changed page at **{light, dark} × {1280w, 390w}**, plus one
138
+ Screenshot every changed page at **{1280w, 390w}** (the hall is dark only since task 1005192, ADR 0402), plus one
139
139
  instance-pack run (`--brand config/branding.json`) proving the theming contract
140
140
  survives. Run the harness with **`--fixture-me`** for these — the pinned snapshot
141
141
  is what makes two runs comparable. With the Playwright MCP (`npx playwright install chrome` once):
142
142
 
143
143
  1. `browser_navigate` → the page. `browser_console_messages` must show **0 errors**.
144
144
  2. 1280w: `browser_resize 1280×900` → `browser_take_screenshot` (fullPage for tall pages).
145
- 3. dark: `browser_evaluate` `() => { localStorage.setItem('hall-theme','dark'); location.reload(); }` → screenshot. (Reset with `'light'`.)
146
- 4. 390w: `browser_resize 390×844` → light + dark screenshots.
145
+ 3. 390w: `browser_resize 390×844` → screenshot.
147
146
 
148
147
  ## Seam assertions (copy-paste `browser_evaluate` snippets)
149
148
 
@@ -7,7 +7,7 @@
7
7
  // bootstrap a new builder runs right after cloning:
8
8
  //
9
9
  // node scripts/setup-builder.js
10
- // node scripts/setup-builder.js --skip-keys # don't prompt for API keys
10
+ // node scripts/setup-builder.js --skip-keys # accepted and ignored (no key is asked for)
11
11
  // node scripts/setup-builder.js --no-auth # skip the GDS Device Flow step
12
12
  // node scripts/setup-builder.js --help
13
13
  //
@@ -15,8 +15,8 @@
15
15
  // "already done" rather than clobbering. What it does, in order:
16
16
  // 1. Verify Node >= 22 and Python >= 3.10 (the two required toolchains).
17
17
  // 2. `npm ci` to install JS dependencies.
18
- // 3. Copy .env.local.example -> .env.local (if absent) + optionally prompt
19
- // for the Google AI Studio key (--skip-keys / non-TTY takes placeholders).
18
+ // 3. Copy .env.local.example -> .env.local (if absent). No API key is asked
19
+ // for (task 1005189): a builder adds one in hall Settings when art needs it.
20
20
  // 4. `node scripts/bongos/setup.js` — GitHub Device Flow auth (--no-auth skips).
21
21
  // 5. Print the "ready" banner pointing at /builder-start.
22
22
  //
@@ -25,7 +25,6 @@
25
25
 
26
26
  const fs = require('node:fs');
27
27
  const path = require('node:path');
28
- const readline = require('node:readline');
29
28
  const { spawnSync } = require('node:child_process');
30
29
 
31
30
  const REPO_ROOT = path.resolve(__dirname, '..');
@@ -33,17 +32,15 @@ const IS_WIN = process.platform === 'win32';
33
32
 
34
33
  // ---- arg parsing ----
35
34
  const argv = process.argv.slice(2);
36
- let SKIP_KEYS = false;
37
35
  let NO_AUTH = false;
38
36
  for (const a of argv) {
39
- if (a === '--skip-keys') SKIP_KEYS = true;
37
+ if (a === '--skip-keys') { /* accepted for old callers; setup asks for no key (task 1005189) */ }
40
38
  else if (a === '--no-auth') NO_AUTH = true;
41
39
  else if (a === '-h' || a === '--help') {
42
40
  console.log(`setup-builder.js — one-command bootstrap for a new OTB builder.
43
41
 
44
42
  Usage:
45
43
  node scripts/setup-builder.js
46
- node scripts/setup-builder.js --skip-keys # don't prompt for API keys
47
44
  node scripts/setup-builder.js --no-auth # skip the GDS Device Flow step
48
45
  node scripts/setup-builder.js --help
49
46
 
@@ -162,13 +159,6 @@ function npmCi() {
162
159
  // ---------------------------------------------------------------------------
163
160
  // 3. .env.local
164
161
  // ---------------------------------------------------------------------------
165
- function ask(question) {
166
- return new Promise((resolve) => {
167
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
168
- rl.question(question, (answer) => { rl.close(); resolve(answer); });
169
- });
170
- }
171
-
172
162
  async function envLocal() {
173
163
  step('3/5 Local environment (.env.local)');
174
164
  const example = path.join(REPO_ROOT, '.env.local.example');
@@ -186,37 +176,8 @@ async function envLocal() {
186
176
  fs.copyFileSync(example, target);
187
177
  ok('Created .env.local from .env.local.example');
188
178
 
189
- // Prompt for the Google AI Studio key only with a TTY and without --skip-keys.
190
- // An empty value is not fatal — the pipeline also resolves the key from
191
- // ~/.config/otb/env, so it just means art-pipeline calls raise the standard
192
- // "GOOGLE_AI_STUDIO_KEY not found" error until set.
193
- if (SKIP_KEYS) {
194
- skip('--skip-keys set; leaving GOOGLE_AI_STUDIO_KEY blank in .env.local');
195
- skip('you can edit .env.local later, or set it in ~/.config/otb/env');
196
- return;
197
- }
198
- if (!process.stdin.isTTY) {
199
- skip('no TTY; leaving GOOGLE_AI_STUDIO_KEY blank in .env.local');
200
- return;
201
- }
202
- console.log('');
203
- console.log(` ${C.dim}Google AI Studio key for the pixel-art pipeline.`);
204
- console.log(' Get one at https://aistudio.google.com/app/apikey .');
205
- console.log(` Press Enter to skip — you can always edit .env.local later.${C.reset}`);
206
- // Strip CR/LF so a pasted value with a stray newline can't inject extra
207
- // lines into the builder's .env.local; trim handles surrounding whitespace.
208
- const input = (await ask(' GOOGLE_AI_STUDIO_KEY: ')).replace(/[\r\n]+/g, '').trim();
209
- if (input) {
210
- const contents = fs.readFileSync(target, 'utf8');
211
- // Function replacement (not a string): String.replace treats $&, $1, $$ etc.
212
- // in a replacement STRING as special, which would silently corrupt a key
213
- // containing a bare `$`. A function return value is spliced verbatim.
214
- const next = contents.replace(/^GOOGLE_AI_STUDIO_KEY=.*$/m, () => `GOOGLE_AI_STUDIO_KEY=${input}`);
215
- fs.writeFileSync(target, next);
216
- ok('Saved GOOGLE_AI_STUDIO_KEY to .env.local');
217
- } else {
218
- skip('left GOOGLE_AI_STUDIO_KEY blank — edit .env.local when you have one');
219
- }
179
+ // No key is asked for here (task 1005189): an optional key at sign-in is noise.
180
+ // The art pipeline says so itself, when art actually needs one.
220
181
  }
221
182
 
222
183
  // ---------------------------------------------------------------------------
@@ -234,7 +195,6 @@ function gdsAuth() {
234
195
  console.log(' Handing off to scripts/bongos/setup.js ...');
235
196
  console.log('');
236
197
  const setupArgs = [path.join(REPO_ROOT, 'scripts', 'bongos', 'setup.js')];
237
- if (SKIP_KEYS) setupArgs.push('--skip-keys');
238
198
  const status = run(process.execPath, setupArgs); // shell defaults off
239
199
  if (status === 0) {
240
200
  ok('GDS session ready');
@@ -149,7 +149,7 @@ async function recentShipsForRollup(builderId) {
149
149
  // preference, exactly as before. A port that cannot be asked leaves its numbers
150
150
  // null ("could not ask"), never 0. Never throws: a failed read drops its craft's
151
151
  // numbers, not the report.
152
- const ROLLUP_CRAFTS = Object.freeze(['engineer', 'artist', 'ideator']);
152
+ const ROLLUP_CRAFTS = Object.freeze(['artisan', 'artist']);
153
153
  const countOrNull = (v) => (Number.isInteger(v) && v >= 0 ? v : null);
154
154
  async function readCraftRollup(builderId, { shipped = 0, preferred = [] } = {}) {
155
155
  const fallback = { counts: null, disciplines: Array.isArray(preferred) ? preferred : [] };
@@ -172,20 +172,20 @@ async function readCraftRollup(builderId, { shipped = 0, preferred = [] } = {})
172
172
  };
173
173
  const [artist, ideator] = await Promise.all([
174
174
  earned.includes('artist') ? ask('copy-desk.artistStats') : null,
175
- earned.includes('ideator') ? ask('ideas.ideatorStats') : null,
175
+ earned.includes('artist') ? ask('ideas.ideatorStats') : null,
176
176
  ]);
177
177
  const credits = (c) => countOrNull(Math.round(Number(byCraft[c])));
178
178
  const counts = {};
179
- if (earned.includes('engineer')) counts.engineer = { shipped: countOrNull(shipped), credits: credits('engineer') };
179
+ if (earned.includes('artisan')) counts.artisan = { shipped: countOrNull(shipped), credits: credits('artisan') };
180
+ // One artist block since the ideator folded in (task 1005141): the page count
181
+ // and the idea counts side by side, under the craft's one credit split.
180
182
  if (earned.includes('artist')) {
181
- counts.artist = { pages_approved: countOrNull(artist && artist.pages_approved), credits: credits('artist') };
182
- }
183
- if (earned.includes('ideator')) {
184
- counts.ideator = {
183
+ counts.artist = {
184
+ pages_approved: countOrNull(artist && artist.pages_approved),
185
185
  filed: countOrNull(ideator && ideator.filed),
186
186
  full_ideas: countOrNull(ideator && ideator.full_ideas),
187
187
  ratified: countOrNull(ideator && ideator.ratified),
188
- credits: credits('ideator'),
188
+ credits: credits('artist'),
189
189
  };
190
190
  }
191
191
  return { counts, disciplines: earned };
@@ -77,6 +77,8 @@ function roleLine(mode) {
77
77
  // so this adds no prompt-injection surface to the session.
78
78
  function describeCraftMode(discipline, modes) {
79
79
  if (!discipline || !modes) return null;
80
+ // A row a rolled-back release wrote may still say engineer/ideator (task 1005141).
81
+ discipline = require('../crafts').normalizeCraft(discipline);
80
82
  const mode = modes[discipline];
81
83
  if (!mode || typeof mode !== 'object' || !mode.directive) return null;
82
84
  const parts = [`${roleLine(mode)} (the craft of the task claimed in this worktree).`, String(mode.directive).trim()];
package/src/bongos/db.js CHANGED
@@ -25,6 +25,7 @@
25
25
  // grade-count SQL runs on both sides of the kernel↔module line — keep in sync.
26
26
 
27
27
  const { pool } = require('./pool');
28
+ const { normalizeCraft: normalizeCraftName } = require('../crafts'); // task 1005147: setBuilderCharacter
28
29
  const secretBox = require('./secret-box');
29
30
  // Module seam (ADR 0083). The builder-exit path EMITS `builder.secrets-scrub-needed`
30
31
  // AFTER COMMIT (best-effort) so a module that stores a builder-keyed secret can
@@ -172,7 +173,10 @@ const UPDATE_BUILDER_DISCIPLINES_SQL =
172
173
  async function updateBuilderDisciplines(builderId, disciplines, { mainDiscipline } = {}) {
173
174
  const { enabledDisciplines } = require('../modules');
174
175
  const offered = enabledDisciplines();
175
- const list = Array.isArray(disciplines) ? disciplines : [];
176
+ // An old spelling (engineer, ideator) is today's craft, not an invalid one (task 1005141).
177
+ const { normalizeCraft, normalizeCrafts } = require('../crafts');
178
+ const list = Array.isArray(disciplines) ? normalizeCrafts(disciplines) : [];
179
+ if (typeof mainDiscipline === 'string') mainDiscipline = normalizeCraft(mainDiscipline);
176
180
  const bad = list.filter((d) => !offered.includes(d));
177
181
  if (bad.length) {
178
182
  const err = new Error(`invalid discipline(s): ${bad.join(', ')}`);
@@ -207,6 +211,38 @@ async function getBuilderRoleChoice(builderId) {
207
211
  return rows[0] ?? null;
208
212
  }
209
213
 
214
+ // The character a builder holds (task 1005147, core_297): 'artisan' | 'artist' |
215
+ // 'patron', or null while they have not picked one. Identity and view only — no
216
+ // gate reads it (criterion character-is-identity-not-authority). Its own read, not a
217
+ // column on getBuilderById, so the kernel's hot identity read never depends on a
218
+ // migration that may not have run yet.
219
+ const CHARACTERS = Object.freeze(['artisan', 'artist', 'patron']);
220
+ async function getBuilderCharacter(builderId) {
221
+ const { rows } = await pool.query(
222
+ `SELECT game_character FROM builders WHERE id = $1`,
223
+ [builderId]
224
+ );
225
+ return rows[0] ? rows[0].game_character : null;
226
+ }
227
+ // Set (a CHARACTERS word, old spellings normalized) or clear (null) the builder's own
228
+ // character. Throws { code: 'invalid_character' } for anything else.
229
+ async function setBuilderCharacter(builderId, character) {
230
+ let value = null;
231
+ if (character !== null) {
232
+ value = normalizeCraftName(character);
233
+ if (!CHARACTERS.includes(value)) {
234
+ const err = new Error(`invalid character: ${character}`);
235
+ err.code = 'invalid_character';
236
+ throw err;
237
+ }
238
+ }
239
+ const { rows } = await pool.query(
240
+ `UPDATE builders SET game_character = $2 WHERE id = $1 RETURNING game_character`,
241
+ [builderId, value]
242
+ );
243
+ return rows[0] ? rows[0].game_character : null;
244
+ }
245
+
210
246
  // The same two facts for a whole list of builders, in ONE statement (task
211
247
  // 1004437): the public leaderboard names every row's roles. Map<idString, row>.
212
248
  async function getBuilderRoleChoices(builderIds) {
@@ -1142,6 +1178,8 @@ module.exports = {
1142
1178
  UPDATE_BUILDER_DISCIPLINES_SQL,
1143
1179
  getBuilderRoleChoice,
1144
1180
  getBuilderRoleChoices,
1181
+ getBuilderCharacter,
1182
+ setBuilderCharacter,
1145
1183
  RANKS,
1146
1184
  setBuilderRank,
1147
1185
  setBuilderTester,
@@ -378,7 +378,85 @@ async function deleteDeployKey(token, { owner, name, keyId, fetchImpl = fetch }
378
378
  throw new GithubApiError(`GitHub deploy-key delete failed (${res.status})`, { status: res.status, code: 'github_error' });
379
379
  }
380
380
 
381
+ // ---- repo collaborators: inviting a person to the repo itself ----
382
+ // A GitHub invite is a SEPARATE door from a Bongos invite. Bongos admission is a row
383
+ // in the project's own database; this puts the person on the repo's collaborator list
384
+ // so they can see (and, only when asked, push to) the code on their own GitHub.
385
+ // ONE permission is offered: 'pull' (read). A builder ships through the server's own
386
+ // publish and needs no write access, so Bongos never grants it (owner decision,
387
+ // 2026-11-04): 'push', 'admin', 'maintain' and 'triage' are all refused. An owner who
388
+ // wants someone to have write access gives it on GitHub directly.
389
+ const REPO_INVITE_PERMISSIONS = Object.freeze(['pull']);
390
+
391
+ function repoPath(owner, name) {
392
+ return `${GH_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
393
+ }
394
+
395
+ // PUT /collaborators/{login}. 201 = a pending invitation was created, 204 = they
396
+ // already have access. Returns { state: 'invited' | 'already_collaborator', ... }.
397
+ async function inviteRepoCollaborator(token, { owner, name, login, permission = 'pull', fetchImpl = fetch } = {}) {
398
+ if (!REPO_INVITE_PERMISSIONS.includes(permission)) {
399
+ throw new GithubApiError('Only read-only access can be offered', { status: 400, code: 'bad_permission' });
400
+ }
401
+ const res = await fetchImpl(`${repoPath(owner, name)}/collaborators/${encodeURIComponent(login)}`, {
402
+ method: 'PUT',
403
+ headers: ghHeaders(token, { 'Content-Type': 'application/json' }),
404
+ body: JSON.stringify({ permission }),
405
+ });
406
+ if (res.status === 201) {
407
+ const inv = await res.json().catch(() => ({}));
408
+ return { state: 'invited', invitation_id: inv.id || null, permission };
409
+ }
410
+ if (res.status === 204) return { state: 'already_collaborator', permission };
411
+ if (res.status === 401) {
412
+ throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
413
+ }
414
+ if (res.status === 403) {
415
+ throw new GithubApiError('GitHub refused — adding people needs repository admin', { status: 403, code: 'not_repo_admin' });
416
+ }
417
+ if (res.status === 404) {
418
+ throw new GithubApiError('GitHub answered 404 for that repository or user', { status: 404, code: 'repo_not_found' });
419
+ }
420
+ if (res.status === 422) {
421
+ throw new GithubApiError('GitHub would not invite that account', { status: 422, code: 'cannot_invite' });
422
+ }
423
+ throw new GithubApiError(`GitHub collaborator invite failed (${res.status})`, { status: res.status, code: 'github_error' });
424
+ }
425
+
426
+ // Where one person stands on the repo: 'collaborator' (accepted), 'pending' (the
427
+ // invitation is out and not yet accepted), or 'none' (never invited, declined or
428
+ // expired). Two reads: the collaborator check, then the open invitations.
429
+ async function repoInviteState(token, { owner, name, login, fetchImpl = fetch } = {}) {
430
+ const who = encodeURIComponent(login);
431
+ const c = await fetchImpl(`${repoPath(owner, name)}/collaborators/${who}`, { headers: ghHeaders(token) });
432
+ if (c.status === 204) return { state: 'collaborator' };
433
+ if (c.status === 401) {
434
+ throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
435
+ }
436
+ if (c.status === 403) {
437
+ throw new GithubApiError('GitHub refused — reading collaborators needs repository admin', { status: 403, code: 'not_repo_admin' });
438
+ }
439
+ if (c.status !== 404) {
440
+ throw new GithubApiError(`GitHub collaborator read failed (${c.status})`, { status: c.status, code: 'github_error' });
441
+ }
442
+ const i = await fetchImpl(`${repoPath(owner, name)}/invitations?per_page=${PER_PAGE}`, { headers: ghHeaders(token) });
443
+ if (i.status === 404) {
444
+ throw new GithubApiError('GitHub answered 404 for that repository', { status: 404, code: 'repo_not_found' });
445
+ }
446
+ if (!i.ok) {
447
+ throw new GithubApiError(`GitHub invitations read failed (${i.status})`, { status: i.status, code: 'github_error' });
448
+ }
449
+ const rows = await i.json();
450
+ const mine = (Array.isArray(rows) ? rows : []).find(
451
+ (r) => r && r.invitee && String(r.invitee.login || '').toLowerCase() === String(login).toLowerCase(),
452
+ );
453
+ return mine
454
+ ? { state: 'pending', invitation_id: mine.id, permission: mine.permissions || null }
455
+ : { state: 'none' };
456
+ }
457
+
381
458
  module.exports = {
459
+ inviteRepoCollaborator, repoInviteState, REPO_INVITE_PERMISSIONS,
382
460
  listUserRepos, createUserRepo, getUserRepo, makeRepoPublic, makeRepoPrivate,
383
461
  listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin,
384
462
  listDeployKeys, addDeployKey, deleteDeployKey,
@@ -366,6 +366,28 @@ const MODULE_GLOBS = {
366
366
  agents: [
367
367
  'modules/agents/', '.claude/agents/', 'scripts/bongos/agents-sync.js',
368
368
  ],
369
+ // --- the MODULE STORE (ADR 0338, task 1004793): publishing, listing, entitlements,
370
+ // testing channel, promotion, usage reports, payouts and the assessment CLIs.
371
+ // It is core code — it lives in src/bongos/, not modules/ — so it has no
372
+ // module.json and this key is a scope like kernel or devsecops. Before this
373
+ // key NO key resolved these files, so no goal could own a change to the store
374
+ // (the 1002690 / 1002736 precedent). Its HTTP surface, src/bongos/routes/modules.js,
375
+ // stays kernel's with the rest of routes/. module-scope-map.js is kernel's too:
376
+ // the name prefix is shared, the job is not. ---
377
+ 'module-store': [
378
+ 'src/bongos/module-store.js', 'src/bongos/module-listing.js',
379
+ 'src/bongos/module-entitlements.js', 'src/bongos/module-overrides.js',
380
+ 'src/bongos/module-payouts.js', 'src/bongos/module-promotion.js',
381
+ 'src/bongos/module-rechecks.js', 'src/bongos/module-subcategories.js',
382
+ 'src/bongos/module-submissions.js', 'src/bongos/module-testing.js',
383
+ 'src/bongos/module-usage-reports.js',
384
+ 'scripts/bongos/module-store-cli.js', 'scripts/bongos/module-artifact.js',
385
+ 'scripts/bongos/module-assess-docs.js', 'scripts/bongos/module-assess-installs.js',
386
+ 'scripts/bongos/module-assess-score.js', 'scripts/bongos/module-assess-security.js',
387
+ 'scripts/bongos/module-assess-tests.js', 'scripts/bongos/module-assess-version.js',
388
+ 'scripts/bongos/module-error-report.js', 'scripts/bongos/module-remove.js',
389
+ 'scripts/bongos/module-test-runner.js',
390
+ ],
369
391
  // --- the hub identity provider + the PROJECTS CATALOG it owns (ADR 0141): platform
370
392
  // accounts, the SSO client registry, project memberships/invites, and the
371
393
  // platform_identity_projects rows the Explore feed reads. Carved already, so the
@@ -8,11 +8,11 @@
8
8
  // WHAT IT ANSWERS. For one builder: which of the three crafts they are shown in,
9
9
  // which one leads, and each craft's own headline numbers.
10
10
  //
11
- // engineer `shipped`, works shipped (unchanged from today), and engineering credits
11
+ // artisan `shipped`, works shipped (unchanged from today), and building credits
12
12
  // artist pages approved, "N of M tweaked" per surface, credits from pages,
13
13
  // this week (copy-desk's studio tally, the `copy-desk.artistStats`
14
14
  // port)
15
- // ideator thoughts filed, ignited, Full Ideas, ratified by the board, total
15
+ // AND (task 1005141, the ideator craft folded in) thoughts filed, ignited, Full Ideas, ratified by the board, total
16
16
  // size across every idea stream with the historical-estimate
17
17
  // marker, and the share still alive (the sky's own reads, the
18
18
  // `ideas.ideatorStats` port)
@@ -40,7 +40,7 @@
40
40
 
41
41
  const seams = require('../module-seams');
42
42
 
43
- const CRAFTS = Object.freeze(['engineer', 'artist', 'ideator']);
43
+ const CRAFTS = Object.freeze(['artisan', 'artist']);
44
44
  // A discipline as a craft, asked of economy (reward.craftForPreference), the
45
45
  // module that decides which craft a credit belongs to. One table, so a builder's
46
46
  // chips and their credits cannot disagree (`ui`, the design discipline, is the
@@ -76,7 +76,7 @@ function resolveMainRole({ mainDiscipline = null, preferred = [], byCraft = null
76
76
  }
77
77
  const pref = preferredCrafts(preferred);
78
78
  if (pref.length) return { main: pref[0], basis: 'preferred' };
79
- return { main: 'engineer', basis: 'default' };
79
+ return { main: 'artisan', basis: 'default' };
80
80
  }
81
81
 
82
82
  // shownCrafts — pure. Main first, then every craft with credits, in the
@@ -97,7 +97,7 @@ function composeRoleStats({ choice = null, byCraft = null, shippedCount = 0, art
97
97
  const { main, basis } = resolveMainRole({ mainDiscipline: choice && choice.main_discipline, preferred, byCraft });
98
98
  const shown = shownCrafts({ main, preferred, byCraft });
99
99
  const credits = byCraft
100
- ? { engineer: byCraft.engineer, artist: byCraft.artist, ideator: byCraft.ideator, other: byCraft.other, total: byCraft.total }
100
+ ? { artisan: byCraft.artisan, artist: byCraft.artist, other: byCraft.other, total: byCraft.total }
101
101
  : null;
102
102
  return {
103
103
  main,
@@ -105,16 +105,32 @@ function composeRoleStats({ choice = null, byCraft = null, shippedCount = 0, art
105
105
  shown,
106
106
  preferred: preferredCrafts(preferred),
107
107
  credits_by_craft: credits,
108
- engineer: shown.includes('engineer')
109
- ? { shipped: Number(shippedCount) || 0, credits: credits ? credits.engineer : null }
108
+ artisan: shown.includes('artisan')
109
+ ? { shipped: Number(shippedCount) || 0, credits: credits ? credits.artisan : null }
110
110
  : null,
111
- artist: shown.includes('artist') ? (artist || emptyArtist()) : null,
112
- ideator: shown.includes('ideator') ? (ideator || emptyIdeator()) : null,
111
+ artist: shown.includes('artist') ? artistBlock(artist, ideator, credits) : null,
113
112
  };
114
113
  }
115
114
 
116
115
  // The shapes a shown craft takes when its port could not be asked: every number
117
116
  // null, so a renderer says "unknown", never "0".
117
+ // The artist craft holds what the artist AND the old ideator craft did (task 1005141,
118
+ // BONGOS-V3: ideation folds into the artist). One block, two sources: the page
119
+ // numbers from copy-desk and the idea numbers from the sky. The two share no field
120
+ // name but `unavailable` (true only when NEITHER could be asked) and `credits`,
121
+ // which is the craft's ledger split when there is one, since that now carries
122
+ // both the page and the idea credit streams (economy credits-by-craft.js).
123
+ function artistBlock(artist, ideator, credits) {
124
+ const pages = artist || emptyArtist();
125
+ const ideas = ideator || emptyIdeator();
126
+ return {
127
+ ...ideas,
128
+ ...pages,
129
+ credits: credits ? credits.artist : pages.credits,
130
+ unavailable: Boolean(pages.unavailable && ideas.unavailable),
131
+ };
132
+ }
133
+
118
134
  function emptyArtist() {
119
135
  return { pages_approved: null, pages: null, surfaces_touched: null, credits: null, this_week: null, surfaces: [], unavailable: true };
120
136
  }
@@ -141,7 +157,7 @@ async function readRoleStats(builderId, { shippedCount = 0, getRoleChoice } = {}
141
157
  const shown = shownCrafts({ main, preferred, byCraft });
142
158
 
143
159
  const artistPort = shown.includes('artist') ? seams.resolveOptional('copy-desk.artistStats') : null;
144
- const ideatorPort = shown.includes('ideator') ? seams.resolveOptional('ideas.ideatorStats') : null;
160
+ const ideatorPort = shown.includes('artist') ? seams.resolveOptional('ideas.ideatorStats') : null;
145
161
  const [artist, ideator] = await Promise.all([
146
162
  typeof artistPort === 'function' ? artistPort(builderId) : null,
147
163
  typeof ideatorPort === 'function' ? ideatorPort(builderId) : null,
@@ -176,7 +192,7 @@ async function readRolesForBuilders(builderIds, { getRoleChoices } = {}) {
176
192
  main_basis: basis,
177
193
  shown: shownCrafts({ main, preferred, byCraft }),
178
194
  credits_by_craft: byCraft
179
- ? { engineer: byCraft.engineer, artist: byCraft.artist, ideator: byCraft.ideator, other: byCraft.other, total: byCraft.total }
195
+ ? { artisan: byCraft.artisan, artist: byCraft.artist, other: byCraft.other, total: byCraft.total }
180
196
  : null,
181
197
  });
182
198
  }
@@ -19,11 +19,12 @@ const express = require('express');
19
19
  const auth = require('../auth');
20
20
  const db = require('../db');
21
21
  const secretBox = require('../secret-box');
22
- const { listUserRepos, createUserRepo, listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin } = require('../github-repos');
22
+ const { listUserRepos, createUserRepo, listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin, inviteRepoCollaborator, repoInviteState, REPO_INVITE_PERMISSIONS } = require('../github-repos');
23
23
  // Namespace require for the visibility helpers (task 1003045): the route tests
24
24
  // stub these on the cached module object, which a load-time destructure would
25
25
  // pin to the real implementations.
26
26
  const githubRepos = require('../github-repos');
27
+ const repoInfo = require('../repo-info'); // namespace, so a test can stub loadRepoInfo
27
28
  const { validateOrRespond } = require('./_helpers');
28
29
  const prelaunch = require('../prelaunch-gate');
29
30
  const log = require('../logger').logger.child({ src: 'github' });
@@ -43,6 +44,28 @@ function orgNotFound(res, org) {
43
44
  return res.fail('org_not_found', { status: 404, message: `GitHub didn’t show your account an organisation called ${org}.` });
44
45
  }
45
46
 
47
+ // The failures the two repo-invite routes share, each in the owner's words.
48
+ async function repoInviteFailure(req, res, err, where) {
49
+ if (err && err.status === 401) {
50
+ await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
51
+ return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect GitHub to invite people to the repository.' });
52
+ }
53
+ if (err && err.code === 'not_repo_admin') {
54
+ return res.fail('not_repo_admin', { status: 409, message: 'GitHub says your account can’t add people to this repository. Only its owner or an admin can: ask them, or sign in with that account.' });
55
+ }
56
+ if (err && err.code === 'repo_not_found') {
57
+ return res.fail('repo_not_found', { status: 404, message: 'GitHub didn’t show that repository or that username to your account. Check the spelling of the name and try again.' });
58
+ }
59
+ if (err && err.code === 'bad_permission') {
60
+ return res.fail('bad_permission', { status: 400, message: 'Only read-only access can be offered on the repository.' });
61
+ }
62
+ if (err && err.code === 'cannot_invite') {
63
+ return res.fail('cannot_invite', { status: 409, message: 'GitHub wouldn’t invite that account. It may already own the repository, or be unable to join. Check the name, or add them on GitHub yourself.' });
64
+ }
65
+ log.error(`[gds] ${where}`, err && err.message);
66
+ return res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again in a moment.' });
67
+ }
68
+
46
69
  // Parse + validate the owner/name ref, or send a 400 and return null.
47
70
  function parseRepoRef(raw, res) {
48
71
  const ref = String(raw || '').trim();
@@ -79,9 +102,84 @@ async function resolveRepoToken(req, res) {
79
102
  return row.token;
80
103
  }
81
104
 
82
- module.exports = function buildGithubRouter() {
105
+ // The repo THIS project's code lives in, or a 409 and null. The invite routes below
106
+ // never take a repo from the request: the owner invites people to the project's own
107
+ // repo, so a typed name cannot point the owner's token at some other repository.
108
+ function projectRepoRef(res) {
109
+ const info = repoInfo.loadRepoInfo();
110
+ if (!info || info.error) {
111
+ res.fail('no_project_repo', { status: 409, message: 'This project has no GitHub repository connected, so there is nothing to invite them to. Connect one on the project’s page on the hub, then try again.' });
112
+ return null;
113
+ }
114
+ return { owner: info.owner, name: info.repo };
115
+ }
116
+
117
+ // A typed GitHub name, with the leading @ allowed, or a 400 and null.
118
+ function parseInviteLogin(raw, res) {
119
+ const login = String(raw || '').trim().replace(/^@/, '');
120
+ if (!validGithubLogin(login)) {
121
+ res.fail('bad_github_login', { status: 400, message: 'That does not look like a GitHub username.' });
122
+ return null;
123
+ }
124
+ return login;
125
+ }
126
+
127
+ // `deps` is the test seam for the two repo-invite helpers: they are imported directly
128
+ // (so the dead-code check sees them used) and a test hands in stand-ins here, because a
129
+ // destructured import cannot be stubbed on the module object the way the older
130
+ // namespace-called helpers below are.
131
+ module.exports = function buildGithubRouter(overrides = {}) {
132
+ const deps = { inviteRepoCollaborator, repoInviteState, ...overrides };
83
133
  const router = express.Router();
84
134
 
135
+ // rank: archon (permission access_request.review, the same gate as a hall invite) —
136
+ // invites a person to THIS project's GitHub repository, acting as the caller's OWN
137
+ // GitHub account (their stored repo token), never a platform credential. GitHub
138
+ // decides whether the caller may add people. The access is always READ-ONLY:
139
+ // write ('push') and above are refused, since a builder ships through the server.
140
+ // It grants no Bongos rank and does not admit anyone to the hall: those are the
141
+ // hall invite's own record.
142
+ router.post('/github/repo-invite', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
143
+ // No act-alone gate here, on purpose: that gate decides who may admit people to the
144
+ // hall (the hall invite carries it). This records a read-only share on GitHub, and
145
+ // GitHub itself only lets an account that administers the repo do it.
146
+ if (validateOrRespond(req, res, {
147
+ github_login: { required: true, type: 'string', maxLength: 60 },
148
+ permission: { type: 'string', enum: REPO_INVITE_PERMISSIONS },
149
+ })) return;
150
+ const login = parseInviteLogin(req.body.github_login, res);
151
+ if (!login) return;
152
+ const ref = projectRepoRef(res);
153
+ if (!ref) return;
154
+ const token = await resolveRepoToken(req, res);
155
+ if (!token) return;
156
+ try {
157
+ const out = await deps.inviteRepoCollaborator(token, { ...ref, login, permission: req.body.permission || 'pull' });
158
+ res.status(out.state === 'invited' ? 201 : 200).json({ ok: true, repo: `${ref.owner}/${ref.name}`, github_login: login, ...out });
159
+ } catch (err) {
160
+ return repoInviteFailure(req, res, err, 'POST /github/repo-invite');
161
+ }
162
+ });
163
+
164
+ // rank: archon (permission access_request.review) — where one person stands on the
165
+ // project's repo: collaborator, pending (invite sent, not yet accepted) or none.
166
+ router.get('/github/repo-invite', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
167
+ // Strict, like the POST: a stray `repo=` (or anything else) is refused, never ignored.
168
+ if (validateOrRespond(req, res, {}, { query: { github_login: { required: true, type: 'string', maxLength: 60 } } })) return;
169
+ const login = parseInviteLogin(req.query.github_login, res);
170
+ if (!login) return;
171
+ const ref = projectRepoRef(res);
172
+ if (!ref) return;
173
+ const token = await resolveRepoToken(req, res);
174
+ if (!token) return;
175
+ try {
176
+ const out = await deps.repoInviteState(token, { ...ref, login });
177
+ res.json({ ok: true, repo: `${ref.owner}/${ref.name}`, github_login: login, ...out });
178
+ } catch (err) {
179
+ return repoInviteFailure(req, res, err, 'GET /github/repo-invite');
180
+ }
181
+ });
182
+
85
183
  // rank: any-builder — acts only on the caller's OWN GitHub identity (the token
86
184
  // they granted). No rank gate beyond being a signed-in builder.
87
185
  router.get('/github/repos', auth.requireBuilder, async (req, res) => {