@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,663 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export function classifyObservation({
|
|
4
|
+
pr = {},
|
|
5
|
+
queue = {},
|
|
6
|
+
seenQueued = false,
|
|
7
|
+
// Process-local: membership was active at least once THIS run. Distinct from
|
|
8
|
+
// `seenQueued`, which `--seen-queued` may seed before any poll has confirmed
|
|
9
|
+
// membership (resume intent). Lost-membership DEQUEUED requires everActive —
|
|
10
|
+
// otherwise a pre-admission watch with `--seen-queued` false-fires after two
|
|
11
|
+
// clean "not in queue" reads (epic-824 #859 watch-859-component-818).
|
|
12
|
+
//
|
|
13
|
+
// Default `null` = "not supplied" so pure unit callers that only pass seenQueued
|
|
14
|
+
// keep the pre-everActive lost-membership semantics. The poll loop always passes
|
|
15
|
+
// an explicit boolean.
|
|
16
|
+
everActive = null,
|
|
17
|
+
missingActiveReads = 0,
|
|
18
|
+
startedMs = null,
|
|
19
|
+
dequeuedAtMs = null,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const labels = Array.isArray(pr.labels) ? pr.labels : []
|
|
22
|
+
const hasQueuedLabel = labels.includes('queued')
|
|
23
|
+
const hasDequeuedLabel = labels.includes('dequeued')
|
|
24
|
+
const queueQueued = queue.queued === true
|
|
25
|
+
const active = hasQueuedLabel || queueQueued
|
|
26
|
+
// Sticky true once observed or supplied; false only when explicitly false and inactive.
|
|
27
|
+
const ever = everActive === true || active
|
|
28
|
+
// Lost-membership may fire when: (a) caller did not supply everActive and seenQueued
|
|
29
|
+
// is true (legacy), or (b) everActive is true (process observed membership).
|
|
30
|
+
// Explicit everActive=false with seeded seenQueued is the pre-admission resume case.
|
|
31
|
+
const mayCountLoss = everActive === null ? seenQueued === true : ever
|
|
32
|
+
|
|
33
|
+
if (pr.state === 'MERGED' || pr.mergedAt) {
|
|
34
|
+
if (!pr.mergeCommitOid) {
|
|
35
|
+
return { terminal: true, outcome: 'CLOSED_UNMERGED', reason: 'merged-without-sha', active, seenQueued: seenQueued || active, everActive: ever }
|
|
36
|
+
}
|
|
37
|
+
return { terminal: true, outcome: 'MERGED', mergedSha: pr.mergeCommitOid, active, seenQueued: seenQueued || active, everActive: ever }
|
|
38
|
+
}
|
|
39
|
+
if (pr.state === 'CLOSED') {
|
|
40
|
+
return { terminal: true, outcome: 'CLOSED_UNMERGED', reason: 'closed-unmerged', active, seenQueued: seenQueued || active, everActive: ever }
|
|
41
|
+
}
|
|
42
|
+
// Issue #629: a `dequeued` label is a lifecycle MIRROR of Mergify's queue, never removed on
|
|
43
|
+
// re-enqueue — so a PR dequeued once, fixed, and re-enqueued still carries the label from
|
|
44
|
+
// the first attempt into a watch launched for the second one. Two guards, both from the
|
|
45
|
+
// issue, cheapest first:
|
|
46
|
+
//
|
|
47
|
+
// 1. Mergify (or a fresh `queued` label) is authoritative for membership. When either
|
|
48
|
+
// reports the PR active right now, a `dequeued` label alongside it cannot be describing
|
|
49
|
+
// THIS moment, so it is ignored outright — no timestamp needed.
|
|
50
|
+
// 2. Otherwise, the label is only THIS watch's terminal signal if it was applied at or
|
|
51
|
+
// after this watch started (`dequeuedAtMs`, parsed from the relevant Mergify status
|
|
52
|
+
// comment's "Left the queue" line — see `dequeuedAtFromComments`). Absent a reliable
|
|
53
|
+
// timestamp, fail toward WAITING, never toward a terminal verdict: a watcher that waits
|
|
54
|
+
// is bounded by the phase-aware stall / --max-minutes and hands back honestly, whereas one that
|
|
55
|
+
// terminates early reports the PREVIOUS attempt's outcome as this one's, as fact — and
|
|
56
|
+
// on the resolve-issues path that outcome is what routes the unit.
|
|
57
|
+
//
|
|
58
|
+
// NOTE-2 (review on PR #637): `dequeuedAtMs` is truncated to the minute (Mergify's
|
|
59
|
+
// rendered "Left the queue" text has no seconds), while `startedMs` is millisecond-
|
|
60
|
+
// precision — so without a grace period, a GENUINE dequeue landing in the same
|
|
61
|
+
// wall-clock minute as the watch start always parses to a value below `startedMs` and
|
|
62
|
+
// reads as stale, a systematic blind spot for the first minute of every watch. A flat
|
|
63
|
+
// 60s grace absorbs that truncation. It trades a symmetric, much narrower risk (a truly
|
|
64
|
+
// stale dequeue from a PRIOR attempt coincidentally landing within 60s of THIS watch's
|
|
65
|
+
// start reading as fresh) for closing a bug that fired on every watch's first minute;
|
|
66
|
+
// #629's actual failure mode is stale labels persisting across minutes-to-hours-old
|
|
67
|
+
// attempts, not a coincidence this narrow.
|
|
68
|
+
const DEQUEUE_TIMESTAMP_GRACE_MS = 60_000
|
|
69
|
+
if (hasDequeuedLabel && !active) {
|
|
70
|
+
const isThisWatchsDequeue = Number.isFinite(startedMs) && Number.isFinite(dequeuedAtMs) && dequeuedAtMs >= startedMs - DEQUEUE_TIMESTAMP_GRACE_MS
|
|
71
|
+
if (isThisWatchsDequeue) {
|
|
72
|
+
return { terminal: true, outcome: 'DEQUEUED', reason: 'dequeued-label', active, seenQueued: true, everActive: ever }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (active) {
|
|
76
|
+
return {
|
|
77
|
+
terminal: false,
|
|
78
|
+
outcome: 'QUEUED',
|
|
79
|
+
active,
|
|
80
|
+
seenQueued: true,
|
|
81
|
+
everActive: true,
|
|
82
|
+
queueState: queue.queueState || queue.state || null,
|
|
83
|
+
position: queue.position ?? null,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Lost-membership needs mayCountLoss (everActive observed, or legacy seenQueued-only
|
|
87
|
+
// callers). Explicit everActive=false with seeded seenQueued is pre-admission resume:
|
|
88
|
+
// do not count absences or fire DEQUEUED (epic-824 #859 first watch).
|
|
89
|
+
if (mayCountLoss && missingActiveReads >= 2) {
|
|
90
|
+
return {
|
|
91
|
+
terminal: true,
|
|
92
|
+
outcome: 'DEQUEUED',
|
|
93
|
+
reason: 'lost-queue-membership',
|
|
94
|
+
// Issue #615 item 5: an unreadable poll now carries `missingActiveReads` forward
|
|
95
|
+
// unchanged rather than resetting it (see `nextMissingActiveReads` above), so the
|
|
96
|
+
// two absences that settle this can be separated by an arbitrarily long unreadable
|
|
97
|
+
// gap — "consecutive reads" over-claims. They are consecutive READABLE reads.
|
|
98
|
+
detail: 'PR was observed queued, then active Mergify queue membership disappeared across consecutive readable reads without a merge.',
|
|
99
|
+
active,
|
|
100
|
+
seenQueued: true,
|
|
101
|
+
everActive: ever,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (mayCountLoss) {
|
|
105
|
+
return { terminal: false, outcome: 'WAITING', reason: 'queue-membership-missing-reread', active, seenQueued: true, everActive: ever }
|
|
106
|
+
}
|
|
107
|
+
// Seeded seenQueued with explicit everActive=false: still pre-admission for THIS process.
|
|
108
|
+
// Keep seenQueued so phases/callers retain the resume hint, but do not start the
|
|
109
|
+
// lost-membership counter until membership is observed.
|
|
110
|
+
return { terminal: false, outcome: 'NOT_YET_QUEUED', active, seenQueued: seenQueued === true, everActive: false }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The ordered list of lifecycle phases this watch has actually passed through — the
|
|
114
|
+
// progression the contract is about, made visible to the caller.
|
|
115
|
+
//
|
|
116
|
+
// Why it is worth carrying: the terminal outcome alone is ambiguous in exactly the place the
|
|
117
|
+
// contract is subtle. `STALLED` after `["NOT_YET_QUEUED"]` means the PR was NEVER admitted —
|
|
118
|
+
// on a manual-delivery repo that almost always means nobody enqueued it, and the fix is an
|
|
119
|
+
// enqueue, not an investigation. `STALLED` after `["NOT_YET_QUEUED","QUEUED"]` means it was
|
|
120
|
+
// admitted and then wedged INSIDE the queue, where the fix is reading the blocking condition.
|
|
121
|
+
// Same verdict string, opposite diagnosis; `seenQueued` carries the bit but not the shape, and
|
|
122
|
+
// a caller reconstructing the shape from the heartbeat stream has to have kept every record.
|
|
123
|
+
//
|
|
124
|
+
// `WAITING` is deliberately not a phase. It is the re-read suspicion state that sits INSIDE
|
|
125
|
+
// `QUEUED` (membership missing once, not yet settled — see `classifyObservation`), so recording
|
|
126
|
+
// it would make a healthy queued PR oscillate QUEUED/WAITING/QUEUED and turn this into noise
|
|
127
|
+
// rather than a progression. Anything not in `LIFECYCLE_PHASES` leaves the list untouched.
|
|
128
|
+
export const LIFECYCLE_PHASES = new Set(['NOT_YET_QUEUED', 'QUEUED', 'MERGED', 'DEQUEUED', 'CLOSED_UNMERGED', 'STALLED'])
|
|
129
|
+
|
|
130
|
+
export function nextPhases(phases = [], outcome) {
|
|
131
|
+
const list = Array.isArray(phases) ? phases : []
|
|
132
|
+
if (!LIFECYCLE_PHASES.has(outcome)) return list
|
|
133
|
+
if (list[list.length - 1] === outcome) return list
|
|
134
|
+
return [...list, outcome]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function observationKey({ pr = {}, queue = {} } = {}) {
|
|
138
|
+
const labels = Array.isArray(pr.labels) ? [...pr.labels].sort().join(',') : ''
|
|
139
|
+
return JSON.stringify([
|
|
140
|
+
pr.state ?? null,
|
|
141
|
+
pr.mergedAt ?? null,
|
|
142
|
+
labels,
|
|
143
|
+
queue.queued ?? null,
|
|
144
|
+
queue.queueState ?? queue.state ?? null,
|
|
145
|
+
queue.position ?? null,
|
|
146
|
+
])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A failed subprocess read (`readable: false`) carries no information about queue membership
|
|
150
|
+
// — it is not evidence the PR is still queued, and it is not evidence it left the queue
|
|
151
|
+
// either. Only a clean read that reports absence should move the counter that eventually
|
|
152
|
+
// settles `lost-queue-membership` in `classifyObservation`. Fixes #611 defect 1: two
|
|
153
|
+
// consecutive transient `gh`/`mergify` failures were previously indistinguishable from two
|
|
154
|
+
// consecutive genuine "not queued" reads, and both incremented the same counter.
|
|
155
|
+
export function nextMissingActiveReads({
|
|
156
|
+
active,
|
|
157
|
+
// Prefer everActive when supplied. Fall back to seenQueued for older callers so a
|
|
158
|
+
// genuine post-entry loss still counts; new callers pass everActive=false until
|
|
159
|
+
// membership is observed in-process (closes the --seen-queued pre-admission false DEQUEUE).
|
|
160
|
+
everActive = null,
|
|
161
|
+
seenQueued = false,
|
|
162
|
+
missingActiveReads = 0,
|
|
163
|
+
readable = true,
|
|
164
|
+
} = {}) {
|
|
165
|
+
if (active) return 0
|
|
166
|
+
const mayCount = everActive === null ? seenQueued === true : everActive === true
|
|
167
|
+
if (!mayCount) return missingActiveReads
|
|
168
|
+
return readable ? missingActiveReads + 1 : missingActiveReads
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function stallVerdict({ lastChangeMs, nowMs, stallMinutes }) {
|
|
172
|
+
const bound = Number(stallMinutes)
|
|
173
|
+
if (!Number.isFinite(bound) || bound <= 0) return { stalled: false, reason: 'invalid stall bound' }
|
|
174
|
+
const stalledForMs = nowMs - lastChangeMs
|
|
175
|
+
return stalledForMs >= bound * 60_000
|
|
176
|
+
? { stalled: true, stalledForMs, reason: `no observable Mergify delivery-state change for ${Math.round(stalledForMs / 60_000)}m` }
|
|
177
|
+
: { stalled: false, stalledForMs }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Phase-aware no-change bound. Pre-admission (`everActive` still false) uses the shorter
|
|
181
|
+
// admission stall — queue entry after a real enqueue should appear quickly, and burning the
|
|
182
|
+
// full post-entry bound on a dead admission path is pure wait cost. Once this process has
|
|
183
|
+
// observed membership, use the full stall bound: batch wait / bisect can sit still for longer
|
|
184
|
+
// while still being healthy. Admission is never allowed to exceed the post-entry stall, so a
|
|
185
|
+
// caller that shortens `--stall-minutes` for tests or a suspect PR keeps both phases tight.
|
|
186
|
+
export function effectiveStallMinutes({
|
|
187
|
+
everActive = false,
|
|
188
|
+
stallMinutes = DEFAULT_STALL_MINUTES,
|
|
189
|
+
admissionStallMinutes = DEFAULT_ADMISSION_STALL_MINUTES,
|
|
190
|
+
} = {}) {
|
|
191
|
+
const post = Number(stallMinutes)
|
|
192
|
+
if (!Number.isFinite(post) || post <= 0) return { minutes: NaN, phase: 'invalid' }
|
|
193
|
+
if (everActive === true) return { minutes: post, phase: 'queued' }
|
|
194
|
+
const admission = Number(admissionStallMinutes)
|
|
195
|
+
const pre = Number.isFinite(admission) && admission > 0 ? Math.min(admission, post) : post
|
|
196
|
+
return { minutes: pre, phase: 'admission' }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Fixes #611 defect 2: `execFileSync` had no `timeout`, so a stalled TLS handshake or hung
|
|
200
|
+
// `gh`/`mergify` invocation blocked the watcher past both `--stall-minutes` and
|
|
201
|
+
// `--max-minutes` — those bounds are only evaluated after a poll returns. The bound is a
|
|
202
|
+
// small multiple of the poll interval: generous enough that normal network latency (measured
|
|
203
|
+
// ~4.5s wall for a healthy `gh pr view`) never trips it, but bounded well under the absolute
|
|
204
|
+
// ceiling so a hung child cannot outlive it. Floors at 10s so a very short poll interval
|
|
205
|
+
// cannot turn this into a near-instant timeout — but that floor is itself clamped against
|
|
206
|
+
// `boundMinutes` (the caller passes the effective phase stall bound capped by `--max-minutes`)
|
|
207
|
+
// so a single read's own timeout can never exceed the watch's own stall/ceiling bounds;
|
|
208
|
+
// otherwise a small `--stall-minutes` / `--admission-stall-minutes` on a slow/misconfigured
|
|
209
|
+
// `--interval-seconds` could let one hung read alone outlive the bound it is meant to sit
|
|
210
|
+
// inside of. Pre-admission uses the shorter admission bound; post-entry uses the full stall.
|
|
211
|
+
//
|
|
212
|
+
// Issue #615 item 1 (round-2 NOTE-2 on #612): clamping to the *whole* bound is only correct
|
|
213
|
+
// for a watcher that makes ONE read per poll. This watcher makes TWO (`gh pr view` +
|
|
214
|
+
// `mergify queue show`), and the stall/ceiling bounds are evaluated only after both reads in
|
|
215
|
+
// a poll return — so clamping each read to the full bound let a single poll take up to
|
|
216
|
+
// `2 x min(effectiveStall, max)` before the loop ever checked it, a 2x overrun the reviewer
|
|
217
|
+
// measured live as 58s against a 12s stall bound. Dividing the clamp by `readsPerPoll` keeps
|
|
218
|
+
// the *sum* of a poll's reads inside the bound: this watcher passes 2 (`READS_PER_POLL`
|
|
219
|
+
// below), the sibling (`watch-delivery.mjs`, one read per poll) passes 1 and is unaffected
|
|
220
|
+
// (dividing by 1 is a no-op) — matching `references/watch-contract.md`'s claim, made true
|
|
221
|
+
// rather than aspirational.
|
|
222
|
+
// Shared by `parseArgs` and `readTimeoutMs`'s defensive fallback, so the two can never drift
|
|
223
|
+
// apart and make the "falls back to the documented default" claim quietly false.
|
|
224
|
+
export const DEFAULT_INTERVAL_SECONDS = 90
|
|
225
|
+
export const DEFAULT_STALL_MINUTES = 10
|
|
226
|
+
// Pre-admission only: never observed membership this process. After enqueue, queue entry
|
|
227
|
+
// should show up well inside a few minutes; waiting the full post-entry stall on a dead
|
|
228
|
+
// admission path (stale Mergify status, unreadable queue API, forgotten enqueue) is pure cost.
|
|
229
|
+
export const DEFAULT_ADMISSION_STALL_MINUTES = 3
|
|
230
|
+
export const DEFAULT_MAX_MINUTES = 60
|
|
231
|
+
|
|
232
|
+
// A hard ceiling on the absolute ceiling, enforced in `runCli` rather than left to the caller's
|
|
233
|
+
// judgement. `--max-minutes` is the backstop that bounds a watch whose queue keeps reporting
|
|
234
|
+
// change — the case `--stall-minutes` deliberately cannot catch, because movement resets it. So
|
|
235
|
+
// it is the only bound standing between "the queue is busy" and a watch that outlives the
|
|
236
|
+
// session that launched it, and a value nobody sanity-checks is exactly how that happens.
|
|
237
|
+
//
|
|
238
|
+
// One hour is chosen against what the bound is actually protecting: a batch takes minutes, so an
|
|
239
|
+
// hour of continuous movement without finishing is already a queue worth handing back to a human
|
|
240
|
+
// rather than waiting out. Handing back at 60m and relaunching is strictly better than a blind
|
|
241
|
+
// 120m wait — the caller gets a `phases`-carrying verdict it can route on, an hour sooner.
|
|
242
|
+
//
|
|
243
|
+
// Over-ceiling values are REFUSED (exit 2), not silently clamped. Clamping would let a caller
|
|
244
|
+
// believe it had a four-hour watch while getting one hour, and the two streams here are already
|
|
245
|
+
// committed: stdout is exactly one terminal JSON verdict, stderr is one NDJSON heartbeat per
|
|
246
|
+
// poll, so there is no channel for a warning that would not break a documented contract. An
|
|
247
|
+
// exit-2 usage error is loud, immediate, and matches how every other bad bound is handled.
|
|
248
|
+
export const MAX_MINUTES_CEILING = 60
|
|
249
|
+
|
|
250
|
+
export function readTimeoutMs(intervalSeconds, boundMinutes, readsPerPoll = 1) {
|
|
251
|
+
const seconds = Number(intervalSeconds)
|
|
252
|
+
const bounded = Number.isFinite(seconds) && seconds > 0 ? seconds : DEFAULT_INTERVAL_SECONDS
|
|
253
|
+
const computed = Math.max(bounded * 2000, 10_000)
|
|
254
|
+
const bound = Number(boundMinutes)
|
|
255
|
+
const reads = Number.isFinite(readsPerPoll) && readsPerPoll > 0 ? readsPerPoll : 1
|
|
256
|
+
return Number.isFinite(bound) && bound > 0 ? Math.min(computed, (bound * 60_000) / reads) : computed
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Both subprocess reads this watcher makes per poll — `gh pr view` in `prObservation`,
|
|
260
|
+
// `mergify queue show` in `queueObservation`. See `readTimeoutMs` above.
|
|
261
|
+
const READS_PER_POLL = 2
|
|
262
|
+
|
|
263
|
+
export function run(cmd, args, options = {}) {
|
|
264
|
+
try {
|
|
265
|
+
return { ok: true, stdout: execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...options }) }
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return { ok: false, stdout: error.stdout?.toString?.() || '', stderr: error.stderr?.toString?.() || error.message || '', error }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Issue #615 items 3-4 (round-2 NOTE-1 on #612): `JSON.parse(text || '{}')` treated an
|
|
272
|
+
// empty string as a valid, truthy `{}` object rather than the honest "not JSON" answer —
|
|
273
|
+
// the same manufactured-informative-read shape `queueObservation`'s BLOCK-1 fix closed for
|
|
274
|
+
// the mergify-CLI path. `prObservation` calls this on `result.ok === true` output only, so
|
|
275
|
+
// the live BLOCK-1 defect never reached it (a failed/timed-out `gh` is caught earlier by
|
|
276
|
+
// `if (!result.ok)`), but a `gh` that exits 0 with empty stdout — unreachable by any
|
|
277
|
+
// current caller, never reproduced against a real `gh` — would have manufactured a
|
|
278
|
+
// `readable: true` observation from nothing. Dropping the fallback closes the class at its
|
|
279
|
+
// root for both call sites: `JSON.parse('')` throwing is the honest answer, matching the
|
|
280
|
+
// sibling watcher's raw `JSON.parse` (`watch-delivery.mjs`'s `observe()`), which was already
|
|
281
|
+
// safe by construction for exactly this reason.
|
|
282
|
+
function parseJson(text) {
|
|
283
|
+
try { return JSON.parse(text) } catch { return null }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Issue #629: the one piece of history a single `labels` snapshot cannot carry — WHEN a
|
|
287
|
+
// `dequeued` label was applied. `gh pr view --json` has no `timelineItems` field in the CLI
|
|
288
|
+
// version this was built against (`gh pr view --json timelineItems` errors "Unknown JSON
|
|
289
|
+
// field"), so this reads the other source the issue names: Mergify's own status comment.
|
|
290
|
+
//
|
|
291
|
+
// BLOCK-1/2 fix (independent review on PR #637): the first version of this function read
|
|
292
|
+
// `mergifyComments[mergifyComments.length - 1]` — "the last mergify-authored comment" — on the
|
|
293
|
+
// premise that it "reflects the most recent cycle". That premise is false: Mergify also
|
|
294
|
+
// authors merge-protection summaries and command acknowledgements from the same account, and
|
|
295
|
+
// those interleave with (and are very often more recent than) the actual queue-status
|
|
296
|
+
// comments. Verified against the REAL payload of PR #620 — the PR issue #629 reproduces on —
|
|
297
|
+
// via `gh pr view --json comments`: of its 4 mergify-authored comments, the one carrying
|
|
298
|
+
// `Left the queue — 2026-07-28 11:04 UTC` is NOT the last one; the last one is a
|
|
299
|
+
// `> queue` / "Command ignored because it is already running" acknowledgement with no payload
|
|
300
|
+
// at all. The shipped helper returned `null` against this exact real data, which made guard 2
|
|
301
|
+
// (the freshness check below) dead code in production: for a GENUINE dequeue the watcher never
|
|
302
|
+
// saw `queued` for, neither this path nor the `missingActiveReads`/`absentStreak` fallback
|
|
303
|
+
// (which requires having observed `queued` first) can fire, so the watch ran to
|
|
304
|
+
// `--max-minutes` instead of settling (reproduced: `reason: 'conditions-unmet',
|
|
305
|
+
// observations: 7`).
|
|
306
|
+
//
|
|
307
|
+
// Fix: every Mergify status comment opens with a machine-readable, HTML-commented payload —
|
|
308
|
+
// `<!--- DO NOT EDIT -*- Mergify Payload -*- {"version":1,"state":"dequeued",...} -*- Mergify
|
|
309
|
+
// Payload End -*- -->` (NOTE-3) — which is exactly the discriminator "is this comment a queue-
|
|
310
|
+
// status comment at all" that the length-based pick was missing. Scan ALL mergify comments,
|
|
311
|
+
// parse each one's payload, and select the LAST one whose `state` is `"dequeued"`.
|
|
312
|
+
//
|
|
313
|
+
// Deliberate choice, because getting it wrong the other way reproduces the same class of bug:
|
|
314
|
+
// this reads for the most recent comment whose STATE HAPPENS TO BE "dequeued" — not simply
|
|
315
|
+
// "the most recent status comment, whatever its state" (on #620 that is the `state:"merged"`
|
|
316
|
+
// comment, and keying off it would read a freshly-MERGED PR as still dequeued). The `dequeued`
|
|
317
|
+
// label lingers on the PR without being cleared across a later re-enqueue, so the label's own
|
|
318
|
+
// claim is "the last attempt that dequeued was THIS one" — the comment that actually made that
|
|
319
|
+
// claim is the one this function must find. If a later attempt is active or merged, the
|
|
320
|
+
// `active`/merged branches in the callers below already win before this function is even
|
|
321
|
+
// consulted for a terminal verdict.
|
|
322
|
+
//
|
|
323
|
+
// The payload has no millisecond-precision exit timestamp of its own (only `queued_at`, the
|
|
324
|
+
// ENTRY time for that attempt) — so the actual "Left the queue" instant still comes from that
|
|
325
|
+
// selected comment's rendered text (kept as NOTE-3's suggested fallback source, not replaced
|
|
326
|
+
// by it). That text is minute-precision with no timezone beyond a literal `UTC` suffix, and
|
|
327
|
+
// it is Mergify's SERVER clock, compared below against this machine's local `Date.now()`
|
|
328
|
+
// (`startedMs`) — a locally-slow clock could in principle make a stale dequeue read as fresh;
|
|
329
|
+
// low risk on an NTP-synced host, called out here rather than silently assumed away (NOTE-2).
|
|
330
|
+
//
|
|
331
|
+
// Returns epoch milliseconds, or null when no mergify comment's payload reports
|
|
332
|
+
// `state: "dequeued"`, or that comment has no parseable "Left the queue" line — callers must
|
|
333
|
+
// treat null as "no reliable timestamp", never as "not dequeued".
|
|
334
|
+
const MERGIFY_PAYLOAD_RE = /-\*- Mergify Payload -\*-\s*\n(\{[\s\S]*?\})\s*\n-\*- Mergify Payload End -\*-/
|
|
335
|
+
const LEFT_QUEUE_RE = /Left the queue\*\*[^`\n]*`(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}) UTC`/g
|
|
336
|
+
|
|
337
|
+
export function dequeuedAtFromComments(comments) {
|
|
338
|
+
const list = Array.isArray(comments) ? comments : []
|
|
339
|
+
const mergifyComments = list.filter((c) => c && c.author && c.author.login === 'mergify')
|
|
340
|
+
let target = null
|
|
341
|
+
for (const comment of mergifyComments) {
|
|
342
|
+
const body = typeof comment.body === 'string' ? comment.body : ''
|
|
343
|
+
const payloadMatch = MERGIFY_PAYLOAD_RE.exec(body)
|
|
344
|
+
if (!payloadMatch) continue
|
|
345
|
+
let payload
|
|
346
|
+
try { payload = JSON.parse(payloadMatch[1]) } catch { continue }
|
|
347
|
+
if (payload && payload.state === 'dequeued') target = comment // last match wins — comments are chronological
|
|
348
|
+
}
|
|
349
|
+
if (!target) return null
|
|
350
|
+
const body = typeof target.body === 'string' ? target.body : ''
|
|
351
|
+
let match
|
|
352
|
+
let last = null
|
|
353
|
+
LEFT_QUEUE_RE.lastIndex = 0
|
|
354
|
+
while ((match = LEFT_QUEUE_RE.exec(body))) last = match
|
|
355
|
+
if (!last) return null
|
|
356
|
+
const [, y, mo, d, h, mi] = last
|
|
357
|
+
return Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Exported so tests can pin that the live read timeout uses the same phase-aware bound the
|
|
361
|
+
// stall clock will fire on — a hung pre-admission poll must not outlive admission-stall, and
|
|
362
|
+
// a post-entry poll must not be clamped to the shorter admission bound forever.
|
|
363
|
+
export function readTimeoutForArgs({
|
|
364
|
+
intervalSeconds,
|
|
365
|
+
stallMinutes,
|
|
366
|
+
admissionStallMinutes,
|
|
367
|
+
maxMinutes,
|
|
368
|
+
everActive = false,
|
|
369
|
+
} = {}) {
|
|
370
|
+
const { minutes: phaseBound } = effectiveStallMinutes({
|
|
371
|
+
everActive,
|
|
372
|
+
stallMinutes,
|
|
373
|
+
admissionStallMinutes,
|
|
374
|
+
})
|
|
375
|
+
const bounds = [phaseBound, maxMinutes].filter((n) => Number.isFinite(n) && n > 0)
|
|
376
|
+
return readTimeoutMs(intervalSeconds, bounds.length ? Math.min(...bounds) : undefined, READS_PER_POLL)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function prObservation({ repo, pr, intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive = false }) {
|
|
380
|
+
// `comments` costs nothing extra to request in this same call and carries the one signal
|
|
381
|
+
// `dequeuedAtFromComments` needs — no additional subprocess read, so the READS_PER_POLL
|
|
382
|
+
// budget below is unaffected by issue #629's fix.
|
|
383
|
+
const result = run('gh', ['pr', 'view', String(pr), '--repo', repo, '--json', 'state,mergedAt,mergeCommit,labels,url,comments'], { timeout: readTimeoutForArgs({ intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) })
|
|
384
|
+
if (!result.ok) return { readable: false, error: result.stderr || result.stdout }
|
|
385
|
+
const data = parseJson(result.stdout)
|
|
386
|
+
if (!data) return { readable: false, error: 'gh pr view returned invalid JSON' }
|
|
387
|
+
return {
|
|
388
|
+
readable: true,
|
|
389
|
+
state: data.state || null,
|
|
390
|
+
mergedAt: data.mergedAt || null,
|
|
391
|
+
mergeCommitOid: data.mergeCommit?.oid || null,
|
|
392
|
+
labels: (data.labels || []).map((label) => label.name).filter(Boolean),
|
|
393
|
+
url: data.url || null,
|
|
394
|
+
dequeuedAtMs: dequeuedAtFromComments(data.comments),
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function mergifyQueueShowArgs({ pr, repo }) {
|
|
399
|
+
return ['queue', '--repository', repo, 'show', String(pr), '--json']
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Exported so the resolve-issues delivery watcher can use THIS reading of queue membership
|
|
403
|
+
// rather than keeping a second copy. That watcher is label-only today, which is why its
|
|
404
|
+
// dequeue resolution is weak: a `queued` label that lags or a `dequeued` label that never
|
|
405
|
+
// lands leaves it with no second opinion, while `mergify queue show --json` is authoritative
|
|
406
|
+
// for membership. Two copies of this inference would drift — and the `queued`-field trap
|
|
407
|
+
// below is exactly the kind of detail one copy would miss.
|
|
408
|
+
export function queueObservation({ pr, repo, intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive = false }) {
|
|
409
|
+
const result = run('mergify', mergifyQueueShowArgs({ pr, repo }), { timeout: readTimeoutForArgs({ intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) })
|
|
410
|
+
// BLOCK-1 fix (independent review on #612): `parseJson` falls back to `'{}'` on empty
|
|
411
|
+
// input, and `{}` is truthy — so a failed or timed-out `mergify` process (empty stdout,
|
|
412
|
+
// its message on stderr) parsed to a truthy `{}` and fell into the "successfully read"
|
|
413
|
+
// branch below, reporting `readable: true, queued: false`. That made `queueObservation`
|
|
414
|
+
// the one place in this file that DID treat an unreadable read as informative, defeating
|
|
415
|
+
// the exact fix `nextMissingActiveReads` exists for: two consecutive `mergify` failures
|
|
416
|
+
// read as two consecutive genuine "not queued" reads and settled a false DEQUEUED, and a
|
|
417
|
+
// timed-out `mergify` (defect 2's fix) fed the same false path, arriving *faster* than the
|
|
418
|
+
// stall bound rather than routing through it. `result.ok` (exit code) AND non-empty stdout
|
|
419
|
+
// are both required before the parsed body is trusted; a non-zero exit or empty stdout is
|
|
420
|
+
// exactly the shape of a failed/timed-out subprocess, not a real answer.
|
|
421
|
+
const stdoutText = typeof result.stdout === 'string' ? result.stdout.trim() : ''
|
|
422
|
+
const parsed = result.ok && stdoutText ? parseJson(stdoutText) : null
|
|
423
|
+
if (parsed) {
|
|
424
|
+
return {
|
|
425
|
+
readable: true,
|
|
426
|
+
queued: parsed.queued === true || Boolean(parsed.queue_rule_name || parsed.queue_rule),
|
|
427
|
+
queueState: parsed.status || parsed.state || parsed.mergeability_check?.state || null,
|
|
428
|
+
position: typeof parsed.position === 'number' ? parsed.position : null,
|
|
429
|
+
raw: parsed,
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const text = `${result.stdout}\n${result.stderr}`.trim()
|
|
433
|
+
if (/not in the merge queue|not queued|not found/i.test(text)) return { readable: true, queued: false, queueState: 'not-queued', position: null, raw: text }
|
|
434
|
+
return { readable: false, queued: false, queueState: null, position: null, raw: text || 'mergify queue show failed' }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Every poll's observation, as one compact NDJSON record — the watcher's heartbeat.
|
|
438
|
+
//
|
|
439
|
+
// Before this existed the loop's ONLY output sites were the three terminal ones (`MERGED`/
|
|
440
|
+
// `DEQUEUED`/`CLOSED_UNMERGED` below, plus the two `STALLED` paths), so a watch sitting in
|
|
441
|
+
// `NOT_YET_QUEUED` or `QUEUED` — the overwhelmingly common case, and the entire healthy path —
|
|
442
|
+
// wrote zero bytes for its whole life. Reproduced live on PR #1396: 25 minutes of watching a
|
|
443
|
+
// PR that was never queued produced a log containing only the harness's own spawn banner, and
|
|
444
|
+
// the watch was killed 4 minutes short of the then-30-minute stall bound, so it never printed
|
|
445
|
+
// even its terminal verdict. (The bound is 10 now, so that same watch would settle on its own
|
|
446
|
+
// well before a human gave up on it — but the heartbeat is what makes the wait legible while
|
|
447
|
+
// it runs, and no bound short enough to substitute for it would be long enough to be correct.) A silent process is indistinguishable from a hung or dead one, which is
|
|
448
|
+
// exactly the state the caller is trying to resolve, and the hand-written shell watchers this
|
|
449
|
+
// script is meant to replace all printed a per-poll status line. Losing that on the way to a
|
|
450
|
+
// correct state machine traded one failure (wrong answers) for another (no answers).
|
|
451
|
+
//
|
|
452
|
+
// Deliberately stderr, not stdout: stdout is the machine contract — exactly one pretty-printed
|
|
453
|
+
// terminal JSON object, which callers parse whole — and interleaving heartbeats into it would
|
|
454
|
+
// break every such caller. stderr is unparsed by the contract and is merged into the log by
|
|
455
|
+
// every process supervisor that would run this, which is the place a human actually looks.
|
|
456
|
+
// `terminal: false` and the separate stream both distinguish these from the real verdict.
|
|
457
|
+
export function progressRecord({ payload, changed, sinceChangeMs, nowIso }) {
|
|
458
|
+
return JSON.stringify({ ...payload, progress: true, changed, sinceChangeMs, at: nowIso })
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Defaults. `stallMinutes` is a NO-CHANGE bound, not an elapsed-time one — any observable
|
|
462
|
+
// movement (queue position, label, queue state) resets it — so 10 minutes of a completely
|
|
463
|
+
// frozen picture is already a long time to wait before handing back, and the earlier 30 (then
|
|
464
|
+
// 15) multiplied every dead watch's cost for no added signal. At a 90s poll, 10 minutes is
|
|
465
|
+
// still ~6 consecutive identical observations before the watch calls it — comfortably more
|
|
466
|
+
// than a transient blip. `intervalSeconds` at 90 also cuts the
|
|
467
|
+
// API budget: two reads per poll is 80/hour per watched PR, against 240/hour at the old 30s,
|
|
468
|
+
// which is what makes an epic-sized fan-out of concurrent watchers fit inside an authenticated
|
|
469
|
+
// token's 5,000/hour limit. The cost is at most 90s of extra merge-detection latency on a
|
|
470
|
+
// queue that takes minutes per batch.
|
|
471
|
+
export function parseArgs(argv) {
|
|
472
|
+
const args = {
|
|
473
|
+
intervalSeconds: DEFAULT_INTERVAL_SECONDS,
|
|
474
|
+
stallMinutes: DEFAULT_STALL_MINUTES,
|
|
475
|
+
admissionStallMinutes: DEFAULT_ADMISSION_STALL_MINUTES,
|
|
476
|
+
maxMinutes: DEFAULT_MAX_MINUTES,
|
|
477
|
+
}
|
|
478
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
479
|
+
const arg = argv[i]
|
|
480
|
+
if (arg === '--repo') args.repo = argv[++i]
|
|
481
|
+
else if (arg === '--pr') args.pr = argv[++i]
|
|
482
|
+
else if (arg === '--interval-seconds') args.intervalSeconds = Number(argv[++i])
|
|
483
|
+
else if (arg === '--stall-minutes') args.stallMinutes = Number(argv[++i])
|
|
484
|
+
else if (arg === '--admission-stall-minutes') args.admissionStallMinutes = Number(argv[++i])
|
|
485
|
+
else if (arg === '--max-minutes') args.maxMinutes = Number(argv[++i])
|
|
486
|
+
else if (arg === '--seen-queued') args.seenQueued = true
|
|
487
|
+
else if (arg === '--quiet') args.quiet = true
|
|
488
|
+
else if (arg === '--help' || arg === '-h') args.help = true
|
|
489
|
+
else args.unknown = arg
|
|
490
|
+
}
|
|
491
|
+
return args
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
495
|
+
|
|
496
|
+
function help() {
|
|
497
|
+
return `watch-pr-delivery --repo <owner/name> --pr <number> [options]
|
|
498
|
+
|
|
499
|
+
Observe a Mergify-queued PR until MERGED, DEQUEUED, CLOSED_UNMERGED, or STALLED.
|
|
500
|
+
The script observes only; it never queues, dequeues, merges, or records state.
|
|
501
|
+
|
|
502
|
+
Enqueue first. On a repo with manual delivery (no auto_merge_conditions, autoqueue: false)
|
|
503
|
+
an unqueued PR never enters the queue on its own, so this watch can only ever end STALLED.
|
|
504
|
+
|
|
505
|
+
Options:
|
|
506
|
+
--interval-seconds <n> Poll interval, default 90
|
|
507
|
+
--stall-minutes <n> Post-entry stall bound (no-change, not elapsed), default 10
|
|
508
|
+
--admission-stall-minutes <n> Pre-admission stall bound (never observed membership), default 3.
|
|
509
|
+
For CI-gated admission (Mergify waiting on a check), relaunch
|
|
510
|
+
with 15 and raise --stall-minutes to at least 15; do not use 60.
|
|
511
|
+
--max-minutes <n> Absolute safety ceiling, default 60, hard maximum 60
|
|
512
|
+
--seen-queued Resume hint only: keep phases/callers aware the PR was queued.
|
|
513
|
+
Does NOT seed lost-membership counting — membership must be
|
|
514
|
+
observed in this process (everActive) before DEQUEUED can fire.
|
|
515
|
+
--quiet Suppress the per-poll progress heartbeat on stderr
|
|
516
|
+
|
|
517
|
+
Output:
|
|
518
|
+
stdout exactly one pretty-printed terminal JSON verdict, at exit
|
|
519
|
+
stderr one compact JSON progress record per poll (progress:true), unless --quiet
|
|
520
|
+
|
|
521
|
+
Both carry "phases": the lifecycle progression observed so far, in order — e.g.
|
|
522
|
+
["NOT_YET_QUEUED"] ending STALLED reason admission-stall means never admitted (enqueue it),
|
|
523
|
+
while ["NOT_YET_QUEUED","QUEUED"] ending STALLED reason stall-bound means admitted then
|
|
524
|
+
wedged in the queue.
|
|
525
|
+
|
|
526
|
+
Exit codes:
|
|
527
|
+
0 MERGED; stdout JSON includes mergedSha, which still needs ancestry proof
|
|
528
|
+
4 terminal but not merged: DEQUEUED, CLOSED_UNMERGED, or STALLED
|
|
529
|
+
2 usage error
|
|
530
|
+
|
|
531
|
+
Contract: this watches a progression, not a state. Once queue entry has been observed,
|
|
532
|
+
queued:false is a queue exit unless the PR merged.`
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export async function runCli(argv) {
|
|
536
|
+
const args = parseArgs(argv)
|
|
537
|
+
if (args.help) {
|
|
538
|
+
console.log(help())
|
|
539
|
+
return 0
|
|
540
|
+
}
|
|
541
|
+
if (args.unknown || !args.repo || !args.pr) {
|
|
542
|
+
console.error(help())
|
|
543
|
+
return 2
|
|
544
|
+
}
|
|
545
|
+
for (const key of ['intervalSeconds', 'stallMinutes', 'admissionStallMinutes', 'maxMinutes']) {
|
|
546
|
+
if (!Number.isFinite(args[key]) || args[key] <= 0) {
|
|
547
|
+
console.error(`${key} must be a positive number`)
|
|
548
|
+
return 2
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (args.maxMinutes > MAX_MINUTES_CEILING) {
|
|
552
|
+
console.error(
|
|
553
|
+
`--max-minutes ${args.maxMinutes} exceeds the ${MAX_MINUTES_CEILING}-minute ceiling. ` +
|
|
554
|
+
'A watch that outlives its session is worse than one that hands back: relaunch after a ' +
|
|
555
|
+
`${MAX_MINUTES_CEILING}m verdict instead. See MAX_MINUTES_CEILING for why.`,
|
|
556
|
+
)
|
|
557
|
+
return 2
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const startedMs = Date.now()
|
|
561
|
+
// `--seen-queued` seeds resume intent only. everActive flips only after THIS process
|
|
562
|
+
// observes membership — required before lost-queue-membership can fire.
|
|
563
|
+
let seenQueued = args.seenQueued === true
|
|
564
|
+
let everActive = false
|
|
565
|
+
let missingActiveReads = 0
|
|
566
|
+
let lastKey = null
|
|
567
|
+
let lastChangeMs = startedMs
|
|
568
|
+
let observations = 0
|
|
569
|
+
let phases = []
|
|
570
|
+
|
|
571
|
+
for (;;) {
|
|
572
|
+
// Pass everActive into the reads so their per-child timeout tracks the phase bound that
|
|
573
|
+
// will actually fire STALLED this poll. Dropping it left admissionStallMinutes on args
|
|
574
|
+
// unused (destructuring stripped it) and hung pre-admission polls could outlive the 3m
|
|
575
|
+
// admission clock while still being clamped only to the 10m post-entry bound.
|
|
576
|
+
const pr = prObservation({ ...args, everActive })
|
|
577
|
+
const queue = queueObservation({ ...args, everActive })
|
|
578
|
+
observations += 1
|
|
579
|
+
const active = (Array.isArray(pr.labels) && pr.labels.includes('queued')) || queue.queued === true
|
|
580
|
+
const readable = pr.readable !== false || queue.readable !== false
|
|
581
|
+
if (active) {
|
|
582
|
+
seenQueued = true
|
|
583
|
+
everActive = true
|
|
584
|
+
}
|
|
585
|
+
missingActiveReads = nextMissingActiveReads({ active, everActive, seenQueued, missingActiveReads, readable })
|
|
586
|
+
|
|
587
|
+
const verdict = classifyObservation({
|
|
588
|
+
pr,
|
|
589
|
+
queue,
|
|
590
|
+
seenQueued,
|
|
591
|
+
everActive,
|
|
592
|
+
missingActiveReads,
|
|
593
|
+
startedMs,
|
|
594
|
+
dequeuedAtMs: pr.dequeuedAtMs ?? null,
|
|
595
|
+
})
|
|
596
|
+
if (verdict.everActive === true) everActive = true
|
|
597
|
+
if (verdict.seenQueued === true) seenQueued = true
|
|
598
|
+
phases = nextPhases(phases, verdict.outcome)
|
|
599
|
+
const payload = {
|
|
600
|
+
outcome: verdict.outcome,
|
|
601
|
+
terminal: verdict.terminal,
|
|
602
|
+
reason: verdict.reason || null,
|
|
603
|
+
detail: verdict.detail || null,
|
|
604
|
+
mergedSha: verdict.mergedSha || null,
|
|
605
|
+
seenQueued,
|
|
606
|
+
everActive,
|
|
607
|
+
phases,
|
|
608
|
+
missingActiveReads,
|
|
609
|
+
observations,
|
|
610
|
+
elapsedMs: Date.now() - startedMs,
|
|
611
|
+
pr: { state: pr.state || null, mergedAt: pr.mergedAt || null, labels: pr.labels || [], url: pr.url || null, readable: pr.readable !== false, dequeuedAtMs: pr.dequeuedAtMs ?? null },
|
|
612
|
+
queue: { queued: queue.queued === true, queueState: queue.queueState || null, position: queue.position ?? null, readable: queue.readable !== false },
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (verdict.terminal) {
|
|
616
|
+
console.log(JSON.stringify(payload, null, 2))
|
|
617
|
+
return verdict.outcome === 'MERGED' ? 0 : 4
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const key = observationKey({ pr, queue })
|
|
621
|
+
const changed = key !== lastKey
|
|
622
|
+
if (changed) {
|
|
623
|
+
lastKey = key
|
|
624
|
+
lastChangeMs = Date.now()
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Heartbeat before the bound checks below, so the last thing in the log before a STALLED
|
|
628
|
+
// verdict is the unchanging observation that produced it — see `progressRecord`.
|
|
629
|
+
if (!args.quiet) {
|
|
630
|
+
const now = Date.now()
|
|
631
|
+
console.error(progressRecord({ payload, changed, sinceChangeMs: now - lastChangeMs, nowIso: new Date(now).toISOString() }))
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const elapsedMs = Date.now() - startedMs
|
|
635
|
+
if (elapsedMs >= args.maxMinutes * 60_000) {
|
|
636
|
+
console.log(JSON.stringify({ ...payload, terminal: true, outcome: 'STALLED', phases: nextPhases(phases, 'STALLED'), reason: 'max-minutes', detail: `watch exceeded ${args.maxMinutes}m absolute ceiling` }, null, 2))
|
|
637
|
+
return 4
|
|
638
|
+
}
|
|
639
|
+
// Phase-aware bound: pre-admission uses --admission-stall-minutes (default 3); once this
|
|
640
|
+
// process has observed membership, use the full --stall-minutes (default 10). Outcome
|
|
641
|
+
// stays STALLED either way — only reason/detail (and wall time) change. Callers already
|
|
642
|
+
// route on phases for never-admitted vs wedged-in-queue.
|
|
643
|
+
const bound = effectiveStallMinutes({
|
|
644
|
+
everActive,
|
|
645
|
+
stallMinutes: args.stallMinutes,
|
|
646
|
+
admissionStallMinutes: args.admissionStallMinutes,
|
|
647
|
+
})
|
|
648
|
+
const stall = stallVerdict({ lastChangeMs, nowMs: Date.now(), stallMinutes: bound.minutes })
|
|
649
|
+
if (stall.stalled) {
|
|
650
|
+
// Same theme as `nextMissingActiveReads`, surfaced for a human/caller reading the
|
|
651
|
+
// terminal payload: a stall reached while the most recent poll was unreadable is not
|
|
652
|
+
// the same situation as a stall reached on clean, unchanging reads — the sibling
|
|
653
|
+
// watcher (`watch-delivery.mjs`) already makes this distinction on its own stall path.
|
|
654
|
+
// Readable pre-admission stalls are `admission-stall` so a dead admission path is not
|
|
655
|
+
// mistaken for a queue that admitted and then froze.
|
|
656
|
+
const reason = !readable ? 'unreadable' : (bound.phase === 'admission' ? 'admission-stall' : 'stall-bound')
|
|
657
|
+
console.log(JSON.stringify({ ...payload, terminal: true, outcome: 'STALLED', phases: nextPhases(phases, 'STALLED'), reason, detail: stall.reason }, null, 2))
|
|
658
|
+
return 4
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
await sleep(args.intervalSeconds * 1000)
|
|
662
|
+
}
|
|
663
|
+
}
|