@bongos/core 1.18.0 → 1.18.18
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 +1740 -640
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +5 -3
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +11 -2
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +15 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +252 -62
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -21
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +41 -23
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +198 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/package_core.mjs +246 -1
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +140 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -58,7 +58,13 @@
|
|
|
58
58
|
// Cookie-authed PATCH; throws { status, data } on a non-2xx. The server enforces
|
|
59
59
|
// the Metic+ gate (ADR 0018); the button only renders for metic+ below.
|
|
60
60
|
async function patchJSON(path, body) {
|
|
61
|
-
|
|
61
|
+
return writeJSON('PATCH', path, body);
|
|
62
|
+
}
|
|
63
|
+
async function postJSON(path, body) {
|
|
64
|
+
return writeJSON('POST', path, body);
|
|
65
|
+
}
|
|
66
|
+
async function writeJSON(method, path, body) {
|
|
67
|
+
var res = await api.request(method, path, { body: body || {} });
|
|
62
68
|
if (res.status === 401) { window.location.assign(API + '/auth/web/start'); throw new Error('redirecting to auth'); }
|
|
63
69
|
var data = null;
|
|
64
70
|
try { data = res.data; } catch (_) { /* empty / non-JSON */ }
|
|
@@ -66,6 +72,16 @@
|
|
|
66
72
|
return data;
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
// dom-utils.js already publishes escapeHtml on window.OTB (this file uses
|
|
76
|
+
// OTB.toast the same way) — a local copy would be its 9th byte-identical twin.
|
|
77
|
+
// The fallback keeps the file's self-contained contract if OTB hasn't loaded.
|
|
78
|
+
function esc(s) {
|
|
79
|
+
if (window.OTB && window.OTB.escapeHtml) return window.OTB.escapeHtml(s);
|
|
80
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
81
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
async function fetchRef(kind, id) {
|
|
70
86
|
if (kind === 'blocker') {
|
|
71
87
|
var db = await getJSON(API + '/blockers/' + encodeURIComponent(id), { softAuth: true });
|
|
@@ -102,37 +118,342 @@
|
|
|
102
118
|
}
|
|
103
119
|
if (r.shipped_at) nodes.push(h('p', 'deeplink-card__note', 'Shipped ' + fmtRel(r.shipped_at)));
|
|
104
120
|
|
|
105
|
-
// task 759: Metic+ idea triage
|
|
106
|
-
//
|
|
107
|
-
//
|
|
121
|
+
// task 759 → task 1002447: Metic+ idea triage, right in the overlay (no
|
|
122
|
+
// /idea-triage skill and no LLM in the loop). 759 shipped Discard alone;
|
|
123
|
+
// 1002447 adds the other two verbs so the whole verdict set is reachable
|
|
124
|
+
// from the browser — criterion `gcarry-bx-hall`. The panel only renders for
|
|
125
|
+
// metic/archon; the server enforces the same gate regardless.
|
|
108
126
|
var rank = (viewer && viewer.rank) ? String(viewer.rank).toLowerCase() : '';
|
|
109
127
|
if (kind === 'idea' && r.status === 'open' && (rank === 'metic' || rank === 'archon')) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
? 'Only Metic+ can triage ideas.'
|
|
125
|
-
: 'Could not discard that idea. Try again.');
|
|
126
|
-
discardBtn.disabled = false;
|
|
127
|
-
discardBtn.textContent = 'Discard';
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
actions.appendChild(discardBtn);
|
|
131
|
-
nodes.push(actions);
|
|
128
|
+
nodes.push(triagePanel(r));
|
|
129
|
+
}
|
|
130
|
+
// A closed idea says what triage decided — otherwise the verdict is a status
|
|
131
|
+
// word with no story behind it.
|
|
132
|
+
if (kind === 'idea' && r.status === 'merged' && r.merged_into_idea_id) {
|
|
133
|
+
var m = h('p', 'deeplink-card__note');
|
|
134
|
+
m.appendChild(document.createTextNode('Merged into '));
|
|
135
|
+
var ml = h('a', 'deeplink-card__link', '#' + r.merged_into_idea_id);
|
|
136
|
+
ml.href = '#/idea/' + encodeURIComponent(r.merged_into_idea_id);
|
|
137
|
+
m.appendChild(ml);
|
|
138
|
+
nodes.push(m);
|
|
139
|
+
}
|
|
140
|
+
if (kind === 'idea' && r.triage_note) {
|
|
141
|
+
nodes.push(h('p', 'deeplink-card__note', 'Triage note: ' + r.triage_note));
|
|
132
142
|
}
|
|
133
143
|
return nodes;
|
|
134
144
|
}
|
|
135
145
|
|
|
146
|
+
// ---- the triage panel (task 1002447) ---------------------------------------
|
|
147
|
+
//
|
|
148
|
+
// Promote → POST /tasks, then PATCH the idea promoted + linked to it.
|
|
149
|
+
// Merge → PATCH merged, pointing at the canonical idea (core_195).
|
|
150
|
+
// Discard → PATCH discarded, with the reason (core_195).
|
|
151
|
+
//
|
|
152
|
+
// Each verb is a disclosure: the action row swaps for a compact form and back,
|
|
153
|
+
// the goals-page invite-box idiom (goals-page.js renderInviteBox). One panel
|
|
154
|
+
// owns its mode and always rebuilds from scratch, so there is no half-updated
|
|
155
|
+
// state to reason about.
|
|
156
|
+
|
|
157
|
+
// Mirrors LIMITS.TITLE / LIMITS.DESCRIPTION (src/bongos/routes/_helpers.js):
|
|
158
|
+
// the form stops where POST /tasks stops, so a builder can't write past the
|
|
159
|
+
// server's cap and get a 400 back for it.
|
|
160
|
+
var TITLE_MAX = 200;
|
|
161
|
+
var DESCRIPTION_MAX = 50000;
|
|
162
|
+
|
|
163
|
+
// The open versions a promotion can target. POST /tasks 409s on a shipped
|
|
164
|
+
// version, so offering one would be an invitation to fail. Cached for the page.
|
|
165
|
+
var versionsCache = null;
|
|
166
|
+
async function openVersions() {
|
|
167
|
+
if (versionsCache) return versionsCache;
|
|
168
|
+
var d = await getJSON(API + '/versions?limit=200', { softAuth: true });
|
|
169
|
+
versionsCache = (d && d.versions ? d.versions : []).filter(function (v) { return v.status !== 'shipped'; });
|
|
170
|
+
return versionsCache;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Home's attention inbox holds the same rows; tell it a verdict landed so the
|
|
174
|
+
// queue advances behind the overlay instead of going stale until a reload.
|
|
175
|
+
function announce(id, verdict) {
|
|
176
|
+
try {
|
|
177
|
+
window.dispatchEvent(new CustomEvent('otb:idea-triaged', { detail: { id: id, verdict: verdict } }));
|
|
178
|
+
} catch (_) { /* CustomEvent unsupported — the refresh is a nicety */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The R01/ADR-0116 envelope is `{ error: { code, message, details } }` — the
|
|
182
|
+
// code is NESTED. Reading err.data.error directly yields the OBJECT, so every
|
|
183
|
+
// `code === '…'` test below would silently never match. The string arm is the
|
|
184
|
+
// pre-envelope shape, kept so an older server still maps.
|
|
185
|
+
function errCode(err) {
|
|
186
|
+
var d = err && err.data;
|
|
187
|
+
var e = d && d.error;
|
|
188
|
+
if (e && typeof e === 'object') return e.code || '';
|
|
189
|
+
if (typeof e === 'string') return e;
|
|
190
|
+
return (d && d.code) || '';
|
|
191
|
+
}
|
|
192
|
+
function errMessage(err) {
|
|
193
|
+
var d = err && err.data;
|
|
194
|
+
var e = d && d.error;
|
|
195
|
+
if (e && typeof e === 'object' && e.message) return e.message;
|
|
196
|
+
return (d && d.message) || '';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function triageError(err, fallback) {
|
|
200
|
+
if (err && err.message === 'redirecting to auth') return null;
|
|
201
|
+
var code = errCode(err);
|
|
202
|
+
// 'rank_forbidden' is what auth.js requireRank actually emits; the 403 arm
|
|
203
|
+
// also covers POST /tasks, which is Metic+ for the same reason.
|
|
204
|
+
if (code === 'rank_forbidden' || (err && err.status === 403)) return 'Only Metic+ can triage ideas.';
|
|
205
|
+
if (code === 'MERGE_SELF') return 'An idea can’t be merged into itself.';
|
|
206
|
+
if (code === 'MERGE_TARGET_NOT_FOUND') return 'No idea with that number.';
|
|
207
|
+
if (code === 'MERGE_TARGET_MERGED') return 'That idea was itself merged — merge into the one it folded into.';
|
|
208
|
+
if (code === 'version_closed') return 'That version has shipped and can’t take new work.';
|
|
209
|
+
if (code === 'version_not_found') return 'That version no longer exists.';
|
|
210
|
+
return errMessage(err) || fallback;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function triagePanel(idea) {
|
|
214
|
+
var box = h('div', 'td-triage');
|
|
215
|
+
var mode = 'idle';
|
|
216
|
+
|
|
217
|
+
// [data-err] marks the one paragraph errors belong in; the merge form also
|
|
218
|
+
// carries a hint paragraph, so a class-based lookup would race it.
|
|
219
|
+
function note(text) {
|
|
220
|
+
var p = box.querySelector('[data-err]');
|
|
221
|
+
if (p) p.textContent = text || '';
|
|
222
|
+
}
|
|
223
|
+
function busy(on, submitLabel) {
|
|
224
|
+
box.querySelectorAll('button, input, select').forEach(function (el) { el.disabled = on; });
|
|
225
|
+
var submit = box.querySelector('[data-submit]');
|
|
226
|
+
if (submit) submit.textContent = on ? '…' : submitLabel;
|
|
227
|
+
}
|
|
228
|
+
function go(next) { mode = next; render(); }
|
|
229
|
+
|
|
230
|
+
// A verdict that leaves nothing to look at closes the overlay; promote
|
|
231
|
+
// produces a task worth linking to, so it stays open on a result line.
|
|
232
|
+
function settled(verdict, message) {
|
|
233
|
+
announce(idea.id, verdict);
|
|
234
|
+
toast(message);
|
|
235
|
+
clearLink();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function render() {
|
|
239
|
+
if (mode === 'idle') return renderIdle();
|
|
240
|
+
if (mode === 'promote') return renderPromote();
|
|
241
|
+
if (mode === 'merge') return renderMerge();
|
|
242
|
+
if (mode === 'discard') return renderDiscard();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renderIdle() {
|
|
246
|
+
box.innerHTML =
|
|
247
|
+
'<div class="td-actions">'
|
|
248
|
+
+ '<button type="button" class="td-btn td-btn--primary" data-go="promote">Promote</button>'
|
|
249
|
+
+ '<button type="button" class="td-btn" data-go="merge">Merge</button>'
|
|
250
|
+
+ '<button type="button" class="td-btn" data-go="discard">Discard</button>'
|
|
251
|
+
+ '</div>';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function renderPromote() {
|
|
255
|
+
box.innerHTML = '<p class="deeplink-card__note">Loading versions…</p>';
|
|
256
|
+
// Both dead ends offer the way back through the same delegated [data-go]
|
|
257
|
+
// handler the forms use — a builder must never be stranded mid-panel.
|
|
258
|
+
function bail(message) {
|
|
259
|
+
box.innerHTML = '<p class="td-action-note">' + esc(message) + '</p>'
|
|
260
|
+
+ '<div class="td-actions"><button type="button" class="td-btn" data-go="idle">Back</button></div>';
|
|
261
|
+
}
|
|
262
|
+
var versions;
|
|
263
|
+
try {
|
|
264
|
+
versions = await openVersions();
|
|
265
|
+
} catch (_) {
|
|
266
|
+
bail('Could not load the version list — try again.');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (!versions.length) {
|
|
270
|
+
bail('No open version to file this against.');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
var preferred = versions.some(function (v) { return v.id === idea.suggested_version; })
|
|
274
|
+
? idea.suggested_version : versions[0].id;
|
|
275
|
+
var options = versions.map(function (v) {
|
|
276
|
+
return '<option value="' + esc(v.id) + '"' + (v.id === preferred ? ' selected' : '') + '>'
|
|
277
|
+
+ esc(v.id) + ' — ' + esc(v.name || '') + '</option>';
|
|
278
|
+
}).join('');
|
|
279
|
+
// suggested_priority is accepted 0-100 on capture but tasks.priority is
|
|
280
|
+
// CHECKed 1-5, so clamp rather than let an out-of-range value match no
|
|
281
|
+
// option and leave the browser silently defaulting to P1.
|
|
282
|
+
var pri = Math.min(5, Math.max(1, Number(idea.suggested_priority) || 3));
|
|
283
|
+
var priOptions = [1, 2, 3, 4, 5].map(function (p) {
|
|
284
|
+
return '<option value="' + p + '"' + (p === pri ? ' selected' : '') + '>P' + p + '</option>';
|
|
285
|
+
}).join('');
|
|
286
|
+
box.innerHTML =
|
|
287
|
+
'<form class="td-form" data-form="promote">'
|
|
288
|
+
+ '<label class="td-field td-field--wide"><span>Title</span>'
|
|
289
|
+
+ '<input type="text" name="title" maxlength="' + TITLE_MAX + '" required value="' + esc(idea.title || '') + '"></label>'
|
|
290
|
+
+ '<label class="td-field td-field--wide"><span>Version</span><select name="version">' + options + '</select></label>'
|
|
291
|
+
+ '<label class="td-field"><span>Priority</span><select name="priority">' + priOptions + '</select></label>'
|
|
292
|
+
+ '<label class="td-field"><span>Estimate (min)</span>'
|
|
293
|
+
+ '<input type="number" name="est" min="0" max="100000" step="5" placeholder="—"></label>'
|
|
294
|
+
+ '<label class="td-field"><span>Reward</span>'
|
|
295
|
+
+ '<input type="number" name="credits" min="0" max="1000000" step="1" placeholder="0"></label>'
|
|
296
|
+
+ '<div class="td-actions">'
|
|
297
|
+
+ '<button type="submit" class="td-btn td-btn--primary" data-submit>Create task</button>'
|
|
298
|
+
+ '<button type="button" class="td-btn" data-go="idle">Cancel</button>'
|
|
299
|
+
+ '</div><p class="td-action-note" data-err></p></form>';
|
|
300
|
+
box.querySelector('[data-form="promote"]').addEventListener('submit', function (ev) {
|
|
301
|
+
ev.preventDefault();
|
|
302
|
+
submitPromote(ev.target);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function submitPromote(form) {
|
|
307
|
+
var title = String(form.title.value || '').trim();
|
|
308
|
+
if (!title) { note('A task needs a title.'); return; }
|
|
309
|
+
note('');
|
|
310
|
+
busy(true, 'Create task');
|
|
311
|
+
// source/source_ref make the create idempotent on (source, source_ref):
|
|
312
|
+
// a retried promote resolves to the SAME task instead of minting a twin.
|
|
313
|
+
// body_md is itself capped at LIMITS.DESCRIPTION, so appending provenance
|
|
314
|
+
// to a maximal one would push the task description past the same cap.
|
|
315
|
+
var provenance = 'Promoted from idea #' + idea.id + '.';
|
|
316
|
+
var body = (idea.body_md ? idea.body_md + '\n\n' : '').slice(0, DESCRIPTION_MAX - provenance.length);
|
|
317
|
+
var payload = {
|
|
318
|
+
version_id: form.version.value,
|
|
319
|
+
title: title,
|
|
320
|
+
description: body + provenance,
|
|
321
|
+
priority: Number(form.priority.value) || 3,
|
|
322
|
+
source: 'idea_inbox',
|
|
323
|
+
source_ref: 'idea:' + idea.id,
|
|
324
|
+
};
|
|
325
|
+
if (form.est.value !== '') payload.est_minutes = Number(form.est.value);
|
|
326
|
+
if (form.credits.value !== '') payload.credits_reward = Number(form.credits.value);
|
|
327
|
+
// 'criterion-proposal' is an idea-only kind; everything else is shared with
|
|
328
|
+
// tasks.kind, so carrying it over saves the classifier a guess.
|
|
329
|
+
if (idea.kind && idea.kind !== 'criterion-proposal') payload.kind = idea.kind;
|
|
330
|
+
// Promote is two writes. If the second fails the task exists but the idea
|
|
331
|
+
// is still open — say so precisely, because "could not create that task"
|
|
332
|
+
// would send the builder to make a duplicate. Retrying is safe and is the
|
|
333
|
+
// right advice: the idempotent create returns the SAME task, then the link
|
|
334
|
+
// is attempted again.
|
|
335
|
+
var task = null;
|
|
336
|
+
try {
|
|
337
|
+
var created = await postJSON(API + '/tasks', payload);
|
|
338
|
+
task = created && created.task ? created.task : null;
|
|
339
|
+
if (!task || task.id == null) throw new Error('no task in response');
|
|
340
|
+
} catch (err) {
|
|
341
|
+
var createMsg = triageError(err, 'Could not create that task. Try again.');
|
|
342
|
+
if (createMsg === null) return;
|
|
343
|
+
busy(false, 'Create task');
|
|
344
|
+
note(createMsg);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
await patchJSON(API + '/inbox/' + encodeURIComponent(idea.id), {
|
|
349
|
+
status: 'promoted', promoted_to_task_id: Number(task.id),
|
|
350
|
+
});
|
|
351
|
+
announce(idea.id, 'promoted');
|
|
352
|
+
box.innerHTML = '<p class="deeplink-card__note">Promoted to task '
|
|
353
|
+
+ '<a class="deeplink-card__link" href="' + hallBase() + '/task/' + encodeURIComponent(task.id) + '">#'
|
|
354
|
+
+ esc(task.id) + '</a> — ' + esc(task.title || '') + '</p>';
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
357
|
+
busy(false, 'Create task');
|
|
358
|
+
note('Task #' + task.id + ' was created, but the idea could not be marked promoted. '
|
|
359
|
+
+ 'Press Create task again — it links the same task, it does not make a second one.');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function renderMerge() {
|
|
364
|
+
box.innerHTML =
|
|
365
|
+
'<form class="td-form" data-form="merge">'
|
|
366
|
+
+ '<label class="td-field td-field--wide"><span>Fold this idea into #</span>'
|
|
367
|
+
+ '<input type="number" name="target" min="1" step="1" required placeholder="idea number"></label>'
|
|
368
|
+
+ '<p class="td-form-hint" data-target-name></p>'
|
|
369
|
+
+ '<label class="td-field td-field--wide"><span>Note (optional)</span>'
|
|
370
|
+
+ '<input type="text" name="note" maxlength="1000" placeholder="why these are the same"></label>'
|
|
371
|
+
+ '<div class="td-actions">'
|
|
372
|
+
+ '<button type="submit" class="td-btn td-btn--primary" data-submit>Merge</button>'
|
|
373
|
+
+ '<button type="button" class="td-btn" data-go="idle">Cancel</button>'
|
|
374
|
+
+ '</div><p class="td-action-note" data-err></p></form>';
|
|
375
|
+
// Echo the target's title so a merge is confirmed against a real idea, not
|
|
376
|
+
// a number the builder half-remembered.
|
|
377
|
+
var input = box.querySelector('[name="target"]');
|
|
378
|
+
input.addEventListener('change', async function () {
|
|
379
|
+
var out = box.querySelector('[data-target-name]');
|
|
380
|
+
var v = String(input.value || '').trim();
|
|
381
|
+
if (!out) return;
|
|
382
|
+
if (!v) { out.textContent = ''; return; }
|
|
383
|
+
try {
|
|
384
|
+
var d = await getJSON(API + '/inbox/' + encodeURIComponent(v), { softAuth: true });
|
|
385
|
+
var t = d && (d.idea || d);
|
|
386
|
+
out.textContent = t && t.title ? '→ ' + t.title : '→ no idea #' + v;
|
|
387
|
+
} catch (_) {
|
|
388
|
+
out.textContent = '→ no idea #' + v;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
box.querySelector('[data-form="merge"]').addEventListener('submit', function (ev) {
|
|
392
|
+
ev.preventDefault();
|
|
393
|
+
submitMerge(ev.target);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function submitMerge(form) {
|
|
398
|
+
var target = Number(form.target.value);
|
|
399
|
+
if (!Number.isInteger(target) || target < 1) { note('Give the idea number to merge into.'); return; }
|
|
400
|
+
note('');
|
|
401
|
+
busy(true, 'Merge');
|
|
402
|
+
try {
|
|
403
|
+
await patchJSON(API + '/inbox/' + encodeURIComponent(idea.id), {
|
|
404
|
+
status: 'merged',
|
|
405
|
+
merged_into_idea_id: target,
|
|
406
|
+
triage_note: String(form.note.value || '').trim() || null,
|
|
407
|
+
});
|
|
408
|
+
settled('merged', 'Idea #' + idea.id + ' merged into #' + target + '.');
|
|
409
|
+
} catch (err) {
|
|
410
|
+
var msg = triageError(err, 'Could not merge that idea. Try again.');
|
|
411
|
+
if (msg === null) return;
|
|
412
|
+
busy(false, 'Merge');
|
|
413
|
+
note(msg);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function renderDiscard() {
|
|
418
|
+
box.innerHTML =
|
|
419
|
+
'<form class="td-form" data-form="discard">'
|
|
420
|
+
+ '<label class="td-field td-field--wide"><span>Reason (optional)</span>'
|
|
421
|
+
+ '<input type="text" name="note" maxlength="1000" placeholder="why this isn’t worth doing"></label>'
|
|
422
|
+
+ '<div class="td-actions">'
|
|
423
|
+
+ '<button type="submit" class="td-btn td-btn--primary" data-submit>Discard</button>'
|
|
424
|
+
+ '<button type="button" class="td-btn" data-go="idle">Cancel</button>'
|
|
425
|
+
+ '</div><p class="td-action-note" data-err></p></form>';
|
|
426
|
+
box.querySelector('[data-form="discard"]').addEventListener('submit', function (ev) {
|
|
427
|
+
ev.preventDefault();
|
|
428
|
+
submitDiscard(ev.target);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function submitDiscard(form) {
|
|
433
|
+
note('');
|
|
434
|
+
busy(true, 'Discard');
|
|
435
|
+
try {
|
|
436
|
+
await patchJSON(API + '/inbox/' + encodeURIComponent(idea.id), {
|
|
437
|
+
status: 'discarded',
|
|
438
|
+
triage_note: String(form.note.value || '').trim() || null,
|
|
439
|
+
});
|
|
440
|
+
settled('discarded', 'Idea #' + idea.id + ' discarded.');
|
|
441
|
+
} catch (err) {
|
|
442
|
+
var msg = triageError(err, 'Could not discard that idea. Try again.');
|
|
443
|
+
if (msg === null) return;
|
|
444
|
+
busy(false, 'Discard');
|
|
445
|
+
note(msg);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
box.addEventListener('click', function (e) {
|
|
450
|
+
var btn = e.target.closest('[data-go]');
|
|
451
|
+
if (btn) go(btn.dataset.go);
|
|
452
|
+
});
|
|
453
|
+
render();
|
|
454
|
+
return box;
|
|
455
|
+
}
|
|
456
|
+
|
|
136
457
|
function bodyFor(state) {
|
|
137
458
|
var nodes = [];
|
|
138
459
|
var label = '#' + state.id;
|
|
@@ -49,12 +49,15 @@
|
|
|
49
49
|
<header class="app-topbar" id="app-topbar"></header>
|
|
50
50
|
<div class="app-content">
|
|
51
51
|
<div class="page-head">
|
|
52
|
+
<!-- task 2175: the breadcrumb sits in the page head, above the card, so a
|
|
53
|
+
deep-linked visitor (this page is deliberately not in the nav) has a
|
|
54
|
+
way back before the task body has loaded. -->
|
|
55
|
+
<a class="task-detail__back" href="/work">← Back to Tasks</a>
|
|
52
56
|
<p class="page-head__sub" id="task-sub">Loading task…</p>
|
|
53
57
|
</div>
|
|
54
58
|
|
|
55
59
|
<main>
|
|
56
60
|
<section class="scroll" id="task-scroll" aria-labelledby="task-h">
|
|
57
|
-
<a class="task-detail__back" href="/work">← Back to the Work Board</a>
|
|
58
61
|
<div id="task-body">
|
|
59
62
|
<p class="deeplink-card__title" id="task-h">Loading…</p>
|
|
60
63
|
</div>
|
|
@@ -71,6 +74,8 @@
|
|
|
71
74
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
72
75
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
73
76
|
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
77
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
78
|
+
<script src="/builders/palette.js" defer></script>
|
|
74
79
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
75
80
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils" defer></script>
|
|
76
81
|
<!-- task-detail.js still owns the #/idea/N and #/blocker/N deep-link overlay
|
|
@@ -148,6 +148,8 @@
|
|
|
148
148
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
149
149
|
chrome exists before the page script runs. -->
|
|
150
150
|
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
151
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
152
|
+
<script src="/builders/palette.js" defer></script>
|
|
151
153
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
152
154
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
153
155
|
<!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Tasks — {{worldName}}</title>
|
|
7
7
|
<meta name="description" content="What {{worldName}} is building right now, and what waits to be claimed.">
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
live in style.css's hall-kit section. -->
|
|
16
16
|
<link rel="stylesheet" href="/builders/work.css">
|
|
17
17
|
</head>
|
|
18
|
-
<body data-page="work" data-page-title="
|
|
18
|
+
<body data-page="work" data-page-title="Tasks">
|
|
19
19
|
|
|
20
20
|
<div class="app">
|
|
21
21
|
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<header class="app-topbar" id="app-topbar"></header>
|
|
25
25
|
<div class="app-content">
|
|
26
26
|
<div class="page-head">
|
|
27
|
-
<p class="page-head__sub" id="work-sub">Loading
|
|
27
|
+
<p class="page-head__sub" id="work-sub">Loading tasks…</p>
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<main>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<section class="scroll" id="gate-scroll" aria-labelledby="gate-h" hidden>
|
|
37
37
|
<h2 id="gate-h" class="scroll__h">Primer required</h2>
|
|
38
38
|
<p class="scroll__lede" id="gate-body">
|
|
39
|
-
|
|
39
|
+
Tasks open once you have read the builder primer.
|
|
40
40
|
<a href="/primer">Read it here</a>, then mark it as read on the Home page.
|
|
41
41
|
</p>
|
|
42
42
|
</section>
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
(OTBKit.makeTabs toggles [hidden] — the window stays the only
|
|
46
46
|
scroller): Ready (default) | In Progress | Backlog | Completed.
|
|
47
47
|
Deep links: /work#in-progress etc. -->
|
|
48
|
-
<nav id="work-tabs" aria-label="
|
|
48
|
+
<nav id="work-tabs" aria-label="Task sections"></nav>
|
|
49
49
|
|
|
50
50
|
<!-- READY — the claimable queue: the page's primary object, above the
|
|
51
51
|
fold as the default tab. -->
|
|
52
52
|
<section class="scroll" id="claim-scroll" aria-labelledby="claim-h">
|
|
53
|
-
<h2 id="claim-h" class="scroll__h">Ready —
|
|
53
|
+
<h2 id="claim-h" class="scroll__h">Ready — tasks to do<span class="count-pill" id="ready-count" hidden></span></h2>
|
|
54
54
|
<p class="scroll__lede">Click <span class="kbd">Claim</span> to take a task right here — no terminal needed. Then click <span class="kbd">📋</span> to copy the task prompt and paste it into a fresh Claude Code session to begin.</p>
|
|
55
55
|
|
|
56
56
|
<!-- task 2170: the filter bar is built by OTBKit.makeFilterBar —
|
|
@@ -129,6 +129,8 @@
|
|
|
129
129
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
130
130
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
131
131
|
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
132
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
133
|
+
<script src="/builders/palette.js" defer></script>
|
|
132
134
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
133
135
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils" defer></script>
|
|
134
136
|
<!-- shared component kit (window.OTBKit) — tabs, pager, filter bar (task 2166). -->
|
|
@@ -384,7 +384,7 @@
|
|
|
384
384
|
</div>
|
|
385
385
|
<div class="task__credits">
|
|
386
386
|
<span class="task__credits-num">${fmtNum(window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward)}</span>
|
|
387
|
-
<span class="task__credits-label">${window.OTBBranding?.taskCreditLabel?.(t) ?? ((window.OTBBranding?.currencyLabel?.() ?? 'credits') + ' Est.')}</span>
|
|
387
|
+
<span class="task__credits-label" title="${window.OTBBranding?.rewardModeNote?.() ?? ''}">${window.OTBBranding?.taskCreditLabel?.(t) ?? ((window.OTBBranding?.currencyLabel?.() ?? 'credits') + ' Est.')}</span>
|
|
388
388
|
</div>
|
|
389
389
|
<div class="task__actions">
|
|
390
390
|
<button class="task__claim" type="button" data-task-id="${t.id}" aria-label="Claim task ${t.id}">Claim</button>
|
package/modules/ideas/inbox.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// GDS idea_inbox helpers — structured replacement for free-form generative-ideas.md.
|
|
2
2
|
//
|
|
3
|
-
// Schema (migration 006_pms_v2.sql):
|
|
3
|
+
// Schema (migration 006_pms_v2.sql, + core_195_idea_triage_provenance.sql):
|
|
4
4
|
// idea_inbox(id, title, body_md, kind, suggested_version, suggested_priority,
|
|
5
|
-
// captured_at, captured_by, status, promoted_to_task_id, reviewed_at
|
|
5
|
+
// captured_at, captured_by, status, promoted_to_task_id, reviewed_at,
|
|
6
|
+
// merged_into_idea_id, triage_note)
|
|
6
7
|
//
|
|
7
8
|
// kind values match tasks.kind (feature | bug | infra | refactor | spike |
|
|
8
9
|
// learning-capture | cleanup | decision | blocker-resolution | unclassified).
|
|
@@ -36,6 +37,16 @@ const VALID_KINDS = [
|
|
|
36
37
|
];
|
|
37
38
|
const VALID_STATUSES = ['open', 'promoted', 'discarded', 'merged'];
|
|
38
39
|
|
|
40
|
+
// The one column list every idea read returns. Four copies had to stay in step
|
|
41
|
+
// (capture / list / get / update-RETURNING); core_195 added two columns to each,
|
|
42
|
+
// so it is now a constant rather than four places to forget.
|
|
43
|
+
const IDEA_COLS = `id, title, body_md, kind, suggested_version, suggested_priority,
|
|
44
|
+
captured_at, captured_by, status, promoted_to_task_id, reviewed_at,
|
|
45
|
+
merged_into_idea_id, triage_note`;
|
|
46
|
+
|
|
47
|
+
// A discard reason / merge note is a sentence, not a document.
|
|
48
|
+
const TRIAGE_NOTE_MAX = 1000;
|
|
49
|
+
|
|
39
50
|
// -------------------------------------------------------------------------
|
|
40
51
|
// Skill-friction tagging (task V3.R70 / #289)
|
|
41
52
|
//
|
|
@@ -130,8 +141,7 @@ async function captureIdea({
|
|
|
130
141
|
`INSERT INTO idea_inbox
|
|
131
142
|
(title, body_md, kind, suggested_version, suggested_priority, captured_by, status)
|
|
132
143
|
VALUES ($1, $2, $3, $4, $5, $6, 'open')
|
|
133
|
-
RETURNING
|
|
134
|
-
captured_at, captured_by, status, promoted_to_task_id, reviewed_at`,
|
|
144
|
+
RETURNING ${IDEA_COLS}`,
|
|
135
145
|
[title, bodyMd, resolvedKind, suggestedVersion, suggestedPriority, capturedBy]
|
|
136
146
|
);
|
|
137
147
|
return rows[0];
|
|
@@ -161,8 +171,7 @@ async function listOpenIdeas({ kind = null, limit = 50, viewerRank = null } = {}
|
|
|
161
171
|
}
|
|
162
172
|
params.push(limit);
|
|
163
173
|
const { rows } = await pool.query(
|
|
164
|
-
`SELECT
|
|
165
|
-
captured_at, captured_by, status, promoted_to_task_id, reviewed_at
|
|
174
|
+
`SELECT ${IDEA_COLS}
|
|
166
175
|
FROM idea_inbox
|
|
167
176
|
WHERE ${where.join(' AND ')}
|
|
168
177
|
ORDER BY captured_at DESC
|
|
@@ -174,8 +183,7 @@ async function listOpenIdeas({ kind = null, limit = 50, viewerRank = null } = {}
|
|
|
174
183
|
|
|
175
184
|
async function getIdea(id) {
|
|
176
185
|
const { rows } = await pool.query(
|
|
177
|
-
`SELECT
|
|
178
|
-
captured_at, captured_by, status, promoted_to_task_id, reviewed_at
|
|
186
|
+
`SELECT ${IDEA_COLS}
|
|
179
187
|
FROM idea_inbox
|
|
180
188
|
WHERE id = $1`,
|
|
181
189
|
[id]
|
|
@@ -206,7 +214,40 @@ async function countOpenIdeas({ viewerRank = null } = {}) {
|
|
|
206
214
|
// Update / merge
|
|
207
215
|
// -------------------------------------------------------------------------
|
|
208
216
|
|
|
209
|
-
|
|
217
|
+
// merged_into_idea_id records WHICH idea a merge folded into — the thing
|
|
218
|
+
// `status='merged'` alone never said. It cannot reuse promoted_to_task_id
|
|
219
|
+
// (that column is REFERENCES tasks(id)), hence core_195. Guarded here rather
|
|
220
|
+
// than left to the constraints so the caller gets a structured 400/404/409
|
|
221
|
+
// instead of a violation 500.
|
|
222
|
+
//
|
|
223
|
+
// The invariant, also enforced by the core_195 CHECK: a target exists IFF the
|
|
224
|
+
// idea is merged. Without the status half, two ordinary PATCHes build the chain
|
|
225
|
+
// the no-chains guard exists to stop — set A's target with no status (A stays
|
|
226
|
+
// open), then merge C into A, whose status still reads 'open'. So the chain
|
|
227
|
+
// guard tests the TARGET COLUMN, not the target's status, and a target write
|
|
228
|
+
// must carry status='merged'.
|
|
229
|
+
async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at, merged_into_idea_id, triage_note } = {}) {
|
|
230
|
+
if (merged_into_idea_id !== undefined && merged_into_idea_id !== null) {
|
|
231
|
+
if (status !== 'merged') {
|
|
232
|
+
throw { code: 'MERGE_STATUS_REQUIRED', merged_into_idea_id };
|
|
233
|
+
}
|
|
234
|
+
if (String(merged_into_idea_id) === String(id)) {
|
|
235
|
+
throw { code: 'MERGE_SELF', merged_into_idea_id };
|
|
236
|
+
}
|
|
237
|
+
const target = await getIdea(merged_into_idea_id);
|
|
238
|
+
if (!target) throw { code: 'MERGE_TARGET_NOT_FOUND', merged_into_idea_id };
|
|
239
|
+
if (target.merged_into_idea_id != null) {
|
|
240
|
+
throw { code: 'MERGE_TARGET_MERGED', merged_into_idea_id, merged_into: target.merged_into_idea_id };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Clearing the link is the un-merge: it must move the idea off 'merged' too,
|
|
244
|
+
// or the row claims to be merged into nothing.
|
|
245
|
+
if (merged_into_idea_id === null && status === 'merged') {
|
|
246
|
+
throw { code: 'MERGE_STATUS_REQUIRED', merged_into_idea_id: null };
|
|
247
|
+
}
|
|
248
|
+
if (triage_note !== undefined && triage_note !== null && String(triage_note).length > TRIAGE_NOTE_MAX) {
|
|
249
|
+
throw { code: 'TRIAGE_NOTE_TOO_LONG', max: TRIAGE_NOTE_MAX };
|
|
250
|
+
}
|
|
210
251
|
const sets = [];
|
|
211
252
|
const params = [id];
|
|
212
253
|
if (status !== undefined) {
|
|
@@ -227,6 +268,14 @@ async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at }
|
|
|
227
268
|
params.push(reviewed_at);
|
|
228
269
|
sets.push(`reviewed_at = $${params.length}`);
|
|
229
270
|
}
|
|
271
|
+
if (merged_into_idea_id !== undefined) {
|
|
272
|
+
params.push(merged_into_idea_id);
|
|
273
|
+
sets.push(`merged_into_idea_id = $${params.length}`);
|
|
274
|
+
}
|
|
275
|
+
if (triage_note !== undefined) {
|
|
276
|
+
params.push(triage_note);
|
|
277
|
+
sets.push(`triage_note = $${params.length}`);
|
|
278
|
+
}
|
|
230
279
|
if (sets.length === 0) {
|
|
231
280
|
return getIdea(id);
|
|
232
281
|
}
|
|
@@ -234,8 +283,7 @@ async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at }
|
|
|
234
283
|
`UPDATE idea_inbox
|
|
235
284
|
SET ${sets.join(', ')}
|
|
236
285
|
WHERE id = $1
|
|
237
|
-
RETURNING
|
|
238
|
-
captured_at, captured_by, status, promoted_to_task_id, reviewed_at`,
|
|
286
|
+
RETURNING ${IDEA_COLS}`,
|
|
239
287
|
params
|
|
240
288
|
);
|
|
241
289
|
if (rows.length === 0) throw { code: 'IDEA_NOT_FOUND' };
|
|
@@ -439,6 +487,8 @@ module.exports = {
|
|
|
439
487
|
classify,
|
|
440
488
|
VALID_KINDS,
|
|
441
489
|
VALID_STATUSES,
|
|
490
|
+
// the triage-note cap, so the route validator declares it from one place
|
|
491
|
+
TRIAGE_NOTE_MAX,
|
|
442
492
|
// skill-friction tagging + rank visibility gate (V3.R70 / #289)
|
|
443
493
|
SKILL_FRICTION_TAG,
|
|
444
494
|
XENOS_RANK,
|