@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.
- package/.bongos-core.json +495 -350
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
- package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
- package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
- package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +195 -3
- package/docs/api-reference.md +8 -4
- package/docs/architecture.md +7 -3
- package/docs/copy-inventory.md +390 -370
- package/docs/copy-registry.json +719 -518
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +5 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/docs/packs/{engineer.md → artisan.md} +5 -5
- package/docs/packs/artist.md +50 -4
- package/docs/page-inventory.json +15 -4
- package/docs/page-readings.json +3182 -3336
- package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
- package/migrations/core_296_crafts_artisan_artist.sql +72 -0
- package/migrations/core_297_builder_character.sql +30 -0
- package/modules/copy-desk/proposals.js +1 -1
- package/modules/economy/credits-by-craft.js +29 -15
- package/modules/hall-ui/public/ai-studio.html +4 -4
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/characters/artisan.png +0 -0
- package/modules/hall-ui/public/characters/artist.png +0 -0
- package/modules/hall-ui/public/characters/patron.png +0 -0
- package/modules/hall-ui/public/city.css +2 -1
- package/modules/hall-ui/public/city.html +9 -6
- package/modules/hall-ui/public/city.js +7 -1
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/diagram-viewer.js +2 -1
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/goal-map.html +9 -6
- package/modules/hall-ui/public/goals.html +5 -5
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/hall-render.js +3 -1
- package/modules/hall-ui/public/home-city-draw.js +3 -3
- package/modules/hall-ui/public/home-city.css +4 -0
- package/modules/hall-ui/public/home-city.js +17 -9
- package/modules/hall-ui/public/home-gallery.js +8 -1
- package/modules/hall-ui/public/home-idea-box.css +2 -2
- package/modules/hall-ui/public/home-idea-box.js +3 -3
- package/modules/hall-ui/public/idea.html +5 -5
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch.html +6 -9
- package/modules/hall-ui/public/market-watch.js +22 -45
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/options-menu.js +409 -0
- package/modules/hall-ui/public/paintbrush.js +3 -3
- package/modules/hall-ui/public/palette.js +11 -13
- package/modules/hall-ui/public/primer.html +5 -5
- package/modules/hall-ui/public/profile-crafts.js +1 -1
- package/modules/hall-ui/public/profile-roles.js +23 -19
- package/modules/hall-ui/public/profile.css +1 -1
- package/modules/hall-ui/public/profile.html +19 -18
- package/modules/hall-ui/public/profile.js +19 -18
- package/modules/hall-ui/public/profile.states.json +4 -4
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/roster.html +4 -4
- package/modules/hall-ui/public/roster.js +3 -3
- package/modules/hall-ui/public/settings.css +24 -4
- package/modules/hall-ui/public/settings.html +21 -17
- package/modules/hall-ui/public/settings.js +44 -44
- package/modules/hall-ui/public/shell-game.css +65 -0
- package/modules/hall-ui/public/shell-game.js +283 -3
- package/modules/hall-ui/public/shell.js +7 -26
- package/modules/hall-ui/public/sky-interaction.js +7 -0
- package/modules/hall-ui/public/speciality-tools.js +1 -2
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/style.css +73 -47
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/tweak-editor.html +5 -5
- package/modules/hall-ui/public/tweak-editor.js +5 -1
- package/modules/hall-ui/public/update-pill.js +91 -2
- package/modules/hall-ui/public/visor.css +80 -0
- package/modules/hall-ui/public/visor.js +316 -0
- package/modules/hall-ui/public/watch-repo-invite.js +159 -0
- package/modules/hall-ui/public/watch.html +6 -5
- package/modules/hall-ui/public/watch.js +17 -2
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/records/builders-roster.md +2 -2
- package/modules/hall-ui/records/craft-mode.md +13 -11
- package/modules/hall-ui/records/game-mode.md +2 -2
- package/modules/hall-ui/records/governor-street.md +1 -1
- package/modules/hall-ui/records/home-city.md +3 -1
- package/modules/hall-ui/records/market-watch-settings.md +2 -0
- package/modules/hall-ui/records/options-menu.md +15 -0
- package/modules/hall-ui/records/profile-roles.md +11 -11
- package/modules/hall-ui/records/sky.md +3 -3
- package/modules/hall-ui/records/start-screen.md +21 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/hall-ui/records/update-pill.md +12 -0
- package/modules/hall-ui/records/visor.md +23 -0
- package/modules/hall-ui/records/welcome-tower.md +5 -5
- package/modules/ideas/ideator-stats.js +3 -1
- package/modules/ideas/inbox.js +2 -2
- package/modules/lifecycle/activity.js +1 -1
- package/modules/lifecycle/cascade.js +4 -4
- package/modules/lifecycle/goal-spec-task.js +1 -1
- package/modules/lifecycle/help-requests.js +19 -15
- package/modules/lifecycle/kickoff-checklist.js +3 -3
- package/modules/lifecycle/kickoff-seed.js +1 -1
- package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
- package/modules/lifecycle/mingle.js +4 -4
- package/modules/lifecycle/page-tweak-write.js +1 -1
- package/modules/lifecycle/routes/task-write-routes.js +3 -1
- package/modules/lifecycle/routes/tasks.js +3 -3
- package/modules/lifecycle/ship-card.js +2 -3
- package/modules/lifecycle/task-classifier.js +39 -29
- package/modules/lifecycle/tests/cascade.mjs +7 -7
- package/modules/onboarding/founding-mode.js +3 -0
- package/modules/onboarding/genesis-home.js +21 -1
- package/modules/onboarding/routes/onboarding.js +2 -0
- package/modules/page-notes/notes.js +3 -2
- package/modules/page-notes/split.js +1 -1
- package/modules/platform-identity/craft-rollup.js +35 -7
- package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
- package/modules/platform-identity/platform-identity.js +2 -2
- package/modules/platform-identity/routes/community-leaderboard.js +2 -1
- package/modules/platform-identity/routes/community-search.js +2 -1
- package/modules/platform-identity/routes/sso.js +2 -1
- package/modules/platform-identity/tests/platform-identity.mjs +6 -6
- package/modules/public-landing/public/community.html +3 -5
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
- package/modules/specialities/routes/specialities.js +2 -2
- package/modules/status-ui/public/status.js +1 -1
- package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
- package/modules/ui-design/kit/fixtures/community-search.json +2 -2
- package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +70 -0
- package/scripts/bongos/autobongos-loop.js +1 -1
- package/scripts/bongos/discipline-modes.json +7 -12
- package/scripts/bongos/fitness.js +1 -1
- package/scripts/bongos/gen-hall-sitemap.js +7 -7
- package/scripts/bongos/grade-smoke.js +2 -2
- package/scripts/bongos/init.js +2 -2
- package/scripts/bongos/newcomer-floor.config.json +1 -2
- package/scripts/bongos/newcomer-floor.js +4 -3
- package/scripts/bongos/newcomer-floor.templates.json +10 -12
- package/scripts/bongos/newcomer-floor.test.js +47 -38
- package/scripts/bongos/role-pack-guard.js +5 -4
- package/scripts/bongos/seed-tasks.example.json +3 -3
- package/scripts/bongos/setup.js +20 -107
- package/scripts/bongos/start.js +2 -2
- package/scripts/bongos/task.js +5 -4
- package/scripts/bongos/triage.js +1 -1
- package/scripts/bongos/worktree.js +16 -4
- package/scripts/setup-builder.js +6 -46
- package/src/bongos/auth-admission.js +8 -8
- package/src/bongos/craft-mode.js +2 -0
- package/src/bongos/db.js +39 -1
- package/src/bongos/github-repos.js +78 -0
- package/src/bongos/module-scope-map.js +22 -0
- package/src/bongos/role-stats.js +27 -11
- package/src/bongos/routes/github.js +100 -2
- package/src/bongos/routes/me.js +33 -0
- package/src/bongos/serve-internal.js +26 -9
- package/src/crafts.js +66 -0
- package/src/module-api.js +6 -1
- package/src/module-loader/manifest-schema.js +16 -0
- package/src/modules.js +2 -2
- package/src/role-responsibilities.js +6 -5
- package/tests/acclaim_feed_evidence_db.mjs +1 -1
- package/tests/activity_rollup_as_of.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +3 -3
- package/tests/application_lifecycle.mjs +1 -1
- package/tests/artist_review.mjs +1 -1
- package/tests/artist_review_goal.mjs +1 -1
- package/tests/blocker_hall_live_proof.mjs +1 -1
- package/tests/blocker_tasks_named_db.mjs +2 -2
- package/tests/canonical_profile_url.mjs +1 -1
- package/tests/cap_newcomer_tasks.mjs +13 -13
- package/tests/character_creation.mjs +294 -0
- package/tests/claim_gate_open_blocker.mjs +2 -2
- package/tests/claim_gate_rebase.mjs +6 -6
- package/tests/claude_materialize.mjs +1 -1
- package/tests/clawback_confirmed_backlog.mjs +3 -3
- package/tests/collab_page.mjs +8 -9
- package/tests/collab_self_settling.mjs +2 -2
- package/tests/community_leaderboard_route.mjs +13 -5
- package/tests/community_proof_db.mjs +3 -0
- package/tests/community_search_route.mjs +7 -6
- package/tests/conductor.mjs +1 -1
- package/tests/conductor_main_e2e.mjs +8 -8
- package/tests/copy_desk_flags_db.mjs +1 -1
- package/tests/copy_desk_page_approve.mjs +2 -2
- package/tests/copy_desk_page_claim.mjs +2 -2
- package/tests/copy_desk_page_next.mjs +1 -1
- package/tests/copy_desk_page_release.mjs +1 -1
- package/tests/copy_desk_page_submit.mjs +2 -2
- package/tests/core_258_box_session_claims_db.mjs +1 -1
- package/tests/craft_mode.mjs +48 -34
- package/tests/craft_mode_home.mjs +20 -20
- package/tests/craft_mode_looks.mjs +16 -15
- package/tests/craft_mode_nav.mjs +31 -29
- package/tests/craft_review_mode.mjs +46 -41
- package/tests/crafts_vocabulary.mjs +70 -0
- package/tests/cross_discipline_priority_direction.mjs +5 -5
- package/tests/fitness.mjs +7 -11
- package/tests/funded_project_pays_proof_db.mjs +1 -1
- package/tests/genesis_home.mjs +21 -1
- package/tests/github_chip.mjs +3 -3
- package/tests/github_repo_invite.mjs +379 -0
- package/tests/goal_criterion_confirm_ui.mjs +1 -1
- package/tests/goal_invite_consent.mjs +1 -1
- package/tests/goal_map_page.mjs +2 -2
- package/tests/goal_sort_staleness.mjs +1 -1
- package/tests/goal_spec_task.mjs +1 -1
- package/tests/goal_spec_task_db.mjs +3 -3
- package/tests/grade_engine.mjs +2 -2
- package/tests/grader_no_artifact.mjs +1 -1
- package/tests/grader_prompt_fencing.mjs +1 -1
- package/tests/grader_prompt_window_bound.mjs +1 -1
- package/tests/grader_reader_lens.mjs +2 -2
- package/tests/grader_severity_gate.mjs +1 -1
- package/tests/grader_unbacked_score.mjs +1 -1
- package/tests/hall_approval_queue.mjs +1 -1
- package/tests/hall_board_world.mjs +2 -2
- package/tests/hall_builders_page_pass.mjs +1 -1
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_kit_charts.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_my_agents.mjs +16 -16
- package/tests/hall_nav.mjs +22 -15
- package/tests/hall_options_menu.mjs +247 -0
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_palette.mjs +8 -9
- package/tests/hall_profile_world.mjs +3 -3
- package/tests/hall_record_world.mjs +2 -2
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +244 -0
- package/tests/hall_street.mjs +2 -2
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_visor.mjs +224 -0
- package/tests/help_requests.mjs +47 -15
- package/tests/helpers.mjs +4 -4
- package/tests/home_city.mjs +6 -4
- package/tests/home_city_widget.mjs +33 -14
- package/tests/home_gallery_widget.mjs +4 -4
- package/tests/home_idea_box.mjs +6 -6
- package/tests/hub_community_page.mjs +3 -3
- package/tests/hub_craft_rollup.mjs +75 -36
- package/tests/hub_recent_ships.mjs +3 -2
- package/tests/idea_cascade.mjs +2 -2
- package/tests/idea_credit_streams_db.mjs +1 -1
- package/tests/idea_detail_page.mjs +1 -1
- package/tests/idea_ideate_full.mjs +5 -2
- package/tests/idea_spark_hall.mjs +1 -1
- package/tests/init.mjs +3 -3
- package/tests/instance_manifest.mjs +2 -2
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/leaderboard_roles.mjs +34 -25
- package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
- package/tests/lightbulb.mjs +14 -10
- package/tests/main_discipline.mjs +8 -8
- package/tests/markdown_line_endings.mjs +1 -1
- package/tests/market_watch_pages.mjs +52 -74
- package/tests/me_streak_leaderboard_queries.mjs +3 -3
- package/tests/migration_allocation.mjs +1 -1
- package/tests/mingle.mjs +36 -21
- package/tests/module-scope-map.mjs +22 -2
- package/tests/module_api.mjs +1 -0
- package/tests/module_contributions.mjs +13 -13
- package/tests/module_manifest.mjs +29 -0
- package/tests/module_skills_hook.mjs +2 -2
- package/tests/modules.mjs +14 -7
- package/tests/nav_permission_atoms.mjs +2 -1
- package/tests/needs_rebase.mjs +1 -1
- package/tests/newcomer_restock.mjs +13 -13
- package/tests/page_notes_panel.mjs +22 -22
- package/tests/page_tweak_no_block.mjs +1 -1
- package/tests/paintbrush.mjs +7 -7
- package/tests/privacy_matrix.mjs +1 -0
- package/tests/profile_activity.mjs +20 -23
- package/tests/profile_earnings.mjs +1 -1
- package/tests/profile_nudge_prefs.mjs +4 -4
- package/tests/profile_role_headline.mjs +73 -46
- package/tests/profile_role_stats.mjs +55 -45
- package/tests/profile_rollup_consent.mjs +19 -6
- package/tests/profile_route.mjs +12 -12
- package/tests/profile_ui_cross_project.mjs +4 -4
- package/tests/project_invite_suggestions.mjs +2 -2
- package/tests/promote_goal_id.mjs +1 -1
- package/tests/publish_manifest.mjs +1 -1
- package/tests/rank_floor_wiring.mjs +1 -1
- package/tests/regrade_actual_minutes.mjs +1 -1
- package/tests/removal_proposal.mjs +2 -2
- package/tests/reward_gate.mjs +4 -4
- package/tests/reward_mode_cost_plus_only.mjs +1 -1
- package/tests/role_responsibilities.mjs +18 -12
- package/tests/role_stats_db.mjs +17 -16
- package/tests/scouting_rollup_fanout.mjs +5 -5
- package/tests/seed_tasks.mjs +5 -2
- package/tests/session_earnings_mirror_db.mjs +1 -1
- package/tests/session_reward_on_land_db.mjs +1 -1
- package/tests/session_reward_reconcile_db.mjs +2 -2
- package/tests/sessions_kit_pager.mjs +1 -1
- package/tests/settings_art_key_absent.mjs +2 -2
- package/tests/settings_main_role.mjs +21 -20
- package/tests/setup_flow.mjs +3 -3
- package/tests/setup_smoke.mjs +20 -0
- package/tests/ship_card.mjs +6 -6
- package/tests/specialities.mjs +16 -4
- package/tests/speciality_db_shape.mjs +6 -6
- package/tests/speciality_injection.mjs +17 -17
- package/tests/speciality_offer_ui.mjs +5 -5
- package/tests/speciality_offered.mjs +6 -5
- package/tests/speciality_session_skills.mjs +7 -7
- package/tests/speciality_skills.mjs +11 -11
- package/tests/speciality_walkthrough_ui.mjs +2 -2
- package/tests/sso_checkin_identity_spoof.mjs +2 -1
- package/tests/task_classifier_vocab.mjs +10 -0
- package/tests/task_cli.mjs +11 -3
- package/tests/task_criteria_unlink_db.mjs +2 -2
- package/tests/task_discipline_required.mjs +38 -16
- package/tests/task_goal_required.mjs +4 -4
- package/tests/task_needs_db.mjs +1 -1
- package/tests/tweak_replaces_artist_review.mjs +1 -1
- package/tests/update_pill.mjs +153 -2
- package/tests/upkeep_home.mjs +1 -1
- package/tests/v3_c1_options_proof.mjs +145 -0
- package/tests/visual_review.mjs +3 -3
- package/tests/watch_applications_queue.mjs +8 -5
- package/tests/watch_sealed_floor.mjs +3 -1
- package/tests/welcome_boot.mjs +12 -9
- package/tests/welcome_boot_browser.mjs +13 -13
- package/tests/worktree_helper.mjs +16 -1
package/modules/ideas/inbox.js
CHANGED
|
@@ -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 '
|
|
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) || '
|
|
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 = ['
|
|
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'
|
|
75
|
-
// '
|
|
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: '
|
|
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: '
|
|
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
|
|
@@ -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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
|
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 '
|
|
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
|
-
// '
|
|
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: '
|
|
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: '
|
|
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 || '
|
|
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
|
-
// (
|
|
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
|
|
71
|
-
+ 'an artist or
|
|
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
|
-
//
|
|
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: '
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
125
|
-
// generation, palettes, walk animations, aesthetic passes
|
|
126
|
-
//
|
|
127
|
-
//
|
|
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/
|
|
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
|
|
152
|
-
// production) and
|
|
153
|
-
//
|
|
154
|
-
// engineer-vs-creative; a creative verdict
|
|
155
|
-
// artist/
|
|
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(['
|
|
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(['
|
|
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
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
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' | '
|
|
282
|
-
//
|
|
283
|
-
//
|
|
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
|
-
|
|
289
|
-
|
|
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
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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 === '
|
|
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: '
|
|
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: '
|
|
266
|
-
assert.equal(H(shippedCopy({ module_key: null, discipline: '
|
|
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: '
|
|
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: '
|
|
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
|
-
//
|
|
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
|
-
|
|
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;
|