@bongos/core 1.17.0
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 +5727 -0
- package/.claude/skills/blocker-review/SKILL.md +91 -0
- package/.claude/skills/blocker-solve/SKILL.md +96 -0
- package/.claude/skills/builder-backup/SKILL.md +54 -0
- package/.claude/skills/builder-box/SKILL.md +45 -0
- package/.claude/skills/builder-claim/SKILL.md +76 -0
- package/.claude/skills/builder-connect/SKILL.md +59 -0
- package/.claude/skills/builder-cost/SKILL.md +47 -0
- package/.claude/skills/builder-end/SKILL.md +81 -0
- package/.claude/skills/builder-exit/SKILL.md +88 -0
- package/.claude/skills/builder-key/SKILL.md +40 -0
- package/.claude/skills/builder-reauth/SKILL.md +50 -0
- package/.claude/skills/builder-redteam/SKILL.md +109 -0
- package/.claude/skills/builder-release/SKILL.md +41 -0
- package/.claude/skills/builder-setup/SKILL.md +62 -0
- package/.claude/skills/builder-ship/SKILL.md +79 -0
- package/.claude/skills/builder-stage/SKILL.md +51 -0
- package/.claude/skills/builder-start/SKILL.md +46 -0
- package/.claude/skills/close-box/SKILL.md +43 -0
- package/.claude/skills/design-sync/SKILL.md +69 -0
- package/.claude/skills/dev/SKILL.md +49 -0
- package/.claude/skills/feedback/SKILL.md +32 -0
- package/.claude/skills/figma-design-sync/SKILL.md +74 -0
- package/.claude/skills/goal-review/SKILL.md +89 -0
- package/.claude/skills/idea-triage/SKILL.md +144 -0
- package/.claude/skills/ideate/SKILL.md +82 -0
- package/.claude/skills/merge-mode/SKILL.md +169 -0
- package/.claude/skills/new-project/SKILL.md +108 -0
- package/.claude/skills/otb-character-review/SKILL.md +71 -0
- package/.claude/skills/otb-design-review/SKILL.md +47 -0
- package/.claude/skills/otb-feedback-capture/SKILL.md +45 -0
- package/.claude/skills/otb-figma-sync/SKILL.md +54 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +45 -0
- package/.claude/skills/paint/SKILL.md +70 -0
- package/.claude/skills/planning-session/SKILL.md +299 -0
- package/.claude/skills/priority-session/SKILL.md +134 -0
- package/.claude/skills/read-session-export/SKILL.md +58 -0
- package/.claude/skills/recall/SKILL.md +52 -0
- package/.claude/skills/session-handoff/SKILL.md +46 -0
- package/.claude/skills/status/SKILL.md +33 -0
- package/.devcontainer/Dockerfile +69 -0
- package/.devcontainer/README.md +73 -0
- package/.devcontainer/devcontainer-lock.json +19 -0
- package/.devcontainer/devcontainer.json +93 -0
- package/.devcontainer/healthcheck.sh +84 -0
- package/.devcontainer/init-firewall.sh +198 -0
- package/.devcontainer/post-create.sh +153 -0
- package/.dockerignore +36 -0
- package/.env.local.example +32 -0
- package/.gitattributes +83 -0
- package/.gitignore +91 -0
- package/CLAUDE.md +242 -0
- package/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +51 -0
- package/Dockerfile +46 -0
- package/GOVERNANCE.md +44 -0
- package/LICENSE +661 -0
- package/README.md +88 -0
- package/bin/bongos.js +222 -0
- package/clients/bongos-client/README.md +48 -0
- package/clients/bongos-client/bongos-client.global.js +617 -0
- package/clients/bongos-client/examples/hello-world.mjs +62 -0
- package/clients/bongos-client/index.cjs +619 -0
- package/clients/bongos-client/index.d.ts +793 -0
- package/clients/bongos-client/index.mjs +614 -0
- package/clients/bongos-client/package.json +32 -0
- package/config/branding.neutral.json +60 -0
- package/config/design-tokens.neutral.json +51 -0
- package/config/design-tokens.schema.json +96 -0
- package/config/modules.neutral.json +9 -0
- package/docker-compose.yml +52 -0
- package/docker-entrypoint.sh +30 -0
- package/docs/adr/0001-stack-choice-single-vps.md +45 -0
- package/docs/adr/0002-digitalocean-over-hetzner.md +27 -0
- package/docs/adr/0003-cloudflare-origin-cert-over-letsencrypt.md +35 -0
- package/docs/adr/0004-amazonprimea-name-and-trademark-acceptance.md +45 -0
- package/docs/adr/0005-invisible-other-players-in-v1.md +37 -0
- package/docs/adr/0006-singleton-worldroom-plus-queueroom.md +30 -0
- package/docs/adr/0007-persistent-identity-via-localstorage-uuid.md +31 -0
- package/docs/adr/0008-family-based-tile-generation.md +86 -0
- package/docs/adr/0008-google-chat-oauth-user-auth.md +37 -0
- package/docs/adr/0009-autotile-architecture.md +181 -0
- package/docs/adr/0010-pms-architecture.md +70 -0
- package/docs/adr/0011-terrain-id-rendering-model.md +174 -0
- package/docs/adr/0012-ground-object-cell-split.md +208 -0
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +158 -0
- package/docs/adr/0014-families-canonical-over-vocabulary.md +103 -0
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +125 -0
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +132 -0
- package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +74 -0
- package/docs/adr/0018-three-rank-model-goes-live.md +97 -0
- package/docs/adr/0019-red-team-bounty-payout-table.md +52 -0
- package/docs/adr/0020-security-model-public-stub.md +70 -0
- package/docs/adr/0021-per-builder-skill-model-preferences.md +99 -0
- package/docs/adr/0022-secrets-policy.md +124 -0
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +118 -0
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +277 -0
- package/docs/adr/0024-multi-agent-system-architecture.md +125 -0
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +59 -0
- package/docs/adr/0025-structured-criterion-task-link.md +44 -0
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +117 -0
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +170 -0
- package/docs/adr/0028-honest-llm-spend-accounting.md +109 -0
- package/docs/adr/0029-status-page-outage-resilience.md +59 -0
- package/docs/adr/0030-builders-hall-live-updates.md +59 -0
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +222 -0
- package/docs/adr/0032-discord-mirror-of-gds.md +50 -0
- package/docs/adr/0033-discord-bot-service-principal.md +82 -0
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +53 -0
- package/docs/adr/0035-builder-onboarding-three-paths.md +128 -0
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +40 -0
- package/docs/adr/0037-discord-channel-management.md +51 -0
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +111 -0
- package/docs/adr/0039-setup-first-onboarding-ux.md +49 -0
- package/docs/adr/0040-remote-control-default-browser-onramp.md +47 -0
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +40 -0
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +82 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +74 -0
- package/docs/adr/0044-mediterranean-palette-replacement.md +73 -0
- package/docs/adr/0044-per-box-live-game-preview.md +57 -0
- package/docs/adr/0045-devbox-desktop-app.md +133 -0
- package/docs/adr/0046-sandbox-first-review-gate.md +72 -0
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +36 -0
- package/docs/adr/0048-works-in-progress-ia-split.md +32 -0
- package/docs/adr/0049-split-parallel-safety-contract.md +46 -0
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +43 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +57 -0
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +70 -0
- package/docs/adr/0053-scoped-dev-box-session.md +55 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +63 -0
- package/docs/adr/0055-server-mediated-branch-publish.md +47 -0
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +62 -0
- package/docs/adr/0057-container-cost-ledger.md +28 -0
- package/docs/adr/0057-discord-archon-approval-channels.md +64 -0
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +64 -0
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +37 -0
- package/docs/adr/0060-gds-retrieval-layer.md +97 -0
- package/docs/adr/0061-context-layer-decomposition.md +61 -0
- package/docs/adr/0062-medusa-instance-model-decoupling.md +107 -0
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +44 -0
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +49 -0
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +69 -0
- package/docs/adr/0066-generated-file-map-sections.md +51 -0
- package/docs/adr/0067-session-start-fast-forward-pull.md +43 -0
- package/docs/adr/0068-shared-art-key-for-newcomers.md +38 -0
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +35 -0
- package/docs/adr/0070-deterministic-session-card-delivery.md +44 -0
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +59 -0
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +35 -0
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +39 -0
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +64 -0
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +37 -0
- package/docs/adr/0074-memory-map-stays-light.md +32 -0
- package/docs/adr/0075-per-system-cost-attribution.md +37 -0
- package/docs/adr/0076-in-session-deterministic-compaction.md +58 -0
- package/docs/adr/0077-content-addressed-llm-cache.md +32 -0
- package/docs/adr/0078-deterministic-first-cron.md +48 -0
- package/docs/adr/0079-value-gated-autonomous-engagement.md +39 -0
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +46 -0
- package/docs/adr/0081-tool-agnostic-design-layer.md +62 -0
- package/docs/adr/0082-productized-key-storage-provisioning.md +32 -0
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +55 -0
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +143 -0
- package/docs/adr/0084-auto-derived-per-task-rank-floor.md +86 -0
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +39 -0
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +123 -0
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +49 -0
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +65 -0
- package/docs/adr/0088-consistent-character-generative-animation.md +169 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +179 -0
- package/docs/adr/0090-metic-task-authoring.md +73 -0
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +106 -0
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +117 -0
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +72 -0
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +49 -0
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +65 -0
- package/docs/adr/0095-cross-agent-context-management.md +58 -0
- package/docs/adr/0096-require-reward-before-workable.md +209 -0
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +46 -0
- package/docs/adr/0097-retroactive-reward-backfill.md +124 -0
- package/docs/adr/0098-public-mirror-publish-manifest.md +92 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +55 -0
- package/docs/adr/0100-two-repo-instance-model.md +60 -0
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +90 -0
- package/docs/adr/0102-finish-bounding-the-kernel.md +36 -0
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +70 -0
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +28 -0
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +42 -0
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +47 -0
- package/docs/adr/0106-goal-owner-manager-authority.md +84 -0
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +83 -0
- package/docs/adr/0108-instance-composition-configurable-root.md +87 -0
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +44 -0
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +49 -0
- package/docs/adr/0111-instance-hosting-provisioning-module.md +135 -0
- package/docs/adr/0112-private-goals-invitations-join-requests.md +72 -0
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +41 -0
- package/docs/adr/0114-instance-docs-discovery-contract.md +32 -0
- package/docs/adr/0115-scheduled-routines-portability.md +31 -0
- package/docs/adr/0116-api-error-envelope-and-code-taxonomy.md +62 -0
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +50 -0
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +47 -0
- package/docs/adr/0119-api-pagination-contract.md +34 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +108 -0
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +97 -0
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +43 -0
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +36 -0
- package/docs/adr/0124-typed-api-response-bodies.md +37 -0
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +36 -0
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +30 -0
- package/docs/adr/0127-cross-box-fleet-observability.md +64 -0
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +46 -0
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +34 -0
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +61 -0
- package/docs/adr/0131-rank-scoped-skill-visibility.md +47 -0
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +54 -0
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +42 -0
- package/docs/adr/0134-private-first-npm-distribution.md +38 -0
- package/docs/adr/README.md +208 -0
- package/docs/api/openapi.json +13720 -0
- package/docs/api-reference.md +437 -0
- package/docs/architecture.md +244 -0
- package/docs/branding-contract.md +66 -0
- package/docs/branding-fill-prompt.md +25 -0
- package/docs/canonical-permissions.md +241 -0
- package/docs/design/container-exterior-tiles.md +49 -0
- package/docs/design/container-exterior.md +81 -0
- package/docs/design/container-interior.md +77 -0
- package/docs/design/modular-architecture/00-research-report.md +107 -0
- package/docs/design/modular-architecture/01-architecture.md +125 -0
- package/docs/design/modular-architecture/02-module-map.md +116 -0
- package/docs/design/modular-architecture/03-builder-flows.md +104 -0
- package/docs/design/modular-architecture/04-module-lifecycle.md +62 -0
- package/docs/design/modular-architecture/README.md +27 -0
- package/docs/design/vanilla-hall-ui-redesign-scope.md +284 -0
- package/docs/design/world-map-scroll.md +41 -0
- package/docs/design-contract.md +320 -0
- package/docs/file-map.md +418 -0
- package/docs/handoff-template.md +92 -0
- package/docs/modules-contract.md +293 -0
- package/docs/onboarding/browser-terminal-guide.md +73 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +28 -0
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +54 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +37 -0
- package/docs/onboarding/diagrams/04-architecture.mmd +51 -0
- package/docs/onboarding/diagrams/05-versioning.mmd +29 -0
- package/docs/onboarding/diagrams/README.md +94 -0
- package/docs/onboarding/diagrams/assertions.json +170 -0
- package/docs/onboarding/diagrams/puppeteer-config.json +1 -0
- package/docs/onboarding/diagrams/render-config.json +9 -0
- package/docs/onboarding/drachmae-and-karma.md +178 -0
- package/docs/onboarding/primer.md +202 -0
- package/docs/project-context.template.md +67 -0
- package/docs/recipes/README.md +35 -0
- package/docs/recipes/audit-replay.md +100 -0
- package/docs/recipes/bongos-cli-release.md +66 -0
- package/docs/recipes/builder-box-lifecycle.md +152 -0
- package/docs/recipes/claude-design-loop.md +143 -0
- package/docs/recipes/claude-gemini-bongos-doors.md +155 -0
- package/docs/recipes/cloudbongos-staging-gate.md +139 -0
- package/docs/recipes/cloudbongos-standup.md +111 -0
- package/docs/recipes/devbox-app-release.md +93 -0
- package/docs/recipes/devbox-app-windows-test.md +128 -0
- package/docs/recipes/failed-ship-recovery-proof.md +102 -0
- package/docs/recipes/figma-design-loop.md +129 -0
- package/docs/recipes/gds-db-backup.md +255 -0
- package/docs/recipes/instance-deploy.md +37 -0
- package/docs/recipes/managed-settings-remote-control.md +416 -0
- package/docs/recipes/memory-backup.md +71 -0
- package/docs/recipes/memory-sync-conflicts.md +98 -0
- package/docs/recipes/multi-builder-merge.md +78 -0
- package/docs/recipes/ops-gotchas.md +212 -0
- package/docs/recipes/overnight-builder.md +53 -0
- package/docs/recipes/packaging-the-core.md +49 -0
- package/docs/recipes/parallel-load-results-2026-05-30.md +63 -0
- package/docs/recipes/parallel-load-test.md +32 -0
- package/docs/recipes/pixel-art-pipeline.md +411 -0
- package/docs/recipes/private-npm-distribution.md +79 -0
- package/docs/recipes/search-index-ingestion.md +67 -0
- package/docs/recipes/self-host.md +60 -0
- package/docs/recipes/ship-and-regrade.md +72 -0
- package/docs/recipes/staging-data.md +50 -0
- package/docs/recipes/staging-environment.md +215 -0
- package/docs/recipes/upgrading-the-core.md +58 -0
- package/docs/research/cursor-competitive-analysis.md +255 -0
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +227 -0
- package/migrations/003_pms.sql +284 -0
- package/migrations/004_pms_cost_idempotency.sql +13 -0
- package/migrations/005_pms_smoke_filter.sql +44 -0
- package/migrations/006_pms_v2.sql +378 -0
- package/migrations/007_pms_v2_hierarchy_trigger.sql +59 -0
- package/migrations/008_pms_v2_migration_allocator.sql +27 -0
- package/migrations/009_pms_v2_blockers_table.sql +68 -0
- package/migrations/010_uptime.sql +29 -0
- package/migrations/011_backfill_v2_v3_done_when.sql +20 -0
- package/migrations/012_pms_v2_task_lifecycle_states.sql +82 -0
- package/migrations/013_claim_locking.sql +22 -0
- package/migrations/014_pms_task_discipline.sql +86 -0
- package/migrations/015_pms_estimation_calibration.sql +140 -0
- package/migrations/016_pms_task_grades.sql +60 -0
- package/migrations/017_version_progress_exclude_abandoned.sql +49 -0
- package/migrations/019_rename_pms_v3_to_gds_v3.sql +65 -0
- package/migrations/020_task_dependencies.sql +99 -0
- package/migrations/021_v3_renumber_and_deps.sql +224 -0
- package/migrations/022_v3_reconcile_dep_gated_status.sql +41 -0
- package/migrations/023_pms_rank.sql +30 -0
- package/migrations/025_builder_exit_status.sql +15 -0
- package/migrations/027_builders_preferred_disciplines.sql +46 -0
- package/migrations/028_tasks_parallel_safe.sql +71 -0
- package/migrations/029_rank_three_live.sql +32 -0
- package/migrations/030_vulnerability_reports.sql +61 -0
- package/migrations/031_reward_simplification.sql +57 -0
- package/migrations/032_repo_health_snapshots.sql +56 -0
- package/migrations/033_audit_log.sql +64 -0
- package/migrations/034_lifecycle_breakdown_leaf_only.sql +62 -0
- package/migrations/035_auth_token_hygiene.sql +66 -0
- package/migrations/037_builder_skill_model_prefs.sql +44 -0
- package/migrations/038_task_override_requests.sql +51 -0
- package/migrations/039_newcomer_friendly_column.sql +124 -0
- package/migrations/040_builder_karma.sql +58 -0
- package/migrations/041_kind_multipliers_and_idea_bonus.sql +42 -0
- package/migrations/042_criterion_proposal_workflow.sql +78 -0
- package/migrations/043_builder_onboarding_observability.sql +66 -0
- package/migrations/044_tasks_requires_rank.sql +97 -0
- package/migrations/045_builder_onboarding_state.sql +147 -0
- package/migrations/046_cost_log_builder_attribution.sql +42 -0
- package/migrations/054_builder_memory.sql +108 -0
- package/migrations/055_task_criteria.sql +90 -0
- package/migrations/058_peer_votes.sql +41 -0
- package/migrations/059_onboarding_pick_disciplines.sql +59 -0
- package/migrations/060_access_requests.sql +46 -0
- package/migrations/061_backfill_override_ship_attribution.sql +29 -0
- package/migrations/062_session_records.sql +34 -0
- package/migrations/063_session_records_tool_errors_idx.sql +11 -0
- package/migrations/064_grade_attempts.sql +18 -0
- package/migrations/065_bfg_principal.sql +58 -0
- package/migrations/066_live_notify_triggers.sql +69 -0
- package/migrations/067_builder_boxes.sql +117 -0
- package/migrations/067_discord_bot_foundation.sql +69 -0
- package/migrations/068_thetes_requires_rank.sql +27 -0
- package/migrations/069_builder_ssh_keys.sql +66 -0
- package/migrations/070_box_intents.sql +101 -0
- package/migrations/071_box_first_provision_approval.sql +46 -0
- package/migrations/072_onboarding_ship_three_remove_primer.sql +80 -0
- package/migrations/073_box_terminal_access.sql +46 -0
- package/migrations/075_builder_sound_prefs.sql +28 -0
- package/migrations/078_box_claude_active.sql +22 -0
- package/migrations/079_box_first_connected_at.sql +20 -0
- package/migrations/081_tasks_needs_migration.sql +34 -0
- package/migrations/084_tasks_security_sensitive.sql +24 -0
- package/migrations/085_security_engagement_docs.sql +34 -0
- package/migrations/086_rename_disciplines.sql +125 -0
- package/migrations/088_builders_anthropic_link.sql +28 -0
- package/migrations/089_builders_monthly_budget.sql +19 -0
- package/migrations/090_box_scoped_sessions.sql +38 -0
- package/migrations/091_session_token_reward.sql +44 -0
- package/migrations/093_tasks_sandbox_review.sql +22 -0
- package/migrations/102_remove_box_first_provision_approval.sql +58 -0
- package/migrations/103_search_chunks.sql +90 -0
- package/migrations/104_builder_box_blocked.sql +34 -0
- package/migrations/105_box_host_keys.sql +35 -0
- package/migrations/111_medusa_customer0_framing.sql +35 -0
- package/migrations/112_drop_track_check.sql +25 -0
- package/migrations/113_merge_lease.sql +16 -0
- package/migrations/114_cloudbongos_rename_live_strings.sql +32 -0
- package/migrations/118_currency_label_decoupling.sql +23 -0
- package/migrations/119_builder_behavior_prefs.sql +31 -0
- package/migrations/121_box_code_version.sql +37 -0
- package/migrations/122_builder_gemini_key.sql +30 -0
- package/migrations/123_cost_log_tiers.sql +33 -0
- package/migrations/124_cost_baseline.sql +45 -0
- package/migrations/125_llm_cache.sql +54 -0
- package/migrations/126_autonomy_runs.sql +33 -0
- package/migrations/143_cost_calibration.sql +63 -0
- package/migrations/145_backfill_task_requires_rank.sql +84 -0
- package/migrations/160_goal_tier.sql +88 -0
- package/migrations/163_polymorphic_dependencies.sql +187 -0
- package/migrations/164_event_sound_prefs.sql +20 -0
- package/migrations/165_task_module_key.sql +15 -0
- package/migrations/171_claims_published_head_sha.sql +24 -0
- package/migrations/174_close_orphaned_claims_on_terminal_tasks.sql +30 -0
- package/migrations/175_task_grades_diff_hash.sql +9 -0
- package/migrations/176_promotion_respects_open_blockers.sql +58 -0
- package/migrations/176_ui_discipline.sql +40 -0
- package/migrations/177_require_reward_before_workable.sql +76 -0
- package/migrations/178_reward_gate_auto_assign_revert_autopromote.sql +61 -0
- package/migrations/179_search_chunk_usefulness.sql +33 -0
- package/migrations/180_one_active_claim_per_session.sql +54 -0
- package/migrations/181_cost_log_skill_name.sql +33 -0
- package/migrations/184_claim_release_requested.sql +25 -0
- package/migrations/185_module_upstreaming_goal_wiring.sql +59 -0
- package/migrations/187_goal_sort_order.sql +30 -0
- package/migrations/187_tasks_kind_check_add_verify.sql +21 -0
- package/migrations/README.md +88 -0
- package/migrations/core_188_goal_visibility_and_membership_requests.sql +71 -0
- package/migrations/core_189_core_upgrades_ledger.sql +37 -0
- package/migrations/core_189_credit_log_task_id_index.sql +19 -0
- package/migrations/core_190_rebuild_claimable_task_views.sql +71 -0
- package/migrations/instance/018_pms_v3_planning_seed.sql +131 -0
- package/migrations/instance/080_gds_v4_planning_seed.sql +75 -0
- package/migrations/instance/110_medusa_planning_seed.sql +70 -0
- package/migrations/instance/120_bongos_v1_planning_seed.sql +69 -0
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +26 -0
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +29 -0
- package/migrations/instance/172_v2_world_planning_seed.sql +163 -0
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +33 -0
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +132 -0
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +116 -0
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +69 -0
- package/modules/autonomy/CLAUDE.md +49 -0
- package/modules/autonomy/db.js +50 -0
- package/modules/autonomy/module.json +13 -0
- package/modules/autonomy/pollers/routine-timers.js +146 -0
- package/modules/autonomy/routes/autonomy.js +37 -0
- package/modules/builder-settings/CLAUDE.md +58 -0
- package/modules/builder-settings/builder-needs.js +115 -0
- package/modules/builder-settings/db.js +168 -0
- package/modules/builder-settings/display-name.js +84 -0
- package/modules/builder-settings/event-sound-prefs.js +169 -0
- package/modules/builder-settings/interaction-prefs.js +201 -0
- package/modules/builder-settings/model-alloc-prefs.js +147 -0
- package/modules/builder-settings/module.json +10 -0
- package/modules/builder-settings/render-prefs.js +103 -0
- package/modules/builder-settings/routes/settings.js +417 -0
- package/modules/builder-settings/settings.js +52 -0
- package/modules/builder-settings/skill-prefs.js +196 -0
- package/modules/builder-settings/sound-prefs.js +178 -0
- package/modules/builder-settings/wandering-prefs.js +282 -0
- package/modules/dev-box/CLAUDE.md +38 -0
- package/modules/dev-box/box-access.js +275 -0
- package/modules/dev-box/box-credential.js +94 -0
- package/modules/dev-box/box-onboard.js +486 -0
- package/modules/dev-box/boxes.js +842 -0
- package/modules/dev-box/db.js +39 -0
- package/modules/dev-box/module.json +14 -0
- package/modules/dev-box/routes/box.js +977 -0
- package/modules/discord/CLAUDE.md +50 -0
- package/modules/discord/auth.js +54 -0
- package/modules/discord/bug-attachments.js +210 -0
- package/modules/discord/db.js +96 -0
- package/modules/discord/discord-approvals.js +145 -0
- package/modules/discord/discord-audit.js +29 -0
- package/modules/discord/discord-bot.js +545 -0
- package/modules/discord/discord-bugs.js +257 -0
- package/modules/discord/discord-channels.js +412 -0
- package/modules/discord/discord-inbound.js +147 -0
- package/modules/discord/discord-message-buffer.js +54 -0
- package/modules/discord/discord-reaction.js +186 -0
- package/modules/discord/discord-role-sync.js +318 -0
- package/modules/discord/module.json +14 -0
- package/modules/discord/pollers/discord-bot.js +19 -0
- package/modules/discord/routes/bug-attachments.js +44 -0
- package/modules/discord/routes/discord.js +102 -0
- package/modules/discord/ship-broadcast.js +78 -0
- package/modules/discord/webhook.js +74 -0
- package/modules/economy/CLAUDE.md +48 -0
- package/modules/economy/achievements.js +594 -0
- package/modules/economy/cost-classify.js +331 -0
- package/modules/economy/cost-summary-shape.js +90 -0
- package/modules/economy/cost.js +438 -0
- package/modules/economy/credits.js +430 -0
- package/modules/economy/migrations/economy_001_fix_gds_shipper_description.sql +91 -0
- package/modules/economy/module.json +10 -0
- package/modules/economy/reward.js +61 -0
- package/modules/economy/routes/cost.js +97 -0
- package/modules/economy/routes/leaderboard.js +26 -0
- package/modules/economy/routes/reward.js +42 -0
- package/modules/economy/streak.js +285 -0
- package/modules/grading/CLAUDE.md +42 -0
- package/modules/grading/db.js +39 -0
- package/modules/grading/grader-rubric.json +151 -0
- package/modules/grading/grader-workers/efficiency.js +213 -0
- package/modules/grading/grader-workers/extract-json.js +80 -0
- package/modules/grading/grader-workers/fence.js +100 -0
- package/modules/grading/grader-workers/hacker.js +239 -0
- package/modules/grading/grader-workers/index.js +183 -0
- package/modules/grading/grader-workers/narc.js +207 -0
- package/modules/grading/grader-workers/quality.js +198 -0
- package/modules/grading/grader-workers/rubric.js +20 -0
- package/modules/grading/grader-workers/worker-prompt.js +89 -0
- package/modules/grading/grader-workers/worker-verdict.js +50 -0
- package/modules/grading/grader.js +1790 -0
- package/modules/grading/module.json +12 -0
- package/modules/grading/routes/grade.js +51 -0
- package/modules/hall-ui/CLAUDE.md +61 -0
- package/modules/hall-ui/module.json +14 -0
- package/modules/hall-ui/public/atlas.html +180 -0
- package/modules/hall-ui/public/atlas.js +336 -0
- package/modules/hall-ui/public/box-panel.js +423 -0
- package/modules/hall-ui/public/brand-apply.js +117 -0
- package/modules/hall-ui/public/builders.js +3028 -0
- package/modules/hall-ui/public/chime.wav +0 -0
- package/modules/hall-ui/public/copy-on-click.js +174 -0
- package/modules/hall-ui/public/cursors/arrow.png +0 -0
- package/modules/hall-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier-out.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier.png +0 -0
- package/modules/hall-ui/public/diagram-viewer.css +125 -0
- package/modules/hall-ui/public/diagram-viewer.js +330 -0
- package/modules/hall-ui/public/diagrams.html +113 -0
- package/modules/hall-ui/public/dom-utils.js +212 -0
- package/modules/hall-ui/public/drachmae.html +68 -0
- package/modules/hall-ui/public/drachmae.js +136 -0
- package/modules/hall-ui/public/gate.html +143 -0
- package/modules/hall-ui/public/gate.js +228 -0
- package/modules/hall-ui/public/goals-page.js +1297 -0
- package/modules/hall-ui/public/goals.html +376 -0
- package/modules/hall-ui/public/goals.js +119 -0
- package/modules/hall-ui/public/hall-widgets.js +189 -0
- package/modules/hall-ui/public/harbor.html +167 -0
- package/modules/hall-ui/public/harbor.js +335 -0
- package/modules/hall-ui/public/index.html +116 -0
- package/modules/hall-ui/public/instances-panel.js +172 -0
- package/modules/hall-ui/public/md-reader.js +234 -0
- package/modules/hall-ui/public/modules.html +119 -0
- package/modules/hall-ui/public/modules.js +154 -0
- package/modules/hall-ui/public/new-project-wizard.js +186 -0
- package/modules/hall-ui/public/not-ready.html +48 -0
- package/modules/hall-ui/public/pair.html +119 -0
- package/modules/hall-ui/public/pair.js +132 -0
- package/modules/hall-ui/public/primer.css +404 -0
- package/modules/hall-ui/public/primer.html +85 -0
- package/modules/hall-ui/public/primer.js +641 -0
- package/modules/hall-ui/public/profile.html +85 -0
- package/modules/hall-ui/public/profile.js +236 -0
- package/modules/hall-ui/public/ranks.html +69 -0
- package/modules/hall-ui/public/ranks.js +206 -0
- package/modules/hall-ui/public/roadmap.html +209 -0
- package/modules/hall-ui/public/roadmap.js +375 -0
- package/modules/hall-ui/public/sessions.html +92 -0
- package/modules/hall-ui/public/sessions.js +222 -0
- package/modules/hall-ui/public/settings.html +640 -0
- package/modules/hall-ui/public/settings.js +1219 -0
- package/modules/hall-ui/public/shell.js +495 -0
- package/modules/hall-ui/public/style.css +1748 -0
- package/modules/hall-ui/public/task-detail.js +253 -0
- package/modules/hall-ui/public/task.html +83 -0
- package/modules/hall-ui/public/task.js +326 -0
- package/modules/hall-ui/public/watch.html +333 -0
- package/modules/hall-ui/public/watch.js +859 -0
- package/modules/hall-ui/public/work.html +469 -0
- package/modules/hall-ui/public/work.js +1292 -0
- package/modules/ideas/CLAUDE.md +37 -0
- package/modules/ideas/blockers.js +100 -0
- package/modules/ideas/inbox.js +459 -0
- package/modules/ideas/module.json +10 -0
- package/modules/ideas/routes/blockers.js +120 -0
- package/modules/ideas/routes/inbox.js +230 -0
- package/modules/lifecycle/CLAUDE.md +63 -0
- package/modules/lifecycle/conflict-resolve.js +436 -0
- package/modules/lifecycle/db.js +4970 -0
- package/modules/lifecycle/done-when.js +355 -0
- package/modules/lifecycle/github-push.js +969 -0
- package/modules/lifecycle/goal-conflicts.js +133 -0
- package/modules/lifecycle/hierarchy.js +104 -0
- package/modules/lifecycle/lifecycle.js +58 -0
- package/modules/lifecycle/merge-lock.js +261 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +28 -0
- package/modules/lifecycle/migrations/lifecycle_002_backfill_orphan_task_goals.sql +36 -0
- package/modules/lifecycle/module.json +13 -0
- package/modules/lifecycle/pollers/publish-reconciler.js +20 -0
- package/modules/lifecycle/publish-reconciler.js +418 -0
- package/modules/lifecycle/routes/analytics.js +63 -0
- package/modules/lifecycle/routes/claims.js +506 -0
- package/modules/lifecycle/routes/dependencies.js +82 -0
- package/modules/lifecycle/routes/done-when.js +136 -0
- package/modules/lifecycle/routes/gate-approvals.js +197 -0
- package/modules/lifecycle/routes/goals.js +1419 -0
- package/modules/lifecycle/routes/lifecycle.js +43 -0
- package/modules/lifecycle/routes/tasks.js +1703 -0
- package/modules/lifecycle/routes/versions.js +128 -0
- package/modules/lifecycle/ship-card.js +505 -0
- package/modules/lifecycle/ship-preflight.js +225 -0
- package/modules/lifecycle/task-classifier.js +512 -0
- package/modules/lifecycle/version-close.js +106 -0
- package/modules/memory/CLAUDE.md +40 -0
- package/modules/memory/learnings.js +275 -0
- package/modules/memory/memory.js +572 -0
- package/modules/memory/module.json +12 -0
- package/modules/memory/routes/learnings.js +90 -0
- package/modules/memory/routes/memory.js +916 -0
- package/modules/memory/routes/search.js +188 -0
- package/modules/memory/search-chunker.js +252 -0
- package/modules/memory/search.js +320 -0
- package/modules/onboarding/CLAUDE.md +55 -0
- package/modules/onboarding/approval-broadcast.js +59 -0
- package/modules/onboarding/db.js +54 -0
- package/modules/onboarding/module.json +10 -0
- package/modules/onboarding/newcomer-restock.js +212 -0
- package/modules/onboarding/onboarding-state.js +532 -0
- package/modules/onboarding/onboarding.js +508 -0
- package/modules/onboarding/port.js +63 -0
- package/modules/onboarding/routes/access-requests.js +208 -0
- package/modules/onboarding/routes/onboarding.js +54 -0
- package/modules/security/CLAUDE.md +44 -0
- package/modules/security/module.json +10 -0
- package/modules/security/routes/reports.js +251 -0
- package/modules/security/secret-scrub.js +201 -0
- package/modules/security/security.js +294 -0
- package/modules/sessions/CLAUDE.md +39 -0
- package/modules/sessions/bfg.js +357 -0
- package/modules/sessions/db.js +326 -0
- package/modules/sessions/module.json +10 -0
- package/modules/sessions/pulse.js +149 -0
- package/modules/sessions/routes/sessions.js +331 -0
- package/modules/status-ui/CLAUDE.md +47 -0
- package/modules/status-ui/module.json +14 -0
- package/modules/status-ui/public/README.md +29 -0
- package/modules/status-ui/public/cursors/arrow.png +0 -0
- package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/status-ui/public/index.html +363 -0
- package/modules/status-ui/public/snapshots/.gitkeep +0 -0
- package/modules/status-ui/public/status.js +1375 -0
- package/modules/status-ui/public/style.css +1342 -0
- package/package-lock.json +975 -0
- package/package.json +28 -0
- package/public-docs/index.html +35 -0
- package/public-docs/redoc.standalone.js +1838 -0
- package/public-docs/redoc.standalone.js.LICENSE.txt +102 -0
- package/public-landing/buddha.png +0 -0
- package/public-landing/create.html +868 -0
- package/public-landing/index.html +168 -0
- package/scripts/bongos-mcp.js +28 -0
- package/scripts/cron/style_guide_audit.sh +134 -0
- package/scripts/discord/README.md +52 -0
- package/scripts/discord/lib.js +80 -0
- package/scripts/discord/post.js +51 -0
- package/scripts/gds/CLAUDE.md +144 -0
- package/scripts/gds/abandon.js +58 -0
- package/scripts/gds/adopt-detect.js +123 -0
- package/scripts/gds/adopt-import.js +111 -0
- package/scripts/gds/adopt-preflight.js +121 -0
- package/scripts/gds/api.js +78 -0
- package/scripts/gds/architect-audit.js +288 -0
- package/scripts/gds/art-stage.js +152 -0
- package/scripts/gds/audit-authorship.sh +243 -0
- package/scripts/gds/audit-deps.js +121 -0
- package/scripts/gds/audit-exports.js +128 -0
- package/scripts/gds/audit-replay.sh +161 -0
- package/scripts/gds/audit-ships.js +251 -0
- package/scripts/gds/autonomy-gate.js +135 -0
- package/scripts/gds/backfill-cost.js +165 -0
- package/scripts/gds/backfill-sessions.js +351 -0
- package/scripts/gds/backfill-task-rewards.js +408 -0
- package/scripts/gds/backup.js +94 -0
- package/scripts/gds/bfg-principal-token.js +65 -0
- package/scripts/gds/box-auth-check.js +160 -0
- package/scripts/gds/box-connect-lib.js +453 -0
- package/scripts/gds/box.js +1258 -0
- package/scripts/gds/build-bongos.js +94 -0
- package/scripts/gds/capture.js +123 -0
- package/scripts/gds/cf-tunnel.js +419 -0
- package/scripts/gds/ci-grade.js +82 -0
- package/scripts/gds/claim-recall.js +112 -0
- package/scripts/gds/claim.js +333 -0
- package/scripts/gds/classify-discipline.js +153 -0
- package/scripts/gds/claude-materialize.js +176 -0
- package/scripts/gds/cleanup-orphans.js +74 -0
- package/scripts/gds/cli-lib.js +518 -0
- package/scripts/gds/code.js +101 -0
- package/scripts/gds/codemod-rename-src-gds.js +145 -0
- package/scripts/gds/conductor-dispatch.js +426 -0
- package/scripts/gds/context-pack.js +228 -0
- package/scripts/gds/coreB-carve-proof.sh +716 -0
- package/scripts/gds/cost.js +47 -0
- package/scripts/gds/db-backup-nightly.sh +160 -0
- package/scripts/gds/db-backup-offsite.sh +152 -0
- package/scripts/gds/demote.js +40 -0
- package/scripts/gds/design-sync.js +62 -0
- package/scripts/gds/diagnose-achievements.js +118 -0
- package/scripts/gds/diagram-facts.js +319 -0
- package/scripts/gds/discipline-modes.json +25 -0
- package/scripts/gds/discord-channels.js +66 -0
- package/scripts/gds/do-api.js +176 -0
- package/scripts/gds/docs-entropy.js +403 -0
- package/scripts/gds/doctor.js +389 -0
- package/scripts/gds/feedback-latest.js +94 -0
- package/scripts/gds/fetch-art-key.js +106 -0
- package/scripts/gds/fetch-sound-prefs.js +88 -0
- package/scripts/gds/figma-design-sync.js +61 -0
- package/scripts/gds/fitness.js +1446 -0
- package/scripts/gds/gate-review.js +322 -0
- package/scripts/gds/gating.js +325 -0
- package/scripts/gds/gds-db-backup.service +24 -0
- package/scripts/gds/gds-db-backup.timer +20 -0
- package/scripts/gds/gds-db-offsite.service +29 -0
- package/scripts/gds/gds-db-offsite.timer +18 -0
- package/scripts/gds/gen-api-client.js +508 -0
- package/scripts/gds/gen-api-docs.js +1037 -0
- package/scripts/gds/gen-atlas.js +366 -0
- package/scripts/gds/gen-diagrams.js +608 -0
- package/scripts/gds/gen-file-map.js +196 -0
- package/scripts/gds/gen-repo-map.js +519 -0
- package/scripts/gds/gen-session-index.js +210 -0
- package/scripts/gds/git-merge-regen.js +147 -0
- package/scripts/gds/grade-smoke.js +205 -0
- package/scripts/gds/hooks/pre-push.js +169 -0
- package/scripts/gds/http-api-client.js +88 -0
- package/scripts/gds/init.js +903 -0
- package/scripts/gds/install-git-hooks.js +114 -0
- package/scripts/gds/key-card.js +213 -0
- package/scripts/gds/key-status.js +126 -0
- package/scripts/gds/land-watch.js +168 -0
- package/scripts/gds/learning-capture.js +145 -0
- package/scripts/gds/lib.sh +44 -0
- package/scripts/gds/linkify-refs.js +473 -0
- package/scripts/gds/local-preview-lib.js +159 -0
- package/scripts/gds/local-preview.js +95 -0
- package/scripts/gds/login.js +225 -0
- package/scripts/gds/main-audit.js +279 -0
- package/scripts/gds/me.js +88 -0
- package/scripts/gds/memory-map.js +174 -0
- package/scripts/gds/merge-lock.js +71 -0
- package/scripts/gds/migration-namespace.js +200 -0
- package/scripts/gds/mirror-lineage-check.js +319 -0
- package/scripts/gds/mirror-redact.js +305 -0
- package/scripts/gds/module.js +482 -0
- package/scripts/gds/newcomer-floor.config.json +13 -0
- package/scripts/gds/newcomer-floor.js +415 -0
- package/scripts/gds/newcomer-floor.templates.json +86 -0
- package/scripts/gds/newcomer-floor.test.js +235 -0
- package/scripts/gds/next-number.js +163 -0
- package/scripts/gds/oauth-secret.js +197 -0
- package/scripts/gds/onboard.js +217 -0
- package/scripts/gds/onboarding-analyze.js +226 -0
- package/scripts/gds/onboarding-config.js +178 -0
- package/scripts/gds/onboarding-ship.js +92 -0
- package/scripts/gds/package-core.js +684 -0
- package/scripts/gds/paste-token.js +131 -0
- package/scripts/gds/portability-smoke-art.sh +85 -0
- package/scripts/gds/portability-smoke-memory.sh +147 -0
- package/scripts/gds/provision.js +1324 -0
- package/scripts/gds/publish-manifest.js +307 -0
- package/scripts/gds/pulse.js +309 -0
- package/scripts/gds/push-main.js +45 -0
- package/scripts/gds/read-session-export.js +359 -0
- package/scripts/gds/reauth.js +149 -0
- package/scripts/gds/recall.js +135 -0
- package/scripts/gds/release.js +71 -0
- package/scripts/gds/render-ideas.js +173 -0
- package/scripts/gds/repo-metrics.js +363 -0
- package/scripts/gds/resolve-skill-model.js +57 -0
- package/scripts/gds/run-routine.js +197 -0
- package/scripts/gds/run-unit-tests.js +178 -0
- package/scripts/gds/sandbox-stage.js +653 -0
- package/scripts/gds/search-ingest-db.js +209 -0
- package/scripts/gds/search-ingest-md.js +225 -0
- package/scripts/gds/security-baseline-check.sh +180 -0
- package/scripts/gds/seed-adopt-tasks.js +75 -0
- package/scripts/gds/seed-api-contract-goal-tasks.js +141 -0
- package/scripts/gds/seed-bongos-app-tasks.js +294 -0
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +81 -0
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +100 -0
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +307 -0
- package/scripts/gds/seed-bongos-module-system-tasks.js +346 -0
- package/scripts/gds/seed-bongos-tasks.js +214 -0
- package/scripts/gds/seed-gds-v4-tasks.js +624 -0
- package/scripts/gds/seed-governance-tasks.js +340 -0
- package/scripts/gds/seed-medusa-tasks.js +440 -0
- package/scripts/gds/seed-module-upstreaming.js +219 -0
- package/scripts/gds/seed-productization-tasks.js +235 -0
- package/scripts/gds/seed-provisioning-tasks.js +83 -0
- package/scripts/gds/seed-staging.sh +104 -0
- package/scripts/gds/seed-v2-world-tasks.js +283 -0
- package/scripts/gds/seed-v3-tasks.js +1981 -0
- package/scripts/gds/session-cost.js +393 -0
- package/scripts/gds/session-digest.js +179 -0
- package/scripts/gds/session-eval.js +576 -0
- package/scripts/gds/session.js +404 -0
- package/scripts/gds/setup.js +504 -0
- package/scripts/gds/shell.js +100 -0
- package/scripts/gds/ship.js +3618 -0
- package/scripts/gds/smoke-batch-claim.sh +213 -0
- package/scripts/gds/smoke-batch-release.sh +296 -0
- package/scripts/gds/smoke-box.sh +150 -0
- package/scripts/gds/smoke-claim-consistency.sh +212 -0
- package/scripts/gds/smoke-dependencies.sh +84 -0
- package/scripts/gds/smoke-gds.js +125 -0
- package/scripts/gds/smoke-write.sh +168 -0
- package/scripts/gds/start.js +782 -0
- package/scripts/gds/status.js +365 -0
- package/scripts/gds/triage.js +366 -0
- package/scripts/gds/upgrade.js +421 -0
- package/scripts/gds/validate-design.js +254 -0
- package/scripts/gds/version-close.js +549 -0
- package/scripts/migrate.sh +183 -0
- package/scripts/migrate_cost_ledger.sh +178 -0
- package/scripts/public-mirror-export.js +291 -0
- package/scripts/render-diagrams.sh +88 -0
- package/scripts/security/dep-audit.js +276 -0
- package/scripts/setup-builder.js +274 -0
- package/scripts/setup-builder.sh +28 -0
- package/scripts/snapshots/backfill-history.mjs +292 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +66 -0
- package/scripts/snapshots/render-pre-art-eras.mjs +329 -0
- package/scripts/snapshots/render-world.mjs +322 -0
- package/scripts/status-mirror-sync.js +192 -0
- package/scripts/status-probe.mjs +185 -0
- package/server.js +212 -0
- package/src/asset-version.js +71 -0
- package/src/bongos/CLAUDE.md +64 -0
- package/src/bongos/api-errors.js +281 -0
- package/src/bongos/api-path-404.js +67 -0
- package/src/bongos/api-prefix.js +38 -0
- package/src/bongos/app-pair.js +172 -0
- package/src/bongos/auth.js +979 -0
- package/src/bongos/cascade-dispatch.js +123 -0
- package/src/bongos/db-kernel.js +252 -0
- package/src/bongos/db.js +911 -0
- package/src/bongos/goal-scope-check.js +120 -0
- package/src/bongos/hierarchy-config.js +189 -0
- package/src/bongos/live-channel.js +145 -0
- package/src/bongos/llm-cache.js +260 -0
- package/src/bongos/llm-pricing.js +220 -0
- package/src/bongos/metrics.js +286 -0
- package/src/bongos/middleware/audit.js +274 -0
- package/src/bongos/middleware/error-envelope.js +93 -0
- package/src/bongos/middleware/rate-limit.js +195 -0
- package/src/bongos/module-scope-map.js +271 -0
- package/src/bongos/optimizer.js +166 -0
- package/src/bongos/path-match.js +47 -0
- package/src/bongos/permission-path-check.js +248 -0
- package/src/bongos/pool.js +55 -0
- package/src/bongos/repo-info.js +91 -0
- package/src/bongos/route-rank-check.js +420 -0
- package/src/bongos/routes/CLAUDE.md +43 -0
- package/src/bongos/routes/_helpers.js +369 -0
- package/src/bongos/routes/audit-log.js +59 -0
- package/src/bongos/routes/auth.js +755 -0
- package/src/bongos/routes/backup.js +104 -0
- package/src/bongos/routes/builders.js +526 -0
- package/src/bongos/routes/healthz.js +16 -0
- package/src/bongos/routes/instance.js +129 -0
- package/src/bongos/routes/live.js +125 -0
- package/src/bongos/routes/llm-cache.js +119 -0
- package/src/bongos/routes/me.js +380 -0
- package/src/bongos/routes/modules.js +131 -0
- package/src/bongos/routes/optimizer.js +54 -0
- package/src/bongos/routes/public.js +476 -0
- package/src/bongos/routes/security.js +207 -0
- package/src/bongos/routes.js +166 -0
- package/src/bongos/secret-box.js +177 -0
- package/src/bongos/sensitive-surfaces.js +30 -0
- package/src/bongos/serve-internal.js +889 -0
- package/src/bongos/uptime-poller.js +90 -0
- package/src/bongos-downloads.js +70 -0
- package/src/branding.js +182 -0
- package/src/build-info.js +77 -0
- package/src/devbox-downloads.js +67 -0
- package/src/instance-config.js +200 -0
- package/src/module-api.js +366 -0
- package/src/module-loader/loader.js +268 -0
- package/src/module-loader/manifest-schema.js +140 -0
- package/src/module-loader/provenance.js +28 -0
- package/src/module-loader/semver.js +100 -0
- package/src/module-seams.js +291 -0
- package/src/modules.js +254 -0
- package/src/platform-server.js +135 -0
- package/src/ui/adapters/claude-design/index.js +333 -0
- package/src/ui/adapters/figma/index.js +328 -0
- package/tests/abandon_task.mjs +138 -0
- package/tests/achievements_reconcile.mjs +167 -0
- package/tests/adopt_detect.mjs +141 -0
- package/tests/adopt_import.mjs +132 -0
- package/tests/adopt_preflight.mjs +105 -0
- package/tests/anti_grid.mjs +89 -0
- package/tests/api_client.mjs +231 -0
- package/tests/api_docs.mjs +283 -0
- package/tests/api_error_envelope_buildcheck.mjs +56 -0
- package/tests/api_error_envelope_middleware.mjs +99 -0
- package/tests/api_error_registry.mjs +108 -0
- package/tests/api_pagination_contract.mjs +51 -0
- package/tests/api_path_404.mjs +96 -0
- package/tests/app_pair.mjs +168 -0
- package/tests/architect.mjs +140 -0
- package/tests/art_key.mjs +121 -0
- package/tests/art_stage.mjs +121 -0
- package/tests/asset_version.mjs +94 -0
- package/tests/atlas.mjs +69 -0
- package/tests/audit_deps.mjs +74 -0
- package/tests/audit_middleware.mjs +385 -0
- package/tests/auth_admission_gate.mjs +164 -0
- package/tests/auth_anthropic_identity.mjs +95 -0
- package/tests/auth_duplicate_cookie.mjs +56 -0
- package/tests/auth_error_brand.mjs +77 -0
- package/tests/auth_flow_slice1.mjs +223 -0
- package/tests/auth_inactive_builder.mjs +94 -0
- package/tests/auth_page_gate.mjs +92 -0
- package/tests/auth_session_ttl.mjs +155 -0
- package/tests/autonomous_fit_protected_paths.mjs +115 -0
- package/tests/autonomy_gate.mjs +85 -0
- package/tests/autotile_variants.mjs +256 -0
- package/tests/autotile_xverify.mjs +133 -0
- package/tests/backfill_sessions.mjs +101 -0
- package/tests/backfill_task_rewards.mjs +152 -0
- package/tests/backup_cli.mjs +104 -0
- package/tests/bfg.mjs +327 -0
- package/tests/bfg_write.mjs +423 -0
- package/tests/bongos_box_connect.mjs +89 -0
- package/tests/bongos_dispatch.mjs +74 -0
- package/tests/bongos_downloads.mjs +118 -0
- package/tests/bongos_login.mjs +34 -0
- package/tests/box_access.mjs +186 -0
- package/tests/box_auth_check.mjs +114 -0
- package/tests/box_code_staleness.mjs +116 -0
- package/tests/box_connect_e2e.mjs +196 -0
- package/tests/box_cost.mjs +70 -0
- package/tests/box_dns_repoint.mjs +151 -0
- package/tests/box_host_keys.mjs +102 -0
- package/tests/box_onboard.mjs +317 -0
- package/tests/box_scope_session.mjs +115 -0
- package/tests/box_ship_permissions.mjs +71 -0
- package/tests/box_terminal_ssrf.mjs +90 -0
- package/tests/boxes.mjs +1255 -0
- package/tests/branding.mjs +88 -0
- package/tests/bug_attachments.mjs +189 -0
- package/tests/build_info.mjs +88 -0
- package/tests/builder_needs.mjs +99 -0
- package/tests/cap_newcomer_tasks.mjs +114 -0
- package/tests/cascade-dispatch.mjs +95 -0
- package/tests/cf_tunnel.mjs +459 -0
- package/tests/claim_recall.mjs +150 -0
- package/tests/claims_bind_session.mjs +47 -0
- package/tests/claude_materialize.mjs +148 -0
- package/tests/cli_box_scope.mjs +52 -0
- package/tests/cli_lib_formatters.mjs +87 -0
- package/tests/cli_token_reissue.mjs +147 -0
- package/tests/client_bundle_route.mjs +47 -0
- package/tests/cloud_bongos_pack.mjs +85 -0
- package/tests/conductor.mjs +496 -0
- package/tests/confirm_attribution.mjs +156 -0
- package/tests/conflict_resolve.mjs +462 -0
- package/tests/context_pack.mjs +240 -0
- package/tests/cost.mjs +94 -0
- package/tests/cost_audit.mjs +122 -0
- package/tests/cost_budget_status.mjs +78 -0
- package/tests/cost_reconcile.mjs +102 -0
- package/tests/cost_summary_shape.mjs +94 -0
- package/tests/credit_grant.mjs +376 -0
- package/tests/criterion_progress.mjs +220 -0
- package/tests/criterion_proposal.mjs +109 -0
- package/tests/criterion_ref_parse.mjs +53 -0
- package/tests/currency_label.mjs +119 -0
- package/tests/day_cycle.mjs +197 -0
- package/tests/dependencies_migration.mjs +77 -0
- package/tests/dependencies_polymorphic.mjs +212 -0
- package/tests/deploy_instance.mjs +78 -0
- package/tests/devbox_downloads.mjs +122 -0
- package/tests/diff_derived_signals.mjs +115 -0
- package/tests/discord_approvals.mjs +174 -0
- package/tests/discord_autojoin.mjs +158 -0
- package/tests/discord_bot.mjs +94 -0
- package/tests/discord_bugs.mjs +297 -0
- package/tests/discord_channels.mjs +337 -0
- package/tests/discord_foundation.mjs +76 -0
- package/tests/discord_inbound.mjs +190 -0
- package/tests/discord_message_buffer.mjs +87 -0
- package/tests/discord_oauth.mjs +55 -0
- package/tests/discord_reaction.mjs +170 -0
- package/tests/discord_role_sync.mjs +151 -0
- package/tests/display_name.mjs +123 -0
- package/tests/dns_preflight.mjs +74 -0
- package/tests/event_sound_prefs.mjs +194 -0
- package/tests/eviction_policy.mjs +279 -0
- package/tests/figma_adapter.mjs +199 -0
- package/tests/file_map.mjs +91 -0
- package/tests/first_admin_bootstrap.mjs +40 -0
- package/tests/fitness.mjs +435 -0
- package/tests/fitness_two_root.mjs +104 -0
- package/tests/fixtures/session-506-digest.json +34 -0
- package/tests/gate_approvals.mjs +451 -0
- package/tests/gate_review.mjs +152 -0
- package/tests/git_merge_regen.mjs +151 -0
- package/tests/github_push_ancestry.mjs +119 -0
- package/tests/github_push_land_proof.mjs +127 -0
- package/tests/github_push_merge_driver.mjs +175 -0
- package/tests/goal_abuse_matrix.mjs +187 -0
- package/tests/goal_achievement.mjs +146 -0
- package/tests/goal_conflicts.mjs +305 -0
- package/tests/goal_dependencies_read.mjs +73 -0
- package/tests/goal_member_roles.mjs +420 -0
- package/tests/goal_membership_requests.mjs +332 -0
- package/tests/goal_routes.mjs +203 -0
- package/tests/goal_scope_check.mjs +167 -0
- package/tests/goal_tier.mjs +312 -0
- package/tests/goal_wall_proof.mjs +112 -0
- package/tests/grade_attempts.mjs +73 -0
- package/tests/grade_attribution.mjs +117 -0
- package/tests/grade_diff_hash.mjs +191 -0
- package/tests/grade_server_authoritative.mjs +305 -0
- package/tests/grade_unavailable_display.mjs +66 -0
- package/tests/grader.mjs +1960 -0
- package/tests/grader_bypass.mjs +57 -0
- package/tests/grader_panel.mjs +252 -0
- package/tests/grader_prompt_fencing.mjs +192 -0
- package/tests/helpers.mjs +52 -0
- package/tests/hierarchy-config.mjs +81 -0
- package/tests/init.mjs +463 -0
- package/tests/instance_config.mjs +76 -0
- package/tests/instance_manifest.mjs +110 -0
- package/tests/interaction_prefs.mjs +72 -0
- package/tests/key-card.mjs +110 -0
- package/tests/key-status.mjs +45 -0
- package/tests/land_watch.mjs +115 -0
- package/tests/lifecycle-card.mjs +145 -0
- package/tests/lifecycle_breakdown_view.mjs +236 -0
- package/tests/linkify_refs.mjs +264 -0
- package/tests/live_channel.mjs +128 -0
- package/tests/live_sse.mjs +122 -0
- package/tests/llm-cache-route.mjs +110 -0
- package/tests/llm-cache.mjs +140 -0
- package/tests/llm-pricing.mjs +152 -0
- package/tests/local_preview.mjs +136 -0
- package/tests/main_audit.mjs +137 -0
- package/tests/memory_isolation.mjs +431 -0
- package/tests/memory_map.mjs +107 -0
- package/tests/memory_pull.mjs +429 -0
- package/tests/merge_lease.mjs +100 -0
- package/tests/merge_lock.mjs +312 -0
- package/tests/metrics.mjs +147 -0
- package/tests/migration_allocation.mjs +146 -0
- package/tests/migration_namespace.mjs +185 -0
- package/tests/migrations_fresh_replay_guard.mjs +38 -0
- package/tests/mirror_lineage_fulltree.mjs +45 -0
- package/tests/mirror_lineage_proof.mjs +72 -0
- package/tests/mirror_redact.mjs +193 -0
- package/tests/model_alloc_prefs.mjs +63 -0
- package/tests/module-scope-map.mjs +88 -0
- package/tests/module_api.mjs +110 -0
- package/tests/module_cli.mjs +292 -0
- package/tests/module_contributions.mjs +148 -0
- package/tests/module_loader.mjs +286 -0
- package/tests/module_manifest.mjs +194 -0
- package/tests/module_owned_tests.mjs +105 -0
- package/tests/module_provenance.mjs +34 -0
- package/tests/module_route_rank.mjs +147 -0
- package/tests/module_seams.mjs +68 -0
- package/tests/modules.mjs +156 -0
- package/tests/modules_route.mjs +38 -0
- package/tests/newcomer_floor.mjs +17 -0
- package/tests/newcomer_restock.mjs +80 -0
- package/tests/next_number.mjs +154 -0
- package/tests/oauth_secret.mjs +60 -0
- package/tests/onboard.mjs +70 -0
- package/tests/onboarding_gate.mjs +267 -0
- package/tests/onboarding_observability.mjs +344 -0
- package/tests/onboarding_route_signals.mjs +175 -0
- package/tests/onboarding_state.mjs +346 -0
- package/tests/one_active_claim_per_session.mjs +111 -0
- package/tests/orphaned_claim_ship.mjs +135 -0
- package/tests/override_attribution.mjs +142 -0
- package/tests/override_request_ownership.mjs +235 -0
- package/tests/package_core.mjs +435 -0
- package/tests/parallel-load.mjs +402 -0
- package/tests/path_match.mjs +62 -0
- package/tests/permission_path.mjs +151 -0
- package/tests/platform_boot.mjs +167 -0
- package/tests/pre_compact.mjs +60 -0
- package/tests/preview_server.mjs +127 -0
- package/tests/profile_route.mjs +197 -0
- package/tests/provision.mjs +805 -0
- package/tests/provisioning_github_app_routes.mjs +98 -0
- package/tests/public_mirror_export.mjs +124 -0
- package/tests/public_task_resolver.mjs +112 -0
- package/tests/publish_branch_route.mjs +422 -0
- package/tests/publish_manifest.mjs +184 -0
- package/tests/publish_reconciler.mjs +728 -0
- package/tests/rank_gate.mjs +463 -0
- package/tests/rate_limit_public.mjs +217 -0
- package/tests/read_session_export.mjs +89 -0
- package/tests/recall_cli.mjs +82 -0
- package/tests/recall_hints.mjs +119 -0
- package/tests/recent_shipped_window.mjs +80 -0
- package/tests/regrade_eligibility.mjs +202 -0
- package/tests/render_prefs.mjs +122 -0
- package/tests/repo_map.mjs +92 -0
- package/tests/reward_gate.mjs +284 -0
- package/tests/route_rank_check.mjs +243 -0
- package/tests/route_validation.mjs +490 -0
- package/tests/routine_timers.mjs +72 -0
- package/tests/rubric_fixtures.mjs +140 -0
- package/tests/run_routine.mjs +69 -0
- package/tests/sandbox_stage.mjs +582 -0
- package/tests/scrubber_corpus.mjs +257 -0
- package/tests/search_chunker.mjs +209 -0
- package/tests/search_ingest.mjs +96 -0
- package/tests/search_ingest_db.mjs +88 -0
- package/tests/search_isolation.mjs +204 -0
- package/tests/search_route.mjs +95 -0
- package/tests/secret_box.mjs +189 -0
- package/tests/security_docs.mjs +186 -0
- package/tests/security_route.mjs +108 -0
- package/tests/served_paths_shipped.mjs +118 -0
- package/tests/session-cards.mjs +142 -0
- package/tests/session_506_regression.mjs +69 -0
- package/tests/session_caps.mjs +238 -0
- package/tests/session_cost.mjs +133 -0
- package/tests/session_digest.mjs +73 -0
- package/tests/session_eval.mjs +386 -0
- package/tests/session_index.mjs +105 -0
- package/tests/session_records.mjs +369 -0
- package/tests/session_search.mjs +78 -0
- package/tests/session_start_freshness.mjs +123 -0
- package/tests/setup_smoke.mjs +154 -0
- package/tests/ship_autolinkify.mjs +60 -0
- package/tests/ship_automerge_injection.mjs +174 -0
- package/tests/ship_broadcast_routing.mjs +125 -0
- package/tests/ship_broadcast_server.mjs +175 -0
- package/tests/ship_card.mjs +280 -0
- package/tests/ship_ci_deploy.mjs +374 -0
- package/tests/ship_preflight.mjs +368 -0
- package/tests/ship_premerge.mjs +75 -0
- package/tests/ship_resume.mjs +129 -0
- package/tests/ship_upload_skip.mjs +89 -0
- package/tests/ship_zero_commit.mjs +60 -0
- package/tests/single_host_hall.mjs +98 -0
- package/tests/skill_prefs.mjs +150 -0
- package/tests/smoke_preflight.mjs +66 -0
- package/tests/snapshot_picker.mjs +233 -0
- package/tests/sound_prefs.mjs +198 -0
- package/tests/sprite_facing.mjs +35 -0
- package/tests/status-probe.mjs +89 -0
- package/tests/status_backfill.mjs +94 -0
- package/tests/stop_refcheck.mjs +127 -0
- package/tests/streak.mjs +175 -0
- package/tests/subdomain_auth.mjs +58 -0
- package/tests/task_classifier_vocab.mjs +41 -0
- package/tests/task_create_idempotency.mjs +96 -0
- package/tests/task_goal_link.mjs +93 -0
- package/tests/task_ownership_gate.mjs +125 -0
- package/tests/terrain_picker.mjs +298 -0
- package/tests/touches_overlap.mjs +75 -0
- package/tests/triage_development.mjs +140 -0
- package/tests/turn_in_place.mjs +280 -0
- package/tests/upgrade.mjs +357 -0
- package/tests/verify_task_guard.mjs +64 -0
- package/tests/version_close.mjs +76 -0
- package/tests/versions_route.mjs +93 -0
- package/tests/walkability.mjs +188 -0
- package/tests/wandering_prefs.mjs +176 -0
- package/tests/worktree_disambiguation.mjs +105 -0
- package/tests/worktree_guard.mjs +90 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
// GDS server-mediated branch publish (ADR 0055 / task 1025).
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS
|
|
4
|
+
// A builder on a cloud dev box cannot publish in `ci` deploy mode: the box has
|
|
5
|
+
// NO GitHub push credential by design (credential-less HTTPS origin, no gh, no
|
|
6
|
+
// GH_TOKEN — infra/builder-box-cloud-init.yaml scrubs the clone token), and its
|
|
7
|
+
// baked GDS session is box-scoped (deny-by-default, #919/ADR 0053). CI self-
|
|
8
|
+
// deploy (#859/ADR 0042) fixed only the LAST leg (Actions deploys main->prod);
|
|
9
|
+
// the box still can't get its branch onto GitHub to open the PR.
|
|
10
|
+
//
|
|
11
|
+
// THE FIX (owner-chosen: "the box holds nothing"). The box uploads its branch as
|
|
12
|
+
// a THIN git bundle to the GDS over a RE-AUTHED (non-box) cli session; the server
|
|
13
|
+
// pushes the branch to GitHub + opens the PR + enables auto-merge using a SERVER-
|
|
14
|
+
// side push credential. The box never holds a GitHub push credential, so the #919
|
|
15
|
+
// model is preserved. The credential is scoped to a single repo (Contents:write +
|
|
16
|
+
// Pull-requests:write + Actions:read) and CANNOT push to branch-protected `main`
|
|
17
|
+
// nor bypass required checks — a leaked server credential can only land code on a
|
|
18
|
+
// feature branch behind the same checks every PR faces.
|
|
19
|
+
//
|
|
20
|
+
// THE SERVER CREDENTIAL (task 1028 / ADR 0055 hardening). Preferred: a GitHub
|
|
21
|
+
// APP — the server holds ONLY the App private key and mints a SHORT-LIVED,
|
|
22
|
+
// repo-scoped INSTALLATION token (1h) per push window, so there's no standing
|
|
23
|
+
// push secret at rest. Set GITHUB_APP_ID + GITHUB_APP_INSTALLATION_ID +
|
|
24
|
+
// GITHUB_APP_PRIVATE_KEY. Fallback (v1, kept for cutover): a standing fine-grained
|
|
25
|
+
// PAT in GITHUB_PUSH_TOKEN. resolveToken() prefers the App, then the PAT; the App
|
|
26
|
+
// JWT is RS256-signed with node:crypto (no jsonwebtoken / Octokit dependency).
|
|
27
|
+
//
|
|
28
|
+
// This module is the server seam: push a bundle, open/find the PR, enable auto-
|
|
29
|
+
// merge, report status. All git runs through execFile arg-arrays (shell:false,
|
|
30
|
+
// the #681 injection discipline); the credentialed URL is built in-process and
|
|
31
|
+
// NEVER persisted to a remote nor echoed on an error path (the box-source-fetch.sh
|
|
32
|
+
// "one-shot URL, scrub the credential" pattern). The GitHub API calls are raw
|
|
33
|
+
// global fetch (no Octokit — fetch is used throughout auth.js), header shape
|
|
34
|
+
// mirrors auth.js fetchGithubUser.
|
|
35
|
+
|
|
36
|
+
const { execFile } = require('node:child_process');
|
|
37
|
+
const crypto = require('node:crypto');
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const os = require('node:os');
|
|
40
|
+
const path = require('node:path');
|
|
41
|
+
const { promisify } = require('node:util');
|
|
42
|
+
const { repoInfo, userAgent } = require('../../src/module-api');
|
|
43
|
+
const { loadRepoInfo } = repoInfo;
|
|
44
|
+
|
|
45
|
+
const execFileP = promisify(execFile);
|
|
46
|
+
|
|
47
|
+
// The server's own checkout root — a fast LOCAL object source for the bundle's
|
|
48
|
+
// `main..HEAD` prerequisites (so we never clone the whole repo over the network).
|
|
49
|
+
// Reading it via `git clone --bare --local` does NOT touch its working tree, so
|
|
50
|
+
// it never races the deploy worktree that `~/deploy.sh` resets.
|
|
51
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
52
|
+
|
|
53
|
+
// A branch ref the server will interpolate into argv + the GitHub API. Keep it
|
|
54
|
+
// to the chars a real `claude/*` worktree branch uses; reject anything that could
|
|
55
|
+
// be an option (leading '-'), a traversal ('..'), or a fully-qualified ref.
|
|
56
|
+
const SAFE_BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/;
|
|
57
|
+
const SAFE_SHA_RE = /^[0-9a-f]{7,64}$/;
|
|
58
|
+
|
|
59
|
+
// Configured if EITHER credential is present: a GitHub App (preferred) or the
|
|
60
|
+
// v1 PAT fallback. The route 503s when neither is set (task 1028).
|
|
61
|
+
function appConfigured() {
|
|
62
|
+
return !!(
|
|
63
|
+
process.env.GITHUB_APP_ID &&
|
|
64
|
+
process.env.GITHUB_APP_INSTALLATION_ID &&
|
|
65
|
+
process.env.GITHUB_APP_PRIVATE_KEY
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isConfigured() {
|
|
70
|
+
return !!process.env.GITHUB_PUSH_TOKEN || appConfigured();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isSafeBranch(branch) {
|
|
74
|
+
return (
|
|
75
|
+
typeof branch === 'string' &&
|
|
76
|
+
SAFE_BRANCH_RE.test(branch) &&
|
|
77
|
+
!branch.includes('..') &&
|
|
78
|
+
!branch.startsWith('refs/')
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function err(code, extra = {}) {
|
|
83
|
+
const e = new Error(code);
|
|
84
|
+
e.code = code;
|
|
85
|
+
Object.assign(e, extra);
|
|
86
|
+
return e;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract the usable `repository` from a GraphQL response, tolerating PARTIAL errors.
|
|
90
|
+
//
|
|
91
|
+
// GitHub GraphQL returns `data` (with the un-readable field nulled) ALONGSIDE an
|
|
92
|
+
// `errors` array on a partial failure — e.g. a FORBIDDEN on statusCheckRollup.contexts
|
|
93
|
+
// when the App installation lacks the `Checks: read` permission (operator blocker). A
|
|
94
|
+
// brittle `if (out.errors) throw` discards the whole usable response and surfaces to the
|
|
95
|
+
// caller as a 502; instead, use the data we DID get (the forbidden sub-fields come back
|
|
96
|
+
// null, which every reader here already null-guards) and throw GRAPHQL_FAILED ONLY when
|
|
97
|
+
// there is no repository data at all (a genuine top-level failure: bad credential, SAML,
|
|
98
|
+
// rate-limit). Returns { repository, partialErrors } — partialErrors is the list of
|
|
99
|
+
// GraphQL error types present on an otherwise-usable response, so a caller can signal
|
|
100
|
+
// reduced fidelity (e.g. "checks unreadable") instead of silently degrading.
|
|
101
|
+
function graphqlRepository(out) {
|
|
102
|
+
const repository = out && out.data && out.data.repository;
|
|
103
|
+
const errors = (out && out.errors) || [];
|
|
104
|
+
if (repository) {
|
|
105
|
+
return { repository, partialErrors: errors.map((e) => e && e.type).filter(Boolean) };
|
|
106
|
+
}
|
|
107
|
+
// No data → fatal. Surface the error detail (truncated) so the cause is loud in logs
|
|
108
|
+
// and in the structured route response.
|
|
109
|
+
throw err('GRAPHQL_FAILED', { detail: JSON.stringify(errors.length ? errors : 'no data').slice(0, 200) });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Build the one-shot authenticated push URL. NEVER logged, NEVER written to a
|
|
113
|
+
// persisted remote — it lives only as an argv element to a single git push/fetch.
|
|
114
|
+
function buildAuthUrl(owner, repo, token) {
|
|
115
|
+
const u = new URL(`https://github.com/${owner}/${repo}.git`);
|
|
116
|
+
u.username = 'x-access-token';
|
|
117
|
+
u.password = token;
|
|
118
|
+
return u.toString();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Strip any embedded push credential from captured git output before it is logged
|
|
122
|
+
// or returned. The auth URL is x-access-token:<token>@github.com/…; git usually
|
|
123
|
+
// redacts the password in its own messages, but belt-and-brace it here too (task
|
|
124
|
+
// 1547 — we now SURFACE git stderr for diagnosability, so it must be scrubbed).
|
|
125
|
+
function scrubCredential(s) {
|
|
126
|
+
return String(s || '')
|
|
127
|
+
.replace(/x-access-token:[^@\s/]+@/gi, 'x-access-token:***@')
|
|
128
|
+
.replace(/(ghs_|ghp_|github_pat_)[A-Za-z0-9_]+/g, '$1***');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Default git seam: execFile arg-array, shell:false, stdout captured. On failure
|
|
132
|
+
// throws GIT_FAILED. We never echo the (possibly credentialed) argv, but DO attach
|
|
133
|
+
// the credential-scrubbed stderr + the caller's step label (task 1547) so a real
|
|
134
|
+
// failure is a one-line log read instead of a multi-hour mystery — the BV1.R04/R05
|
|
135
|
+
// outage was a ~2h opaque GIT_FAILED window with no clue WHICH step or WHY. Tests
|
|
136
|
+
// inject their own `exec`.
|
|
137
|
+
async function defaultExec(args, opts = {}) {
|
|
138
|
+
const { step, ...execOpts } = opts;
|
|
139
|
+
try {
|
|
140
|
+
const { stdout } = await execFileP('git', args, {
|
|
141
|
+
encoding: 'utf8',
|
|
142
|
+
timeout: 120000,
|
|
143
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
144
|
+
...execOpts,
|
|
145
|
+
shell: false,
|
|
146
|
+
});
|
|
147
|
+
return stdout;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
// Identify the failing step by the caller's label, never by echoing argv.
|
|
150
|
+
// Surface the scrubbed stderr (truncated) so the cause is loud in the log.
|
|
151
|
+
const stderr = scrubCredential(e && (e.stderr || e.message)).trim().slice(0, 500);
|
|
152
|
+
throw err('GIT_FAILED', { git_step: step || (args && args[0]) || null, stderr });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Default GitHub-API seam (raw fetch). Tests inject `fetchImpl`.
|
|
157
|
+
function ghHeaders(token) {
|
|
158
|
+
return {
|
|
159
|
+
Authorization: `Bearer ${token}`,
|
|
160
|
+
Accept: 'application/vnd.github+json',
|
|
161
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
162
|
+
'User-Agent': userAgent(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// GitHub App credential (task 1028 / ADR 0055 hardening)
|
|
168
|
+
// Hold only the App private key at rest; mint a short-lived, repo-scoped
|
|
169
|
+
// installation token per push window instead of a standing PAT.
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
function b64url(input) {
|
|
173
|
+
return Buffer.from(input)
|
|
174
|
+
.toString('base64')
|
|
175
|
+
.replace(/\+/g, '-')
|
|
176
|
+
.replace(/\//g, '_')
|
|
177
|
+
.replace(/=+$/, '');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A PEM may arrive as a single env-line with literal "\n"; restore real newlines
|
|
181
|
+
// (and tolerate a key already containing real newlines).
|
|
182
|
+
function normalizePem(pem) {
|
|
183
|
+
return pem.includes('\\n') && !pem.includes('\n') ? pem.replace(/\\n/g, '\n') : pem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Mint an App JWT (RS256, node:crypto — no jsonwebtoken dependency). iat is
|
|
187
|
+
// backdated 60s for clock skew; exp is +9min (GitHub caps App JWTs at 10).
|
|
188
|
+
function mintAppJwt({ appId, privateKeyPem, nowSec }) {
|
|
189
|
+
const iat = nowSec - 60;
|
|
190
|
+
const exp = nowSec + 9 * 60;
|
|
191
|
+
const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
|
|
192
|
+
const payload = b64url(JSON.stringify({ iat, exp, iss: String(appId) }));
|
|
193
|
+
const signingInput = `${header}.${payload}`;
|
|
194
|
+
const signer = crypto.createSign('RSA-SHA256');
|
|
195
|
+
signer.update(signingInput);
|
|
196
|
+
signer.end();
|
|
197
|
+
const sig = b64url(signer.sign(normalizePem(privateKeyPem)));
|
|
198
|
+
return `${signingInput}.${sig}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Module-level cache of the current installation token, reused until ~60s before
|
|
202
|
+
// expiry. Installation tokens are short-lived by construction (1h) and repo-
|
|
203
|
+
// scoped to the single repo this server serves.
|
|
204
|
+
let _instCache = { token: null, expEpoch: 0 };
|
|
205
|
+
function _resetAppTokenCacheForTests() {
|
|
206
|
+
_instCache = { token: null, expEpoch: 0 };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Returns a fresh/cached App installation token, or null if the App isn't
|
|
210
|
+
// configured (so resolveToken can fall through to the PAT). Throws
|
|
211
|
+
// APP_TOKEN_FAILED if the App IS configured but the exchange fails.
|
|
212
|
+
async function getInstallationToken(deps = {}) {
|
|
213
|
+
if (!appConfigured()) return null;
|
|
214
|
+
const nowSec = Math.floor((deps.now ? deps.now() : Date.now()) / 1000);
|
|
215
|
+
if (_instCache.token && _instCache.expEpoch - nowSec > 60) return _instCache.token;
|
|
216
|
+
|
|
217
|
+
const jwt = mintAppJwt({
|
|
218
|
+
appId: process.env.GITHUB_APP_ID,
|
|
219
|
+
privateKeyPem: process.env.GITHUB_APP_PRIVATE_KEY,
|
|
220
|
+
nowSec,
|
|
221
|
+
});
|
|
222
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
223
|
+
const res = await doFetch(
|
|
224
|
+
`https://api.github.com/app/installations/${process.env.GITHUB_APP_INSTALLATION_ID}/access_tokens`,
|
|
225
|
+
{
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: {
|
|
228
|
+
Authorization: `Bearer ${jwt}`,
|
|
229
|
+
Accept: 'application/vnd.github+json',
|
|
230
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
231
|
+
'User-Agent': userAgent(),
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
const detail = await res.text().catch(() => '');
|
|
237
|
+
throw err('APP_TOKEN_FAILED', { status: res.status, detail: detail.slice(0, 200) });
|
|
238
|
+
}
|
|
239
|
+
const data = await res.json();
|
|
240
|
+
_instCache = {
|
|
241
|
+
token: data.token,
|
|
242
|
+
expEpoch: Math.floor(new Date(data.expires_at).getTime() / 1000),
|
|
243
|
+
};
|
|
244
|
+
return data.token;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Resolve the credential for git push + GitHub API: an explicitly-injected token
|
|
248
|
+
// (tests) first, then a GitHub App installation token, then the v1 PAT, else null.
|
|
249
|
+
async function resolveToken(deps = {}) {
|
|
250
|
+
if (deps.token) return deps.token;
|
|
251
|
+
const appTok = await getInstallationToken(deps);
|
|
252
|
+
if (appTok) return appTok;
|
|
253
|
+
return process.env.GITHUB_PUSH_TOKEN || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// pushBundle — receive a thin branch bundle, push it to origin via the server's
|
|
258
|
+
// credential. Returns { head_sha } (the tip actually pushed). Throws:
|
|
259
|
+
// PUSH_UNCONFIGURED — no server push credential (neither GitHub App nor PAT)
|
|
260
|
+
// NO_REPO_INFO — couldn't resolve owner/repo
|
|
261
|
+
// BAD_BRANCH — branch ref failed the safe pattern
|
|
262
|
+
// TIP_MISMATCH — the bundle's tip != the claimed head_sha (with {tip})
|
|
263
|
+
// GIT_FAILED — any git step failed
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
async function pushBundle({ bundlePath, branch, headSha }, deps = {}) {
|
|
266
|
+
const token = await resolveToken(deps);
|
|
267
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
268
|
+
if (!isSafeBranch(branch)) throw err('BAD_BRANCH');
|
|
269
|
+
if (headSha != null && !SAFE_SHA_RE.test(String(headSha))) throw err('BAD_SHA');
|
|
270
|
+
|
|
271
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
272
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
273
|
+
const { owner, repo } = repoInfo;
|
|
274
|
+
|
|
275
|
+
const exec = deps.exec || defaultExec;
|
|
276
|
+
const repoRoot = deps.repoRoot || REPO_ROOT;
|
|
277
|
+
const authUrl = buildAuthUrl(owner, repo, token);
|
|
278
|
+
const ref = `refs/heads/${branch}`;
|
|
279
|
+
|
|
280
|
+
const tmp = (deps.mkdtemp || (() => fs.mkdtempSync(path.join(os.tmpdir(), 'otb-publish-'))))();
|
|
281
|
+
const workGit = path.join(tmp, 'work.git');
|
|
282
|
+
try {
|
|
283
|
+
// 1. Fast LOCAL bare clone for main's objects (no network, no worktree touch).
|
|
284
|
+
// Fall back to a shallow network clone if the local checkout isn't a repo
|
|
285
|
+
// (stripped container, test env).
|
|
286
|
+
try {
|
|
287
|
+
await exec(['clone', '--bare', '--local', repoRoot, workGit], { step: 'clone-local' });
|
|
288
|
+
} catch (_) {
|
|
289
|
+
await exec(['clone', '--bare', '--depth=200', authUrl, workGit], { step: 'clone-network' });
|
|
290
|
+
}
|
|
291
|
+
// 2. Top up recent `main` objects so a freshly-branched base commit is present
|
|
292
|
+
// even if the droplet checkout trails origin/main (just-merged-not-deployed).
|
|
293
|
+
// Best-effort: the bundle is usually self-sufficient against the local main.
|
|
294
|
+
try {
|
|
295
|
+
await exec(['-C', workGit, 'fetch', '--no-tags', authUrl, 'main'], { step: 'fetch-main' });
|
|
296
|
+
} catch (_) {
|
|
297
|
+
/* tolerated — step 3 fails loudly if a prerequisite is genuinely missing */
|
|
298
|
+
}
|
|
299
|
+
// 3. Unbundle the thin branch bundle (prerequisites = main, present above).
|
|
300
|
+
await exec(['-C', workGit, 'fetch', bundlePath, `${ref}:${ref}`], { step: 'unbundle' });
|
|
301
|
+
// 4. Verify the unbundled tip equals the claimed head_sha (anti-spoof: the
|
|
302
|
+
// grade ran against THIS sha client-side; the pushed code must match).
|
|
303
|
+
const tip = (await exec(['-C', workGit, 'rev-parse', ref], { step: 'rev-parse' })).trim();
|
|
304
|
+
if (headSha && tip !== headSha) throw err('TIP_MISMATCH', { tip });
|
|
305
|
+
// 5. Push to GitHub via the explicit authenticated URL (never the inherited
|
|
306
|
+
// remote). Refuse non-fast-forward implicitly (no --force, ever).
|
|
307
|
+
await exec(['-C', workGit, 'push', authUrl, `${ref}:${ref}`], { step: 'push' });
|
|
308
|
+
return { head_sha: tip };
|
|
309
|
+
} finally {
|
|
310
|
+
try {
|
|
311
|
+
(deps.rmtmp || ((d) => fs.rmSync(d, { recursive: true, force: true })))(tmp);
|
|
312
|
+
} catch (_) {
|
|
313
|
+
/* temp cleanup is best-effort */
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// openOrFindPullRequest — open a PR branch->main, or reuse the existing open one
|
|
320
|
+
// (gh pr create is not idempotent; mirror ciLand's "check first" behaviour).
|
|
321
|
+
// Returns { number, url, node_id }.
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
async function openOrFindPullRequest({ branch, title, body }, deps = {}) {
|
|
324
|
+
const token = await resolveToken(deps);
|
|
325
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
326
|
+
if (!isSafeBranch(branch)) throw err('BAD_BRANCH');
|
|
327
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
328
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
329
|
+
const { owner, repo } = repoInfo;
|
|
330
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
331
|
+
const api = `https://api.github.com/repos/${owner}/${repo}`;
|
|
332
|
+
|
|
333
|
+
// Reuse an existing open PR for this head if one exists.
|
|
334
|
+
const listRes = await doFetch(
|
|
335
|
+
`${api}/pulls?head=${encodeURIComponent(`${owner}:${branch}`)}&state=open`,
|
|
336
|
+
{ headers: ghHeaders(token) }
|
|
337
|
+
);
|
|
338
|
+
if (listRes.ok) {
|
|
339
|
+
const existing = await listRes.json();
|
|
340
|
+
if (Array.isArray(existing) && existing.length) {
|
|
341
|
+
const pr = existing[0];
|
|
342
|
+
return { number: pr.number, url: pr.html_url, node_id: pr.node_id };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const createRes = await doFetch(`${api}/pulls`, {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
349
|
+
body: JSON.stringify({ title, head: branch, base: 'main', body }),
|
|
350
|
+
});
|
|
351
|
+
if (!createRes.ok) {
|
|
352
|
+
const detail = await createRes.text().catch(() => '');
|
|
353
|
+
throw err('PR_CREATE_FAILED', { status: createRes.status, detail: detail.slice(0, 300) });
|
|
354
|
+
}
|
|
355
|
+
const pr = await createRes.json();
|
|
356
|
+
return { number: pr.number, url: pr.html_url, node_id: pr.node_id };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// enableAutoMerge — arm GitHub to merge once required checks + branch protection
|
|
360
|
+
// pass (the GraphQL analogue of `gh pr merge --auto --merge`). mergeMethod MERGE
|
|
361
|
+
// keeps the project's --no-ff merge-commit convention. Best-effort: a repo
|
|
362
|
+
// without auto-merge enabled, or a PR already mergeable, returns false and the
|
|
363
|
+
// caller surfaces the open PR (exactly like ciLand on a failed enable).
|
|
364
|
+
async function enableAutoMerge({ nodeId }, deps = {}) {
|
|
365
|
+
const token = await resolveToken(deps);
|
|
366
|
+
if (!token || !nodeId) return false;
|
|
367
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
368
|
+
const query = `mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:MERGE}){clientMutationId}}`;
|
|
369
|
+
try {
|
|
370
|
+
const res = await doFetch('https://api.github.com/graphql', {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
373
|
+
body: JSON.stringify({ query, variables: { id: nodeId } }),
|
|
374
|
+
});
|
|
375
|
+
if (!res.ok) return false;
|
|
376
|
+
const out = await res.json();
|
|
377
|
+
return !out.errors;
|
|
378
|
+
} catch (_) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// getPublishStatus — report the PR + prod-deploy state so ship.js can poll
|
|
384
|
+
// WITHOUT gh on the box. Returns:
|
|
385
|
+
// { pr_state, merged, merge_sha, deploy_status } where
|
|
386
|
+
// pr_state ∈ open|closed|none, deploy_status ∈ pending|success|none.
|
|
387
|
+
// Mirrors ship.js pollCiDeploy's two-stage check (PR merged -> deploy run).
|
|
388
|
+
async function getPublishStatus({ branch }, deps = {}) {
|
|
389
|
+
const token = await resolveToken(deps);
|
|
390
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
391
|
+
if (!isSafeBranch(branch)) throw err('BAD_BRANCH');
|
|
392
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
393
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
394
|
+
const { owner, repo } = repoInfo;
|
|
395
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
396
|
+
const api = `https://api.github.com/repos/${owner}/${repo}`;
|
|
397
|
+
|
|
398
|
+
// Find the most-recent PR for this head (open OR closed/merged).
|
|
399
|
+
const prRes = await doFetch(
|
|
400
|
+
`${api}/pulls?head=${encodeURIComponent(`${owner}:${branch}`)}&state=all&sort=created&direction=desc&per_page=1`,
|
|
401
|
+
{ headers: ghHeaders(token) }
|
|
402
|
+
);
|
|
403
|
+
if (!prRes.ok) throw err('STATUS_FETCH_FAILED', { status: prRes.status });
|
|
404
|
+
const prs = await prRes.json();
|
|
405
|
+
if (!Array.isArray(prs) || !prs.length) {
|
|
406
|
+
return {
|
|
407
|
+
pr_state: 'none', merged: false, merge_sha: null, deploy_status: 'none',
|
|
408
|
+
pr_url: null, pr_number: null, pr_title: null, pr_body: null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const pr = prs[0];
|
|
412
|
+
const merged = !!pr.merged_at;
|
|
413
|
+
const mergeSha = pr.merge_commit_sha || null;
|
|
414
|
+
// task 1547: surface the matched PR's identity so the reconcile can verify the PR
|
|
415
|
+
// belongs to THIS task (anti branch-reuse) when no published tip was recorded.
|
|
416
|
+
const prMeta = { pr_number: pr.number || null, pr_title: pr.title || null, pr_body: pr.body || null };
|
|
417
|
+
if (!merged) {
|
|
418
|
+
return { pr_state: pr.state, merged: false, merge_sha: mergeSha, deploy_status: 'pending', pr_url: pr.html_url, ...prMeta };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Merged → did the prod-deploy workflow succeed for the merge commit?
|
|
422
|
+
let deployStatus = 'pending';
|
|
423
|
+
try {
|
|
424
|
+
const runsRes = await doFetch(
|
|
425
|
+
`${api}/actions/workflows/deploy-prod.yml/runs?branch=main&per_page=15`,
|
|
426
|
+
{ headers: ghHeaders(token) }
|
|
427
|
+
);
|
|
428
|
+
if (runsRes.ok) {
|
|
429
|
+
const data = await runsRes.json();
|
|
430
|
+
const runs = (data && data.workflow_runs) || [];
|
|
431
|
+
const hit = runs.some(
|
|
432
|
+
(r) => r && r.head_sha === mergeSha && r.status === 'completed' && r.conclusion === 'success'
|
|
433
|
+
);
|
|
434
|
+
if (hit) deployStatus = 'success';
|
|
435
|
+
}
|
|
436
|
+
} catch (_) {
|
|
437
|
+
/* leave pending on a transient runs-API miss */
|
|
438
|
+
}
|
|
439
|
+
return { pr_state: pr.state, merged: true, merge_sha: mergeSha, deploy_status: deployStatus, pr_url: pr.html_url, ...prMeta };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// task 1547 — does the PR matched by getPublishStatus actually belong to THIS task?
|
|
443
|
+
// The robust land proof is published-tip containment; this is the fallback used when
|
|
444
|
+
// no published tip was recorded (a legacy row, or a publish in flight across the
|
|
445
|
+
// deploy that introduced the tip column). The server-mediated publish writes
|
|
446
|
+
// "task <id>:" in the PR title and "for task <id>" in the body (routes/tasks.js),
|
|
447
|
+
// and ship.js's local path titles its PR "task <id>: …" too, so a word-boundary
|
|
448
|
+
// "task #?<id>" match positively binds the matched PR to this task — and a REUSED
|
|
449
|
+
// branch whose latest PR was a DIFFERENT task (the BV1.R04/R05 → PR #386/task 1297
|
|
450
|
+
// case) fails the match, so the reconcile refuses to ship. Pure + sync.
|
|
451
|
+
function prReferencesTask(status, taskId) {
|
|
452
|
+
if (!status || !Number.isInteger(Number(taskId))) return false;
|
|
453
|
+
const hay = `${status.pr_title || ''}\n${status.pr_body || ''}`;
|
|
454
|
+
return new RegExp(`\\btask\\s+#?${Number(taskId)}\\b`, 'i').test(hay);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// Active merge driver (task 1162) — land a green PR the moment it's mergeable
|
|
459
|
+
// instead of waiting on GitHub's auto-merge daemon.
|
|
460
|
+
//
|
|
461
|
+
// WHY. enableAutoMerge only ARMS GitHub; GitHub's auto-merge fires on check-
|
|
462
|
+
// completion EVENTS. When a publish is re-driven and auto-merge is re-armed on a
|
|
463
|
+
// PR whose checks are ALREADY green, no fresh event arrives and the daemon never
|
|
464
|
+
// re-evaluates — the PR sits open + CLEAN + mergeable indefinitely, getPublishStatus
|
|
465
|
+
// reports merged:false forever, and the task strands at 'confirmed' (observed live
|
|
466
|
+
// on PR #125 / task 1093: 7/7 checks green, CLEAN, ~30+ min, never merged, needed a
|
|
467
|
+
// human click). This makes the SERVER perform the merge.
|
|
468
|
+
//
|
|
469
|
+
// SAFETY. The merge uses the same server credential that pushed the branch + opened
|
|
470
|
+
// the PR — it merges exactly where it is already allowed and CANNOT bypass branch
|
|
471
|
+
// protection. We merge ONLY when GitHub's own gate is satisfied (mergeable AND
|
|
472
|
+
// mergeStateStatus===CLEAN, the same condition GitHub auto-merge waits for). A
|
|
473
|
+
// review-gated PR the App can't approve reports BLOCKED here and the merge API 405s
|
|
474
|
+
// — we surface that reason, we never force it.
|
|
475
|
+
|
|
476
|
+
// getMergeState — one GraphQL read of the most-recent PR for `branch`: its merged
|
|
477
|
+
// flag, mergeability, merge-state (the CLEAN/BLOCKED/… GitHub's auto-merge gates
|
|
478
|
+
// on), review decision, head sha, and per-check rollup. Pure w.r.t. injected
|
|
479
|
+
// fetchImpl/repoInfo/token so it's unit-tested with no network.
|
|
480
|
+
async function getMergeState({ branch }, deps = {}) {
|
|
481
|
+
const token = await resolveToken(deps);
|
|
482
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
483
|
+
if (!isSafeBranch(branch)) throw err('BAD_BRANCH');
|
|
484
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
485
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
486
|
+
const { owner, repo } = repoInfo;
|
|
487
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
488
|
+
const query = `query($owner:String!,$repo:String!,$head:String!){
|
|
489
|
+
repository(owner:$owner,name:$repo){
|
|
490
|
+
pullRequests(headRefName:$head, first:1, orderBy:{field:CREATED_AT,direction:DESC}){
|
|
491
|
+
nodes{
|
|
492
|
+
number id state merged mergeable reviewDecision mergeStateStatus
|
|
493
|
+
commits(last:1){ nodes{ commit{ oid
|
|
494
|
+
statusCheckRollup{ state contexts(first:50){ nodes{
|
|
495
|
+
__typename
|
|
496
|
+
... on CheckRun{ name conclusion status }
|
|
497
|
+
... on StatusContext{ context state }
|
|
498
|
+
}}}
|
|
499
|
+
}}}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}`;
|
|
504
|
+
const res = await doFetch('https://api.github.com/graphql', {
|
|
505
|
+
method: 'POST',
|
|
506
|
+
headers: {
|
|
507
|
+
...ghHeaders(token),
|
|
508
|
+
'Content-Type': 'application/json',
|
|
509
|
+
// mergeStateStatus historically required this preview media type; GA now,
|
|
510
|
+
// but sending it is harmless and belt-and-braces against older endpoints.
|
|
511
|
+
Accept: 'application/vnd.github.merge-info-preview+json',
|
|
512
|
+
},
|
|
513
|
+
body: JSON.stringify({ query, variables: { owner, repo, head: branch } }),
|
|
514
|
+
});
|
|
515
|
+
if (!res.ok) throw err('STATUS_FETCH_FAILED', { status: res.status });
|
|
516
|
+
const out = await res.json();
|
|
517
|
+
// Partial FORBIDDEN on the check rollup (App lacks Checks:read) must not sink the whole
|
|
518
|
+
// read: mergeIfGreen gates on mergeable/mergeStateStatus (top-level, readable), and the
|
|
519
|
+
// checks array only feeds the human-readable block reason.
|
|
520
|
+
const { repository } = graphqlRepository(out);
|
|
521
|
+
const node = repository.pullRequests
|
|
522
|
+
&& repository.pullRequests.nodes && repository.pullRequests.nodes[0];
|
|
523
|
+
if (!node) {
|
|
524
|
+
return {
|
|
525
|
+
pr_state: 'none', number: null, node_id: null, merged: false, head_sha: null,
|
|
526
|
+
mergeable: null, merge_state: null, review_decision: null, rollup_state: null, checks: [],
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const commit = (node.commits && node.commits.nodes && node.commits.nodes[0] && node.commits.nodes[0].commit) || {};
|
|
530
|
+
const rollup = commit.statusCheckRollup || null;
|
|
531
|
+
// .filter(Boolean): a partial FORBIDDEN (App lacks Checks:read) nulls the individual
|
|
532
|
+
// CheckRun ELEMENTS inside contexts.nodes; drop the holes before mapping.
|
|
533
|
+
const checks = ((rollup && rollup.contexts && rollup.contexts.nodes) || []).filter(Boolean).map((c) =>
|
|
534
|
+
c.__typename === 'CheckRun'
|
|
535
|
+
? { name: c.name, conclusion: c.conclusion, status: c.status }
|
|
536
|
+
: { name: c.context, conclusion: c.state, status: 'COMPLETED' }
|
|
537
|
+
);
|
|
538
|
+
return {
|
|
539
|
+
pr_state: String(node.state || '').toLowerCase(), // open|closed|merged
|
|
540
|
+
number: node.number,
|
|
541
|
+
node_id: node.id,
|
|
542
|
+
merged: !!node.merged,
|
|
543
|
+
head_sha: commit.oid || null,
|
|
544
|
+
mergeable: node.mergeable || null, // MERGEABLE|CONFLICTING|UNKNOWN
|
|
545
|
+
merge_state: node.mergeStateStatus || null, // CLEAN|BLOCKED|BEHIND|DIRTY|DRAFT|UNSTABLE|HAS_HOOKS|UNKNOWN
|
|
546
|
+
review_decision: node.reviewDecision || null, // APPROVED|REVIEW_REQUIRED|CHANGES_REQUESTED|null
|
|
547
|
+
rollup_state: (rollup && rollup.state) || null, // SUCCESS|PENDING|FAILURE|ERROR|EXPECTED
|
|
548
|
+
checks,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// A non-passing check conclusion (everything that isn't a clean success/skip).
|
|
553
|
+
const _BAD_CHECK = (c) =>
|
|
554
|
+
c.conclusion && !['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(String(c.conclusion).toUpperCase());
|
|
555
|
+
|
|
556
|
+
// Human-readable reason a not-yet-merged PR isn't mergeable, surfaced to a
|
|
557
|
+
// credential-less session so it reports the truth instead of guessing.
|
|
558
|
+
function mergeBlockReason(s) {
|
|
559
|
+
if (!s) return 'unknown';
|
|
560
|
+
if (s.merged) return null;
|
|
561
|
+
if (s.pr_state !== 'open') return 'no_open_pr';
|
|
562
|
+
if (s.mergeable === 'CONFLICTING' || s.merge_state === 'DIRTY') return 'merge_conflict';
|
|
563
|
+
if (s.merge_state === 'BEHIND') return 'branch_behind_base';
|
|
564
|
+
if (s.merge_state === 'DRAFT') return 'draft_pr';
|
|
565
|
+
if (s.review_decision === 'CHANGES_REQUESTED') return 'changes_requested';
|
|
566
|
+
if (s.review_decision === 'REVIEW_REQUIRED') return 'review_required';
|
|
567
|
+
if (s.merge_state === 'BLOCKED') {
|
|
568
|
+
const bad = s.checks.filter(_BAD_CHECK);
|
|
569
|
+
if (bad.length) return `failing_check:${bad[0].name}`;
|
|
570
|
+
return 'blocked_required_review_or_pending_check';
|
|
571
|
+
}
|
|
572
|
+
if (s.merge_state === 'UNSTABLE') {
|
|
573
|
+
const bad = s.checks.filter(_BAD_CHECK);
|
|
574
|
+
return bad.length ? `non_required_check_failing:${bad[0].name}` : 'non_required_check_pending';
|
|
575
|
+
}
|
|
576
|
+
if (s.mergeable === 'UNKNOWN' || s.merge_state === 'UNKNOWN' || s.merge_state == null) {
|
|
577
|
+
return 'mergeability_computing';
|
|
578
|
+
}
|
|
579
|
+
return String(s.merge_state).toLowerCase();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// mergeIfGreen — read the PR's merge state and, IFF GitHub's own gate is satisfied
|
|
583
|
+
// (mergeable AND CLEAN), perform the merge the auto-merge daemon didn't. Returns a
|
|
584
|
+
// rich result that doubles as diagnostics for the publish-status route:
|
|
585
|
+
// { pr_state, number, merged, merged_now, mergeable, merge_state,
|
|
586
|
+
// review_decision, rollup_state, checks, reason }
|
|
587
|
+
// merged_now is true ONLY when this call performed the merge. reason is the blocker
|
|
588
|
+
// when merged stays false. Best-effort merge: a 405 (review-gated)/409 (head moved)
|
|
589
|
+
// leaves the PR for the next sweep with the status surfaced, never forced.
|
|
590
|
+
async function mergeIfGreen({ branch }, deps = {}) {
|
|
591
|
+
const s = await getMergeState({ branch }, deps);
|
|
592
|
+
const base = {
|
|
593
|
+
pr_state: s.pr_state, number: s.number, merged: s.merged, merged_now: false,
|
|
594
|
+
mergeable: s.mergeable, merge_state: s.merge_state, review_decision: s.review_decision,
|
|
595
|
+
rollup_state: s.rollup_state, checks: s.checks, reason: null,
|
|
596
|
+
};
|
|
597
|
+
if (s.merged) return base;
|
|
598
|
+
if (s.pr_state !== 'open') return { ...base, reason: 'no_open_pr' };
|
|
599
|
+
if (!(s.mergeable === 'MERGEABLE' && s.merge_state === 'CLEAN')) {
|
|
600
|
+
return { ...base, reason: mergeBlockReason(s) };
|
|
601
|
+
}
|
|
602
|
+
// Green + stalled → merge it. mergeMethod MERGE keeps the --no-ff convention
|
|
603
|
+
// (matches enableAutoMerge). Pin the expected head sha so a race that moved the
|
|
604
|
+
// tip 409s instead of merging stale code.
|
|
605
|
+
const token = await resolveToken(deps);
|
|
606
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
607
|
+
const { owner, repo } = repoInfo;
|
|
608
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
609
|
+
const res = await doFetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${s.number}/merge`, {
|
|
610
|
+
method: 'PUT',
|
|
611
|
+
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
612
|
+
body: JSON.stringify({ merge_method: 'merge', ...(s.head_sha ? { sha: s.head_sha } : {}) }),
|
|
613
|
+
});
|
|
614
|
+
if (res.ok) return { ...base, merged: true, merged_now: true };
|
|
615
|
+
const detail = await res.text().catch(() => '');
|
|
616
|
+
return { ...base, reason: `merge_api_${res.status}`, detail: detail.slice(0, 160) };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// task 1139 (ADR 0058): server-set commit status — the TRUSTLESS rank attestation the
|
|
620
|
+
// gate-review CI check reads. Only the server (holding the App/PAT credential AND the
|
|
621
|
+
// authenticated builder's server-resolved rank) can set it, so CI can trust it where it
|
|
622
|
+
// must NOT trust a PR-author-controlled signal (a PR title is spoofable). Best-effort:
|
|
623
|
+
// returns false on any failure, in which case the gate-review check sees no attestation
|
|
624
|
+
// and escalates — the SAFE direction (no rank bypass without a real server signal).
|
|
625
|
+
async function setCommitStatus({ sha, state, context, description, targetUrl }, deps = {}) {
|
|
626
|
+
const token = await resolveToken(deps);
|
|
627
|
+
if (!token) return false;
|
|
628
|
+
if (!SAFE_SHA_RE.test(String(sha || ''))) return false;
|
|
629
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
630
|
+
if (!repoInfo || repoInfo.error) return false;
|
|
631
|
+
const { owner, repo } = repoInfo;
|
|
632
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
633
|
+
try {
|
|
634
|
+
const res = await doFetch(`https://api.github.com/repos/${owner}/${repo}/statuses/${sha}`, {
|
|
635
|
+
method: 'POST',
|
|
636
|
+
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
637
|
+
body: JSON.stringify({
|
|
638
|
+
state,
|
|
639
|
+
context,
|
|
640
|
+
description: String(description || '').slice(0, 140),
|
|
641
|
+
...(targetUrl ? { target_url: targetUrl } : {}),
|
|
642
|
+
}),
|
|
643
|
+
});
|
|
644
|
+
return res.ok;
|
|
645
|
+
} catch (_) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// task 1049 (B — server-side reconcile-on-read): does a publish-status report a
|
|
651
|
+
// fully-landed change — PR MERGED and deploy-prod SUCCEEDED? When true, the
|
|
652
|
+
// publish-status route flips the task confirmed→shipped so a successful publish
|
|
653
|
+
// never strands at confirmed waiting on a human to re-poll (the #1027
|
|
654
|
+
// retrospective). Pure + exported so the route's reconcile trigger is unit-tested.
|
|
655
|
+
function landConfirmed(status) {
|
|
656
|
+
return !!status && status.merged === true && status.deploy_status === 'success';
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// task 1547 — is a landConfirmed status PROVEN to be THIS task's land, and not a
|
|
660
|
+
// reused worktree branch matching an OLDER task's already-merged PR (the BV1.R04/R05
|
|
661
|
+
// false-ship: branch claude/jovial-rosalind-84d6c3 → PR #386 / task 1297, merged,
|
|
662
|
+
// matched again for tasks 1299/1300 whose own push had failed)?
|
|
663
|
+
//
|
|
664
|
+
// The robust proof is the SERVER-pushed tip (claims.published_head_sha, task 1547):
|
|
665
|
+
// it must be an ancestor of main. NOTE claims.head_sha is the claim-TIME base — for a
|
|
666
|
+
// task whose base is already on main (e.g. an R03 base) an ancestry check on it
|
|
667
|
+
// false-positives — so we deliberately key on the published tip, never the base.
|
|
668
|
+
// When no tip was recorded (a legacy row, or a publish in flight across the deploy
|
|
669
|
+
// that introduced the column), fall back to verifying the matched PR references this
|
|
670
|
+
// task id. Returns { proven, reason }. The compare call is the only network hop and
|
|
671
|
+
// is injected-deps-pure for tests.
|
|
672
|
+
async function landProvenForTask({ taskId, publishedSha, status }, deps = {}) {
|
|
673
|
+
if (publishedSha && SAFE_SHA_RE.test(String(publishedSha))) {
|
|
674
|
+
const inMain = await isAncestorOfDefaultBranch({ sha: publishedSha }, deps);
|
|
675
|
+
return inMain
|
|
676
|
+
? { proven: true, reason: `published tip ${publishedSha.slice(0, 8)} in main` }
|
|
677
|
+
: { proven: false, reason: `published tip ${publishedSha.slice(0, 8)} not in main (branch-reuse/stale PR)` };
|
|
678
|
+
}
|
|
679
|
+
if (prReferencesTask(status, taskId)) {
|
|
680
|
+
return { proven: true, reason: `matched PR references task ${taskId} (no published tip recorded)` };
|
|
681
|
+
}
|
|
682
|
+
return { proven: false, reason: `no published tip recorded and matched PR does not reference task ${taskId}` };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// task 1313 — proof-of-land that does NOT depend on the PR/branch still existing.
|
|
686
|
+
//
|
|
687
|
+
// WHY. getPublishStatus keys a land on finding the head branch's PR (merged +
|
|
688
|
+
// deploy succeeded). But a merge-commit merge that DELETES the branch afterwards
|
|
689
|
+
// leaves no PR discoverable by `head=owner:branch`, so getPublishStatus returns
|
|
690
|
+
// pr_state:'none' forever and the task strands at 'confirmed' even though the work
|
|
691
|
+
// is live on main. Other strand shapes have no branch at all (a claim with no
|
|
692
|
+
// worktree_name) or a corrupt head_sha. The branch/PR is incidental; the durable
|
|
693
|
+
// truth is "is the work's commit in main's history?" — answer that directly.
|
|
694
|
+
//
|
|
695
|
+
// Uses GitHub's compare API: compare/{base}...{head}. The `status` field is one of
|
|
696
|
+
// ahead | behind | identical | diverged, describing HEAD relative to BASE. When
|
|
697
|
+
// the claim's head_sha is an ancestor of main it has NO commits main lacks, so it
|
|
698
|
+
// is `behind` (or `identical` when it IS main's tip) — both mean "contained in
|
|
699
|
+
// main" ⇒ merged. `ahead`/`diverged` mean head carries commits not on main (e.g. a
|
|
700
|
+
// squash-merge produced a different commit, or it never landed) ⇒ NOT landed here;
|
|
701
|
+
// those still heal via the normal PR path, which getPublishStatus handles.
|
|
702
|
+
//
|
|
703
|
+
// SAFETY. sha MUST be a real commit-sha (SAFE_SHA_RE). The compare endpoint
|
|
704
|
+
// resolves refs, so passing a ref like "HEAD" or a branch name would resolve
|
|
705
|
+
// against the repo and could FALSE-POSITIVE (e.g. "HEAD" → main's tip → identical).
|
|
706
|
+
// We reject anything non-sha-shaped up front. A 404 (unknown commit — never pushed)
|
|
707
|
+
// is a clean "not landed", not an error. Pure w.r.t. injected fetchImpl/repoInfo/
|
|
708
|
+
// token so it unit-tests with no network.
|
|
709
|
+
async function isAncestorOfDefaultBranch({ sha, base = 'main' } = {}, deps = {}) {
|
|
710
|
+
if (typeof sha !== 'string' || !SAFE_SHA_RE.test(sha)) return false;
|
|
711
|
+
const token = await resolveToken(deps);
|
|
712
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
713
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
714
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
715
|
+
const { owner, repo } = repoInfo;
|
|
716
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
717
|
+
const res = await doFetch(
|
|
718
|
+
`https://api.github.com/repos/${owner}/${repo}/compare/${encodeURIComponent(base)}...${encodeURIComponent(sha)}`,
|
|
719
|
+
{ headers: ghHeaders(token) }
|
|
720
|
+
);
|
|
721
|
+
if (res.status === 404) return false; // unknown commit → not in main's history
|
|
722
|
+
if (!res.ok) throw err('STATUS_FETCH_FAILED', { status: res.status });
|
|
723
|
+
const data = await res.json();
|
|
724
|
+
return !!data && (data.status === 'behind' || data.status === 'identical');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ---------------------------------------------------------------------------
|
|
728
|
+
// Gate-surface PR approval from the hall (task 1179, ADR 0058 amendment).
|
|
729
|
+
//
|
|
730
|
+
// The gate-review CI check clears an ESCALATE (hard floor) ONLY on an owner
|
|
731
|
+
// approval. Historically that was a GitHub APPROVED review by the owner on the
|
|
732
|
+
// head sha. This adds a SECOND, trust-equal owner-approval channel the hall can
|
|
733
|
+
// drive: a server-set `gate-owner-approved` commit status — set EXACTLY like the
|
|
734
|
+
// existing `gate-author-trust` status (setCommitStatus), so only the server (which
|
|
735
|
+
// re-checks the caller's Archon rank server-side, ADR 0016) can produce it; a PR
|
|
736
|
+
// author cannot forge it. The gate-review workflow's trust step reads it on the
|
|
737
|
+
// head sha (sha-pinned, so a later commit's head carries no approval — the same
|
|
738
|
+
// red-team #2b protection as the GitHub-review path).
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
const GATE_OWNER_APPROVED_CONTEXT = 'gate-owner-approved';
|
|
741
|
+
|
|
742
|
+
// Set the server-attested owner-approval status on `sha`. Thin wrapper over
|
|
743
|
+
// setCommitStatus that pins the context + a provenance description (who approved,
|
|
744
|
+
// via where). Best-effort (returns false on failure → the gate simply stays
|
|
745
|
+
// un-approved, the safe direction). `login` is the approving Archon's handle.
|
|
746
|
+
async function setGateOwnerApproved({ sha, login, targetUrl }, deps = {}) {
|
|
747
|
+
return setCommitStatus(
|
|
748
|
+
{
|
|
749
|
+
sha,
|
|
750
|
+
state: 'success',
|
|
751
|
+
context: GATE_OWNER_APPROVED_CONTEXT,
|
|
752
|
+
description: `owner approval (${login || 'archon'}) via GDS hall`,
|
|
753
|
+
targetUrl,
|
|
754
|
+
},
|
|
755
|
+
deps
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// listOpenPullRequests — one GraphQL read of the open PRs (most-recently-updated
|
|
760
|
+
// first) with the metadata the hall's gate-approval list needs: number, title,
|
|
761
|
+
// url, head ref + head sha, author login, body (for "task NNNN" extraction), and
|
|
762
|
+
// the head commit's check rollup (so the route can read the live `gate-review`
|
|
763
|
+
// check conclusion — the authoritative "is this awaiting approval" signal, rather
|
|
764
|
+
// than re-deriving it). Pure w.r.t. injected fetchImpl/repoInfo/token.
|
|
765
|
+
async function listOpenPullRequests(deps = {}) {
|
|
766
|
+
const token = await resolveToken(deps);
|
|
767
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
768
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
769
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
770
|
+
const { owner, repo } = repoInfo;
|
|
771
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
772
|
+
const first = Math.min(Math.max(Number(deps.first) || 40, 1), 100);
|
|
773
|
+
const query = `query($owner:String!,$repo:String!,$first:Int!){
|
|
774
|
+
repository(owner:$owner,name:$repo){
|
|
775
|
+
pullRequests(states:OPEN, first:$first, orderBy:{field:UPDATED_AT,direction:DESC}){
|
|
776
|
+
nodes{
|
|
777
|
+
number title url isDraft headRefName bodyText
|
|
778
|
+
author{ login }
|
|
779
|
+
reviewDecision
|
|
780
|
+
commits(last:1){ nodes{ commit{ oid
|
|
781
|
+
statusCheckRollup{ state contexts(first:50){ nodes{
|
|
782
|
+
__typename
|
|
783
|
+
... on CheckRun{ name conclusion status detailsUrl }
|
|
784
|
+
... on StatusContext{ context state targetUrl }
|
|
785
|
+
}}}
|
|
786
|
+
}}}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}`;
|
|
791
|
+
const res = await doFetch('https://api.github.com/graphql', {
|
|
792
|
+
method: 'POST',
|
|
793
|
+
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
794
|
+
body: JSON.stringify({ query, variables: { owner, repo, first } }),
|
|
795
|
+
});
|
|
796
|
+
if (!res.ok) throw err('STATUS_FETCH_FAILED', { status: res.status });
|
|
797
|
+
const out = await res.json();
|
|
798
|
+
// Tolerate a partial FORBIDDEN (App lacks Checks:read → statusCheckRollup.contexts
|
|
799
|
+
// nulled): the PR list itself is still usable, so the approve path keeps working; the
|
|
800
|
+
// per-PR `checks` array just comes back empty.
|
|
801
|
+
const { repository, partialErrors } = graphqlRepository(out);
|
|
802
|
+
const nodes = ((repository.pullRequests || {}).nodes || []);
|
|
803
|
+
const result = nodes.map((n) => {
|
|
804
|
+
const commit = (n.commits && n.commits.nodes && n.commits.nodes[0] && n.commits.nodes[0].commit) || {};
|
|
805
|
+
const rollup = commit.statusCheckRollup || null;
|
|
806
|
+
// .filter(Boolean): a partial FORBIDDEN (App lacks Checks:read) nulls the individual
|
|
807
|
+
// CheckRun ELEMENTS inside contexts.nodes (the array survives with null holes); the
|
|
808
|
+
// readable StatusContexts — gate-author-trust / gate-owner-approved — remain.
|
|
809
|
+
const checks = ((rollup && rollup.contexts && rollup.contexts.nodes) || []).filter(Boolean).map((c) =>
|
|
810
|
+
c.__typename === 'CheckRun'
|
|
811
|
+
? { name: c.name, conclusion: c.conclusion, status: c.status, url: c.detailsUrl || null }
|
|
812
|
+
: { name: c.context, conclusion: c.state, status: 'COMPLETED', url: c.targetUrl || null }
|
|
813
|
+
);
|
|
814
|
+
return {
|
|
815
|
+
number: n.number,
|
|
816
|
+
title: n.title || '',
|
|
817
|
+
url: n.url,
|
|
818
|
+
is_draft: !!n.isDraft,
|
|
819
|
+
head_ref: n.headRefName || null,
|
|
820
|
+
head_sha: commit.oid || null,
|
|
821
|
+
author: (n.author && n.author.login) || null,
|
|
822
|
+
body: n.bodyText || '',
|
|
823
|
+
review_decision: n.reviewDecision || null,
|
|
824
|
+
rollup_state: (rollup && rollup.state) || null,
|
|
825
|
+
checks,
|
|
826
|
+
};
|
|
827
|
+
});
|
|
828
|
+
// Signal reduced fidelity (e.g. checks unreadable → App lacks Checks:read) so a caller
|
|
829
|
+
// can say so instead of silently presenting an empty check rollup as "all clear".
|
|
830
|
+
// Non-enumerable: the array is JSON-serialized directly by the GET route and this must
|
|
831
|
+
// not leak into that payload — the route reads it explicitly.
|
|
832
|
+
if (partialErrors.length) {
|
|
833
|
+
Object.defineProperty(result, 'partialErrors', { value: partialErrors, enumerable: false });
|
|
834
|
+
}
|
|
835
|
+
return result;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// getPullRequestFiles — the changed paths of a PR, mapped to the shape
|
|
839
|
+
// classifyChanges() consumes ({ path, mode, deleted }). The REST files API does
|
|
840
|
+
// not return the git file mode, so we default to a regular-file mode; the symlink
|
|
841
|
+
// pre-flight reject is a CI-authoritative edge that does not affect this read-only
|
|
842
|
+
// "why did it escalate" display. Caps at 300 files (3 pages) — far beyond any real
|
|
843
|
+
// gate PR; a larger PR is itself an escalate-by-size in CI.
|
|
844
|
+
async function getPullRequestFiles({ number }, deps = {}) {
|
|
845
|
+
const token = await resolveToken(deps);
|
|
846
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
847
|
+
if (!Number.isInteger(number) || number < 1) throw err('BAD_PR');
|
|
848
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
849
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
850
|
+
const { owner, repo } = repoInfo;
|
|
851
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
852
|
+
const api = `https://api.github.com/repos/${owner}/${repo}`;
|
|
853
|
+
const files = [];
|
|
854
|
+
for (let page = 1; page <= 3; page += 1) {
|
|
855
|
+
const res = await doFetch(`${api}/pulls/${number}/files?per_page=100&page=${page}`, {
|
|
856
|
+
headers: ghHeaders(token),
|
|
857
|
+
});
|
|
858
|
+
if (!res.ok) throw err('STATUS_FETCH_FAILED', { status: res.status });
|
|
859
|
+
const batch = await res.json();
|
|
860
|
+
if (!Array.isArray(batch) || !batch.length) break;
|
|
861
|
+
for (const f of batch) {
|
|
862
|
+
files.push({
|
|
863
|
+
path: f.filename,
|
|
864
|
+
mode: '100644',
|
|
865
|
+
deleted: f.status === 'removed',
|
|
866
|
+
status: f.status,
|
|
867
|
+
additions: f.additions || 0,
|
|
868
|
+
deletions: f.deletions || 0,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (batch.length < 100) break;
|
|
872
|
+
}
|
|
873
|
+
return files;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// How long to wait between the close PATCH and the reopen PATCH (task 1241). See the
|
|
877
|
+
// recheckPullRequest header for WHY this delay exists; overridable via deps.reopenDelayMs
|
|
878
|
+
// so tests run instantly (and an operator can tune it without a code change).
|
|
879
|
+
const RECHECK_REOPEN_DELAY_MS = 4000;
|
|
880
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
881
|
+
|
|
882
|
+
// recheckPullRequest — re-trigger the gate-review check after an owner approval was
|
|
883
|
+
// recorded. WHY close+reopen: setting a commit status does NOT fire a `pull_request`
|
|
884
|
+
// event, so the (red) gate-review check would not re-evaluate the new approval and
|
|
885
|
+
// the required check would stay failing. Re-running the check via the Actions API
|
|
886
|
+
// would need an `actions:write` App permission the publish App is not granted;
|
|
887
|
+
// closing then reopening the PR fires `pull_request: reopened` (a default activity
|
|
888
|
+
// type for the gate-review trigger) using the `pull_requests:write` the App already
|
|
889
|
+
// holds — zero new permission, and the re-run carries the real PR context so the
|
|
890
|
+
// fresh `gate-review` check posts on the correct head sha. Re-arms auto-merge after
|
|
891
|
+
// reopen (closing clears it). Best-effort; the publish reconciler's mergeIfGreen
|
|
892
|
+
// sweep (task 1162) is the backstop if auto-merge does not re-fire.
|
|
893
|
+
//
|
|
894
|
+
// WHY THE DELAY (task 1241). When the close + reopen PATCHes land ~1s apart, GitHub
|
|
895
|
+
// COALESCES them into no net state change and dispatches NO `pull_request: reopened`
|
|
896
|
+
// event — so the gate-review run never re-fires and the required check stays red after
|
|
897
|
+
// an owner Approve (observed on PR #167: close 22:36:39Z, reopen 22:36:40Z by the App
|
|
898
|
+
// bot, zero gate-review runs followed; a later manual rerun cleared it green, proving
|
|
899
|
+
// only the auto-re-trigger was broken). Sleeping a few seconds between the two PATCHes
|
|
900
|
+
// makes GitHub register two DISTINCT state transitions, so the reopen fires a real
|
|
901
|
+
// `reopened` event carrying full PR context. Stays inside the App's existing
|
|
902
|
+
// `pull_requests:write` — no new permission (the deterministic Actions rerun API would
|
|
903
|
+
// need `actions:write`, deliberately not granted; see blocker 37).
|
|
904
|
+
async function recheckPullRequest({ number }, deps = {}) {
|
|
905
|
+
const token = await resolveToken(deps);
|
|
906
|
+
if (!token) throw err('PUSH_UNCONFIGURED');
|
|
907
|
+
if (!Number.isInteger(number) || number < 1) throw err('BAD_PR');
|
|
908
|
+
const repoInfo = deps.repoInfo || loadRepoInfo();
|
|
909
|
+
if (!repoInfo || repoInfo.error) throw err('NO_REPO_INFO');
|
|
910
|
+
const { owner, repo } = repoInfo;
|
|
911
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
912
|
+
const reopenDelayMs = Number.isFinite(deps.reopenDelayMs) ? deps.reopenDelayMs : RECHECK_REOPEN_DELAY_MS;
|
|
913
|
+
const api = `https://api.github.com/repos/${owner}/${repo}`;
|
|
914
|
+
const patchState = async (state) => {
|
|
915
|
+
const res = await doFetch(`${api}/pulls/${number}`, {
|
|
916
|
+
method: 'PATCH',
|
|
917
|
+
headers: { ...ghHeaders(token), 'Content-Type': 'application/json' },
|
|
918
|
+
body: JSON.stringify({ state }),
|
|
919
|
+
});
|
|
920
|
+
if (!res.ok) {
|
|
921
|
+
const detail = await res.text().catch(() => '');
|
|
922
|
+
throw err('PR_PATCH_FAILED', { status: res.status, detail: detail.slice(0, 200) });
|
|
923
|
+
}
|
|
924
|
+
return res.json();
|
|
925
|
+
};
|
|
926
|
+
await patchState('closed');
|
|
927
|
+
// Let GitHub register the close as a distinct state transition before reopening, so
|
|
928
|
+
// the reopen actually dispatches a `pull_request: reopened` event (task 1241).
|
|
929
|
+
if (reopenDelayMs > 0) await sleep(reopenDelayMs);
|
|
930
|
+
const reopened = await patchState('open');
|
|
931
|
+
let autoMerge = false;
|
|
932
|
+
try {
|
|
933
|
+
if (reopened && reopened.node_id) autoMerge = await enableAutoMerge({ nodeId: reopened.node_id }, deps);
|
|
934
|
+
} catch (_) {
|
|
935
|
+
/* auto-merge re-arm is best-effort; mergeIfGreen sweep backstops it */
|
|
936
|
+
}
|
|
937
|
+
return { reopened: true, auto_merge: autoMerge, node_id: (reopened && reopened.node_id) || null };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
module.exports = {
|
|
941
|
+
isConfigured,
|
|
942
|
+
appConfigured,
|
|
943
|
+
isSafeBranch,
|
|
944
|
+
pushBundle,
|
|
945
|
+
openOrFindPullRequest,
|
|
946
|
+
enableAutoMerge,
|
|
947
|
+
getPublishStatus,
|
|
948
|
+
getMergeState,
|
|
949
|
+
mergeIfGreen,
|
|
950
|
+
setCommitStatus,
|
|
951
|
+
setGateOwnerApproved,
|
|
952
|
+
listOpenPullRequests,
|
|
953
|
+
getPullRequestFiles,
|
|
954
|
+
recheckPullRequest,
|
|
955
|
+
GATE_OWNER_APPROVED_CONTEXT,
|
|
956
|
+
landConfirmed,
|
|
957
|
+
landProvenForTask,
|
|
958
|
+
prReferencesTask,
|
|
959
|
+
scrubCredential,
|
|
960
|
+
isAncestorOfDefaultBranch,
|
|
961
|
+
resolveToken,
|
|
962
|
+
getInstallationToken,
|
|
963
|
+
// exported for unit tests
|
|
964
|
+
buildAuthUrl,
|
|
965
|
+
mintAppJwt,
|
|
966
|
+
mergeBlockReason,
|
|
967
|
+
_resetAppTokenCacheForTests,
|
|
968
|
+
SAFE_BRANCH_RE,
|
|
969
|
+
};
|