@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
@@ -78,7 +78,7 @@ const PUBLISH = [
78
78
  'docs/project-context.template.md', // the neutral scaffold (NOT the OTB-filled one)
79
79
  'config/design-tokens.schema.json', // generic contract schema
80
80
  '.claude/skills/status/SKILL.md',
81
- 'docs/packs/engineer.md', // the role packs the kernel/pack split moved the methodology into (task 1002990)
81
+ 'docs/packs/artisan.md', // the role packs the kernel/pack split moved the methodology into (task 1002990)
82
82
  'package.json',
83
83
  'Dockerfile',
84
84
  '.env.local.example',
@@ -55,7 +55,7 @@ async function createdFloor({ scopeModules, needsMigration = false, requiresRank
55
55
  // Stated rather than inferred (task 1002988): createTask now refuses a craft it
56
56
  // cannot work out, and "a task" says nothing. This fixture is about the RANK
57
57
  // FLOOR, so it should not be at the mercy of the discipline heuristic.
58
- discipline: 'engineer',
58
+ discipline: 'artisan',
59
59
  goalId: '1000111',
60
60
  needsMigration,
61
61
  requiresRank,
@@ -43,7 +43,7 @@ const TASK = {
43
43
  id: 1003964,
44
44
  title: 'A re-graded task keeps the first attempt figure',
45
45
  kind: 'bug',
46
- discipline: 'engineer',
46
+ discipline: 'artisan',
47
47
  est_minutes: 60,
48
48
  actual_minutes: 31337,
49
49
  scoping_confidence: 4,
@@ -439,7 +439,7 @@ await test('the generated task is a backlog REQUEST carrying the preamble', () =
439
439
  assert.equal(spec.source, cascade.CASCADE_SOURCE);
440
440
  assert.match(spec.description, /it is a REQUEST/);
441
441
  assert.match(spec.description, /task 1003944/);
442
- assert.equal(spec.discipline, 'engineer');
442
+ assert.equal(spec.discipline, 'artisan');
443
443
  });
444
444
 
445
445
  await test('cascadesFor threads ctx into the body, not just the predicate', () => {
@@ -503,7 +503,7 @@ await test('nothing in the proposal path changes a status or a credit', () => {
503
503
 
504
504
  const shippedTask = (over = {}) => ({
505
505
  id: '7002', version_id: 'BONGOS-V2', goal_id: '1000095',
506
- title: 'Delete the old scorer', discipline: 'engineer',
506
+ title: 'Delete the old scorer', discipline: 'artisan',
507
507
  kind: 'feature', status: 'shipped', source: 'manual', ...over,
508
508
  });
509
509
 
@@ -224,7 +224,7 @@ async function withStubbedCreatePool(run) {
224
224
 
225
225
  t('a task CREATED with NO reward is born with a positive, capped value', async () => {
226
226
  await withStubbedCreatePool(async (captured) => {
227
- const row = await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'engineer', kind: 'feature', estMinutes: 40 });
227
+ const row = await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'artisan', kind: 'feature', estMinutes: 40 });
228
228
  assert.ok(captured.creditsReward > 0, 'INSERT must bind a positive credits_reward');
229
229
  assert.equal(captured.creditsReward, 40, 'feature @40min -> 40 drachmae');
230
230
  assert.ok(captured.creditsReward <= db.REWARD_SUGGESTION_CAP, 'must respect the cap');
@@ -234,7 +234,7 @@ t('a task CREATED with NO reward is born with a positive, capped value', async (
234
234
 
235
235
  t('a task CREATED with credits_reward 0 (the route default) is auto-assigned', async () => {
236
236
  await withStubbedCreatePool(async (captured) => {
237
- await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'engineer', creditsReward: 0, kind: 'bug', estMinutes: 40 });
237
+ await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'artisan', creditsReward: 0, kind: 'bug', estMinutes: 40 });
238
238
  // bug weight 1.25 -> 50; 0 is treated as "unset" so the default kicks in.
239
239
  assert.equal(captured.creditsReward, 50);
240
240
  });
@@ -242,14 +242,14 @@ t('a task CREATED with credits_reward 0 (the route default) is auto-assigned', a
242
242
 
243
243
  t('a huge-est new task is CAPPED at creation, not ballooned', async () => {
244
244
  await withStubbedCreatePool(async (captured) => {
245
- await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'engineer', kind: 'feature', estMinutes: 9999 });
245
+ await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'artisan', kind: 'feature', estMinutes: 9999 });
246
246
  assert.equal(captured.creditsReward, db.REWARD_SUGGESTION_CAP);
247
247
  });
248
248
  });
249
249
 
250
250
  t('an EXPLICIT positive credits_reward is PRESERVED (never overridden)', async () => {
251
251
  await withStubbedCreatePool(async (captured) => {
252
- await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'engineer', creditsReward: 17, kind: 'feature', estMinutes: 40 });
252
+ await db.createTask({ versionId: 1, goalId: 77, title: 't', discipline: 'artisan', creditsReward: 17, kind: 'feature', estMinutes: 40 });
253
253
  assert.equal(captured.creditsReward, 17, 'author-set positive value must survive untouched');
254
254
  });
255
255
  });
@@ -80,7 +80,7 @@ try {
80
80
  await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'rm-w','rm-worker','RM Worker','archon')`, [WORKER]);
81
81
  // A confirmed task owned by WORKER with a real estimate. In DEFAULT mode this
82
82
  // would pay 60 × 1.0 (feature) at ship; in cost-plus-only it must pay nothing.
83
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'rm task','confirmed',60,'feature',$3,'engineer')`, [TASK, VER, WORKER]);
83
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'rm task','confirmed',60,'feature',$3,'artisan')`, [TASK, VER, WORKER]);
84
84
  await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [TASK, WORKER]);
85
85
 
86
86
  // Drive the REAL ship transition (the same call the reconciler/ship.js use).
@@ -39,21 +39,27 @@ function test(name, fn) {
39
39
 
40
40
  // --- the source ---------------------------------------------------------
41
41
 
42
- // The owner's sentences, verbatim from criterion wa6-written-responsibilities.
43
- // Duplicated here deliberately — a test that read the value it is checking would
44
- // assert nothing. This copy is the fixture; the source file is the subject.
42
+ // The owner's sentences, verbatim from criterion wa6-written-responsibilities,
43
+ // as carried through the BONGOS-V3 craft rename (task 1005141): the engineer is the
44
+ // ARTISAN, and the ideator folded into the ARTIST, so the artist's statement is the
45
+ // old artist sentence followed by the old ideator sentence. Only craft NAMES moved
46
+ // ("Ideators and Artists" → "Artists", "Engineers" → "Artisans"); every other
47
+ // word is the owner's. Duplicated here deliberately — a test that read the value it
48
+ // is checking would assert nothing. This copy is the fixture; the source file is
49
+ // the subject.
45
50
  const OWNER_TEXT = {
46
- engineer: 'Running and optimizing the running of Claude nonstop, and ensuring Ideators and Artists can continue to interface with that system effectively.',
47
- 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.',
48
- 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.',
51
+ artisan: 'Running and optimizing the running of Claude nonstop, and ensuring Artists can continue to interface with that system effectively.',
52
+ 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.'
53
+ + ' 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.',
49
54
  };
50
55
 
51
- test('the three statements are the owner\'s text, character for character', () => {
56
+ test('the two statements are the owner\'s text, character for character', () => {
52
57
  assert.deepEqual({ ...src.ROLE_RESPONSIBILITIES }, OWNER_TEXT);
53
58
  });
54
59
 
55
- test('exactly the three CORE crafts carry a statement — no more, no fewer', () => {
56
- assert.deepEqual(Object.keys(src.ROLE_RESPONSIBILITIES), ['engineer', 'artist', 'ideator']);
60
+ test('exactly the two CORE crafts carry a statement — no more, no fewer', () => {
61
+ assert.deepEqual(Object.keys(src.ROLE_RESPONSIBILITIES), ['artisan', 'artist']);
62
+ assert.equal(src.responsibilityFor('ideator'), null, 'there is no ideator statement: it lives in the artist\'s');
57
63
  // Governor is deferred (ADR 0274) and `ui` is module-contributed (ADR 0272):
58
64
  // inventing a sentence for either is the paraphrase this task forbids.
59
65
  assert.equal(src.responsibilityFor('ui'), null);
@@ -65,7 +71,7 @@ test('the map is frozen — a consumer cannot edit the standard at runtime', ()
65
71
  });
66
72
 
67
73
  test('responsibilityFor: case-insensitive, and absence is null not a placeholder', () => {
68
- assert.equal(src.responsibilityFor('ENGINEER'), OWNER_TEXT.engineer);
74
+ assert.equal(src.responsibilityFor('ARTISAN'), OWNER_TEXT.artisan);
69
75
  for (const bad of [null, undefined, '', 'unclassified', 42, {}]) {
70
76
  assert.equal(src.responsibilityFor(bad), null, `${JSON.stringify(bad)} must be null`);
71
77
  }
@@ -157,9 +163,9 @@ test('no consumer hardcodes a statement — the packs\' generated blocks are the
157
163
  // criterion record, or this fixture is a second copy that can drift.
158
164
  const ALLOWED = new Set([
159
165
  'src/role-responsibilities.js', // the source
160
- 'docs/packs/engineer.md', // generated blocks
166
+ 'docs/packs/artisan.md', // generated blocks
167
+ 'docs/packs/artist.md',
161
168
  'docs/packs/artist.md',
162
- 'docs/packs/ideator.md',
163
169
  'tests/role_responsibilities.mjs', // this fixture
164
170
  ]);
165
171
  const scan = ['src', 'scripts', 'modules', 'tests', 'docs/packs'];
@@ -102,7 +102,7 @@ async function fixture() {
102
102
  const other = await insertRow('builders', { github_id: `${tag}-gh2`, github_login: `${tag}-2`, display_name: `${tag}-2`, total_credits: 0 });
103
103
  const version = await insertRow('versions', { id: `${tag}-v`, status: 'building' });
104
104
  const mk = (title, discipline) => insertRow('tasks', { title: `${tag} ${title}`, status: 'shipped', version_id: version.id, discipline });
105
- const eng = await mk('eng', 'engineer');
105
+ const eng = await mk('eng', 'artisan');
106
106
  const page = await mk('page', 'artist');
107
107
  const ui = await mk('ui', 'ui');
108
108
  const sid = `${tag}-session`;
@@ -139,23 +139,24 @@ async function main() {
139
139
  await test('credits by craft: every reason lands in its craft and the parts sum to the ledger exactly', async () => {
140
140
  const map = await byCraft.creditsByCraft([f.b.id], { client });
141
141
  const row = map.get(String(f.b.id));
142
- // engineer: 10 shipped + half of the 9 session + the 3 lone session = 17.5
143
- // artist: 30 page + the ui half of the session (4.5) = 34.5
144
- // ideator: 4 stream 1 + 5 full pass = 9
142
+ // artisan: 10 shipped + half of the 9 session + the 3 lone session = 17.5
143
+ // artist: 30 page + the ui half of the session (4.5)
144
+ // + the idea streams the old ideator craft held
145
+ // (4 stream 1 + 5 full pass), folded in by task 1005141 = 43.5
145
146
  // other: 2 achievement + 7 wildcard twin = 9
146
- assert.equal(row.ideator, 9);
147
147
  assert.equal(row.other, 9);
148
148
  assert.equal(row.total, 70);
149
- assert.equal(row.engineer + row.artist + row.ideator + row.other, row.total, 'the parts are exact to the whole');
150
- // 17.5 and 34.5 floor to 51 of 52; the one left over goes to the larger
151
- // remainder, and on a tie to the earlier craft, engineer.
152
- assert.equal(row.engineer, 18);
153
- assert.equal(row.artist, 34);
149
+ assert.equal(row.artisan + row.artist + row.other, row.total, 'the parts are exact to the whole');
150
+ // 17.5 and 43.5 floor to 60 of 61; the one left over goes to the larger
151
+ // remainder, and on a tie to the earlier craft, artisan.
152
+ assert.equal(row.artisan, 18);
153
+ assert.equal(row.artist, 43);
154
+ assert.equal(row.ideas, 9, 'the idea credits are still told apart inside the artist craft');
154
155
  });
155
156
 
156
157
  await test('credits by craft: another builder on the same task is never counted', async () => {
157
158
  const map = await byCraft.creditsByCraft([f.other.id], { client });
158
- assert.equal(map.get(String(f.other.id)).engineer, 50);
159
+ assert.equal(map.get(String(f.other.id)).artisan, 50);
159
160
  assert.equal(map.get(String(f.other.id)).total, 50);
160
161
  });
161
162
 
@@ -177,11 +178,11 @@ async function main() {
177
178
  const { UPDATE_BUILDER_DISCIPLINES_SQL: sql } = require('../src/bongos/db.js');
178
179
  const run = async (list, setMain, main) =>
179
180
  (await client.query(sql, [f.b.id, list, setMain, main])).rows[0].main_discipline;
180
- assert.equal(await run(['engineer', 'artist'], true, 'artist'), 'artist', 'chosen');
181
- assert.equal(await run(['engineer', 'artist', 'ideator'], false, null), 'artist', 'not mentioned, still one of the crafts: kept');
182
- assert.equal(await run(['engineer'], false, null), null, 'the craft was dropped: a main role they no longer practise is cleared');
183
- assert.equal(await run(['engineer'], true, 'engineer'), 'engineer');
184
- assert.equal(await run(['engineer'], true, null), null, 'cleared on purpose');
181
+ assert.equal(await run(['artisan', 'artist'], true, 'artist'), 'artist', 'chosen');
182
+ assert.equal(await run(['artisan', 'artist', 'artist'], false, null), 'artist', 'not mentioned, still one of the crafts: kept');
183
+ assert.equal(await run(['artisan'], false, null), null, 'the craft was dropped: a main role they no longer practise is cleared');
184
+ assert.equal(await run(['artisan'], true, 'artisan'), 'artisan');
185
+ assert.equal(await run(['artisan'], true, null), null, 'cleared on purpose');
185
186
  });
186
187
 
187
188
  await test('artist page read: only rounds whose LATEST web claim is theirs, never a CLI claim, never an ordinary task', async () => {
@@ -82,9 +82,9 @@ try {
82
82
  await q(
83
83
  `INSERT INTO platform_identity_builder_project_activity
84
84
  (client_id, github_id, credits, tasks_shipped, karma, disciplines, last_active)
85
- VALUES ($1, $3, 100, 5, 3, '{engineer,ui}', '2026-01-01T00:00:00Z'),
86
- ($2, $3, 50, 2, 1, '{artist,engineer,ui}', '2026-02-01T00:00:00Z'),
87
- ($1, $4, 60, 4, 2, '{engineer}', '2026-03-01T00:00:00Z'),
85
+ VALUES ($1, $3, 100, 5, 3, '{artisan,ui}', '2026-01-01T00:00:00Z'),
86
+ ($2, $3, 50, 2, 1, '{artist,artisan,ui}', '2026-02-01T00:00:00Z'),
87
+ ($1, $4, 60, 4, 2, '{artisan}', '2026-03-01T00:00:00Z'),
88
88
  ($2, $5, 10, 1, 0, '{}', '2026-04-01T00:00:00Z')`,
89
89
  [...CLIENTS, MULTI, SOLO, GHOST],
90
90
  );
@@ -99,14 +99,14 @@ try {
99
99
  ok(m.total_tasks_shipped === 7, `total_tasks_shipped is 5+2=7 (got ${m.total_tasks_shipped})`);
100
100
  ok(m.total_karma === 4, `total_karma is 3+1=4 (got ${m.total_karma})`);
101
101
  ok(new Date(m.last_active).toISOString() === '2026-02-01T00:00:00.000Z', 'last_active is the max across projects');
102
- assert.deepEqual([...m.disciplines].sort(), ['artist', 'engineer', 'ui']);
102
+ assert.deepEqual([...m.disciplines].sort(), ['artisan', 'artist', 'ui']);
103
103
  pass++; console.log(' ok - disciplines are the deduped union across projects');
104
104
  ok(m.github_login === 'multi', 'account display fields join through');
105
105
 
106
106
  const s = by[SOLO];
107
107
  ok(s.project_count === 1 && s.total_credits === 60 && s.total_tasks_shipped === 4 && s.total_karma === 2,
108
108
  'single-discipline builder totals are unchanged');
109
- assert.deepEqual(s.disciplines, ['engineer']);
109
+ assert.deepEqual(s.disciplines, ['artisan']);
110
110
  pass++; console.log(' ok - single discipline survives as a one-element array');
111
111
 
112
112
  const g = by[GHOST];
@@ -31,6 +31,9 @@ const FIX = path.join(ROOT, 'tests/fixtures/seed-tasks');
31
31
  const { validateSpec, seedTasks } = require(SEEDER);
32
32
 
33
33
  // spec fixture -> the old script it replaced, and the sha256 of that script's write log.
34
+ // These fixtures are HISTORY and keep the pre-1005141 craft spellings ('engineer',
35
+ // 'ideator') on purpose: rewriting them moves the hash. seed-tasks.js passes
36
+ // discipline through verbatim; the task-write route normalizes an old spelling.
34
37
  const HISTORICAL = [
35
38
  { spec: 'bongos-v1-idea-routing.json', old: 'seed-bongos-v1-idea-routing-tasks.js', hash: 'f5bc176890077a62754812cf69666bd49ba0da441dafa84702e1883156c302f8' },
36
39
  { spec: 'bv1-profiles.json', old: 'seed-bv1-profiles-tasks.js', hash: 'cf81ee14131a20fe5a07c05b7c5fe3362bc940d5c5592031ad911b709db129a7' },
@@ -111,8 +114,8 @@ await test('an invalid spec is refused before any write', async () => {
111
114
  const base = () => ({
112
115
  version_id: 'V', goal_id: 7, source_ref_prefix: 'p',
113
116
  tasks: [
114
- { ref: 'R1', title: 't1', description: 'd', kind: 'feature', discipline: 'engineer', priority: 3, criterion_ids: ['slug-a'] },
115
- { ref: 'R2', title: 't2', description: 'd', kind: 'verify', discipline: 'engineer', priority: 3, criterion_ids: ['slug-a'], deps: ['R1'] },
117
+ { ref: 'R1', title: 't1', description: 'd', kind: 'feature', discipline: 'artisan', priority: 3, criterion_ids: ['slug-a'] },
118
+ { ref: 'R2', title: 't2', description: 'd', kind: 'verify', discipline: 'artisan', priority: 3, criterion_ids: ['slug-a'], deps: ['R1'] },
116
119
  ],
117
120
  });
118
121
 
@@ -126,7 +126,7 @@ async function fixture(client) {
126
126
  status: 'shipped',
127
127
  version_id: version.id,
128
128
  // Named, not placeholder-filled: tasks_discipline_check admits only the live crafts.
129
- discipline: 'engineer',
129
+ discipline: 'artisan',
130
130
  });
131
131
  const sessionId = `${tag}-session`;
132
132
  await insertRow(client, 'session_records', {
@@ -61,7 +61,7 @@ await test('real-DB: fixture setup', async () => {
61
61
  [ctx.versionId, `V ${suffix}`]
62
62
  );
63
63
  const tk = await pool.query(
64
- `INSERT INTO tasks (version_id, title, status, discipline) VALUES ($1,$2,'shipped','engineer') RETURNING id`,
64
+ `INSERT INTO tasks (version_id, title, status, discipline) VALUES ($1,$2,'shipped','artisan') RETURNING id`,
65
65
  [ctx.versionId, `shipped task ${suffix}`]
66
66
  );
67
67
  ctx.taskId = Number(tk.rows[0].id);
@@ -61,12 +61,12 @@ await test('real-DB: fixture setup', async () => {
61
61
  [ctx.versionId, `V ${suffix}`]
62
62
  );
63
63
  const shipped = await pool.query(
64
- `INSERT INTO tasks (version_id, title, status, discipline) VALUES ($1,$2,'shipped','engineer') RETURNING id`,
64
+ `INSERT INTO tasks (version_id, title, status, discipline) VALUES ($1,$2,'shipped','artisan') RETURNING id`,
65
65
  [ctx.versionId, `shipped ${suffix}`]
66
66
  );
67
67
  ctx.shippedTaskId = Number(shipped.rows[0].id);
68
68
  const confirmed = await pool.query(
69
- `INSERT INTO tasks (version_id, title, status, discipline) VALUES ($1,$2,'confirmed','engineer') RETURNING id`,
69
+ `INSERT INTO tasks (version_id, title, status, discipline) VALUES ($1,$2,'confirmed','artisan') RETURNING id`,
70
70
  [ctx.versionId, `confirmed ${suffix}`]
71
71
  );
72
72
  ctx.confirmedTaskId = Number(confirmed.rows[0].id);
@@ -123,7 +123,7 @@ function harness({ total = 320, failAfter = null, viewerRank = 'metic', perPageP
123
123
  // the same reason categoryChip is: an undefined helper throws mid-render.
124
124
  roleAxisLabel: () => 'Role',
125
125
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
126
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
126
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
127
127
  roleIsUnset: (d) => !d || d === 'unclassified',
128
128
  ROLE_UNSET: 'unclassified',
129
129
  escapeHtml: (s) => String(s == null ? '' : s),
@@ -52,7 +52,7 @@ async function boot(artKeyStatus) {
52
52
  // The markup ships both hidden (settings.html), so the stub starts them hidden.
53
53
  $('#art-key').hidden = true;
54
54
  $('#artkey-banner').hidden = true;
55
- const builder = { id: 4, github_login: 'b4', rank: 'thetes', preferred_disciplines: ['engineer'] };
55
+ const builder = { id: 4, github_login: 'b4', rank: 'thetes', preferred_disciplines: ['artisan'] };
56
56
  const me = { builder, needs: { items: [needs.artKeyNeed({ rank: 'thetes' })] } };
57
57
  const api = {
58
58
  async request(method, url) {
@@ -74,7 +74,7 @@ async function boot(artKeyStatus) {
74
74
  __BRANDING__: {}, __MODULES__: { enabled: {} },
75
75
  BongosClient: { createClient: () => api },
76
76
  OTB: {
77
- $, escapeHtml, toast() {}, disciplines: () => ['engineer'], roleLabel: (d) => d,
77
+ $, escapeHtml, toast() {}, disciplines: () => ['artisan'], roleLabel: (d) => d,
78
78
  hubOrigin: () => null, settingsPanels: () => [],
79
79
  },
80
80
  OTBKit: { emptyStateHtml: (m) => m, makeSectionRail: () => ({ refresh() {}, activate() {} }) },
@@ -33,7 +33,8 @@ async function t(name, fn) {
33
33
  catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
34
34
  }
35
35
 
36
- const DISCIPLINES = ['engineer', 'artist', 'ideator', 'ui'];
36
+ // The two core crafts since task 1005141 (the ideator folded into the artist), plus ui.
37
+ const DISCIPLINES = ['artisan', 'artist', 'ui'];
37
38
 
38
39
  // A permissive element: everything settings.js touches at boot, with listeners
39
40
  // recorded so a test can fire them.
@@ -120,7 +121,7 @@ async function boot({ builder }) {
120
121
  return { $, options, checked, toggleRole, pickMain, save, patches };
121
122
  }
122
123
 
123
- const B = (over = {}) => ({ id: 4, github_login: 'b4', display_name: 'Builder 4', preferred_disciplines: ['engineer', 'ideator'], main_discipline: null, ...over });
124
+ const B = (over = {}) => ({ id: 4, github_login: 'b4', display_name: 'Builder 4', preferred_disciplines: ['artisan', 'artist'], main_discipline: null, ...over });
124
125
 
125
126
  console.log('\nthe main-role picker:');
126
127
 
@@ -139,39 +140,39 @@ await t('GET /me carries the saved main role (getBuilderById selects the column)
139
140
 
140
141
  await t('options are "Let my work decide" plus exactly the ticked roles; nothing saved → by work', async () => {
141
142
  const p = await boot({ builder: B() });
142
- assert.deepEqual(p.options().map((o) => o.value), ['', 'engineer', 'ideator']);
143
+ assert.deepEqual(p.options().map((o) => o.value), ['', 'artisan', 'artist']);
143
144
  assert.equal(p.options()[0].label, 'Let my work decide');
144
- assert.deepEqual(p.options().map((o) => o.label).slice(1), ['Engineer', 'Ideator']);
145
+ assert.deepEqual(p.options().map((o) => o.label).slice(1), ['Artisan', 'Artist']);
145
146
  assert.equal(p.checked(), '', 'no saved main role → "Let my work decide" is chosen');
146
147
  });
147
148
 
148
149
  await t('a saved main role is the one chosen on load', async () => {
149
- const p = await boot({ builder: B({ main_discipline: 'ideator' }) });
150
- assert.equal(p.checked(), 'ideator');
150
+ const p = await boot({ builder: B({ main_discipline: 'artist' }) });
151
+ assert.equal(p.checked(), 'artist');
151
152
  });
152
153
 
153
154
  await t('ticking a role adds its option; unticking the MAIN role hands the choice back to your work', async () => {
154
- const p = await boot({ builder: B({ main_discipline: 'ideator' }) });
155
- p.toggleRole('artist', true);
156
- assert.deepEqual(p.options().map((o) => o.value), ['', 'engineer', 'artist', 'ideator'], 'in the instance\'s role order');
157
- assert.equal(p.checked(), 'ideator', 'adding a role keeps the pick');
158
- p.toggleRole('ideator', false);
159
- assert.deepEqual(p.options().map((o) => o.value), ['', 'engineer', 'artist']);
155
+ const p = await boot({ builder: B({ main_discipline: 'artist' }) });
156
+ p.toggleRole('ui', true);
157
+ assert.deepEqual(p.options().map((o) => o.value), ['', 'artisan', 'artist', 'ui'], 'in the instance\'s role order');
158
+ assert.equal(p.checked(), 'artist', 'adding a role keeps the pick');
159
+ p.toggleRole('artist', false);
160
+ assert.deepEqual(p.options().map((o) => o.value), ['', 'artisan', 'ui']);
160
161
  assert.equal(p.checked(), '', 'a role you no longer have cannot lead');
161
162
  });
162
163
 
163
164
  await t('Save sends the pick in the same PATCH, and the echo re-seeds it', async () => {
164
165
  const p = await boot({ builder: B() });
165
- p.pickMain('ideator');
166
+ p.pickMain('artist');
166
167
  await p.save();
167
168
  assert.equal(p.patches.length, 1);
168
- assert.deepEqual(JSON.parse(JSON.stringify(p.patches[0])), { preferred_disciplines: ['engineer', 'ideator'], main_discipline: 'ideator' });
169
- assert.equal(p.checked(), 'ideator', 'the server\'s echo is what the control shows after saving');
170
- assert.match(p.$('#craft-status').textContent, /Main role: Ideator\./);
169
+ assert.deepEqual(JSON.parse(JSON.stringify(p.patches[0])), { preferred_disciplines: ['artisan', 'artist'], main_discipline: 'artist' });
170
+ assert.equal(p.checked(), 'artist', 'the server\'s echo is what the control shows after saving');
171
+ assert.match(p.$('#craft-status').textContent, /Main role: Artist\./);
171
172
  });
172
173
 
173
174
  await t('"Let my work decide" saves null, which clears a saved main role', async () => {
174
- const p = await boot({ builder: B({ main_discipline: 'engineer' }) });
175
+ const p = await boot({ builder: B({ main_discipline: 'artisan' }) });
175
176
  p.pickMain('');
176
177
  await p.save();
177
178
  assert.equal(p.patches[0].main_discipline, null);
@@ -180,10 +181,10 @@ await t('"Let my work decide" saves null, which clears a saved main role', async
180
181
  });
181
182
 
182
183
  await t('unticking the main role, then saving, sends null — never a role outside the set', async () => {
183
- const p = await boot({ builder: B({ main_discipline: 'ideator' }) });
184
- p.toggleRole('ideator', false);
184
+ const p = await boot({ builder: B({ main_discipline: 'artist' }) });
185
+ p.toggleRole('artist', false);
185
186
  await p.save();
186
- assert.deepEqual(JSON.parse(JSON.stringify(p.patches[0])), { preferred_disciplines: ['engineer'], main_discipline: null });
187
+ assert.deepEqual(JSON.parse(JSON.stringify(p.patches[0])), { preferred_disciplines: ['artisan'], main_discipline: null });
187
188
  });
188
189
 
189
190
  console.log(`\n${passed} passed, ${failed} failed`);
@@ -77,7 +77,7 @@ function bootGate({ patchOk = true } = {}) {
77
77
  activeElement: null,
78
78
  };
79
79
  // the crafts this project offers (dom-utils.js reads them off __MODULES__)
80
- const window = { OTB: { ...realOTB(), disciplines: () => ['engineer', 'artist', 'ui'] }, location: { reload: () => { reloaded += 1; } } };
80
+ const window = { OTB: { ...realOTB(), disciplines: () => ['artisan', 'artist', 'ui'] }, location: { reload: () => { reloaded += 1; } } };
81
81
  const sandbox = { window, document, setInterval: (fn) => { intervals.push(fn); return intervals.length; }, clearInterval() {} };
82
82
  vm.createContext(sandbox);
83
83
  vm.runInContext(GATE, sandbox, { filename: 'setup-gate.js' });
@@ -104,7 +104,7 @@ await t('the screen lists every step in order, ticks the cleared ones, and opens
104
104
  assert.deepEqual(steps.map((s) => s.className), ['setup-gate__step is-done', 'setup-gate__step is-done', 'setup-gate__step is-current', 'setup-gate__step']);
105
105
  assert.match(steps[2].textContent, /Hint pick_disciplines/, 'only the current step shows its hint');
106
106
  assert.doesNotMatch(steps[3].textContent, /Hint first_start/);
107
- assert.deepEqual(gate.querySelectorAll('[data-craft]').map((b) => b.textContent), ['Engineer', 'Artist', 'UI'],
107
+ assert.deepEqual(gate.querySelectorAll('[data-craft]').map((b) => b.textContent), ['Artisan', 'Artist', 'UI'],
108
108
  'the crafts this project offers');
109
109
  });
110
110
 
@@ -128,7 +128,7 @@ await t('choosing a craft saves it through PATCH /me/disciplines, the route ever
128
128
  await t('a craft that does not save says so and can be tried again', async () => {
129
129
  const g = bootGate({ patchOk: false });
130
130
  g.show({ complete: false, steps: STEPS(['signed_in', 'cli_setup']) });
131
- g.click(g.gate().querySelector('[data-craft="engineer"]'));
131
+ g.click(g.gate().querySelector('[data-craft="artisan"]'));
132
132
  g.click(g.gate().querySelector('[data-save-crafts]'));
133
133
  await settle();
134
134
  const err = g.gate().querySelector('[data-craft-err]');
@@ -145,6 +145,26 @@ test('--dry-run respects a pre-existing valid-looking session in HOME', () => {
145
145
  }
146
146
  });
147
147
 
148
+ // Setup asks for no API key (task 1005189). It used to stop every new builder at
149
+ // a Google AI Studio key question, on every project — including ones with no art
150
+ // pipeline, and agent-run setups where nobody can type an answer. The dry-run
151
+ // plan is the half a spawn can see; the source check covers the prompt itself,
152
+ // which only ever ran on a TTY.
153
+ test('setup asks for no API key — not in the plan, not in either script', () => {
154
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-setup-smoke-'));
155
+ try {
156
+ const r = runSetup(['--dry-run', '--skip-keys'], { HOME: fakeHome });
157
+ assert.equal(r.status, 0, `--skip-keys must still be accepted; stderr: ${r.stderr}`);
158
+ assert.doesNotMatch(r.stdout, /gemini|ai studio|GOOGLE_AI_STUDIO_KEY/i, 'the dry-run plan must not list a key step');
159
+ } finally {
160
+ fs.rmSync(fakeHome, { recursive: true, force: true });
161
+ }
162
+ for (const rel of ['scripts/bongos/setup.js', 'scripts/setup-builder.js']) {
163
+ const src = fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8');
164
+ assert.doesNotMatch(src, /GOOGLE_AI_STUDIO_KEY|aistudio\.google\.com/, `${rel} must not ask for or print anything about an image key`);
165
+ }
166
+ });
167
+
148
168
  // ---- runner ----
149
169
 
150
170
  let failed = 0;
@@ -225,7 +225,7 @@ test('claimed: neutral tone, scope rows, discipline action, brief in dropdown',
225
225
  const m = card.buildCardModel({
226
226
  stage: 'claimed', taskId: 1275, title: 'Lifecycle card', brand: BRAND,
227
227
  description: 'Extend the card to every stage.',
228
- estMinutes: 120, touchesCount: 4, version: 'GDS-V4', kind: 'feature', priority: 2, discipline: 'engineer',
228
+ estMinutes: 120, touchesCount: 4, version: 'GDS-V4', kind: 'feature', priority: 2, discipline: 'artisan',
229
229
  });
230
230
  assert.equal(m.stage, 'claimed');
231
231
  assert.equal(m.cfg.tone, 'neutral');
@@ -233,7 +233,7 @@ test('claimed: neutral tone, scope rows, discipline action, brief in dropdown',
233
233
  assert.equal(row(m, 'Estimate'), '~2h');
234
234
  assert.equal(row(m, 'Touches'), '4 files');
235
235
  assert.equal(row(m, 'Value'), undefined); // no value summary pre-work
236
- assert.equal(m.action.cmd, 'Read docs/packs/engineer.md and start the work');
236
+ assert.equal(m.action.cmd, 'Read docs/packs/artisan.md and start the work');
237
237
  const html = card.renderCardHtml(m);
238
238
  assert.match(html, /Extend the card to every stage\./); // brief in dropdown
239
239
  });
@@ -246,13 +246,13 @@ test('claimed: neutral tone, scope rows, discipline action, brief in dropdown',
246
246
  test('claimed action routes by discipline — core crafts to their pack, modules to their skill', () => {
247
247
  const cmdFor = (discipline) => card.buildCardModel({ stage: 'claimed', taskId: 1, discipline, brand: BRAND }).action.cmd;
248
248
  assert.equal(cmdFor('artist'), 'Read docs/packs/artist.md and start the work');
249
- assert.equal(cmdFor('ideator'), 'Read docs/packs/ideator.md and start the work');
249
+ assert.equal(cmdFor('artist'), 'Read docs/packs/artist.md and start the work');
250
250
  assert.equal(cmdFor('ui'), '/design');
251
251
  // An unknown or unset discipline falls back to the Engineer pack, exactly as it
252
252
  // used to fall back to /dev — engineering is the project's default loop.
253
- assert.equal(cmdFor('unclassified'), 'Read docs/packs/engineer.md and start the work');
254
- assert.equal(cmdFor(undefined), 'Read docs/packs/engineer.md and start the work');
255
- for (const d of ['engineer', 'artist', 'ideator']) {
253
+ assert.equal(cmdFor('unclassified'), 'Read docs/packs/artisan.md and start the work');
254
+ assert.equal(cmdFor(undefined), 'Read docs/packs/artisan.md and start the work');
255
+ for (const d of ['artisan', 'artist', 'artist']) {
256
256
  assert.ok(!cmdFor(d).startsWith('/'), `${d} must not be routed to a slash command — /dev, /paint and /ideate no longer exist`);
257
257
  }
258
258
  });
@@ -14,10 +14,10 @@ const require = createRequire(import.meta.url);
14
14
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
15
  const S = require(path.join(ROOT, 'modules', 'specialities', 'specialities.js'));
16
16
 
17
- const DISCIPLINES = ['engineer', 'ideator', 'artist', 'ui'];
17
+ const DISCIPLINES = ['artisan', 'artist', 'ui'];
18
18
  const draft = (over = {}) => ({
19
19
  name: 'Economics',
20
- discipline: 'ideator',
20
+ discipline: 'artist',
21
21
  owner_kind: 'builder',
22
22
  owner_builder_id: '1000025',
23
23
  ...over,
@@ -130,7 +130,7 @@ test('canAdopt refuses rather than throws on missing inputs', () => {
130
130
  });
131
131
 
132
132
  // ---------- the injected fragment ----------
133
- const active = { id: '7', slug: 'economics', name: 'Economics', discipline: 'ideator', summary: 'Markets and incentives.', contract_md: 'Argue from mechanism, not vibes.' };
133
+ const active = { id: '7', slug: 'economics', name: 'Economics', discipline: 'artist', summary: 'Markets and incentives.', contract_md: 'Argue from mechanism, not vibes.' };
134
134
 
135
135
  test('no active speciality injects nothing at all', () => {
136
136
  assert.equal(S.describeForApi(null), null);
@@ -140,7 +140,7 @@ test('the contract carries the name, the craft and the owner terms', () => {
140
140
  const d = S.describeForApi(active, {});
141
141
  assert.equal(d.inject, true);
142
142
  assert.match(d.contract, /Economics/);
143
- assert.match(d.contract, /under ideator/);
143
+ assert.match(d.contract, /under artist/);
144
144
  assert.match(d.contract, /Argue from mechanism/);
145
145
  assert.equal(d.speciality_id, '7');
146
146
  });
@@ -212,3 +212,15 @@ test('an empty borrowed speciality still injects nothing', () => {
212
212
  const bare = { id: '9', slug: 'x', name: '', discipline: '', summary: '', contract_md: '' };
213
213
  assert.equal(S.describeForApi(bare, { selfAuthored: false }).inject, false);
214
214
  });
215
+
216
+ test('the write routes read an OLD craft spelling as the new one (task 1005141)', () => {
217
+ // An old CLI or form still posts discipline 'engineer'/'ideator'; the routes run
218
+ // it through the doorway's normalizeCraft before validating, on create AND update.
219
+ const fs = require('node:fs');
220
+ const src = fs.readFileSync(path.join(ROOT, 'modules', 'specialities', 'routes', 'specialities.js'), 'utf8');
221
+ assert.equal((src.match(/discipline: api\.crafts\.normalizeCraft\(b\.discipline\)/g) || []).length, 2);
222
+ const { normalizeCraft } = require(path.join(ROOT, 'src', 'crafts.js'));
223
+ assert.equal(normalizeCraft('engineer'), 'artisan');
224
+ assert.equal(normalizeCraft('ideator'), 'artist');
225
+ assert.equal(S.validateDraft(draft({ discipline: normalizeCraft('ideator') }), { disciplines: DISCIPLINES }).ok, true);
226
+ });
@@ -38,13 +38,13 @@ function fake(results = []) {
38
38
 
39
39
  test('adopt DEACTIVATES the craft before inserting — the unique index demands it', async () => {
40
40
  const q = fake([{ rows: [], rowCount: 1 }, { rows: [{ builder_id: '1', speciality_id: '7' }], rowCount: 1 }]);
41
- await db.adopt({ builderId: '1', specialityId: '7', discipline: 'ideator' }, q);
41
+ await db.adopt({ builderId: '1', specialityId: '7', discipline: 'artist' }, q);
42
42
 
43
43
  assert.equal(q.calls.length, 2, 'exactly two statements: the deactivate, then the insert');
44
44
  const [first, second] = q.calls;
45
45
  assert.match(first.sql, /UPDATE specialities_adoptions SET active = FALSE/);
46
46
  assert.match(first.sql, /builder_id = \$1 AND discipline = \$2 AND active/);
47
- assert.deepEqual(first.params, ['1', 'ideator'], 'scoped to THIS builder and THIS craft, not all of either');
47
+ assert.deepEqual(first.params, ['1', 'artist'], 'scoped to THIS builder and THIS craft, not all of either');
48
48
  assert.match(second.sql, /INSERT INTO specialities_adoptions/);
49
49
  // The order is the whole point: insert-then-deactivate would hit the index.
50
50
  assert.ok(q.calls.indexOf(first) < q.calls.indexOf(second));
@@ -68,8 +68,8 @@ test('the injected contract is read with the contract text itself', async () =>
68
68
 
69
69
  test('activeForCraft reads ONE craft for ONE builder, and only an active row', async () => {
70
70
  const q = fake([{ rows: [{ id: '7' }], rowCount: 1 }]);
71
- await db.activeForCraft('1', 'ideator', q);
72
- assert.deepEqual(q.calls[0].params, ['1', 'ideator']);
71
+ await db.activeForCraft('1', 'artist', q);
72
+ assert.deepEqual(q.calls[0].params, ['1', 'artist']);
73
73
  assert.match(q.calls[0].sql, /a\.active/);
74
74
  });
75
75
 
@@ -136,8 +136,8 @@ test('describeTrainingFor with no ids asks the database nothing', async () => {
136
136
  });
137
137
 
138
138
  test('taskDiscipline reads the CORE tasks table by id', async () => {
139
- const q = fake([{ rows: [{ discipline: 'engineer' }], rowCount: 1 }]);
140
- assert.equal(await db.taskDiscipline('1234', q), 'engineer');
139
+ const q = fake([{ rows: [{ discipline: 'artisan' }], rowCount: 1 }]);
140
+ assert.equal(await db.taskDiscipline('1234', q), 'artisan');
141
141
  assert.deepEqual(q.calls[0].params, ['1234']);
142
142
  });
143
143