@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.
Files changed (397) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +90 -0
  3. package/bin/1aboveio-skills.mjs +18 -0
  4. package/package.json +28 -0
  5. package/runtime/skills/distribution/generated/recipes.json +1189 -0
  6. package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
  7. package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  8. package/skills/backend/airflow-dag-develop/LICENSE +3 -0
  9. package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
  10. package/skills/backend/app-debug/LICENSE +3 -0
  11. package/skills/backend/app-debug/SKILL.md +109 -0
  12. package/skills/backend/app-debug/references/common-errors.md +128 -0
  13. package/skills/backend/python-backend/LICENSE +3 -0
  14. package/skills/backend/python-backend/SKILL.md +326 -0
  15. package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
  16. package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
  17. package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
  18. package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
  19. package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
  20. package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
  21. package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
  22. package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
  23. package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
  24. package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
  25. package/skills/cicd-pipeline/docker/LICENSE +3 -0
  26. package/skills/cicd-pipeline/docker/SKILL.md +126 -0
  27. package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
  28. package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
  29. package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
  30. package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
  31. package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
  32. package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
  33. package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
  34. package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
  35. package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
  36. package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
  37. package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
  38. package/skills/cicd-pipeline/mergify/LICENSE +3 -0
  39. package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
  40. package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
  41. package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
  42. package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
  43. package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
  44. package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
  45. package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
  46. package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
  47. package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
  48. package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
  49. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
  50. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
  51. package/skills/cicd-pipeline/podman/LICENSE +3 -0
  52. package/skills/cicd-pipeline/podman/SKILL.md +70 -0
  53. package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
  54. package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  55. package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
  56. package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
  57. package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
  58. package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
  59. package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
  60. package/skills/engineering/e2e-test/LICENSE +3 -0
  61. package/skills/engineering/e2e-test/SKILL.md +156 -0
  62. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
  63. package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
  64. package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
  65. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  66. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
  67. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
  68. package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
  69. package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
  70. package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
  71. package/skills/engineering/e2e-test/references/debugging.md +36 -0
  72. package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
  73. package/skills/engineering/e2e-test/references/reviewing.md +39 -0
  74. package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
  75. package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
  76. package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
  77. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  78. package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
  79. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  80. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  81. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  82. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  83. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
  84. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  85. package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
  86. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
  87. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  88. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  89. package/skills/engineering/engineering-runtime/SKILL.md +10 -0
  90. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  91. package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
  92. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  93. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  94. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  95. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  96. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  97. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  98. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  99. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
  100. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
  101. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  102. package/skills/engineering/ensure-coverage/SKILL.md +136 -0
  103. package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
  104. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
  105. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
  106. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  107. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  108. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  109. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
  110. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  111. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
  112. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
  113. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
  114. package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
  115. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
  116. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
  117. package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
  118. package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
  119. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
  120. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  121. package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
  122. package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
  123. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
  124. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
  125. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
  126. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
  127. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
  128. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
  129. package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
  130. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
  131. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  132. package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
  133. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
  134. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  135. package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
  136. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  137. package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
  138. package/skills/engineering/harness-runtime/LICENSE +3 -0
  139. package/skills/engineering/harness-runtime/SKILL.md +18 -0
  140. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  141. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  142. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  143. package/skills/engineering/harness-runtime/contracts.md +15 -0
  144. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  145. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  146. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  147. package/skills/engineering/harness-runtime/index.mjs +767 -0
  148. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  149. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  150. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
  151. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  152. package/skills/engineering/implement-and-pr/LICENSE +3 -0
  153. package/skills/engineering/implement-and-pr/SKILL.md +176 -0
  154. package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
  155. package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
  156. package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
  157. package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
  158. package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
  159. package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
  160. package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
  161. package/skills/engineering/resolve-issues/LICENSE +3 -0
  162. package/skills/engineering/resolve-issues/SKILL.md +167 -0
  163. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
  164. package/skills/engineering/resolve-issues/references/breaker.md +82 -0
  165. package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
  166. package/skills/engineering/resolve-issues/references/delivery.md +108 -0
  167. package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
  168. package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
  169. package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
  170. package/skills/engineering/resolve-issues/references/finalization.md +68 -0
  171. package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
  172. package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
  173. package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
  174. package/skills/engineering/resolve-issues/references/intake.md +86 -0
  175. package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
  176. package/skills/engineering/resolve-issues/references/interference.md +87 -0
  177. package/skills/engineering/resolve-issues/references/loop.md +134 -0
  178. package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
  179. package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
  180. package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
  181. package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
  182. package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
  183. package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
  184. package/skills/engineering/resolve-issues/references/run-state.md +288 -0
  185. package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
  186. package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
  187. package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
  188. package/skills/engineering/resolve-issues/references/why.md +653 -0
  189. package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
  190. package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
  191. package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
  192. package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
  193. package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
  194. package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
  195. package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
  196. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  197. package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
  198. package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
  199. package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
  200. package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
  201. package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
  202. package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
  203. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
  204. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
  205. package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
  206. package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
  207. package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
  208. package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
  209. package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
  210. package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
  211. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  212. package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
  213. package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
  214. package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
  215. package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
  216. package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
  217. package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
  218. package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
  219. package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
  220. package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
  221. package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
  222. package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
  223. package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
  224. package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
  225. package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
  226. package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
  227. package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
  228. package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
  229. package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
  230. package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
  231. package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
  232. package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
  233. package/skills/engineering/resolve-release/LICENSE +3 -0
  234. package/skills/engineering/resolve-release/SKILL.md +112 -0
  235. package/skills/engineering/resolve-release/references/assembly.md +137 -0
  236. package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
  237. package/skills/engineering/resolve-release/references/candidate.md +167 -0
  238. package/skills/engineering/resolve-release/references/exposure.md +178 -0
  239. package/skills/engineering/resolve-release/references/handoff.md +24 -0
  240. package/skills/engineering/resolve-release/references/postmortem.md +230 -0
  241. package/skills/engineering/resolve-release/references/preflight.md +207 -0
  242. package/skills/engineering/resolve-release/references/principles.md +94 -0
  243. package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
  244. package/skills/engineering/resolve-release/references/related-skills.md +12 -0
  245. package/skills/engineering/resolve-release/references/routing.md +149 -0
  246. package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
  247. package/skills/engineering/resolve-release/references/versioning.md +202 -0
  248. package/skills/engineering/resolve-release/references/why.md +53 -0
  249. package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
  250. package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
  251. package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
  252. package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
  253. package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
  254. package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
  255. package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
  256. package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
  257. package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
  258. package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
  259. package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
  260. package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
  261. package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
  262. package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
  263. package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
  264. package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
  265. package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
  266. package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
  267. package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
  268. package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
  269. package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
  270. package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
  271. package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
  272. package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
  273. package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
  274. package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
  275. package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
  276. package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
  277. package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
  278. package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
  279. package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
  280. package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
  281. package/skills/engineering/resolve-release/vendor/README.md +60 -0
  282. package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
  283. package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
  284. package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
  285. package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
  286. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
  287. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
  288. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
  289. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
  290. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
  291. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
  292. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
  293. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
  294. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
  295. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
  296. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
  297. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
  298. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
  299. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
  300. package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
  301. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
  302. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
  303. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
  304. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
  305. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
  306. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
  307. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
  308. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
  309. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
  310. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
  311. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
  312. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
  313. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
  314. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
  315. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
  316. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  317. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
  318. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
  319. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
  320. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
  321. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
  322. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
  323. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
  324. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
  325. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
  326. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
  327. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
  328. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
  329. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
  330. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
  331. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
  332. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
  333. package/skills/engineering/review-pr/LICENSE +3 -0
  334. package/skills/engineering/review-pr/SKILL.md +123 -0
  335. package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
  336. package/skills/engineering/review-pr/references/code-correctness.md +5 -0
  337. package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
  338. package/skills/engineering/review-pr/references/coordination.md +18 -0
  339. package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
  340. package/skills/engineering/review-pr/references/finding-themes.md +7 -0
  341. package/skills/engineering/review-pr/references/github-posting.md +98 -0
  342. package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
  343. package/skills/engineering/review-pr/references/incremental-output.md +16 -0
  344. package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
  345. package/skills/engineering/review-pr/references/output-format.md +99 -0
  346. package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
  347. package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
  348. package/skills/engineering/review-pr/references/re-review.md +18 -0
  349. package/skills/engineering/review-pr/references/review-method.md +199 -0
  350. package/skills/engineering/review-pr/references/review-mode.md +30 -0
  351. package/skills/engineering/review-pr/references/review-posture.md +53 -0
  352. package/skills/engineering/review-pr/references/round1-depth.md +62 -0
  353. package/skills/engineering/review-pr/references/scripts.md +17 -0
  354. package/skills/engineering/review-pr/references/workflow.md +16 -0
  355. package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
  356. package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
  357. package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
  358. package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
  359. package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
  360. package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
  361. package/skills/engineering/smoke/LICENSE +3 -0
  362. package/skills/engineering/smoke/SKILL.md +131 -0
  363. package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
  364. package/skills/engineering/smoke/references/manifest.md +192 -0
  365. package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
  366. package/skills/fullstack/better-auth/LICENSE +3 -0
  367. package/skills/fullstack/better-auth/SKILL.md +601 -0
  368. package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
  369. package/skills/fullstack/monorepo/LICENSE +3 -0
  370. package/skills/fullstack/monorepo/SKILL.md +465 -0
  371. package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
  372. package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
  373. package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
  374. package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
  375. package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
  376. package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
  377. package/skills/fullstack/prisma-setup/LICENSE +3 -0
  378. package/skills/fullstack/prisma-setup/SKILL.md +180 -0
  379. package/skills/fullstack/prisma-setup/nextjs.md +258 -0
  380. package/skills/fullstack/prisma-setup/turborepo.md +301 -0
  381. package/skills/fullstack/shadcn/LICENSE +3 -0
  382. package/skills/fullstack/shadcn/SKILL.md +119 -0
  383. package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
  384. package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
  385. package/skills/fullstack/shadcn/cli.md +411 -0
  386. package/skills/fullstack/shadcn/customization.md +224 -0
  387. package/skills/fullstack/shadcn/evals/evals.json +90 -0
  388. package/skills/fullstack/shadcn/mcp.md +101 -0
  389. package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
  390. package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
  391. package/skills/fullstack/shadcn/rules/composition.md +195 -0
  392. package/skills/fullstack/shadcn/rules/data-table.md +201 -0
  393. package/skills/fullstack/shadcn/rules/forms.md +255 -0
  394. package/skills/fullstack/shadcn/rules/icons.md +103 -0
  395. package/skills/fullstack/shadcn/rules/styling.md +167 -0
  396. package/skills/fullstack/zod-v4/LICENSE +3 -0
  397. 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
+ }