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