@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
package/modules/lifecycle/db.js
CHANGED
|
@@ -34,6 +34,15 @@ const { pool, withTx, insertAuditLog, LIVE_RANK_LADDER } = api;
|
|
|
34
34
|
// so the module reaches the `reward`/`onboarding` ports + emits kernel events
|
|
35
35
|
// (task.shipped, builder.box-reconcile-needed) without importing core.
|
|
36
36
|
const seams = api;
|
|
37
|
+
// Sibling read-model helper (task 1002164) — the shared pending-review predicate.
|
|
38
|
+
// done-when.js imports only src/module-api, so this stays acyclic.
|
|
39
|
+
const doneWhen = require('./done-when.js');
|
|
40
|
+
// The catch-all goal title, shared with the orphan advisory (task 1002539) so the
|
|
41
|
+
// SQL below and the detector that names the same goal cannot drift apart.
|
|
42
|
+
const { generalGoalTitle } = require('./goal-advisory.js');
|
|
43
|
+
// BV1.R106 (ADR 0151): the permission-expressed QUEUE axis of claim eligibility.
|
|
44
|
+
// Kept deliberately separate from the per-task requires_rank floor below.
|
|
45
|
+
const { claimEligibilityAllows } = require('./claim-eligibility.js');
|
|
37
46
|
// Pure kernel-adjacent matchers via the doorway (never the core file).
|
|
38
47
|
const { matchProtected } = api.permissionPathCheck;
|
|
39
48
|
const { touchesOverlap } = api.pathMatch;
|
|
@@ -164,11 +173,46 @@ const GENERAL_QUEUE_RANKS = new Set(['thetes', 'metic', 'archon']);
|
|
|
164
173
|
// Pure + exported so tests/rank_gate.mjs can exercise it without a DB; claimTask
|
|
165
174
|
// + claimTasksBatch both call it (composed with rankAllowsTask).
|
|
166
175
|
|
|
176
|
+
// BV1.R106 (ADR 0151): this rank rule is no longer the primary gate — the claim
|
|
177
|
+
// paths decide the queue axis on the PERMISSIONS `task.claim.any` /
|
|
178
|
+
// `task.claim.newcomer` (claim-eligibility.js) and keep this as the fallback for a
|
|
179
|
+
// builder whose role row the run-once compat backfill never created. It stays
|
|
180
|
+
// exported: tests/rank_gate.mjs pins it, and it is the oracle the parity test in
|
|
181
|
+
// tests/governance_claim_eligibility.mjs compares the permission predicate against.
|
|
182
|
+
|
|
167
183
|
function xenosClaimAllowed(rank, newcomerFriendly) {
|
|
168
184
|
if (GENERAL_QUEUE_RANKS.has(rank)) return true;
|
|
169
185
|
return newcomerFriendly === true;
|
|
170
186
|
}
|
|
171
187
|
|
|
188
|
+
// claimPermissionsFor — the builder's effective permission set for the claim
|
|
189
|
+
// decision, read through the `governance` kernel PORT (never by importing the
|
|
190
|
+
// module — the one-way rule, ADR 0083). Returns a Set, or NULL when authority
|
|
191
|
+
// cannot be ESTABLISHED, which claimEligibilityAllows reads as "fall back to the
|
|
192
|
+
// incumbent rank rule":
|
|
193
|
+
// • the governance module is not mounted (no port), or the resolver throws;
|
|
194
|
+
// • the builder resolves to ZERO permissions — i.e. they hold no role row.
|
|
195
|
+
// governance_002's backfill is run-once and nothing yet re-assigns a rank-role
|
|
196
|
+
// when a builder is created or re-ranked, so this is a live gap, not a theory.
|
|
197
|
+
// Every seeded rank-role grants at least twelve permissions, so an empty set never
|
|
198
|
+
// means "a real role that grants nothing" today.
|
|
199
|
+
//
|
|
200
|
+
// Called BEFORE the claim transaction opens: authority is a per-request read (ADR
|
|
201
|
+
// 0016 — resolved server-side, uncached), and taking a second pool connection while
|
|
202
|
+
// a tx holds FOR UPDATE locks is a needless deadlock/exhaustion hazard.
|
|
203
|
+
async function claimPermissionsFor(builderId) {
|
|
204
|
+
const governance = seams.resolveOptional('governance');
|
|
205
|
+
if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
|
|
206
|
+
try {
|
|
207
|
+
const keys = await governance.resolveBuilderPermissions(builderId);
|
|
208
|
+
if (!keys || keys.length === 0) return null;
|
|
209
|
+
return new Set(keys);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error('[gds] claim eligibility: permission resolve failed, falling back to the rank rule', err);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
172
216
|
// V3.R82 (#301): the INVERSE of newcomer_friendly. Some tasks raise the floor
|
|
173
217
|
// instead of opening the door — `requires_rank` is a per-task MINIMUM rank
|
|
174
218
|
// that a builder must meet to claim. Default 'xenos' means "no extra gate"
|
|
@@ -318,6 +362,24 @@ function disambiguateWorktreeName(desired, taken) {
|
|
|
318
362
|
//
|
|
319
363
|
// Returns the updated builder row, or throws { code:'BUILDER_NOT_FOUND' } /
|
|
320
364
|
// { code:'RANK_UNCHANGED' }.
|
|
365
|
+
// syncGovernanceRankRole — keep governance_builder_roles in step with builders.rank
|
|
366
|
+
// (BV1.R95b, ADR 0151). This module's rank writer backs the automatic xenos→thetes
|
|
367
|
+
// graduation, which had the same hole as the Archon-driven path: the rank moved and
|
|
368
|
+
// the role did not, so a freshly-graduated Thetes kept the xenos role.
|
|
369
|
+
//
|
|
370
|
+
// Resolved through the `governance.roleSync` kernel port (never a sibling require —
|
|
371
|
+
// ADR 0083) and run on the CALLER's transaction client, so the rotation commits
|
|
372
|
+
// atomically with the ship + graduation. A missing port (governance disabled) skips:
|
|
373
|
+
// no role ⇒ zero permissions, fail-closed.
|
|
374
|
+
//
|
|
375
|
+
// TWINNED with src/bongos/db.js (the boundary-forced _applyRankChangeTx duplication,
|
|
376
|
+
// ADR 0091 §2) — keep the two in sync.
|
|
377
|
+
async function syncGovernanceRankRole(client, builderId, rank) {
|
|
378
|
+
const roleSync = seams.resolveOptional('governance.roleSync');
|
|
379
|
+
if (!roleSync || typeof roleSync.syncRankRole !== 'function') return null;
|
|
380
|
+
return roleSync.syncRankRole(client, { builderId, rank });
|
|
381
|
+
}
|
|
382
|
+
|
|
321
383
|
// _applyRankChangeTx — the in-transaction CORE of a rank change, shared by the
|
|
322
384
|
// Archon-driven setBuilderRank and the automatic xenos→thetes graduation in the
|
|
323
385
|
// confirm paths (#692 / ADR 0034). Runs inside the CALLER's transaction/client
|
|
@@ -326,7 +388,6 @@ function disambiguateWorktreeName(desired, taken) {
|
|
|
326
388
|
// — on demotion — revokes live sessions so a stale token can't replay old
|
|
327
389
|
// authority. Returns the audit shape (incl. the builder's `status`, so the
|
|
328
390
|
// caller can fire the post-commit box reconcile on its OWN connection).
|
|
329
|
-
|
|
330
391
|
async function _applyRankChangeTx(client, { builderId, newRank, actorBuilderId, reasonNote = null, enforceSingleRungPromotion = false }) {
|
|
331
392
|
const { rows } = await client.query(
|
|
332
393
|
`SELECT id, rank, status FROM builders WHERE id = $1 FOR UPDATE`,
|
|
@@ -351,6 +412,12 @@ async function _applyRankChangeTx(client, { builderId, newRank, actorBuilderId,
|
|
|
351
412
|
[builderId, newRank]
|
|
352
413
|
);
|
|
353
414
|
|
|
415
|
+
// BV1.R95b — rotate the seeded rank-role IN THIS TRANSACTION (removing the old one,
|
|
416
|
+
// not merely adding the new). Not try/catch-wrapped: a failure must roll the rank
|
|
417
|
+
// change back rather than leave rank and role disagreeing. See the twin in
|
|
418
|
+
// src/bongos/db.js.
|
|
419
|
+
await syncGovernanceRankRole(client, builderId, newRank);
|
|
420
|
+
|
|
354
421
|
// V3.R28 / #249 — ceremony delta. Promotion (moving UP the ladder = lower
|
|
355
422
|
// index in RANKS) earns +5; demotion is audit-only (0).
|
|
356
423
|
const oldIdx = RANKS.indexOf(oldRank);
|
|
@@ -623,15 +690,20 @@ async function getGoalDependencies(goalId, deps = {}) {
|
|
|
623
690
|
// by the owner's WITHIN-version priority (sort_order, task 1760), falling back
|
|
624
691
|
// to created_at/id so a fresh instance and prod agree on row order pre-reorder.
|
|
625
692
|
|
|
626
|
-
|
|
693
|
+
// `limit` is optional and defaults to unbounded — the goal board reads every goal.
|
|
694
|
+
// A caller that only needs the first N (the orphan advisory wants 5 candidates)
|
|
695
|
+
// passes it so Postgres does the truncating instead of shipping the whole version's
|
|
696
|
+
// goals to be discarded in JS (task 1002539).
|
|
697
|
+
async function listGoals({ versionId = null, status = null, limit = null } = {}, deps = {}) {
|
|
627
698
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
628
699
|
const { rows } = await activePool.query(
|
|
629
700
|
`SELECT ${GOAL_COLS}
|
|
630
701
|
FROM goals
|
|
631
702
|
WHERE ($1::text IS NULL OR version_id = $1)
|
|
632
703
|
AND ($2::text IS NULL OR status = $2)
|
|
633
|
-
ORDER BY version_id, sort_order, created_at, id
|
|
634
|
-
|
|
704
|
+
ORDER BY version_id, sort_order, created_at, id
|
|
705
|
+
LIMIT $3::int`,
|
|
706
|
+
[versionId, status, limit]
|
|
635
707
|
);
|
|
636
708
|
return rows;
|
|
637
709
|
}
|
|
@@ -779,14 +851,44 @@ async function listGoalMembers(goalId, deps = {}) {
|
|
|
779
851
|
|
|
780
852
|
async function listCriteriaForGoal(goalId, deps = {}) {
|
|
781
853
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
854
|
+
// task 1002164: carry the linked-task counts so the caller can derive the
|
|
855
|
+
// "met — pending review" state the hall renders. The counts are aggregated
|
|
856
|
+
// ONCE, over only this goal's criteria (the `counts` CTE narrows on the `crit`
|
|
857
|
+
// CTE before grouping), rather than per-row correlated subqueries — one pass
|
|
858
|
+
// over the idx_task_criteria_criterion range instead of 2N lookups. The
|
|
859
|
+
// criterion columns are still listed explicitly so the projection this read
|
|
860
|
+
// already feeds (the version-criteria view, the task-create suggester) is
|
|
861
|
+
// unchanged apart from the two additive count columns.
|
|
782
862
|
const { rows } = await activePool.query(
|
|
783
|
-
`
|
|
784
|
-
|
|
785
|
-
|
|
863
|
+
`WITH crit AS (
|
|
864
|
+
SELECT id, version_id, criterion_id, criterion_md, satisfied, sort_order, goal_id
|
|
865
|
+
FROM done_when_criteria
|
|
866
|
+
WHERE goal_id = $1
|
|
867
|
+
),
|
|
868
|
+
counts AS (
|
|
869
|
+
SELECT tc.criterion_id,
|
|
870
|
+
count(*)::int AS linked_tasks,
|
|
871
|
+
count(*) FILTER (WHERE t.status <> 'shipped')::int AS unshipped_tasks
|
|
872
|
+
FROM task_criteria tc
|
|
873
|
+
JOIN tasks t ON t.id = tc.task_id
|
|
874
|
+
WHERE tc.criterion_id IN (SELECT id FROM crit)
|
|
875
|
+
GROUP BY tc.criterion_id
|
|
876
|
+
)
|
|
877
|
+
SELECT c.id, c.version_id, c.criterion_id, c.criterion_md, c.satisfied,
|
|
878
|
+
c.sort_order, c.goal_id,
|
|
879
|
+
COALESCE(n.linked_tasks, 0) AS linked_tasks,
|
|
880
|
+
COALESCE(n.unshipped_tasks, 0) AS unshipped_tasks
|
|
881
|
+
FROM crit c
|
|
882
|
+
LEFT JOIN counts n ON n.criterion_id = c.id
|
|
786
883
|
ORDER BY sort_order, id`,
|
|
787
884
|
[goalId]
|
|
788
885
|
);
|
|
789
|
-
return rows
|
|
886
|
+
return rows.map((r) => ({
|
|
887
|
+
...r,
|
|
888
|
+
pending_review: doneWhen.isPendingReview({
|
|
889
|
+
satisfied: r.satisfied, linked: r.linked_tasks, unshipped: r.unshipped_tasks,
|
|
890
|
+
}),
|
|
891
|
+
}));
|
|
790
892
|
}
|
|
791
893
|
|
|
792
894
|
// isGoalMember — is this builder a member of this goal? The membership predicate
|
|
@@ -1179,14 +1281,15 @@ async function listPendingRequestsForGoal(goalId, deps = {}) {
|
|
|
1179
1281
|
// holds for every create-vector. Returns the goal id, or null when the version has
|
|
1180
1282
|
// no general goal (then the caller leaves goal_id NULL rather than failing a
|
|
1181
1283
|
// create). Honors the #538 deps.pool test seam (NODE_ENV==='test') so it is
|
|
1182
|
-
// unit-testable DB-free. The title
|
|
1183
|
-
//
|
|
1284
|
+
// unit-testable DB-free. The title mirrors migration 160's insert exactly (a spaced
|
|
1285
|
+
// em-dash); it is built by ../goal-advisory.generalGoalTitle so this SQL and the
|
|
1286
|
+
// advisory that detects the same goal can never disagree (task 1002539).
|
|
1184
1287
|
async function generalGoalIdForVersion(versionId, deps = {}) {
|
|
1185
1288
|
if (versionId == null) return null;
|
|
1186
1289
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
1187
1290
|
const { rows } = await activePool.query(
|
|
1188
|
-
|
|
1189
|
-
[versionId]
|
|
1291
|
+
'SELECT id FROM goals WHERE version_id = $1 AND title = $2 LIMIT 1',
|
|
1292
|
+
[versionId, generalGoalTitle(versionId)]
|
|
1190
1293
|
);
|
|
1191
1294
|
return rows[0] ? rows[0].id : null;
|
|
1192
1295
|
}
|
|
@@ -1575,22 +1678,28 @@ async function updateTaskModuleKey(id, moduleKey) { return updateTaskColumn(id,
|
|
|
1575
1678
|
|
|
1576
1679
|
// task 1957: credits_awarded is the ACTUAL drachmae paid out for completing a
|
|
1577
1680
|
// task — the sum of the TASK-REWARD credit_log deltas booked against it: the
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1681
|
+
// per-task credit (= credits_reward × kind-multiplier), the idea-promotion
|
|
1682
|
+
// bonus, and the net-negative (simplification) bonus, across all builders. The
|
|
1683
|
+
// reason allowlist is deliberate: achievement unlocks ('achievement.*') also
|
|
1684
|
+
// carry the shipped task's id but are GAMIFICATION, not a reward for the task,
|
|
1685
|
+
// so they must NOT inflate the payout (session.token_reward and rank_change
|
|
1686
|
+
// carry task_id=NULL and are excluded already).
|
|
1687
|
+
//
|
|
1688
|
+
// ADR 0120 "pay on land": the per-task credit reason is now 'task.shipped'
|
|
1689
|
+
// (booked at the confirmed→shipped flip) instead of 'task.confirmed' (booked at
|
|
1690
|
+
// confirm). BOTH strings are allow-listed: historical tasks confirmed under the
|
|
1691
|
+
// old rule carry 'task.confirmed' rows, new landings carry 'task.shipped', and
|
|
1692
|
+
// the one-time clawback reverses the old 'task.confirmed' deltas (a negative
|
|
1693
|
+
// row nets them to 0) — so summing both is correct across the transition. The
|
|
1694
|
+
// CASE still short-circuits to 0 for unsettled tasks; note a 'confirmed' task
|
|
1695
|
+
// now shows 0 until it LANDS (the credit no longer exists at confirm) — the UI
|
|
1696
|
+
// renders the "Est." label until the 'task.shipped' row books at land.
|
|
1588
1697
|
const CREDITS_AWARDED_SUBQUERY =
|
|
1589
1698
|
`CASE WHEN t.status IN ('confirmed', 'shipped')
|
|
1590
1699
|
THEN (SELECT COALESCE(SUM(cl.delta), 0)::int
|
|
1591
1700
|
FROM credit_log cl
|
|
1592
1701
|
WHERE cl.task_id = t.id
|
|
1593
|
-
AND cl.reason IN ('task.confirmed', 'idea_promotion_bonus', 'ship.net_negative'))
|
|
1702
|
+
AND cl.reason IN ('task.confirmed', 'task.shipped', 'idea_promotion_bonus', 'ship.net_negative'))
|
|
1594
1703
|
ELSE 0 END`;
|
|
1595
1704
|
|
|
1596
1705
|
async function getTask(id) {
|
|
@@ -1975,6 +2084,54 @@ async function getTaskDependents(taskId) {
|
|
|
1975
2084
|
return getTaskDepEdges(taskId, { reverse: true });
|
|
1976
2085
|
}
|
|
1977
2086
|
|
|
2087
|
+
// attachTaskDepSummaries (hall redesign task 1002439) — enrich an already-fetched
|
|
2088
|
+
// task list with the two dependency numbers the Goals rollup + Tasks dependency
|
|
2089
|
+
// view need, WITHOUT an N+1 (two aggregate queries over the whole id set, not one
|
|
2090
|
+
// per task). For each task:
|
|
2091
|
+
// blocked_by[] — the tasks THIS task waits on ({id,title,status,goal_id,satisfied}),
|
|
2092
|
+
// where satisfied === (target.status === 'shipped'), matching the
|
|
2093
|
+
// claim gate's DEPS_NOT_SHIPPED rule (ADR 0015). A `backlog` task
|
|
2094
|
+
// with any unsatisfied entry is what the UI paints "blocked".
|
|
2095
|
+
// blocks — how many tasks are waiting on THIS one (reverse edge count) —
|
|
2096
|
+
// the "clearing this unblocks N" signal.
|
|
2097
|
+
// task->task edges only (from_kind/to_kind='task'), same filter as getTaskDepEdges.
|
|
2098
|
+
// Mutates + returns the passed array so callers can `await db.attachTaskDepSummaries(tasks)`.
|
|
2099
|
+
async function attachTaskDepSummaries(tasks) {
|
|
2100
|
+
if (!Array.isArray(tasks) || tasks.length === 0) return tasks;
|
|
2101
|
+
const ids = tasks.map((t) => Number(t.id)).filter(Number.isFinite);
|
|
2102
|
+
if (ids.length === 0) return tasks;
|
|
2103
|
+
const [fwd, rev] = await Promise.all([
|
|
2104
|
+
pool.query(
|
|
2105
|
+
`SELECT d.from_id AS task_id,
|
|
2106
|
+
json_agg(json_build_object(
|
|
2107
|
+
'id', t2.id, 'title', t2.title, 'status', t2.status,
|
|
2108
|
+
'goal_id', t2.goal_id, 'satisfied', (t2.status = 'shipped')
|
|
2109
|
+
) ORDER BY t2.id) AS blocked_by
|
|
2110
|
+
FROM dependencies d
|
|
2111
|
+
JOIN tasks t2 ON t2.id = d.to_id
|
|
2112
|
+
WHERE d.from_kind = 'task' AND d.to_kind = 'task'
|
|
2113
|
+
AND d.from_id = ANY($1::bigint[])
|
|
2114
|
+
GROUP BY d.from_id`,
|
|
2115
|
+
[ids]
|
|
2116
|
+
),
|
|
2117
|
+
pool.query(
|
|
2118
|
+
`SELECT d.to_id AS task_id, count(*)::int AS blocks
|
|
2119
|
+
FROM dependencies d
|
|
2120
|
+
WHERE d.from_kind = 'task' AND d.to_kind = 'task'
|
|
2121
|
+
AND d.to_id = ANY($1::bigint[])
|
|
2122
|
+
GROUP BY d.to_id`,
|
|
2123
|
+
[ids]
|
|
2124
|
+
),
|
|
2125
|
+
]);
|
|
2126
|
+
const blockedBy = new Map(fwd.rows.map((r) => [String(r.task_id), r.blocked_by]));
|
|
2127
|
+
const blocks = new Map(rev.rows.map((r) => [String(r.task_id), r.blocks]));
|
|
2128
|
+
for (const t of tasks) {
|
|
2129
|
+
t.blocked_by = blockedBy.get(String(t.id)) || [];
|
|
2130
|
+
t.blocks = blocks.get(String(t.id)) || 0;
|
|
2131
|
+
}
|
|
2132
|
+
return tasks;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1978
2135
|
// Generic cycle check over the polymorphic graph. Adding edge (from -> to) creates
|
|
1979
2136
|
// a cycle iff `from` is already reachable FROM `to` along existing edges, so we seed
|
|
1980
2137
|
// the walk at `to` and look for `from`. Nodes are (kind, id) pairs tokenized as
|
|
@@ -2323,6 +2480,7 @@ async function unsatisfiedDeps(taskId, client = pool) {
|
|
|
2323
2480
|
// give a useful error rather than a generic constraint failure).
|
|
2324
2481
|
|
|
2325
2482
|
async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSessionId, enforceOverlap = false }) {
|
|
2483
|
+
const heldPermissions = await claimPermissionsFor(builderId); // BV1.R106 — before the tx opens
|
|
2326
2484
|
const client = await pool.connect();
|
|
2327
2485
|
try {
|
|
2328
2486
|
await client.query('BEGIN');
|
|
@@ -2339,6 +2497,23 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
2339
2497
|
}
|
|
2340
2498
|
const builderRank = builderRows[0] ? builderRows[0].rank : null;
|
|
2341
2499
|
|
|
2500
|
+
// ADR 0120 part 3: the needs_rebase claim gate. A builder who owes a rebase
|
|
2501
|
+
// on any un-landed confirmed task (flagged by the reconciler, part 2) is
|
|
2502
|
+
// blocked from taking on NEW work until they LAND it (rebase the branch +
|
|
2503
|
+
// re-ship → reconciler) or RELEASE it — both clear the flag. Their
|
|
2504
|
+
// already-active claims are untouched (we only refuse fresh claims, never
|
|
2505
|
+
// freeze in-flight work). Since the reward now lands only on ship (part 1),
|
|
2506
|
+
// the builder is motivated to clear it; since it gates their queue, they
|
|
2507
|
+
// can't walk away from it. Runs in THIS txn so it can't race a concurrent
|
|
2508
|
+
// ship that clears the last flag.
|
|
2509
|
+
const owedRebases = await needsRebaseTasksForBuilder(builderId, client);
|
|
2510
|
+
if (owedRebases.length > 0) {
|
|
2511
|
+
throw {
|
|
2512
|
+
code: 'REBASE_REQUIRED',
|
|
2513
|
+
rebaseTasks: owedRebases.map((t) => ({ id: t.id, title: t.title, reason: t.needs_rebase_reason })),
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2342
2517
|
const { rows: taskRows } = await client.query(
|
|
2343
2518
|
`SELECT id, status, touches, newcomer_friendly, requires_rank, needs_migration, module_key, credits_reward, reward_gate_exempt, est_minutes, kind FROM tasks WHERE id = $1 FOR UPDATE`,
|
|
2344
2519
|
[taskId]
|
|
@@ -2358,8 +2533,10 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
2358
2533
|
// only explicitly-tagged-open tasks" — markdown describes it, this check
|
|
2359
2534
|
// enforces it. Error code is rank_too_low_for_task (V3.R15); XENOS_RESTRICTED
|
|
2360
2535
|
// is kept as an alias on the error object for callers on the pre-rename
|
|
2361
|
-
// release.
|
|
2362
|
-
|
|
2536
|
+
// release. BV1.R106 (ADR 0151): same rule, now decided on the PERMISSIONS
|
|
2537
|
+
// task.claim.any / task.claim.newcomer — see ./claim-eligibility.js.
|
|
2538
|
+
const newcomerFriendly = task.newcomer_friendly;
|
|
2539
|
+
if (!claimEligibilityAllows({ heldPermissions, newcomerFriendly, rankRuleAllows: xenosClaimAllowed(builderRank, newcomerFriendly) })) {
|
|
2363
2540
|
throw { code: 'rank_too_low_for_task', legacy_code: 'XENOS_RESTRICTED' };
|
|
2364
2541
|
}
|
|
2365
2542
|
|
|
@@ -2577,6 +2754,8 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
2577
2754
|
// sharing any rows can't deadlock by locking in opposite orders.
|
|
2578
2755
|
uniqIds.sort((a, b) => a - b);
|
|
2579
2756
|
|
|
2757
|
+
// BV1.R106: resolved BEFORE the tx opens — see claimPermissionsFor.
|
|
2758
|
+
const heldPermissions = await claimPermissionsFor(builderId);
|
|
2580
2759
|
const client = await pool.connect();
|
|
2581
2760
|
try {
|
|
2582
2761
|
await client.query('BEGIN');
|
|
@@ -2620,9 +2799,20 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
2620
2799
|
// Mirrors the single claimTask gate so the batch path can't be used to
|
|
2621
2800
|
// sidestep the sandbox. Failure reason carries both names — the new
|
|
2622
2801
|
// canonical code and the legacy alias — so callers on either release work.
|
|
2802
|
+
//
|
|
2803
|
+
// BV1.R106 (ADR 0151): the inner decision now reads the PERMISSION atoms, as
|
|
2804
|
+
// in claimTask. The outer `builderRank === 'xenos'` short-circuit is LEFT AS
|
|
2805
|
+
// IS: it is not a fast path but a semantic one (a reserved/unwired rank is
|
|
2806
|
+
// never checked here, while claimTask checks every rank), so removing it would
|
|
2807
|
+
// change who can batch-claim. That pre-existing asymmetry is reported, not
|
|
2808
|
+
// fixed, under this task — closing it is an authority decision of its own.
|
|
2623
2809
|
if (builderRank === 'xenos') {
|
|
2624
2810
|
for (const t of taskRows) {
|
|
2625
|
-
if (!
|
|
2811
|
+
if (!claimEligibilityAllows({
|
|
2812
|
+
heldPermissions,
|
|
2813
|
+
newcomerFriendly: t.newcomer_friendly,
|
|
2814
|
+
rankRuleAllows: xenosClaimAllowed(builderRank, t.newcomer_friendly),
|
|
2815
|
+
})) {
|
|
2626
2816
|
failures.push({ task_id: t.id, reason: 'rank_too_low_for_task', legacy_reason: 'XENOS_RESTRICTED' });
|
|
2627
2817
|
}
|
|
2628
2818
|
}
|
|
@@ -2858,7 +3048,13 @@ async function releaseClaimsBatch({ claimIds, builderId, resolverSessionId, forc
|
|
|
2858
3048
|
[claimId]
|
|
2859
3049
|
);
|
|
2860
3050
|
await client.query(
|
|
2861
|
-
|
|
3051
|
+
// ADR 0120 part 2: releasing a task frees it from the builder's queue,
|
|
3052
|
+
// so clear any needs_rebase flag — the rebase is no longer their chore
|
|
3053
|
+
// (this is the "release to unblock" exit the ADR guarantees).
|
|
3054
|
+
`UPDATE tasks
|
|
3055
|
+
SET status = 'ready', needs_rebase = false, needs_rebase_reason = NULL,
|
|
3056
|
+
needs_rebase_at = NULL, updated_at = now()
|
|
3057
|
+
WHERE id = $1`,
|
|
2862
3058
|
[claim.task_id]
|
|
2863
3059
|
);
|
|
2864
3060
|
await sealSessionLog(client, {
|
|
@@ -2923,12 +3119,21 @@ async function validateClaimBatch({ taskIds, builderId }) {
|
|
|
2923
3119
|
const reasonsById = new Map(uniqIds.map((id) => [id, []]));
|
|
2924
3120
|
const push = (id, reason) => reasonsById.get(id).push(reason);
|
|
2925
3121
|
|
|
3122
|
+
// BV1.R106: same permission-expressed queue gate as claimTasksBatch (including
|
|
3123
|
+
// its `builderRank === 'xenos'` short-circuit), resolved once for the whole set
|
|
3124
|
+
// — authority is per-request, not per-row.
|
|
3125
|
+
const heldPermissions = await claimPermissionsFor(builderId);
|
|
3126
|
+
|
|
2926
3127
|
for (const id of uniqIds) {
|
|
2927
3128
|
if (builderInactive) push(id, { reason: 'BUILDER_INACTIVE' });
|
|
2928
3129
|
const t = byId.get(id);
|
|
2929
3130
|
if (!t) { push(id, { reason: 'TASK_NOT_FOUND' }); continue; }
|
|
2930
3131
|
if (t.status !== 'ready') push(id, { reason: 'TASK_NOT_READY', currentStatus: t.status });
|
|
2931
|
-
if (builderRank === 'xenos' && !
|
|
3132
|
+
if (builderRank === 'xenos' && !claimEligibilityAllows({
|
|
3133
|
+
heldPermissions,
|
|
3134
|
+
newcomerFriendly: t.newcomer_friendly,
|
|
3135
|
+
rankRuleAllows: xenosClaimAllowed(builderRank, t.newcomer_friendly),
|
|
3136
|
+
})) {
|
|
2932
3137
|
push(id, { reason: 'rank_too_low_for_task', legacy_reason: 'XENOS_RESTRICTED' });
|
|
2933
3138
|
}
|
|
2934
3139
|
if (!rankAllowsTask(builderRank, t.requires_rank)) {
|
|
@@ -3002,7 +3207,8 @@ async function validateClaimBatch({ taskIds, builderId }) {
|
|
|
3002
3207
|
// Resolve a claim. Three-state task lifecycle:
|
|
3003
3208
|
// active → completed (builder declared done; awaiting grader)
|
|
3004
3209
|
// active → completed → confirmed (promoted by applyGrade when the subagent
|
|
3005
|
-
// grader passes;
|
|
3210
|
+
// grader passes; verified but NOT yet paid —
|
|
3211
|
+
// ADR 0120 books credits at ship, not confirm)
|
|
3006
3212
|
// active → ready (abandoned or partial — back to the queue)
|
|
3007
3213
|
//
|
|
3008
3214
|
// Caller passes `verification` = { passed: bool, scanner_clean, smoke_clean,
|
|
@@ -3168,8 +3374,9 @@ async function resolveClaim({ claimId, builderId, outcome, notesMd, valueSummary
|
|
|
3168
3374
|
}
|
|
3169
3375
|
|
|
3170
3376
|
// Final transition: confirmed → shipped. Called by /merge-mode (or auto-merge
|
|
3171
|
-
// in /builder-ship) once the branch is on main + deployed.
|
|
3172
|
-
//
|
|
3377
|
+
// in /builder-ship) once the branch is on main + deployed. THIS is the credit
|
|
3378
|
+
// moment — ADR 0120 pays on land, so awardLandingCredit books the reward here
|
|
3379
|
+
// (credit_log.reason = 'task.shipped'), not at confirm. Sets shipped_at.
|
|
3173
3380
|
//
|
|
3174
3381
|
// V3.R14 (#238): also stamps builders.first_ship_at the first time this
|
|
3175
3382
|
// builder ever crosses the ship threshold. COALESCE makes the write
|
|
@@ -3177,20 +3384,104 @@ async function resolveClaim({ claimId, builderId, outcome, notesMd, valueSummary
|
|
|
3177
3384
|
// the same transaction so a roster query that joins both tables sees a
|
|
3178
3385
|
// consistent view.
|
|
3179
3386
|
|
|
3387
|
+
// awardLandingCredit — the per-task reward, booked at the SHIPPED transition
|
|
3388
|
+
// (ADR 0120 "pay on land"). Moved out of the three confirm paths (confirmTask /
|
|
3389
|
+
// decideOverrideRequest / applyGrade): the per-task credit + idea-promotion
|
|
3390
|
+
// bonus now exist ONLY once the code is merged to `main`, so a task that
|
|
3391
|
+
// strands at 'confirmed' pays nothing. Idempotent on (task_id,
|
|
3392
|
+
// reason='task.shipped') — mirrors the old (task_id, 'task.confirmed') guard —
|
|
3393
|
+
// so a re-ship or a reconciler re-sweep is a true no-op. The reason is
|
|
3394
|
+
// deliberately 'task.shipped' (not 'task.confirmed'): after the one-time
|
|
3395
|
+
// clawback (ADR 0120 part 4) reverses a stranded task's OLD 'task.confirmed'
|
|
3396
|
+
// row, re-landing books a FRESH 'task.shipped' row — the reward is fully
|
|
3397
|
+
// re-earnable, exactly as the ADR promises. Caller passes its ship txn client
|
|
3398
|
+
// so the credit commits atomically with the status flip.
|
|
3399
|
+
async function awardLandingCredit(client, { taskId, creditedBuilderId, creditsReward, kind }) {
|
|
3400
|
+
const reward = seams.resolveOptional('reward');
|
|
3401
|
+
if (!reward || creditedBuilderId == null || !(Number(creditsReward) > 0)) {
|
|
3402
|
+
return { creditsAwarded: 0, ideaBonus: null };
|
|
3403
|
+
}
|
|
3404
|
+
const { rows: existing } = await client.query(
|
|
3405
|
+
`SELECT 1 FROM credit_log WHERE task_id = $1 AND reason = 'task.shipped' LIMIT 1`,
|
|
3406
|
+
[taskId]
|
|
3407
|
+
);
|
|
3408
|
+
if (existing.length > 0) return { creditsAwarded: 0, ideaBonus: null };
|
|
3409
|
+
const credit = await reward.insertMultipliedConfirmCredit(
|
|
3410
|
+
client, creditedBuilderId, taskId, creditsReward, kind, 'task.shipped'
|
|
3411
|
+
);
|
|
3412
|
+
const ideaBonus = await reward.maybeAwardIdeaPromotionBonus(
|
|
3413
|
+
client, taskId, creditedBuilderId, credit.creditsAwarded
|
|
3414
|
+
);
|
|
3415
|
+
return {
|
|
3416
|
+
creditsAwarded: credit.creditsAwarded,
|
|
3417
|
+
creditsRaw: credit.raw,
|
|
3418
|
+
creditsMultiplier: credit.multiplier,
|
|
3419
|
+
ideaBonus,
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
// flagTaskNeedsRebase — ADR 0120 part 2. The publish-reconciler calls this when
|
|
3424
|
+
// a confirmed task's land cannot be completed after the auto-land grace (its
|
|
3425
|
+
// retries + the ADR 0082 generated-file self-heal): a persistently-red PR that
|
|
3426
|
+
// hit the regen-heal cap, or a strand older than the stale threshold that can't
|
|
3427
|
+
// be proven landed (never pushed / no recorded tip). It is a SIGNAL, not a
|
|
3428
|
+
// status change — the task stays 'confirmed'; it is simply marked as awaiting
|
|
3429
|
+
// its BUILDER's rebase (attribution via tasks.shipped_by = claim holder).
|
|
3430
|
+
//
|
|
3431
|
+
// Idempotent + self-limiting: only a still-'confirmed', not-already-flagged task
|
|
3432
|
+
// is touched (WHERE status='confirmed' AND needs_rebase=false), so a task that
|
|
3433
|
+
// has since shipped or been released is never (re-)flagged, and a re-sweep of an
|
|
3434
|
+
// already-flagged task is a no-op. Returns true iff this call newly flagged it
|
|
3435
|
+
// (so the reconciler logs the transition once). needs_rebase_at is set once
|
|
3436
|
+
// (COALESCE) so the hall/CLI can show how long it has been outstanding.
|
|
3437
|
+
async function flagTaskNeedsRebase(taskId, reason = null) {
|
|
3438
|
+
const { rows } = await pool.query(
|
|
3439
|
+
`UPDATE tasks
|
|
3440
|
+
SET needs_rebase = true,
|
|
3441
|
+
needs_rebase_reason = $2,
|
|
3442
|
+
needs_rebase_at = COALESCE(needs_rebase_at, now()),
|
|
3443
|
+
updated_at = now()
|
|
3444
|
+
WHERE id = $1 AND status = 'confirmed' AND needs_rebase = false
|
|
3445
|
+
RETURNING id`,
|
|
3446
|
+
[taskId, reason]
|
|
3447
|
+
);
|
|
3448
|
+
return rows.length > 0;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// needsRebaseTasksForBuilder — the confirmed tasks a builder owes a rebase on
|
|
3452
|
+
// (ADR 0120 parts 2-3). Attribution is tasks.shipped_by (= the claim holder,
|
|
3453
|
+
// COALESCE'd there at confirm). Used by the claim gate (part 3) to refuse NEW
|
|
3454
|
+
// claims and by the hall/CLI surfacing (part 5). Accepts an optional client so
|
|
3455
|
+
// it can run inside claimTask's FOR UPDATE txn or standalone on the pool.
|
|
3456
|
+
// Ordered oldest-flagged first so the surfacing can show the longest-outstanding.
|
|
3457
|
+
async function needsRebaseTasksForBuilder(builderId, client = pool) {
|
|
3458
|
+
const { rows } = await client.query(
|
|
3459
|
+
`SELECT id, title, needs_rebase_reason, needs_rebase_at
|
|
3460
|
+
FROM tasks
|
|
3461
|
+
WHERE needs_rebase = true AND status = 'confirmed' AND shipped_by = $1
|
|
3462
|
+
ORDER BY needs_rebase_at ASC NULLS LAST, id ASC`,
|
|
3463
|
+
[builderId]
|
|
3464
|
+
);
|
|
3465
|
+
return rows;
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3180
3468
|
async function shipTask({ taskId, builderId }, deps = {}) {
|
|
3181
3469
|
// #538 test seam (mirrors confirmTask): a caller-supplied pool is honored ONLY
|
|
3182
3470
|
// under NODE_ENV==='test', so a regression test can drive the txn with a
|
|
3183
3471
|
// SQL-aware fakeClient. Production callers omit deps → module pool.
|
|
3184
3472
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
3185
|
-
const { shipRow, graduationBuilder, rankGraduation, actualShipperId, closedClaimCount } = await withTx(async (client) => {
|
|
3473
|
+
const { shipRow, graduationBuilder, rankGraduation, actualShipperId, closedClaimCount, creditedBuilderId, creditsAwarded, ideaBonus, justUnlocked } = await withTx(async (client) => {
|
|
3186
3474
|
const { rows } = await client.query(
|
|
3187
3475
|
`UPDATE tasks
|
|
3188
3476
|
SET status = 'shipped',
|
|
3189
3477
|
shipped_at = now(),
|
|
3190
3478
|
shipped_by = COALESCE(shipped_by, $2),
|
|
3479
|
+
needs_rebase = false,
|
|
3480
|
+
needs_rebase_reason = NULL,
|
|
3481
|
+
needs_rebase_at = NULL,
|
|
3191
3482
|
updated_at = now()
|
|
3192
3483
|
WHERE id = $1 AND status = 'confirmed'
|
|
3193
|
-
RETURNING id, title, status, shipped_at, shipped_by, security_sensitive, value_summary`,
|
|
3484
|
+
RETURNING id, title, status, shipped_at, shipped_by, security_sensitive, value_summary, credits_reward, kind`,
|
|
3194
3485
|
[taskId, builderId]
|
|
3195
3486
|
);
|
|
3196
3487
|
if (rows.length === 0) {
|
|
@@ -3277,7 +3568,47 @@ async function shipTask({ taskId, builderId }, deps = {}) {
|
|
|
3277
3568
|
// xenos→thetes atomically with this ship. Rank-guarded + idempotent.
|
|
3278
3569
|
const rankGraduation = await maybeAutoGraduateToThetes(client, actualShipperId);
|
|
3279
3570
|
|
|
3280
|
-
|
|
3571
|
+
// ADR 0120 "pay on land": the per-task credit + idea-promotion bonus are
|
|
3572
|
+
// booked HERE, at the confirmed→shipped flip — no longer at confirm. The
|
|
3573
|
+
// credit only exists once the code is on `main`; a task that strands at
|
|
3574
|
+
// 'confirmed' (never lands) now pays nothing. Attribution is the CLAIM
|
|
3575
|
+
// HOLDER (the builder who did the work), computed the same way as
|
|
3576
|
+
// confirmTask — never the caller who happened to trigger the ship (the
|
|
3577
|
+
// reconciler / a supervisor). mostRecentClaimHolderId reads the latest
|
|
3578
|
+
// claim by claimed_at regardless of released_at, so it is correct even
|
|
3579
|
+
// though the claims for this task were just closed above. Idempotent on
|
|
3580
|
+
// (task_id, reason='task.shipped'), so a re-ship / reconciler re-sweep is a
|
|
3581
|
+
// true no-op. Runs in THIS txn so the credit commits atomically with the
|
|
3582
|
+
// status flip.
|
|
3583
|
+
const creditedBuilderId = (await mostRecentClaimHolderId(client, taskId)) ?? actualShipperId;
|
|
3584
|
+
const landing = await awardLandingCredit(client, {
|
|
3585
|
+
taskId,
|
|
3586
|
+
creditedBuilderId,
|
|
3587
|
+
creditsReward: rows[0].credits_reward,
|
|
3588
|
+
kind: rows[0].kind,
|
|
3589
|
+
});
|
|
3590
|
+
// task 1679 pattern: state-based achievement reconcile now that the credit
|
|
3591
|
+
// (above) + the ship count are reflected in this txn. SAVEPOINT-wrapped so
|
|
3592
|
+
// a gamification error can never fail the ship. Moved to ship because the
|
|
3593
|
+
// credit it keys off (breaking-the-100c-bar etc.) now lands here.
|
|
3594
|
+
let justUnlocked = [];
|
|
3595
|
+
const rewardPort = seams.resolveOptional('reward');
|
|
3596
|
+
if (rewardPort && rewardPort.reconcileBuilderAchievements && creditedBuilderId != null) {
|
|
3597
|
+
await client.query('SAVEPOINT ach_reconcile_ship');
|
|
3598
|
+
try {
|
|
3599
|
+
justUnlocked = await rewardPort.reconcileBuilderAchievements(client, creditedBuilderId, { taskId });
|
|
3600
|
+
await client.query('RELEASE SAVEPOINT ach_reconcile_ship');
|
|
3601
|
+
} catch (_) {
|
|
3602
|
+
await client.query('ROLLBACK TO SAVEPOINT ach_reconcile_ship');
|
|
3603
|
+
justUnlocked = [];
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
return {
|
|
3608
|
+
shipRow: rows[0], graduationBuilder, rankGraduation, actualShipperId,
|
|
3609
|
+
closedClaimCount: closedClaims.length,
|
|
3610
|
+
creditedBuilderId, creditsAwarded: landing.creditsAwarded, ideaBonus: landing.ideaBonus, justUnlocked,
|
|
3611
|
+
};
|
|
3281
3612
|
}, { pool: activePool });
|
|
3282
3613
|
if (closedClaimCount > 0) {
|
|
3283
3614
|
// Post-commit observability: a normal ship closes 0 here (resolveClaim
|
|
@@ -3297,7 +3628,14 @@ async function shipTask({ taskId, builderId }, deps = {}) {
|
|
|
3297
3628
|
builderId: actualShipperId, rank: 'thetes', status: rankGraduation.status, actor: 'auto-graduation:#692',
|
|
3298
3629
|
}).catch(() => {});
|
|
3299
3630
|
}
|
|
3300
|
-
return {
|
|
3631
|
+
return {
|
|
3632
|
+
...shipRow,
|
|
3633
|
+
graduatedTo: rankGraduation ? 'thetes' : null,
|
|
3634
|
+
// ADR 0120: the per-task reward that landed on THIS ship (0 on a re-ship /
|
|
3635
|
+
// reconciler re-sweep — idempotent). The ship route surfaces it so the
|
|
3636
|
+
// builder sees "credits landed" at ship, not at confirm.
|
|
3637
|
+
creditedBuilderId, creditsAwarded, ideaBonus, justUnlocked,
|
|
3638
|
+
};
|
|
3301
3639
|
}
|
|
3302
3640
|
|
|
3303
3641
|
// task 1134 — feed for the server-owned publish reconciler (src/bongos/
|
|
@@ -3544,6 +3882,9 @@ async function releaseClaimOnBehalf({ claimId, archonId, archonLogin, reason })
|
|
|
3544
3882
|
await client.query(
|
|
3545
3883
|
`UPDATE tasks
|
|
3546
3884
|
SET status = 'ready',
|
|
3885
|
+
needs_rebase = false,
|
|
3886
|
+
needs_rebase_reason = NULL,
|
|
3887
|
+
needs_rebase_at = NULL,
|
|
3547
3888
|
updated_at = now()
|
|
3548
3889
|
WHERE id = $1`,
|
|
3549
3890
|
[taskId]
|
|
@@ -3610,25 +3951,16 @@ async function confirmTask({ taskId, builderId }, deps = {}) {
|
|
|
3610
3951
|
// without economy, so every use is guarded (degrades to a best-effort no-op).
|
|
3611
3952
|
const reward = seams.resolveOptional('reward');
|
|
3612
3953
|
|
|
3613
|
-
//
|
|
3614
|
-
//
|
|
3615
|
-
//
|
|
3616
|
-
//
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
if (reward && existing.length === 0 && task.credits_reward > 0) {
|
|
3624
|
-
const credit = await reward.insertMultipliedConfirmCredit(
|
|
3625
|
-
client, creditedBuilderId, taskId, task.credits_reward, task.kind
|
|
3626
|
-
);
|
|
3627
|
-
creditsAwarded = credit.creditsAwarded;
|
|
3628
|
-
ideaBonus = await reward.maybeAwardIdeaPromotionBonus(
|
|
3629
|
-
client, taskId, creditedBuilderId, creditsAwarded
|
|
3630
|
-
);
|
|
3631
|
-
}
|
|
3954
|
+
// ADR 0120 "pay on land": the per-task credit + idea-promotion bonus are NO
|
|
3955
|
+
// LONGER awarded at confirm. They land at the confirmed→shipped flip
|
|
3956
|
+
// (shipTask → awardLandingCredit, keyed 'task.shipped'), so a task that
|
|
3957
|
+
// strands at 'confirmed' pays nothing until its code is on `main`. confirm
|
|
3958
|
+
// still flips the status and attributes shipped_by to the claim holder
|
|
3959
|
+
// (above); the reward waits for the land. creditsAwarded/ideaBonus stay in
|
|
3960
|
+
// the return shape (0 / null at confirm) so callers/tests keep a stable
|
|
3961
|
+
// contract.
|
|
3962
|
+
const creditsAwarded = 0;
|
|
3963
|
+
const ideaBonus = null;
|
|
3632
3964
|
|
|
3633
3965
|
// task 1679: confirmTask is the --skip-grade / grader_bypass ship path — the
|
|
3634
3966
|
// DOMINANT one — and until now it awarded credits but NEVER evaluated
|
|
@@ -3679,8 +4011,9 @@ async function confirmTask({ taskId, builderId }, deps = {}) {
|
|
|
3679
4011
|
// still owns the work; release it first so the abandon
|
|
3680
4012
|
// can never race a ship in flight.
|
|
3681
4013
|
// Any other status (backlog/ready/blocked/completed/confirmed) abandons. The
|
|
3682
|
-
// route is archon-gated
|
|
3683
|
-
// by the writing middleware, so the
|
|
4014
|
+
// route is metic+archon-gated (ADR 0152; archon-only until then), and the abandon
|
|
4015
|
+
// + its reason are captured in audit_log by the writing middleware, so the
|
|
4016
|
+
// append-only ledger records who retired what. restoreTask (below) is the undo.
|
|
3684
4017
|
//
|
|
3685
4018
|
// NOTE on dependencies: abandoning a task that is listed in another task's
|
|
3686
4019
|
// dependencies[] does NOT auto-adjust those dependents — they will simply never
|
|
@@ -3749,6 +4082,69 @@ async function abandonTask({ taskId, reason }, deps = {}) {
|
|
|
3749
4082
|
}
|
|
3750
4083
|
}
|
|
3751
4084
|
|
|
4085
|
+
// restoreTask (ADR 0152) — the inverse of abandonTask: 'abandoned' → 'backlog'.
|
|
4086
|
+
// Added when abandon was re-ranked Archon→Metic+, because until then abandon was
|
|
4087
|
+
// one-way through the API (promote took only backlog/blocked, PATCH refuses
|
|
4088
|
+
// `status`), so a mis-retire could only be undone with hand-SQL on prod. Handing a
|
|
4089
|
+
// Metic the kill switch without an undo at the same rank would have moved the
|
|
4090
|
+
// owner-cleanup burden rather than removed it.
|
|
4091
|
+
//
|
|
4092
|
+
// Restores to **backlog**, not ready: the task was retired deliberately, so it
|
|
4093
|
+
// comes back un-workable and needs a second, explicit promote to be claimable.
|
|
4094
|
+
// abandonTask stamps `value_summary = 'Abandoned: <reason>'`; a restored task
|
|
4095
|
+
// clears exactly that stamp (and nothing else) so a live task never carries a
|
|
4096
|
+
// retire note into its ship card / broadcast.
|
|
4097
|
+
//
|
|
4098
|
+
// Guards: TASK_NOT_FOUND, and TASK_NOT_ABANDONED for any other status (the route
|
|
4099
|
+
// checks first, but the db guard makes a direct caller — a CLI, a poller — safe
|
|
4100
|
+
// too). Reached via POST /tasks/:id/promote; the audit_log row is written by the
|
|
4101
|
+
// writing middleware there, same as the abandon it reverses.
|
|
4102
|
+
//
|
|
4103
|
+
// #538 test seam: a caller-supplied pool is honored ONLY under NODE_ENV==='test'
|
|
4104
|
+
// (matches abandonTask/confirmTask). Production callers omit deps → module pool.
|
|
4105
|
+
async function restoreTask({ taskId }, deps = {}) {
|
|
4106
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
4107
|
+
const client = await activePool.connect();
|
|
4108
|
+
try {
|
|
4109
|
+
await client.query('BEGIN');
|
|
4110
|
+
|
|
4111
|
+
const { rows: taskRows } = await client.query(
|
|
4112
|
+
`SELECT id, title, status FROM tasks WHERE id = $1 FOR UPDATE`,
|
|
4113
|
+
[taskId]
|
|
4114
|
+
);
|
|
4115
|
+
if (taskRows.length === 0) {
|
|
4116
|
+
throw Object.assign(new Error('task not found'), { code: 'TASK_NOT_FOUND' });
|
|
4117
|
+
}
|
|
4118
|
+
const task = taskRows[0];
|
|
4119
|
+
if (task.status !== 'abandoned') {
|
|
4120
|
+
throw Object.assign(new Error('task is not abandoned'), { code: 'TASK_NOT_ABANDONED' });
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
const { rows: updated } = await client.query(
|
|
4124
|
+
`UPDATE tasks
|
|
4125
|
+
SET status = 'backlog',
|
|
4126
|
+
value_summary = CASE WHEN value_summary LIKE 'Abandoned: %' THEN NULL ELSE value_summary END,
|
|
4127
|
+
updated_at = now()
|
|
4128
|
+
WHERE id = $1
|
|
4129
|
+
RETURNING id, title, status`,
|
|
4130
|
+
[taskId]
|
|
4131
|
+
);
|
|
4132
|
+
|
|
4133
|
+
await client.query('COMMIT');
|
|
4134
|
+
return {
|
|
4135
|
+
taskId,
|
|
4136
|
+
taskTitle: updated[0].title,
|
|
4137
|
+
taskStatus: 'backlog',
|
|
4138
|
+
previousStatus: 'abandoned',
|
|
4139
|
+
};
|
|
4140
|
+
} catch (err) {
|
|
4141
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
4142
|
+
throw err;
|
|
4143
|
+
} finally {
|
|
4144
|
+
client.release();
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
|
|
3752
4148
|
// task override requests — V3.R74 (#293). Rare path: a builder whose ship was
|
|
3753
4149
|
// rejected by the subagent grader (task stays at 'completed', no credits)
|
|
3754
4150
|
// believes the grader was wrong and asks an Archon to confirm anyway.
|
|
@@ -3886,31 +4282,14 @@ async function decideOverrideRequest({ requestId, archonId, decision, note }, de
|
|
|
3886
4282
|
// can always destructure it from confirmResult without a guard (#538).
|
|
3887
4283
|
confirmResult = { taskFlipped: false, creditsAwarded: 0, ideaBonus: null, creditedBuilderId };
|
|
3888
4284
|
} else {
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
// V3.R27 (#247): apply kind multiplier on the override-confirm path
|
|
3898
|
-
// too, so a builder who had to argue for their ship is not punished
|
|
3899
|
-
// by getting the legacy 1.0× payout. Credit allocation carved to
|
|
3900
|
-
// modules/economy/ (BV1.R77 / ADR 0093 §1-2): resolve the `reward`
|
|
3901
|
-
// kernel port and write the credit inside this same txn.
|
|
3902
|
-
const reward = seams.resolveOptional('reward');
|
|
3903
|
-
const credit = await reward.insertMultipliedConfirmCredit(
|
|
3904
|
-
client, creditedBuilderId, task.id, task.credits_reward, task.kind
|
|
3905
|
-
);
|
|
3906
|
-
creditsAwarded = credit.creditsAwarded;
|
|
3907
|
-
ideaBonus = await reward.maybeAwardIdeaPromotionBonus(
|
|
3908
|
-
client, task.id, creditedBuilderId, creditsAwarded
|
|
3909
|
-
);
|
|
3910
|
-
}
|
|
3911
|
-
// #704: thetes graduation is evaluated at SHIP time (shipTask), not on
|
|
3912
|
-
// confirm/override — credits here, rank graduation when the task ships.
|
|
3913
|
-
confirmResult = { taskFlipped: true, creditsAwarded, ideaBonus, creditedBuilderId };
|
|
4285
|
+
// ADR 0120 "pay on land": the override-approval path flips the task
|
|
4286
|
+
// completed→confirmed and attributes shipped_by to the claim holder
|
|
4287
|
+
// (above), but the per-task credit + idea bonus now wait for the land
|
|
4288
|
+
// (shipTask → awardLandingCredit, keyed 'task.shipped'). A stranded
|
|
4289
|
+
// override-confirmed task pays nothing until its code is on `main`.
|
|
4290
|
+
// creditsAwarded/ideaBonus stay in the return shape (0 / null) for a
|
|
4291
|
+
// stable contract; thetes graduation was already ship-time (#704).
|
|
4292
|
+
confirmResult = { taskFlipped: true, creditsAwarded: 0, ideaBonus: null, creditedBuilderId };
|
|
3914
4293
|
}
|
|
3915
4294
|
}
|
|
3916
4295
|
|
|
@@ -4068,57 +4447,20 @@ async function applyGrade({ taskId, builderId, gradeResult, committedFiles = nul
|
|
|
4068
4447
|
netNegativeBonus = nn.bonus;
|
|
4069
4448
|
subtractorUnlocks = nn.justUnlocked;
|
|
4070
4449
|
|
|
4071
|
-
//
|
|
4072
|
-
//
|
|
4073
|
-
//
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
)
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
// V3.R27 (#247): apply the per-kind multiplier. The credit_log
|
|
4086
|
-
// description records "<raw>c × <mult> = <effective>c (<kind>)" so
|
|
4087
|
-
// the audit trail is self-explaining; the trigger on credit_log rolls
|
|
4088
|
-
// the effective value into builders.total_credits exactly as before.
|
|
4089
|
-
const credit = await reward.insertMultipliedConfirmCredit(
|
|
4090
|
-
client, creditedBuilderId, taskId, task.credits_reward, task.kind
|
|
4091
|
-
);
|
|
4092
|
-
creditsAwarded = credit.creditsAwarded;
|
|
4093
|
-
creditsRaw = credit.raw;
|
|
4094
|
-
creditsMultiplier = credit.multiplier;
|
|
4095
|
-
|
|
4096
|
-
// V3.R27 (#247): staged idea-promotion bonus. If this task came out
|
|
4097
|
-
// of idea_inbox and the capturer is a different builder than the
|
|
4098
|
-
// shipper, give the capturer 25% of the multiplier-adjusted reward.
|
|
4099
|
-
// Runs INSIDE the same txn so total_credits and bonus stay atomic.
|
|
4100
|
-
ideaBonus = await reward.maybeAwardIdeaPromotionBonus(
|
|
4101
|
-
client, taskId, creditedBuilderId, creditsAwarded
|
|
4102
|
-
);
|
|
4103
|
-
|
|
4104
|
-
// #704: thetes graduation is evaluated at SHIP time (shipTask), not at
|
|
4105
|
-
// grader-confirm — credits land here, rank graduation when the task ships.
|
|
4106
|
-
|
|
4107
|
-
const { rows: afterRows } = await client.query(
|
|
4108
|
-
`SELECT total_credits FROM builders WHERE id = $1`,
|
|
4109
|
-
[creditedBuilderId]
|
|
4110
|
-
);
|
|
4111
|
-
const totalAfter = Number(afterRows[0]?.total_credits ?? 0);
|
|
4112
|
-
|
|
4113
|
-
const shipUnlocks = await reward.evaluateAfterShip(client, creditedBuilderId, taskId, committedFiles);
|
|
4114
|
-
const creditUnlocks = await reward.evaluateAfterCreditAward(
|
|
4115
|
-
client, creditedBuilderId, totalBefore, totalAfter
|
|
4116
|
-
);
|
|
4117
|
-
justUnlocked = [...subtractorUnlocks, ...shipUnlocks, ...creditUnlocks];
|
|
4118
|
-
} else {
|
|
4119
|
-
const shipUnlocks = await reward.evaluateAfterShip(client, creditedBuilderId, taskId, committedFiles);
|
|
4120
|
-
justUnlocked = [...subtractorUnlocks, ...shipUnlocks];
|
|
4121
|
-
}
|
|
4450
|
+
// ADR 0120 "pay on land": the per-task credit + idea-promotion bonus are
|
|
4451
|
+
// NO LONGER awarded at grader-confirm. They land at the confirmed→shipped
|
|
4452
|
+
// flip (shipTask → awardLandingCredit, keyed 'task.shipped'), so a task
|
|
4453
|
+
// that strands at 'confirmed' pays nothing until its code is on `main`.
|
|
4454
|
+
// The net-negative (simplification) bonus above STAYS here — it derives
|
|
4455
|
+
// from grade-time diff_stats that only exist at grade time, so it cannot
|
|
4456
|
+
// move to shipTask (and is outside ADR 0120's two named reward writes).
|
|
4457
|
+
// creditsAwarded/creditsRaw/creditsMultiplier/ideaBonus stay 0/null at
|
|
4458
|
+
// confirm for a stable return contract; the credit lands at ship. The
|
|
4459
|
+
// credit-crossing achievement evaluator (evaluateAfterCreditAward) moves
|
|
4460
|
+
// with the credit to shipTask's reconcile; here we still fire the
|
|
4461
|
+
// ship/subtractor unlocks that DON'T depend on the per-task credit.
|
|
4462
|
+
const shipUnlocks = await reward.evaluateAfterShip(client, creditedBuilderId, taskId, committedFiles);
|
|
4463
|
+
justUnlocked = [...subtractorUnlocks, ...shipUnlocks];
|
|
4122
4464
|
|
|
4123
4465
|
// task 1679: state-based safety net. The granular evaluators above are
|
|
4124
4466
|
// boundary-/path-sensitive — breaking-the-100c-bar only fires when a
|
|
@@ -4193,19 +4535,52 @@ async function getActiveClaimsForBuilder(builderId) {
|
|
|
4193
4535
|
return rows;
|
|
4194
4536
|
}
|
|
4195
4537
|
|
|
4538
|
+
// Task-scoped dev box (goal 1000051 / R92, ADR 0148 §2). The set-union of the
|
|
4539
|
+
// module KEYS a builder's active claims put in scope: each active claim's task →
|
|
4540
|
+
// its goal → goal.scope_modules (the ADR 0086 scope wall), unioned + deduped
|
|
4541
|
+
// across all the builder's active claims. This is the "what code does this
|
|
4542
|
+
// builder's current work need" signal the dev-box source-access resolver
|
|
4543
|
+
// (modules/dev-box/box-access.js, wired in R93/R94) maps to git sparse-checkout
|
|
4544
|
+
// globs via the doorway `moduleScopeMap`. Returns [] when the builder holds no
|
|
4545
|
+
// active claim, or when every claimed task carries no goal / an empty
|
|
4546
|
+
// scope_modules — the box then falls back to BASE-only (the pre-existing Bongos
|
|
4547
|
+
// instance). KEYS only; glob resolution is the box module's job (it reaches
|
|
4548
|
+
// moduleScopeMap through the doorway, never a core-internal require — the
|
|
4549
|
+
// R90/ADR-0148 boundary). Honours the #538 deps.pool test seam
|
|
4550
|
+
// (NODE_ENV==='test' only), like the goals reads above.
|
|
4551
|
+
async function moduleScopeForActiveClaims(builderId, deps = {}) {
|
|
4552
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
4553
|
+
const id = Number(builderId);
|
|
4554
|
+
if (!Number.isInteger(id)) {
|
|
4555
|
+
throw new Error('moduleScopeForActiveClaims: builderId must be numeric');
|
|
4556
|
+
}
|
|
4557
|
+
const { rows } = await activePool.query(
|
|
4558
|
+
`SELECT DISTINCT m AS module_key
|
|
4559
|
+
FROM claims c
|
|
4560
|
+
JOIN tasks t ON t.id = c.task_id
|
|
4561
|
+
JOIN goals g ON g.id = t.goal_id
|
|
4562
|
+
CROSS JOIN LATERAL unnest(g.scope_modules) AS m
|
|
4563
|
+
WHERE c.builder_id = $1 AND c.released_at IS NULL
|
|
4564
|
+
ORDER BY m`,
|
|
4565
|
+
[id]
|
|
4566
|
+
);
|
|
4567
|
+
return rows.map((r) => r.module_key);
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4196
4570
|
// task 1642 — every active claim bound to a given CLI session. "Active" is the
|
|
4197
4571
|
// claims-table convention (released_at IS NULL); there is NO `status` column on
|
|
4198
4572
|
// claims (a common misread — the task's status lives on `tasks`). Keyed on
|
|
4199
|
-
// creator_session_id (the
|
|
4200
|
-
//
|
|
4201
|
-
//
|
|
4202
|
-
//
|
|
4203
|
-
// must never reach this — they are session-UNBOUND
|
|
4204
|
-
// that any CLI session may adopt; the route
|
|
4573
|
+
// creator_session_id (the auth-token session shared across a builder's worktrees),
|
|
4574
|
+
// NOT builder_id. This backs the one-active-claim guard (shouldRefuseSessionClaim),
|
|
4575
|
+
// which further discriminates by `worktree_name` — the real commingle boundary —
|
|
4576
|
+
// so two claims can coexist for one auth session as long as they land in different
|
|
4577
|
+
// worktrees. NULL-session callers must never reach this — they are session-UNBOUND
|
|
4578
|
+
// web claims (bind_session=false) that any CLI session may adopt; the route
|
|
4579
|
+
// short-circuits before calling here.
|
|
4205
4580
|
async function getActiveClaimsForSession(creatorSessionId) {
|
|
4206
4581
|
if (!creatorSessionId) return [];
|
|
4207
4582
|
const { rows } = await pool.query(
|
|
4208
|
-
`SELECT c.id AS claim_id, c.task_id, c.creator_session_id, c.claimed_at,
|
|
4583
|
+
`SELECT c.id AS claim_id, c.task_id, c.creator_session_id, c.worktree_name, c.claimed_at,
|
|
4209
4584
|
t.title
|
|
4210
4585
|
FROM claims c JOIN tasks t ON t.id = c.task_id
|
|
4211
4586
|
WHERE c.creator_session_id = $1 AND c.released_at IS NULL
|
|
@@ -4952,6 +5327,7 @@ module.exports = {
|
|
|
4952
5327
|
updateTaskStatus,
|
|
4953
5328
|
getTaskDependencies,
|
|
4954
5329
|
getTaskDependents,
|
|
5330
|
+
attachTaskDepSummaries,
|
|
4955
5331
|
addTaskDependency,
|
|
4956
5332
|
removeTaskDependency,
|
|
4957
5333
|
setTaskDependencies,
|
|
@@ -4974,12 +5350,16 @@ module.exports = {
|
|
|
4974
5350
|
resolveClaim,
|
|
4975
5351
|
confirmTask,
|
|
4976
5352
|
abandonTask,
|
|
5353
|
+
restoreTask, // ADR 0152 — the abandoned→backlog undo that pairs with the Metic+ abandon grant
|
|
4977
5354
|
applyGrade,
|
|
4978
5355
|
shipTask,
|
|
5356
|
+
flagTaskNeedsRebase, // ADR 0120 part 2 — reconciler flags an unlandable confirmed task for its builder
|
|
5357
|
+
needsRebaseTasksForBuilder, // ADR 0120 part 3/5 — the claim gate + hall/CLI read the builder's owed rebases
|
|
4979
5358
|
listConfirmedTasksForReconcile, // task 1134 — server-owned publish reconciler feed
|
|
4980
5359
|
setClaimPublishedSha, // task 1547 — record the SERVER-pushed tip for per-task land proof
|
|
4981
5360
|
getClaimPublishedSha, // task 1547 — read the published tip for the reconcile gate
|
|
4982
5361
|
getActiveClaimsForBuilder,
|
|
5362
|
+
moduleScopeForActiveClaims, // goal 1000051 / R92 — dev-box task-scoped source access (ADR 0148 §2)
|
|
4983
5363
|
getActiveClaimsForSession,
|
|
4984
5364
|
getLastClaimHolder, // --regrade ownership guard (#678)
|
|
4985
5365
|
getTaskGrade, // V4.R08 — diff-hash reuse check before grading
|