@bongos/core 1.24.78 → 1.24.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/.bongos-core.json +498 -353
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  4. package/.claude/skills/design/SKILL.md +1 -1
  5. package/.claude/skills/goal-create/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +1 -1
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +8 -0
  9. package/clients/bongos-client/index.cjs +8 -0
  10. package/clients/bongos-client/index.d.ts +11 -0
  11. package/clients/bongos-client/index.mjs +8 -0
  12. package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
  13. package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
  14. package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
  15. package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +195 -3
  18. package/docs/api-reference.md +8 -4
  19. package/docs/architecture.md +7 -3
  20. package/docs/copy-inventory.md +390 -370
  21. package/docs/copy-registry.json +719 -518
  22. package/docs/design/characters/character-sprites-source.webp +0 -0
  23. package/docs/file-map.md +7 -8
  24. package/docs/module-api-changelog.md +4 -0
  25. package/docs/modules-contract.md +5 -0
  26. package/docs/onboarding/slash-commands.md +1 -1
  27. package/docs/packs/{engineer.md → artisan.md} +5 -5
  28. package/docs/packs/artist.md +50 -4
  29. package/docs/page-inventory.json +15 -4
  30. package/docs/page-readings.json +3182 -3336
  31. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  32. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  33. package/migrations/core_297_builder_character.sql +30 -0
  34. package/modules/copy-desk/proposals.js +1 -1
  35. package/modules/economy/credits-by-craft.js +29 -15
  36. package/modules/hall-ui/public/ai-studio.html +4 -4
  37. package/modules/hall-ui/public/atlas.html +5 -5
  38. package/modules/hall-ui/public/atlas.js +3 -1
  39. package/modules/hall-ui/public/blockers.html +5 -5
  40. package/modules/hall-ui/public/board-room.html +4 -4
  41. package/modules/hall-ui/public/characters/artisan.png +0 -0
  42. package/modules/hall-ui/public/characters/artist.png +0 -0
  43. package/modules/hall-ui/public/characters/patron.png +0 -0
  44. package/modules/hall-ui/public/city.css +2 -1
  45. package/modules/hall-ui/public/city.html +9 -6
  46. package/modules/hall-ui/public/city.js +7 -1
  47. package/modules/hall-ui/public/collab.html +4 -4
  48. package/modules/hall-ui/public/commands.html +4 -4
  49. package/modules/hall-ui/public/copy-desk.html +4 -4
  50. package/modules/hall-ui/public/craft-mode.js +258 -64
  51. package/modules/hall-ui/public/deploy.html +4 -4
  52. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  53. package/modules/hall-ui/public/dom-utils.js +1 -1
  54. package/modules/hall-ui/public/goal-map.html +9 -6
  55. package/modules/hall-ui/public/goals.html +5 -5
  56. package/modules/hall-ui/public/government.html +4 -4
  57. package/modules/hall-ui/public/hall-render.js +3 -1
  58. package/modules/hall-ui/public/home-city-draw.js +3 -3
  59. package/modules/hall-ui/public/home-city.css +4 -0
  60. package/modules/hall-ui/public/home-city.js +17 -9
  61. package/modules/hall-ui/public/home-gallery.js +8 -1
  62. package/modules/hall-ui/public/home-idea-box.css +2 -2
  63. package/modules/hall-ui/public/home-idea-box.js +3 -3
  64. package/modules/hall-ui/public/idea.html +5 -5
  65. package/modules/hall-ui/public/ideas.html +5 -5
  66. package/modules/hall-ui/public/index.html +11 -7
  67. package/modules/hall-ui/public/market-watch-item.html +4 -4
  68. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  69. package/modules/hall-ui/public/market-watch.html +6 -9
  70. package/modules/hall-ui/public/market-watch.js +22 -45
  71. package/modules/hall-ui/public/modules.html +4 -4
  72. package/modules/hall-ui/public/money.html +4 -4
  73. package/modules/hall-ui/public/my-agents.html +4 -4
  74. package/modules/hall-ui/public/options-menu.js +409 -0
  75. package/modules/hall-ui/public/paintbrush.js +3 -3
  76. package/modules/hall-ui/public/palette.js +11 -13
  77. package/modules/hall-ui/public/primer.html +5 -5
  78. package/modules/hall-ui/public/profile-crafts.js +1 -1
  79. package/modules/hall-ui/public/profile-roles.js +23 -19
  80. package/modules/hall-ui/public/profile.css +1 -1
  81. package/modules/hall-ui/public/profile.html +19 -18
  82. package/modules/hall-ui/public/profile.js +19 -18
  83. package/modules/hall-ui/public/profile.states.json +4 -4
  84. package/modules/hall-ui/public/project-settings.html +4 -4
  85. package/modules/hall-ui/public/roster.html +4 -4
  86. package/modules/hall-ui/public/roster.js +3 -3
  87. package/modules/hall-ui/public/settings.css +24 -4
  88. package/modules/hall-ui/public/settings.html +21 -17
  89. package/modules/hall-ui/public/settings.js +44 -44
  90. package/modules/hall-ui/public/shell-game.css +65 -0
  91. package/modules/hall-ui/public/shell-game.js +283 -3
  92. package/modules/hall-ui/public/shell.js +7 -26
  93. package/modules/hall-ui/public/sky-interaction.js +7 -0
  94. package/modules/hall-ui/public/speciality-tools.js +1 -2
  95. package/modules/hall-ui/public/street-inside.js +3 -4
  96. package/modules/hall-ui/public/street.html +4 -4
  97. package/modules/hall-ui/public/studio.html +5 -5
  98. package/modules/hall-ui/public/style.css +73 -47
  99. package/modules/hall-ui/public/task-detail.js +5 -4
  100. package/modules/hall-ui/public/task.html +5 -5
  101. package/modules/hall-ui/public/thinking.css +10 -0
  102. package/modules/hall-ui/public/thinking.html +9 -6
  103. package/modules/hall-ui/public/tweak-editor.html +5 -5
  104. package/modules/hall-ui/public/tweak-editor.js +5 -1
  105. package/modules/hall-ui/public/update-pill.js +91 -2
  106. package/modules/hall-ui/public/visor.css +80 -0
  107. package/modules/hall-ui/public/visor.js +316 -0
  108. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  109. package/modules/hall-ui/public/watch.html +6 -5
  110. package/modules/hall-ui/public/watch.js +17 -2
  111. package/modules/hall-ui/public/welcome-boot.js +15 -6
  112. package/modules/hall-ui/public/work.html +5 -5
  113. package/modules/hall-ui/records/builders-roster.md +2 -2
  114. package/modules/hall-ui/records/craft-mode.md +13 -11
  115. package/modules/hall-ui/records/game-mode.md +2 -2
  116. package/modules/hall-ui/records/governor-street.md +1 -1
  117. package/modules/hall-ui/records/home-city.md +3 -1
  118. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  119. package/modules/hall-ui/records/options-menu.md +15 -0
  120. package/modules/hall-ui/records/profile-roles.md +11 -11
  121. package/modules/hall-ui/records/sky.md +3 -3
  122. package/modules/hall-ui/records/start-screen.md +21 -0
  123. package/modules/hall-ui/records/tweak-editor.md +1 -1
  124. package/modules/hall-ui/records/update-pill.md +12 -0
  125. package/modules/hall-ui/records/visor.md +23 -0
  126. package/modules/hall-ui/records/welcome-tower.md +5 -5
  127. package/modules/ideas/ideator-stats.js +3 -1
  128. package/modules/ideas/inbox.js +2 -2
  129. package/modules/lifecycle/activity.js +1 -1
  130. package/modules/lifecycle/cascade.js +4 -4
  131. package/modules/lifecycle/goal-spec-task.js +1 -1
  132. package/modules/lifecycle/help-requests.js +19 -15
  133. package/modules/lifecycle/kickoff-checklist.js +3 -3
  134. package/modules/lifecycle/kickoff-seed.js +1 -1
  135. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  136. package/modules/lifecycle/mingle.js +4 -4
  137. package/modules/lifecycle/page-tweak-write.js +1 -1
  138. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  139. package/modules/lifecycle/routes/tasks.js +3 -3
  140. package/modules/lifecycle/ship-card.js +2 -3
  141. package/modules/lifecycle/task-classifier.js +39 -29
  142. package/modules/lifecycle/tests/cascade.mjs +7 -7
  143. package/modules/onboarding/founding-mode.js +3 -0
  144. package/modules/onboarding/genesis-home.js +21 -1
  145. package/modules/onboarding/routes/onboarding.js +2 -0
  146. package/modules/page-notes/notes.js +3 -2
  147. package/modules/page-notes/split.js +1 -1
  148. package/modules/platform-identity/craft-rollup.js +35 -7
  149. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  150. package/modules/platform-identity/platform-identity.js +2 -2
  151. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  152. package/modules/platform-identity/routes/community-search.js +2 -1
  153. package/modules/platform-identity/routes/sso.js +2 -1
  154. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  155. package/modules/public-landing/public/community.html +3 -5
  156. package/modules/public-landing/public/index.html +1 -1
  157. package/modules/public-landing/public/projects.html +1 -1
  158. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  159. package/modules/specialities/routes/specialities.js +2 -2
  160. package/modules/status-ui/public/status.js +1 -1
  161. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  162. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  163. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  164. package/package-lock.json +2 -2
  165. package/package.json +1 -1
  166. package/release-notes.json +76 -0
  167. package/scripts/bongos/autobongos-host.js +19 -4
  168. package/scripts/bongos/autobongos-loop.js +1 -1
  169. package/scripts/bongos/claude-materialize.js +20 -1
  170. package/scripts/bongos/discipline-modes.json +7 -12
  171. package/scripts/bongos/fitness.js +1 -1
  172. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  173. package/scripts/bongos/grade-smoke.js +2 -2
  174. package/scripts/bongos/init.js +2 -2
  175. package/scripts/bongos/newcomer-floor.config.json +1 -2
  176. package/scripts/bongos/newcomer-floor.js +4 -3
  177. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  178. package/scripts/bongos/newcomer-floor.test.js +47 -38
  179. package/scripts/bongos/role-pack-guard.js +5 -4
  180. package/scripts/bongos/seed-tasks.example.json +3 -3
  181. package/scripts/bongos/setup.js +20 -107
  182. package/scripts/bongos/start.js +2 -2
  183. package/scripts/bongos/task.js +5 -4
  184. package/scripts/bongos/triage.js +1 -1
  185. package/scripts/bongos/worktree.js +16 -4
  186. package/scripts/setup-builder.js +6 -46
  187. package/src/bongos/auth-admission.js +8 -8
  188. package/src/bongos/craft-mode.js +2 -0
  189. package/src/bongos/db.js +39 -1
  190. package/src/bongos/github-repos.js +78 -0
  191. package/src/bongos/module-scope-map.js +22 -0
  192. package/src/bongos/role-stats.js +27 -11
  193. package/src/bongos/routes/github.js +100 -2
  194. package/src/bongos/routes/me.js +33 -0
  195. package/src/bongos/serve-internal.js +26 -9
  196. package/src/crafts.js +66 -0
  197. package/src/module-api.js +6 -1
  198. package/src/module-loader/manifest-schema.js +16 -0
  199. package/src/modules.js +2 -2
  200. package/src/role-responsibilities.js +6 -5
  201. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  202. package/tests/activity_rollup_as_of.mjs +1 -1
  203. package/tests/api_alias_caller_ratchet.mjs +3 -3
  204. package/tests/application_lifecycle.mjs +1 -1
  205. package/tests/artist_review.mjs +1 -1
  206. package/tests/artist_review_goal.mjs +1 -1
  207. package/tests/autobongos_host.mjs +19 -1
  208. package/tests/blocker_hall_live_proof.mjs +1 -1
  209. package/tests/blocker_tasks_named_db.mjs +2 -2
  210. package/tests/canonical_profile_url.mjs +1 -1
  211. package/tests/cap_newcomer_tasks.mjs +13 -13
  212. package/tests/character_creation.mjs +294 -0
  213. package/tests/claim_gate_open_blocker.mjs +2 -2
  214. package/tests/claim_gate_rebase.mjs +6 -6
  215. package/tests/claude_materialize.mjs +75 -1
  216. package/tests/clawback_confirmed_backlog.mjs +3 -3
  217. package/tests/collab_page.mjs +8 -9
  218. package/tests/collab_self_settling.mjs +2 -2
  219. package/tests/community_leaderboard_route.mjs +13 -5
  220. package/tests/community_proof_db.mjs +3 -0
  221. package/tests/community_search_route.mjs +7 -6
  222. package/tests/conductor.mjs +1 -1
  223. package/tests/conductor_main_e2e.mjs +8 -8
  224. package/tests/copy_desk_flags_db.mjs +1 -1
  225. package/tests/copy_desk_page_approve.mjs +2 -2
  226. package/tests/copy_desk_page_claim.mjs +2 -2
  227. package/tests/copy_desk_page_next.mjs +1 -1
  228. package/tests/copy_desk_page_release.mjs +1 -1
  229. package/tests/copy_desk_page_submit.mjs +2 -2
  230. package/tests/core_258_box_session_claims_db.mjs +1 -1
  231. package/tests/craft_mode.mjs +48 -34
  232. package/tests/craft_mode_home.mjs +20 -20
  233. package/tests/craft_mode_looks.mjs +16 -15
  234. package/tests/craft_mode_nav.mjs +31 -29
  235. package/tests/craft_review_mode.mjs +46 -41
  236. package/tests/crafts_vocabulary.mjs +70 -0
  237. package/tests/cross_discipline_priority_direction.mjs +5 -5
  238. package/tests/fitness.mjs +7 -11
  239. package/tests/funded_project_pays_proof_db.mjs +1 -1
  240. package/tests/genesis_home.mjs +21 -1
  241. package/tests/github_chip.mjs +3 -3
  242. package/tests/github_repo_invite.mjs +379 -0
  243. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  244. package/tests/goal_invite_consent.mjs +1 -1
  245. package/tests/goal_map_page.mjs +2 -2
  246. package/tests/goal_sort_staleness.mjs +1 -1
  247. package/tests/goal_spec_task.mjs +1 -1
  248. package/tests/goal_spec_task_db.mjs +3 -3
  249. package/tests/grade_engine.mjs +2 -2
  250. package/tests/grader_no_artifact.mjs +1 -1
  251. package/tests/grader_prompt_fencing.mjs +1 -1
  252. package/tests/grader_prompt_window_bound.mjs +1 -1
  253. package/tests/grader_reader_lens.mjs +2 -2
  254. package/tests/grader_severity_gate.mjs +1 -1
  255. package/tests/grader_unbacked_score.mjs +1 -1
  256. package/tests/hall_approval_queue.mjs +1 -1
  257. package/tests/hall_board_world.mjs +2 -2
  258. package/tests/hall_builders_page_pass.mjs +1 -1
  259. package/tests/hall_city.mjs +1 -1
  260. package/tests/hall_fullscreen.mjs +27 -3
  261. package/tests/hall_kit_charts.mjs +1 -1
  262. package/tests/hall_landing_boot.mjs +1 -1
  263. package/tests/hall_my_agents.mjs +16 -16
  264. package/tests/hall_nav.mjs +22 -15
  265. package/tests/hall_options_menu.mjs +247 -0
  266. package/tests/hall_oversight_world.mjs +1 -1
  267. package/tests/hall_palette.mjs +8 -9
  268. package/tests/hall_profile_world.mjs +3 -3
  269. package/tests/hall_record_world.mjs +2 -2
  270. package/tests/hall_settings_world.mjs +2 -1
  271. package/tests/hall_sitemap.mjs +6 -5
  272. package/tests/hall_start_screen.mjs +244 -0
  273. package/tests/hall_street.mjs +2 -2
  274. package/tests/hall_street_inside.mjs +17 -9
  275. package/tests/hall_studio_world.mjs +3 -3
  276. package/tests/hall_visor.mjs +224 -0
  277. package/tests/help_requests.mjs +47 -15
  278. package/tests/helpers.mjs +4 -4
  279. package/tests/home_city.mjs +6 -4
  280. package/tests/home_city_widget.mjs +33 -14
  281. package/tests/home_gallery_widget.mjs +4 -4
  282. package/tests/home_idea_box.mjs +6 -6
  283. package/tests/hub_community_page.mjs +3 -3
  284. package/tests/hub_craft_rollup.mjs +75 -36
  285. package/tests/hub_recent_ships.mjs +3 -2
  286. package/tests/idea_cascade.mjs +2 -2
  287. package/tests/idea_credit_streams_db.mjs +1 -1
  288. package/tests/idea_detail_page.mjs +1 -1
  289. package/tests/idea_ideate_full.mjs +5 -2
  290. package/tests/idea_spark_hall.mjs +1 -1
  291. package/tests/init.mjs +3 -3
  292. package/tests/instance_manifest.mjs +2 -2
  293. package/tests/kickoff_checklist.mjs +1 -1
  294. package/tests/leaderboard_roles.mjs +34 -25
  295. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  296. package/tests/lightbulb.mjs +14 -10
  297. package/tests/main_discipline.mjs +8 -8
  298. package/tests/markdown_line_endings.mjs +1 -1
  299. package/tests/market_watch_pages.mjs +52 -74
  300. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  301. package/tests/migration_allocation.mjs +1 -1
  302. package/tests/mingle.mjs +36 -21
  303. package/tests/module-scope-map.mjs +22 -2
  304. package/tests/module_api.mjs +1 -0
  305. package/tests/module_contributions.mjs +13 -13
  306. package/tests/module_manifest.mjs +29 -0
  307. package/tests/module_skills_hook.mjs +2 -2
  308. package/tests/modules.mjs +14 -7
  309. package/tests/nav_permission_atoms.mjs +2 -1
  310. package/tests/needs_rebase.mjs +1 -1
  311. package/tests/newcomer_restock.mjs +13 -13
  312. package/tests/page_notes_panel.mjs +22 -22
  313. package/tests/page_tweak_no_block.mjs +1 -1
  314. package/tests/paintbrush.mjs +7 -7
  315. package/tests/privacy_matrix.mjs +1 -0
  316. package/tests/profile_activity.mjs +20 -23
  317. package/tests/profile_earnings.mjs +1 -1
  318. package/tests/profile_nudge_prefs.mjs +4 -4
  319. package/tests/profile_role_headline.mjs +73 -46
  320. package/tests/profile_role_stats.mjs +55 -45
  321. package/tests/profile_rollup_consent.mjs +19 -6
  322. package/tests/profile_route.mjs +12 -12
  323. package/tests/profile_ui_cross_project.mjs +4 -4
  324. package/tests/project_invite_suggestions.mjs +2 -2
  325. package/tests/promote_goal_id.mjs +1 -1
  326. package/tests/publish_manifest.mjs +1 -1
  327. package/tests/rank_floor_wiring.mjs +1 -1
  328. package/tests/regrade_actual_minutes.mjs +1 -1
  329. package/tests/removal_proposal.mjs +2 -2
  330. package/tests/reward_gate.mjs +4 -4
  331. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  332. package/tests/role_responsibilities.mjs +18 -12
  333. package/tests/role_stats_db.mjs +17 -16
  334. package/tests/scouting_rollup_fanout.mjs +5 -5
  335. package/tests/seed_tasks.mjs +5 -2
  336. package/tests/session_earnings_mirror_db.mjs +1 -1
  337. package/tests/session_reward_on_land_db.mjs +1 -1
  338. package/tests/session_reward_reconcile_db.mjs +2 -2
  339. package/tests/sessions_kit_pager.mjs +1 -1
  340. package/tests/settings_art_key_absent.mjs +2 -2
  341. package/tests/settings_main_role.mjs +21 -20
  342. package/tests/setup_flow.mjs +3 -3
  343. package/tests/setup_smoke.mjs +20 -0
  344. package/tests/ship_card.mjs +6 -6
  345. package/tests/specialities.mjs +16 -4
  346. package/tests/speciality_db_shape.mjs +6 -6
  347. package/tests/speciality_injection.mjs +17 -17
  348. package/tests/speciality_offer_ui.mjs +5 -5
  349. package/tests/speciality_offered.mjs +6 -5
  350. package/tests/speciality_session_skills.mjs +7 -7
  351. package/tests/speciality_skills.mjs +11 -11
  352. package/tests/speciality_walkthrough_ui.mjs +2 -2
  353. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  354. package/tests/task_classifier_vocab.mjs +10 -0
  355. package/tests/task_cli.mjs +11 -3
  356. package/tests/task_criteria_unlink_db.mjs +2 -2
  357. package/tests/task_discipline_required.mjs +38 -16
  358. package/tests/task_goal_required.mjs +4 -4
  359. package/tests/task_needs_db.mjs +1 -1
  360. package/tests/tweak_replaces_artist_review.mjs +1 -1
  361. package/tests/update_pill.mjs +153 -2
  362. package/tests/upkeep_home.mjs +1 -1
  363. package/tests/v3_c1_options_proof.mjs +145 -0
  364. package/tests/visual_review.mjs +3 -3
  365. package/tests/watch_applications_queue.mjs +8 -5
  366. package/tests/watch_sealed_floor.mjs +3 -1
  367. package/tests/welcome_boot.mjs +12 -9
  368. package/tests/welcome_boot_browser.mjs +13 -13
  369. package/tests/worktree_helper.mjs +16 -1
@@ -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) => {
@@ -217,6 +217,12 @@ module.exports = function buildMeRouter() {
217
217
  const openPayoutHoldsP = (async () => {
218
218
  try { return await require('../payout-gates').openHoldsFor(req.builder.id); } catch (_) { return null; }
219
219
  })();
220
+ // task 1005147: the character this builder holds, or null (not picked yet — the
221
+ // hall then shows character creation). Fail-open to null: a core that runs
222
+ // before core_297 has no column, and that must never break /me.
223
+ const characterP = (async () => {
224
+ try { return await db.getBuilderCharacter(req.builder.id); } catch (_) { return null; }
225
+ })();
220
226
  const stuckPageTweaksP = (async () => {
221
227
  try {
222
228
  const port = seams.resolveOptional('copy-desk.stuckPageTweaksFor');
@@ -476,6 +482,9 @@ module.exports = function buildMeRouter() {
476
482
  // when lifecycle is disabled or the read failed, and the shell treats null
477
483
  // as OFF — a mode that cannot be read is not on.
478
484
  social: socialMode,
485
+ // task 1005147: the builder's character ('artisan' | 'artist' | 'patron'),
486
+ // or null until they pick one. Identity and view only; rank still gates.
487
+ character: await characterP,
479
488
  // task 1004422 (BV2.PS03, goal 1000121): PROJECT FOUNDING MODE. Genesis is a
480
489
  // mode the EXISTING hall wears (the blend rule, docs/design/project-startup-
481
490
  // direction.md), so it rides /me exactly like social: the shell stamps
@@ -724,6 +733,30 @@ module.exports = function buildMeRouter() {
724
733
  }
725
734
  });
726
735
 
736
+ // task 1005147 (BONGOS-V3 G2): character creation. Body: { character } —
737
+ // 'artisan' | 'artist' | 'patron', or null to clear it (the hall clears it and
738
+ // signs out: switching character is signing out and back in, spec settled point 2).
739
+ // Identity and view only: no claim, vote, deploy or spend gate reads it
740
+ // (criterion character-is-identity-not-authority). 422 invalid_character otherwise.
741
+ // rank: any-builder — a builder writes only their own row (req.builder.id), the
742
+ // /me/disciplines posture.
743
+ router.put('/me/character', auth.requireBuilder, async (req, res) => {
744
+ // Nullable by the validator's default: null is the deliberate clear.
745
+ if (validateOrRespond(req, res, { character: { type: 'string', maxLength: 32 } }, { strict: true })) return;
746
+ const body = req.body || {};
747
+ if (!Object.prototype.hasOwnProperty.call(body, 'character')) {
748
+ return res.fail('character_required', 400);
749
+ }
750
+ const raw = body.character;
751
+ try {
752
+ const character = await db.setBuilderCharacter(req.builder.id, raw);
753
+ res.json({ character });
754
+ } catch (err) {
755
+ if (err.code === 'invalid_character') return res.fail('invalid_character', 422);
756
+ throw err;
757
+ }
758
+ });
759
+
727
760
  // V3.R10 (#235), criterion C3 — link the authed builder's Anthropic account.
728
761
  // Body: { anthropic_email, anthropic_user_id }. Self-attestation: the builder's
729
762
  // own Claude Code reads ~/.claude.json oauthAccount during /builder-setup,
@@ -965,18 +965,35 @@ function mountInternalSurfaces(app) {
965
965
  return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/roster${qs}`);
966
966
  });
967
967
 
968
- // task 1005045 (owner decision 2026-11-03, idea 1001310): the owner's Market Watch
969
- // settings left their own page (task 1004938) for an owner-only Settings tab on
970
- // /market-watch, so the sidebar says "Market Watch" once. The old URL lands on that
971
- // tab; 302 for the same reason as the directory above, and no page gate: the redirect
972
- // discloses nothing, /market-watch has its own, and the settings routes are the wall.
973
- const RETIRED_MW_SETTINGS_RE = /^\/builders\/market-watch-settings(?:\.html)?\/?$/;
968
+ // THE ONE OPTIONS MENU (task 1005144, BONGOS-V3 R04, ADR 0401). Every setting is
969
+ // reached from Settings, which is the options menu now; the screens that were pages
970
+ // of their own open INSIDE it, in a frame that asks for the old address with
971
+ // ?embed=1. A plain visit to an old address (a bookmark, a doc, a Discord message)
972
+ // lands on the menu with that screen open (?open=<entry>), and the browser carries
973
+ // the old #anchor across the redirect because the Location names none. The framed
974
+ // request passes through to the page and its own page gate below. 302 and no gate,
975
+ // as the redirects that follow: the redirect discloses nothing, Settings has its own
976
+ // sign-in gate, and each screen keeps its page gate and its routes (ADR 0016).
977
+ // options-menu.js RETIRED is this map's twin (tests/hall_options_menu.mjs holds them
978
+ // equal). market-watch-settings was the owner's tab on /market-watch (task 1005045)
979
+ // and a page before that; it has no page left, so it never passes through.
980
+ const RETIRED_TO_OPTIONS = Object.freeze({
981
+ 'my-agents': 'my-agents',
982
+ 'project-settings': 'project-settings',
983
+ modules: 'modules',
984
+ government: 'permissions',
985
+ 'market-watch-rules': 'market-watch-rules',
986
+ 'market-watch-settings': 'market-watch',
987
+ });
988
+ const RETIRED_OPTIONS_RE = /^\/builders\/(my-agents|project-settings|modules|government|market-watch-rules|market-watch-settings)(?:\.html)?\/?$/;
974
989
  app.use((req, res, next) => {
975
990
  if (isModuleSurfaceHost(req)) return next();
976
991
  if (req.method !== 'GET' && req.method !== 'HEAD') return next();
977
- if (!RETIRED_MW_SETTINGS_RE.test(path.posix.normalize(req.path))) return next();
978
- const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
979
- return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/market-watch${qs}#settings`);
992
+ const m = RETIRED_OPTIONS_RE.exec(path.posix.normalize(req.path));
993
+ if (!m) return next();
994
+ const embed = new URLSearchParams(req.url.includes('?') ? req.url.slice(req.url.indexOf('?') + 1) : '').get('embed');
995
+ if (embed === '1' && m[1] !== 'market-watch-settings') return next();
996
+ return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/settings?open=${RETIRED_TO_OPTIONS[m[1]]}`);
980
997
  });
981
998
 
982
999
  // Pages folded into another page; 302 and no gate, as above. Task 1005047: Fleet costs is
package/src/crafts.js ADDED
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+ // src/crafts.js — the craft vocabulary, in one place (task 1005141, BONGOS-V3 C1).
3
+ //
4
+ // The game conversion (docs/specs/bongos-v3-game-conversion.md, "Task outline / G1",
5
+ // settled point 6) names the three characters a builder holds:
6
+ //
7
+ // artisan — builds: code, infra, Bongos itself (was 'engineer')
8
+ // artist — how the project looks and reads, AND its ideas (was 'artist' + 'ideator')
9
+ // patron — watches, proposes, and (with a seat) governs (was the 'governing' craft mode)
10
+ //
11
+ // 'ui' stays a module-contributed discipline (ADR 0272); it is not a character.
12
+ //
13
+ // THE OLD SPELLINGS STILL ARRIVE. A rolled-back release, a co-tenant instance on an
14
+ // older core (federated craft rollups), an old CLI, a bookmarked ?discipline=engineer
15
+ // and a help request's historical 'builder' all still say the old word. Every input
16
+ // boundary runs a value through normalizeCraft() so the old word lands as the new one;
17
+ // nothing downstream should ever compare against 'engineer' or 'ideator' again. The
18
+ // database keeps ACCEPTING the old words too (core_296's CHECKs are a superset) because
19
+ // `bongos upgrade` rolls code back without rolling the database back — a contraction
20
+ // that drops them is a later, separate migration.
21
+ //
22
+ // The crafts a TASK can carry are artisan and artist. Patron is a character, not a
23
+ // kind of work: a task is built (artisan) or made/imagined (artist), and governing
24
+ // work is a vote, not a task.
25
+ //
26
+ // ONLY THE TWO FUNCTIONS ARE EXPORTED. A list of the crafts, display labels and a
27
+ // label helper were exported here once and nothing read them, which the dead-code
28
+ // ratchet (knip) counts as a regression. Add one back when a caller needs it.
29
+
30
+ // Old spelling → new spelling. 'builder' is the help-request spelling of the
31
+ // engineering craft (ADR 0174); 'building'/'artistry'/'ideating'/'governing' are the
32
+ // hall's old craft-mode ids (modules/hall-ui/public/craft-mode.js).
33
+ const LEGACY = Object.freeze({
34
+ engineer: 'artisan',
35
+ engineering: 'artisan',
36
+ builder: 'artisan',
37
+ building: 'artisan',
38
+ ideator: 'artist',
39
+ ideating: 'artist',
40
+ artistry: 'artist',
41
+ governing: 'patron',
42
+ governor: 'patron',
43
+ ruler: 'patron',
44
+ });
45
+
46
+ // Any spelling → today's. Non-strings pass through untouched (callers validate);
47
+ // an unknown word passes through too, so a module-contributed craft is never eaten.
48
+ function normalizeCraft(value) {
49
+ if (typeof value !== 'string') return value;
50
+ const v = value.trim().toLowerCase();
51
+ return Object.prototype.hasOwnProperty.call(LEGACY, v) ? LEGACY[v] : v;
52
+ }
53
+
54
+ // A list of crafts → today's spellings, deduped in first-seen order. The fold matters:
55
+ // ['artist', 'ideator'] is one craft now, not two.
56
+ function normalizeCrafts(list) {
57
+ if (!Array.isArray(list)) return list;
58
+ const out = [];
59
+ for (const item of list) {
60
+ const c = normalizeCraft(item);
61
+ if (!out.includes(c)) out.push(c);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ module.exports = { normalizeCraft, normalizeCrafts };
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.78'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.80'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -134,6 +134,11 @@ module.exports = {
134
134
  // sleep(ms) — the one server-side "wait N milliseconds" (a Promise over setTimeout).
135
135
  // The browser twin is window.OTB.sleep; fitness fails on a third copy (task 1004893).
136
136
  sleep,
137
+ // crafts — the craft vocabulary (artisan · artist · patron, plus module 'ui') and
138
+ // normalizeCraft/normalizeCrafts, which turn an old spelling ('artisan',
139
+ // 'artist', 'builder') into today's. Every craft read from outside goes through
140
+ // it (task 1005141, src/crafts.js).
141
+ get crafts() { return require('./crafts'); },
137
142
 
138
143
  // --- auth & rank (ADR 0016) — the gates the loader composes around a module's
139
144
  // routes; a module declares the rank it needs and never wires auth itself.
@@ -16,6 +16,9 @@
16
16
  // // what update, rollout channels and re-assessment name.
17
17
  // "coreVersion": "^1.0.0", // REQUIRED. semver range of src/module-api.js CORE_VERSION it supports.
18
18
  // "default": false, // OPTIONAL (default false). Enablement fail-safe when no config names it.
19
+ // "language": "javascript", // OPTIONAL (default "javascript", task 1004794). What the module's code is
20
+ // // written in — one of MODULE_LANGUAGES. A closed list: only languages
21
+ // // the core can actually run are on it.
19
22
  // "author": "…", // OPTIONAL. Attribution (ADR 0107 §2) — portable, not a cross-GDS FK.
20
23
  // "origin": "…", // OPTIONAL. Origin instance/project this module was built in.
21
24
  // "license": "…", // OPTIONAL. e.g. "AGPL-3.0" — the license this module is offered under.
@@ -138,6 +141,14 @@ function isClaudeArtifactUrl(v) {
138
141
  return u.protocol === 'https:' && !u.username && !u.password && (host === 'claude.ai' || host.endsWith('.claude.ai'));
139
142
  }
140
143
 
144
+ // The languages a module may declare (task 1004794). Closed on purpose: a value is on
145
+ // this list only once the core can RUN a module written in it, so a manifest that says
146
+ // "python" today fails loudly at validation instead of being loaded as JavaScript and
147
+ // failing somewhere far from the cause. How a separate-program module runs beside
148
+ // Bongos is decided in its own ADR (task 1004797); that work adds its languages here.
149
+ const MODULE_LANGUAGES = Object.freeze(['javascript']);
150
+ const DEFAULT_MODULE_LANGUAGE = 'javascript';
151
+
141
152
  const MAINTENANCE_STATUSES = ['core-maintained', 'maintained', 'deprecated', 'orphaned'];
142
153
  const MAINTENANCE_KEYS = ['status', 'since', 'removeAfter', 'successor', 'note'];
143
154
  // Statuses that describe a module on its way out — these owe a deprecation path.
@@ -213,6 +224,11 @@ function validateManifest(obj) {
213
224
  // --- optional default ---
214
225
  if ('default' in obj && typeof obj.default !== 'boolean') E('default: must be a boolean when present');
215
226
 
227
+ // --- optional language (task 1004794) — absent means "javascript" ---
228
+ if ('language' in obj && !MODULE_LANGUAGES.includes(obj.language)) {
229
+ E(`language: ${JSON.stringify(obj.language)} is not a supported module language (allowed: ${MODULE_LANGUAGES.join(', ')}; omit it for "${DEFAULT_MODULE_LANGUAGE}")`);
230
+ }
231
+
216
232
  // --- optional attribution (ADR 0107 §2 — portable credit, not a cross-GDS FK) ---
217
233
  for (const k of ATTRIBUTION_KEYS) {
218
234
  if (k in obj && !isStr(obj[k])) E(`${k}: must be a non-empty string when present`);
package/src/modules.js CHANGED
@@ -86,7 +86,7 @@ const MODULE_REGISTRY = {};
86
86
  // shipping a pixel-art pipeline. Making it core aligns the one outlier list with
87
87
  // the DB CHECK and keeps the copyright exclusion untouched. Order mirrors the
88
88
  // migration 176 enum (engineer | artist | ideator | ui).
89
- const CORE_DISCIPLINES = ['engineer', 'artist', 'ideator'];
89
+ const CORE_DISCIPLINES = ['artisan', 'artist'];
90
90
 
91
91
  const KNOWN_MODULES = Object.keys(MODULE_REGISTRY);
92
92
 
@@ -475,7 +475,7 @@ function isModuleEnabled(name, set = modules()) {
475
475
  const enabledModules = (set = modules()) => knownModules().filter((k) => set[k] === true);
476
476
 
477
477
  // Disciplines this instance offers = core + every enabled module's declared
478
- // disciplines, deduped, stable order. Vanilla = ['engineer','ideator'].
478
+ // disciplines, deduped, stable order. Vanilla = ['artisan','artist'].
479
479
  function enabledDisciplines(set = modules()) {
480
480
  const reg = effectiveRegistry();
481
481
  const out = [...CORE_DISCIPLINES];
@@ -29,15 +29,16 @@
29
29
  'use strict';
30
30
 
31
31
  // discipline key (tasks.discipline / builders.preferred_disciplines) → the owner's
32
- // sentence. Keys mirror CORE_DISCIPLINES order (engineer | artist | ideator), the
33
- // migration 176 enum order. A module-contributed discipline (ui-design → 'ui') has
32
+ // sentence. Keys mirror CORE_DISCIPLINES order (artisan | artist), the
33
+ // core_296 order. A module-contributed discipline (ui-design → 'ui') has
34
34
  // no statement here on purpose: a craft a module brings is that module's to
35
35
  // describe, and inventing one for it would be exactly the paraphrase this file
36
36
  // forbids. Consumers must therefore treat "no statement" as normal, not as an error.
37
37
  const ROLE_RESPONSIBILITIES = Object.freeze({
38
- engineer: 'Running and optimizing the running of Claude nonstop, and ensuring Ideators and Artists can continue to interface with that system effectively.',
39
- artist: 'No slop in the appearance and text of the project; the story, emotion and ideology of the project are communicated effectively; project purpose and gravitas are upheld.',
40
- ideator: 'Make good ideas; be a philosopher / thought leader for the project; enable Engineers to scope and Artists to create with maximum efficiency — a baseline for creating scopes of work.',
38
+ artisan: 'Running and optimizing the running of Claude nonstop, and ensuring Artists can continue to interface with that system effectively.',
39
+ // The artist holds both of the owner's sentences since the ideator folded in
40
+ // (task 1005141, BONGOS-V3): the craft names changed, the words did not.
41
+ artist: 'No slop in the appearance and text of the project; the story, emotion and ideology of the project are communicated effectively; project purpose and gravitas are upheld. Make good ideas; be a philosopher / thought leader for the project; enable Artisans to scope and Artists to create with maximum efficiency — a baseline for creating scopes of work.',
41
42
  });
42
43
 
43
44
  // Provenance, carried with the text so every rendering can say where it came from
@@ -111,7 +111,7 @@ try {
111
111
  ]) {
112
112
  await q(
113
113
  `INSERT INTO tasks (id, version_id, title, status, shipped_by, shipped_at, discipline, kind)
114
- VALUES ($1, $2, $3, 'shipped', $4, now(), 'engineer', 'feature')`,
114
+ VALUES ($1, $2, $3, 'shipped', $4, now(), 'artisan', 'feature')`,
115
115
  [id, VER, title, SHIPPER],
116
116
  );
117
117
  }
@@ -57,7 +57,7 @@ const pin = (rel, exports) => {
57
57
  await t('reportActivityRollup sends Bongos-Report-As-Of, stamped before the totals were read', async () => {
58
58
  const senderPool = mockPool((sql) => {
59
59
  if (/FROM builders WHERE id/.test(sql)) {
60
- return { rows: [{ github_id: 42, total_credits: 7, karma: 1, rank: 'metic', preferred_disciplines: ['engineer'] }] };
60
+ return { rows: [{ github_id: 42, total_credits: 7, karma: 1, rank: 'metic', preferred_disciplines: ['artisan'] }] };
61
61
  }
62
62
  if (/count\(\*\)/.test(sql)) return { rows: [{ n: 3 }] };
63
63
  return { rows: [] };
@@ -89,7 +89,7 @@ test('the exclusions are real and each one is reachable', () => {
89
89
  'docs/architecture.md',
90
90
  'docs/canonical-permissions.md',
91
91
  'docs/recipes/parallel-goal-waves.md',
92
- 'docs/packs/engineer.md',
92
+ 'docs/packs/artisan.md',
93
93
  'docs/onboarding/drachmae-and-karma.md',
94
94
  ]) {
95
95
  assert.equal(scan.isExcludedPath(p), false, `${p} must NOT be excluded`);
@@ -112,11 +112,11 @@ test('docs/ is SPLIT: the frozen record stays out, live instruction is now count
112
112
  'docs/governance/rank-map-2026-08-06.md': 'GET /api/gds/tasks\n',
113
113
  'docs/recipes/failed-ship-recovery-proof.md': 'POST /api/gds/claims/1/resolve\n',
114
114
  'docs/recipes/how-to-do-a-thing.md': 'node scripts/bongos/api.js GET /api/gds/tasks/1\n',
115
- 'docs/packs/engineer.md': 'create a task via POST /api/gds/tasks\n',
115
+ 'docs/packs/artisan.md': 'create a task via POST /api/gds/tasks\n',
116
116
  }));
117
117
  assert.deepEqual(
118
118
  Object.keys(r.byFile).sort(),
119
- ['docs/packs/engineer.md', 'docs/recipes/how-to-do-a-thing.md'],
119
+ ['docs/packs/artisan.md', 'docs/recipes/how-to-do-a-thing.md'],
120
120
  'only the instructional docs count — and they MUST count, or the narrowing did nothing',
121
121
  );
122
122
  assert.equal(r.total, 2);
@@ -661,7 +661,7 @@ test('list-pending: the real vouch writer’s row resolves its profile and guild
661
661
 
662
662
  test('list-pending: a vouched TAKEOVER resolves the vouched applicant, not whoever filed first', async () => {
663
663
  reset();
664
- hubViews['8001'] = { profile: { view: 'sliver', sliver: { disciplines: ['engineer'] } }, guild: null };
664
+ hubViews['8001'] = { profile: { view: 'sliver', sliver: { disciplines: ['artisan'] } }, guild: null };
665
665
  await applyAs('ada', { [VOUCH_HEADER]: vouchFor('ada', { githubId: 8001, handle: 'ada-h' }) });
666
666
  const r = await call('GET', '/api/gds/access-requests');
667
667
  const ada = r.json.requests.find((x) => x.github_login === 'ada');
@@ -214,7 +214,7 @@ test('the panel names a public page as site:<first path segment>, and its ideas
214
214
 
215
215
  test('the public button and the hall palette wear one name', () => {
216
216
  const craft = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/craft-mode.js'), 'utf8');
217
- const tool = /id: 'palette', icon: 'artistry', short: '([^']+)', label: '([^']+)'/.exec(craft);
217
+ const tool = /id: 'palette', icon: '[^']+', short: '([^']+)', label: '([^']+)'/.exec(craft);
218
218
  assert.ok(tool, 'the palette tool');
219
219
  assert.ok(LOADER.includes("var SHORT = '" + tool[1] + "';"), 'the short name');
220
220
  assert.ok(LOADER.includes("var LABEL = '" + tool[2] + "';"), 'the full name');
@@ -100,7 +100,7 @@ await test('createTask with artistReview lands in the goal; without one the catc
100
100
  await createTask({ versionId: 'BONGOS-V3', title: 'Tweak: Home (builders:home)', discipline: 'artist', artistReview: true }, { client: c });
101
101
  assert.equal(c.state.tasks[0].goal_id, 91, 'the studio task names the artist review goal');
102
102
  await assert.rejects(
103
- () => createTask({ versionId: 'BONGOS-V3', title: 'plain', discipline: 'engineer' }, { client: fakeClient() }),
103
+ () => createTask({ versionId: 'BONGOS-V3', title: 'plain', discipline: 'artisan' }, { client: fakeClient() }),
104
104
  /goalId is required/, 'an ordinary goal-less create still throws');
105
105
  });
106
106
 
@@ -221,6 +221,21 @@ test('the supervisor runs under the runner\'s own config root, and keeps a gauge
221
221
  assert.equal(host.supervisorEnv({ AUTOBONGOS_GAUGE_FILE: '/set/by/hand' }, paths).AUTOBONGOS_GAUGE_FILE, '/set/by/hand');
222
222
  });
223
223
 
224
+ test('AUTOBONGOS_HOME moves the runner\'s copy, logs and lock, and never the login', () => {
225
+ // The login must stay where the read-deny rule looks for it (task 1005186): a
226
+ // device someone customised is the last one that should fall outside the rule.
227
+ const home = path.join(tmp, 'fixed-login-home');
228
+ const standard = host.hostPaths({}, home);
229
+ const custom = host.hostPaths({ AUTOBONGOS_HOME: path.join(tmp, 'somewhere-else') }, home);
230
+ assert.equal(standard.configRoot, path.join(home, '.cloudbongos-runner', 'config'));
231
+ assert.equal(custom.configRoot, standard.configRoot, 'the same folder under the person\'s home, whatever the variable says');
232
+ assert.equal(custom.root, path.join(tmp, 'somewhere-else'));
233
+ assert.equal(custom.log, path.join(tmp, 'somewhere-else', 'logs', 'autobongos.log'));
234
+ assert.equal(custom.lock, path.join(tmp, 'somewhere-else', 'host.json'));
235
+ // And that is the folder the supervisor and its workers are pointed at.
236
+ assert.equal(host.supervisorEnv({ AUTOBONGOS_HOME: path.join(tmp, 'somewhere-else') }, custom).BONGOS_CONFIG_ROOT, standard.configRoot);
237
+ });
238
+
224
239
  test('under BONGOS_CONFIG_ROOT every session path moves, and the builder\'s own session is not a fallback', () => {
225
240
  const root = fresh('cfgroot');
226
241
  const probe = `
@@ -636,7 +651,10 @@ test('the digest is read from the RUNNER’s run log: bare, the digest CLI looks
636
651
  assert.deepEqual(argv.slice(1), ['--all', '--json'], 'whatever the person passed is handed through');
637
652
 
638
653
  // Through the device program: the runner's night.
639
- const viaHost = spawnSync(process.execPath, [HOST_JS, 'digest', '--all', '--json'], { encoding: 'utf8', env: { ...process.env, AUTOBONGOS_HOME: home, AUTOBONGOS_LOG_FILE: '' } });
654
+ // The login folder is always under the person's home (task 1005186), so the
655
+ // child is given this test's stand-in home: HOME on a Mac or Linux, USERPROFILE
656
+ // on Windows. Otherwise it would read the real one.
657
+ const viaHost = spawnSync(process.execPath, [HOST_JS, 'digest', '--all', '--json'], { encoding: 'utf8', env: { ...process.env, HOME: home, USERPROFILE: home, AUTOBONGOS_HOME: home, AUTOBONGOS_LOG_FILE: '' } });
640
658
  assert.equal(viaHost.status, 0, viaHost.stderr);
641
659
  assert.ok(viaHost.stdout.includes('424242'), 'the task the runner built is in it');
642
660
 
@@ -183,7 +183,7 @@ async function seed() {
183
183
  // since core_247 (task 1002988); every real create path resolves one.
184
184
  await q(
185
185
  `INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline)
186
- VALUES ($1,$2,'bhlp fixture task','ready',10,'feature','engineer')`,
186
+ VALUES ($1,$2,'bhlp fixture task','ready',10,'feature','artisan')`,
187
187
  [TASK, VER],
188
188
  );
189
189
  }