@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
|
@@ -25,6 +25,17 @@
|
|
|
25
25
|
|
|
26
26
|
const { pool } = require('../../src/module-api');
|
|
27
27
|
|
|
28
|
+
// isPendingReview — the ONE derivation of the "met — pending review" auto-flag
|
|
29
|
+
// (ADR 0086 §6 / BV1.R63): a criterion with >=1 linked task, every one shipped,
|
|
30
|
+
// and not yet confirmed. Pure + shared so every read model agrees — the rollup
|
|
31
|
+
// below (buildCriterionProgress) and the hall's goal-detail read
|
|
32
|
+
// (db.listCriteriaForGoal, task 1002164) both call it; listPendingReviewCriteria
|
|
33
|
+
// expresses the same rule as a SQL HAVING clause. Counts are coerced because
|
|
34
|
+
// node-postgres renders count() as a string unless cast.
|
|
35
|
+
function isPendingReview({ satisfied, linked, unshipped }) {
|
|
36
|
+
return !satisfied && Number(linked) > 0 && Number(unshipped) === 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
// -------------------------------------------------------------------------
|
|
29
40
|
// Reads
|
|
30
41
|
// -------------------------------------------------------------------------
|
|
@@ -262,9 +273,12 @@ function buildCriterionProgress(versionId, rows, unattributedCount = 0, goalRows
|
|
|
262
273
|
// Auto-flag (ADR 0086 §6 / BV1.R63): a criterion is "met — pending review"
|
|
263
274
|
// when it has ≥1 linked task and every one has shipped, but it has NOT yet
|
|
264
275
|
// been confirmed (satisfied=false). Purely DERIVED — no stored column, no
|
|
265
|
-
// manual propose step. /goal-review
|
|
266
|
-
// (→ satisfied) or reject;
|
|
267
|
-
|
|
276
|
+
// manual propose step. /goal-review and the hall's goal-detail page (task
|
|
277
|
+
// 1002164) surface these for a Metic+ to confirm (→ satisfied) or reject;
|
|
278
|
+
// /status renders the flag inline.
|
|
279
|
+
const pending_review = isPendingReview({
|
|
280
|
+
satisfied: c.satisfied, linked: c.tasks.length, unshipped: remaining.length,
|
|
281
|
+
});
|
|
268
282
|
return { ...c, counts: { ...counts, shipped, remaining: remaining.length }, remaining, pending_review };
|
|
269
283
|
});
|
|
270
284
|
|
|
@@ -350,6 +364,7 @@ module.exports = {
|
|
|
350
364
|
criterionProgress,
|
|
351
365
|
buildCriterionProgress,
|
|
352
366
|
buildGoals,
|
|
367
|
+
isPendingReview,
|
|
353
368
|
markCriterion,
|
|
354
369
|
unmarkCriterion,
|
|
355
370
|
};
|
|
@@ -383,7 +383,8 @@ async function enableAutoMerge({ nodeId }, deps = {}) {
|
|
|
383
383
|
// getPublishStatus — report the PR + prod-deploy state so ship.js can poll
|
|
384
384
|
// WITHOUT gh on the box. Returns:
|
|
385
385
|
// { pr_state, merged, merge_sha, deploy_status } where
|
|
386
|
-
// pr_state ∈ open|closed|none, deploy_status ∈ pending|success|none
|
|
386
|
+
// pr_state ∈ open|closed|none, deploy_status ∈ pending|success|none|unconfigured
|
|
387
|
+
// ('unconfigured' = this repo has no deploy-prod.yml — task 1002472).
|
|
387
388
|
// Mirrors ship.js pollCiDeploy's two-stage check (PR merged -> deploy run).
|
|
388
389
|
async function getPublishStatus({ branch }, deps = {}) {
|
|
389
390
|
const token = await resolveToken(deps);
|
|
@@ -432,7 +433,17 @@ async function getPublishStatus({ branch }, deps = {}) {
|
|
|
432
433
|
(r) => r && r.head_sha === mergeSha && r.status === 'completed' && r.conclusion === 'success'
|
|
433
434
|
);
|
|
434
435
|
if (hit) deployStatus = 'success';
|
|
436
|
+
} else if (runsRes.status === 404) {
|
|
437
|
+
// task 1002472 — the workflow FILE does not exist in this repo, so there is no
|
|
438
|
+
// deploy to wait for and 'pending' would never resolve. The 404 is unambiguous:
|
|
439
|
+
// the /pulls fetch above already threw STATUS_FETCH_FAILED if the repo were
|
|
440
|
+
// unreachable, so reaching here means the repo resolved and only the workflow is
|
|
441
|
+
// missing — a permanent condition. Instances that publish by pin-bump
|
|
442
|
+
// (package-core → bongos upgrade, ADR 0108) never have a deploy-prod.yml at all;
|
|
443
|
+
// reporting 'pending' pinned every merged PR at confirmed forever.
|
|
444
|
+
deployStatus = 'unconfigured';
|
|
435
445
|
}
|
|
446
|
+
// Any other non-ok (5xx, 403, rate limit) is transient → stay 'pending' and retry.
|
|
436
447
|
} catch (_) {
|
|
437
448
|
/* leave pending on a transient runs-API miss */
|
|
438
449
|
}
|
|
@@ -652,8 +663,14 @@ async function setCommitStatus({ sha, state, context, description, targetUrl },
|
|
|
652
663
|
// publish-status route flips the task confirmed→shipped so a successful publish
|
|
653
664
|
// never strands at confirmed waiting on a human to re-poll (the #1027
|
|
654
665
|
// retrospective). Pure + exported so the route's reconcile trigger is unit-tested.
|
|
666
|
+
// task 1002472 — 'unconfigured' (no deploy-prod.yml in the repo, see getPublishStatus)
|
|
667
|
+
// counts as landed too: on an instance that publishes by pin-bump (ADR 0108) the MERGE
|
|
668
|
+
// is the land, and requiring a deploy that can never run pinned every merged PR at
|
|
669
|
+
// 'confirmed' forever. MERGED stays mandatory, and this is only the gate on the
|
|
670
|
+
// branch/PR path — landProvenForTask still has to prove the land is THIS task's.
|
|
655
671
|
function landConfirmed(status) {
|
|
656
|
-
|
|
672
|
+
if (!status || status.merged !== true) return false;
|
|
673
|
+
return status.deploy_status === 'success' || status.deploy_status === 'unconfigured';
|
|
657
674
|
}
|
|
658
675
|
|
|
659
676
|
// task 1547 — is a landConfirmed status PROVEN to be THIS task's land, and not a
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// modules/lifecycle/goal-advisory.js — the ORPHAN-TASK advisory (task 1002539).
|
|
2
|
+
//
|
|
3
|
+
// THE HOLE. Nothing errors when a task has no goal to live in. POST /tasks with no
|
|
4
|
+
// goal_id returns 201 every time: db.createTask defaults a goal-less task into its
|
|
5
|
+
// version's catch-all "<version> — general" goal, and if that goal does not exist
|
|
6
|
+
// the fallback is NULL and the task is created goal-less, silently.
|
|
7
|
+
//
|
|
8
|
+
// And for a modern version it does not exist. The catch-all rows were created ONLY
|
|
9
|
+
// by migrations/160_goal_tier.sql, by selecting over the `versions` table AT THAT
|
|
10
|
+
// MOMENT. db.createVersion does not create one and there is no trigger — so every
|
|
11
|
+
// version created after migration 160 gives each of its tasks goal_id = NULL, with
|
|
12
|
+
// no signal to anyone. A task with no goal has no scope wall, no done-when criteria
|
|
13
|
+
// gating it, and no workspace it gets reviewed in (ADR 0086 §2).
|
|
14
|
+
//
|
|
15
|
+
// This module makes that visible WITHOUT blocking anything: POST /tasks attaches
|
|
16
|
+
// the verdict to its 201 body as `goal_advisory`, the /goal-create skill picks it
|
|
17
|
+
// up, and a task that lands somewhere real gets `null` — silence.
|
|
18
|
+
//
|
|
19
|
+
// PURE + exported so every branch is pinned by a DB-free unit test (the
|
|
20
|
+
// criterion-suggest / goal-authz precedent). A module sibling, like ../db.
|
|
21
|
+
|
|
22
|
+
// The catch-all goal migration 160 created per version. ONE definition, shared with
|
|
23
|
+
// db.generalGoalIdForVersion, which used to spell it inline under a "keep the two in
|
|
24
|
+
// sync" comment. The dash is an EM DASH (U+2014) with spaces either side — an ASCII
|
|
25
|
+
// hyphen or an en dash is a different, ordinary goal and must not match.
|
|
26
|
+
function generalGoalTitle(versionId) {
|
|
27
|
+
return `${versionId} — general`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// adviseGoal — PURE, FAIL-OPEN. Returns null when the task has a real home, or the
|
|
31
|
+
// advisory body when it does not. Two reasons, checked in order:
|
|
32
|
+
//
|
|
33
|
+
// no_goal — goal_id is NULL. The version has no catch-all at all (created after
|
|
34
|
+
// migration 160), so the task is structurally orphaned.
|
|
35
|
+
// catch_all — the task landed in "<version> — general": a bucket, not a workspace.
|
|
36
|
+
// No scope wall, no criteria, no members, no review. Better than NULL,
|
|
37
|
+
// still not a home.
|
|
38
|
+
//
|
|
39
|
+
// Fail-OPEN on ambiguity (return null, say nothing) — the opposite posture from an
|
|
40
|
+
// authorization gate. This is advice appended to a request that ALREADY SUCCEEDED;
|
|
41
|
+
// a wrong guess here nags a builder about a task that is fine, which is worse than
|
|
42
|
+
// staying quiet. A missing versionId or an unreadable goal title yields null.
|
|
43
|
+
function adviseGoal({ versionId, goalId, goalTitle } = {}) {
|
|
44
|
+
if (goalId == null) {
|
|
45
|
+
return {
|
|
46
|
+
reason: 'no_goal',
|
|
47
|
+
message: 'This task has no goal to live in — no scope wall, no done-when criteria gating it, '
|
|
48
|
+
+ 'and no workspace it gets reviewed in (ADR 0086 §2). Its version has no catch-all goal, '
|
|
49
|
+
+ 'which is normal for any version created after the goal tier shipped.',
|
|
50
|
+
version_id: versionId ?? null,
|
|
51
|
+
goal_id: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (versionId == null || typeof goalTitle !== 'string') return null;
|
|
55
|
+
if (goalTitle !== generalGoalTitle(versionId)) return null;
|
|
56
|
+
return {
|
|
57
|
+
reason: 'catch_all',
|
|
58
|
+
message: `This task landed in "${goalTitle}", the version's catch-all bucket rather than a real goal — `
|
|
59
|
+
+ 'no scope wall, no criteria, and no owner reviewing it.',
|
|
60
|
+
version_id: versionId,
|
|
61
|
+
goal_id: goalId,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// candidateGoals — PURE. The version's OPEN goals minus the catch-all, shaped down
|
|
66
|
+
// to what a caller needs to choose a home. Order is preserved (listGoals already
|
|
67
|
+
// sorts by sort_order), so the caller can trust the first entries are the ranked
|
|
68
|
+
// ones. Non-array input yields [] rather than throwing — same fail-open posture.
|
|
69
|
+
function candidateGoals(goals, versionId) {
|
|
70
|
+
if (!Array.isArray(goals)) return [];
|
|
71
|
+
const catchAll = versionId == null ? null : generalGoalTitle(versionId);
|
|
72
|
+
return goals
|
|
73
|
+
.filter((g) => g && g.title !== catchAll)
|
|
74
|
+
.map((g) => ({ id: g.id, title: g.title, scope_modules: g.scope_modules || [] }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// How many alternative goals the advisory offers. Small on purpose — this is a hint
|
|
78
|
+
// for a human/agent choosing a home, not a browsable list.
|
|
79
|
+
const CANDIDATE_LIMIT = 5;
|
|
80
|
+
|
|
81
|
+
// buildGoalAdvisory — the whole advisory, DB reads included, so POST /tasks calls one
|
|
82
|
+
// line and this is directly unit-testable with a stub db (no route simulation).
|
|
83
|
+
//
|
|
84
|
+
// It swallows its own failures on purpose. The task row is ALREADY COMMITTED when
|
|
85
|
+
// this runs, so a throw would turn a created task into a 500 that the caller then
|
|
86
|
+
// retries — and source_ref idempotency only saves callers who set one, so a bare
|
|
87
|
+
// retry mints a DUPLICATE task. Degrading to null (silence) is always correct here.
|
|
88
|
+
// Only the unhappy path pays for the second query.
|
|
89
|
+
async function buildGoalAdvisory({ db, versionId, task }) {
|
|
90
|
+
try {
|
|
91
|
+
const homeGoal = task.goal_id != null ? await db.getGoal(task.goal_id) : null;
|
|
92
|
+
const verdict = adviseGoal({
|
|
93
|
+
versionId,
|
|
94
|
+
goalId: task.goal_id,
|
|
95
|
+
goalTitle: homeGoal && homeGoal.title,
|
|
96
|
+
});
|
|
97
|
+
if (!verdict) return null;
|
|
98
|
+
// LIMIT 6, not 6-in-JS: Postgres truncates instead of shipping every open goal
|
|
99
|
+
// on the version to be discarded here. Six because candidateGoals drops the
|
|
100
|
+
// catch-all, so 6 rows still guarantee the 5 candidates below.
|
|
101
|
+
const open = await db.listGoals({ versionId, status: 'open', limit: CANDIDATE_LIMIT + 1 });
|
|
102
|
+
return {
|
|
103
|
+
...verdict,
|
|
104
|
+
candidate_goals: candidateGoals(open, versionId).slice(0, CANDIDATE_LIMIT),
|
|
105
|
+
next_step: 'Run /goal-create to plan a goal for this work, or file it under an existing one: '
|
|
106
|
+
+ `PATCH /tasks/${task.id} {"goal_id": <id>}`,
|
|
107
|
+
};
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error('[gds] goal-advisory (non-blocking)', e && e.message);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// modules/lifecycle/goal-authz.js — the goal AUTHORITY MODEL, as pure functions.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from routes/goals.js (task 1002538 / ADR 0154) so a SECOND route can
|
|
4
|
+
// share it: POST /versions/:id/done-when now lets a goal's own authority author
|
|
5
|
+
// that goal's criteria, and authorization logic must have exactly one definition.
|
|
6
|
+
// routes/goals.js re-exports authorizeGoalManagement (isGoalOwner it imports for
|
|
7
|
+
// internal use only), so its existing DB-free unit tests (goal_conflicts,
|
|
8
|
+
// goal_member_roles, goal_membership_requests) are unchanged.
|
|
9
|
+
//
|
|
10
|
+
// A module sibling, like ../goal-conflicts — routes consume pure files here; no
|
|
11
|
+
// route requires another route.
|
|
12
|
+
|
|
13
|
+
// The goal AUTHORITY MODEL (task 1735). Two layers, deliberately different kinds:
|
|
14
|
+
// - OWNER — goals.created_by, the fixed authority. Not a role row: it is set at
|
|
15
|
+
// create time (the creator) and moves only via the explicit transfer route.
|
|
16
|
+
// The owner holds every goal power.
|
|
17
|
+
// - MANAGER — the goal_members.role 'lead' value (the hall renders it
|
|
18
|
+
// "manager"). Owner-equivalent EXCEPT: a manager cannot DEMOTE a fellow
|
|
19
|
+
// manager (owner-only), and cannot transfer ownership.
|
|
20
|
+
// A goal with created_by NULL (the migration-160 system defaults) has no owner;
|
|
21
|
+
// Archons act as one — the archon arm of both gates below.
|
|
22
|
+
function isGoalOwner(goal, builderId) {
|
|
23
|
+
return goal.created_by != null && String(goal.created_by) === String(builderId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// isGoalOwnerOrManager — the AUTHORITY predicate on its own, with no goal-state
|
|
27
|
+
// check in front of it. authorizeGoalManagement answers "may this actor manage
|
|
28
|
+
// this goal *right now*" (state first, so an owner gets a useful goal_not_open);
|
|
29
|
+
// a caller that must not LEAK the goal's existence needs the authority question
|
|
30
|
+
// alone, asked first. ADR 0154: POST /versions/:id/done-when uses this to mask an
|
|
31
|
+
// unauthorized caller behind the same 400 as a non-existent goal.
|
|
32
|
+
function isGoalOwnerOrManager({ goal, actorId, actorRank, actorMembership }) {
|
|
33
|
+
if (actorRank === 'archon') return true;
|
|
34
|
+
if (!goal) return false;
|
|
35
|
+
return isGoalOwner(goal, actorId) || (!!actorMembership && actorMembership.role === 'lead');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// authorizeGoalManagement — the shared ORDERED, FAIL-CLOSED prefix for actions
|
|
39
|
+
// a goal's own authority performs (task 1736: conflict resolution; the same
|
|
40
|
+
// three steps open authorizeMemberRoleChange): goal exists → goal open →
|
|
41
|
+
// actor is owner|manager|Archon. Returns { ok:true } or { ok:false, status,
|
|
42
|
+
// body }. PURE + exported, same precedent as its siblings.
|
|
43
|
+
function authorizeGoalManagement({ goal, actorId, actorRank, actorMembership }) {
|
|
44
|
+
if (!goal) {
|
|
45
|
+
return { ok: false, status: 404, body: { error: 'goal_not_found' } };
|
|
46
|
+
}
|
|
47
|
+
if (goal.status !== 'open') {
|
|
48
|
+
return { ok: false, status: 409, body: {
|
|
49
|
+
error: 'goal_not_open', status: goal.status,
|
|
50
|
+
message: 'Only an open goal can be managed.',
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
if (!isGoalOwnerOrManager({ goal, actorId, actorRank, actorMembership })) {
|
|
54
|
+
return { ok: false, status: 403, body: {
|
|
55
|
+
error: 'not_owner_or_manager',
|
|
56
|
+
message: 'Only this goal\'s owner or a manager (or an Archon) may do this.',
|
|
57
|
+
} };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { isGoalOwner, isGoalOwnerOrManager, authorizeGoalManagement };
|
|
@@ -48,6 +48,10 @@ module.exports = {
|
|
|
48
48
|
recentGradesForBuilder: (...a) => db.recentGradesForBuilder(...a),
|
|
49
49
|
countShippedTasksForBuilder: (...a) => db.countShippedTasksForBuilder(...a),
|
|
50
50
|
recentShippedTasksByBuilder: (...a) => db.recentShippedTasksByBuilder(...a),
|
|
51
|
+
// ADR 0120 part 5: the confirmed tasks a builder owes a rebase on — the same
|
|
52
|
+
// read the part-3 claim gate uses, surfaced to me.js so the CLI + hall can
|
|
53
|
+
// show the owed-rebase warning that gates new claims (reward lands on ship).
|
|
54
|
+
needsRebaseTasksForBuilder: (...a) => db.needsRebaseTasksForBuilder(...a),
|
|
51
55
|
// builders.js (the Archon roster): the roster + stale-claim + per-builder grade.
|
|
52
56
|
getBuildersRoster: (...a) => db.getBuildersRoster(...a),
|
|
53
57
|
getActiveClaimsForBuilderWithStale: (...a) => db.getActiveClaimsForBuilderWithStale(...a),
|
|
@@ -55,4 +59,9 @@ module.exports = {
|
|
|
55
59
|
// analytics.js (the AI-dev-curve): the builder + ecosystem analytics.
|
|
56
60
|
builderAnalytics: (...a) => db.builderAnalytics(...a),
|
|
57
61
|
ecosystemAnalytics: (...a) => db.ecosystemAnalytics(...a),
|
|
62
|
+
// dev-box (modules/dev-box/routes/box.js, via resolveOptional('lifecycle')): the
|
|
63
|
+
// module-key scope of a builder's active claims — the task-scoped source-access
|
|
64
|
+
// resolver (goal 1000051 / R92, ADR 0148 §2). Keys only; box-access maps them to
|
|
65
|
+
// git sparse-checkout globs through the doorway `moduleScopeMap`.
|
|
66
|
+
moduleScopeForActiveClaims: (...a) => db.moduleScopeForActiveClaims(...a),
|
|
58
67
|
};
|
|
@@ -72,6 +72,12 @@ const healAttempts = new Map(); // branch -> regen-heal attempts this process
|
|
|
72
72
|
|
|
73
73
|
let intervalTimer = null;
|
|
74
74
|
let firstTimer = null;
|
|
75
|
+
// task 1002461: the no-credential skip is WARN'd once per unconfigured streak, not
|
|
76
|
+
// once per tick — a 5-min poller would otherwise bury the journal. Re-armed by any
|
|
77
|
+
// sweep that proceeds, so a credential LOSS after a healthy period is reported.
|
|
78
|
+
// Injectable (deps.warnState) for the same reason healAttempts is: the tests in this
|
|
79
|
+
// suite run concurrently, so a shared module latch would leak across them.
|
|
80
|
+
const warnState = { unconfigured: false };
|
|
75
81
|
|
|
76
82
|
// task 1915 — is this mergeIfGreen result the "mergeable but a check is RED" shape
|
|
77
83
|
// whose commonest cause is a STALE generated index (see refreshStaleGenerated)? Only
|
|
@@ -86,6 +92,32 @@ function couldBeStaleGenerated(m) {
|
|
|
86
92
|
return /^failing_check:/.test(reason) || /^non_required_check_failing:/.test(reason);
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
// task 1002461 — render the reconciler's operating target: which repo it resolved
|
|
96
|
+
// and whether it holds a credential. This is the line that makes a WRONG repo bind
|
|
97
|
+
// visible without owner access (the live failure: the hub bound
|
|
98
|
+
// cloudbongos/cloudbongos-instance while the PRs were in lars589/cloud-bongos).
|
|
99
|
+
// Pure — takes the resolved facts, returns the string — so the wording is
|
|
100
|
+
// unit-tested without a boot.
|
|
101
|
+
function describeTarget({ configured, repo } = {}) {
|
|
102
|
+
const cred = configured ? 'credential present' : 'NO PUSH CREDENTIAL';
|
|
103
|
+
if (!repo || repo.error) {
|
|
104
|
+
return `target: UNRESOLVED (${(repo && repo.detail) || (repo && repo.error) || 'no repo info'}) · ${cred}`;
|
|
105
|
+
}
|
|
106
|
+
return `target: ${repo.owner}/${repo.repo} (branch ${repo.branch || 'main'}) · ${cred}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Resolve the repo the reconciler would act on, never throwing — an observability
|
|
110
|
+
// helper must not be able to break the sweep it is describing. `deps.repoInfo` is
|
|
111
|
+
// the test seam.
|
|
112
|
+
function resolveTarget(deps = {}) {
|
|
113
|
+
if (deps.repoInfo) return deps.repoInfo;
|
|
114
|
+
try {
|
|
115
|
+
return seams.repoInfo.loadRepoInfo();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return { error: 'lookup_failed', detail: e && e.message };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
89
121
|
// One sweep. Pure w.r.t. injected deps (db / githubPush / emit / loggers)
|
|
90
122
|
// so the whole thing is unit-tested with no DB and no network. Returns a summary
|
|
91
123
|
// { skipped?, checked, reconciled, errors } for observability + tests.
|
|
@@ -99,9 +131,23 @@ async function runSweep(deps = {}) {
|
|
|
99
131
|
|
|
100
132
|
// No server push credential (local dev, tests, an un-provisioned host) → there
|
|
101
133
|
// is nothing to reconcile against, and getPublishStatus would 503. No-op.
|
|
134
|
+
//
|
|
135
|
+
// task 1002461: this used to return SILENTLY while start() logged "started —
|
|
136
|
+
// sweeping confirmed→shipped every Ns", so the journal showed a healthy poller
|
|
137
|
+
// that could never land anything — the stranding went unnoticed for 6+ days. Say
|
|
138
|
+
// it, once per unconfigured streak, and name the repo so a wrong bind shows up in
|
|
139
|
+
// the same line.
|
|
140
|
+
const warn = deps.warnState || warnState;
|
|
102
141
|
if (!push.isConfigured()) {
|
|
142
|
+
if (!warn.unconfigured) {
|
|
143
|
+
warn.unconfigured = true;
|
|
144
|
+
errFn(
|
|
145
|
+
`[publish-reconciler] WARN ${describeTarget({ configured: false, repo: resolveTarget(deps) })} — confirmed→shipped can NEVER land until a credential is set (GITHUB_APP_ID + GITHUB_APP_INSTALLATION_ID + GITHUB_APP_PRIVATE_KEY, or GITHUB_PUSH_TOKEN). Subsequent sweeps stay quiet until this changes.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
103
148
|
return { skipped: 'unconfigured', checked: 0, reconciled: 0, merged: 0, resolvedConflicts: 0, errors: 0 };
|
|
104
149
|
}
|
|
150
|
+
warn.unconfigured = false; // re-arm: a later credential LOSS must warn again
|
|
105
151
|
|
|
106
152
|
let rows;
|
|
107
153
|
try {
|
|
@@ -146,6 +192,10 @@ async function runSweep(deps = {}) {
|
|
|
146
192
|
let landed = false;
|
|
147
193
|
let landNote = '';
|
|
148
194
|
let branchStatus = null;
|
|
195
|
+
// task 1002472 — set when a PR exists but has not fully landed. Suppresses the
|
|
196
|
+
// land proof (an open PR must never ship) WITHOUT suppressing the observability
|
|
197
|
+
// below, which the old bare `continue` did.
|
|
198
|
+
let prNotLanded = false;
|
|
149
199
|
|
|
150
200
|
if (branchUsable) {
|
|
151
201
|
// Active merge driver (task 1162): GitHub's auto-merge fires on check-completion
|
|
@@ -209,6 +259,17 @@ async function runSweep(deps = {}) {
|
|
|
209
259
|
logFn(
|
|
210
260
|
`[publish-reconciler] task ${r.task_id} (${branch}) PR #${m.number} still red after ${n} regen-heal attempt(s) (${m.reason}) — capped (persistent staleness treadmill or a real check failure); left for a human`
|
|
211
261
|
);
|
|
262
|
+
// ADR 0120 part 2: the auto-land grace (retries + ADR 0082 self-heal)
|
|
263
|
+
// is exhausted and the PR is still red — a real check failure or a
|
|
264
|
+
// hand-written conflict only the builder can resolve. Flag needs_rebase
|
|
265
|
+
// so their queue is gated until they fix + re-land (or release it).
|
|
266
|
+
try {
|
|
267
|
+
if (await db.flagTaskNeedsRebase(r.task_id, `failing_check_capped:${m.reason}`)) {
|
|
268
|
+
logFn(`[publish-reconciler] task ${r.task_id} flagged needs_rebase (regen-heal cap) — its builder must fix + re-land`);
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
errFn(`[publish-reconciler] flagNeedsRebase (cap) task ${r.task_id} (non-blocking): ${e && e.message}`);
|
|
272
|
+
}
|
|
212
273
|
} else {
|
|
213
274
|
try {
|
|
214
275
|
const rr = await resolver.refreshStaleGenerated({ branch });
|
|
@@ -247,7 +308,14 @@ async function runSweep(deps = {}) {
|
|
|
247
308
|
if (!push.landConfirmed(status) && status && status.pr_state !== 'none') {
|
|
248
309
|
// A PR exists but hasn't fully landed yet (open / merged-but-deploy-pending /
|
|
249
310
|
// blocked). Respect it — wait for the deploy — rather than risk shipping early.
|
|
250
|
-
|
|
311
|
+
//
|
|
312
|
+
// task 1002472: this used to be a bare `continue`, which jumped PAST the
|
|
313
|
+
// unprovable counter, the STALE WARN and flagTaskNeedsRebase below — so this
|
|
314
|
+
// strand shape was completely invisible (tasks 1002427/1002428 sat 50h at
|
|
315
|
+
// 'confirmed' reporting needs_rebase:false). Flag it instead and fall through
|
|
316
|
+
// to the observability block. The flag still suppresses the land proof, so an
|
|
317
|
+
// un-landed PR is no more shippable than it was.
|
|
318
|
+
prNotLanded = true;
|
|
251
319
|
}
|
|
252
320
|
branchStatus = status;
|
|
253
321
|
}
|
|
@@ -262,7 +330,7 @@ async function runSweep(deps = {}) {
|
|
|
262
330
|
// references this task id. landProvenForTask still cures the task-1313 strand class
|
|
263
331
|
// (PR deleted post-merge / no worktree_name / ambiguous branch) — now keyed on the
|
|
264
332
|
// unambiguous published tip rather than the base.
|
|
265
|
-
if (!landed && typeof push.landProvenForTask === 'function') {
|
|
333
|
+
if (!landed && !prNotLanded && typeof push.landProvenForTask === 'function') {
|
|
266
334
|
try {
|
|
267
335
|
const proof = await push.landProvenForTask({
|
|
268
336
|
taskId: r.task_id, publishedSha: r.published_head_sha, status: branchStatus,
|
|
@@ -284,12 +352,31 @@ async function runSweep(deps = {}) {
|
|
|
284
352
|
// (codeless confirm, or a land whose published tip was never recorded on the
|
|
285
353
|
// claim) now surfaces in the logs instead of stranding silently.
|
|
286
354
|
unprovable++;
|
|
287
|
-
|
|
355
|
+
// task 1002472: 'pr_not_landed' distinguishes "a PR exists and we are waiting on
|
|
356
|
+
// it" from "we looked for proof and found none" — the two need different
|
|
357
|
+
// remediation (wait/unblock the PR vs. push or rebase the branch).
|
|
358
|
+
const reason = prNotLanded
|
|
359
|
+
? 'pr_not_landed'
|
|
360
|
+
: (!branch && !r.published_head_sha) ? 'no_branch_no_tip' : 'land_not_proven';
|
|
288
361
|
const ageMs = r.confirmed_at ? (nowMs - new Date(r.confirmed_at).getTime()) : null;
|
|
289
362
|
if (ageMs != null && ageMs > STALE_CONFIRMED_MS) {
|
|
290
363
|
staleStrands++;
|
|
364
|
+
// ADR 0120 part 2: an aged-out, unprovable confirmed task is a strand
|
|
365
|
+
// whose land its BUILDER must complete — rebase a branch gone stale after
|
|
366
|
+
// a big codebase change, or push a branch that only ever lived on their
|
|
367
|
+
// machine (the `no_branch_no_tip` case). Flag needs_rebase (attributed via
|
|
368
|
+
// shipped_by = the claim holder); the claim gate then blocks that builder's
|
|
369
|
+
// NEW claims until they land or release it. Since the reward now lands only
|
|
370
|
+
// on ship (part 1), they are motivated to clear it. NOTE it is no longer
|
|
371
|
+
// "credited" — part 1 moved the payout from confirm to the land.
|
|
372
|
+
let flagged = false;
|
|
373
|
+
try {
|
|
374
|
+
flagged = await db.flagTaskNeedsRebase(r.task_id, `strand:${reason}`);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
errFn(`[publish-reconciler] flagNeedsRebase (strand) task ${r.task_id} (non-blocking): ${e && e.message}`);
|
|
377
|
+
}
|
|
291
378
|
errFn(
|
|
292
|
-
`[publish-reconciler] STRAND task ${r.task_id}: confirmed ~${Math.round(ageMs / 3_600_000)}h ago, unprovable (${reason}) —
|
|
379
|
+
`[publish-reconciler] STRAND task ${r.task_id}: confirmed ~${Math.round(ageMs / 3_600_000)}h ago, unprovable (${reason}) — no landed proof; ${flagged ? 'flagged needs_rebase for its builder' : 'needs_rebase already set'}`
|
|
293
380
|
);
|
|
294
381
|
}
|
|
295
382
|
continue;
|
|
@@ -399,8 +486,16 @@ function start(deps = {}) {
|
|
|
399
486
|
intervalTimer = setInterval(tick, INTERVAL_MS);
|
|
400
487
|
if (intervalTimer.unref) intervalTimer.unref();
|
|
401
488
|
|
|
489
|
+
// task 1002461: the bare "started" line was actively misleading — it printed the
|
|
490
|
+
// same whether the poller could work or not. Always state the resolved target +
|
|
491
|
+
// credential alongside it, so a wrong repo bind or a missing credential is
|
|
492
|
+
// visible at boot instead of needing a simulation to prove. loadRepoInfo caches
|
|
493
|
+
// on first call, so this is also the value every later sweep uses (an env change
|
|
494
|
+
// needs a service restart to take effect).
|
|
495
|
+
const push = deps.githubPush || githubPush; // same resolution runSweep uses
|
|
496
|
+
const target = describeTarget({ configured: push.isConfigured(), repo: resolveTarget(deps) });
|
|
402
497
|
console.log(
|
|
403
|
-
`[publish-reconciler] started — sweeping confirmed→shipped every ${Math.round(INTERVAL_MS / 1000)}s`
|
|
498
|
+
`[publish-reconciler] started — sweeping confirmed→shipped every ${Math.round(INTERVAL_MS / 1000)}s · ${target}`
|
|
404
499
|
);
|
|
405
500
|
}
|
|
406
501
|
|
|
@@ -415,4 +510,12 @@ function stop() {
|
|
|
415
510
|
}
|
|
416
511
|
}
|
|
417
512
|
|
|
418
|
-
module.exports = {
|
|
513
|
+
module.exports = {
|
|
514
|
+
start,
|
|
515
|
+
stop,
|
|
516
|
+
runSweep,
|
|
517
|
+
runCostSweep,
|
|
518
|
+
couldBeStaleGenerated,
|
|
519
|
+
describeTarget,
|
|
520
|
+
INTERVAL_MS,
|
|
521
|
+
};
|
|
@@ -41,28 +41,38 @@ function taskNotReadyHint(taskId, currentStatus) {
|
|
|
41
41
|
: `Task #${idNum} is '${currentStatus}', not 'ready' — only 'ready' tasks are claimable (promote applies to 'backlog': POST /api/bongos/tasks/${idNum}/promote).`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// task 1642 — the pure
|
|
45
|
-
// tasks from one
|
|
46
|
-
//
|
|
47
|
-
// drags the other's commits with it (commingled ships). The structural cure:
|
|
48
|
-
//
|
|
44
|
+
// task 1642 (+ the worktree-keying fix) — the pure one-active-claim decision.
|
|
45
|
+
// Working two tasks from one WORKING TREE means every commit lands on the single
|
|
46
|
+
// branch that tree owns; /builder-ship publishes the whole branch, so shipping one
|
|
47
|
+
// task drags the other's commits with it (commingled ships). The structural cure:
|
|
48
|
+
// a working tree may hold at most ONE active claim at a time.
|
|
49
49
|
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
50
|
+
// The hazard boundary is the WORKING TREE (one worktree = one branch = one HEAD),
|
|
51
|
+
// NOT the auth session. Originally this keyed on creator_session_id — but that is
|
|
52
|
+
// the AUTH-TOKEN session, which a builder shares across EVERY concurrent worktree
|
|
53
|
+
// on one login (the CLI token lives in ~/.config, not per-worktree). So the guard
|
|
54
|
+
// wrongly blocked the second of two genuinely-parallel worktrees — contradicting
|
|
55
|
+
// its own stated intent ("a builder may run many sessions in parallel, each with
|
|
56
|
+
// its own worktree and exactly one claim; that norm must keep working"). The fix:
|
|
57
|
+
// scope refusal to the SAME working tree. Two claims can coexist for one auth
|
|
58
|
+
// session AS LONG AS they land in different worktrees; a second claim into the
|
|
59
|
+
// SAME tree (or a second worktree-less claim off one plain clone, where the single
|
|
60
|
+
// branch really would commingle) is still refused.
|
|
54
61
|
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
+
// `worktreeName` is the incoming claim's worktree (claims.worktree_name — the CLI
|
|
63
|
+
// sends it; the server enforces its cross-active-claim uniqueness, V3.R51/#270).
|
|
64
|
+
// Two trees are "the same" iff their worktree_names are equal, OR both are null
|
|
65
|
+
// (no worktree info → assume one shared clone → commingle risk).
|
|
66
|
+
//
|
|
67
|
+
// NULL-session handling is unchanged: a web claim (bind_session=false) stores
|
|
68
|
+
// creator_session_id=NULL and is adoptable by any of the builder's CLI sessions —
|
|
69
|
+
// the incoming caller passes creatorSessionId=null and we allow (nothing to scope).
|
|
70
|
+
// The DB helper filters activeClaimsForSession to the SAME non-null session, so a
|
|
71
|
+
// pile of NULL-session web claims can never count against a CLI session either.
|
|
62
72
|
//
|
|
63
73
|
// Pure + exported so the decision is unit-testable without a DB or a route.
|
|
64
74
|
// Returns { refuse, inFlightTaskId, inFlightTitle, reason }.
|
|
65
|
-
function shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId }) {
|
|
75
|
+
function shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId, worktreeName = null }) {
|
|
66
76
|
// No session to scope to (web/unbound claim, or a CLI caller with no session):
|
|
67
77
|
// there is nothing this guard can key on — let it through. The per-task unique
|
|
68
78
|
// index and the newcomer gate still apply.
|
|
@@ -70,10 +80,19 @@ function shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId })
|
|
|
70
80
|
return { refuse: false, inFlightTaskId: null, inFlightTitle: null, reason: null };
|
|
71
81
|
}
|
|
72
82
|
const claims = Array.isArray(activeClaimsForSession) ? activeClaimsForSession : [];
|
|
73
|
-
|
|
74
|
-
//
|
|
83
|
+
const incomingTree = worktreeName == null ? null : worktreeName;
|
|
84
|
+
// Two claims share a WORKING TREE iff their worktree_names match, or both are
|
|
85
|
+
// null (a worktree-less clone — the single branch genuinely commingles).
|
|
86
|
+
const sameWorkingTree = (name) => {
|
|
87
|
+
const t = name == null ? null : name;
|
|
88
|
+
return t === incomingTree;
|
|
89
|
+
};
|
|
90
|
+
// Defence in depth: only count claims that belong to THIS session, are genuinely
|
|
91
|
+
// session-bound (a NULL-session row must never trip the block), AND would land in
|
|
92
|
+
// the SAME working tree as the incoming claim.
|
|
75
93
|
const inFlight = claims.find(
|
|
76
94
|
(c) => c && c.creator_session_id != null && c.creator_session_id === creatorSessionId
|
|
95
|
+
&& sameWorkingTree(c.worktree_name ?? null)
|
|
77
96
|
);
|
|
78
97
|
if (inFlight) {
|
|
79
98
|
return {
|
|
@@ -100,18 +119,23 @@ async function gateOneActiveClaimPerSession(req, res, next) {
|
|
|
100
119
|
// never do a needless lookup and never block a web claim.
|
|
101
120
|
const creatorSessionId = resolveCreatorSessionId(req.body?.bind_session, req.gdsSession?.session_id);
|
|
102
121
|
if (!creatorSessionId) return next();
|
|
122
|
+
// The incoming claim's working tree — the hazard boundary the guard keys on. A
|
|
123
|
+
// second claim in a DIFFERENT worktree is safe (different branch); only a second
|
|
124
|
+
// claim into the SAME tree (or off one worktree-less clone) commingles.
|
|
125
|
+
const worktreeName = req.body?.worktree_name ?? null;
|
|
103
126
|
let activeClaimsForSession;
|
|
104
127
|
try {
|
|
105
128
|
activeClaimsForSession = await db.getActiveClaimsForSession(creatorSessionId);
|
|
106
129
|
} catch (err) {
|
|
107
130
|
// FAIL-CLOSED. A lookup failure must not let a commingling second claim land.
|
|
108
131
|
console.error('[gds] gateOneActiveClaimPerSession lookup failed (failing closed):', err);
|
|
109
|
-
return res.fail('session_claim_check_failed', { status: 409, message: 'Could not verify this
|
|
132
|
+
return res.fail('session_claim_check_failed', { status: 409, message: 'Could not verify this worktree has no other claim in flight. Try again, or start a fresh worktree for this task.' });
|
|
110
133
|
}
|
|
111
|
-
const decision = shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId });
|
|
134
|
+
const decision = shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId, worktreeName });
|
|
112
135
|
if (decision.refuse) {
|
|
113
136
|
const n = decision.inFlightTaskId;
|
|
114
|
-
|
|
137
|
+
const where = worktreeName ? `worktree "${worktreeName}"` : 'this worktree';
|
|
138
|
+
return res.fail('session_already_has_active_claim', { status: 409, message: `${where} already has task ${n} in flight — ship or release it first, or claim this one from a different worktree.`, details: { active_task_id: n, active_task_title: decision.inFlightTitle, worktree_name: worktreeName, what_to_do: `Run /builder-ship ${n} (or /builder-release ${n}) before claiming new work in ${where}, or open a separate worktree for this task (each worktree may hold one claim).` } });
|
|
115
139
|
}
|
|
116
140
|
next();
|
|
117
141
|
}
|
|
@@ -206,7 +230,18 @@ module.exports = function buildClaimsRouter() {
|
|
|
206
230
|
// silently disambiguated it (e.g. "foo" → "foo-2") and the claim still
|
|
207
231
|
// succeeded — so the caller must read claim.worktree_name back, not assume
|
|
208
232
|
// it got the name it asked for.
|
|
209
|
-
|
|
233
|
+
// ADR 0146 / task 1002469: ride the instance's resolved reward policy along
|
|
234
|
+
// with the claim so the CLI banner can label `task.credits_reward` honestly
|
|
235
|
+
// in the same breath it advertises it. A cost-plus-only instance never pays
|
|
236
|
+
// that estimate, and the claim banner is where the expectation forms.
|
|
237
|
+
// Resolved by economy (never re-derived); null when economy is disabled.
|
|
238
|
+
let rewardMode = null;
|
|
239
|
+
try {
|
|
240
|
+
const rw = seams.resolveOptional('reward');
|
|
241
|
+
const p = rw && typeof rw.rewardConfig === 'function' ? rw.rewardConfig() : null;
|
|
242
|
+
if (p && typeof p.mode === 'string') rewardMode = { mode: p.mode, perTaskCredit: p.perTaskCredit !== false };
|
|
243
|
+
} catch (_) { /* display-only: a claim must never fail over this */ }
|
|
244
|
+
res.status(201).json({ claim, task, just_unlocked: justUnlocked ?? [], overlap_warning: overlapWarning ?? null, reward: rewardMode });
|
|
210
245
|
} catch (err) {
|
|
211
246
|
if (err && err.code) {
|
|
212
247
|
const status =
|
|
@@ -215,6 +250,10 @@ module.exports = function buildClaimsRouter() {
|
|
|
215
250
|
err.code === 'TOUCHES_CONFLICT' ? 409 :
|
|
216
251
|
err.code === 'MODULE_CONFLICT' ? 409 :
|
|
217
252
|
err.code === 'DEPS_NOT_SHIPPED' ? 409 :
|
|
253
|
+
// ADR 0120 part 3: the builder owes a rebase on an un-landed confirmed
|
|
254
|
+
// task (reconciler-flagged needs_rebase); NEW claims are gated until
|
|
255
|
+
// they land or release it. Existing claims are unaffected.
|
|
256
|
+
err.code === 'REBASE_REQUIRED' ? 409 :
|
|
218
257
|
// task 1670 / ADR 0096: an unrewarded (0/NULL credits_reward) task can't
|
|
219
258
|
// be claimed — the claim-time backstop to the promote gate. Grandfathered
|
|
220
259
|
// tasks (already 'ready' at migration 177) are exempt and never reach here.
|
|
@@ -236,6 +275,12 @@ module.exports = function buildClaimsRouter() {
|
|
|
236
275
|
if (err.code === 'TASK_NOT_READY') {
|
|
237
276
|
err.hint = taskNotReadyHint(taskId, err.currentStatus);
|
|
238
277
|
}
|
|
278
|
+
// ADR 0120 part 3: name the offending task(s) + the two ways to clear the
|
|
279
|
+
// gate, so the builder isn't left guessing why their claim was refused.
|
|
280
|
+
if (err.code === 'REBASE_REQUIRED') {
|
|
281
|
+
const ids = (err.rebaseTasks || []).map((t) => `task ${t.id}`);
|
|
282
|
+
err.hint = `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Land each (rebase its branch + re-ship, or /merge-mode) — or /builder-release it — then claim new work.`;
|
|
283
|
+
}
|
|
239
284
|
return res.fail(err.code, status, { ...err });
|
|
240
285
|
}
|
|
241
286
|
console.error('[gds] POST /claims', err);
|