@bongos/core 1.24.79 → 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 (366) hide show
  1. package/.bongos-core.json +495 -350
  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 +2 -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 +70 -0
  167. package/scripts/bongos/autobongos-loop.js +1 -1
  168. package/scripts/bongos/discipline-modes.json +7 -12
  169. package/scripts/bongos/fitness.js +1 -1
  170. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  171. package/scripts/bongos/grade-smoke.js +2 -2
  172. package/scripts/bongos/init.js +2 -2
  173. package/scripts/bongos/newcomer-floor.config.json +1 -2
  174. package/scripts/bongos/newcomer-floor.js +4 -3
  175. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  176. package/scripts/bongos/newcomer-floor.test.js +47 -38
  177. package/scripts/bongos/role-pack-guard.js +5 -4
  178. package/scripts/bongos/seed-tasks.example.json +3 -3
  179. package/scripts/bongos/setup.js +20 -107
  180. package/scripts/bongos/start.js +2 -2
  181. package/scripts/bongos/task.js +5 -4
  182. package/scripts/bongos/triage.js +1 -1
  183. package/scripts/bongos/worktree.js +16 -4
  184. package/scripts/setup-builder.js +6 -46
  185. package/src/bongos/auth-admission.js +8 -8
  186. package/src/bongos/craft-mode.js +2 -0
  187. package/src/bongos/db.js +39 -1
  188. package/src/bongos/github-repos.js +78 -0
  189. package/src/bongos/module-scope-map.js +22 -0
  190. package/src/bongos/role-stats.js +27 -11
  191. package/src/bongos/routes/github.js +100 -2
  192. package/src/bongos/routes/me.js +33 -0
  193. package/src/bongos/serve-internal.js +26 -9
  194. package/src/crafts.js +66 -0
  195. package/src/module-api.js +6 -1
  196. package/src/module-loader/manifest-schema.js +16 -0
  197. package/src/modules.js +2 -2
  198. package/src/role-responsibilities.js +6 -5
  199. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  200. package/tests/activity_rollup_as_of.mjs +1 -1
  201. package/tests/api_alias_caller_ratchet.mjs +3 -3
  202. package/tests/application_lifecycle.mjs +1 -1
  203. package/tests/artist_review.mjs +1 -1
  204. package/tests/artist_review_goal.mjs +1 -1
  205. package/tests/blocker_hall_live_proof.mjs +1 -1
  206. package/tests/blocker_tasks_named_db.mjs +2 -2
  207. package/tests/canonical_profile_url.mjs +1 -1
  208. package/tests/cap_newcomer_tasks.mjs +13 -13
  209. package/tests/character_creation.mjs +294 -0
  210. package/tests/claim_gate_open_blocker.mjs +2 -2
  211. package/tests/claim_gate_rebase.mjs +6 -6
  212. package/tests/claude_materialize.mjs +1 -1
  213. package/tests/clawback_confirmed_backlog.mjs +3 -3
  214. package/tests/collab_page.mjs +8 -9
  215. package/tests/collab_self_settling.mjs +2 -2
  216. package/tests/community_leaderboard_route.mjs +13 -5
  217. package/tests/community_proof_db.mjs +3 -0
  218. package/tests/community_search_route.mjs +7 -6
  219. package/tests/conductor.mjs +1 -1
  220. package/tests/conductor_main_e2e.mjs +8 -8
  221. package/tests/copy_desk_flags_db.mjs +1 -1
  222. package/tests/copy_desk_page_approve.mjs +2 -2
  223. package/tests/copy_desk_page_claim.mjs +2 -2
  224. package/tests/copy_desk_page_next.mjs +1 -1
  225. package/tests/copy_desk_page_release.mjs +1 -1
  226. package/tests/copy_desk_page_submit.mjs +2 -2
  227. package/tests/core_258_box_session_claims_db.mjs +1 -1
  228. package/tests/craft_mode.mjs +48 -34
  229. package/tests/craft_mode_home.mjs +20 -20
  230. package/tests/craft_mode_looks.mjs +16 -15
  231. package/tests/craft_mode_nav.mjs +31 -29
  232. package/tests/craft_review_mode.mjs +46 -41
  233. package/tests/crafts_vocabulary.mjs +70 -0
  234. package/tests/cross_discipline_priority_direction.mjs +5 -5
  235. package/tests/fitness.mjs +7 -11
  236. package/tests/funded_project_pays_proof_db.mjs +1 -1
  237. package/tests/genesis_home.mjs +21 -1
  238. package/tests/github_chip.mjs +3 -3
  239. package/tests/github_repo_invite.mjs +379 -0
  240. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  241. package/tests/goal_invite_consent.mjs +1 -1
  242. package/tests/goal_map_page.mjs +2 -2
  243. package/tests/goal_sort_staleness.mjs +1 -1
  244. package/tests/goal_spec_task.mjs +1 -1
  245. package/tests/goal_spec_task_db.mjs +3 -3
  246. package/tests/grade_engine.mjs +2 -2
  247. package/tests/grader_no_artifact.mjs +1 -1
  248. package/tests/grader_prompt_fencing.mjs +1 -1
  249. package/tests/grader_prompt_window_bound.mjs +1 -1
  250. package/tests/grader_reader_lens.mjs +2 -2
  251. package/tests/grader_severity_gate.mjs +1 -1
  252. package/tests/grader_unbacked_score.mjs +1 -1
  253. package/tests/hall_approval_queue.mjs +1 -1
  254. package/tests/hall_board_world.mjs +2 -2
  255. package/tests/hall_builders_page_pass.mjs +1 -1
  256. package/tests/hall_city.mjs +1 -1
  257. package/tests/hall_fullscreen.mjs +27 -3
  258. package/tests/hall_kit_charts.mjs +1 -1
  259. package/tests/hall_landing_boot.mjs +1 -1
  260. package/tests/hall_my_agents.mjs +16 -16
  261. package/tests/hall_nav.mjs +22 -15
  262. package/tests/hall_options_menu.mjs +247 -0
  263. package/tests/hall_oversight_world.mjs +1 -1
  264. package/tests/hall_palette.mjs +8 -9
  265. package/tests/hall_profile_world.mjs +3 -3
  266. package/tests/hall_record_world.mjs +2 -2
  267. package/tests/hall_settings_world.mjs +2 -1
  268. package/tests/hall_sitemap.mjs +6 -5
  269. package/tests/hall_start_screen.mjs +244 -0
  270. package/tests/hall_street.mjs +2 -2
  271. package/tests/hall_street_inside.mjs +17 -9
  272. package/tests/hall_studio_world.mjs +3 -3
  273. package/tests/hall_visor.mjs +224 -0
  274. package/tests/help_requests.mjs +47 -15
  275. package/tests/helpers.mjs +4 -4
  276. package/tests/home_city.mjs +6 -4
  277. package/tests/home_city_widget.mjs +33 -14
  278. package/tests/home_gallery_widget.mjs +4 -4
  279. package/tests/home_idea_box.mjs +6 -6
  280. package/tests/hub_community_page.mjs +3 -3
  281. package/tests/hub_craft_rollup.mjs +75 -36
  282. package/tests/hub_recent_ships.mjs +3 -2
  283. package/tests/idea_cascade.mjs +2 -2
  284. package/tests/idea_credit_streams_db.mjs +1 -1
  285. package/tests/idea_detail_page.mjs +1 -1
  286. package/tests/idea_ideate_full.mjs +5 -2
  287. package/tests/idea_spark_hall.mjs +1 -1
  288. package/tests/init.mjs +3 -3
  289. package/tests/instance_manifest.mjs +2 -2
  290. package/tests/kickoff_checklist.mjs +1 -1
  291. package/tests/leaderboard_roles.mjs +34 -25
  292. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  293. package/tests/lightbulb.mjs +14 -10
  294. package/tests/main_discipline.mjs +8 -8
  295. package/tests/markdown_line_endings.mjs +1 -1
  296. package/tests/market_watch_pages.mjs +52 -74
  297. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  298. package/tests/migration_allocation.mjs +1 -1
  299. package/tests/mingle.mjs +36 -21
  300. package/tests/module-scope-map.mjs +22 -2
  301. package/tests/module_api.mjs +1 -0
  302. package/tests/module_contributions.mjs +13 -13
  303. package/tests/module_manifest.mjs +29 -0
  304. package/tests/module_skills_hook.mjs +2 -2
  305. package/tests/modules.mjs +14 -7
  306. package/tests/nav_permission_atoms.mjs +2 -1
  307. package/tests/needs_rebase.mjs +1 -1
  308. package/tests/newcomer_restock.mjs +13 -13
  309. package/tests/page_notes_panel.mjs +22 -22
  310. package/tests/page_tweak_no_block.mjs +1 -1
  311. package/tests/paintbrush.mjs +7 -7
  312. package/tests/privacy_matrix.mjs +1 -0
  313. package/tests/profile_activity.mjs +20 -23
  314. package/tests/profile_earnings.mjs +1 -1
  315. package/tests/profile_nudge_prefs.mjs +4 -4
  316. package/tests/profile_role_headline.mjs +73 -46
  317. package/tests/profile_role_stats.mjs +55 -45
  318. package/tests/profile_rollup_consent.mjs +19 -6
  319. package/tests/profile_route.mjs +12 -12
  320. package/tests/profile_ui_cross_project.mjs +4 -4
  321. package/tests/project_invite_suggestions.mjs +2 -2
  322. package/tests/promote_goal_id.mjs +1 -1
  323. package/tests/publish_manifest.mjs +1 -1
  324. package/tests/rank_floor_wiring.mjs +1 -1
  325. package/tests/regrade_actual_minutes.mjs +1 -1
  326. package/tests/removal_proposal.mjs +2 -2
  327. package/tests/reward_gate.mjs +4 -4
  328. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  329. package/tests/role_responsibilities.mjs +18 -12
  330. package/tests/role_stats_db.mjs +17 -16
  331. package/tests/scouting_rollup_fanout.mjs +5 -5
  332. package/tests/seed_tasks.mjs +5 -2
  333. package/tests/session_earnings_mirror_db.mjs +1 -1
  334. package/tests/session_reward_on_land_db.mjs +1 -1
  335. package/tests/session_reward_reconcile_db.mjs +2 -2
  336. package/tests/sessions_kit_pager.mjs +1 -1
  337. package/tests/settings_art_key_absent.mjs +2 -2
  338. package/tests/settings_main_role.mjs +21 -20
  339. package/tests/setup_flow.mjs +3 -3
  340. package/tests/setup_smoke.mjs +20 -0
  341. package/tests/ship_card.mjs +6 -6
  342. package/tests/specialities.mjs +16 -4
  343. package/tests/speciality_db_shape.mjs +6 -6
  344. package/tests/speciality_injection.mjs +17 -17
  345. package/tests/speciality_offer_ui.mjs +5 -5
  346. package/tests/speciality_offered.mjs +6 -5
  347. package/tests/speciality_session_skills.mjs +7 -7
  348. package/tests/speciality_skills.mjs +11 -11
  349. package/tests/speciality_walkthrough_ui.mjs +2 -2
  350. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  351. package/tests/task_classifier_vocab.mjs +10 -0
  352. package/tests/task_cli.mjs +11 -3
  353. package/tests/task_criteria_unlink_db.mjs +2 -2
  354. package/tests/task_discipline_required.mjs +38 -16
  355. package/tests/task_goal_required.mjs +4 -4
  356. package/tests/task_needs_db.mjs +1 -1
  357. package/tests/tweak_replaces_artist_review.mjs +1 -1
  358. package/tests/update_pill.mjs +153 -2
  359. package/tests/upkeep_home.mjs +1 -1
  360. package/tests/v3_c1_options_proof.mjs +145 -0
  361. package/tests/visual_review.mjs +3 -3
  362. package/tests/watch_applications_queue.mjs +8 -5
  363. package/tests/watch_sealed_floor.mjs +3 -1
  364. package/tests/welcome_boot.mjs +12 -9
  365. package/tests/welcome_boot_browser.mjs +13 -13
  366. package/tests/worktree_helper.mjs +16 -1
@@ -396,9 +396,9 @@ async function routeIdeaToGoal({
396
396
  // default — createTask auto-assigns the capped suggestion) and
397
397
  // `discipline` — NO LONGER 'unclassified' (task 1002988). createTask refuses a
398
398
  // craft it cannot infer; refusing a filer is the wrong trade for an INBOX, so we
399
- // ask the same question and fall back to 'engineer' (free to re-triage; a lost capture is not).
399
+ // ask the same question and fall back to 'artisan' (free to re-triage; a lost capture is not).
400
400
  const task = await lifecycle.createTask({
401
- discipline: (lifecycle.resolveWriteDiscipline?.({ title, description: bodyMd, touches: declaredTouches })?.discipline) || 'engineer',
401
+ discipline: (lifecycle.resolveWriteDiscipline?.({ title, description: bodyMd, touches: declaredTouches })?.discipline) || 'artisan',
402
402
  versionId: goal.version_id,
403
403
  title,
404
404
  description: bodyMd,
@@ -45,7 +45,7 @@ const { creditsEarnedBySql } = require('./db-tasks.js');
45
45
  // (migrations 086 + 176). Anything else is treated as "no filter" rather than
46
46
  // 400ing the read: a caller naming a craft that does not exist gets the honest
47
47
  // full list, not an error card in the middle of a profile.
48
- const CRAFTS = ['engineer', 'artist', 'ideator', 'ui', 'unclassified'];
48
+ const CRAFTS = ['artisan', 'artist', 'ui', 'unclassified'];
49
49
 
50
50
  // One page of an activity list. Clamped here as well as at the route, because
51
51
  // these are the functions a CLI or a later surface will call directly.
@@ -71,8 +71,8 @@
71
71
  // key string, stable + unique. It is half the source_ref, so RENAMING
72
72
  // a key re-files every task the rule ever generated. Treat it as
73
73
  // permanent.
74
- // discipline the role the generated work is FOR ('artist', 'ideator',
75
- // 'engineer'). This is the whole point of the mechanism: the
74
+ // discipline the role the generated work is FOR ('artist' or
75
+ // 'artisan'). This is the whole point of the mechanism: the
76
76
  // discipline of the generated task is not the discipline of the
77
77
  // ship that caused it.
78
78
  // when(task, ctx) a predicate over the shipped task row and the run CONTEXT.
@@ -216,7 +216,7 @@ const CASCADES = Object.freeze([
216
216
  key: REMOVAL_RULE_KEY,
217
217
  // ENGINEER, not artist: confirming asks whether deleted code is genuinely
218
218
  // gone or was moved, which is a question about the diff.
219
- discipline: 'engineer',
219
+ discipline: 'artisan',
220
220
  kind: 'unclassified',
221
221
  // The anti-recursion guard FIRST: a cascade-generated task must never
222
222
  // cascade, so it is rejected before any other test runs.
@@ -250,7 +250,7 @@ const TRIGGERS = Object.freeze({
250
250
  const IDEA_CASCADES = Object.freeze([
251
251
  Object.freeze({
252
252
  key: 'engineer-scoping-on-full-idea',
253
- discipline: 'engineer',
253
+ discipline: 'artisan',
254
254
  kind: 'unclassified',
255
255
  // A Full Idea is the ideator's hand-off point: the five questions are
256
256
  // answered and the thing is described well enough to cost. What it still
@@ -50,7 +50,7 @@ function specTaskFields(goal, { createdBy = null } = {}) {
50
50
  ].join('\n'),
51
51
  status: 'ready',
52
52
  kind: 'spike',
53
- discipline: 'ideator',
53
+ discipline: 'artist',
54
54
  source: SPEC_SOURCE,
55
55
  sourceRef: specSourceRef(id),
56
56
  createdBy,
@@ -14,19 +14,22 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- const { pool } = require('../../src/module-api');
17
+ const { pool, crafts: craftVocab } = require('../../src/module-api');
18
18
 
19
- // ADR 0174's crafts. Note the vocabulary seam: `builders.preferred_disciplines`
20
- // spells this same craft 'engineer', not 'builder'. THIS FUNCTION IS THE ONE PLACE
21
- // that knows the mapping — the matching surface (C02) must consume it rather than
22
- // re-deriving, or the two spellings drift and a builder silently matches nothing.
23
- const HELP_CRAFTS = Object.freeze(['artist', 'builder', 'ideator', 'ui']);
24
- const CRAFT_TO_DISCIPLINE = Object.freeze({ artist: 'artist', ideator: 'ideator', builder: 'engineer', ui: 'ui' });
19
+ // The crafts a request can be addressed to: the two working characters plus the
20
+ // module-contributed `ui`. Since task 1005141 (BONGOS-V3) the help-request spelling
21
+ // and the discipline spelling are THE SAME WORD — the old seam where this craft was
22
+ // 'builder' here and 'engineer' on a builder is closed. The maps stay, because they
23
+ // are also where an OLD spelling is read as today's: a rolled-back release can still
24
+ // write 'engineer'/'ideator' into preferred_disciplines, and craftMapSql() below runs
25
+ // against that column.
26
+ const HELP_CRAFTS = Object.freeze(['artisan', 'artist', 'ui']);
27
+ const CRAFT_TO_DISCIPLINE = Object.freeze({ artisan: 'artisan', artist: 'artist', ui: 'ui' });
25
28
 
26
- const DISCIPLINE_TO_CRAFT = Object.freeze({ artist: 'artist', ideator: 'ideator', engineer: 'builder', ui: 'ui' });
29
+ const DISCIPLINE_TO_CRAFT = Object.freeze({ artisan: 'artisan', artist: 'artist', ui: 'ui', engineer: 'artisan', ideator: 'artist' });
27
30
 
28
31
  function helpCrafts() { return [...HELP_CRAFTS]; }
29
- function disciplineForCraft(craft) { return CRAFT_TO_DISCIPLINE[craft] || null; }
32
+ function disciplineForCraft(craft) { return CRAFT_TO_DISCIPLINE[craftVocab.normalizeCraft(craft)] || null; }
30
33
  function craftForDiscipline(d) { return DISCIPLINE_TO_CRAFT[d] || null; }
31
34
 
32
35
  // The caller's OWN crafts, read from builders.preferred_disciplines. This exists
@@ -90,7 +93,8 @@ function capRows(n) {
90
93
  function validateHelpRequest(input) {
91
94
  const errors = [];
92
95
  const b = input || {};
93
- const craft = b.needs_craft == null ? null : String(b.needs_craft);
96
+ // An old spelling ('builder', 'engineer', 'ideator') is today's craft, not an unknown one.
97
+ const craft = b.needs_craft == null ? null : craftVocab.normalizeCraft(String(b.needs_craft));
94
98
  const person = b.needs_builder_id == null ? null : Number(b.needs_builder_id);
95
99
 
96
100
  if (craft === null && person === null) {
@@ -133,7 +137,7 @@ async function createHelpRequest(input, deps = {}) {
133
137
  (requested_by, needs_craft, needs_builder_id, what_is_stuck, task_id, goal_id)
134
138
  VALUES ($1, $2, $3, $4, $5, $6)
135
139
  RETURNING ${COLS}`,
136
- [input.requestedBy, input.needsCraft ?? null, input.needsBuilderId ?? null,
140
+ [input.requestedBy, input.needsCraft == null ? null : craftVocab.normalizeCraft(String(input.needsCraft)), input.needsBuilderId ?? null,
137
141
  String(input.whatIsStuck).trim(), input.taskId ?? null, input.goalId ?? null]
138
142
  );
139
143
  return rows[0];
@@ -150,7 +154,7 @@ async function createHelpRequest(input, deps = {}) {
150
154
  // EXCEPT its author, since answering your own request is not help.
151
155
  async function listOpenForBuilder({ builderId, crafts = [], limit = MAX_ROWS }, deps = {}) {
152
156
  const activePool = deps.pool || pool;
153
- const safeCrafts = (Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
157
+ const safeCrafts = craftVocab.normalizeCrafts(Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
154
158
  const { rows } = await activePool.query(
155
159
  `SELECT ${LIST_COLS} ${LIST_FROM}
156
160
  WHERE h.status = 'open'
@@ -193,7 +197,7 @@ async function countOpenForBuilder(builderId, deps = {}) {
193
197
  }
194
198
 
195
199
  // The DISCIPLINE_TO_CRAFT map as a SQL CASE, generated from the same object the
196
- // JS path uses — so the two can never disagree about what an 'engineer' is.
200
+ // JS path uses — so the two can never disagree about what an 'artisan' is.
197
201
  function craftMapSql(col) {
198
202
  const whens = Object.entries(DISCIPLINE_TO_CRAFT)
199
203
  .map(([d, c]) => `WHEN '${d}' THEN '${c}'`).join(' ');
@@ -293,7 +297,7 @@ async function settleFromLedger(deps = {}) {
293
297
  // The author exclusion stays too: your own settled asks are the OTHER list.
294
298
  async function listArchivedForBuilder({ builderId, crafts = [], limit = MAX_ROWS }, deps = {}) {
295
299
  const activePool = deps.pool || pool;
296
- const safeCrafts = (Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
300
+ const safeCrafts = craftVocab.normalizeCrafts(Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
297
301
  const { rows } = await activePool.query(
298
302
  `SELECT ${LIST_COLS} ${LIST_FROM}
299
303
  WHERE h.status <> 'open'
@@ -350,7 +354,7 @@ async function getHelpRequest(id, deps = {}) {
350
354
  // closing an ask is not accepting work, it is ending a question.
351
355
  async function setHelpRequestStatus({ id, builderId, status, crafts = [] }, deps = {}) {
352
356
  const activePool = deps.pool || pool;
353
- const safeCrafts = (Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
357
+ const safeCrafts = craftVocab.normalizeCrafts(Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
354
358
  const askerOnly = status === 'withdrawn';
355
359
  const { rows } = await activePool.query(
356
360
  `UPDATE lifecycle_help_requests
@@ -59,11 +59,11 @@ const MERGE_GATE = {
59
59
 
60
60
  const INSTANCE_LOOK = {
61
61
  title: 'Fill the instance look with your own LLM',
62
- // 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
62
+ // 'artist' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
63
63
  // kind and the task silently vanished from the checklist (the one non-infra
64
64
  // kickoff step). Deciding the instance look is a decision by an ideator.
65
65
  kind: 'decision',
66
- discipline: 'ideator',
66
+ discipline: 'artist',
67
67
  description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
68
68
  'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
69
69
  };
@@ -178,7 +178,7 @@ function genesisChecklist({ existing = false } = {}) {
178
178
  // The ref names the step, never its position, so the brownfield step slotting in
179
179
  // ahead of scope's own cannot re-key it into a duplicate.
180
180
  out[stage] = list.map((t) => ({
181
- ...t, kind: 'decision', discipline: 'ideator', ref: t === GENESIS_EXISTING_FIRST ? 'genesis:scope:existing' : `genesis:${stage}`,
181
+ ...t, kind: 'decision', discipline: 'artist', ref: t === GENESIS_EXISTING_FIRST ? 'genesis:scope:existing' : `genesis:${stage}`,
182
182
  }));
183
183
  }
184
184
  return out;
@@ -57,7 +57,7 @@ async function seedKickoffIfEmpty({ builderId = null } = {}, deps = {}) {
57
57
  description: t.description || '',
58
58
  status: 'ready',
59
59
  kind: t.kind || 'infra',
60
- discipline: t.discipline || 'engineer',
60
+ discipline: t.discipline || 'artisan',
61
61
  source: 'first-sign-in',
62
62
  createdBy: builderId,
63
63
  // BV1.R11 (task 1003598): this runs on an instance with zero tasks and, in
@@ -0,0 +1,29 @@
1
+ -- lifecycle_021_crafts_artisan_artist.sql — task 1005141 (BONGOS-V3 C1): the lifecycle
2
+ -- module's craft columns follow core_296's rename (engineer → artisan, ideator → artist).
3
+ --
4
+ -- lifecycle_help_requests.needs_craft spelled the engineering craft 'builder' (ADR 0174);
5
+ -- that seam closes here — it is 'artisan' like everywhere else. The CHECK is a superset
6
+ -- of old and new words so a rolled-back release can still write (see core_296).
7
+ --
8
+ -- lifecycle_mingle_pairs records the two crafts AS MATCHED, for audit, under a
9
+ -- cross-craft CHECK, in the help-request spelling. builder → artisan is renamed. An artist × ideator pair is left
10
+ -- exactly as recorded: the fold would make it artist × artist, which is both a false
11
+ -- record of why it was matched and a CHECK violation. The code normalizes on read.
12
+
13
+ BEGIN;
14
+
15
+ ALTER TABLE lifecycle_help_requests DROP CONSTRAINT IF EXISTS lifecycle_help_requests_craft_chk;
16
+ UPDATE lifecycle_help_requests SET needs_craft = 'artisan' WHERE needs_craft IN ('builder', 'engineer');
17
+ UPDATE lifecycle_help_requests SET needs_craft = 'artist' WHERE needs_craft = 'ideator';
18
+ ALTER TABLE lifecycle_help_requests ADD CONSTRAINT lifecycle_help_requests_craft_chk
19
+ CHECK (needs_craft IS NULL OR needs_craft IN ('artisan', 'artist', 'patron', 'ui', 'builder', 'ideator'));
20
+
21
+ UPDATE lifecycle_mingle_pairs SET craft_lo = 'artisan'
22
+ WHERE craft_lo IN ('builder', 'engineer') AND craft_hi <> 'artisan';
23
+ UPDATE lifecycle_mingle_pairs SET craft_hi = 'artisan'
24
+ WHERE craft_hi IN ('builder', 'engineer') AND craft_lo <> 'artisan';
25
+
26
+ INSERT INTO schema_migrations (version) VALUES ('lifecycle_021_crafts_artisan_artist')
27
+ ON CONFLICT (version) DO NOTHING;
28
+
29
+ COMMIT;
@@ -38,7 +38,7 @@
38
38
  //
39
39
  // THE CRAFT VOCABULARY IS NOT REDEFINED HERE. help-requests.js declares itself the
40
40
  // one place that knows how a craft maps to `builders.preferred_disciplines`
41
- // ('builder' the craft is 'engineer' the discipline), so this file consumes
41
+ // (and reads an old 'engineer'/'ideator' discipline as today's), so this file consumes
42
42
  // `craftMapSql` / `craftForDiscipline` from it rather than re-deriving. Two
43
43
  // spellings of the same craft would silently match nobody.
44
44
  //
@@ -67,14 +67,14 @@ const MAX_ROWS = 100;
67
67
  const MATCHING = Object.freeze({
68
68
  rule: 'cross_craft',
69
69
  description:
70
- 'A pair is two builders whose crafts do not overlap at all — an engineer with '
71
- + 'an artist or an ideator, never two of the same. The premise is that the '
70
+ 'A pair is two builders whose crafts do not overlap at all — an artisan with '
71
+ + 'an artist or a UI designer, never two of the same. The premise is that the '
72
72
  + 'person you need is often not another one of you.',
73
73
  // Stated because it is the road not taken, and an unexplained absence reads as
74
74
  // an oversight to the next person holding the matcher.
75
75
  rejected: 'same_craft — it duplicates the craft-matched Collab request path (criterion collab-page-matches-my-craft).',
76
76
  // DISJOINT, not merely "different". A builder who is both an artist and an
77
- // engineer shares a craft with every engineer, so pairing them would put two of
77
+ // artisan shares a craft with every artisan, so pairing them would put two of
78
78
  // the same in a room and call it cross-craft.
79
79
  overlap: 'none',
80
80
  consent: 'Both switches required: the project enables mingle AND the builder opts in. Either off means no pair.',
@@ -161,7 +161,7 @@ async function submitPageTweak({ taskId, builderId, versionId = null, freeze },
161
161
  // page does not wait for it.
162
162
  status: 'backlog',
163
163
  kind: 'feature',
164
- discipline: 'engineer',
164
+ discipline: 'artisan',
165
165
  priority: 4,
166
166
  touches: u.touches || [],
167
167
  source: u.source,
@@ -23,7 +23,7 @@ const hierarchy = require('../hierarchy');
23
23
  const { suggestCriteria } = require('../criterion-suggest');
24
24
  const { buildGoalAdvisory } = require('../goal-advisory');
25
25
  const { buildDependencyAdvisory } = require('../dependency-advisory');
26
- const { classifyTaskKind, VALID_KINDS, WRITABLE_DISCIPLINES, resolveWriteDiscipline, VOTABLE_TASK_STATUSES, TASK_FIELD_BOUNDS } = require('../task-classifier');
26
+ const { classifyTaskKind, VALID_KINDS, WRITABLE_DISCIPLINES, resolveWriteDiscipline, VOTABLE_TASK_STATUSES, TASK_FIELD_BOUNDS, normalizeDisciplineValue } = require('../task-classifier');
27
27
  const { isModuleKey, moduleKeys } = api.moduleScopeMap;
28
28
  const { validateOrRespond, LIMITS, parseId } = api;
29
29
  const { badKind, badNeeds, detectsMigration } = require('./task-route-helpers.js');
@@ -438,6 +438,8 @@ module.exports = function registerTaskWriteRoutes(router) {
438
438
  const hasParent = Object.prototype.hasOwnProperty.call(body, 'parent_task_id');
439
439
  const hasKind = Object.prototype.hasOwnProperty.call(body, 'kind');
440
440
  const hasDiscipline = Object.prototype.hasOwnProperty.call(body, 'discipline');
441
+ // An old craft spelling is today's craft (task 1005141): written as the new word.
442
+ if (hasDiscipline) body.discipline = normalizeDisciplineValue(body.discipline);
441
443
  // V3.R15 #239: newcomer_friendly is the canonical name; xenos_claimable is
442
444
  // the pre-rename alias and still accepted. Either presence triggers the
443
445
  // same update path.
@@ -63,7 +63,7 @@ const NO_PR_STATUS = Object.freeze({
63
63
  pr_state: 'none', merged: false, merge_sha: null, deploy_status: 'none', pr_url: null, pr_number: null, pr_title: null, pr_body: null,
64
64
  branch_hint: 'No pull request titled for this task is among the 100 most recent. Pass ?branch=<name> to ask about a specific branch.' });
65
65
  const seams = api;
66
- const { classifyTaskKind, VALID_KINDS, VALID_DISCIPLINES, VOTABLE_TASK_STATUSES, TASK_FIELD_BOUNDS } = require('../task-classifier');
66
+ const { classifyTaskKind, VALID_KINDS, VALID_DISCIPLINES, VOTABLE_TASK_STATUSES, TASK_FIELD_BOUNDS, normalizeDisciplineValue } = require('../task-classifier');
67
67
  const { isModuleKey, moduleKeys } = api.moduleScopeMap;
68
68
  const { validateOrRespond, LIMITS, parseId } = api;
69
69
  // The onboarding state machine + the evergreen newcomer-chore restock carved into
@@ -146,7 +146,7 @@ module.exports = function buildTasksRouter() {
146
146
  const CREATED_BY_MSG = "created_by must be a positive builder id, or 'none' for the unattributed.";
147
147
 
148
148
  router.get('/tasks', auth.requireBuilder, async (req, res) => {
149
- const { version, status, kind, discipline, goal_id } = req.query;
149
+ const { version, status, kind, goal_id } = req.query; const discipline = normalizeDisciplineValue(req.query.discipline); // old craft spellings read as today's (task 1005141)
150
150
  if (kind && !VALID_KINDS.has(kind)) {
151
151
  return badKind(res);
152
152
  }
@@ -203,7 +203,7 @@ module.exports = function buildTasksRouter() {
203
203
  router.get('/tasks/claimable',
204
204
  auth.requireBuilder,
205
205
  async (req, res) => {
206
- const { version, discipline, fit } = req.query;
206
+ const { version, fit } = req.query; const discipline = normalizeDisciplineValue(req.query.discipline);
207
207
  if (discipline && !VALID_DISCIPLINES.has(discipline)) {
208
208
  return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
209
209
  }
@@ -223,9 +223,8 @@ function fmtMins(n) {
223
223
  // "Open the work" button firing a slash command that no longer resolves. A
224
224
  // module-contributed discipline still routes to its skill ('ui' -> /design).
225
225
  const DISCIPLINE_PACK = {
226
- engineer: 'docs/packs/engineer.md',
226
+ artisan: 'docs/packs/artisan.md',
227
227
  artist: 'docs/packs/artist.md',
228
- ideator: 'docs/packs/ideator.md',
229
228
  };
230
229
  const DISCIPLINE_SKILL = { ui: 'design' };
231
230
 
@@ -241,7 +240,7 @@ function nextActionFor(stage, { taskId, discipline, sandboxUrl } = {}) {
241
240
  if (skill) return { kind: 'prompt', label: 'Open the work', cmd: `/${skill}` };
242
241
  // Default to the Engineer pack for an unknown or unset discipline, the same way
243
242
  // this used to default to /dev: engineering is the project's default loop.
244
- const pack = DISCIPLINE_PACK[discipline] || DISCIPLINE_PACK.engineer;
243
+ const pack = DISCIPLINE_PACK[discipline] || DISCIPLINE_PACK.artisan;
245
244
  return { kind: 'prompt', label: 'Open the work', cmd: `Read ${pack} and start the work` };
246
245
  }
247
246
  return { kind: 'prompt', label: 'Continue', cmd: '/builder-start' };
@@ -117,18 +117,18 @@ function classifyTaskKind(title, description) {
117
117
  // -------------------------------------------------------------------------
118
118
  //
119
119
  // Orthogonal to kind. Classifies the builder-interest axis (migration 086):
120
- // engineer — the prompter is reasoning about algorithms, infra, data
120
+ // artisan — the prompter is reasoning about algorithms, infra, data
121
121
  // plumbing, dev tooling, scanners, schemas, integrations.
122
122
  // The person guiding the prompt brings software-engineering
123
123
  // experience.
124
- // artist — producing or shipping visual artifacts: sprite/tile
125
- // generation, palettes, walk animations, aesthetic passes.
126
- // ideator — thinking/writing/scoping: quest narrative, world/level
127
- // design, lore, briefs, research, naming.
124
+ // artist — producing or shipping visual artifacts (sprite/tile
125
+ // generation, palettes, walk animations, aesthetic passes)
126
+ // and, since task 1005141, the old ideator's lane too:
127
+ // thinking/writing/scoping, narrative, lore, briefs, naming.
128
128
  // unclassified — signals weak or balanced; hand-triage.
129
129
  //
130
130
  // The scoring engine first decides engineer-vs-creative; a creative verdict
131
- // is then split into artist/ideator by splitCreativeDiscipline().
131
+ // is then split into artist/ui by splitCreativeDiscipline().
132
132
  //
133
133
  // Calibrated against Lars's hand-classifications of 25 tasks (mig 014
134
134
  // backfill, 2026-05-12). Key insights from those decisions:
@@ -148,11 +148,11 @@ function classifyTaskKind(title, description) {
148
148
  // summed and capped. The 0.4 description weight is what stops incidental
149
149
  // creative phrasing in long descriptions from flipping engineering tasks.
150
150
 
151
- // Migration 086 split the old "creative" lane into 'artist' (visual asset
152
- // production) and 'ideator' (thinking/writing/scoping) and renamed
153
- // 'engineering' → 'engineer'. The binary scoring engine below still decides
154
- // engineer-vs-creative; a creative verdict is then disambiguated into
155
- // artist/ideator/ui by splitCreativeDiscipline(). When that split is unclear,
151
+ // Migration 086 split the old "creative" lane into artist (visual asset
152
+ // production) and ideator (thinking/writing/scoping); task 1005141 folded the
153
+ // ideator back into the artist craft and renamed engineer → artisan. The binary
154
+ // scoring engine below still decides engineer-vs-creative; a creative verdict
155
+ // is then disambiguated into artist/ui by splitCreativeDiscipline(). When that split is unclear,
156
156
  // it returns 'unclassified' (hand-triage) — matching the #615 backfill rule.
157
157
  //
158
158
  // Migration 176 (task 1376) added 'ui' — UI/interface DESIGN work (Figma,
@@ -160,7 +160,7 @@ function classifyTaskKind(title, description) {
160
160
  // pixel-art `artist` lane (ADR 0079): producing interface designs is a
161
161
  // distinct discipline from producing pixel-art assets, and routes to its own
162
162
  // /design work-mode (discipline-modes.json) instead of /paint.
163
- const VALID_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui', 'unclassified']);
163
+ const VALID_DISCIPLINES = new Set(['artisan', 'artist', 'ui', 'unclassified']);
164
164
 
165
165
  // The disciplines a WRITE may set (task 1002988). 'unclassified' is deliberately
166
166
  // absent: it is a triage state, not a craft, and a create that lands one produces
@@ -169,12 +169,18 @@ const VALID_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui', 'uncla
169
169
  // ?discipline=unclassified filter keep resolving while the backfill runs.
170
170
  // 'ui' stays: ADR 0272 keeps it as a module-contributed discipline, and the owner
171
171
  // reconfirmed that on 2026-09-17 when abandoning task 1002987.
172
- const WRITABLE_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui']);
172
+ const WRITABLE_DISCIPLINES = new Set(['artisan', 'artist', 'ui']);
173
173
 
174
174
  // Mirrors the `kind` branch in the create routes: act on the classifier only when
175
175
  // it is at least half sure, otherwise treat it as no answer at all.
176
176
  const DISCIPLINE_CONFIDENCE_FLOOR = 0.5;
177
177
 
178
+ // The one place this module reads an old craft spelling as today's (task 1005141):
179
+ // engineer → artisan, ideator → artist. 'unclassified' and 'ui' pass through.
180
+ function normalizeDisciplineValue(d) {
181
+ return typeof d === 'string' ? require('../../src/module-api').crafts.normalizeCraft(d) : d;
182
+ }
183
+
178
184
  // Resolve the discipline for a task WRITE. Honours an explicit valid value,
179
185
  // otherwise INFERS from title + description + touches, and reports failure so the
180
186
  // caller can 400 — per the owner decision of 2026-09-17, a bare 400 on a missing
@@ -186,6 +192,8 @@ const DISCIPLINE_CONFIDENCE_FLOOR = 0.5;
186
192
  // have left the other quietly minting the very rows this task exists to remove.
187
193
  // Returns { discipline, inferred?, confidence? } or { error, unclassifiable? }.
188
194
  function resolveWriteDiscipline({ discipline, title, description, touches } = {}) {
195
+ // An old spelling ('engineer', 'ideator') is today's craft, not a bad one (task 1005141).
196
+ discipline = normalizeDisciplineValue(discipline);
189
197
  if (discipline != null && discipline !== 'unclassified') {
190
198
  return WRITABLE_DISCIPLINES.has(discipline) ? { discipline } : { error: 'bad_discipline' };
191
199
  }
@@ -228,10 +236,10 @@ const TASK_FIELD_BOUNDS = {
228
236
  const VOTABLE_TASK_STATUSES = new Set(['confirmed', 'shipped']);
229
237
 
230
238
  // Disambiguation signals for the creative lane (migration 086). Visual
231
- // art-production cues → artist; narrative / design-thinking / writing cues →
232
- // ideator. Scored by distinct-rule hit count over title+description, plus
233
- // touches[] path prefixes; the stronger side wins, ties / no-signal fall to
234
- // 'unclassified' for hand-triage.
239
+ // art-production cues and narrative / design-thinking / writing cues are both
240
+ // the artist craft since task 1005141 (the ideator folded into it); the two
241
+ // lists stay separate so each reads as what it detects. Scored by distinct-rule
242
+ // hit count over title+description, plus touches[] path prefixes.
235
243
  const ARTIST_SIGNALS = [
236
244
  /\bpixel[-\s]?art\b/i,
237
245
  /\btile-?sheets?\b/i,
@@ -278,23 +286,24 @@ const UI_PATH_PREFIXES = [
278
286
  'docs/design/',
279
287
  ];
280
288
 
281
- // Split a 'creative' verdict into 'artist' | 'ideator' | 'ui' | 'unclassified'.
282
- // Three sub-lanes are scored by distinct-rule hit count + touches[] hits; the
283
- // strongest single lane wins. A tie at the top (including 0-0-0) → hand-triage.
289
+ // Split a 'creative' verdict into 'artist' | 'ui' | 'unclassified'.
290
+ // The art and ideas signals are summed into ONE artist score (one craft since
291
+ // task 1005141 — scoring them as two lanes would call an even art/ideas task a
292
+ // tie and send it to hand-triage); it competes with the ui score. The stronger
293
+ // lane wins; a tie (including 0-0) → hand-triage.
284
294
  function splitCreativeDiscipline(title, description, touches) {
285
295
  const text = `${title || ''} ${description || ''}`;
286
296
  const artist = ARTIST_SIGNALS.filter((re) => re.test(text)).length
287
- + countTouchesHits(touches, ARTIST_PATH_PREFIXES);
288
- const ideator = IDEATOR_SIGNALS.filter((re) => re.test(text)).length
289
- + countTouchesHits(touches, IDEATOR_PATH_PREFIXES);
297
+ + countTouchesHits(touches, ARTIST_PATH_PREFIXES)
298
+ + IDEATOR_SIGNALS.filter((re) => re.test(text)).length
299
+ + countTouchesHits(touches, IDEATOR_PATH_PREFIXES);
290
300
  const ui = UI_SIGNALS.filter((re) => re.test(text)).length
291
301
  + countTouchesHits(touches, UI_PATH_PREFIXES);
292
302
  const ranked = [
293
303
  { d: 'artist', n: artist },
294
- { d: 'ideator', n: ideator },
295
304
  { d: 'ui', n: ui },
296
305
  ].sort((a, b) => b.n - a.n);
297
- // No signal anywhere, or the top two are tied → hand-triage.
306
+ // No signal anywhere, or the two lanes are tied → hand-triage.
298
307
  if (ranked[0].n === 0 || ranked[0].n === ranked[1].n) return 'unclassified';
299
308
  return ranked[0].d;
300
309
  }
@@ -326,7 +335,7 @@ const CREATIVE_TEXT_RULES = [
326
335
  // UI/interface DESIGN cues (mig 176 / task 1376) — route the creative verdict
327
336
  // to the `ui` sub-lane via splitCreativeDiscipline. Kept here (not engineering)
328
337
  // so "UI redesign of the hall" / "wireframe the checkout" land creative-first;
329
- // splitCreativeDiscipline then picks `ui` over `artist`/`ideator`.
338
+ // splitCreativeDiscipline then picks `ui` over `artist`.
330
339
  { re: /\b(UI|UX)\s+(design|redesign|pass|polish|refresh|overhaul)\b/i, weight: 0.8 },
331
340
  { re: /\b(user\s+(interface|experience)|interface\s+design|wireframes?|mock-?ups?)\b/i, weight: 0.8 },
332
341
  { re: /\b(figma|claude[-\s]?design)\b.*\b(redesign|design|layout|screen|mock-?up|wireframe)\b/i, weight: 0.8 },
@@ -503,7 +512,7 @@ function classifyTaskDiscipline(title, description, touches) {
503
512
  const d = splitCreativeDiscipline(titleText, descText, touches);
504
513
  return { discipline: d, confidence: d === 'unclassified' ? 0 : cT.max };
505
514
  }
506
- return { discipline: 'engineer', confidence: eT.max };
515
+ return { discipline: 'artisan', confidence: eT.max };
507
516
  }
508
517
  // (b) Same max-weight (e.g. both 0.85). Combine title-rule count and
509
518
  // touches[] hit count into a single asymmetry score — a side that
@@ -517,7 +526,7 @@ function classifyTaskDiscipline(title, description, touches) {
517
526
  const d = splitCreativeDiscipline(titleText, descText, touches);
518
527
  return { discipline: d, confidence: d === 'unclassified' ? 0 : 0.6 + Math.min(0.3, cAsym * 0.05) };
519
528
  }
520
- return { discipline: 'engineer', confidence: 0.6 + Math.min(0.3, -cAsym * 0.05) };
529
+ return { discipline: 'artisan', confidence: 0.6 + Math.min(0.3, -cAsym * 0.05) };
521
530
  }
522
531
  return { discipline: 'unclassified', confidence: 0 };
523
532
  }
@@ -541,7 +550,7 @@ function classifyTaskDiscipline(title, description, touches) {
541
550
  const d = splitCreativeDiscipline(titleText, descText, touches);
542
551
  return { discipline: d, confidence: d === 'unclassified' ? 0 : creative };
543
552
  }
544
- return { discipline: 'engineer', confidence: engineering };
553
+ return { discipline: 'artisan', confidence: engineering };
545
554
  }
546
555
 
547
556
  module.exports = {
@@ -550,6 +559,7 @@ module.exports = {
550
559
  buildBlockerVendorRe,
551
560
  VALID_KINDS,
552
561
  VALID_DISCIPLINES,
562
+ normalizeDisciplineValue,
553
563
  WRITABLE_DISCIPLINES,
554
564
  resolveWriteDiscipline,
555
565
  VOTABLE_TASK_STATUSES,
@@ -41,7 +41,7 @@ const shipped = (over = {}) => ({
41
41
  version_id: 'BONGOS-V1',
42
42
  goal_id: '1000074',
43
43
  title: 'R09 · Reword the hall empty states',
44
- discipline: 'engineer',
44
+ discipline: 'artisan',
45
45
  kind: 'feature',
46
46
  status: 'shipped',
47
47
  ...over,
@@ -51,7 +51,7 @@ const shipped = (over = {}) => ({
51
51
  const REVIEW_RULE = {
52
52
  key: 'test-artist-review',
53
53
  discipline: 'artist',
54
- when: (t) => t.discipline === 'engineer',
54
+ when: (t) => t.discipline === 'artisan',
55
55
  title: (t) => `Review the copy that shipped in task ${t.id}`,
56
56
  body: (t) => `Look at what ${t.title} put in front of a person.`,
57
57
  };
@@ -195,7 +195,7 @@ await test('the production table is valid, and a duplicate key is refused', () =
195
195
 
196
196
  const shippedCopy = (over = {}) => ({
197
197
  id: '7001', version_id: 'BONGOS-V1', goal_id: '1000074',
198
- title: 'Reword the hall empty states', discipline: 'engineer',
198
+ title: 'Reword the hall empty states', discipline: 'artisan',
199
199
  kind: 'feature', status: 'shipped', module_key: 'hall-ui', source: 'manual',
200
200
  ...over,
201
201
  });
@@ -262,8 +262,8 @@ await test('"copy or visuals" is a NARROW predicate — three ways in, and a def
262
262
  assert.equal(H(shippedCopy({ module_key: null, source: 'copy-proposal' })), true, 'a landed copy proposal');
263
263
  assert.equal(H(shippedCopy({ module_key: null, source: 'manual', discipline: 'artist' })), true, 'work an artist did');
264
264
  // The default is NO — a backend/infra ship is not copy/visual work.
265
- assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'engineer', source: 'manual' })), false);
266
- assert.equal(H(shippedCopy({ module_key: null, discipline: 'engineer', source: 'manual' })), false);
265
+ assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'artisan', source: 'manual' })), false);
266
+ assert.equal(H(shippedCopy({ module_key: null, discipline: 'artisan', source: 'manual' })), false);
267
267
  assert.equal(H(null), false);
268
268
  });
269
269
 
@@ -303,7 +303,7 @@ await test('the runner still hands the artist gate to the rules, and a reader th
303
303
 
304
304
  await test('a page file changed under modules/*/public/ is visual work whatever the label says', () => {
305
305
  const H = cascade.hasCopyOrVisualWork;
306
- const unlabelled = shippedCopy({ module_key: null, discipline: 'engineer' });
306
+ const unlabelled = shippedCopy({ module_key: null, discipline: 'artisan' });
307
307
  assert.equal(H(unlabelled), false, 'the label alone says no — the gap task 1003480 fell through');
308
308
  for (const p of [
309
309
  'modules/hall-ui/public/goal-forest-lib.js',
@@ -316,7 +316,7 @@ await test('a page file changed under modules/*/public/ is visual work whatever
316
316
 
317
317
  await test('a backend file in a UI module is NOT visual work — page files only', () => {
318
318
  const H = cascade.hasCopyOrVisualWork;
319
- const unlabelled = shippedCopy({ module_key: null, discipline: 'engineer' });
319
+ const unlabelled = shippedCopy({ module_key: null, discipline: 'artisan' });
320
320
  assert.equal(H(unlabelled, [
321
321
  'modules/hall-ui/module.json',
322
322
  'modules/public-landing/routes/hub.js',
@@ -55,6 +55,9 @@ const KEYS = Object.freeze({
55
55
  // When the planet came alive (founding switched off). With STARTED_AT it bounds the
56
56
  // window a builder must have joined in to carry the founding mark (task 1004424).
57
57
  ENDED_AT: 'founding.ended_at',
58
+ // When the owner skipped genesis instead of finishing it (genesis-home.js skipGenesis). Write-once, so the
59
+ // record says the planet came alive by skip, not by the five stages.
60
+ SKIPPED_AT: 'founding.skipped_at',
58
61
  // Genesis is OFF unless a project turns it on (owner, 2026-11-01: not polished enough, so every
59
62
  // project opens as the full hall). Read as ordinary 'true'; absent or anything else is OFF, which is
60
63
  // also what pauses a project that was already mid-founding. A demo, or a project born with genesis,
@@ -426,6 +426,26 @@ async function comeAlive(builderId, deps = {}) {
426
426
  return { ok: true };
427
427
  }
428
428
 
429
+ // skipGenesis — the owner's way out of genesis from ANY stage (incl. decide), with no Board
430
+ // Room sitting and no finished-tasks gate. Founding mode switches off exactly as comeAlive
431
+ // does (ended_at stamped, the full hall opens), and the skip is recorded once in
432
+ // founding.skipped_at with the stage it left, so the record shows it was skipped, not
433
+ // finished. Genesis still happens once (spec D8): a project that is already alive refuses.
434
+ // Filed genesis tasks stay on the board; nothing is deleted.
435
+ async function skipGenesis(builderId, deps = {}) {
436
+ const s = seams(deps);
437
+ const ended = await s.get(fm.KEYS.ENDED_AT);
438
+ if (ended && ended.value) return { ok: false, reason: 'already_alive' };
439
+ const mode = await fm.foundingModeFor(modeDeps(s));
440
+ if (!mode) return { ok: false, reason: 'not_founding' };
441
+ const prior = await s.get(fm.KEYS.SKIPPED_AT);
442
+ if (!prior || !prior.value) {
443
+ await s.set(fm.KEYS.SKIPPED_AT, JSON.stringify({ at: (s.now || new Date()).toISOString(), stage: mode.stage }), builderId);
444
+ }
445
+ await fm.setFoundingMode({ active: false }, builderId, modeDeps(s));
446
+ return { ok: true, skipped: true, from_stage: mode.stage };
447
+ }
448
+
429
449
  // onStagePassed — the `board.genesis_stage.passed` listener. Best-effort: the next read
430
450
  // reconciles anyway.
431
451
  async function onStagePassed(deps = {}) {
@@ -434,7 +454,7 @@ async function onStagePassed(deps = {}) {
434
454
 
435
455
  module.exports = {
436
456
  PLAN_KEY, STAGE_INFO, PROPER, DEMO_PEOPLE, DEMO_HOURS, intIn, parsePlan, shapeGenesis, demoMinutes, demoPlan,
437
- readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, onStagePassed,
457
+ readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, skipGenesis, onStagePassed,
438
458
  // the founding-mode deps this file reads the mode with, for founding-birth.js (task 1004506)
439
459
  modeDepsFor: (deps) => modeDeps(seams(deps)),
440
460
  };
@@ -186,4 +186,6 @@ function mountGenesisRoutes(router) {
186
186
  }
187
187
  });
188
188
  router.post('/founding/alive', api.requireBuilder, owns, handle((req) => genesis.comeAlive(req.builder.id)));
189
+ // Skip genesis: the owner opens the full hall from any stage, no sitting, no gate.
190
+ router.post('/founding/skip', api.requireBuilder, owns, handle((req) => genesis.skipGenesis(req.builder.id)));
189
191
  }
@@ -91,10 +91,11 @@ function referencedImageIds(bodyMd) {
91
91
  // the ideas are triaged like any other, and nothing here grants anything.
92
92
  const FENCE_LOOKALIKE_RE = /~{4,}[A-Za-z0-9-]*~{4,}/g;
93
93
  // Two writers share the split: the artist's feedback doc (the palette) and the
94
- // ideator's notes (the lightbulb, task 1004498). Only the framing words differ.
94
+ // artist's notes (the lightbulb, task 1004498; the ideator folded into the artist in
95
+ // task 1005141, so the second voice is named for what it writes). Only the framing words differ.
95
96
  const VOICES = {
96
97
  artist: { who: 'artist', what: 'feedback about one page of a web app', ask: 'separate change the artist asks for or problem they point at', label: 'ARTIST-FEEDBACK', doc: 'doc', split: 'feedback' },
97
- ideator: { who: 'ideator', what: 'notes, written while looking at one page of a web app', ask: 'separate idea, change or problem the ideator raises', label: 'IDEATOR-NOTES', doc: 'notes', split: 'notes' },
98
+ notes: { who: 'artist', what: 'notes, written while looking at one page of a web app', ask: 'separate idea, change or problem the artist raises', label: 'IDEATOR-NOTES', doc: 'notes', split: 'notes' },
98
99
  };
99
100
  function buildSplitPrompt({ pageTitle, pageId, bodyMd, voice = 'artist', nonce = crypto.randomBytes(8).toString('hex') }) {
100
101
  const v = VOICES[voice] || VOICES.artist;