@bongos/core 1.24.79 → 1.24.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +554 -394
- 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/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
- package/docs/adr/README.md +4 -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 +393 -378
- package/docs/copy-registry.json +722 -578
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +4 -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 +16 -7
- package/docs/page-readings.json +3184 -3338
- 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 +6 -4
- package/modules/hall-ui/public/ai-studio.states.json +1 -0
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/atlas.states.json +1 -0
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/board-room.states.json +1 -0
- 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 +3 -4
- package/modules/hall-ui/public/city.html +9 -9
- package/modules/hall-ui/public/city.js +17 -56
- package/modules/hall-ui/public/city.states.json +7 -32
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/collab.states.json +1 -0
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/commands.states.json +1 -0
- package/modules/hall-ui/public/controls.css +63 -11
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/copy-desk.states.json +1 -0
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/deploy.states.json +1 -0
- 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/goals.states.json +1 -0
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/government.states.json +1 -0
- 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/idea.states.json +1 -0
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/ideas.states.json +1 -0
- package/modules/hall-ui/public/index-founding.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
- package/modules/hall-ui/public/index-genesis.states.json +1 -0
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/index.states.json +1 -0
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-item.states.json +1 -0
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
- 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/market-watch.states.json +1 -0
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/modules.states.json +1 -0
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/money.states.json +1 -0
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/my-agents.states.json +1 -0
- 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/primer.states.json +1 -0
- 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 +5 -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/roster.states.json +1 -0
- 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/settings.states.json +1 -0
- package/modules/hall-ui/public/shell-game.css +110 -0
- package/modules/hall-ui/public/shell-game.js +316 -3
- package/modules/hall-ui/public/shell.js +15 -66
- 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-card.states.json +1 -0
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.css +1 -5
- package/modules/hall-ui/public/street.html +6 -10
- package/modules/hall-ui/public/street.js +8 -59
- package/modules/hall-ui/public/street.states.json +7 -32
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/studio.states.json +1 -0
- package/modules/hall-ui/public/style.css +197 -112
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/thinking.states.json +1 -0
- 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/tweak-editor.states.json +1 -0
- 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/watch.states.json +1 -0
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work-founding.states.json +1 -0
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/public/work.states.json +1 -0
- 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 +25 -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/hall-ui/specimens/components.states.json +2 -1
- 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/modules/ui-design/public/brand.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +76 -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/hall-preview/README.md +2 -3
- 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 +33 -11
- package/src/crafts.js +66 -0
- package/src/hall-helm.js +79 -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/design_reference.mjs +1 -1
- 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 +3 -2
- package/tests/hall_city.mjs +16 -30
- package/tests/hall_controls.mjs +52 -4
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_helm.mjs +187 -0
- 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 +6 -4
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +307 -0
- package/tests/hall_street.mjs +15 -28
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_topbar_320.mjs +4 -2
- 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
|
@@ -291,9 +291,21 @@ function cmdAdd(root, argv) {
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
console.log('');
|
|
294
|
-
console.log(
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
for (const line of nextStepLines(wtPath)) console.log(line);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// task 1005185 - feedback bug scripts: the "Next:" hint names the claim command that
|
|
298
|
+
// exists in the new tree. A project that installs the core from npm has no scripts/ of
|
|
299
|
+
// its own — scripts is a git-ignored link into node_modules/@bongos/core, so a fresh
|
|
300
|
+
// worktree has none and `node scripts/...` dies MODULE_NOT_FOUND. There the CLI is
|
|
301
|
+
// `npx bongos <verb>` (npx walks up to the main checkout's node_modules). Same check
|
|
302
|
+
// claim.js's finishHintLines uses (task 1005113).
|
|
303
|
+
function nextStepLines(wtPath, { installed } = {}) {
|
|
304
|
+
if (installed === undefined) {
|
|
305
|
+
try { installed = require('./fitness-lib').isInstalledCore(); } catch { installed = false; }
|
|
306
|
+
}
|
|
307
|
+
const claim = installed ? 'npx bongos claim <task-id>' : 'node scripts/bongos/claim.js <task-id>';
|
|
308
|
+
return ['Next:', ` cd "${wtPath}"`, ` ${claim} (worktree auto-detected)`];
|
|
297
309
|
}
|
|
298
310
|
|
|
299
311
|
function cmdRemove(root, argv) {
|
|
@@ -445,5 +457,5 @@ if (require.main === module) main();
|
|
|
445
457
|
|
|
446
458
|
module.exports = {
|
|
447
459
|
worktreeNameFor, needsJunction, removalPlan, nodeModulesState, shadowingNodeModules, huskDisposition,
|
|
448
|
-
unlinkNodeModulesLink,
|
|
460
|
+
unlinkNodeModulesLink, nextStepLines,
|
|
449
461
|
};
|
|
@@ -135,15 +135,14 @@ overlay). A concept no table holds is not drawn: never add sample data for one.
|
|
|
135
135
|
|
|
136
136
|
## The evidence matrix (per page-redesign ship)
|
|
137
137
|
|
|
138
|
-
Screenshot every changed page at **{
|
|
138
|
+
Screenshot every changed page at **{1280w, 390w}** (the hall is dark only since task 1005192, ADR 0402), plus one
|
|
139
139
|
instance-pack run (`--brand config/branding.json`) proving the theming contract
|
|
140
140
|
survives. Run the harness with **`--fixture-me`** for these — the pinned snapshot
|
|
141
141
|
is what makes two runs comparable. With the Playwright MCP (`npx playwright install chrome` once):
|
|
142
142
|
|
|
143
143
|
1. `browser_navigate` → the page. `browser_console_messages` must show **0 errors**.
|
|
144
144
|
2. 1280w: `browser_resize 1280×900` → `browser_take_screenshot` (fullPage for tall pages).
|
|
145
|
-
3.
|
|
146
|
-
4. 390w: `browser_resize 390×844` → light + dark screenshots.
|
|
145
|
+
3. 390w: `browser_resize 390×844` → screenshot.
|
|
147
146
|
|
|
148
147
|
## Seam assertions (copy-paste `browser_evaluate` snippets)
|
|
149
148
|
|
package/scripts/setup-builder.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// bootstrap a new builder runs right after cloning:
|
|
8
8
|
//
|
|
9
9
|
// node scripts/setup-builder.js
|
|
10
|
-
// node scripts/setup-builder.js --skip-keys #
|
|
10
|
+
// node scripts/setup-builder.js --skip-keys # accepted and ignored (no key is asked for)
|
|
11
11
|
// node scripts/setup-builder.js --no-auth # skip the GDS Device Flow step
|
|
12
12
|
// node scripts/setup-builder.js --help
|
|
13
13
|
//
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// "already done" rather than clobbering. What it does, in order:
|
|
16
16
|
// 1. Verify Node >= 22 and Python >= 3.10 (the two required toolchains).
|
|
17
17
|
// 2. `npm ci` to install JS dependencies.
|
|
18
|
-
// 3. Copy .env.local.example -> .env.local (if absent)
|
|
19
|
-
// for
|
|
18
|
+
// 3. Copy .env.local.example -> .env.local (if absent). No API key is asked
|
|
19
|
+
// for (task 1005189): a builder adds one in hall Settings when art needs it.
|
|
20
20
|
// 4. `node scripts/bongos/setup.js` — GitHub Device Flow auth (--no-auth skips).
|
|
21
21
|
// 5. Print the "ready" banner pointing at /builder-start.
|
|
22
22
|
//
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
|
|
26
26
|
const fs = require('node:fs');
|
|
27
27
|
const path = require('node:path');
|
|
28
|
-
const readline = require('node:readline');
|
|
29
28
|
const { spawnSync } = require('node:child_process');
|
|
30
29
|
|
|
31
30
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
@@ -33,17 +32,15 @@ const IS_WIN = process.platform === 'win32';
|
|
|
33
32
|
|
|
34
33
|
// ---- arg parsing ----
|
|
35
34
|
const argv = process.argv.slice(2);
|
|
36
|
-
let SKIP_KEYS = false;
|
|
37
35
|
let NO_AUTH = false;
|
|
38
36
|
for (const a of argv) {
|
|
39
|
-
if (a === '--skip-keys')
|
|
37
|
+
if (a === '--skip-keys') { /* accepted for old callers; setup asks for no key (task 1005189) */ }
|
|
40
38
|
else if (a === '--no-auth') NO_AUTH = true;
|
|
41
39
|
else if (a === '-h' || a === '--help') {
|
|
42
40
|
console.log(`setup-builder.js — one-command bootstrap for a new OTB builder.
|
|
43
41
|
|
|
44
42
|
Usage:
|
|
45
43
|
node scripts/setup-builder.js
|
|
46
|
-
node scripts/setup-builder.js --skip-keys # don't prompt for API keys
|
|
47
44
|
node scripts/setup-builder.js --no-auth # skip the GDS Device Flow step
|
|
48
45
|
node scripts/setup-builder.js --help
|
|
49
46
|
|
|
@@ -162,13 +159,6 @@ function npmCi() {
|
|
|
162
159
|
// ---------------------------------------------------------------------------
|
|
163
160
|
// 3. .env.local
|
|
164
161
|
// ---------------------------------------------------------------------------
|
|
165
|
-
function ask(question) {
|
|
166
|
-
return new Promise((resolve) => {
|
|
167
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
168
|
-
rl.question(question, (answer) => { rl.close(); resolve(answer); });
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
162
|
async function envLocal() {
|
|
173
163
|
step('3/5 Local environment (.env.local)');
|
|
174
164
|
const example = path.join(REPO_ROOT, '.env.local.example');
|
|
@@ -186,37 +176,8 @@ async function envLocal() {
|
|
|
186
176
|
fs.copyFileSync(example, target);
|
|
187
177
|
ok('Created .env.local from .env.local.example');
|
|
188
178
|
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// ~/.config/otb/env, so it just means art-pipeline calls raise the standard
|
|
192
|
-
// "GOOGLE_AI_STUDIO_KEY not found" error until set.
|
|
193
|
-
if (SKIP_KEYS) {
|
|
194
|
-
skip('--skip-keys set; leaving GOOGLE_AI_STUDIO_KEY blank in .env.local');
|
|
195
|
-
skip('you can edit .env.local later, or set it in ~/.config/otb/env');
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (!process.stdin.isTTY) {
|
|
199
|
-
skip('no TTY; leaving GOOGLE_AI_STUDIO_KEY blank in .env.local');
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
console.log('');
|
|
203
|
-
console.log(` ${C.dim}Google AI Studio key for the pixel-art pipeline.`);
|
|
204
|
-
console.log(' Get one at https://aistudio.google.com/app/apikey .');
|
|
205
|
-
console.log(` Press Enter to skip — you can always edit .env.local later.${C.reset}`);
|
|
206
|
-
// Strip CR/LF so a pasted value with a stray newline can't inject extra
|
|
207
|
-
// lines into the builder's .env.local; trim handles surrounding whitespace.
|
|
208
|
-
const input = (await ask(' GOOGLE_AI_STUDIO_KEY: ')).replace(/[\r\n]+/g, '').trim();
|
|
209
|
-
if (input) {
|
|
210
|
-
const contents = fs.readFileSync(target, 'utf8');
|
|
211
|
-
// Function replacement (not a string): String.replace treats $&, $1, $$ etc.
|
|
212
|
-
// in a replacement STRING as special, which would silently corrupt a key
|
|
213
|
-
// containing a bare `$`. A function return value is spliced verbatim.
|
|
214
|
-
const next = contents.replace(/^GOOGLE_AI_STUDIO_KEY=.*$/m, () => `GOOGLE_AI_STUDIO_KEY=${input}`);
|
|
215
|
-
fs.writeFileSync(target, next);
|
|
216
|
-
ok('Saved GOOGLE_AI_STUDIO_KEY to .env.local');
|
|
217
|
-
} else {
|
|
218
|
-
skip('left GOOGLE_AI_STUDIO_KEY blank — edit .env.local when you have one');
|
|
219
|
-
}
|
|
179
|
+
// No key is asked for here (task 1005189): an optional key at sign-in is noise.
|
|
180
|
+
// The art pipeline says so itself, when art actually needs one.
|
|
220
181
|
}
|
|
221
182
|
|
|
222
183
|
// ---------------------------------------------------------------------------
|
|
@@ -234,7 +195,6 @@ function gdsAuth() {
|
|
|
234
195
|
console.log(' Handing off to scripts/bongos/setup.js ...');
|
|
235
196
|
console.log('');
|
|
236
197
|
const setupArgs = [path.join(REPO_ROOT, 'scripts', 'bongos', 'setup.js')];
|
|
237
|
-
if (SKIP_KEYS) setupArgs.push('--skip-keys');
|
|
238
198
|
const status = run(process.execPath, setupArgs); // shell defaults off
|
|
239
199
|
if (status === 0) {
|
|
240
200
|
ok('GDS session ready');
|
|
@@ -149,7 +149,7 @@ async function recentShipsForRollup(builderId) {
|
|
|
149
149
|
// preference, exactly as before. A port that cannot be asked leaves its numbers
|
|
150
150
|
// null ("could not ask"), never 0. Never throws: a failed read drops its craft's
|
|
151
151
|
// numbers, not the report.
|
|
152
|
-
const ROLLUP_CRAFTS = Object.freeze(['
|
|
152
|
+
const ROLLUP_CRAFTS = Object.freeze(['artisan', 'artist']);
|
|
153
153
|
const countOrNull = (v) => (Number.isInteger(v) && v >= 0 ? v : null);
|
|
154
154
|
async function readCraftRollup(builderId, { shipped = 0, preferred = [] } = {}) {
|
|
155
155
|
const fallback = { counts: null, disciplines: Array.isArray(preferred) ? preferred : [] };
|
|
@@ -172,20 +172,20 @@ async function readCraftRollup(builderId, { shipped = 0, preferred = [] } = {})
|
|
|
172
172
|
};
|
|
173
173
|
const [artist, ideator] = await Promise.all([
|
|
174
174
|
earned.includes('artist') ? ask('copy-desk.artistStats') : null,
|
|
175
|
-
earned.includes('
|
|
175
|
+
earned.includes('artist') ? ask('ideas.ideatorStats') : null,
|
|
176
176
|
]);
|
|
177
177
|
const credits = (c) => countOrNull(Math.round(Number(byCraft[c])));
|
|
178
178
|
const counts = {};
|
|
179
|
-
if (earned.includes('
|
|
179
|
+
if (earned.includes('artisan')) counts.artisan = { shipped: countOrNull(shipped), credits: credits('artisan') };
|
|
180
|
+
// One artist block since the ideator folded in (task 1005141): the page count
|
|
181
|
+
// and the idea counts side by side, under the craft's one credit split.
|
|
180
182
|
if (earned.includes('artist')) {
|
|
181
|
-
counts.artist = {
|
|
182
|
-
|
|
183
|
-
if (earned.includes('ideator')) {
|
|
184
|
-
counts.ideator = {
|
|
183
|
+
counts.artist = {
|
|
184
|
+
pages_approved: countOrNull(artist && artist.pages_approved),
|
|
185
185
|
filed: countOrNull(ideator && ideator.filed),
|
|
186
186
|
full_ideas: countOrNull(ideator && ideator.full_ideas),
|
|
187
187
|
ratified: countOrNull(ideator && ideator.ratified),
|
|
188
|
-
credits: credits('
|
|
188
|
+
credits: credits('artist'),
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
return { counts, disciplines: earned };
|
package/src/bongos/craft-mode.js
CHANGED
|
@@ -77,6 +77,8 @@ function roleLine(mode) {
|
|
|
77
77
|
// so this adds no prompt-injection surface to the session.
|
|
78
78
|
function describeCraftMode(discipline, modes) {
|
|
79
79
|
if (!discipline || !modes) return null;
|
|
80
|
+
// A row a rolled-back release wrote may still say engineer/ideator (task 1005141).
|
|
81
|
+
discipline = require('../crafts').normalizeCraft(discipline);
|
|
80
82
|
const mode = modes[discipline];
|
|
81
83
|
if (!mode || typeof mode !== 'object' || !mode.directive) return null;
|
|
82
84
|
const parts = [`${roleLine(mode)} (the craft of the task claimed in this worktree).`, String(mode.directive).trim()];
|
package/src/bongos/db.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// grade-count SQL runs on both sides of the kernel↔module line — keep in sync.
|
|
26
26
|
|
|
27
27
|
const { pool } = require('./pool');
|
|
28
|
+
const { normalizeCraft: normalizeCraftName } = require('../crafts'); // task 1005147: setBuilderCharacter
|
|
28
29
|
const secretBox = require('./secret-box');
|
|
29
30
|
// Module seam (ADR 0083). The builder-exit path EMITS `builder.secrets-scrub-needed`
|
|
30
31
|
// AFTER COMMIT (best-effort) so a module that stores a builder-keyed secret can
|
|
@@ -172,7 +173,10 @@ const UPDATE_BUILDER_DISCIPLINES_SQL =
|
|
|
172
173
|
async function updateBuilderDisciplines(builderId, disciplines, { mainDiscipline } = {}) {
|
|
173
174
|
const { enabledDisciplines } = require('../modules');
|
|
174
175
|
const offered = enabledDisciplines();
|
|
175
|
-
|
|
176
|
+
// An old spelling (engineer, ideator) is today's craft, not an invalid one (task 1005141).
|
|
177
|
+
const { normalizeCraft, normalizeCrafts } = require('../crafts');
|
|
178
|
+
const list = Array.isArray(disciplines) ? normalizeCrafts(disciplines) : [];
|
|
179
|
+
if (typeof mainDiscipline === 'string') mainDiscipline = normalizeCraft(mainDiscipline);
|
|
176
180
|
const bad = list.filter((d) => !offered.includes(d));
|
|
177
181
|
if (bad.length) {
|
|
178
182
|
const err = new Error(`invalid discipline(s): ${bad.join(', ')}`);
|
|
@@ -207,6 +211,38 @@ async function getBuilderRoleChoice(builderId) {
|
|
|
207
211
|
return rows[0] ?? null;
|
|
208
212
|
}
|
|
209
213
|
|
|
214
|
+
// The character a builder holds (task 1005147, core_297): 'artisan' | 'artist' |
|
|
215
|
+
// 'patron', or null while they have not picked one. Identity and view only — no
|
|
216
|
+
// gate reads it (criterion character-is-identity-not-authority). Its own read, not a
|
|
217
|
+
// column on getBuilderById, so the kernel's hot identity read never depends on a
|
|
218
|
+
// migration that may not have run yet.
|
|
219
|
+
const CHARACTERS = Object.freeze(['artisan', 'artist', 'patron']);
|
|
220
|
+
async function getBuilderCharacter(builderId) {
|
|
221
|
+
const { rows } = await pool.query(
|
|
222
|
+
`SELECT game_character FROM builders WHERE id = $1`,
|
|
223
|
+
[builderId]
|
|
224
|
+
);
|
|
225
|
+
return rows[0] ? rows[0].game_character : null;
|
|
226
|
+
}
|
|
227
|
+
// Set (a CHARACTERS word, old spellings normalized) or clear (null) the builder's own
|
|
228
|
+
// character. Throws { code: 'invalid_character' } for anything else.
|
|
229
|
+
async function setBuilderCharacter(builderId, character) {
|
|
230
|
+
let value = null;
|
|
231
|
+
if (character !== null) {
|
|
232
|
+
value = normalizeCraftName(character);
|
|
233
|
+
if (!CHARACTERS.includes(value)) {
|
|
234
|
+
const err = new Error(`invalid character: ${character}`);
|
|
235
|
+
err.code = 'invalid_character';
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const { rows } = await pool.query(
|
|
240
|
+
`UPDATE builders SET game_character = $2 WHERE id = $1 RETURNING game_character`,
|
|
241
|
+
[builderId, value]
|
|
242
|
+
);
|
|
243
|
+
return rows[0] ? rows[0].game_character : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
210
246
|
// The same two facts for a whole list of builders, in ONE statement (task
|
|
211
247
|
// 1004437): the public leaderboard names every row's roles. Map<idString, row>.
|
|
212
248
|
async function getBuilderRoleChoices(builderIds) {
|
|
@@ -1142,6 +1178,8 @@ module.exports = {
|
|
|
1142
1178
|
UPDATE_BUILDER_DISCIPLINES_SQL,
|
|
1143
1179
|
getBuilderRoleChoice,
|
|
1144
1180
|
getBuilderRoleChoices,
|
|
1181
|
+
getBuilderCharacter,
|
|
1182
|
+
setBuilderCharacter,
|
|
1145
1183
|
RANKS,
|
|
1146
1184
|
setBuilderRank,
|
|
1147
1185
|
setBuilderTester,
|
|
@@ -378,7 +378,85 @@ async function deleteDeployKey(token, { owner, name, keyId, fetchImpl = fetch }
|
|
|
378
378
|
throw new GithubApiError(`GitHub deploy-key delete failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
// ---- repo collaborators: inviting a person to the repo itself ----
|
|
382
|
+
// A GitHub invite is a SEPARATE door from a Bongos invite. Bongos admission is a row
|
|
383
|
+
// in the project's own database; this puts the person on the repo's collaborator list
|
|
384
|
+
// so they can see (and, only when asked, push to) the code on their own GitHub.
|
|
385
|
+
// ONE permission is offered: 'pull' (read). A builder ships through the server's own
|
|
386
|
+
// publish and needs no write access, so Bongos never grants it (owner decision,
|
|
387
|
+
// 2026-11-04): 'push', 'admin', 'maintain' and 'triage' are all refused. An owner who
|
|
388
|
+
// wants someone to have write access gives it on GitHub directly.
|
|
389
|
+
const REPO_INVITE_PERMISSIONS = Object.freeze(['pull']);
|
|
390
|
+
|
|
391
|
+
function repoPath(owner, name) {
|
|
392
|
+
return `${GH_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// PUT /collaborators/{login}. 201 = a pending invitation was created, 204 = they
|
|
396
|
+
// already have access. Returns { state: 'invited' | 'already_collaborator', ... }.
|
|
397
|
+
async function inviteRepoCollaborator(token, { owner, name, login, permission = 'pull', fetchImpl = fetch } = {}) {
|
|
398
|
+
if (!REPO_INVITE_PERMISSIONS.includes(permission)) {
|
|
399
|
+
throw new GithubApiError('Only read-only access can be offered', { status: 400, code: 'bad_permission' });
|
|
400
|
+
}
|
|
401
|
+
const res = await fetchImpl(`${repoPath(owner, name)}/collaborators/${encodeURIComponent(login)}`, {
|
|
402
|
+
method: 'PUT',
|
|
403
|
+
headers: ghHeaders(token, { 'Content-Type': 'application/json' }),
|
|
404
|
+
body: JSON.stringify({ permission }),
|
|
405
|
+
});
|
|
406
|
+
if (res.status === 201) {
|
|
407
|
+
const inv = await res.json().catch(() => ({}));
|
|
408
|
+
return { state: 'invited', invitation_id: inv.id || null, permission };
|
|
409
|
+
}
|
|
410
|
+
if (res.status === 204) return { state: 'already_collaborator', permission };
|
|
411
|
+
if (res.status === 401) {
|
|
412
|
+
throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
|
|
413
|
+
}
|
|
414
|
+
if (res.status === 403) {
|
|
415
|
+
throw new GithubApiError('GitHub refused — adding people needs repository admin', { status: 403, code: 'not_repo_admin' });
|
|
416
|
+
}
|
|
417
|
+
if (res.status === 404) {
|
|
418
|
+
throw new GithubApiError('GitHub answered 404 for that repository or user', { status: 404, code: 'repo_not_found' });
|
|
419
|
+
}
|
|
420
|
+
if (res.status === 422) {
|
|
421
|
+
throw new GithubApiError('GitHub would not invite that account', { status: 422, code: 'cannot_invite' });
|
|
422
|
+
}
|
|
423
|
+
throw new GithubApiError(`GitHub collaborator invite failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Where one person stands on the repo: 'collaborator' (accepted), 'pending' (the
|
|
427
|
+
// invitation is out and not yet accepted), or 'none' (never invited, declined or
|
|
428
|
+
// expired). Two reads: the collaborator check, then the open invitations.
|
|
429
|
+
async function repoInviteState(token, { owner, name, login, fetchImpl = fetch } = {}) {
|
|
430
|
+
const who = encodeURIComponent(login);
|
|
431
|
+
const c = await fetchImpl(`${repoPath(owner, name)}/collaborators/${who}`, { headers: ghHeaders(token) });
|
|
432
|
+
if (c.status === 204) return { state: 'collaborator' };
|
|
433
|
+
if (c.status === 401) {
|
|
434
|
+
throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
|
|
435
|
+
}
|
|
436
|
+
if (c.status === 403) {
|
|
437
|
+
throw new GithubApiError('GitHub refused — reading collaborators needs repository admin', { status: 403, code: 'not_repo_admin' });
|
|
438
|
+
}
|
|
439
|
+
if (c.status !== 404) {
|
|
440
|
+
throw new GithubApiError(`GitHub collaborator read failed (${c.status})`, { status: c.status, code: 'github_error' });
|
|
441
|
+
}
|
|
442
|
+
const i = await fetchImpl(`${repoPath(owner, name)}/invitations?per_page=${PER_PAGE}`, { headers: ghHeaders(token) });
|
|
443
|
+
if (i.status === 404) {
|
|
444
|
+
throw new GithubApiError('GitHub answered 404 for that repository', { status: 404, code: 'repo_not_found' });
|
|
445
|
+
}
|
|
446
|
+
if (!i.ok) {
|
|
447
|
+
throw new GithubApiError(`GitHub invitations read failed (${i.status})`, { status: i.status, code: 'github_error' });
|
|
448
|
+
}
|
|
449
|
+
const rows = await i.json();
|
|
450
|
+
const mine = (Array.isArray(rows) ? rows : []).find(
|
|
451
|
+
(r) => r && r.invitee && String(r.invitee.login || '').toLowerCase() === String(login).toLowerCase(),
|
|
452
|
+
);
|
|
453
|
+
return mine
|
|
454
|
+
? { state: 'pending', invitation_id: mine.id, permission: mine.permissions || null }
|
|
455
|
+
: { state: 'none' };
|
|
456
|
+
}
|
|
457
|
+
|
|
381
458
|
module.exports = {
|
|
459
|
+
inviteRepoCollaborator, repoInviteState, REPO_INVITE_PERMISSIONS,
|
|
382
460
|
listUserRepos, createUserRepo, getUserRepo, makeRepoPublic, makeRepoPrivate,
|
|
383
461
|
listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin,
|
|
384
462
|
listDeployKeys, addDeployKey, deleteDeployKey,
|
|
@@ -366,6 +366,28 @@ const MODULE_GLOBS = {
|
|
|
366
366
|
agents: [
|
|
367
367
|
'modules/agents/', '.claude/agents/', 'scripts/bongos/agents-sync.js',
|
|
368
368
|
],
|
|
369
|
+
// --- the MODULE STORE (ADR 0338, task 1004793): publishing, listing, entitlements,
|
|
370
|
+
// testing channel, promotion, usage reports, payouts and the assessment CLIs.
|
|
371
|
+
// It is core code — it lives in src/bongos/, not modules/ — so it has no
|
|
372
|
+
// module.json and this key is a scope like kernel or devsecops. Before this
|
|
373
|
+
// key NO key resolved these files, so no goal could own a change to the store
|
|
374
|
+
// (the 1002690 / 1002736 precedent). Its HTTP surface, src/bongos/routes/modules.js,
|
|
375
|
+
// stays kernel's with the rest of routes/. module-scope-map.js is kernel's too:
|
|
376
|
+
// the name prefix is shared, the job is not. ---
|
|
377
|
+
'module-store': [
|
|
378
|
+
'src/bongos/module-store.js', 'src/bongos/module-listing.js',
|
|
379
|
+
'src/bongos/module-entitlements.js', 'src/bongos/module-overrides.js',
|
|
380
|
+
'src/bongos/module-payouts.js', 'src/bongos/module-promotion.js',
|
|
381
|
+
'src/bongos/module-rechecks.js', 'src/bongos/module-subcategories.js',
|
|
382
|
+
'src/bongos/module-submissions.js', 'src/bongos/module-testing.js',
|
|
383
|
+
'src/bongos/module-usage-reports.js',
|
|
384
|
+
'scripts/bongos/module-store-cli.js', 'scripts/bongos/module-artifact.js',
|
|
385
|
+
'scripts/bongos/module-assess-docs.js', 'scripts/bongos/module-assess-installs.js',
|
|
386
|
+
'scripts/bongos/module-assess-score.js', 'scripts/bongos/module-assess-security.js',
|
|
387
|
+
'scripts/bongos/module-assess-tests.js', 'scripts/bongos/module-assess-version.js',
|
|
388
|
+
'scripts/bongos/module-error-report.js', 'scripts/bongos/module-remove.js',
|
|
389
|
+
'scripts/bongos/module-test-runner.js',
|
|
390
|
+
],
|
|
369
391
|
// --- the hub identity provider + the PROJECTS CATALOG it owns (ADR 0141): platform
|
|
370
392
|
// accounts, the SSO client registry, project memberships/invites, and the
|
|
371
393
|
// platform_identity_projects rows the Explore feed reads. Carved already, so the
|
package/src/bongos/role-stats.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
// WHAT IT ANSWERS. For one builder: which of the three crafts they are shown in,
|
|
9
9
|
// which one leads, and each craft's own headline numbers.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
11
|
+
// artisan `shipped`, works shipped (unchanged from today), and building credits
|
|
12
12
|
// artist pages approved, "N of M tweaked" per surface, credits from pages,
|
|
13
13
|
// this week (copy-desk's studio tally, the `copy-desk.artistStats`
|
|
14
14
|
// port)
|
|
15
|
-
//
|
|
15
|
+
// AND (task 1005141, the ideator craft folded in) thoughts filed, ignited, Full Ideas, ratified by the board, total
|
|
16
16
|
// size across every idea stream with the historical-estimate
|
|
17
17
|
// marker, and the share still alive (the sky's own reads, the
|
|
18
18
|
// `ideas.ideatorStats` port)
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
const seams = require('../module-seams');
|
|
42
42
|
|
|
43
|
-
const CRAFTS = Object.freeze(['
|
|
43
|
+
const CRAFTS = Object.freeze(['artisan', 'artist']);
|
|
44
44
|
// A discipline as a craft, asked of economy (reward.craftForPreference), the
|
|
45
45
|
// module that decides which craft a credit belongs to. One table, so a builder's
|
|
46
46
|
// chips and their credits cannot disagree (`ui`, the design discipline, is the
|
|
@@ -76,7 +76,7 @@ function resolveMainRole({ mainDiscipline = null, preferred = [], byCraft = null
|
|
|
76
76
|
}
|
|
77
77
|
const pref = preferredCrafts(preferred);
|
|
78
78
|
if (pref.length) return { main: pref[0], basis: 'preferred' };
|
|
79
|
-
return { main: '
|
|
79
|
+
return { main: 'artisan', basis: 'default' };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// shownCrafts — pure. Main first, then every craft with credits, in the
|
|
@@ -97,7 +97,7 @@ function composeRoleStats({ choice = null, byCraft = null, shippedCount = 0, art
|
|
|
97
97
|
const { main, basis } = resolveMainRole({ mainDiscipline: choice && choice.main_discipline, preferred, byCraft });
|
|
98
98
|
const shown = shownCrafts({ main, preferred, byCraft });
|
|
99
99
|
const credits = byCraft
|
|
100
|
-
? {
|
|
100
|
+
? { artisan: byCraft.artisan, artist: byCraft.artist, other: byCraft.other, total: byCraft.total }
|
|
101
101
|
: null;
|
|
102
102
|
return {
|
|
103
103
|
main,
|
|
@@ -105,16 +105,32 @@ function composeRoleStats({ choice = null, byCraft = null, shippedCount = 0, art
|
|
|
105
105
|
shown,
|
|
106
106
|
preferred: preferredCrafts(preferred),
|
|
107
107
|
credits_by_craft: credits,
|
|
108
|
-
|
|
109
|
-
? { shipped: Number(shippedCount) || 0, credits: credits ? credits.
|
|
108
|
+
artisan: shown.includes('artisan')
|
|
109
|
+
? { shipped: Number(shippedCount) || 0, credits: credits ? credits.artisan : null }
|
|
110
110
|
: null,
|
|
111
|
-
artist: shown.includes('artist') ? (artist
|
|
112
|
-
ideator: shown.includes('ideator') ? (ideator || emptyIdeator()) : null,
|
|
111
|
+
artist: shown.includes('artist') ? artistBlock(artist, ideator, credits) : null,
|
|
113
112
|
};
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
// The shapes a shown craft takes when its port could not be asked: every number
|
|
117
116
|
// null, so a renderer says "unknown", never "0".
|
|
117
|
+
// The artist craft holds what the artist AND the old ideator craft did (task 1005141,
|
|
118
|
+
// BONGOS-V3: ideation folds into the artist). One block, two sources: the page
|
|
119
|
+
// numbers from copy-desk and the idea numbers from the sky. The two share no field
|
|
120
|
+
// name but `unavailable` (true only when NEITHER could be asked) and `credits`,
|
|
121
|
+
// which is the craft's ledger split when there is one, since that now carries
|
|
122
|
+
// both the page and the idea credit streams (economy credits-by-craft.js).
|
|
123
|
+
function artistBlock(artist, ideator, credits) {
|
|
124
|
+
const pages = artist || emptyArtist();
|
|
125
|
+
const ideas = ideator || emptyIdeator();
|
|
126
|
+
return {
|
|
127
|
+
...ideas,
|
|
128
|
+
...pages,
|
|
129
|
+
credits: credits ? credits.artist : pages.credits,
|
|
130
|
+
unavailable: Boolean(pages.unavailable && ideas.unavailable),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
function emptyArtist() {
|
|
119
135
|
return { pages_approved: null, pages: null, surfaces_touched: null, credits: null, this_week: null, surfaces: [], unavailable: true };
|
|
120
136
|
}
|
|
@@ -141,7 +157,7 @@ async function readRoleStats(builderId, { shippedCount = 0, getRoleChoice } = {}
|
|
|
141
157
|
const shown = shownCrafts({ main, preferred, byCraft });
|
|
142
158
|
|
|
143
159
|
const artistPort = shown.includes('artist') ? seams.resolveOptional('copy-desk.artistStats') : null;
|
|
144
|
-
const ideatorPort = shown.includes('
|
|
160
|
+
const ideatorPort = shown.includes('artist') ? seams.resolveOptional('ideas.ideatorStats') : null;
|
|
145
161
|
const [artist, ideator] = await Promise.all([
|
|
146
162
|
typeof artistPort === 'function' ? artistPort(builderId) : null,
|
|
147
163
|
typeof ideatorPort === 'function' ? ideatorPort(builderId) : null,
|
|
@@ -176,7 +192,7 @@ async function readRolesForBuilders(builderIds, { getRoleChoices } = {}) {
|
|
|
176
192
|
main_basis: basis,
|
|
177
193
|
shown: shownCrafts({ main, preferred, byCraft }),
|
|
178
194
|
credits_by_craft: byCraft
|
|
179
|
-
? {
|
|
195
|
+
? { artisan: byCraft.artisan, artist: byCraft.artist, other: byCraft.other, total: byCraft.total }
|
|
180
196
|
: null,
|
|
181
197
|
});
|
|
182
198
|
}
|
|
@@ -19,11 +19,12 @@ const express = require('express');
|
|
|
19
19
|
const auth = require('../auth');
|
|
20
20
|
const db = require('../db');
|
|
21
21
|
const secretBox = require('../secret-box');
|
|
22
|
-
const { listUserRepos, createUserRepo, listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin } = require('../github-repos');
|
|
22
|
+
const { listUserRepos, createUserRepo, listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin, inviteRepoCollaborator, repoInviteState, REPO_INVITE_PERMISSIONS } = require('../github-repos');
|
|
23
23
|
// Namespace require for the visibility helpers (task 1003045): the route tests
|
|
24
24
|
// stub these on the cached module object, which a load-time destructure would
|
|
25
25
|
// pin to the real implementations.
|
|
26
26
|
const githubRepos = require('../github-repos');
|
|
27
|
+
const repoInfo = require('../repo-info'); // namespace, so a test can stub loadRepoInfo
|
|
27
28
|
const { validateOrRespond } = require('./_helpers');
|
|
28
29
|
const prelaunch = require('../prelaunch-gate');
|
|
29
30
|
const log = require('../logger').logger.child({ src: 'github' });
|
|
@@ -43,6 +44,28 @@ function orgNotFound(res, org) {
|
|
|
43
44
|
return res.fail('org_not_found', { status: 404, message: `GitHub didn’t show your account an organisation called ${org}.` });
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// The failures the two repo-invite routes share, each in the owner's words.
|
|
48
|
+
async function repoInviteFailure(req, res, err, where) {
|
|
49
|
+
if (err && err.status === 401) {
|
|
50
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
51
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect GitHub to invite people to the repository.' });
|
|
52
|
+
}
|
|
53
|
+
if (err && err.code === 'not_repo_admin') {
|
|
54
|
+
return res.fail('not_repo_admin', { status: 409, message: 'GitHub says your account can’t add people to this repository. Only its owner or an admin can: ask them, or sign in with that account.' });
|
|
55
|
+
}
|
|
56
|
+
if (err && err.code === 'repo_not_found') {
|
|
57
|
+
return res.fail('repo_not_found', { status: 404, message: 'GitHub didn’t show that repository or that username to your account. Check the spelling of the name and try again.' });
|
|
58
|
+
}
|
|
59
|
+
if (err && err.code === 'bad_permission') {
|
|
60
|
+
return res.fail('bad_permission', { status: 400, message: 'Only read-only access can be offered on the repository.' });
|
|
61
|
+
}
|
|
62
|
+
if (err && err.code === 'cannot_invite') {
|
|
63
|
+
return res.fail('cannot_invite', { status: 409, message: 'GitHub wouldn’t invite that account. It may already own the repository, or be unable to join. Check the name, or add them on GitHub yourself.' });
|
|
64
|
+
}
|
|
65
|
+
log.error(`[gds] ${where}`, err && err.message);
|
|
66
|
+
return res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again in a moment.' });
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
// Parse + validate the owner/name ref, or send a 400 and return null.
|
|
47
70
|
function parseRepoRef(raw, res) {
|
|
48
71
|
const ref = String(raw || '').trim();
|
|
@@ -79,9 +102,84 @@ async function resolveRepoToken(req, res) {
|
|
|
79
102
|
return row.token;
|
|
80
103
|
}
|
|
81
104
|
|
|
82
|
-
|
|
105
|
+
// The repo THIS project's code lives in, or a 409 and null. The invite routes below
|
|
106
|
+
// never take a repo from the request: the owner invites people to the project's own
|
|
107
|
+
// repo, so a typed name cannot point the owner's token at some other repository.
|
|
108
|
+
function projectRepoRef(res) {
|
|
109
|
+
const info = repoInfo.loadRepoInfo();
|
|
110
|
+
if (!info || info.error) {
|
|
111
|
+
res.fail('no_project_repo', { status: 409, message: 'This project has no GitHub repository connected, so there is nothing to invite them to. Connect one on the project’s page on the hub, then try again.' });
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return { owner: info.owner, name: info.repo };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A typed GitHub name, with the leading @ allowed, or a 400 and null.
|
|
118
|
+
function parseInviteLogin(raw, res) {
|
|
119
|
+
const login = String(raw || '').trim().replace(/^@/, '');
|
|
120
|
+
if (!validGithubLogin(login)) {
|
|
121
|
+
res.fail('bad_github_login', { status: 400, message: 'That does not look like a GitHub username.' });
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return login;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// `deps` is the test seam for the two repo-invite helpers: they are imported directly
|
|
128
|
+
// (so the dead-code check sees them used) and a test hands in stand-ins here, because a
|
|
129
|
+
// destructured import cannot be stubbed on the module object the way the older
|
|
130
|
+
// namespace-called helpers below are.
|
|
131
|
+
module.exports = function buildGithubRouter(overrides = {}) {
|
|
132
|
+
const deps = { inviteRepoCollaborator, repoInviteState, ...overrides };
|
|
83
133
|
const router = express.Router();
|
|
84
134
|
|
|
135
|
+
// rank: archon (permission access_request.review, the same gate as a hall invite) —
|
|
136
|
+
// invites a person to THIS project's GitHub repository, acting as the caller's OWN
|
|
137
|
+
// GitHub account (their stored repo token), never a platform credential. GitHub
|
|
138
|
+
// decides whether the caller may add people. The access is always READ-ONLY:
|
|
139
|
+
// write ('push') and above are refused, since a builder ships through the server.
|
|
140
|
+
// It grants no Bongos rank and does not admit anyone to the hall: those are the
|
|
141
|
+
// hall invite's own record.
|
|
142
|
+
router.post('/github/repo-invite', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
143
|
+
// No act-alone gate here, on purpose: that gate decides who may admit people to the
|
|
144
|
+
// hall (the hall invite carries it). This records a read-only share on GitHub, and
|
|
145
|
+
// GitHub itself only lets an account that administers the repo do it.
|
|
146
|
+
if (validateOrRespond(req, res, {
|
|
147
|
+
github_login: { required: true, type: 'string', maxLength: 60 },
|
|
148
|
+
permission: { type: 'string', enum: REPO_INVITE_PERMISSIONS },
|
|
149
|
+
})) return;
|
|
150
|
+
const login = parseInviteLogin(req.body.github_login, res);
|
|
151
|
+
if (!login) return;
|
|
152
|
+
const ref = projectRepoRef(res);
|
|
153
|
+
if (!ref) return;
|
|
154
|
+
const token = await resolveRepoToken(req, res);
|
|
155
|
+
if (!token) return;
|
|
156
|
+
try {
|
|
157
|
+
const out = await deps.inviteRepoCollaborator(token, { ...ref, login, permission: req.body.permission || 'pull' });
|
|
158
|
+
res.status(out.state === 'invited' ? 201 : 200).json({ ok: true, repo: `${ref.owner}/${ref.name}`, github_login: login, ...out });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return repoInviteFailure(req, res, err, 'POST /github/repo-invite');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// rank: archon (permission access_request.review) — where one person stands on the
|
|
165
|
+
// project's repo: collaborator, pending (invite sent, not yet accepted) or none.
|
|
166
|
+
router.get('/github/repo-invite', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
167
|
+
// Strict, like the POST: a stray `repo=` (or anything else) is refused, never ignored.
|
|
168
|
+
if (validateOrRespond(req, res, {}, { query: { github_login: { required: true, type: 'string', maxLength: 60 } } })) return;
|
|
169
|
+
const login = parseInviteLogin(req.query.github_login, res);
|
|
170
|
+
if (!login) return;
|
|
171
|
+
const ref = projectRepoRef(res);
|
|
172
|
+
if (!ref) return;
|
|
173
|
+
const token = await resolveRepoToken(req, res);
|
|
174
|
+
if (!token) return;
|
|
175
|
+
try {
|
|
176
|
+
const out = await deps.repoInviteState(token, { ...ref, login });
|
|
177
|
+
res.json({ ok: true, repo: `${ref.owner}/${ref.name}`, github_login: login, ...out });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return repoInviteFailure(req, res, err, 'GET /github/repo-invite');
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
85
183
|
// rank: any-builder — acts only on the caller's OWN GitHub identity (the token
|
|
86
184
|
// they granted). No rank gate beyond being a signed-in builder.
|
|
87
185
|
router.get('/github/repos', auth.requireBuilder, async (req, res) => {
|