@1aboveio/skills 0.10.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/LICENSE +3 -0
- package/README.md +90 -0
- package/bin/1aboveio-skills.mjs +18 -0
- package/package.json +28 -0
- package/runtime/skills/distribution/generated/recipes.json +1189 -0
- package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
- package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/backend/airflow-dag-develop/LICENSE +3 -0
- package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
- package/skills/backend/app-debug/LICENSE +3 -0
- package/skills/backend/app-debug/SKILL.md +109 -0
- package/skills/backend/app-debug/references/common-errors.md +128 -0
- package/skills/backend/python-backend/LICENSE +3 -0
- package/skills/backend/python-backend/SKILL.md +326 -0
- package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
- package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
- package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
- package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
- package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd-pipeline/docker/LICENSE +3 -0
- package/skills/cicd-pipeline/docker/SKILL.md +126 -0
- package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
- package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
- package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
- package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
- package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
- package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
- package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
- package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd-pipeline/mergify/LICENSE +3 -0
- package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
- package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
- package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
- package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
- package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
- package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
- package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
- package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
- package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd-pipeline/podman/LICENSE +3 -0
- package/skills/cicd-pipeline/podman/SKILL.md +70 -0
- package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +156 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
- package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
- package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
- package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
- package/skills/engineering/e2e-test/references/debugging.md +36 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
- package/skills/engineering/e2e-test/references/reviewing.md +39 -0
- package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +10 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +136 -0
- package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
- package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
- package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
- package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +18 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/implement-and-pr/LICENSE +3 -0
- package/skills/engineering/implement-and-pr/SKILL.md +176 -0
- package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
- package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
- package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
- package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
- package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
- package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
- package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +167 -0
- package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
- package/skills/engineering/resolve-issues/references/breaker.md +82 -0
- package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
- package/skills/engineering/resolve-issues/references/delivery.md +108 -0
- package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
- package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
- package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
- package/skills/engineering/resolve-issues/references/finalization.md +68 -0
- package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
- package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
- package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
- package/skills/engineering/resolve-issues/references/intake.md +86 -0
- package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
- package/skills/engineering/resolve-issues/references/interference.md +87 -0
- package/skills/engineering/resolve-issues/references/loop.md +134 -0
- package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
- package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
- package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
- package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
- package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
- package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
- package/skills/engineering/resolve-issues/references/run-state.md +288 -0
- package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
- package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
- package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
- package/skills/engineering/resolve-issues/references/why.md +653 -0
- package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
- package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
- package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
- package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
- package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
- package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
- package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
- package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
- package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
- package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
- package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
- package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
- package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
- package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
- package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
- package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
- package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
- package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
- package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
- package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
- package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
- package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
- package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
- package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
- package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
- package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
- package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
- package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
- package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
- package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
- package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
- package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
- package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
- package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
- package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +112 -0
- package/skills/engineering/resolve-release/references/assembly.md +137 -0
- package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
- package/skills/engineering/resolve-release/references/candidate.md +167 -0
- package/skills/engineering/resolve-release/references/exposure.md +178 -0
- package/skills/engineering/resolve-release/references/handoff.md +24 -0
- package/skills/engineering/resolve-release/references/postmortem.md +230 -0
- package/skills/engineering/resolve-release/references/preflight.md +207 -0
- package/skills/engineering/resolve-release/references/principles.md +94 -0
- package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
- package/skills/engineering/resolve-release/references/related-skills.md +12 -0
- package/skills/engineering/resolve-release/references/routing.md +149 -0
- package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
- package/skills/engineering/resolve-release/references/versioning.md +202 -0
- package/skills/engineering/resolve-release/references/why.md +53 -0
- package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
- package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
- package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
- package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
- package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
- package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
- package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
- package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
- package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
- package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
- package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
- package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
- package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
- package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
- package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
- package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
- package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
- package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
- package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
- package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
- package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
- package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
- package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
- package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
- package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
- package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
- package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
- package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
- package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
- package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
- package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/README.md +60 -0
- package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
- package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
- package/skills/engineering/review-pr/LICENSE +3 -0
- package/skills/engineering/review-pr/SKILL.md +123 -0
- package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
- package/skills/engineering/review-pr/references/code-correctness.md +5 -0
- package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
- package/skills/engineering/review-pr/references/coordination.md +18 -0
- package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
- package/skills/engineering/review-pr/references/finding-themes.md +7 -0
- package/skills/engineering/review-pr/references/github-posting.md +98 -0
- package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
- package/skills/engineering/review-pr/references/incremental-output.md +16 -0
- package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
- package/skills/engineering/review-pr/references/output-format.md +99 -0
- package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
- package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
- package/skills/engineering/review-pr/references/re-review.md +18 -0
- package/skills/engineering/review-pr/references/review-method.md +199 -0
- package/skills/engineering/review-pr/references/review-mode.md +30 -0
- package/skills/engineering/review-pr/references/review-posture.md +53 -0
- package/skills/engineering/review-pr/references/round1-depth.md +62 -0
- package/skills/engineering/review-pr/references/scripts.md +17 -0
- package/skills/engineering/review-pr/references/workflow.md +16 -0
- package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
- package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
- package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
- package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
- package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
- package/skills/engineering/smoke/LICENSE +3 -0
- package/skills/engineering/smoke/SKILL.md +131 -0
- package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
- package/skills/engineering/smoke/references/manifest.md +192 -0
- package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
- package/skills/fullstack/better-auth/LICENSE +3 -0
- package/skills/fullstack/better-auth/SKILL.md +601 -0
- package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
- package/skills/fullstack/monorepo/LICENSE +3 -0
- package/skills/fullstack/monorepo/SKILL.md +465 -0
- package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
- package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
- package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
- package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
- package/skills/fullstack/prisma-setup/LICENSE +3 -0
- package/skills/fullstack/prisma-setup/SKILL.md +180 -0
- package/skills/fullstack/prisma-setup/nextjs.md +258 -0
- package/skills/fullstack/prisma-setup/turborepo.md +301 -0
- package/skills/fullstack/shadcn/LICENSE +3 -0
- package/skills/fullstack/shadcn/SKILL.md +119 -0
- package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
- package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
- package/skills/fullstack/shadcn/cli.md +411 -0
- package/skills/fullstack/shadcn/customization.md +224 -0
- package/skills/fullstack/shadcn/evals/evals.json +90 -0
- package/skills/fullstack/shadcn/mcp.md +101 -0
- package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
- package/skills/fullstack/shadcn/rules/composition.md +195 -0
- package/skills/fullstack/shadcn/rules/data-table.md +201 -0
- package/skills/fullstack/shadcn/rules/forms.md +255 -0
- package/skills/fullstack/shadcn/rules/icons.md +103 -0
- package/skills/fullstack/shadcn/rules/styling.md +167 -0
- package/skills/fullstack/zod-v4/LICENSE +3 -0
- package/skills/fullstack/zod-v4/SKILL.md +287 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
// watch-delivery — wait for a PR to reach a terminal delivery state, ONCE,
|
|
2
|
+
// outside the orchestrator's context.
|
|
3
|
+
//
|
|
4
|
+
// On enqueueTrigger ∈ {hybrid, auto} the PR may merge without ever being enqueued
|
|
5
|
+
// (@mergifyio queue never posted). This script still observes MERGED — it does not
|
|
6
|
+
// require everQueued / prior enqueue (#791 / issue-664 / identity#669).
|
|
7
|
+
//
|
|
8
|
+
// Why this is a script and not a loop the orchestrator writes each time.
|
|
9
|
+
//
|
|
10
|
+
// The orchestrator is meant to be a thin scheduler: heavy steps run in spawns that return
|
|
11
|
+
// a compact result, and durable state lives in the manifest. Watching a queue is the
|
|
12
|
+
// opposite of heavy — it is a `gh pr view` every twenty seconds — but it is the single
|
|
13
|
+
// largest consumer of foreground turns in a real run, and every one of those turns costs
|
|
14
|
+
// context the scheduler needs for scheduling.
|
|
15
|
+
//
|
|
16
|
+
// Measured, in this repo's own runs: delivering one PR took 13 foreground `gh pr view`
|
|
17
|
+
// calls. A Pi session delivering an epic spent 36 `gh pr view` + 18 `gh pr checks` + 17
|
|
18
|
+
// `mergify queue` calls in the foreground — 71 polling calls, none of which produced a
|
|
19
|
+
// decision, and that run ended with the orchestrator hand-patching its own manifest JSON
|
|
20
|
+
// because its context had filled. Polling did not cause that on its own, but it is the
|
|
21
|
+
// bulk of what filled the window.
|
|
22
|
+
//
|
|
23
|
+
// So the watch runs here: one launch, one terminal answer, no intermediate turns. What
|
|
24
|
+
// does NOT move is the decision to enqueue or the recording of the outcome — those are
|
|
25
|
+
// guarantee 4's authorization and the audit trail, and they stay with the orchestrator.
|
|
26
|
+
// This script observes; it never acts on the PR, and it takes no merge authorization.
|
|
27
|
+
//
|
|
28
|
+
// node watch-delivery.mjs --pr 557 --repo owner/name [--bound-minutes 10]
|
|
29
|
+
// [--interval-seconds 20] [--provider mergify|github]
|
|
30
|
+
// [--head <sha>] # exact HEAD for the CI→delivery pipeline (#792)
|
|
31
|
+
//
|
|
32
|
+
// OUTPUT (stdout, always JSON): { terminal, outcome, mergedSha, reason, observations, elapsedMs, ciStatus? }
|
|
33
|
+
//
|
|
34
|
+
// EXIT CODES:
|
|
35
|
+
// 0 merged — `mergedSha` is the PR's MERGE COMMIT, and the caller still owes the
|
|
36
|
+
// ancestry proof (`combine-and-verify.mjs delivered`). A merge tool's answer is a
|
|
37
|
+
// request; only ancestry is a result, and this script deliberately does not pretend
|
|
38
|
+
// otherwise.
|
|
39
|
+
// 4 terminal but NOT merged — dequeued, closed, or stalled past the bound. Owes a
|
|
40
|
+
// hand-back with the reason, which is a routing class the caller maps through
|
|
41
|
+
// `dequeueRouting`.
|
|
42
|
+
// 5 CI failed on the exact HEAD (`--head` set) — fold into a fix round; never a
|
|
43
|
+
// success. Distinct from stall so the orchestrator can route without re-deriving
|
|
44
|
+
// the failure class from detail strings.
|
|
45
|
+
// 2 usage error.
|
|
46
|
+
//
|
|
47
|
+
// It never exits 0 on a timeout. "Still going" and "finished" must not collapse into one
|
|
48
|
+
// code, because the caller writes a terminal delivery outcome from this and a wrong
|
|
49
|
+
// `merged` there is the epic-986 shape (four units recorded merge-ready, mergedSha null).
|
|
50
|
+
//
|
|
51
|
+
// #792 / issue-664 / identity#669: when `--head` is set this is the single background
|
|
52
|
+
// CI→delivery pipeline after PASS. Merge is still preferred over CI red (hybrid can
|
|
53
|
+
// land while checks are still settling); CI red is only terminal while the PR is open.
|
|
54
|
+
|
|
55
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
56
|
+
import { existsSync } from 'node:fs'
|
|
57
|
+
import { dirname, resolve as resolvePath } from 'node:path'
|
|
58
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
59
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
60
|
+
import { checksForPhase, validateQueueExecutionArtifact } from '../../engineering-runtime/scripts/required-check-plan.mjs'
|
|
61
|
+
|
|
62
|
+
// ---- pure, unit-testable core ----
|
|
63
|
+
|
|
64
|
+
// A PR's own state is the authority on "did it merge". The queue's state answers "is it
|
|
65
|
+
// still being worked", which is a different question and cannot settle this one.
|
|
66
|
+
// `everQueued` / `absentStreak` carry the one piece of history a single observation cannot:
|
|
67
|
+
// whether this PR was ever *in* the queue. The mergify skill's label lifecycle is explicit
|
|
68
|
+
// that a still-open PR which has been seen queued and then LOSES the label is a queue exit,
|
|
69
|
+
// not a healthy snapshot — and the first version of this function ignored that, treating
|
|
70
|
+
// `{"queued": false}` after observed entry as "keep waiting", which is precisely the
|
|
71
|
+
// reading delivery.md cites that skill to forbid. A watcher that waits out a PR the queue
|
|
72
|
+
// already ejected reports nothing until its bound expires, and the unit looks alive
|
|
73
|
+
// throughout.
|
|
74
|
+
//
|
|
75
|
+
// Absence is not read on sight, though: GitHub can briefly expose neither label mid-
|
|
76
|
+
// transition, so one absent observation is only a suspicion. The rule is re-read, and a
|
|
77
|
+
// second consecutive absence settles it — which is what "re-read labels immediately" means
|
|
78
|
+
// operationally.
|
|
79
|
+
// `provider` is load-bearing rather than decorative: the `queued`/`dequeued` labels are
|
|
80
|
+
// **Mergify's** mechanics. GitHub's native merge queue does not write them, so applying the
|
|
81
|
+
// label rules there would read every PR as never-queued and silently disable the transition
|
|
82
|
+
// check. Same doctrine the delivery flow already follows — the mode picks the state
|
|
83
|
+
// machine, the provider picks the mechanics — so the provider-neutral half (merged, closed)
|
|
84
|
+
// runs for everyone and the label half runs only where labels exist.
|
|
85
|
+
// CI rollup for the CI→delivery pipeline (#792). Pure and unit-testable: the poll loop
|
|
86
|
+
// feeds it whatever `gh pr checks --json` returned (or a pre-rolled status) and it answers
|
|
87
|
+
// pending / green / red without talking to the network.
|
|
88
|
+
//
|
|
89
|
+
// `state` may be a rollup string (`SUCCESS`/`FAILURE`/`PENDING`/`ERROR`/`NEUTRAL`/`SKIPPED`)
|
|
90
|
+
// or an array of per-check objects (`{ state|conclusion|status, name? }`). GitHub's
|
|
91
|
+
// vocabulary is wider than three buckets; anything not clearly success/neutral/skipped is
|
|
92
|
+
// either still running (pending) or a failure (red). Empty/missing is pending — "no checks
|
|
93
|
+
// yet" is not green.
|
|
94
|
+
export function classifyCiState(input = {}) {
|
|
95
|
+
const raw = input.state ?? input.ciStatus ?? input.checks ?? null
|
|
96
|
+
if (raw == null) return { status: 'pending', reason: null }
|
|
97
|
+
|
|
98
|
+
const toBucket = (s) => {
|
|
99
|
+
const v = String(s ?? '').toUpperCase()
|
|
100
|
+
if (!v) return 'pending'
|
|
101
|
+
if (['SUCCESS', 'NEUTRAL', 'SKIPPED', 'PASS', 'PASSED', 'COMPLETED'].includes(v)) return 'green'
|
|
102
|
+
if (['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE', 'FAIL', 'FAILED'].includes(v)) return 'red'
|
|
103
|
+
// PENDING, QUEUED, IN_PROGRESS, EXPECTED, REQUESTED, WAITING, …
|
|
104
|
+
return 'pending'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof raw === 'string') {
|
|
108
|
+
const status = toBucket(raw)
|
|
109
|
+
return { status, reason: status === 'red' ? 'ci-failed' : null }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (Array.isArray(raw)) {
|
|
113
|
+
if (raw.length === 0) return { status: 'pending', reason: null }
|
|
114
|
+
let anyPending = false
|
|
115
|
+
let anyRed = false
|
|
116
|
+
const failed = []
|
|
117
|
+
for (const c of raw) {
|
|
118
|
+
// Prefer conclusion (final) over status (lifecycle). A completed check with
|
|
119
|
+
// conclusion FAILURE is red even when status is COMPLETED.
|
|
120
|
+
const bucket = toBucket(c?.conclusion ?? c?.state ?? c?.status)
|
|
121
|
+
if (bucket === 'red') {
|
|
122
|
+
anyRed = true
|
|
123
|
+
if (c?.name) failed.push(c.name)
|
|
124
|
+
} else if (bucket === 'pending') {
|
|
125
|
+
anyPending = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (anyRed) return { status: 'red', reason: 'ci-failed', failedChecks: failed }
|
|
129
|
+
if (anyPending) return { status: 'pending', reason: null }
|
|
130
|
+
return { status: 'green', reason: null }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { status: 'pending', reason: null }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Exact-HEAD identity for the CI→delivery pipeline (#792 guarantee 3).
|
|
137
|
+
//
|
|
138
|
+
// When `--head <sha>` is supplied, every terminal classification must be for THAT head.
|
|
139
|
+
// GitHub's `headRefOid` is the PR's current tip; if it no longer matches the supplied
|
|
140
|
+
// SHA, a merge or CI failure belongs to a different commit and must not be attributed
|
|
141
|
+
// to the reviewed one. Fail closed with `reason: 'head-drift'` so the orchestrator can
|
|
142
|
+
// route "rewritten head → re-review" instead of recording delivery for a stale SHA.
|
|
143
|
+
//
|
|
144
|
+
// Matching is prefix-tolerant: callers often pass a short SHA while GitHub returns the
|
|
145
|
+
// full 40-char oid. Comparison is case-insensitive.
|
|
146
|
+
export function headsMatch(supplied, observed) {
|
|
147
|
+
if (typeof supplied !== 'string' || typeof observed !== 'string') return false
|
|
148
|
+
const a = supplied.trim().toLowerCase()
|
|
149
|
+
const b = observed.trim().toLowerCase()
|
|
150
|
+
if (!a || !b) return false
|
|
151
|
+
return a === b || a.startsWith(b) || b.startsWith(a)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Returns a terminal hand-back verdict when the PR tip has moved off `--head`, or null
|
|
155
|
+
// when the exact-HEAD contract still holds (or when no head was required).
|
|
156
|
+
// Only call on a readable observation — an unreadable poll has no tip to compare.
|
|
157
|
+
export function headDriftVerdict({ suppliedHead, headRefOid } = {}) {
|
|
158
|
+
if (typeof suppliedHead !== 'string' || !suppliedHead.trim()) return null
|
|
159
|
+
const expected = suppliedHead.trim()
|
|
160
|
+
if (typeof headRefOid !== 'string' || !headRefOid.trim()) {
|
|
161
|
+
return {
|
|
162
|
+
terminal: true,
|
|
163
|
+
outcome: 'handed-back',
|
|
164
|
+
mergedSha: null,
|
|
165
|
+
reason: 'head-drift',
|
|
166
|
+
detail: `PR headRefOid is missing while --head ${expected} was required — cannot prove exact-HEAD delivery`,
|
|
167
|
+
expectedHead: expected,
|
|
168
|
+
observedHead: headRefOid ?? null,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (headsMatch(expected, headRefOid)) return null
|
|
172
|
+
return {
|
|
173
|
+
terminal: true,
|
|
174
|
+
outcome: 'handed-back',
|
|
175
|
+
mergedSha: null,
|
|
176
|
+
reason: 'head-drift',
|
|
177
|
+
detail: `PR head moved from ${expected} to ${headRefOid} — exact-HEAD delivery contract broken; re-review the new head`,
|
|
178
|
+
expectedHead: expected,
|
|
179
|
+
observedHead: headRefOid,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Combine PR lifecycle + CI into one poll decision for the CI→delivery pipeline.
|
|
184
|
+
// Precedence is deliberate:
|
|
185
|
+
// 0. head-drift (checked by the poll loop before this) — exact-HEAD contract first.
|
|
186
|
+
// 1. MERGED / CLOSED / dequeued — delivery terminal wins over CI (hybrid can merge
|
|
187
|
+
// while checks are still red or pending; the PR's own state is the authority).
|
|
188
|
+
// 2. CI red while still open — terminal `ci-failed` (exit 5). Fold into a fix round.
|
|
189
|
+
// 3. Otherwise keep watching (CI pending/green is not terminal on its own — green is
|
|
190
|
+
// the condition under which enqueue/auto-merge may still fire; the watch ends on
|
|
191
|
+
// the delivery outcome or the stall/ceiling bound).
|
|
192
|
+
export function classifyWatchPoll({ pr, ci } = {}) {
|
|
193
|
+
const prVerdict = classifyPrState(pr ?? {})
|
|
194
|
+
if (prVerdict.terminal) return { ...prVerdict, ciStatus: ci?.status ?? null }
|
|
195
|
+
const ciStatus = ci?.status ?? null
|
|
196
|
+
if (ciStatus === 'red') {
|
|
197
|
+
return {
|
|
198
|
+
terminal: true,
|
|
199
|
+
outcome: 'handed-back',
|
|
200
|
+
mergedSha: null,
|
|
201
|
+
reason: 'ci-failed',
|
|
202
|
+
ciStatus: 'red',
|
|
203
|
+
detail: ci?.failedChecks?.length
|
|
204
|
+
? `CI failed on exact HEAD: ${ci.failedChecks.join(', ')}`
|
|
205
|
+
: 'CI failed on exact HEAD',
|
|
206
|
+
failedChecks: ci?.failedChecks ?? [],
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { terminal: false, outcome: null, mergedSha: null, reason: null, ciStatus }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function classifyPrState({ state, mergedAt, mergeCommitOid, labels, everQueued = false, absentStreak = 0, provider = 'mergify', startedMs = null, dequeuedAtMs = null, queueActive = null } = {}) {
|
|
213
|
+
const tags = Array.isArray(labels) ? labels : []
|
|
214
|
+
const labelsAreMeaningful = provider === 'mergify'
|
|
215
|
+
if (state === 'MERGED' || mergedAt) {
|
|
216
|
+
// A merged PR with no merge commit is not a usable result: the caller records
|
|
217
|
+
// `mergedSha` and proves ancestry with it, and `null` there is unprovable.
|
|
218
|
+
// `mergedAt` is carried on the terminal answer so the orchestrator can store it on
|
|
219
|
+
// record-delivery-outcome (#791 / issue-664 / identity#669): hybrid/auto merges land
|
|
220
|
+
// without enqueue, and the postmortem clock needs the PR's own merge time rather than
|
|
221
|
+
// a late bookkeeping timestamp. Terminal regardless of everQueued — do not require a
|
|
222
|
+
// prior @mergifyio queue before observing a merge.
|
|
223
|
+
return mergeCommitOid
|
|
224
|
+
? { terminal: true, outcome: 'merged', mergedSha: mergeCommitOid, mergedAt: mergedAt ?? null, reason: null }
|
|
225
|
+
: { terminal: true, outcome: 'handed-back', mergedSha: null, mergedAt: mergedAt ?? null, reason: 'merged-without-sha' }
|
|
226
|
+
}
|
|
227
|
+
if (state === 'CLOSED') {
|
|
228
|
+
return { terminal: true, outcome: 'handed-back', mergedSha: null, reason: 'manual' }
|
|
229
|
+
}
|
|
230
|
+
// The `dequeued` label is a terminal queue exit — the mergify skill owns this lifecycle
|
|
231
|
+
// and the reason it is not merely "not queued": GitHub can briefly expose neither label
|
|
232
|
+
// during a transition, so absence proves nothing and presence proves an exit.
|
|
233
|
+
//
|
|
234
|
+
// Issue #629: that label is a lifecycle MIRROR, never removed on re-enqueue, so a PR
|
|
235
|
+
// dequeued once, fixed, and re-enqueued still carries it into a watch launched for the
|
|
236
|
+
// second attempt. Two guards, cheapest first (sibling of the identical fix in the mergify
|
|
237
|
+
// skill's `watch-pr-delivery.mjs`):
|
|
238
|
+
//
|
|
239
|
+
// 1. A fresh `queued` label reports the PR active right now — a `dequeued` label alongside
|
|
240
|
+
// it cannot be describing THIS moment, so it is ignored outright, no timestamp needed.
|
|
241
|
+
// 2. Otherwise the label is only THIS watch's terminal signal if it was applied at or after
|
|
242
|
+
// this watch started (`dequeuedAtMs`, parsed from the relevant Mergify status comment's
|
|
243
|
+
// "Left the queue" line — see `dequeuedAtFromComments`). Absent a reliable timestamp,
|
|
244
|
+
// fail toward WAITING, never toward a terminal verdict: a watcher that waits is bounded
|
|
245
|
+
// by --bound-minutes/--max-minutes and hands back honestly, whereas one that terminates
|
|
246
|
+
// early reports the PREVIOUS attempt's outcome as this one's, as fact — and that outcome
|
|
247
|
+
// is what routes the unit on the resolve-issues path this script serves.
|
|
248
|
+
//
|
|
249
|
+
// NOTE-2 (review on PR #637): `dequeuedAtMs` is truncated to the minute (Mergify's
|
|
250
|
+
// rendered "Left the queue" text has no seconds), while `startedMs` is millisecond-
|
|
251
|
+
// precision — so without a grace period, a GENUINE dequeue landing in the same
|
|
252
|
+
// wall-clock minute as the watch start always parses to a value below `startedMs` and
|
|
253
|
+
// reads as stale, a systematic blind spot for the first minute of every watch. A flat
|
|
254
|
+
// 60s grace absorbs that truncation. It trades a symmetric, much narrower risk (a truly
|
|
255
|
+
// stale dequeue from a PRIOR attempt coincidentally landing within 60s of THIS watch's
|
|
256
|
+
// start reading as fresh) for closing a bug that fired on every watch's first minute;
|
|
257
|
+
// #629's actual failure mode is stale labels persisting across minutes-to-hours-old
|
|
258
|
+
// attempts, not a coincidence this narrow.
|
|
259
|
+
const DEQUEUE_TIMESTAMP_GRACE_MS = 60_000
|
|
260
|
+
// Membership is the OR of two sources, and the order matters. The labels are a mirror the
|
|
261
|
+
// queue writes; `mergify queue show` is the queue itself. A mirror can lag, can be missing,
|
|
262
|
+
// and can linger from a previous attempt — so a `queued` label proves membership but its
|
|
263
|
+
// ABSENCE proves nothing while the queue tool still reports the PR active. `queueActive` is
|
|
264
|
+
// `null` when the queue could not be consulted, which is deliberately not `false`: unknown
|
|
265
|
+
// must not read as "left the queue", the exact conflation that made dequeues resolve badly.
|
|
266
|
+
const activeNow = (labelsAreMeaningful && tags.includes('queued')) || queueActive === true
|
|
267
|
+
if (labelsAreMeaningful && tags.includes('dequeued') && !activeNow) {
|
|
268
|
+
const isThisWatchsDequeue = Number.isFinite(startedMs) && Number.isFinite(dequeuedAtMs) && dequeuedAtMs >= startedMs - DEQUEUE_TIMESTAMP_GRACE_MS
|
|
269
|
+
if (isThisWatchsDequeue) {
|
|
270
|
+
return { terminal: true, outcome: 'handed-back', mergedSha: null, reason: 'dequeued' }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Seen queued, now not queued, still open and unmerged, and it has survived a re-read:
|
|
274
|
+
// the queue let go of this PR without the label ever appearing. Terminal.
|
|
275
|
+
// `!activeNow`, not `!tags.includes('queued')`. Adding the queue tool to `activeNow` above
|
|
276
|
+
// and leaving this branch on the label alone would have kept the exact bug being fixed: a
|
|
277
|
+
// PR the queue still holds, whose label lagged, settling as a dequeue on the re-read. Both
|
|
278
|
+
// membership witnesses have to be silent before this is a queue exit.
|
|
279
|
+
if (labelsAreMeaningful && everQueued && !activeNow && absentStreak >= 2) {
|
|
280
|
+
return {
|
|
281
|
+
terminal: true, outcome: 'handed-back', mergedSha: null, reason: 'dequeued',
|
|
282
|
+
// Issue #615 item 5 (sibling of the mergify watcher's identical wording fix): an
|
|
283
|
+
// unreadable poll carries `absentStreak` forward unchanged rather than resetting it
|
|
284
|
+
// (see `nextAbsentStreak` below), so the two absences that settle this can be
|
|
285
|
+
// separated by an arbitrarily long unreadable gap — they are consecutive READABLE
|
|
286
|
+
// reads, not consecutive reads.
|
|
287
|
+
detail: 'the PR was observed in the queue and then lost the `queued` label across two consecutive readable reads without merging — a queue exit, not a healthy snapshot',
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { terminal: false, outcome: null, mergedSha: null, reason: null }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// A failed `gh pr view` read (`readable: false`) carries no information about queue
|
|
294
|
+
// membership — it is not evidence the PR is still queued, and it is not evidence it left the
|
|
295
|
+
// queue either. Only a clean read that reports absence should move the counter that
|
|
296
|
+
// eventually settles `dequeued` in `classifyPrState`. Fixes #611 defect 1 in this watcher too
|
|
297
|
+
// (the mergify skill's `watch-pr-delivery.mjs` had the identical gap, closed there first by
|
|
298
|
+
// `nextMissingActiveReads`, kept the same shape here as `nextAbsentStreak`): two consecutive
|
|
299
|
+
// transient `gh` failures after `everQueued` were previously indistinguishable from two
|
|
300
|
+
// consecutive genuine "not queued" reads, and both advanced the same counter.
|
|
301
|
+
export function nextAbsentStreak({ queued, everQueued, absentStreak = 0, readable = true }) {
|
|
302
|
+
if (queued) return 0
|
|
303
|
+
if (!everQueued) return absentStreak
|
|
304
|
+
return readable ? absentStreak + 1 : absentStreak
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Compact, order-stable fingerprint of check-run progress for the stall clock.
|
|
308
|
+
// Used so a PR that stays OPEN+queued while CI conclusions move still restarts the bound
|
|
309
|
+
// (issue-799-800-resolve-release #807 / PR #810: skill-script-tests ran for ~12m under a
|
|
310
|
+
// frozen OPEN+queued key and the watch handed back conditions-unmet two minutes before merge).
|
|
311
|
+
export function checksFingerprint(checks) {
|
|
312
|
+
if (!Array.isArray(checks) || checks.length === 0) return null
|
|
313
|
+
const rows = checks.map((c) => {
|
|
314
|
+
const name = c?.name ?? c?.context ?? ''
|
|
315
|
+
const conclusion = c?.conclusion ?? ''
|
|
316
|
+
const status = c?.status ?? c?.state ?? ''
|
|
317
|
+
return `${name}|${status}|${conclusion}`
|
|
318
|
+
}).sort()
|
|
319
|
+
return rows.join(';')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// The stall clock restarts only when something the delivery surface actually CHANGES. A
|
|
323
|
+
// repeat poll returning the same key reads as a stall rather than as life — the same
|
|
324
|
+
// rule `record-delivery-observation` applies, kept identical here on purpose so a watch
|
|
325
|
+
// and a recorded observation cannot disagree about whether a unit is progressing.
|
|
326
|
+
//
|
|
327
|
+
// The key is deliberately wider than the original (state, queueState, position, labels)
|
|
328
|
+
// triple. issue-799-800-resolve-release #807 taught why: `runCli` built the key from the
|
|
329
|
+
// `gh pr view` observation alone, while Mergify membership lived in a separate `qv`
|
|
330
|
+
// object that never fed the key. A PR sitting in the queue with labels=['queued'] and
|
|
331
|
+
// position frozen at 0 ("Preparing checks") looked identical every poll, so the 10m stall
|
|
332
|
+
// bound fired while the queue was still holding the PR and CI was still running. Fold in:
|
|
333
|
+
// · queueState / position / queueActive from the queue tool (when available)
|
|
334
|
+
// · checksFingerprint so CI progress under a frozen queue picture also counts as life
|
|
335
|
+
export function queueExecutionFingerprint(observation) {
|
|
336
|
+
if (!observation || typeof observation !== 'object' || Array.isArray(observation)) return null
|
|
337
|
+
return JSON.stringify([
|
|
338
|
+
observation.kind ?? null,
|
|
339
|
+
observation.repository ?? null,
|
|
340
|
+
observation.pr ?? null,
|
|
341
|
+
observation.prHeadSha ?? null,
|
|
342
|
+
observation.queuePr ?? null,
|
|
343
|
+
observation.queueHeadSha ?? null,
|
|
344
|
+
observation.provider ?? null,
|
|
345
|
+
observation.status ?? null,
|
|
346
|
+
checksFingerprint(observation.checks),
|
|
347
|
+
])
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function observationKey({
|
|
351
|
+
state, queueState, position, labels, queueActive = null, checks = null, checksFp = null,
|
|
352
|
+
queueExecution = null,
|
|
353
|
+
} = {}) {
|
|
354
|
+
const tags = Array.isArray(labels) ? [...labels].sort().join(',') : ''
|
|
355
|
+
const fp = checksFp != null ? checksFp : checksFingerprint(checks)
|
|
356
|
+
// queueActive is tri-state: true / false / null(unknown). Null must not collapse with
|
|
357
|
+
// false — unknown membership is not "left the queue" (same doctrine as classifyPrState).
|
|
358
|
+
const active = queueActive === true ? true : queueActive === false ? false : null
|
|
359
|
+
return JSON.stringify([
|
|
360
|
+
state ?? null, queueState ?? null, position ?? null, tags, active, fp,
|
|
361
|
+
queueExecutionFingerprint(queueExecution),
|
|
362
|
+
])
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Bounded on STALL, not elapsed time. A unit can sit in `waiting_for_previous_batches` for
|
|
366
|
+
// an hour while genuinely advancing, and a flat timeout hands back healthy units.
|
|
367
|
+
export function stallVerdict({ lastChangeMs, nowMs, boundMinutes }) {
|
|
368
|
+
const bound = Number(boundMinutes)
|
|
369
|
+
if (!Number.isFinite(bound) || bound <= 0) {
|
|
370
|
+
return { stalled: false, reason: 'bound is not a positive number — refusing to time out on a bad bound' }
|
|
371
|
+
}
|
|
372
|
+
const stalledFor = nowMs - lastChangeMs
|
|
373
|
+
return stalledFor >= bound * 60_000
|
|
374
|
+
? { stalled: true, reason: `no observable change for ${Math.round(stalledFor / 60_000)}m (bound ${bound}m) — the queue is not reporting progress`, stalledForMs: stalledFor }
|
|
375
|
+
: { stalled: false, stalledForMs: stalledFor }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---- impure observation ----
|
|
379
|
+
|
|
380
|
+
// Fixes #611 defect 2 (the same gap as the mergify skill's `watch-pr-delivery.mjs`):
|
|
381
|
+
// `execFileSync` had no `timeout`, so a stalled TLS handshake or hung `gh` invocation blocked
|
|
382
|
+
// this watcher past both `--bound-minutes` and `--max-minutes` — those bounds are only
|
|
383
|
+
// evaluated after a poll returns. The bound is a small multiple of the poll interval:
|
|
384
|
+
// generous enough that normal network latency never trips it, bounded well under the
|
|
385
|
+
// absolute ceiling so a hung child cannot outlive it. Floors at 10s so a very short poll
|
|
386
|
+
// interval cannot turn this into a near-instant timeout — but that floor is itself clamped
|
|
387
|
+
// against `boundMinutes` (the caller passes `min(--bound-minutes, --max-minutes)`) so a
|
|
388
|
+
// single read's own timeout can never exceed the watch's own stall/ceiling bounds.
|
|
389
|
+
//
|
|
390
|
+
// Issue #615 item 1 (sibling of the mergify watcher's identical fix): the clamp divides by
|
|
391
|
+
// `readsPerPoll`, the number of subprocess reads made per poll, so a poll's reads together
|
|
392
|
+
// cannot outlive the bound even if every one of them hangs. This watcher makes exactly one
|
|
393
|
+
// read per poll (`gh pr view` in `observe`, below), so `readsPerPoll` is 1 and dividing by it
|
|
394
|
+
// is a no-op — the mergify watcher's sibling (`watch-pr-delivery.mjs`, two reads per poll)
|
|
395
|
+
// is where the division actually changes behaviour.
|
|
396
|
+
export function readTimeoutMs(intervalSeconds, boundMinutes, readsPerPoll = 1) {
|
|
397
|
+
const seconds = Number(intervalSeconds)
|
|
398
|
+
const bounded = Number.isFinite(seconds) && seconds > 0 ? seconds : 20
|
|
399
|
+
const computed = Math.max(bounded * 2000, 10_000)
|
|
400
|
+
const bound = Number(boundMinutes)
|
|
401
|
+
const reads = Number.isFinite(readsPerPoll) && readsPerPoll > 0 ? readsPerPoll : 1
|
|
402
|
+
return Number.isFinite(bound) && bound > 0 ? Math.min(computed, (bound * 60_000) / reads) : computed
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The single subprocess read this watcher makes per poll — `gh pr view` in `observe`. See
|
|
406
|
+
// `readTimeoutMs` above.
|
|
407
|
+
const READS_PER_POLL = 1
|
|
408
|
+
|
|
409
|
+
export function tryRun(cmd, args, options = {}) {
|
|
410
|
+
try { return { ok: true, out: execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...options }) } }
|
|
411
|
+
catch (error) { return { ok: false, out: '', error } }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Resolve the mergify skill's watcher so this one can borrow its queue read.
|
|
415
|
+
//
|
|
416
|
+
// Why a search rather than one path: the two skills are SIBLINGS once installed
|
|
417
|
+
// (`~/.agents/skills/{resolve-issues,mergify}`) but not in the authoring repo, where mergify
|
|
418
|
+
// lives under `cicd-pipeline/` and resolve-issues under `engineering/`. A single hardcoded
|
|
419
|
+
// `<skillsRoot>/mergify/...` therefore resolves in a consumer repo and fails here — the exact
|
|
420
|
+
// mirror of the relative-path defect this repo's script-hygiene gate exists to catch. Both
|
|
421
|
+
// layouts are tried, relative to THIS file, so neither environment is the special case.
|
|
422
|
+
//
|
|
423
|
+
// `--queue-watcher` overrides for anything unusual. Returns null when nothing resolves.
|
|
424
|
+
const QUEUE_WATCHER_CANDIDATES = [
|
|
425
|
+
// From `<root>/resolve-issues/scripts/` the sibling skill is up TWO levels; from the repo's
|
|
426
|
+
// `skills/engineering/resolve-issues/scripts/` it is up three and across into another scope.
|
|
427
|
+
'../../mergify/scripts/watch-pr-delivery-core.mjs', // installed farm: siblings
|
|
428
|
+
'../../../cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs', // authoring repo
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
export function resolveQueueWatcher(explicit, fromUrl = import.meta.url) {
|
|
432
|
+
const here = fileURLToPath(fromUrl)
|
|
433
|
+
if (typeof explicit === 'string' && explicit.trim()) {
|
|
434
|
+
if (!existsSync(explicit)) return null
|
|
435
|
+
const siblingCore = resolvePath(dirname(explicit), 'watch-pr-delivery-core.mjs')
|
|
436
|
+
return explicit.endsWith('watch-pr-delivery.mjs') && existsSync(siblingCore) ? siblingCore : explicit
|
|
437
|
+
}
|
|
438
|
+
for (const rel of QUEUE_WATCHER_CANDIDATES) {
|
|
439
|
+
const p = resolvePath(dirname(here), rel)
|
|
440
|
+
if (existsSync(p)) return p
|
|
441
|
+
}
|
|
442
|
+
return null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// The queue's own view of membership, borrowed from the mergify skill rather than reimplemented.
|
|
446
|
+
//
|
|
447
|
+
// This watcher was label-only, and that is precisely why its dequeue resolution was weak: the
|
|
448
|
+
// `queued`/`dequeued` labels are a MIRROR of the queue, so a label that lags, never lands, or
|
|
449
|
+
// lingers from a previous attempt leaves the classifier with no second opinion. `mergify queue
|
|
450
|
+
// show --json` is authoritative for membership — and it is the source that distinguishes "the
|
|
451
|
+
// queue is holding this PR" from "nothing is holding it", which read identically in labels.
|
|
452
|
+
//
|
|
453
|
+
// Degradation is LOUD. If the mergify watcher cannot be resolved this returns `available:
|
|
454
|
+
// false` and the caller says so in its output, because silently continuing label-only is the
|
|
455
|
+
// failure being fixed: the watch still runs, still looks healthy, and still resolves dequeues
|
|
456
|
+
// badly, with nothing reporting which mode it was in.
|
|
457
|
+
// Import once, then hand back a synchronous per-poll reader — or null when the mergify
|
|
458
|
+
// watcher is unavailable, which the caller reports rather than silently absorbing.
|
|
459
|
+
async function loadMergifyObservationCore(watcherPath, { pr, repo, intervalSeconds, boundMinutes }) {
|
|
460
|
+
if (!watcherPath) return null
|
|
461
|
+
try {
|
|
462
|
+
const { queueObservation, dequeuedAtFromComments } = await import(pathToFileURL(watcherPath).href)
|
|
463
|
+
if (typeof queueObservation !== 'function' || typeof dequeuedAtFromComments !== 'function') return null
|
|
464
|
+
return {
|
|
465
|
+
dequeuedAtFromComments,
|
|
466
|
+
readQueue: () => {
|
|
467
|
+
try {
|
|
468
|
+
const q = queueObservation({ pr, repo, intervalSeconds, stallMinutes: boundMinutes, maxMinutes: boundMinutes })
|
|
469
|
+
// position must reach the stall key (issue-799-800 #807): dropping it here made
|
|
470
|
+
// every "Preparing checks" poll look identical even when position later moved.
|
|
471
|
+
return {
|
|
472
|
+
available: true,
|
|
473
|
+
active: q.queued === true,
|
|
474
|
+
readable: q.readable !== false,
|
|
475
|
+
queueState: q.queueState ?? null,
|
|
476
|
+
position: typeof q.position === 'number' ? q.position : null,
|
|
477
|
+
}
|
|
478
|
+
} catch { return { available: true, active: null, readable: false, queueState: null, position: null } }
|
|
479
|
+
},
|
|
480
|
+
}
|
|
481
|
+
} catch { return null }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function observe(pr, repo, intervalSeconds, boundMinutes, dequeuedAtFromComments = () => null) {
|
|
485
|
+
// `comments` costs nothing extra in this same call and carries the one signal
|
|
486
|
+
// `dequeuedAtFromComments` needs — no additional subprocess read, so the READS_PER_POLL
|
|
487
|
+
// budget below is unaffected by issue #629's fix.
|
|
488
|
+
// `headRefOid` is required for the exact-HEAD CI→delivery contract (#792): when
|
|
489
|
+
// `--head` is set the poll loop compares it to the supplied SHA before any terminal
|
|
490
|
+
// classification, so a rewritten tip cannot be reported as a merge/CI result for the
|
|
491
|
+
// old reviewed commit. Same single `gh pr view` call — no extra read budget.
|
|
492
|
+
const r = tryRun('gh', ['pr', 'view', String(pr), '--repo', repo, '--json', 'state,mergedAt,mergeCommit,labels,comments,statusCheckRollup,headRefOid'], { timeout: readTimeoutMs(intervalSeconds, boundMinutes, READS_PER_POLL) })
|
|
493
|
+
// An unreadable PR is NOT "still running" — it is an unknown, and treating it as life
|
|
494
|
+
// keeps the watch alive against a PR that may not exist. Report it and let the stall
|
|
495
|
+
// clock end the watch honestly.
|
|
496
|
+
if (!r.ok) return { readable: false, state: null, mergedAt: null, mergeCommitOid: null, labels: [], dequeuedAtMs: null, checks: null, headRefOid: null }
|
|
497
|
+
try {
|
|
498
|
+
const d = JSON.parse(r.out)
|
|
499
|
+
// statusCheckRollup is an array of CheckRun/StatusContext objects. Map to the shape
|
|
500
|
+
// classifyCiState accepts so the poll loop does not re-derive the vocabulary.
|
|
501
|
+
const checks = Array.isArray(d.statusCheckRollup)
|
|
502
|
+
? d.statusCheckRollup.map((c) => ({
|
|
503
|
+
name: c.name ?? c.context ?? null,
|
|
504
|
+
// CheckRun uses conclusion + status; StatusContext uses state.
|
|
505
|
+
conclusion: c.conclusion ?? null,
|
|
506
|
+
state: c.state ?? null,
|
|
507
|
+
status: c.status ?? null,
|
|
508
|
+
}))
|
|
509
|
+
: null
|
|
510
|
+
return {
|
|
511
|
+
readable: true,
|
|
512
|
+
state: d.state ?? null,
|
|
513
|
+
mergedAt: d.mergedAt ?? null,
|
|
514
|
+
mergeCommitOid: d.mergeCommit?.oid ?? null,
|
|
515
|
+
labels: (d.labels ?? []).map((l) => l.name).filter(Boolean),
|
|
516
|
+
dequeuedAtMs: dequeuedAtFromComments(d.comments),
|
|
517
|
+
checks,
|
|
518
|
+
headRefOid: d.headRefOid ?? null,
|
|
519
|
+
}
|
|
520
|
+
} catch { return { readable: false, state: null, mergedAt: null, mergeCommitOid: null, labels: [], dequeuedAtMs: null, checks: null, headRefOid: null } }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Every poll's observation, as one compact NDJSON record — the watcher's heartbeat.
|
|
524
|
+
//
|
|
525
|
+
// Same defect, same fix as the mergify skill's `watch-pr-delivery.mjs` (these two are
|
|
526
|
+
// deliberately kept in step): the loop's only output sites were the terminal ones, so a watch
|
|
527
|
+
// sitting in the healthy `queued`/not-yet-queued path wrote zero bytes for its whole life.
|
|
528
|
+
// Reproduced live on a mergify-skill watch of PR #1396 — 25 minutes, log contained nothing but
|
|
529
|
+
// the supervisor's spawn banner, and it was killed before the then-30-minute stall bound so it
|
|
530
|
+
// never printed even a verdict. It matters MORE here: this watcher runs unattended inside the resolve-issues
|
|
531
|
+
// delivery loop, where a silent process and a hung one look identical to the orchestrator
|
|
532
|
+
// deciding whether a unit is progressing.
|
|
533
|
+
//
|
|
534
|
+
// stderr, not stdout, because stdout is the machine contract — exactly one terminal JSON
|
|
535
|
+
// document the caller parses whole — and interleaving heartbeats would break every caller.
|
|
536
|
+
// stderr is merged into the log by any supervisor that runs this, which is where a human looks.
|
|
537
|
+
export function progressRecord({ observation, changed, sinceChangeMs, nowIso }) {
|
|
538
|
+
return JSON.stringify({ ...observation, progress: true, terminal: false, changed, sinceChangeMs, at: nowIso })
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Defaults, aligned with the mergify skill's watcher so the two siblings do not drift into
|
|
542
|
+
// different cadences for the same job. `boundMinutes` is a NO-CHANGE bound — any observed
|
|
543
|
+
// movement resets it — so 10 minutes of a frozen picture is already a long wait before handing
|
|
544
|
+
// back: at a 90s poll that is still ~6 consecutive identical observations, well past a
|
|
545
|
+
// transient blip. The 90s interval also bounds the API budget: one read per poll is 40 calls/hour per
|
|
546
|
+
// watched PR, so even an epic-sized fan-out of concurrent watchers stays far inside an
|
|
547
|
+
// authenticated token's 5,000/hour. The cost is at most 90s of extra merge-detection latency
|
|
548
|
+
// against a queue that takes minutes per batch.
|
|
549
|
+
export const DEFAULT_INTERVAL_SECONDS = 90
|
|
550
|
+
export const DEFAULT_BOUND_MINUTES = 10
|
|
551
|
+
export const DEFAULT_MAX_MINUTES = 60
|
|
552
|
+
|
|
553
|
+
// Kept deliberately equal to the mergify watcher's `MAX_MINUTES_CEILING` — these two watchers
|
|
554
|
+
// implement one contract, and a ceiling honoured by only one of them is not a ceiling. A
|
|
555
|
+
// resolve-issues delivery that routed through this wrapper instead of the sibling would
|
|
556
|
+
// otherwise still be able to wait two hours, which is the failure mode the cap exists to end.
|
|
557
|
+
// Read the rationale there; enforced here the same way, as an exit-2 usage error rather than a
|
|
558
|
+
// silent clamp.
|
|
559
|
+
export const MAX_MINUTES_CEILING = 60
|
|
560
|
+
|
|
561
|
+
export function invokeQueueExecutionProvider(providerPath, request, { timeoutMs = 30_000 } = {}) {
|
|
562
|
+
const input = JSON.stringify(request)
|
|
563
|
+
const result = spawnSync(process.execPath, [providerPath], {
|
|
564
|
+
input,
|
|
565
|
+
encoding: 'utf8',
|
|
566
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
567
|
+
timeout: timeoutMs,
|
|
568
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
569
|
+
})
|
|
570
|
+
if (result.error?.code === 'ETIMEDOUT' || (result.status == null && result.signal)) {
|
|
571
|
+
return { ok: false, error: `queue-execution provider timed out after ${timeoutMs}ms` }
|
|
572
|
+
}
|
|
573
|
+
if (result.error) {
|
|
574
|
+
return { ok: false, error: `queue-execution provider failed to start: ${String(result.error.message || result.error)}` }
|
|
575
|
+
}
|
|
576
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : ''
|
|
577
|
+
if (result.status !== 0) {
|
|
578
|
+
const detail = stderr ? `: ${stderr}` : ''
|
|
579
|
+
return { ok: false, error: `queue-execution provider exited with code ${result.status}${detail}` }
|
|
580
|
+
}
|
|
581
|
+
if (stderr) return { ok: false, error: `queue-execution provider wrote stderr: ${stderr}` }
|
|
582
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : ''
|
|
583
|
+
if (!stdout) return { ok: false, error: 'queue-execution provider returned empty stdout' }
|
|
584
|
+
try {
|
|
585
|
+
return { ok: true, value: JSON.parse(stdout), requestJson: input }
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return { ok: false, error: `queue-execution provider returned malformed JSON: ${String(error.message || error)}` }
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function captureQueueExecution(provider, { repository, pr, prHeadSha, requiredCheckPlan }) {
|
|
592
|
+
const names = checksForPhase(requiredCheckPlan, 'queue-head').map((check) => check.name)
|
|
593
|
+
if (!names.length) return { artifact: null, errors: [] }
|
|
594
|
+
if (typeof provider !== 'function') return { artifact: null, errors: ['queue-execution provider is unavailable'] }
|
|
595
|
+
try {
|
|
596
|
+
const artifact = provider({ operation: 'read-queue-execution', repository, pr, prHeadSha, names })
|
|
597
|
+
const validation = validateQueueExecutionArtifact(requiredCheckPlan, artifact, {
|
|
598
|
+
expectedRepository: repository,
|
|
599
|
+
expectedPr: pr,
|
|
600
|
+
expectedPrHeadSha: prHeadSha,
|
|
601
|
+
})
|
|
602
|
+
return validation.ok ? { artifact, errors: [] } : { artifact: null, errors: validation.errors }
|
|
603
|
+
} catch (error) {
|
|
604
|
+
return { artifact: null, errors: [String(error.message || error)] }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function parseArgs(argv) {
|
|
609
|
+
const f = { boundMinutes: DEFAULT_BOUND_MINUTES, intervalSeconds: DEFAULT_INTERVAL_SECONDS, maxMinutes: DEFAULT_MAX_MINUTES, provider: 'mergify' }
|
|
610
|
+
for (let i = 0; i < argv.length; i++) {
|
|
611
|
+
const a = argv[i]
|
|
612
|
+
if (a === '--pr') f.pr = argv[++i]
|
|
613
|
+
else if (a === '--repo') f.repo = argv[++i]
|
|
614
|
+
else if (a === '--provider') f.provider = argv[++i]
|
|
615
|
+
else if (a === '--bound-minutes') f.boundMinutes = Number(argv[++i])
|
|
616
|
+
else if (a === '--max-minutes') f.maxMinutes = Number(argv[++i])
|
|
617
|
+
else if (a === '--interval-seconds') f.intervalSeconds = Number(argv[++i])
|
|
618
|
+
else if (a === '--head') f.head = argv[++i]
|
|
619
|
+
else if (a === '--quiet') f.quiet = true
|
|
620
|
+
else if (a === '--queue-watcher') f.queueWatcher = argv[++i]
|
|
621
|
+
else if (a === '--required-check-plan') f.requiredCheckPlan = argv[++i]
|
|
622
|
+
else if (a === '--queue-execution-provider') f.queueExecutionProvider = argv[++i]
|
|
623
|
+
else if (a === '--help' || a === '-h') f.help = true
|
|
624
|
+
}
|
|
625
|
+
return f
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
629
|
+
|
|
630
|
+
export async function runCli(argv) {
|
|
631
|
+
const f = parseArgs(argv)
|
|
632
|
+
let requiredCheckPlan = null
|
|
633
|
+
if (f.requiredCheckPlan) {
|
|
634
|
+
try { requiredCheckPlan = JSON.parse(await (await import('node:fs/promises')).readFile(f.requiredCheckPlan, 'utf8')) }
|
|
635
|
+
catch { console.error('watch-delivery: --required-check-plan must name readable JSON'); return 2 }
|
|
636
|
+
}
|
|
637
|
+
if (f.help) {
|
|
638
|
+
console.log(`watch-delivery --pr <n> --repo <owner/name> [--bound-minutes 10] [--interval-seconds 90] [--head <sha>]
|
|
639
|
+
[--required-check-plan <json> --queue-execution-provider <provider.mjs>]
|
|
640
|
+
Waits for a PR to reach a terminal delivery state (including hybrid/auto merge without enqueue), in ONE launch instead of a
|
|
641
|
+
foreground poll loop. Observes only — never enqueues, never merges, never records.
|
|
642
|
+
|
|
643
|
+
With --head, this is the single CI→delivery pipeline after PASS (#792 / issue-664 / identity#669):
|
|
644
|
+
poll CI on that exact HEAD and the PR lifecycle together. Merge still wins over CI red (hybrid
|
|
645
|
+
can land while checks settle). CI red while OPEN is exit 5 — fold into a fix round. If the PR
|
|
646
|
+
tip moves off the supplied SHA, exit 4 with reason head-drift (never merged/ci-failed for the
|
|
647
|
+
old SHA) so the orchestrator can re-review the rewritten head.
|
|
648
|
+
|
|
649
|
+
--bound-minutes stall bound (default 10) — restarts on any change to the delivery surface
|
|
650
|
+
(PR state, queue state/position/membership, labels, CI fingerprint)
|
|
651
|
+
--interval-seconds poll interval (default 90)
|
|
652
|
+
--max-minutes absolute ceiling (default 60, hard maximum 60) — a backstop so a queue
|
|
653
|
+
whose position keeps moving cannot keep the watch alive forever
|
|
654
|
+
--head exact HEAD SHA for the CI→delivery pipeline; without it CI is not classified
|
|
655
|
+
--required-check-plan frozen plan whose queue-head checks require synthetic execution evidence
|
|
656
|
+
--queue-execution-provider stdin/stdout JSON provider polled for synthetic PR/check progress;
|
|
657
|
+
on merge its result must validate as the final queue-execution artifact
|
|
658
|
+
--quiet suppress the per-poll progress heartbeat on stderr
|
|
659
|
+
|
|
660
|
+
stdout = exactly one terminal JSON verdict, at exit. stderr = one compact JSON record per
|
|
661
|
+
poll (progress:true) unless --quiet, including synthetic PR identity and required queue-head
|
|
662
|
+
check states when observable, so a healthy watch is visibly alive rather than silent.
|
|
663
|
+
|
|
664
|
+
exit 0 = merged (mergedSha is the MERGE COMMIT; the caller still owes the ancestry proof)
|
|
665
|
+
exit 4 = terminal but not merged — reason distinguishes the route:
|
|
666
|
+
dequeued / conditions-unmet / head-drift / unreadable → unit hand-back
|
|
667
|
+
stall-while-queued → queue still holds the PR; re-observe or relaunch the watch
|
|
668
|
+
(do NOT unit-hand-back or re-enqueue the same HEAD)
|
|
669
|
+
exit 5 = CI failed on the exact HEAD (--head set) — fold into a fix round; never a success
|
|
670
|
+
exit 2 = usage error
|
|
671
|
+
|
|
672
|
+
Bounded on STALL, not elapsed time: a unit can sit in a queue for an hour while genuinely
|
|
673
|
+
advancing. Never exits 0 on a timeout — "still going" and "finished" must not collapse.
|
|
674
|
+
A PR observed in the queue that later loses the queued label across two consecutive
|
|
675
|
+
reads is a queue exit, not a healthy snapshot.`)
|
|
676
|
+
return 0
|
|
677
|
+
}
|
|
678
|
+
if (!f.pr || !f.repo) {
|
|
679
|
+
console.error('watch-delivery: --pr and --repo are required')
|
|
680
|
+
return 2
|
|
681
|
+
}
|
|
682
|
+
if (!Number.isFinite(f.boundMinutes) || f.boundMinutes <= 0 || !Number.isFinite(f.intervalSeconds) || f.intervalSeconds <= 0 || !Number.isFinite(f.maxMinutes) || f.maxMinutes <= 0) {
|
|
683
|
+
console.error('watch-delivery: --bound-minutes, --interval-seconds and --max-minutes must be positive numbers')
|
|
684
|
+
return 2
|
|
685
|
+
}
|
|
686
|
+
if (f.maxMinutes > MAX_MINUTES_CEILING) {
|
|
687
|
+
console.error(`watch-delivery: --max-minutes ${f.maxMinutes} exceeds the ${MAX_MINUTES_CEILING}-minute ceiling; hand back and relaunch instead of waiting longer`)
|
|
688
|
+
return 2
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Resolved and imported ONCE, ahead of the loop. Two things are reported rather than
|
|
692
|
+
// assumed: which source is answering membership, and — when the mergify watcher cannot be
|
|
693
|
+
// found — that this watch is running label-only. Degrading quietly is what made a weak
|
|
694
|
+
// dequeue resolution look like a healthy watch.
|
|
695
|
+
const watcherPath = resolveQueueWatcher(f.queueWatcher)
|
|
696
|
+
const observationCore = await loadMergifyObservationCore(watcherPath, f)
|
|
697
|
+
const readQueue = observationCore?.readQueue ?? null
|
|
698
|
+
const queueSource = readQueue ? `mergify queue show (${watcherPath})` : 'labels only'
|
|
699
|
+
if (!readQueue && f.provider === 'mergify') {
|
|
700
|
+
console.error(`watch-delivery: the mergify skill's watch-pr-delivery.mjs was not found, so queue membership `
|
|
701
|
+
+ `is read from the queued/dequeued LABELS alone. Labels mirror the queue and can lag, go missing, or linger `
|
|
702
|
+
+ `from a previous attempt, so a dequeue may resolve late or not at all. Pass --queue-watcher <path> to fix.`)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const startedMs = Date.now()
|
|
706
|
+
let lastKey = null
|
|
707
|
+
let lastChangeMs = startedMs
|
|
708
|
+
let observations = 0
|
|
709
|
+
let everQueued = false
|
|
710
|
+
let absentStreak = 0
|
|
711
|
+
|
|
712
|
+
for (;;) {
|
|
713
|
+
const bounds = [f.boundMinutes, f.maxMinutes].filter((n) => Number.isFinite(n) && n > 0)
|
|
714
|
+
const obs = observe(
|
|
715
|
+
f.pr,
|
|
716
|
+
f.repo,
|
|
717
|
+
f.intervalSeconds,
|
|
718
|
+
bounds.length ? Math.min(...bounds) : undefined,
|
|
719
|
+
observationCore?.dequeuedAtFromComments,
|
|
720
|
+
)
|
|
721
|
+
observations += 1
|
|
722
|
+
// History the classifier needs but a single observation cannot carry.
|
|
723
|
+
const qv = readQueue ? readQueue() : { available: false, active: null, readable: false }
|
|
724
|
+
const queueChecks = requiredCheckPlan ? checksForPhase(requiredCheckPlan, 'queue-head') : []
|
|
725
|
+
const queueRequest = queueChecks.length ? {
|
|
726
|
+
operation: 'read-queue-execution',
|
|
727
|
+
repository: f.repo,
|
|
728
|
+
pr: Number(f.pr),
|
|
729
|
+
prHeadSha: f.head ?? requiredCheckPlan?.headSha,
|
|
730
|
+
names: queueChecks.map((check) => check.name),
|
|
731
|
+
} : null
|
|
732
|
+
let queueExecution = null
|
|
733
|
+
let queueExecutionError = null
|
|
734
|
+
if (queueRequest) {
|
|
735
|
+
if (!f.queueExecutionProvider) {
|
|
736
|
+
queueExecutionError = 'queue-execution provider is unavailable'
|
|
737
|
+
} else {
|
|
738
|
+
const timeoutMs = readTimeoutMs(
|
|
739
|
+
f.intervalSeconds,
|
|
740
|
+
bounds.length ? Math.min(...bounds) : undefined,
|
|
741
|
+
3,
|
|
742
|
+
)
|
|
743
|
+
const result = invokeQueueExecutionProvider(f.queueExecutionProvider, queueRequest, { timeoutMs })
|
|
744
|
+
if (result.ok) queueExecution = result.value
|
|
745
|
+
else queueExecutionError = result.error
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// The label OR the queue tool. `absentStreak` — the counter that settles a dequeue — may
|
|
749
|
+
// only advance on a READABLE absence, so an unavailable queue read must not feed it.
|
|
750
|
+
const queued = obs.labels.includes('queued') || qv.active === true
|
|
751
|
+
if (queued) everQueued = true
|
|
752
|
+
absentStreak = nextAbsentStreak({ queued, everQueued, absentStreak, readable: obs.readable !== false && (!qv.available || qv.readable) })
|
|
753
|
+
const prInput = { ...obs, everQueued, absentStreak, provider: f.provider, startedMs, queueActive: qv.available ? qv.active : null }
|
|
754
|
+
// Exact-HEAD gate first (#792 guarantee 3). A readable tip that no longer matches
|
|
755
|
+
// `--head` is a distinct head-drift hand-back — never a merged/ci-failed verdict for
|
|
756
|
+
// the old SHA. Unreadable polls skip this and fall through to the stall clock.
|
|
757
|
+
if (f.head && obs.readable) {
|
|
758
|
+
const drift = headDriftVerdict({ suppliedHead: f.head, headRefOid: obs.headRefOid })
|
|
759
|
+
if (drift) {
|
|
760
|
+
console.log(JSON.stringify({
|
|
761
|
+
...drift,
|
|
762
|
+
head: f.head,
|
|
763
|
+
observations,
|
|
764
|
+
elapsedMs: Date.now() - startedMs,
|
|
765
|
+
readable: obs.readable,
|
|
766
|
+
queueSource,
|
|
767
|
+
}, null, 2))
|
|
768
|
+
return 4
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// With --head this is the CI→delivery pipeline (#792): classify CI + PR together so a
|
|
772
|
+
// single background launch reaches a terminal delivery or CI-failed signal without a
|
|
773
|
+
// human turn. Without --head, preserve the prior delivery-only classification.
|
|
774
|
+
const ci = f.head ? classifyCiState({ checks: obs.checks }) : null
|
|
775
|
+
const verdict = f.head
|
|
776
|
+
? classifyWatchPoll({ pr: prInput, ci })
|
|
777
|
+
: classifyPrState(prInput)
|
|
778
|
+
if (verdict.terminal) {
|
|
779
|
+
const queueCapture = verdict.outcome === 'merged' && requiredCheckPlan
|
|
780
|
+
? captureQueueExecution(
|
|
781
|
+
queueRequest
|
|
782
|
+
? () => {
|
|
783
|
+
if (queueExecutionError) throw new Error(queueExecutionError)
|
|
784
|
+
return queueExecution
|
|
785
|
+
}
|
|
786
|
+
: null,
|
|
787
|
+
{
|
|
788
|
+
repository: f.repo,
|
|
789
|
+
pr: Number(f.pr),
|
|
790
|
+
prHeadSha: f.head ?? requiredCheckPlan?.headSha,
|
|
791
|
+
requiredCheckPlan,
|
|
792
|
+
},
|
|
793
|
+
)
|
|
794
|
+
: { artifact: null, errors: [] }
|
|
795
|
+
const terminal = {
|
|
796
|
+
...verdict,
|
|
797
|
+
...(f.head ? { head: f.head } : {}),
|
|
798
|
+
observations,
|
|
799
|
+
elapsedMs: Date.now() - startedMs,
|
|
800
|
+
readable: obs.readable,
|
|
801
|
+
queueSource,
|
|
802
|
+
...(queueCapture.artifact ? { queueExecutionArtifact: queueCapture.artifact } : {}),
|
|
803
|
+
...(queueCapture.errors.length ? { evidenceBlocked: true, evidenceErrors: queueCapture.errors } : {}),
|
|
804
|
+
}
|
|
805
|
+
console.log(JSON.stringify(terminal, null, 2))
|
|
806
|
+
if (verdict.outcome === 'merged' && !terminal.evidenceBlocked) return 0
|
|
807
|
+
if (verdict.outcome === 'merged') return 4
|
|
808
|
+
if (verdict.reason === 'ci-failed') return 5
|
|
809
|
+
return 4
|
|
810
|
+
}
|
|
811
|
+
// Key the stall clock on the full delivery surface — PR state + queue tool + CI
|
|
812
|
+
// fingerprint — not the gh-only observation. See observationKey's incident note.
|
|
813
|
+
const queueActive = qv.available ? qv.active : null
|
|
814
|
+
const key = observationKey({
|
|
815
|
+
state: obs.state,
|
|
816
|
+
labels: obs.labels,
|
|
817
|
+
queueState: qv.available ? (qv.queueState ?? null) : null,
|
|
818
|
+
position: qv.available ? (qv.position ?? null) : null,
|
|
819
|
+
queueActive,
|
|
820
|
+
checks: obs.checks,
|
|
821
|
+
queueExecution,
|
|
822
|
+
})
|
|
823
|
+
const changed = key !== lastKey
|
|
824
|
+
if (changed) { lastKey = key; lastChangeMs = Date.now() }
|
|
825
|
+
|
|
826
|
+
// Heartbeat before the bound checks below, so the last line in the log before a hand-back
|
|
827
|
+
// is the unchanging observation that caused it — see `progressRecord`.
|
|
828
|
+
if (!f.quiet) {
|
|
829
|
+
const nowMs = Date.now()
|
|
830
|
+
console.error(progressRecord({
|
|
831
|
+
observation: {
|
|
832
|
+
outcome: verdict.outcome, reason: verdict.reason ?? null,
|
|
833
|
+
state: obs.state, mergedAt: obs.mergedAt, labels: obs.labels ?? [], readable: obs.readable !== false,
|
|
834
|
+
everQueued, absentStreak, observations, elapsedMs: nowMs - startedMs, queueSource, queueActive,
|
|
835
|
+
queueState: qv.available ? (qv.queueState ?? null) : null,
|
|
836
|
+
position: qv.available ? (qv.position ?? null) : null,
|
|
837
|
+
...(queueExecution ? { queueExecution } : {}),
|
|
838
|
+
...(queueExecutionError ? { queueExecutionError } : {}),
|
|
839
|
+
},
|
|
840
|
+
changed, sinceChangeMs: nowMs - lastChangeMs, nowIso: new Date(nowMs).toISOString(),
|
|
841
|
+
}))
|
|
842
|
+
}
|
|
843
|
+
// The absolute ceiling, and why it exists alongside the stall bound. Stall is the
|
|
844
|
+
// right *primary* bound — a unit can sit in `waiting_for_previous_batches` for an hour
|
|
845
|
+
// while genuinely advancing, so a flat timeout hands back healthy units. But a stall
|
|
846
|
+
// clock that resets on any observed change has no upper bound at all: a busy queue
|
|
847
|
+
// whose position keeps moving (5→4→3→2→1→5…) resets it every poll, and this loop never
|
|
848
|
+
// returns. For a job designed to run unattended in the background, "never returns" is
|
|
849
|
+
// a hang, not patience. So the ceiling is a backstop, deliberately generous, and it
|
|
850
|
+
// reports itself as such rather than as a stall.
|
|
851
|
+
const elapsedMs = Date.now() - startedMs
|
|
852
|
+
if (elapsedMs >= f.maxMinutes * 60_000) {
|
|
853
|
+
console.log(JSON.stringify({
|
|
854
|
+
terminal: true, outcome: 'handed-back', mergedSha: null, reason: 'conditions-unmet',
|
|
855
|
+
detail: `watch hit its absolute ceiling of ${f.maxMinutes}m while still observing change (${observations} observations) — the queue is moving but not finishing; this is a backstop, not a stall`,
|
|
856
|
+
observations, elapsedMs, readable: obs.readable,
|
|
857
|
+
}, null, 2))
|
|
858
|
+
return 4
|
|
859
|
+
}
|
|
860
|
+
const stall = stallVerdict({ lastChangeMs, nowMs: Date.now(), boundMinutes: f.boundMinutes })
|
|
861
|
+
if (stall.stalled) {
|
|
862
|
+
// Distinguishing "still held by the queue, picture frozen" from "nothing is holding
|
|
863
|
+
// this PR and conditions never cleared" is load-bearing for the orchestrator.
|
|
864
|
+
// issue-799-800 #807: a healthy PR sat in Mergify "Preparing checks" with labels
|
|
865
|
+
// unchanged for the whole bound; the watch exited conditions-unmet and the unit was
|
|
866
|
+
// treated as a terminal hand-back — then the PR merged two minutes later and the
|
|
867
|
+
// outcome was only written after a multi-hour session gap (outcome-write-lag).
|
|
868
|
+
// When membership is still active, the reason is stall-while-queued: exit 4 still
|
|
869
|
+
// (watch process ends) but the orchestrator must re-observe (one gh pr view / relaunch
|
|
870
|
+
// the watch), never record unit-level handed-back or re-enqueue the same HEAD.
|
|
871
|
+
const stillHeld = queueActive === true || (everQueued && obs.labels?.includes('queued'))
|
|
872
|
+
const reason = !obs.readable
|
|
873
|
+
? 'unreadable'
|
|
874
|
+
: stillHeld
|
|
875
|
+
? 'stall-while-queued'
|
|
876
|
+
: 'conditions-unmet'
|
|
877
|
+
const detail = stillHeld
|
|
878
|
+
? `${stall.reason}; queue still holds this PR — relaunch the watch or re-observe once, do not hand the unit back or re-enqueue`
|
|
879
|
+
: stall.reason
|
|
880
|
+
console.log(JSON.stringify({
|
|
881
|
+
terminal: true, outcome: 'handed-back', mergedSha: null,
|
|
882
|
+
reason, detail, observations, elapsedMs: Date.now() - startedMs, readable: obs.readable,
|
|
883
|
+
queueActive, everQueued,
|
|
884
|
+
}, null, 2))
|
|
885
|
+
return 4
|
|
886
|
+
}
|
|
887
|
+
await sleep(f.intervalSeconds * 1000)
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (isMainModule(import.meta.url)) {
|
|
892
|
+
runCli(process.argv.slice(2)).then((code) => { process.exitCode = code })
|
|
893
|
+
}
|