@bongos/core 1.18.3 → 1.18.19
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 +1576 -286
- 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 +4 -2
- 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 +6 -1
- 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/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 +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- 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 +8 -0
- 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/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/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/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/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/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/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 +51 -58
- 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 -27
- 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/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- 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 +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- 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 +124 -2
- package/scripts/migrate.sh +99 -14
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -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 +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +124 -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 +12 -0
- 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/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 +93 -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/migrate_root_resolution.mjs +124 -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/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 +132 -7
- 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 +80 -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 +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -86,6 +86,11 @@
|
|
|
86
86
|
// view, GET /goals/:id/requests), or null while loading / on error / not a
|
|
87
87
|
// manager. Feature-detected: an older server 404s and the panel just stays hidden.
|
|
88
88
|
pendingRequests: null,
|
|
89
|
+
// task 1002439 (hall redesign) — goal_id → task-readiness rollup
|
|
90
|
+
// {done,active,ready,blocked,backlog,total,nextReady}. Fed by loadRollups()
|
|
91
|
+
// from GET /tasks?version=…&include=deps. Empty on an older server (the
|
|
92
|
+
// include=deps enrichment 404s gracefully → the list falls back to plain cards).
|
|
93
|
+
rollups: new Map(),
|
|
89
94
|
};
|
|
90
95
|
|
|
91
96
|
async function getJSON(path) {
|
|
@@ -147,6 +152,52 @@
|
|
|
147
152
|
return m.display_name || (m.github_login ? `@${m.github_login}` : `builder #${m.builder_id}`);
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
// ---- task-readiness rollup (hall redesign task 1002439) --------------------
|
|
156
|
+
// Dependencies are a TASK property; a goal is only "blocked" in the derived
|
|
157
|
+
// sense that all its actionable tasks wait on upstream tasks (the Tasks page,
|
|
158
|
+
// not this one, owns the actual task→task graph). So the Goals page rolls each
|
|
159
|
+
// goal's tasks into these buckets:
|
|
160
|
+
// done — settled (shipped/confirmed/completed)
|
|
161
|
+
// active — a live claim ("in progress")
|
|
162
|
+
// ready — claimable now (deps met + promoted)
|
|
163
|
+
// blocked — backlog with ≥1 unshipped dependency
|
|
164
|
+
// backlog — backlog, no unmet deps (just not started/promoted)
|
|
165
|
+
function readinessOf(t) {
|
|
166
|
+
const s = t.status;
|
|
167
|
+
if (DONE_STATUSES.has(s)) return 'done';
|
|
168
|
+
if (CUT_STATUSES.has(s)) return 'cut';
|
|
169
|
+
if (s === 'active') return 'active';
|
|
170
|
+
if (s === 'ready') return 'ready';
|
|
171
|
+
if (s === 'backlog' && (t.blocked_by || []).some((d) => !d.satisfied)) return 'blocked';
|
|
172
|
+
return 'backlog';
|
|
173
|
+
}
|
|
174
|
+
const EMPTY_ROLLUP = { done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, total: 0, nextReady: null };
|
|
175
|
+
const rollupOf = (goalId) => state.rollups.get(String(goalId)) || EMPTY_ROLLUP;
|
|
176
|
+
|
|
177
|
+
// Fetch every task for the versions the goals span (one request each, with dep
|
|
178
|
+
// summaries) and fold into per-goal rollups. Failure-tolerant: an older server
|
|
179
|
+
// that doesn't honor include=deps just yields empty rollups and the list falls
|
|
180
|
+
// back to plain cards. Tasks come back priority-ordered, so the first `ready`
|
|
181
|
+
// one is the goal's best next pick.
|
|
182
|
+
async function loadRollups() {
|
|
183
|
+
const versions = [...new Set(state.goals.map((g) => g.version_id).filter(Boolean))];
|
|
184
|
+
const lists = await Promise.all(versions.map((v) =>
|
|
185
|
+
getJSON(`${API}/tasks?version=${encodeURIComponent(v)}&include=deps`)
|
|
186
|
+
.then((r) => r.tasks || []).catch(() => [])));
|
|
187
|
+
const roll = new Map();
|
|
188
|
+
for (const t of lists.flat()) {
|
|
189
|
+
if (t.goal_id == null) continue;
|
|
190
|
+
const key = String(t.goal_id);
|
|
191
|
+
let r = roll.get(key);
|
|
192
|
+
if (!r) { r = { done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, total: 0, nextReady: null }; roll.set(key, r); }
|
|
193
|
+
const bucket = readinessOf(t);
|
|
194
|
+
r[bucket] += 1;
|
|
195
|
+
if (bucket !== 'cut') r.total += 1;
|
|
196
|
+
if (bucket === 'ready' && !r.nextReady) r.nextReady = t;
|
|
197
|
+
}
|
|
198
|
+
state.rollups = roll;
|
|
199
|
+
}
|
|
200
|
+
|
|
150
201
|
/* ---------------- list view ---------------- */
|
|
151
202
|
|
|
152
203
|
function populateVersionFilter() {
|
|
@@ -171,6 +222,13 @@
|
|
|
171
222
|
// the server-side rank gate on POST /goals/reorder is the real enforcement.
|
|
172
223
|
const isArchon = () => (state.me?.builder?.rank || '').toLowerCase() === 'archon';
|
|
173
224
|
|
|
225
|
+
// task 1002164 — confirming a criterion is Metic+ (POST /done-when/:id/satisfy,
|
|
226
|
+
// ADR 0086 §6). Client-side this only decides whether to SHOW the control; the
|
|
227
|
+
// server's requireRank('metic','archon') is the real gate.
|
|
228
|
+
const RANK_ORDER = { archon: 4, metic: 3, thetes: 2, xenos: 1 };
|
|
229
|
+
const canConfirmCriteria = () =>
|
|
230
|
+
(RANK_ORDER[(state.me?.builder?.rank || '').toLowerCase()] ?? 0) >= RANK_ORDER.metic;
|
|
231
|
+
|
|
174
232
|
// task 1932 — the current filtered goal set, in display order. Shared by
|
|
175
233
|
// renderList (what to page) and the drag-reorder dragend (which must submit
|
|
176
234
|
// the FULL version order, not just the visible page).
|
|
@@ -189,67 +247,105 @@
|
|
|
189
247
|
(!mineOnly || iAmMember(g)));
|
|
190
248
|
}
|
|
191
249
|
|
|
250
|
+
// task 1002439 — a goal row's readiness rollup line (only the non-zero buckets).
|
|
251
|
+
const ROLL_DOTS = [
|
|
252
|
+
['ready', 'ready', 'var(--ok)'],
|
|
253
|
+
['active', 'in progress', 'var(--info)'],
|
|
254
|
+
['blocked', 'blocked', 'var(--terracotta)'],
|
|
255
|
+
['backlog', 'backlog', 'var(--ink-faint)'],
|
|
256
|
+
];
|
|
257
|
+
function goalRollLine(r) {
|
|
258
|
+
const parts = ROLL_DOTS.filter(([k]) => r[k] > 0)
|
|
259
|
+
.map(([k, label, c]) => `<span class="gr-d"><i style="background:${c}"></i><b>${r[k]}</b> ${label}</span>`);
|
|
260
|
+
return parts.length ? `<div class="gr-roll">${parts.join('')}</div>` : '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// task 1002439 — one goal, rendered as a rollup row: criteria ring · title +
|
|
264
|
+
// meta + readiness rollup · one next action (claim the next ready task, drill
|
|
265
|
+
// into what's blocking, or a review/achieved note).
|
|
266
|
+
function goalRowHtml(g) {
|
|
267
|
+
const d = state.details.get(String(g.id));
|
|
268
|
+
const crit = d ? (d.criteria || []) : [];
|
|
269
|
+
const sat = crit.filter((c) => c.satisfied).length;
|
|
270
|
+
const r = rollupOf(g.id);
|
|
271
|
+
const achieved = g.status === 'achieved';
|
|
272
|
+
const ringHtml = crit.length ? ring(sat, crit.length, achieved, 44)
|
|
273
|
+
: `<span class="gr-crit-none" aria-hidden="true">—</span>`;
|
|
274
|
+
const goalHref = `#/goal/${escapeHtml(String(g.id))}`;
|
|
275
|
+
let right;
|
|
276
|
+
if (achieved) {
|
|
277
|
+
right = `<span class="gr-met">✓ achieved</span>`;
|
|
278
|
+
} else if (crit.length && sat >= crit.length) {
|
|
279
|
+
right = `<span class="gr-met">Criteria met — awaiting review</span>`;
|
|
280
|
+
} else if (r.nextReady) {
|
|
281
|
+
right = `<div class="gr-next"><span class="gr-next__t">${escapeHtml(r.nextReady.title || 'Next task')}</span>`
|
|
282
|
+
+ `<a class="gr-claim" href="#/task/${escapeHtml(String(r.nextReady.id))}">Claim next</a></div>`;
|
|
283
|
+
} else if (r.blocked > 0) {
|
|
284
|
+
right = `<a class="gr-drill" href="${goalHref}">See what's blocking <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>`;
|
|
285
|
+
} else {
|
|
286
|
+
right = `<span class="gr-quiet">No tasks yet</span>`;
|
|
287
|
+
}
|
|
288
|
+
// NOTE: a rollup row nests real links (title → goal, Claim → task overlay,
|
|
289
|
+
// drill → goal), so the row itself is a <div>, not an <a> — nested anchors
|
|
290
|
+
// are invalid. Only the explicit links navigate.
|
|
291
|
+
return `<div class="gr-row" data-goal-id="${escapeHtml(String(g.id))}">
|
|
292
|
+
<div class="gr-ringcol">${ringHtml}</div>
|
|
293
|
+
<div class="gr-main">
|
|
294
|
+
<a class="gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
295
|
+
<div class="gr-meta">#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}</div>
|
|
296
|
+
${achieved ? '' : goalRollLine(r)}
|
|
297
|
+
</div>
|
|
298
|
+
<div class="gr-right">${right}</div>
|
|
299
|
+
</div>`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// task 1002439 — the Goals page, reorganised around momentum: a task-readiness
|
|
303
|
+
// summary, then goals grouped Active (has ready/in-progress work) · Blocked
|
|
304
|
+
// (only waiting-on-upstream tasks) · Achieved. "Blocked" here is a DERIVED
|
|
305
|
+
// rollup of task readiness — the actual task→task dependency graph lives on the
|
|
306
|
+
// Tasks page (owner call, this task). Filters (version/status/membership) still
|
|
307
|
+
// apply; pagination + Archon drag-reorder are retired for the grouped view.
|
|
192
308
|
function renderList() {
|
|
193
309
|
const grid = $('#goal-grid');
|
|
194
310
|
if (!grid) return;
|
|
195
|
-
const ver = $('#f-goal-version')?.value || '';
|
|
196
|
-
// task 1932 — filter via the shared helper (drag-reorder reuses it).
|
|
197
311
|
const rows = filteredGoals();
|
|
198
|
-
const reorderable = !!ver && isArchon();
|
|
199
|
-
|
|
200
312
|
const count = $('#goals-count');
|
|
201
313
|
if (count) count.textContent = `${rows.length} goal${rows.length === 1 ? '' : 's'}`;
|
|
314
|
+
const pagerMount = $('#goals-pager-mount');
|
|
315
|
+
if (pagerMount) pagerMount.hidden = true;
|
|
202
316
|
|
|
203
317
|
if (!rows.length) {
|
|
204
318
|
grid.innerHTML = emptyStateHtml('No goals match these filters.');
|
|
205
|
-
pagers.list.page = 1;
|
|
206
|
-
const emptyPager = $('#goals-pager');
|
|
207
|
-
if (emptyPager) emptyPager.hidden = true;
|
|
208
319
|
return;
|
|
209
320
|
}
|
|
210
321
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
<span class="goal-card__title">${escapeHtml(g.title || 'Untitled goal')}</span>
|
|
239
|
-
</div>
|
|
240
|
-
<div class="goal-card__meta">
|
|
241
|
-
<span class="goal-card__id" title="Goal id — use this to reference the goal (e.g. 'goal ${escapeHtml(String(g.id))}'). Distinct from the priority position on the left.">#${escapeHtml(String(g.id))}</span>
|
|
242
|
-
<span>${escapeHtml(g.version_id || '')}</span>
|
|
243
|
-
${statusBadge(g.status)}
|
|
244
|
-
${blockedPill}
|
|
245
|
-
${joined ? '<span class="role-pill">joined</span>' : ''}
|
|
246
|
-
</div>
|
|
247
|
-
<div class="goal-card__foot">
|
|
248
|
-
<span><strong>${members.length}</strong> member${members.length === 1 ? '' : 's'}</span>
|
|
249
|
-
${leads.length ? `<span>lead: ${escapeHtml(leads.map(memberName).join(', '))}</span>` : ''}
|
|
250
|
-
</div>
|
|
251
|
-
</a>`;
|
|
252
|
-
}).join('');
|
|
322
|
+
const isBlocked = (g) => {
|
|
323
|
+
const r = rollupOf(g.id);
|
|
324
|
+
return g.status === 'open' && r.ready === 0 && r.active === 0 && r.blocked > 0;
|
|
325
|
+
};
|
|
326
|
+
const achieved = rows.filter((g) => g.status === 'achieved');
|
|
327
|
+
const archived = rows.filter((g) => g.status === 'archived');
|
|
328
|
+
const openRows = rows.filter((g) => g.status !== 'achieved' && g.status !== 'archived');
|
|
329
|
+
const blocked = openRows.filter(isBlocked);
|
|
330
|
+
const active = openRows.filter((g) => !isBlocked(g));
|
|
331
|
+
|
|
332
|
+
let R = 0, A = 0, B = 0;
|
|
333
|
+
openRows.forEach((g) => { const r = rollupOf(g.id); R += r.ready; A += r.active; B += r.blocked; });
|
|
334
|
+
const summary = `<div class="gr-summary">
|
|
335
|
+
<span class="gr-sm"><span class="gr-sm__dot" style="background:var(--ok)"></span><b>${R}</b> ready to claim</span>
|
|
336
|
+
<span class="gr-sm"><span class="gr-sm__dot" style="background:var(--info)"></span><b>${A}</b> in progress</span>
|
|
337
|
+
<span class="gr-sm"><span class="gr-sm__dot" style="background:var(--terracotta)"></span><b>${B}</b> blocked</span>
|
|
338
|
+
</div>`;
|
|
339
|
+
|
|
340
|
+
const sec = (label, n) => `<div class="gr-sec"><b>${label}</b><span class="gr-sec__n">${n}</span></div>`;
|
|
341
|
+
const listOf = (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
|
|
342
|
+
const pill = (g) => `<a class="gr-pill" href="#/goal/${escapeHtml(String(g.id))}"><span class="gr-pill__ok">✓</span><b>${escapeHtml(g.title || 'Untitled')}</b><span class="gr-pill__id">#${escapeHtml(String(g.id))}</span></a>`;
|
|
343
|
+
|
|
344
|
+
grid.innerHTML = summary
|
|
345
|
+
+ (active.length ? sec('Active', active.length) + listOf(active) : '')
|
|
346
|
+
+ (blocked.length ? sec('Blocked', blocked.length) + listOf(blocked) : '')
|
|
347
|
+
+ (achieved.length ? sec('Achieved', achieved.length) + `<div class="gr-pills">${achieved.map(pill).join('')}</div>` : '')
|
|
348
|
+
+ (archived.length ? sec('Archived', archived.length) + `<div class="gr-pills gr-pills--muted">${archived.map(pill).join('')}</div>` : '');
|
|
253
349
|
}
|
|
254
350
|
|
|
255
351
|
// ---- drag-to-reorder (task 1760, Archon-only, one version at a time) ------
|
|
@@ -312,37 +408,273 @@
|
|
|
312
408
|
|
|
313
409
|
/* ---------------- detail view ---------------- */
|
|
314
410
|
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
411
|
+
// ===================================================================
|
|
412
|
+
// task 1002425 — Tasks-board parity for the goal's "To do" pane.
|
|
413
|
+
//
|
|
414
|
+
// The goal-detail To-do list now mirrors the Tasks board's Ready queue so a
|
|
415
|
+
// goal's tasks have the SAME FUNCTIONALITY as the board: rich claimable cards,
|
|
416
|
+
// a shared filter bar (search / sort / kind / discipline / newcomer / run-mode),
|
|
417
|
+
// multi-select with bulk claim, and a per-card copy-prompt — all opening the
|
|
418
|
+
// same shared task-detail overlay. The helpers below are PORTED from the board
|
|
419
|
+
// (work.js) so the two surfaces render + behave identically. (They're deliberate
|
|
420
|
+
// duplication for now; a follow-up idea to extract a shared task-card/board
|
|
421
|
+
// component — the drift-proof endgame — is filed at ship.)
|
|
422
|
+
//
|
|
423
|
+
// Scoped to one goal, so: no Version/Goal filter, no In-Progress/Backlog tab
|
|
424
|
+
// split. Every open (todo) task renders as a card whose action adapts to its
|
|
425
|
+
// status — ready → Claim (+ select + copy); mine-active → Release;
|
|
426
|
+
// someone else's active → a muted "claimed · @who"; backlog/blocked → open-only.
|
|
427
|
+
// ===================================================================
|
|
428
|
+
|
|
429
|
+
// Board-identical numeric helpers (work.js:214-240).
|
|
430
|
+
function creditsPerMinute(t) {
|
|
431
|
+
const c = Number(t.credits_reward); const m = Number(t.est_minutes);
|
|
432
|
+
if (!c || !m || m <= 0) return -Infinity;
|
|
433
|
+
return c / m;
|
|
434
|
+
}
|
|
435
|
+
function priorityRank(p) {
|
|
436
|
+
if (!p) return 99;
|
|
437
|
+
const m = /^P(\d+)/i.exec(String(p));
|
|
438
|
+
return m ? Number(m[1]) : 99;
|
|
439
|
+
}
|
|
440
|
+
// Two tasks can't run in parallel if their touches[] overlap (exact path or a
|
|
441
|
+
// directory-prefix either way); empty touches never conflict. Powers the
|
|
442
|
+
// filter bar's "Run mode" control (work.js:230-239).
|
|
443
|
+
function touchesConflict(aList, bList) {
|
|
444
|
+
const norm = (p) => String(p).replace(/\/+$/, '');
|
|
445
|
+
for (const a of (Array.isArray(aList) ? aList : [])) {
|
|
446
|
+
const na = norm(a);
|
|
447
|
+
for (const b of (Array.isArray(bList) ? bList : [])) {
|
|
448
|
+
const nb = norm(b);
|
|
449
|
+
if (na === nb || na.startsWith(nb + '/') || nb.startsWith(na + '/')) return true;
|
|
330
450
|
}
|
|
331
|
-
const who = c && (c.login || c.name) ? ` · @${escapeHtml(c.login || c.name)}` : '';
|
|
332
|
-
return `<td class="ledger__action"><span class="ledger__claimed" title="Claimed by another builder">claimed${who}</span></td>`;
|
|
333
451
|
}
|
|
334
|
-
return
|
|
452
|
+
return false;
|
|
335
453
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
454
|
+
// Per-task rank floor tag (work.js:247-254).
|
|
455
|
+
function rankTagHtml(requiresRank) {
|
|
456
|
+
const rr = (requiresRank || 'xenos').toLowerCase();
|
|
457
|
+
if (rr === 'xenos') {
|
|
458
|
+
return '<span class="task__tag task__tag--rank-open" title="No rank floor — any builder can claim this task">Any rank</span>';
|
|
459
|
+
}
|
|
460
|
+
const label = rr === 'archon' ? 'Archon' : rr.charAt(0).toUpperCase() + rr.slice(1) + '+';
|
|
461
|
+
return `<span class="task__tag task__tag--rank" title="Minimum rank required to claim this task">${escapeHtml(label)}</span>`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Free-text search over the goal's todo list (work.js:261-274, minus the
|
|
465
|
+
// goal-title match — a single goal makes that redundant).
|
|
466
|
+
function todoSearchQuery() {
|
|
467
|
+
const el = $('#f-goal-search');
|
|
468
|
+
return el ? el.value.trim().toLowerCase() : '';
|
|
469
|
+
}
|
|
470
|
+
function matchesTodoSearch(t, q) {
|
|
471
|
+
if (!q) return true;
|
|
472
|
+
const needle = q.replace(/^#/, '');
|
|
473
|
+
if (!needle) return true;
|
|
474
|
+
if (String(t.id).includes(needle)) return true;
|
|
475
|
+
if ((t.title || '').toLowerCase().includes(needle)) return true;
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Copy-prompt (work.js:454-503). deHashRefs stops a bare #NNN autolinking to
|
|
480
|
+
// GitHub in a pasted session; the prompt points the builder at the hall task URL.
|
|
481
|
+
function deHashRefs(text) {
|
|
482
|
+
return String(text == null ? '' : text).replace(/#(\d{1,5})\b/g, 'task $1');
|
|
483
|
+
}
|
|
484
|
+
function buildPromptForTask(t) {
|
|
485
|
+
const desc = deHashRefs((t.description || '').trim());
|
|
486
|
+
const hallBase = location.hostname.startsWith('builders.') ? location.origin : `${location.origin}/builders`;
|
|
487
|
+
const hallUrl = `${hallBase}#/task/${t.id}`;
|
|
488
|
+
return [
|
|
489
|
+
`I am working on GDS task ${t.id} (${t.title}) — ${hallUrl}`,
|
|
490
|
+
`Read the task description, follow its acceptance criteria, stay strictly within touches[], and ship via /builder-ship when done.`,
|
|
491
|
+
``,
|
|
492
|
+
`Task description:`,
|
|
493
|
+
desc || '(no description)',
|
|
494
|
+
].join('\n');
|
|
495
|
+
}
|
|
496
|
+
async function copyPromptForTask(taskId, btn) {
|
|
497
|
+
const t = state.work.todo.find((x) => String(x.id) === String(taskId));
|
|
498
|
+
if (!t) return;
|
|
499
|
+
const prompt = buildPromptForTask(t);
|
|
500
|
+
const icon = btn.querySelector('.task__copy-icon');
|
|
501
|
+
try {
|
|
502
|
+
await navigator.clipboard.writeText(prompt);
|
|
503
|
+
if (icon) icon.textContent = '✓';
|
|
504
|
+
setTimeout(() => { if (icon) icon.textContent = '📋'; }, 1600);
|
|
505
|
+
toast(`Prompt for #${taskId} copied to your clipboard.`);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
try {
|
|
508
|
+
const ta = document.createElement('textarea');
|
|
509
|
+
ta.value = prompt; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
|
510
|
+
document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
|
|
511
|
+
toast(`Prompt for #${taskId} copied.`);
|
|
512
|
+
} catch (_) { toast('Could not copy to the clipboard. Try again.'); }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// The claimable card (ported from work.js renderClaimable, work.js:345-394).
|
|
517
|
+
// Uses the SAME .task markup + classes as the board, so work.css (linked from
|
|
518
|
+
// goals.html) styles it identically. The lead cell + actions adapt to status.
|
|
519
|
+
function goalTaskCardHtml(t) {
|
|
520
|
+
const id = String(t.id);
|
|
521
|
+
const cpm = creditsPerMinute(t);
|
|
522
|
+
const cpmText = Number.isFinite(cpm) ? `${cpm.toFixed(2)} ${curLabel()}/min` : `— ${curLabel()}/min`;
|
|
523
|
+
const minutesText = t.est_minutes ? `${t.est_minutes}m` : '—';
|
|
524
|
+
const discipline = t.discipline || 'unclassified';
|
|
525
|
+
const disciplineTag = discipline !== 'unclassified' ? `<span class="task__tag">${escapeHtml(discipline)}</span>` : '';
|
|
526
|
+
const newcomerTag = t.newcomer_friendly ? '<span class="task__tag task__tag--newcomer">newcomer</span>' : '';
|
|
527
|
+
const rankTag = rankTagHtml(t.requires_rank);
|
|
528
|
+
const touches = Array.isArray(t.touches) ? t.touches : [];
|
|
529
|
+
const touchesChip = touches.length
|
|
530
|
+
? `<span class="task__touches" title="${escapeHtml(touches.join('\n'))}">✎ ${touches.length} file${touches.length === 1 ? '' : 's'}</span>`
|
|
531
|
+
: '';
|
|
532
|
+
const claim = state.claimsByTask.get(id);
|
|
533
|
+
const isReady = t.status === 'ready' && !!state.me;
|
|
534
|
+
const mineActive = t.status === 'active' && claim && sameId(claim.builder_id, selfId());
|
|
535
|
+
const selected = goalSel.claim.has(id);
|
|
536
|
+
// Lead: a select checkbox on ready cards (drives bulk claim); otherwise a
|
|
537
|
+
// status tag so active/backlog cards read at a glance.
|
|
538
|
+
const lead = isReady
|
|
539
|
+
? `<input type="checkbox" class="task__select" data-task-id="${escapeHtml(id)}" aria-label="Select task ${escapeHtml(id)} for bulk claim"${selected ? ' checked' : ''}>`
|
|
540
|
+
: `<span class="task__tag" title="Task status">${escapeHtml(t.status || '')}</span>`;
|
|
541
|
+
// Actions: ready → Claim + copy; mine-active → Release; other's active →
|
|
542
|
+
// "claimed · @who"; anything else → nothing (open via the ⤢ button / card).
|
|
543
|
+
let actions = '';
|
|
544
|
+
if (isReady) {
|
|
545
|
+
actions = `<button class="task__claim" type="button" data-claim-task="${escapeHtml(id)}" aria-label="Claim task ${escapeHtml(id)}">Claim</button>`
|
|
546
|
+
+ `<button class="task__copy" type="button" data-copy-task="${escapeHtml(id)}" title="Copy the task prompt to your clipboard" aria-label="Copy prompt for task ${escapeHtml(id)}"><span class="task__copy-icon" aria-hidden="true">📋</span></button>`;
|
|
547
|
+
} else if (mineActive) {
|
|
548
|
+
actions = `<button class="wip__release" type="button" data-release-claim="${escapeHtml(String(claim.claim_id))}" data-release-task="${escapeHtml(id)}" aria-label="Release task ${escapeHtml(id)}">Release</button>`;
|
|
549
|
+
} else if (t.status === 'active' && claim) {
|
|
550
|
+
const who = (claim.login || claim.name) ? ` · @${escapeHtml(claim.login || claim.name)}` : '';
|
|
551
|
+
actions = `<span class="ledger__claimed" title="Claimed by another builder">claimed${who}</span>`;
|
|
552
|
+
}
|
|
553
|
+
return `
|
|
554
|
+
<div class="task${selected ? ' task--selected' : ''}${isReady ? '' : ' task--readonly'}" data-task-id="${escapeHtml(id)}">
|
|
555
|
+
<div class="task__id">
|
|
556
|
+
${lead}
|
|
557
|
+
<span>#${escapeHtml(id)}</span>
|
|
558
|
+
<button class="task__open" type="button" data-task-id="${escapeHtml(id)}" title="Open task details" aria-label="Open details for task ${escapeHtml(id)}">⤢</button>
|
|
559
|
+
</div>
|
|
560
|
+
<div class="task__body">
|
|
561
|
+
<p class="task__title">${escapeHtml(t.title || '')}</p>
|
|
562
|
+
<div class="task__meta">
|
|
563
|
+
<span class="task__tag">${escapeHtml(t.version_id || '?')}</span>
|
|
564
|
+
<span class="task__tag">${escapeHtml(t.kind || 'unclassified')}</span>
|
|
565
|
+
${disciplineTag}
|
|
566
|
+
${newcomerTag}
|
|
567
|
+
${rankTag}
|
|
568
|
+
${t.priority ? `<span class="task__tag">${escapeHtml(t.priority)}</span>` : ''}
|
|
569
|
+
<span>${minutesText}</span>
|
|
570
|
+
<span>${cpmText}</span>
|
|
571
|
+
${touchesChip}
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="task__credits">
|
|
575
|
+
<span class="task__credits-num">${fmtNum(window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward)}</span>
|
|
576
|
+
<span class="task__credits-label" title="${window.OTBBranding?.rewardModeNote?.() ?? ''}">${window.OTBBranding?.taskCreditLabel?.(t) ?? (curLabel() + ' Est.')}</span>
|
|
577
|
+
</div>
|
|
578
|
+
<div class="task__actions">${actions}</div>
|
|
579
|
+
</div>`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ---- multi-select + bulk claim (work.js:583-828, claim-only subset) --------
|
|
583
|
+
// Selection holds String(task_id) of READY cards the viewer ticked. Bulk
|
|
584
|
+
// release + copy parallel/sequential prompts stay board-only for now (a goal is
|
|
585
|
+
// a narrow slice where the headline value is "grab several of this goal's ready
|
|
586
|
+
// tasks at once"); see the goals.html toolbar comment.
|
|
587
|
+
const goalSel = { claim: new Set() };
|
|
588
|
+
function goalSelTotal() { return goalSel.claim.size; }
|
|
589
|
+
function toggleGoalCardSelect(card, forceChecked) {
|
|
590
|
+
const cb = card.querySelector('.task__select');
|
|
591
|
+
if (!cb) return;
|
|
592
|
+
const next = forceChecked != null ? forceChecked : !cb.checked;
|
|
593
|
+
cb.checked = next;
|
|
594
|
+
const id = String(cb.dataset.taskId);
|
|
595
|
+
if (next) goalSel.claim.add(id); else goalSel.claim.delete(id);
|
|
596
|
+
card.classList.toggle('task--selected', next);
|
|
597
|
+
renderGoalBulkToolbar();
|
|
598
|
+
}
|
|
599
|
+
function clearGoalSelection() {
|
|
600
|
+
goalSel.claim.clear();
|
|
601
|
+
document.querySelectorAll('#goal-todo-list .task--selected').forEach((c) => c.classList.remove('task--selected'));
|
|
602
|
+
document.querySelectorAll('#goal-todo-list .task__select:checked').forEach((cb) => { cb.checked = false; });
|
|
603
|
+
renderGoalBulkToolbar();
|
|
604
|
+
}
|
|
605
|
+
// Drop any selected id that's fallen out from under the selection (claimed by
|
|
606
|
+
// someone else, shipped, or we navigated to another goal) so a stale tick never
|
|
607
|
+
// outlives its card. Called after every task reload (work.js:606-619).
|
|
608
|
+
function pruneGoalSelection() {
|
|
609
|
+
const readyIds = new Set(state.work.todo.filter((t) => t.status === 'ready').map((t) => String(t.id)));
|
|
610
|
+
for (const id of Array.from(goalSel.claim)) if (!readyIds.has(id)) goalSel.claim.delete(id);
|
|
611
|
+
}
|
|
612
|
+
function renderGoalBulkToolbar() {
|
|
613
|
+
const bar = $('#goal-bulk-toolbar');
|
|
614
|
+
if (!bar) return;
|
|
615
|
+
if (goalSelTotal() === 0) { bar.hidden = true; return; }
|
|
616
|
+
bar.hidden = false;
|
|
617
|
+
const countEl = $('#goal-bulk-count');
|
|
618
|
+
if (countEl) countEl.textContent = `${goalSel.claim.size} to claim`;
|
|
619
|
+
const claimBtn = $('#goal-bulk-claim-btn');
|
|
620
|
+
if (claimBtn) {
|
|
621
|
+
claimBtn.disabled = goalSel.claim.size === 0;
|
|
622
|
+
claimBtn.textContent = goalSel.claim.size > 1 ? `Bulk claim (${goalSel.claim.size})` : 'Bulk claim';
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// A single batch-item failure reason → a readable fragment (work.js:545-562).
|
|
626
|
+
function friendlyBatchReason(item) {
|
|
627
|
+
switch (item && item.reason) {
|
|
628
|
+
case 'TASK_NOT_FOUND': return 'no longer exists';
|
|
629
|
+
case 'TASK_NOT_READY': return `no longer ready${item.currentStatus ? ` (${item.currentStatus})` : ''}`;
|
|
630
|
+
case 'ALREADY_CLAIMED': return 'already claimed';
|
|
631
|
+
case 'TOUCHES_CONFLICT': return `overlaps ${item.conflictTaskTitle || 'an active claim'}`;
|
|
632
|
+
case 'BATCH_TOUCHES_CONFLICT': return `overlaps task #${item.conflictsWithTaskId} in this selection`;
|
|
633
|
+
case 'DEPS_NOT_SHIPPED': return 'waits on unshipped dependencies';
|
|
634
|
+
case 'rank_too_low_for_task':
|
|
635
|
+
case 'XENOS_RESTRICTED':
|
|
636
|
+
case 'INSUFFICIENT_RANK': return 'rank too low';
|
|
637
|
+
case 'BUILDER_INACTIVE': return 'account inactive';
|
|
638
|
+
default: return (item && item.reason) || 'not claimable';
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Bulk claim the selected ready cards (work.js doBulkClaim, claims/batch is
|
|
642
|
+
// all-or-nothing: a 409 batch_failed means nothing was claimed). Reloads the
|
|
643
|
+
// goal's tasks either way so the cards reflect fresh truth.
|
|
644
|
+
async function doGoalBulkClaim() {
|
|
645
|
+
const ids = [...goalSel.claim].map(Number);
|
|
646
|
+
if (!ids.length) return;
|
|
647
|
+
const btn = $('#goal-bulk-claim-btn');
|
|
648
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
|
|
649
|
+
try {
|
|
650
|
+
if (ids.length === 1) {
|
|
651
|
+
await postJSON(`${API}/claims`, { task_id: ids[0], bind_session: false });
|
|
652
|
+
toast(`You claimed #${ids[0]}. Open a terminal to start work.`);
|
|
653
|
+
} else {
|
|
654
|
+
await postJSON(`${API}/claims/batch`, { task_ids: ids, bind_session: false });
|
|
655
|
+
toast(`Claimed ${ids.length} tasks. Open a terminal to start work.`);
|
|
656
|
+
}
|
|
657
|
+
goalSel.claim.clear();
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
660
|
+
// claims/batch is all-or-nothing: a 409 batch_failed means NOTHING was
|
|
661
|
+
// claimed. Read failures from the ADR 0116 envelope (error.details.failures)
|
|
662
|
+
// or the flat legacy shape, via the same normalizers friendlyClaimError uses.
|
|
663
|
+
const det = errDetails(err.data);
|
|
664
|
+
if (err.status === 409 && errCode(err.data) === 'batch_failed') {
|
|
665
|
+
const failures = Array.isArray(det.failures) ? det.failures
|
|
666
|
+
: (Array.isArray(err.data && err.data.failures) ? err.data.failures : []);
|
|
667
|
+
const lines = failures.map((f) => `#${f.task_id} (${friendlyBatchReason(f)})`);
|
|
668
|
+
toast(lines.length
|
|
669
|
+
? `The batch could not be claimed — ${lines.join('; ')}.`
|
|
670
|
+
: 'The batch could not be claimed. It was left untouched (all-or-nothing).');
|
|
671
|
+
} else {
|
|
672
|
+
toast(friendlyClaimError(err.status, err.data));
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
if (state.current != null) await loadGoalTasks(state.current);
|
|
676
|
+
else renderGoalBulkToolbar();
|
|
677
|
+
}
|
|
346
678
|
}
|
|
347
679
|
function taskRowsDone(tasks) {
|
|
348
680
|
return tasks.map((t) => `
|
|
@@ -459,46 +791,101 @@
|
|
|
459
791
|
}
|
|
460
792
|
|
|
461
793
|
function renderWork() {
|
|
462
|
-
|
|
794
|
+
renderTodoCards();
|
|
795
|
+
// ---- Completed → unchanged ledger table (the board's Completed tab is also
|
|
796
|
+
// a plain list, so the goal keeps its sortable table here). ----
|
|
463
797
|
const doneBody = document.querySelector('#goal-done tbody');
|
|
464
|
-
const todo = sortWorkRows(state.work.todo, state.workSort.todo);
|
|
465
|
-
const done = sortWorkRows(state.work.done, state.workSort.done);
|
|
466
|
-
// task 1956 → kit (2171): paginate each table through the shared pager.
|
|
467
|
-
const todoSlice = paginate(todo, pagers.todo);
|
|
468
|
-
const doneSlice = paginate(done, pagers.done);
|
|
469
|
-
if (todoBody) {
|
|
470
|
-
todoBody.innerHTML = todo.length
|
|
471
|
-
? taskRowsTodo(todoSlice)
|
|
472
|
-
: `<tr><td colspan="6" class="ledger__empty">${emptyStateHtml(`Nothing queued under this goal. Members can author bounded tasks here${state.me ? '' : ' once signed in'}.`)}</td></tr>`;
|
|
473
|
-
}
|
|
474
798
|
if (doneBody) {
|
|
799
|
+
const done = sortWorkRows(state.work.done, state.workSort.done);
|
|
800
|
+
const doneSlice = paginate(done, pagers.done);
|
|
475
801
|
doneBody.innerHTML = done.length
|
|
476
802
|
? taskRowsDone(doneSlice)
|
|
477
803
|
: `<tr><td colspan="5" class="ledger__empty">${emptyStateHtml('Nothing completed yet.')}</td></tr>`;
|
|
478
804
|
}
|
|
479
805
|
workTabs?.setCount('todo', state.work.todo.length);
|
|
480
806
|
workTabs?.setCount('done', state.work.done.length);
|
|
481
|
-
reflectSortHeaders('todo');
|
|
482
807
|
reflectSortHeaders('done');
|
|
808
|
+
renderGoalBulkToolbar();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// task 1002425 — the To-do list as Tasks-board cards: filter → run-mode →
|
|
812
|
+
// sort → paginate → render, mirroring work.js renderClaimable (276-395). The
|
|
813
|
+
// sort control lives on the filter bar (replacing the old column headers).
|
|
814
|
+
function renderTodoCards() {
|
|
815
|
+
const listEl = $('#goal-todo-list');
|
|
816
|
+
if (!listEl) return;
|
|
817
|
+
const sQuery = todoSearchQuery();
|
|
818
|
+
const kFilter = $('#f-goal-kind')?.value || '';
|
|
819
|
+
const dFilter = $('#f-goal-discipline')?.value || '';
|
|
820
|
+
const nFilter = $('#f-goal-newcomer')?.value || '';
|
|
821
|
+
const sortBy = $('#f-goal-sort')?.value || 'cpm';
|
|
822
|
+
let list = state.work.todo.filter((t) => {
|
|
823
|
+
if (!matchesTodoSearch(t, sQuery)) return false;
|
|
824
|
+
if (kFilter && (t.kind || 'unclassified') !== kFilter) return false;
|
|
825
|
+
if (dFilter && (t.discipline || 'unclassified') !== dFilter) return false;
|
|
826
|
+
if (nFilter === 'yes' && !t.newcomer_friendly) return false;
|
|
827
|
+
return true;
|
|
828
|
+
});
|
|
829
|
+
// Run-mode filter: parallel-safe (touches[] disjoint from the rest of the
|
|
830
|
+
// visible set) vs. file-conflicting. O(n²) over touches — fine at goal scale.
|
|
831
|
+
const fitFilter = $('#f-goal-fit')?.value || '';
|
|
832
|
+
if (fitFilter === 'parallel' || fitFilter === 'conflict') {
|
|
833
|
+
const base = list;
|
|
834
|
+
list = base.filter((t) => {
|
|
835
|
+
const conflicts = base.some((o) => o !== t && touchesConflict(t.touches, o.touches));
|
|
836
|
+
return fitFilter === 'parallel' ? !conflicts : conflicts;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (sortBy === 'cpm') list = list.slice().sort((a, b) => creditsPerMinute(b) - creditsPerMinute(a));
|
|
840
|
+
else if (sortBy === 'credits') list = list.slice().sort((a, b) => creditVal(b) - creditVal(a));
|
|
841
|
+
else if (sortBy === 'minutes') list = list.slice().sort((a, b) => (Number(a.est_minutes) || Infinity) - (Number(b.est_minutes) || Infinity));
|
|
842
|
+
else if (sortBy === 'priority') list = list.slice().sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority));
|
|
843
|
+
else if (sortBy === 'id') list = list.slice().sort((a, b) => Number(a.id) - Number(b.id));
|
|
844
|
+
else if (sortBy === 'status') list = list.slice().sort((a, b) => String(a.status).localeCompare(String(b.status)));
|
|
845
|
+
|
|
846
|
+
const countEl = $('#goal-todo-count');
|
|
847
|
+
if (countEl) countEl.textContent = state.work.todo.length ? `${list.length} of ${state.work.todo.length} shown` : '';
|
|
848
|
+
if (!list.length) {
|
|
849
|
+
listEl.innerHTML = emptyStateHtml(state.work.todo.length
|
|
850
|
+
? 'No tasks match these filters. Try widening them.'
|
|
851
|
+
: `Nothing queued under this goal. Members can author bounded tasks here${state.me ? '' : ' once signed in'}.`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const shown = paginate(list, pagers.todo);
|
|
855
|
+
listEl.innerHTML = shown.map(goalTaskCardHtml).join('');
|
|
483
856
|
}
|
|
484
857
|
|
|
485
858
|
// task 1828 — claim a ready task straight from a goal's Work list. Mirrors the
|
|
486
859
|
// Work Board's doClaim (POST /claims, bind_session:false) + its typed-error
|
|
487
860
|
// messages; on success the goal's tasks reload so the row updates in place.
|
|
488
861
|
const pendingClaims = new Set();
|
|
862
|
+
// task 1002425 — the API emits the ADR 0116 envelope `{ error: { code, message,
|
|
863
|
+
// details } }` and the client passes the raw body through (throwOnError:false),
|
|
864
|
+
// so the typed code is at data.error.code, not data.error. These normalize BOTH
|
|
865
|
+
// that envelope and the flat legacy `{ error: 'CODE', ...fields }` shape — without
|
|
866
|
+
// them the switch below fell through to the generic default for every claim error.
|
|
867
|
+
function errCode(data) {
|
|
868
|
+
const e = data && data.error;
|
|
869
|
+
return (e && typeof e === 'object') ? e.code : e;
|
|
870
|
+
}
|
|
871
|
+
function errDetails(data) {
|
|
872
|
+
const e = data && data.error;
|
|
873
|
+
return (e && typeof e === 'object' && e.details) ? e.details : (data || {});
|
|
874
|
+
}
|
|
489
875
|
function friendlyClaimError(status, data) {
|
|
490
|
-
|
|
876
|
+
const det = errDetails(data);
|
|
877
|
+
switch (errCode(data)) {
|
|
491
878
|
case 'ALREADY_CLAIMED': return 'Someone else claimed that task first. Reloading.';
|
|
492
879
|
case 'TASK_NOT_READY': return 'That task is no longer ready to be claimed.';
|
|
493
880
|
case 'TOUCHES_CONFLICT': {
|
|
494
|
-
const titles = Array.isArray(
|
|
881
|
+
const titles = Array.isArray(det.conflicts) ? det.conflicts.map((c) => c.title).filter(Boolean) : [];
|
|
495
882
|
return titles.length ? `That task overlaps an active claim: ${titles.join('; ')}.` : 'That task overlaps another active claim.';
|
|
496
883
|
}
|
|
497
884
|
case 'DEPS_NOT_SHIPPED': return 'That task waits on unshipped dependencies.';
|
|
498
885
|
case 'rank_too_low_for_task':
|
|
499
886
|
case 'XENOS_RESTRICTED':
|
|
500
887
|
case 'INSUFFICIENT_RANK': return 'Your rank is not yet high enough for that task.';
|
|
501
|
-
case 'newcomer_must_ship_first': return
|
|
888
|
+
case 'newcomer_must_ship_first': return det.what_to_do || 'Ship your current claim before opening another.';
|
|
502
889
|
case 'NOT_YOUR_CLAIM': return 'That claim is not yours to release.';
|
|
503
890
|
case 'ALREADY_RESOLVED': return 'That claim was already resolved. Reloading.';
|
|
504
891
|
case 'BUILDER_INACTIVE': return 'Your account is inactive. Contact an Archon.';
|
|
@@ -620,9 +1007,20 @@
|
|
|
620
1007
|
|
|
621
1008
|
/* ----- Completion (band column + #/goal/N/completion full view) ----- */
|
|
622
1009
|
|
|
1010
|
+
// task 1002164 — a criterion has three states, not two: confirmed (satisfied),
|
|
1011
|
+
// met-but-awaiting-confirmation (pending_review, derived server-side), and open.
|
|
1012
|
+
const isPending = (c) => !c.satisfied && !!c.pending_review;
|
|
1013
|
+
|
|
623
1014
|
function completionSummaryHtml(g, crit, sat, ringSize) {
|
|
1015
|
+
const pending = crit.filter(isPending).length;
|
|
1016
|
+
// task 1002164 — an open goal used to punt confirmation to the /goal-review
|
|
1017
|
+
// CLI skill here; confirming now happens on this page, so the line reports
|
|
1018
|
+
// what is actually actionable instead of naming a command.
|
|
1019
|
+
const openLine = pending
|
|
1020
|
+
? `${fmtNum(pending)} criteri${pending === 1 ? 'on is' : 'a are'} met and ready to confirm.`
|
|
1021
|
+
: 'Open — a criterion becomes confirmable once every task linked to it has shipped.';
|
|
624
1022
|
const line = g.status === 'achieved' ? 'Goal achieved — every criterion confirmed.'
|
|
625
|
-
: (g.status === 'archived' ? 'Archived.' :
|
|
1023
|
+
: (g.status === 'archived' ? 'Archived.' : openLine);
|
|
626
1024
|
return `<div style="display:flex;align-items:center;gap:16px">
|
|
627
1025
|
${ring(sat, crit.length, g.status === 'achieved', ringSize)}
|
|
628
1026
|
<div>
|
|
@@ -635,7 +1033,7 @@
|
|
|
635
1033
|
// is the point of clicking into the dedicated completion view. The capped band
|
|
636
1034
|
// (withDetail=false) instead reveals detail via a per-criterion <details> expand
|
|
637
1035
|
// (BV1.R79) so the full text is one click away, not hover-only.
|
|
638
|
-
function critListHtml(list, withDetail) {
|
|
1036
|
+
function critListHtml(list, withDetail, canConfirm) {
|
|
639
1037
|
if (!list.length) return emptyStateHtml('No done-when criteria yet.');
|
|
640
1038
|
return `<ul class="crit-list${withDetail ? ' crit-list--full' : ''}">${list.map((c) => {
|
|
641
1039
|
const parts = window.OTB.splitCriterion ? window.OTB.splitCriterion(c.criterion_md) : { label: c.criterion_md, detail: '' };
|
|
@@ -644,7 +1042,15 @@
|
|
|
644
1042
|
: (parts.detail
|
|
645
1043
|
? `<details class="crit-d"><summary class="crit__text">${escapeHtml(parts.label || '')}</summary><div class="crit__detail">${escapeHtml(parts.detail)}</div></details>`
|
|
646
1044
|
: `<span class="crit__text">${escapeHtml(parts.label || '')}</span>`);
|
|
647
|
-
|
|
1045
|
+
const pending = isPending(c);
|
|
1046
|
+
// A Xenos sees the state but not the control — the server enforces either way.
|
|
1047
|
+
const action = !pending ? ''
|
|
1048
|
+
: (canConfirm
|
|
1049
|
+
? `<button type="button" class="crit__confirm" data-confirm-crit="${escapeHtml(String(c.id))}"
|
|
1050
|
+
title="Every task linked to this criterion has shipped — confirm it as satisfied">Confirm</button>`
|
|
1051
|
+
: '<span class="crit__flag" title="Every task linked to this criterion has shipped — a Metic can confirm it">Ready</span>');
|
|
1052
|
+
const cls = c.satisfied ? ' crit--done' : (pending ? ' crit--pending' : '');
|
|
1053
|
+
return `<li class="crit${cls}"><span class="crit__mark">${c.satisfied ? '✓' : (pending ? '!' : '')}</span>${body}<span class="crit__act">${action}</span></li>`;
|
|
648
1054
|
}).join('')}</ul>`;
|
|
649
1055
|
}
|
|
650
1056
|
function renderCompletion() {
|
|
@@ -653,16 +1059,48 @@
|
|
|
653
1059
|
const g = d.goal;
|
|
654
1060
|
const crit = d.criteria || [];
|
|
655
1061
|
const sat = crit.filter((c) => c.satisfied).length;
|
|
1062
|
+
// task 1002164 — an archived/achieved goal has nothing left to confirm.
|
|
1063
|
+
const canConfirm = g.status === 'open' && canConfirmCriteria();
|
|
656
1064
|
const band = $('#goal-completion-body');
|
|
657
|
-
if (band) band.innerHTML = completionSummaryHtml(g, crit, sat, 56) + critListHtml(crit.slice(0, CRITERIA_CAP), false);
|
|
1065
|
+
if (band) band.innerHTML = completionSummaryHtml(g, crit, sat, 56) + critListHtml(crit.slice(0, CRITERIA_CAP), false, canConfirm);
|
|
658
1066
|
renderViewAllFoot('#goal-completion-foot', crit.length, CRITERIA_CAP, 'completion', 'criteria');
|
|
659
1067
|
setCount('#goal-completion-count', crit.length);
|
|
660
1068
|
const full = $('#goal-completion-full');
|
|
661
|
-
if (full) full.innerHTML = completionSummaryHtml(g, crit, sat, 64) + critListHtml(crit, true);
|
|
1069
|
+
if (full) full.innerHTML = completionSummaryHtml(g, crit, sat, 64) + critListHtml(crit, true, canConfirm);
|
|
662
1070
|
const back = $('#goal-completion-back');
|
|
663
1071
|
if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
|
|
664
1072
|
}
|
|
665
1073
|
|
|
1074
|
+
// task 1002164 — confirm a pending-review criterion straight from the hall
|
|
1075
|
+
// (POST /done-when/:id/satisfy, Metic+). This is the in-hall replacement for
|
|
1076
|
+
// the /goal-review LLM skill: confirming the goal's LAST open criterion flips
|
|
1077
|
+
// it open→achieved server-side, which the forced detail reload then reflects.
|
|
1078
|
+
const pendingConfirms = new Set();
|
|
1079
|
+
async function confirmCriterion(critId, btn) {
|
|
1080
|
+
if (pendingConfirms.has(critId)) return;
|
|
1081
|
+
pendingConfirms.add(critId);
|
|
1082
|
+
const label = btn ? btn.textContent : '';
|
|
1083
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Confirming…'; }
|
|
1084
|
+
try {
|
|
1085
|
+
const res = await postJSON(`${API}/done-when/${encodeURIComponent(critId)}/satisfy`, {});
|
|
1086
|
+
toast(res && res.goal_achieved ? 'Criterion confirmed — that was the last one, so this goal is achieved.' : 'Criterion confirmed.');
|
|
1087
|
+
if (state.current != null) {
|
|
1088
|
+
await loadDetail(state.current, { force: true });
|
|
1089
|
+
renderDetail(state.current);
|
|
1090
|
+
}
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
1093
|
+
const code = err && err.data && err.data.error;
|
|
1094
|
+
const reason = (code && (code.code || code)) === 'rank_forbidden'
|
|
1095
|
+
? 'Confirming a criterion needs Metic rank or higher.'
|
|
1096
|
+
: `Could not confirm that criterion (${(err && err.status) || 'error'}).`;
|
|
1097
|
+
toast(reason);
|
|
1098
|
+
if (btn) { btn.disabled = false; btn.textContent = label; }
|
|
1099
|
+
} finally {
|
|
1100
|
+
pendingConfirms.delete(critId);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
666
1104
|
/* ----- Members (band column + #/goal/N/members full view) ----- */
|
|
667
1105
|
|
|
668
1106
|
function memberAvatar(m) {
|
|
@@ -891,6 +1329,7 @@
|
|
|
891
1329
|
.sort((a, b) => new Date(b.shipped_at || 0) - new Date(a.shipped_at || 0)),
|
|
892
1330
|
};
|
|
893
1331
|
state.workLoaded = true; // task 1828 — member stats can now compute
|
|
1332
|
+
pruneGoalSelection(); // task 1002425 — drop ticks for cards that vanished (or a goal switch)
|
|
894
1333
|
renderWork();
|
|
895
1334
|
renderMembers(); // refresh per-member goal stats now that tasks landed
|
|
896
1335
|
// Prefer the server-side detector when it exists (task 1736); any failure
|
|
@@ -918,9 +1357,10 @@
|
|
|
918
1357
|
if (err && err.message === 'redirecting to auth') return;
|
|
919
1358
|
console.error('[goals] tasks load failed', err);
|
|
920
1359
|
if (!sameId(state.current, goalId)) return;
|
|
921
|
-
|
|
1360
|
+
// task 1002425 — To do is now a card list (#goal-todo-list), not a table.
|
|
1361
|
+
const todoList = $('#goal-todo-list');
|
|
922
1362
|
const doneBody = document.querySelector('#goal-done tbody');
|
|
923
|
-
if (
|
|
1363
|
+
if (todoList) todoList.innerHTML = emptyStateHtml('Tasks could not be loaded.');
|
|
924
1364
|
if (doneBody) doneBody.innerHTML = '<tr><td colspan="5" class="ledger__empty">Tasks could not be loaded.</td></tr>';
|
|
925
1365
|
state.conflictPairs = null;
|
|
926
1366
|
state.conflictSource = null;
|
|
@@ -1212,13 +1652,18 @@
|
|
|
1212
1652
|
if (el) el.hidden = true;
|
|
1213
1653
|
});
|
|
1214
1654
|
['#goal-conflicts-card', '#goal-conflicts-view-card'].forEach((sel) => $(sel)?.classList.remove('goal-conflicts-card--live'));
|
|
1215
|
-
reflectSortHeaders('todo');
|
|
1216
1655
|
reflectSortHeaders('done');
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1656
|
+
// task 1002425 — To do is a card list now; Completed stays a table. Show a
|
|
1657
|
+
// placeholder in the card list + skeleton rows in the Completed table while
|
|
1658
|
+
// the goal's tasks load, and reset the To-do count + hide the bulk toolbar.
|
|
1659
|
+
const todoList = $('#goal-todo-list');
|
|
1219
1660
|
const doneBody = document.querySelector('#goal-done tbody');
|
|
1220
|
-
if (
|
|
1661
|
+
if (todoList) todoList.innerHTML = '<div class="profile__placeholder">…</div>';
|
|
1221
1662
|
if (doneBody) doneBody.innerHTML = skeletonRows(3, 5);
|
|
1663
|
+
const todoCount = $('#goal-todo-count');
|
|
1664
|
+
if (todoCount) todoCount.textContent = '';
|
|
1665
|
+
const bulkBar = $('#goal-bulk-toolbar');
|
|
1666
|
+
if (bulkBar) bulkBar.hidden = true;
|
|
1222
1667
|
const conflicts = $('#goal-conflicts');
|
|
1223
1668
|
if (conflicts) conflicts.innerHTML = '<span class="kit-skel"></span>';
|
|
1224
1669
|
const conflictsFull = $('#goal-conflicts-full');
|
|
@@ -1331,7 +1776,12 @@
|
|
|
1331
1776
|
populateVersionFilter();
|
|
1332
1777
|
// Prefetch details (ring + members on the cards). Sequentially-parallel,
|
|
1333
1778
|
// failure-tolerant: a card without detail still renders.
|
|
1334
|
-
|
|
1779
|
+
// task 1002439 — also fold in per-goal task-readiness rollups (one request
|
|
1780
|
+
// per version, not per goal). Runs alongside the detail prefetch.
|
|
1781
|
+
await Promise.allSettled([
|
|
1782
|
+
...state.goals.map((g) => loadDetail(g.id)),
|
|
1783
|
+
loadRollups(),
|
|
1784
|
+
]);
|
|
1335
1785
|
await route();
|
|
1336
1786
|
} catch (err) {
|
|
1337
1787
|
if (err && err.message === 'redirecting to auth') return;
|
|
@@ -1353,7 +1803,7 @@
|
|
|
1353
1803
|
mount: $('#goals-filters'),
|
|
1354
1804
|
primary: [
|
|
1355
1805
|
{ id: 'f-goal-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
|
|
1356
|
-
{ id: 'f-goal-status', label: 'Status',
|
|
1806
|
+
{ id: 'f-goal-status', label: 'Status', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
|
|
1357
1807
|
{ id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
|
|
1358
1808
|
],
|
|
1359
1809
|
onChange: onListFilterChange,
|
|
@@ -1388,6 +1838,41 @@
|
|
|
1388
1838
|
{ id: 'todo', label: 'To do', panel: 'goal-todo-pane' },
|
|
1389
1839
|
{ id: 'done', label: 'Completed', panel: 'goal-done-pane' },
|
|
1390
1840
|
],
|
|
1841
|
+
// task 1002425 — clear the card selection when leaving To do (matches the
|
|
1842
|
+
// board, work.js:1185) so the bulk toolbar can't linger over Completed.
|
|
1843
|
+
onChange: () => { if (goalSelTotal()) clearGoalSelection(); },
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
// task 1002425 — the To-do filter bar, scoped to this goal: Search + Sort
|
|
1847
|
+
// primary, Kind / Discipline / Newcomer / Run-mode behind the "Filters"
|
|
1848
|
+
// disclosure (the same set the board offers, minus Version/Goal which are
|
|
1849
|
+
// redundant inside a goal). url:false is load-bearing — this page owns the
|
|
1850
|
+
// #/goal/N hash route, so filter state must NOT ride the URL.
|
|
1851
|
+
makeFilterBar({
|
|
1852
|
+
mount: $('#goal-work-filters'),
|
|
1853
|
+
search: { id: 'f-goal-search', label: 'Search', placeholder: 'task # or title…' },
|
|
1854
|
+
primary: [
|
|
1855
|
+
{ id: 'f-goal-sort', label: 'Sort', options: [
|
|
1856
|
+
{ value: 'cpm', label: `${curLabel()} / minute (high → low)` },
|
|
1857
|
+
{ value: 'credits', label: `${curLabel()} (high → low)` },
|
|
1858
|
+
{ value: 'minutes', label: 'minutes (low → high)' },
|
|
1859
|
+
{ value: 'priority', label: 'priority (P0 first)' },
|
|
1860
|
+
{ value: 'id', label: 'task number (low → high)' },
|
|
1861
|
+
{ value: 'status', label: 'status' },
|
|
1862
|
+
] },
|
|
1863
|
+
],
|
|
1864
|
+
overflow: [
|
|
1865
|
+
{ id: 'f-goal-kind', label: 'Kind', options: [{ value: '', label: '— any —' },
|
|
1866
|
+
'feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture',
|
|
1867
|
+
'cleanup', 'decision', 'blocker-resolution', 'unclassified'] },
|
|
1868
|
+
{ id: 'f-goal-discipline', label: 'Discipline', options: [{ value: '', label: '— any —' }, 'engineer', 'artist', 'ideator', 'ui', 'unclassified'] },
|
|
1869
|
+
{ id: 'f-goal-newcomer', label: 'Newcomer', options: [{ value: '', label: '— any —' }, { value: 'yes', label: 'newcomer-friendly only' }] },
|
|
1870
|
+
{ id: 'f-goal-fit', label: 'Run mode', options: [{ value: '', label: '— any —' },
|
|
1871
|
+
{ value: 'parallel', label: 'parallel-safe only' },
|
|
1872
|
+
{ value: 'conflict', label: 'has file conflicts' }] },
|
|
1873
|
+
],
|
|
1874
|
+
url: false,
|
|
1875
|
+
onChange: () => { pagers.todo.page = 1; renderTodoCards(); renderGoalBulkToolbar(); },
|
|
1391
1876
|
});
|
|
1392
1877
|
// task 1828 — click / keyboard sort on the Work-table column headers.
|
|
1393
1878
|
Object.entries(WORK_SORT_HEADERS).forEach(([table, cols]) => {
|
|
@@ -1400,16 +1885,43 @@
|
|
|
1400
1885
|
});
|
|
1401
1886
|
});
|
|
1402
1887
|
});
|
|
1403
|
-
// task
|
|
1404
|
-
|
|
1405
|
-
|
|
1888
|
+
// task 1002425 — the Completed table keeps its row-opens-overlay behaviour.
|
|
1889
|
+
$('#goal-done')?.addEventListener('click', (e) => {
|
|
1890
|
+
const row = e.target.closest('.ledger__row--task');
|
|
1891
|
+
if (row && row.dataset.taskId && window.OTBTaskDetail) window.OTBTaskDetail.open(row.dataset.taskId);
|
|
1892
|
+
});
|
|
1893
|
+
// task 1002425 — the To-do card list: multi-select + inline Claim/Release +
|
|
1894
|
+
// copy-prompt + open, mirroring the board's task-list wiring (work.js:1245-1272).
|
|
1895
|
+
$('#goal-todo-list')?.addEventListener('click', (e) => {
|
|
1896
|
+
const selectCb = e.target.closest('.task__select');
|
|
1897
|
+
if (selectCb) { const card = selectCb.closest('.task'); if (card) toggleGoalCardSelect(card, selectCb.checked); return; }
|
|
1898
|
+
const claimBtn = e.target.closest('[data-claim-task]');
|
|
1899
|
+
if (claimBtn) { e.stopPropagation(); doGoalClaim(claimBtn.dataset.claimTask, claimBtn); return; }
|
|
1900
|
+
const copyBtn = e.target.closest('[data-copy-task]');
|
|
1901
|
+
if (copyBtn) { copyPromptForTask(copyBtn.dataset.copyTask, copyBtn); return; }
|
|
1902
|
+
const releaseBtn = e.target.closest('[data-release-claim]');
|
|
1903
|
+
if (releaseBtn) { e.stopPropagation(); doGoalRelease(releaseBtn.dataset.releaseClaim, releaseBtn.dataset.releaseTask, releaseBtn); return; }
|
|
1904
|
+
const openBtn = e.target.closest('.task__open');
|
|
1905
|
+
if (openBtn && openBtn.dataset.taskId && window.OTBTaskDetail) { window.OTBTaskDetail.open(openBtn.dataset.taskId); return; }
|
|
1906
|
+
// Click elsewhere: a ready card toggles its selection; a read-only (active /
|
|
1907
|
+
// backlog) card opens the detail overlay (board affordance, work.js:1268-1271).
|
|
1908
|
+
const card = e.target.closest('.task');
|
|
1909
|
+
if (!card || !card.dataset.taskId) return;
|
|
1910
|
+
if (card.classList.contains('task--readonly')) {
|
|
1911
|
+
if (window.OTBTaskDetail) window.OTBTaskDetail.open(card.dataset.taskId);
|
|
1912
|
+
} else {
|
|
1913
|
+
toggleGoalCardSelect(card);
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
// task 1002425 — the goal's bulk-action toolbar.
|
|
1917
|
+
$('#goal-bulk-claim-btn')?.addEventListener('click', doGoalBulkClaim);
|
|
1918
|
+
$('#goal-bulk-clear-btn')?.addEventListener('click', clearGoalSelection);
|
|
1919
|
+
// task 1002164 — Confirm on a pending-review criterion, in both the capped
|
|
1920
|
+
// band column and the dedicated #/goal/N/completion view.
|
|
1921
|
+
['#goal-completion-body', '#goal-completion-full'].forEach((sel) => {
|
|
1406
1922
|
$(sel)?.addEventListener('click', (e) => {
|
|
1407
|
-
const
|
|
1408
|
-
if (
|
|
1409
|
-
const releaseBtn = e.target.closest('[data-release-claim]');
|
|
1410
|
-
if (releaseBtn) { e.stopPropagation(); doGoalRelease(releaseBtn.dataset.releaseClaim, releaseBtn.dataset.releaseTask, releaseBtn); return; }
|
|
1411
|
-
const row = e.target.closest('.ledger__row--task');
|
|
1412
|
-
if (row && row.dataset.taskId && window.OTBTaskDetail) window.OTBTaskDetail.open(row.dataset.taskId);
|
|
1923
|
+
const btn = e.target.closest('[data-confirm-crit]');
|
|
1924
|
+
if (btn) { e.preventDefault(); confirmCriterion(btn.dataset.confirmCrit, btn); }
|
|
1413
1925
|
});
|
|
1414
1926
|
});
|
|
1415
1927
|
// task 1953 — Approve/Deny on the goal's own pending-requests panel.
|