@bongos/core 1.24.79 → 1.24.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/.bongos-core.json +554 -394
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  4. package/.claude/skills/design/SKILL.md +1 -1
  5. package/.claude/skills/goal-create/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +1 -1
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +8 -0
  9. package/clients/bongos-client/index.cjs +8 -0
  10. package/clients/bongos-client/index.d.ts +11 -0
  11. package/clients/bongos-client/index.mjs +8 -0
  12. package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
  13. package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
  14. package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
  15. package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
  16. package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
  17. package/docs/adr/README.md +4 -0
  18. package/docs/api/openapi.json +195 -3
  19. package/docs/api-reference.md +8 -4
  20. package/docs/architecture.md +7 -3
  21. package/docs/copy-inventory.md +393 -378
  22. package/docs/copy-registry.json +722 -578
  23. package/docs/design/characters/character-sprites-source.webp +0 -0
  24. package/docs/file-map.md +7 -8
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +5 -0
  27. package/docs/onboarding/slash-commands.md +1 -1
  28. package/docs/packs/{engineer.md → artisan.md} +5 -5
  29. package/docs/packs/artist.md +50 -4
  30. package/docs/page-inventory.json +16 -7
  31. package/docs/page-readings.json +3184 -3338
  32. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  33. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  34. package/migrations/core_297_builder_character.sql +30 -0
  35. package/modules/copy-desk/proposals.js +1 -1
  36. package/modules/economy/credits-by-craft.js +29 -15
  37. package/modules/hall-ui/public/ai-studio.html +6 -4
  38. package/modules/hall-ui/public/ai-studio.states.json +1 -0
  39. package/modules/hall-ui/public/atlas.html +5 -5
  40. package/modules/hall-ui/public/atlas.js +3 -1
  41. package/modules/hall-ui/public/atlas.states.json +1 -0
  42. package/modules/hall-ui/public/blockers.html +5 -5
  43. package/modules/hall-ui/public/board-room.html +4 -4
  44. package/modules/hall-ui/public/board-room.states.json +1 -0
  45. package/modules/hall-ui/public/characters/artisan.png +0 -0
  46. package/modules/hall-ui/public/characters/artist.png +0 -0
  47. package/modules/hall-ui/public/characters/patron.png +0 -0
  48. package/modules/hall-ui/public/city.css +3 -4
  49. package/modules/hall-ui/public/city.html +9 -9
  50. package/modules/hall-ui/public/city.js +17 -56
  51. package/modules/hall-ui/public/city.states.json +7 -32
  52. package/modules/hall-ui/public/collab.html +4 -4
  53. package/modules/hall-ui/public/collab.states.json +1 -0
  54. package/modules/hall-ui/public/commands.html +4 -4
  55. package/modules/hall-ui/public/commands.states.json +1 -0
  56. package/modules/hall-ui/public/controls.css +63 -11
  57. package/modules/hall-ui/public/copy-desk.html +4 -4
  58. package/modules/hall-ui/public/copy-desk.states.json +1 -0
  59. package/modules/hall-ui/public/craft-mode.js +258 -64
  60. package/modules/hall-ui/public/deploy.html +4 -4
  61. package/modules/hall-ui/public/deploy.states.json +1 -0
  62. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/goal-map.html +9 -6
  65. package/modules/hall-ui/public/goals.html +5 -5
  66. package/modules/hall-ui/public/goals.states.json +1 -0
  67. package/modules/hall-ui/public/government.html +4 -4
  68. package/modules/hall-ui/public/government.states.json +1 -0
  69. package/modules/hall-ui/public/hall-render.js +3 -1
  70. package/modules/hall-ui/public/home-city-draw.js +3 -3
  71. package/modules/hall-ui/public/home-city.css +4 -0
  72. package/modules/hall-ui/public/home-city.js +17 -9
  73. package/modules/hall-ui/public/home-gallery.js +8 -1
  74. package/modules/hall-ui/public/home-idea-box.css +2 -2
  75. package/modules/hall-ui/public/home-idea-box.js +3 -3
  76. package/modules/hall-ui/public/idea.html +5 -5
  77. package/modules/hall-ui/public/idea.states.json +1 -0
  78. package/modules/hall-ui/public/ideas.html +5 -5
  79. package/modules/hall-ui/public/ideas.states.json +1 -0
  80. package/modules/hall-ui/public/index-founding.states.json +1 -0
  81. package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
  82. package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
  83. package/modules/hall-ui/public/index-genesis.states.json +1 -0
  84. package/modules/hall-ui/public/index.html +11 -7
  85. package/modules/hall-ui/public/index.states.json +1 -0
  86. package/modules/hall-ui/public/market-watch-item.html +4 -4
  87. package/modules/hall-ui/public/market-watch-item.states.json +1 -0
  88. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  89. package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
  90. package/modules/hall-ui/public/market-watch.html +6 -9
  91. package/modules/hall-ui/public/market-watch.js +22 -45
  92. package/modules/hall-ui/public/market-watch.states.json +1 -0
  93. package/modules/hall-ui/public/modules.html +4 -4
  94. package/modules/hall-ui/public/modules.states.json +1 -0
  95. package/modules/hall-ui/public/money.html +4 -4
  96. package/modules/hall-ui/public/money.states.json +1 -0
  97. package/modules/hall-ui/public/my-agents.html +4 -4
  98. package/modules/hall-ui/public/my-agents.states.json +1 -0
  99. package/modules/hall-ui/public/options-menu.js +409 -0
  100. package/modules/hall-ui/public/paintbrush.js +3 -3
  101. package/modules/hall-ui/public/palette.js +11 -13
  102. package/modules/hall-ui/public/primer.html +5 -5
  103. package/modules/hall-ui/public/primer.states.json +1 -0
  104. package/modules/hall-ui/public/profile-crafts.js +1 -1
  105. package/modules/hall-ui/public/profile-roles.js +23 -19
  106. package/modules/hall-ui/public/profile.css +1 -1
  107. package/modules/hall-ui/public/profile.html +19 -18
  108. package/modules/hall-ui/public/profile.js +19 -18
  109. package/modules/hall-ui/public/profile.states.json +5 -4
  110. package/modules/hall-ui/public/project-settings.html +4 -4
  111. package/modules/hall-ui/public/roster.html +4 -4
  112. package/modules/hall-ui/public/roster.js +3 -3
  113. package/modules/hall-ui/public/roster.states.json +1 -0
  114. package/modules/hall-ui/public/settings.css +24 -4
  115. package/modules/hall-ui/public/settings.html +21 -17
  116. package/modules/hall-ui/public/settings.js +44 -44
  117. package/modules/hall-ui/public/settings.states.json +1 -0
  118. package/modules/hall-ui/public/shell-game.css +110 -0
  119. package/modules/hall-ui/public/shell-game.js +316 -3
  120. package/modules/hall-ui/public/shell.js +15 -66
  121. package/modules/hall-ui/public/sky-interaction.js +7 -0
  122. package/modules/hall-ui/public/speciality-tools.js +1 -2
  123. package/modules/hall-ui/public/street-card.states.json +1 -0
  124. package/modules/hall-ui/public/street-inside.js +3 -4
  125. package/modules/hall-ui/public/street.css +1 -5
  126. package/modules/hall-ui/public/street.html +6 -10
  127. package/modules/hall-ui/public/street.js +8 -59
  128. package/modules/hall-ui/public/street.states.json +7 -32
  129. package/modules/hall-ui/public/studio.html +5 -5
  130. package/modules/hall-ui/public/studio.states.json +1 -0
  131. package/modules/hall-ui/public/style.css +197 -112
  132. package/modules/hall-ui/public/task-detail.js +5 -4
  133. package/modules/hall-ui/public/task.html +5 -5
  134. package/modules/hall-ui/public/task.states.json +1 -0
  135. package/modules/hall-ui/public/thinking.css +10 -0
  136. package/modules/hall-ui/public/thinking.html +9 -6
  137. package/modules/hall-ui/public/thinking.states.json +1 -0
  138. package/modules/hall-ui/public/tweak-editor.html +5 -5
  139. package/modules/hall-ui/public/tweak-editor.js +5 -1
  140. package/modules/hall-ui/public/tweak-editor.states.json +1 -0
  141. package/modules/hall-ui/public/update-pill.js +91 -2
  142. package/modules/hall-ui/public/visor.css +80 -0
  143. package/modules/hall-ui/public/visor.js +316 -0
  144. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  145. package/modules/hall-ui/public/watch.html +6 -5
  146. package/modules/hall-ui/public/watch.js +17 -2
  147. package/modules/hall-ui/public/watch.states.json +1 -0
  148. package/modules/hall-ui/public/welcome-boot.js +15 -6
  149. package/modules/hall-ui/public/work-founding.states.json +1 -0
  150. package/modules/hall-ui/public/work.html +5 -5
  151. package/modules/hall-ui/public/work.states.json +1 -0
  152. package/modules/hall-ui/records/builders-roster.md +2 -2
  153. package/modules/hall-ui/records/craft-mode.md +13 -11
  154. package/modules/hall-ui/records/game-mode.md +2 -2
  155. package/modules/hall-ui/records/governor-street.md +1 -1
  156. package/modules/hall-ui/records/home-city.md +3 -1
  157. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  158. package/modules/hall-ui/records/options-menu.md +15 -0
  159. package/modules/hall-ui/records/profile-roles.md +11 -11
  160. package/modules/hall-ui/records/sky.md +3 -3
  161. package/modules/hall-ui/records/start-screen.md +25 -0
  162. package/modules/hall-ui/records/tweak-editor.md +1 -1
  163. package/modules/hall-ui/records/update-pill.md +12 -0
  164. package/modules/hall-ui/records/visor.md +23 -0
  165. package/modules/hall-ui/records/welcome-tower.md +5 -5
  166. package/modules/hall-ui/specimens/components.states.json +2 -1
  167. package/modules/ideas/ideator-stats.js +3 -1
  168. package/modules/ideas/inbox.js +2 -2
  169. package/modules/lifecycle/activity.js +1 -1
  170. package/modules/lifecycle/cascade.js +4 -4
  171. package/modules/lifecycle/goal-spec-task.js +1 -1
  172. package/modules/lifecycle/help-requests.js +19 -15
  173. package/modules/lifecycle/kickoff-checklist.js +3 -3
  174. package/modules/lifecycle/kickoff-seed.js +1 -1
  175. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  176. package/modules/lifecycle/mingle.js +4 -4
  177. package/modules/lifecycle/page-tweak-write.js +1 -1
  178. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  179. package/modules/lifecycle/routes/tasks.js +3 -3
  180. package/modules/lifecycle/ship-card.js +2 -3
  181. package/modules/lifecycle/task-classifier.js +39 -29
  182. package/modules/lifecycle/tests/cascade.mjs +7 -7
  183. package/modules/onboarding/founding-mode.js +3 -0
  184. package/modules/onboarding/genesis-home.js +21 -1
  185. package/modules/onboarding/routes/onboarding.js +2 -0
  186. package/modules/page-notes/notes.js +3 -2
  187. package/modules/page-notes/split.js +1 -1
  188. package/modules/platform-identity/craft-rollup.js +35 -7
  189. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  190. package/modules/platform-identity/platform-identity.js +2 -2
  191. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  192. package/modules/platform-identity/routes/community-search.js +2 -1
  193. package/modules/platform-identity/routes/sso.js +2 -1
  194. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  195. package/modules/public-landing/public/community.html +3 -5
  196. package/modules/public-landing/public/index.html +1 -1
  197. package/modules/public-landing/public/projects.html +1 -1
  198. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  199. package/modules/specialities/routes/specialities.js +2 -2
  200. package/modules/status-ui/public/status.js +1 -1
  201. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  202. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  203. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  204. package/modules/ui-design/public/brand.html +2 -2
  205. package/package-lock.json +2 -2
  206. package/package.json +1 -1
  207. package/release-notes.json +76 -0
  208. package/scripts/bongos/autobongos-loop.js +1 -1
  209. package/scripts/bongos/discipline-modes.json +7 -12
  210. package/scripts/bongos/fitness.js +1 -1
  211. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  212. package/scripts/bongos/grade-smoke.js +2 -2
  213. package/scripts/bongos/init.js +2 -2
  214. package/scripts/bongos/newcomer-floor.config.json +1 -2
  215. package/scripts/bongos/newcomer-floor.js +4 -3
  216. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  217. package/scripts/bongos/newcomer-floor.test.js +47 -38
  218. package/scripts/bongos/role-pack-guard.js +5 -4
  219. package/scripts/bongos/seed-tasks.example.json +3 -3
  220. package/scripts/bongos/setup.js +20 -107
  221. package/scripts/bongos/start.js +2 -2
  222. package/scripts/bongos/task.js +5 -4
  223. package/scripts/bongos/triage.js +1 -1
  224. package/scripts/bongos/worktree.js +16 -4
  225. package/scripts/hall-preview/README.md +2 -3
  226. package/scripts/setup-builder.js +6 -46
  227. package/src/bongos/auth-admission.js +8 -8
  228. package/src/bongos/craft-mode.js +2 -0
  229. package/src/bongos/db.js +39 -1
  230. package/src/bongos/github-repos.js +78 -0
  231. package/src/bongos/module-scope-map.js +22 -0
  232. package/src/bongos/role-stats.js +27 -11
  233. package/src/bongos/routes/github.js +100 -2
  234. package/src/bongos/routes/me.js +33 -0
  235. package/src/bongos/serve-internal.js +33 -11
  236. package/src/crafts.js +66 -0
  237. package/src/hall-helm.js +79 -0
  238. package/src/module-api.js +6 -1
  239. package/src/module-loader/manifest-schema.js +16 -0
  240. package/src/modules.js +2 -2
  241. package/src/role-responsibilities.js +6 -5
  242. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  243. package/tests/activity_rollup_as_of.mjs +1 -1
  244. package/tests/api_alias_caller_ratchet.mjs +3 -3
  245. package/tests/application_lifecycle.mjs +1 -1
  246. package/tests/artist_review.mjs +1 -1
  247. package/tests/artist_review_goal.mjs +1 -1
  248. package/tests/blocker_hall_live_proof.mjs +1 -1
  249. package/tests/blocker_tasks_named_db.mjs +2 -2
  250. package/tests/canonical_profile_url.mjs +1 -1
  251. package/tests/cap_newcomer_tasks.mjs +13 -13
  252. package/tests/character_creation.mjs +294 -0
  253. package/tests/claim_gate_open_blocker.mjs +2 -2
  254. package/tests/claim_gate_rebase.mjs +6 -6
  255. package/tests/claude_materialize.mjs +1 -1
  256. package/tests/clawback_confirmed_backlog.mjs +3 -3
  257. package/tests/collab_page.mjs +8 -9
  258. package/tests/collab_self_settling.mjs +2 -2
  259. package/tests/community_leaderboard_route.mjs +13 -5
  260. package/tests/community_proof_db.mjs +3 -0
  261. package/tests/community_search_route.mjs +7 -6
  262. package/tests/conductor.mjs +1 -1
  263. package/tests/conductor_main_e2e.mjs +8 -8
  264. package/tests/copy_desk_flags_db.mjs +1 -1
  265. package/tests/copy_desk_page_approve.mjs +2 -2
  266. package/tests/copy_desk_page_claim.mjs +2 -2
  267. package/tests/copy_desk_page_next.mjs +1 -1
  268. package/tests/copy_desk_page_release.mjs +1 -1
  269. package/tests/copy_desk_page_submit.mjs +2 -2
  270. package/tests/core_258_box_session_claims_db.mjs +1 -1
  271. package/tests/craft_mode.mjs +48 -34
  272. package/tests/craft_mode_home.mjs +20 -20
  273. package/tests/craft_mode_looks.mjs +16 -15
  274. package/tests/craft_mode_nav.mjs +31 -29
  275. package/tests/craft_review_mode.mjs +46 -41
  276. package/tests/crafts_vocabulary.mjs +70 -0
  277. package/tests/cross_discipline_priority_direction.mjs +5 -5
  278. package/tests/design_reference.mjs +1 -1
  279. package/tests/fitness.mjs +7 -11
  280. package/tests/funded_project_pays_proof_db.mjs +1 -1
  281. package/tests/genesis_home.mjs +21 -1
  282. package/tests/github_chip.mjs +3 -3
  283. package/tests/github_repo_invite.mjs +379 -0
  284. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  285. package/tests/goal_invite_consent.mjs +1 -1
  286. package/tests/goal_map_page.mjs +2 -2
  287. package/tests/goal_sort_staleness.mjs +1 -1
  288. package/tests/goal_spec_task.mjs +1 -1
  289. package/tests/goal_spec_task_db.mjs +3 -3
  290. package/tests/grade_engine.mjs +2 -2
  291. package/tests/grader_no_artifact.mjs +1 -1
  292. package/tests/grader_prompt_fencing.mjs +1 -1
  293. package/tests/grader_prompt_window_bound.mjs +1 -1
  294. package/tests/grader_reader_lens.mjs +2 -2
  295. package/tests/grader_severity_gate.mjs +1 -1
  296. package/tests/grader_unbacked_score.mjs +1 -1
  297. package/tests/hall_approval_queue.mjs +1 -1
  298. package/tests/hall_board_world.mjs +2 -2
  299. package/tests/hall_builders_page_pass.mjs +3 -2
  300. package/tests/hall_city.mjs +16 -30
  301. package/tests/hall_controls.mjs +52 -4
  302. package/tests/hall_fullscreen.mjs +27 -3
  303. package/tests/hall_helm.mjs +187 -0
  304. package/tests/hall_kit_charts.mjs +1 -1
  305. package/tests/hall_landing_boot.mjs +1 -1
  306. package/tests/hall_my_agents.mjs +16 -16
  307. package/tests/hall_nav.mjs +22 -15
  308. package/tests/hall_options_menu.mjs +247 -0
  309. package/tests/hall_oversight_world.mjs +1 -1
  310. package/tests/hall_palette.mjs +8 -9
  311. package/tests/hall_profile_world.mjs +3 -3
  312. package/tests/hall_record_world.mjs +6 -4
  313. package/tests/hall_settings_world.mjs +2 -1
  314. package/tests/hall_sitemap.mjs +6 -5
  315. package/tests/hall_start_screen.mjs +307 -0
  316. package/tests/hall_street.mjs +15 -28
  317. package/tests/hall_street_inside.mjs +17 -9
  318. package/tests/hall_studio_world.mjs +3 -3
  319. package/tests/hall_topbar_320.mjs +4 -2
  320. package/tests/hall_visor.mjs +224 -0
  321. package/tests/help_requests.mjs +47 -15
  322. package/tests/helpers.mjs +4 -4
  323. package/tests/home_city.mjs +6 -4
  324. package/tests/home_city_widget.mjs +33 -14
  325. package/tests/home_gallery_widget.mjs +4 -4
  326. package/tests/home_idea_box.mjs +6 -6
  327. package/tests/hub_community_page.mjs +3 -3
  328. package/tests/hub_craft_rollup.mjs +75 -36
  329. package/tests/hub_recent_ships.mjs +3 -2
  330. package/tests/idea_cascade.mjs +2 -2
  331. package/tests/idea_credit_streams_db.mjs +1 -1
  332. package/tests/idea_detail_page.mjs +1 -1
  333. package/tests/idea_ideate_full.mjs +5 -2
  334. package/tests/idea_spark_hall.mjs +1 -1
  335. package/tests/init.mjs +3 -3
  336. package/tests/instance_manifest.mjs +2 -2
  337. package/tests/kickoff_checklist.mjs +1 -1
  338. package/tests/leaderboard_roles.mjs +34 -25
  339. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  340. package/tests/lightbulb.mjs +14 -10
  341. package/tests/main_discipline.mjs +8 -8
  342. package/tests/markdown_line_endings.mjs +1 -1
  343. package/tests/market_watch_pages.mjs +52 -74
  344. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  345. package/tests/migration_allocation.mjs +1 -1
  346. package/tests/mingle.mjs +36 -21
  347. package/tests/module-scope-map.mjs +22 -2
  348. package/tests/module_api.mjs +1 -0
  349. package/tests/module_contributions.mjs +13 -13
  350. package/tests/module_manifest.mjs +29 -0
  351. package/tests/module_skills_hook.mjs +2 -2
  352. package/tests/modules.mjs +14 -7
  353. package/tests/nav_permission_atoms.mjs +2 -1
  354. package/tests/needs_rebase.mjs +1 -1
  355. package/tests/newcomer_restock.mjs +13 -13
  356. package/tests/page_notes_panel.mjs +22 -22
  357. package/tests/page_tweak_no_block.mjs +1 -1
  358. package/tests/paintbrush.mjs +7 -7
  359. package/tests/privacy_matrix.mjs +1 -0
  360. package/tests/profile_activity.mjs +20 -23
  361. package/tests/profile_earnings.mjs +1 -1
  362. package/tests/profile_nudge_prefs.mjs +4 -4
  363. package/tests/profile_role_headline.mjs +73 -46
  364. package/tests/profile_role_stats.mjs +55 -45
  365. package/tests/profile_rollup_consent.mjs +19 -6
  366. package/tests/profile_route.mjs +12 -12
  367. package/tests/profile_ui_cross_project.mjs +4 -4
  368. package/tests/project_invite_suggestions.mjs +2 -2
  369. package/tests/promote_goal_id.mjs +1 -1
  370. package/tests/publish_manifest.mjs +1 -1
  371. package/tests/rank_floor_wiring.mjs +1 -1
  372. package/tests/regrade_actual_minutes.mjs +1 -1
  373. package/tests/removal_proposal.mjs +2 -2
  374. package/tests/reward_gate.mjs +4 -4
  375. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  376. package/tests/role_responsibilities.mjs +18 -12
  377. package/tests/role_stats_db.mjs +17 -16
  378. package/tests/scouting_rollup_fanout.mjs +5 -5
  379. package/tests/seed_tasks.mjs +5 -2
  380. package/tests/session_earnings_mirror_db.mjs +1 -1
  381. package/tests/session_reward_on_land_db.mjs +1 -1
  382. package/tests/session_reward_reconcile_db.mjs +2 -2
  383. package/tests/sessions_kit_pager.mjs +1 -1
  384. package/tests/settings_art_key_absent.mjs +2 -2
  385. package/tests/settings_main_role.mjs +21 -20
  386. package/tests/setup_flow.mjs +3 -3
  387. package/tests/setup_smoke.mjs +20 -0
  388. package/tests/ship_card.mjs +6 -6
  389. package/tests/specialities.mjs +16 -4
  390. package/tests/speciality_db_shape.mjs +6 -6
  391. package/tests/speciality_injection.mjs +17 -17
  392. package/tests/speciality_offer_ui.mjs +5 -5
  393. package/tests/speciality_offered.mjs +6 -5
  394. package/tests/speciality_session_skills.mjs +7 -7
  395. package/tests/speciality_skills.mjs +11 -11
  396. package/tests/speciality_walkthrough_ui.mjs +2 -2
  397. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  398. package/tests/task_classifier_vocab.mjs +10 -0
  399. package/tests/task_cli.mjs +11 -3
  400. package/tests/task_criteria_unlink_db.mjs +2 -2
  401. package/tests/task_discipline_required.mjs +38 -16
  402. package/tests/task_goal_required.mjs +4 -4
  403. package/tests/task_needs_db.mjs +1 -1
  404. package/tests/tweak_replaces_artist_review.mjs +1 -1
  405. package/tests/update_pill.mjs +153 -2
  406. package/tests/upkeep_home.mjs +1 -1
  407. package/tests/v3_c1_options_proof.mjs +145 -0
  408. package/tests/visual_review.mjs +3 -3
  409. package/tests/watch_applications_queue.mjs +8 -5
  410. package/tests/watch_sealed_floor.mjs +3 -1
  411. package/tests/welcome_boot.mjs +12 -9
  412. package/tests/welcome_boot_browser.mjs +13 -13
  413. package/tests/worktree_helper.mjs +16 -1
@@ -21,8 +21,8 @@ const conductor = require(path.join(ROOT, '.claude', 'hooks', 'conductor.js'));
21
21
 
22
22
  const sp = (over = {}) => ({
23
23
  inject: true,
24
- contract: 'Speciality: Economics (under ideator).',
25
- discipline: 'ideator',
24
+ contract: 'Speciality: Economics (under artist).',
25
+ discipline: 'artist',
26
26
  name: 'Economics',
27
27
  ...over,
28
28
  });
@@ -30,9 +30,9 @@ const claim = (discipline) => ({ task_id: '1', title: 't', discipline });
30
30
  const ids = (out) => out.map((a) => a.id);
31
31
 
32
32
  test('the contract is injected when its craft matches the claim in hand', () => {
33
- const out = conductor.evaluateTriggers({ activeClaims: [claim('ideator')], specialities: [sp()] });
34
- assert.ok(ids(out).includes('speciality:ideator'), `expected the ideator contract, got ${JSON.stringify(ids(out))}`);
35
- const entry = out.find((a) => a.id === 'speciality:ideator');
33
+ const out = conductor.evaluateTriggers({ activeClaims: [claim('artist')], specialities: [sp()] });
34
+ assert.ok(ids(out).includes('speciality:artist'), `expected the artist contract, got ${JSON.stringify(ids(out))}`);
35
+ const entry = out.find((a) => a.id === 'speciality:artist');
36
36
  assert.match(entry.text, /Economics/);
37
37
  });
38
38
 
@@ -41,7 +41,7 @@ test('a speciality for a DIFFERENT craft is not injected', () => {
41
41
  // actively misleading at worst. This is the assertion that keeps the selection
42
42
  // honest — without it, "inject everything active" would pass the test above.
43
43
  const out = conductor.evaluateTriggers({
44
- activeClaims: [claim('engineer')],
44
+ activeClaims: [claim('artisan')],
45
45
  specialities: [sp({ discipline: 'artist', contract: 'Speciality: Pixel art (under artist).' })],
46
46
  });
47
47
  assert.equal(ids(out).filter((i) => String(i).startsWith('speciality:')).length, 0);
@@ -55,7 +55,7 @@ test('with no active claim there is no craft, so nothing is injected', () => {
55
55
 
56
56
  test('inject:false is honoured — an empty speciality spends no context', () => {
57
57
  const out = conductor.evaluateTriggers({
58
- activeClaims: [claim('ideator')],
58
+ activeClaims: [claim('artist')],
59
59
  specialities: [sp({ inject: false })],
60
60
  });
61
61
  assert.equal(ids(out).filter((i) => String(i).startsWith('speciality:')).length, 0);
@@ -63,24 +63,24 @@ test('inject:false is honoured — an empty speciality spends no context', () =>
63
63
 
64
64
  test('one contract per craft, and two crafts in play get one each', () => {
65
65
  const out = conductor.evaluateTriggers({
66
- activeClaims: [claim('ideator'), claim('engineer')],
67
- specialities: [sp(), sp({ discipline: 'engineer', contract: 'Speciality: Compilers (under engineer).' })],
66
+ activeClaims: [claim('artist'), claim('artisan')],
67
+ specialities: [sp(), sp({ discipline: 'artisan', contract: 'Speciality: Compilers (under artisan).' })],
68
68
  });
69
69
  const got = ids(out).filter((i) => String(i).startsWith('speciality:'));
70
- assert.deepEqual(got.sort(), ['speciality:engineer', 'speciality:ideator']);
70
+ assert.deepEqual(got.sort(), ['speciality:artisan', 'speciality:artist']);
71
71
  });
72
72
 
73
73
  test('malformed entries are skipped rather than thrown on', () => {
74
74
  const out = conductor.evaluateTriggers({
75
- activeClaims: [claim('ideator')],
75
+ activeClaims: [claim('artist')],
76
76
  specialities: [null, { inject: true }, sp({ contract: '' }), sp()],
77
77
  });
78
78
  assert.equal(ids(out).filter((i) => String(i).startsWith('speciality:')).length, 1);
79
79
  });
80
80
 
81
81
  test('no specialities at all changes nothing about the hook', () => {
82
- const before = conductor.evaluateTriggers({ activeClaims: [claim('ideator')] });
83
- const after = conductor.evaluateTriggers({ activeClaims: [claim('ideator')], specialities: [] });
82
+ const before = conductor.evaluateTriggers({ activeClaims: [claim('artist')] });
83
+ const after = conductor.evaluateTriggers({ activeClaims: [claim('artist')], specialities: [] });
84
84
  assert.deepEqual(ids(before), ids(after));
85
85
  });
86
86
 
@@ -112,16 +112,16 @@ test('the adoption read selects the contract itself', () => {
112
112
  test('two claims in two worktrees: each session gets only its own contract', () => {
113
113
  const claims = [
114
114
  { ...claim('artist'), task_id: '1', worktree_name: 'task-1' },
115
- { ...claim('engineer'), task_id: '2', worktree_name: 'task-2' },
115
+ { ...claim('artisan'), task_id: '2', worktree_name: 'task-2' },
116
116
  ];
117
117
  const specs = [
118
118
  sp({ discipline: 'artist', contract: 'Speciality: Pixel art (under artist).' }),
119
- sp({ discipline: 'engineer', contract: 'Speciality: Compilers (under engineer).' }),
119
+ sp({ discipline: 'artisan', contract: 'Speciality: Compilers (under artisan).' }),
120
120
  ];
121
121
  const got = (worktree) => ids(conductor.evaluateTriggers({ activeClaims: claims, specialities: specs, worktree }))
122
122
  .filter((i) => String(i).startsWith('speciality:'));
123
123
  assert.deepEqual(got('task-1'), ['speciality:artist']);
124
- assert.deepEqual(got('task-2'), ['speciality:engineer']);
124
+ assert.deepEqual(got('task-2'), ['speciality:artisan']);
125
125
  // No claim bound to this tree (main checkout): today's behaviour, every craft in play.
126
- assert.deepEqual(got('main-checkout').sort(), ['speciality:artist', 'speciality:engineer']);
126
+ assert.deepEqual(got('main-checkout').sort(), ['speciality:artisan', 'speciality:artist']);
127
127
  });
@@ -34,8 +34,8 @@ const INSTALLED = [
34
34
  { name: 'status', does: 'Shows how far along a version is.', cost: 'Free, and it changes nothing.' },
35
35
  ];
36
36
 
37
- const mine = (over = {}) => ({ id: 12, name: 'Pricing', discipline: 'engineer', summary: '', contract_md: '', visibility: 'private', adoptable: true, mine: true, skills: ['recall', 'retired-skill'], ...over });
38
- const theirs = (over = {}) => ({ id: 9, name: 'Economics', discipline: 'ideator', summary: '', contract_md: '', visibility: 'public', adoptable: true, mine: false, skills: ['recall'], ...over });
37
+ const mine = (over = {}) => ({ id: 12, name: 'Pricing', discipline: 'artisan', summary: '', contract_md: '', visibility: 'private', adoptable: true, mine: true, skills: ['recall', 'retired-skill'], ...over });
38
+ const theirs = (over = {}) => ({ id: 9, name: 'Economics', discipline: 'artist', summary: '', contract_md: '', visibility: 'public', adoptable: true, mine: false, skills: ['recall'], ...over });
39
39
 
40
40
  // The ticked boxes in rendered markup.
41
41
  const checkedIn = (html) => [...html.matchAll(/<input type="checkbox" name="skill" value="([^"]+)" checked/g)].map((m) => m[1]);
@@ -74,7 +74,7 @@ for (const page of SPECIALITY_PAGES) {
74
74
  const h = await boot(page);
75
75
  const form = h.el('speciality-create');
76
76
  assert.deepEqual(Object.keys(form.elements).sort(), ['contract_md', 'discipline', 'name', 'sellable', 'summary', 'visibility']);
77
- assert.deepEqual(form.elements.discipline.querySelectorAll('option').map((o) => o.attrs.value), ['', 'engineer', 'ideator', 'artist', 'ui']);
77
+ assert.deepEqual(form.elements.discipline.querySelectorAll('option').map((o) => o.attrs.value), ['', 'artisan', 'artist', 'ui']);
78
78
  assert.deepEqual(form.elements.visibility.querySelectorAll('option').map((o) => o.attrs.value), ['private', 'project', 'public']);
79
79
  assert.ok(form.querySelector('details#speciality-create-offer'), 'the picker is inside the form');
80
80
  assert.ok(form.querySelector('#speciality-create-skills'), 'with the host its choices fill');
@@ -191,12 +191,12 @@ for (const page of SPECIALITY_PAGES) {
191
191
  assert.deepEqual(checkedIn(host.innerHTML), [], 'a new speciality starts offering nothing');
192
192
  const form = h.el('speciality-create');
193
193
  form.elements.name.value = 'New one';
194
- form.elements.discipline.value = 'engineer';
194
+ form.elements.discipline.value = 'artisan';
195
195
  h.tick('#speciality-create-skills input[value="status"]');
196
196
  await h.submit('#speciality-create');
197
197
  const post = writes(h).find((r) => r.method === 'POST' && r.path === '/specialities');
198
198
  assert.ok(post, 'created');
199
- assert.deepEqual(post.body, { name: 'New one', discipline: 'engineer', summary: '', contract_md: '', visibility: 'private', sellable: false, skills: ['status'] });
199
+ assert.deepEqual(post.body, { name: 'New one', discipline: 'artisan', summary: '', contract_md: '', visibility: 'private', sellable: false, skills: ['status'] });
200
200
  assert.ok(h.toasts.includes('Speciality created.'));
201
201
  });
202
202
 
@@ -25,7 +25,7 @@ const db = require(path.join(ROOT, 'modules', 'specialities', 'db.js'));
25
25
 
26
26
  const ME = '1000025';
27
27
  const spec = (over = {}) => ({
28
- id: '1', name: 'Economics', discipline: 'ideator',
28
+ id: '1', name: 'Economics', discipline: 'artist',
29
29
  owner_kind: 'builder', owner_builder_id: '1000099',
30
30
  visibility: 'public', offered: true, ...over,
31
31
  });
@@ -55,13 +55,13 @@ test('a builder-owned speciality set to "project" is not suggested to others', (
55
55
  });
56
56
 
57
57
  test('a speciality you already hold is not suggested back to you', () => {
58
- const adoptions = [{ id: '1', discipline: 'ideator', active: true }];
58
+ const adoptions = [{ id: '1', discipline: 'artist', active: true }];
59
59
  assert.deepEqual(S.offeredTo([spec()], viewer, adoptions), []);
60
60
  });
61
61
 
62
62
  test('an inactive past adoption still suppresses the suggestion', () => {
63
63
  // You have had it; re-suggesting something you dropped is nagging, not helping.
64
- const adoptions = [{ id: '1', discipline: 'ideator', active: false }];
64
+ const adoptions = [{ id: '1', discipline: 'artist', active: false }];
65
65
  assert.deepEqual(S.offeredTo([spec()], viewer, adoptions), []);
66
66
  });
67
67
 
@@ -69,12 +69,13 @@ test('a craft you already have an ACTIVE speciality in yields no suggestion', ()
69
69
  // One active speciality per craft is a DB constraint (the partial unique index),
70
70
  // so offering a second in the same craft is offering something that would be
71
71
  // refused. Surfacing it as neither is the honest answer.
72
- const adoptions = [{ id: '77', discipline: 'ideator', active: true }];
72
+ const adoptions = [{ id: '77', discipline: 'artist', active: true }];
73
73
  assert.deepEqual(S.offeredTo([spec()], viewer, adoptions), []);
74
74
  });
75
75
 
76
76
  test('a different craft is still suggested while one craft is taken', () => {
77
- const adoptions = [{ id: '77', discipline: 'artist', active: true }];
77
+ // Since task 1005141 ideator folded into artist, so the "other" craft here is artisan.
78
+ const adoptions = [{ id: '77', discipline: 'artisan', active: true }];
78
79
  assert.deepEqual(S.offeredTo([spec()], viewer, adoptions).map((s) => s.id), ['1']);
79
80
  });
80
81
 
@@ -29,7 +29,7 @@ const { test, summary } = makeRunner();
29
29
  const LEAN = /chose to lean on these skills/;
30
30
 
31
31
  // ---- the pure line ----------------------------------------------------------
32
- const row = (over = {}) => ({ id: 9, slug: 'economics', name: 'Economics', discipline: 'ideator', summary: '', contract_md: '', skills: ['recall', 'status'], enabled_skills: [], active: true, ...over });
32
+ const row = (over = {}) => ({ id: 9, slug: 'economics', name: 'Economics', discipline: 'artist', summary: '', contract_md: '', skills: ['recall', 'status'], enabled_skills: [], active: true, ...over });
33
33
 
34
34
  await test('enabled skills are named in the contract', () => {
35
35
  const d = S.describeForApi(row({ enabled_skills: ['recall', 'status'] }));
@@ -100,7 +100,7 @@ await test('the port is the one the router registered', () => {
100
100
  });
101
101
 
102
102
  await test('an adoption with enabled skills reaches the session in its craft', async () => {
103
- const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('ideator')]);
103
+ const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('artist')]);
104
104
  const sp = specEntries(out);
105
105
  assert.equal(sp.length, 1);
106
106
  assert.match(sp[0].text, /Economics/);
@@ -108,19 +108,19 @@ await test('an adoption with enabled skills reaches the session in its craft', a
108
108
  });
109
109
 
110
110
  await test('with nothing enabled the session gets the contract and not a word more', async () => {
111
- const out = await sessionFor([row({ enabled_skills: [] })], [claim('ideator')]);
111
+ const out = await sessionFor([row({ enabled_skills: [] })], [claim('artist')]);
112
112
  const sp = specEntries(out);
113
113
  assert.equal(sp.length, 1, 'the speciality itself is still there');
114
114
  assert.doesNotMatch(sp[0].text, LEAN);
115
115
  });
116
116
 
117
117
  await test('craft-scoped like the contract: an engineering session hears nothing of an ideator\'s skills', async () => {
118
- const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('engineer')]);
118
+ const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('artisan')]);
119
119
  assert.equal(specEntries(out).length, 0);
120
120
  });
121
121
 
122
122
  await test('an inactive adoption relays nothing, skills included', async () => {
123
- const out = await sessionFor([row({ active: false, enabled_skills: ['recall'] })], [claim('ideator')]);
123
+ const out = await sessionFor([row({ active: false, enabled_skills: ['recall'] })], [claim('artist')]);
124
124
  assert.equal(specEntries(out).length, 0);
125
125
  });
126
126
 
@@ -128,7 +128,7 @@ await test('a failed skills read degrades to the offered filter, never a lost co
128
128
  const saved = skills.installedSkillsCached;
129
129
  skills.installedSkillsCached = () => { throw new Error('disk gone'); };
130
130
  try {
131
- const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('ideator')]);
131
+ const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('artist')]);
132
132
  assert.match(specEntries(out)[0].text, /\/recall/);
133
133
  } finally { skills.installedSkillsCached = saved; }
134
134
  });
@@ -165,7 +165,7 @@ await test('three active adoptions cost ONE training read, and each keeps its ow
165
165
  try {
166
166
  store.adoptions = [
167
167
  row({ id: 9, name: 'Economics', contract_md: 'Mine.' }),
168
- row({ id: 10, name: 'Law', discipline: 'engineer', contract_md: 'Theirs.' }),
168
+ row({ id: 10, name: 'Law', discipline: 'artisan', contract_md: 'Theirs.' }),
169
169
  row({ id: 11, name: 'Music', discipline: 'artist', contract_md: 'Gone.' }),
170
170
  row({ id: 12, name: 'Old', discipline: 'ui', active: false }),
171
171
  ];
@@ -147,16 +147,16 @@ await test('an unreadable root contributes nothing rather than throwing', () =>
147
147
 
148
148
  // ---- 2. the pure rules ------------------------------------------------------
149
149
  const INSTALLED = new Set(['recall', 'status', 'costing']);
150
- const draft = (over = {}) => ({ name: 'Economics', discipline: 'ideator', owner_kind: 'builder', owner_builder_id: 1, ...over });
150
+ const draft = (over = {}) => ({ name: 'Economics', discipline: 'artist', owner_kind: 'builder', owner_builder_id: 1, ...over });
151
151
  const codes = (r) => r.errors.map((e) => e.code);
152
152
 
153
153
  await test('a speciality may offer skills this instance has', () => {
154
- const r = S.validateDraft(draft({ skills: ['recall', 'costing'] }), { disciplines: ['ideator'], installedSkills: INSTALLED });
154
+ const r = S.validateDraft(draft({ skills: ['recall', 'costing'] }), { disciplines: ['artist'], installedSkills: INSTALLED });
155
155
  assert.equal(r.ok, true, JSON.stringify(r.errors));
156
156
  });
157
157
 
158
158
  await test('a skill this instance does NOT have is refused, by name', () => {
159
- const r = S.validateDraft(draft({ skills: ['recall', 'teleport'] }), { disciplines: ['ideator'], installedSkills: INSTALLED });
159
+ const r = S.validateDraft(draft({ skills: ['recall', 'teleport'] }), { disciplines: ['artist'], installedSkills: INSTALLED });
160
160
  assert.equal(r.ok, false);
161
161
  const e = r.errors.find((x) => x.code === 'unknown_skill');
162
162
  assert.ok(e, JSON.stringify(r.errors));
@@ -164,14 +164,14 @@ await test('a skill this instance does NOT have is refused, by name', () => {
164
164
  });
165
165
 
166
166
  await test('a non-empty offered set nothing could check is refused, never admitted', () => {
167
- const r = S.validateDraft(draft({ skills: ['recall'] }), { disciplines: ['ideator'] });
167
+ const r = S.validateDraft(draft({ skills: ['recall'] }), { disciplines: ['artist'] });
168
168
  assert.ok(codes(r).includes('uncheckable'));
169
169
  // and an empty set needs no check
170
- assert.equal(S.validateDraft(draft({ skills: [] }), { disciplines: ['ideator'] }).ok, true);
170
+ assert.equal(S.validateDraft(draft({ skills: [] }), { disciplines: ['artist'] }).ok, true);
171
171
  });
172
172
 
173
173
  await test('shape: not a list, a bad name, a duplicate, too many', () => {
174
- const v = (skillsList) => codes(S.validateDraft(draft({ skills: skillsList }), { disciplines: ['ideator'], installedSkills: INSTALLED }));
174
+ const v = (skillsList) => codes(S.validateDraft(draft({ skills: skillsList }), { disciplines: ['artist'], installedSkills: INSTALLED }));
175
175
  assert.ok(v('recall').includes('invalid'));
176
176
  assert.ok(v(['Recall']).includes('invalid_name'));
177
177
  assert.ok(v(['recall', 'recall']).includes('duplicate'));
@@ -201,7 +201,7 @@ await test('the migration stores an EMPTY enabled set by default, not a UI defau
201
201
  await test('adopting never writes the enabled set, so every new adoption takes the empty default', async () => {
202
202
  const seen = [];
203
203
  const exec = async (text, params) => { seen.push({ text, params }); return { rows: [{ builder_id: 1, speciality_id: 9, enabled_skills: [] }], rowCount: 1 }; };
204
- const row = await db.adopt({ builderId: 1, specialityId: 9, discipline: 'ideator' }, exec);
204
+ const row = await db.adopt({ builderId: 1, specialityId: 9, discipline: 'artist' }, exec);
205
205
  const insert = seen.find((q) => /INSERT INTO specialities_adoptions/.test(q.text));
206
206
  assert.ok(insert, 'the adopt INSERT ran');
207
207
  assert.ok(!/enabled_skills\s*[,)]/.test(insert.text.split('VALUES')[0]), 'the INSERT column list does not name enabled_skills');
@@ -220,7 +220,7 @@ await test('the enabled set is written only on the caller\'s own ACTIVE adoption
220
220
 
221
221
  // ---- 4. the real router -----------------------------------------------------
222
222
  const store = {
223
- speciality: { id: 9, name: 'Economics', discipline: 'ideator', owner_kind: 'builder', owner_builder_id: 1, visibility: 'public', sellable: false, skills: ['recall', 'status'] },
223
+ speciality: { id: 9, name: 'Economics', discipline: 'artist', owner_kind: 'builder', owner_builder_id: 1, visibility: 'public', sellable: false, skills: ['recall', 'status'] },
224
224
  adoption: null, created: null, updated: null,
225
225
  };
226
226
  db.getSpeciality = async () => ({ ...store.speciality });
@@ -235,7 +235,7 @@ skills.installedSkillsCached = () => new Map([
235
235
  ]);
236
236
  db.listAdoptions = async () => (store.adoption ? [{ id: 9, active: store.adoption.active, enabled_skills: store.adoption.enabled_skills }] : []);
237
237
  api.requireBuilder = (req, res, next) => { req.builder = { id: 1, rank: 'metic' }; next(); };
238
- api.enabledDisciplines = () => ['ideator', 'engineer', 'artist'];
238
+ api.enabledDisciplines = () => ['artist', 'artisan', 'artist'];
239
239
 
240
240
  const express = require('express');
241
241
  const app = express();
@@ -252,14 +252,14 @@ const call = async (method, url, body) => {
252
252
 
253
253
  try {
254
254
  await test('POST /specialities stores an offered set of installed skills', async () => {
255
- const r = await call('POST', '/specialities', { name: 'Economics', discipline: 'ideator', skills: ['recall', 'costing'] });
255
+ const r = await call('POST', '/specialities', { name: 'Economics', discipline: 'artist', skills: ['recall', 'costing'] });
256
256
  assert.equal(r.status, 201, JSON.stringify(r.body));
257
257
  assert.deepEqual(store.created.skills, ['recall', 'costing']);
258
258
  });
259
259
 
260
260
  await test('POST /specialities refuses a skill the instance does not have', async () => {
261
261
  store.created = null;
262
- const r = await call('POST', '/specialities', { name: 'Econ2', discipline: 'ideator', skills: ['teleport'] });
262
+ const r = await call('POST', '/specialities', { name: 'Econ2', discipline: 'artist', skills: ['teleport'] });
263
263
  assert.equal(r.status, 400);
264
264
  assert.ok(JSON.stringify(r.body).includes('unknown_skill'));
265
265
  assert.equal(store.created, null, 'nothing was written');
@@ -39,14 +39,14 @@ const explained = (name, extra = {}) => ({
39
39
 
40
40
  function speciality(over = {}) {
41
41
  return {
42
- id: 9, name: 'Economics', discipline: 'ideator', summary: 'Markets.', contract_md: '',
42
+ id: 9, name: 'Economics', discipline: 'artist', summary: 'Markets.', contract_md: '',
43
43
  visibility: 'public', adoptable: true, mine: false, skills: ['recall', 'status', 'goal-review', 'gone'],
44
44
  ...over,
45
45
  };
46
46
  }
47
47
 
48
48
  const SKILLS_BODY = {
49
- speciality: { id: 9, name: 'Economics', discipline: 'ideator' },
49
+ speciality: { id: 9, name: 'Economics', discipline: 'artist' },
50
50
  adopted: true,
51
51
  enabled: [],
52
52
  skills: [
@@ -83,6 +83,7 @@ require.cache[moduleApiPath] = {
83
83
  resolveEnv: () => null, // no hub signing key ⇒ device legs 503
84
84
  userAgent: () => 'cloud-bongos-tests',
85
85
  LIVE_RANK_LADDER: ['xenos', 'thetes', 'metic', 'archon'],
86
+ crafts: require('../src/crafts.js'), // the rollup normalizes reported disciplines (task 1005141)
86
87
  },
87
88
  };
88
89
 
@@ -163,7 +164,7 @@ const rollupBody = (over = {}) => ({
163
164
  karma: 999999,
164
165
  tasks_shipped: 9999,
165
166
  rank: 'archon',
166
- disciplines: ['engineer'],
167
+ disciplines: ['artisan'],
167
168
  ...over,
168
169
  });
169
170
 
@@ -93,3 +93,13 @@ test('no hall page carries its own copy of the kind list', () => {
93
93
  `${page} spells out the kind vocabulary — fill it from /me task_kinds via OTB.fillKindFilter instead`);
94
94
  }
95
95
  });
96
+
97
+ // task 1005141 folded the ideator into the artist craft. The art cues and the
98
+ // ideas cues are now ONE craft's score, so a task with one of each is an artist
99
+ // task — scored as two lanes it read as a tie and fell to hand-triage.
100
+ test('an even art/ideas task is the artist craft, not a tie', () => {
101
+ const { classifyTaskDiscipline } = classifier;
102
+ assert.equal(classifyTaskDiscipline('Sprites for the quest', '', []).discipline, 'artist');
103
+ // ui still competes as its own lane.
104
+ assert.equal(classifyTaskDiscipline('UI redesign wireframes for the landing page', '', []).discipline, 'ui');
105
+ });
@@ -24,13 +24,13 @@ t('requires --title and --description', () => {
24
24
  assert.ok(errors.some((e) => /--description/.test(e)));
25
25
  });
26
26
 
27
- t('minimal valid: defaults kind=feature, discipline=engineer, status=backlog', () => {
27
+ t('minimal valid: defaults kind=feature, discipline=artisan, status=backlog', () => {
28
28
  const { body, errors } = buildTaskBody(['--title', 'T', '--description', 'D']);
29
29
  assert.equal(errors.length, 0);
30
30
  assert.equal(body.title, 'T');
31
31
  assert.equal(body.description, 'D');
32
32
  assert.equal(body.kind, 'feature');
33
- assert.equal(body.discipline, 'engineer');
33
+ assert.equal(body.discipline, 'artisan');
34
34
  assert.equal(body.status, 'backlog');
35
35
  assert.equal(body.source, 'bongos-task-cli');
36
36
  // optional fields absent unless provided
@@ -92,7 +92,15 @@ t('hallLink builds a hall URL from the API base origin, falls back cleanly', ()
92
92
 
93
93
  t('enum vocab is the expected shape', () => {
94
94
  assert.ok(VALID_KINDS.includes('feature') && VALID_KINDS.includes('bug'));
95
- assert.deepEqual(VALID_DISCIPLINES, ['engineer', 'artist', 'ideator', 'ui']);
95
+ // task 1005141: engineer is 'artisan'; ideator folded into 'artist'.
96
+ assert.deepEqual(VALID_DISCIPLINES, ['artisan', 'artist', 'ui']);
97
+ });
98
+
99
+ t('the OLD --discipline spellings still work, mapped to the new craft (task 1005141)', () => {
100
+ assert.equal(buildTaskBody(['--title', 'T', '--description', 'D', '--discipline', 'engineer']).body.discipline, 'artisan');
101
+ assert.equal(buildTaskBody(['--title', 'T', '--description', 'D', '--discipline', 'ideator']).body.discipline, 'artist');
102
+ const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--discipline', 'wizard']);
103
+ assert.ok(errors.some((e) => /--discipline/.test(e)), 'an unknown craft is still refused');
96
104
  });
97
105
 
98
106
  console.log(`\n${passed} passed, ${failed} failed`);
@@ -70,8 +70,8 @@ try {
70
70
  await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'unlink-s','unlink-signer','Unlink Signer','archon')`, [SIGNER]);
71
71
 
72
72
  const goal = await db.createGoal({ versionId: VER, title: 'Unlink goal', createdBy: SIGNER });
73
- const shipped = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The shipped half', kind: 'feature', discipline: 'engineer', creditsReward: 10 });
74
- const open = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The open half', kind: 'feature', discipline: 'engineer', creditsReward: 10 });
73
+ const shipped = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The shipped half', kind: 'feature', discipline: 'artisan', creditsReward: 10 });
74
+ const open = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The open half', kind: 'feature', discipline: 'artisan', creditsReward: 10 });
75
75
  await q(`UPDATE tasks SET status='shipped', shipped_at = now() - interval '1 hour' WHERE id=$1`, [shipped.id]);
76
76
 
77
77
  // Two criteria, the same two links on each. Only the first carries a sign-off.
@@ -18,13 +18,16 @@ const { resolveWriteDiscipline, WRITABLE_DISCIPLINES, VALID_DISCIPLINES } = clas
18
18
 
19
19
  // --- the writable set -------------------------------------------------------
20
20
 
21
- test('the writable set is the four crafts — ui included, unclassified excluded', () => {
22
- // FOUR, not three. The task was written 2026-08-16 naming three; ADR 0272
23
- // (2026-09-09) keeps `ui` as a module-contributed discipline and the owner
24
- // reconfirmed it on 2026-09-17 when abandoning task 1002987. Narrowing to three
25
- // would break live ui rows — the backfill found 3 of them by classification alone.
26
- assert.deepEqual([...WRITABLE_DISCIPLINES].sort(), ['artist', 'engineer', 'ideator', 'ui']);
21
+ test('the writable set is the crafts — ui included, unclassified excluded', () => {
22
+ // ADR 0272 (2026-09-09) keeps `ui` as a module-contributed discipline and the owner
23
+ // reconfirmed it on 2026-09-17 when abandoning task 1002987; dropping it would
24
+ // break live ui rows. Task 1005141 (BONGOS-V3) renamed engineer → 'artisan' and
25
+ // folded ideator into 'artist', so the four became three: artisan, artist, ui.
26
+ assert.deepEqual([...WRITABLE_DISCIPLINES].sort(), ['artisan', 'artist', 'ui']);
27
27
  assert.ok(!WRITABLE_DISCIPLINES.has('unclassified'), 'unclassified is a triage state, not a craft');
28
+ for (const old of ['engineer', 'ideator']) {
29
+ assert.ok(!WRITABLE_DISCIPLINES.has(old), `'${old}' is an old spelling, not a writable craft`);
30
+ }
28
31
  });
29
32
 
30
33
  test('the READ set still carries unclassified, so old rows and filters resolve', () => {
@@ -41,9 +44,20 @@ test('an explicit valid discipline is honoured verbatim', () => {
41
44
  }
42
45
  });
43
46
 
47
+ test('an OLD spelling is normalized to the new craft, not refused (task 1005141)', () => {
48
+ assert.deepEqual(resolveWriteDiscipline({ discipline: 'engineer', title: 'x' }), { discipline: 'artisan' });
49
+ assert.deepEqual(resolveWriteDiscipline({ discipline: 'ideator', title: 'x' }), { discipline: 'artist' });
50
+ assert.equal(classifier.normalizeDisciplineValue('engineer'), 'artisan');
51
+ assert.equal(classifier.normalizeDisciplineValue('ideator'), 'artist');
52
+ assert.equal(classifier.normalizeDisciplineValue('ui'), 'ui');
53
+ assert.equal(classifier.normalizeDisciplineValue(null), null, 'a non-string passes through for the caller to judge');
54
+ assert.deepEqual(resolveWriteDiscipline({ discipline: 'wizard', title: 'x' }), { error: 'bad_discipline' },
55
+ 'normalizing does not turn an unknown word into a craft');
56
+ });
57
+
44
58
  test('an omitted discipline is INFERRED, and reports that it was', () => {
45
59
  const r = resolveWriteDiscipline({ title: 'Refactor the database migration script' });
46
- assert.equal(r.discipline, 'engineer');
60
+ assert.equal(r.discipline, 'artisan');
47
61
  assert.equal(r.inferred, true, 'the caller must be able to tell a guess from a statement');
48
62
  assert.ok(r.confidence >= 0.5);
49
63
  });
@@ -52,7 +66,7 @@ test('an explicit "unclassified" is treated as no answer at all, not as a value'
52
66
  // modules/ideas/inbox.js sent this literal string on every promotion. If it were
53
67
  // honoured as a value, the create path would still mint the rows this task removes.
54
68
  const r = resolveWriteDiscipline({ discipline: 'unclassified', title: 'Refactor the database migration script' });
55
- assert.equal(r.discipline, 'engineer');
69
+ assert.equal(r.discipline, 'artisan');
56
70
  assert.equal(r.inferred, true);
57
71
  });
58
72
 
@@ -109,13 +123,14 @@ test('createTask no longer has an "unclassified" default to fall back on', () =>
109
123
 
110
124
  // --- the migration: the absence is the thing under test ---------------------
111
125
 
112
- test('core_247 narrows the CHECK to the four crafts and REJECTS unclassified', () => {
126
+ test('core_247 narrowed the CHECK to the four crafts of its day and REJECTED unclassified', () => {
127
+ // core_247 is history: it predates task 1005141, so it names engineer/ideator.
128
+ // The live constraint is core_296's — see the next test.
113
129
  const sql = fs.readFileSync(path.join(ROOT, 'migrations', 'core_247_discipline_required.sql'), 'utf8');
114
130
  const m = sql.match(/CHECK \(discipline IN \(([^)]*)\)\)/);
115
131
  assert.ok(m, 'the migration must re-add a discipline CHECK constraint');
116
132
  const accepted = m[1].split(',').map((s) => s.trim().replace(/'/g, '')).sort();
117
- assert.deepEqual(accepted, ['artist', 'engineer', 'ideator', 'ui'],
118
- 'the accepted set must match WRITABLE_DISCIPLINES exactly');
133
+ assert.deepEqual(accepted, ['artist', 'engineer', 'ideator', 'ui']);
119
134
  assert.ok(!accepted.includes('unclassified'), 'the whole point: the constraint rejects it');
120
135
  // The net that makes the migration safe on a checkout where nobody ran the script.
121
136
  assert.match(sql, /UPDATE tasks SET discipline = 'engineer'[\s\S]*?WHERE discipline = 'unclassified'/,
@@ -125,10 +140,17 @@ test('core_247 narrows the CHECK to the four crafts and REJECTS unclassified', (
125
140
  test('the migration constraint and the code agree on the accepted set', () => {
126
141
  // Two places state the vocabulary; a drift between them is a 500 at write time
127
142
  // (code accepts a value the DB rejects) or dead validation (the reverse).
128
- const sql = fs.readFileSync(path.join(ROOT, 'migrations', 'core_247_discipline_required.sql'), 'utf8');
143
+ // The live CHECK is core_296's (task 1005141). It is a deliberate SUPERSET: it
144
+ // still admits the old 'engineer'/'ideator' because `bongos upgrade` rolls code
145
+ // back without rolling the DB back. So every writable craft is accepted, and the
146
+ // only extras are exactly those two old spellings — never 'unclassified'.
147
+ const sql = fs.readFileSync(path.join(ROOT, 'migrations', 'core_296_crafts_artisan_artist.sql'), 'utf8');
129
148
  const accepted = sql.match(/CHECK \(discipline IN \(([^)]*)\)\)/)[1]
130
149
  .split(',').map((s) => s.trim().replace(/'/g, '')).sort();
131
- assert.deepEqual(accepted, [...WRITABLE_DISCIPLINES].sort());
150
+ for (const d of WRITABLE_DISCIPLINES) assert.ok(accepted.includes(d), `the DB must accept writable '${d}'`);
151
+ const extras = accepted.filter((d) => !WRITABLE_DISCIPLINES.has(d)).sort();
152
+ assert.deepEqual(extras, ['engineer', 'ideator'], 'only the rollback spellings may be extra');
153
+ assert.ok(!accepted.includes('unclassified'));
132
154
  });
133
155
 
134
156
  // --- the create vectors that must keep working ------------------------------
@@ -139,7 +161,7 @@ test('every direct createTask call site names a discipline or resolves one', ()
139
161
  ['modules/ideas/inbox.js', /discipline: \(lifecycle\.resolveWriteDiscipline/],
140
162
  ['modules/copy-desk/routes/copy-desk.js', /discipline: 'artist'/],
141
163
  ['modules/onboarding/newcomer-restock.js', /^\s*discipline,\s*$/m],
142
- ['modules/lifecycle/kickoff-seed.js', /discipline: t\.discipline \|\| 'engineer'/],
164
+ ['modules/lifecycle/kickoff-seed.js', /discipline: t\.discipline \|\| 'artisan'/],
143
165
  ['modules/lifecycle/cascade.js', /discipline: rule\.discipline/],
144
166
  ['scripts/bongos/triage.js', /discipline: resolveWriteDiscipline\(/],
145
167
  ];
@@ -150,7 +172,7 @@ test('every direct createTask call site names a discipline or resolves one', ()
150
172
  });
151
173
 
152
174
  test('init.js no longer clobbers its own discipline default with undefined', () => {
153
- // The spread ran AFTER `t.discipline || 'engineer'`, so `discipline: undefined` in
175
+ // The spread ran AFTER `t.discipline || 'artisan'`, so `discipline: undefined` in
154
176
  // the extras object reset it to nothing — which is why a freshly scaffolded
155
177
  // instance was born with role-less starter tasks.
156
178
  // Comment lines are stripped first: the fix's own explanation names the old
@@ -159,5 +181,5 @@ test('init.js no longer clobbers its own discipline default with undefined', ()
159
181
  const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'init.js'), 'utf8')
160
182
  .split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');
161
183
  assert.ok(!/discipline: undefined/.test(src), 'a later spread must not undo the default');
162
- assert.match(src, /discipline: t\.discipline \|\| 'engineer'/);
184
+ assert.match(src, /discipline: t\.discipline \|\| 'artisan'/);
163
185
  });
@@ -47,13 +47,13 @@ const permissivePool = {
47
47
 
48
48
  test('a create with no goalId THROWS instead of quietly filing into a bucket', async () => {
49
49
  await assert.rejects(
50
- () => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'engineer' }, { pool: permissivePool }),
50
+ () => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'artisan' }, { pool: permissivePool }),
51
51
  /goalId is required/,
52
52
  'the old behaviour was to succeed and say nothing');
53
53
  });
54
54
 
55
55
  test('the error tells the caller both ways out', async () => {
56
- const err = await createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'engineer' }, { pool: permissivePool })
56
+ const err = await createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'artisan' }, { pool: permissivePool })
57
57
  .then(() => null, (e) => e);
58
58
  assert.ok(err, 'must reject');
59
59
  assert.match(err.message, /BV1\.R11/, 'names the rule, so the reason is findable');
@@ -63,7 +63,7 @@ test('the error tells the caller both ways out', async () => {
63
63
  test('only a literal true opts in — a truthy value still throws', async () => {
64
64
  for (const v of [1, 'yes', {}, 'true']) {
65
65
  await assert.rejects(
66
- () => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'engineer', allowCatchAll: v }, { pool: permissivePool }),
66
+ () => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'artisan', allowCatchAll: v }, { pool: permissivePool }),
67
67
  /goalId is required/,
68
68
  `${JSON.stringify(v)} must not open the hatch`);
69
69
  }
@@ -73,7 +73,7 @@ test('naming a goal needs no flag at all', async () => {
73
73
  // The ordinary path must stay ordinary: a caller with a goal never learns this
74
74
  // parameter exists.
75
75
  await createTask(
76
- { versionId: 'BONGOS-V1', title: 't', discipline: 'engineer', goalId: 77 },
76
+ { versionId: 'BONGOS-V1', title: 't', discipline: 'artisan', goalId: 77 },
77
77
  { pool: permissivePool, client: permissivePool },
78
78
  );
79
79
  });
@@ -58,7 +58,7 @@ try {
58
58
  [id, `tn-${id}`, `tn-${id}`, grants]);
59
59
  }
60
60
  for (const [id, needs] of [[T_NONE, '{}'], [T_OWNER, '{owner}'], [T_SERVER, '{server}']]) {
61
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline,needs) VALUES ($1,$2,$3,'ready',10,'feature','engineer',$4::text[])`,
61
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline,needs) VALUES ($1,$2,$3,'ready',10,'feature','artisan',$4::text[])`,
62
62
  [id, VER, `tn ${id}`, needs]);
63
63
  }
64
64
 
@@ -38,7 +38,7 @@ const api = require('../src/module-api.js');
38
38
  // used: a hall-ui module ship that changed a page file.
39
39
  const SHIP = {
40
40
  id: '555', title: 'Reword the studio greeting', status: 'shipped',
41
- discipline: 'engineer', kind: 'feature', source: 'manual', module_key: 'hall-ui',
41
+ discipline: 'artisan', kind: 'feature', source: 'manual', module_key: 'hall-ui',
42
42
  version_id: 'BONGOS-V2', goal_id: '1000095',
43
43
  touches: ['modules/hall-ui/public/studio.html'],
44
44
  shipped_at: new Date('2026-09-22T09:00:00Z'),