@1aboveio/skills 0.0.0-stage → 1.0.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 (633) hide show
  1. package/README.md +30 -2
  2. package/bin/cli.mjs +14 -0
  3. package/bin/select-core.mjs +172 -0
  4. package/package.json +146 -4
  5. package/skills/app-dev/airflow/LICENSE +3 -0
  6. package/skills/app-dev/airflow/SKILL.md +199 -0
  7. package/skills/app-dev/airflow/release-check.mjs +93 -0
  8. package/skills/app-dev/android-foundation/LICENSE +3 -0
  9. package/skills/app-dev/android-foundation/SKILL.md +192 -0
  10. package/skills/app-dev/android-foundation/assets/android-ci-job.yml +217 -0
  11. package/skills/app-dev/android-foundation/references/ci.md +87 -0
  12. package/skills/app-dev/android-foundation/references/gotchas.md +221 -0
  13. package/skills/app-dev/android-foundation/references/setup.md +164 -0
  14. package/skills/app-dev/android-foundation/release-check.mjs +93 -0
  15. package/skills/app-dev/android-foundation/scripts/device-run.sh +176 -0
  16. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +172 -0
  17. package/skills/app-dev/app-debug/LICENSE +3 -0
  18. package/skills/app-dev/app-debug/SKILL.md +110 -0
  19. package/skills/app-dev/app-debug/references/common-errors.md +128 -0
  20. package/skills/app-dev/app-debug/release-check.mjs +93 -0
  21. package/skills/app-dev/better-auth/LICENSE +3 -0
  22. package/skills/app-dev/better-auth/SKILL.md +598 -0
  23. package/skills/app-dev/better-auth/references/feishu-api.md +270 -0
  24. package/skills/app-dev/better-auth/release-check.mjs +93 -0
  25. package/skills/app-dev/data-catalog/LICENSE +3 -0
  26. package/skills/app-dev/data-catalog/SKILL.md +204 -0
  27. package/skills/app-dev/data-catalog/agents/openai.yaml +3 -0
  28. package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +75 -0
  29. package/skills/app-dev/data-catalog/references/field-definitions.md +116 -0
  30. package/skills/app-dev/data-catalog/references/format.md +121 -0
  31. package/skills/app-dev/data-catalog/release-check.mjs +93 -0
  32. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +36 -0
  33. package/skills/app-dev/data-catalog/scripts/data_catalog.py +1392 -0
  34. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +117 -0
  35. package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +17 -0
  36. package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +571 -0
  37. package/skills/app-dev/ios-foundation/LICENSE +3 -0
  38. package/skills/app-dev/ios-foundation/SKILL.md +184 -0
  39. package/skills/app-dev/ios-foundation/references/setup.md +243 -0
  40. package/skills/app-dev/ios-foundation/release-check.mjs +93 -0
  41. package/skills/app-dev/monorepo/LICENSE +3 -0
  42. package/skills/app-dev/monorepo/SKILL.md +462 -0
  43. package/skills/app-dev/monorepo/release-check.mjs +93 -0
  44. package/skills/app-dev/nextjs-fullstack/LICENSE +3 -0
  45. package/skills/app-dev/nextjs-fullstack/SKILL.md +211 -0
  46. package/skills/app-dev/nextjs-fullstack/conventions.md +318 -0
  47. package/skills/app-dev/nextjs-fullstack/frontend-conventions.md +61 -0
  48. package/skills/app-dev/nextjs-fullstack/nextjs16.md +287 -0
  49. package/skills/app-dev/nextjs-fullstack/release-check.mjs +93 -0
  50. package/skills/app-dev/nextjs-fullstack/server-actions.md +409 -0
  51. package/skills/app-dev/prisma-setup/LICENSE +3 -0
  52. package/skills/app-dev/prisma-setup/SKILL.md +181 -0
  53. package/skills/app-dev/prisma-setup/nextjs.md +258 -0
  54. package/skills/app-dev/prisma-setup/release-check.mjs +93 -0
  55. package/skills/app-dev/prisma-setup/turborepo.md +301 -0
  56. package/skills/app-dev/pyspark/LICENSE +3 -0
  57. package/skills/app-dev/pyspark/SKILL.md +127 -0
  58. package/skills/app-dev/pyspark/assets/templates/etl.py +748 -0
  59. package/skills/app-dev/pyspark/assets/templates/utils/__init__.py +1 -0
  60. package/skills/app-dev/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
  61. package/skills/app-dev/pyspark/references/diagnosis-and-profiling.md +169 -0
  62. package/skills/app-dev/pyspark/references/etl-contract.md +129 -0
  63. package/skills/app-dev/pyspark/references/production-validation.md +131 -0
  64. package/skills/app-dev/pyspark/references/reconciliation.md +38 -0
  65. package/skills/app-dev/pyspark/references/transformation-design.md +190 -0
  66. package/skills/app-dev/pyspark/references/velocity-feature-calculation.md +193 -0
  67. package/skills/app-dev/pyspark/release-check.mjs +93 -0
  68. package/skills/app-dev/pyspark/scripts/spark_eventlog_summary.py +223 -0
  69. package/skills/app-dev/python-backend/LICENSE +3 -0
  70. package/skills/app-dev/python-backend/SKILL.md +325 -0
  71. package/skills/app-dev/python-backend/assets/uv-install.sh +48 -0
  72. package/skills/app-dev/python-backend/references/uv-installs.md +83 -0
  73. package/skills/app-dev/python-backend/release-check.mjs +93 -0
  74. package/skills/app-dev/shadcn/LICENSE +3 -0
  75. package/skills/app-dev/shadcn/SKILL.md +120 -0
  76. package/skills/app-dev/shadcn/assets/shadcn-small.png +0 -0
  77. package/skills/app-dev/shadcn/assets/shadcn.png +0 -0
  78. package/skills/app-dev/shadcn/cli.md +411 -0
  79. package/skills/app-dev/shadcn/customization.md +224 -0
  80. package/skills/app-dev/shadcn/mcp.md +101 -0
  81. package/skills/app-dev/shadcn/release-check.mjs +93 -0
  82. package/skills/app-dev/shadcn/rules/base-vs-radix.md +323 -0
  83. package/skills/app-dev/shadcn/rules/component-selection.md +67 -0
  84. package/skills/app-dev/shadcn/rules/composition.md +195 -0
  85. package/skills/app-dev/shadcn/rules/data-table.md +201 -0
  86. package/skills/app-dev/shadcn/rules/forms.md +255 -0
  87. package/skills/app-dev/shadcn/rules/icons.md +103 -0
  88. package/skills/app-dev/shadcn/rules/styling.md +167 -0
  89. package/skills/app-dev/zod-v4/LICENSE +3 -0
  90. package/skills/app-dev/zod-v4/SKILL.md +286 -0
  91. package/skills/app-dev/zod-v4/release-check.mjs +93 -0
  92. package/skills/cicd/cloud-build/LICENSE +3 -0
  93. package/skills/cicd/cloud-build/SKILL.md +707 -0
  94. package/skills/cicd/cloud-build/release-check.mjs +93 -0
  95. package/skills/cicd/cloud-debug/LICENSE +3 -0
  96. package/skills/cicd/cloud-debug/SKILL.md +315 -0
  97. package/skills/cicd/cloud-debug/references/build-failures.md +238 -0
  98. package/skills/cicd/cloud-debug/references/deploy-failures.md +376 -0
  99. package/skills/cicd/cloud-debug/references/pipeline-failures.md +378 -0
  100. package/skills/cicd/cloud-debug/release-check.mjs +93 -0
  101. package/skills/cicd/cloud-deploy/LICENSE +3 -0
  102. package/skills/cicd/cloud-deploy/SKILL.md +230 -0
  103. package/skills/cicd/cloud-deploy/references/config-templates.md +257 -0
  104. package/skills/cicd/cloud-deploy/release-check.mjs +93 -0
  105. package/skills/cicd/docker/LICENSE +3 -0
  106. package/skills/cicd/docker/SKILL.md +151 -0
  107. package/skills/cicd/docker/release-check.mjs +93 -0
  108. package/skills/cicd/github-actions/LICENSE +3 -0
  109. package/skills/cicd/github-actions/SKILL.md +111 -0
  110. package/skills/cicd/github-actions/references/audit-profile-optimize.md +150 -0
  111. package/skills/cicd/github-actions/references/ci-case-studies.md +141 -0
  112. package/skills/cicd/github-actions/references/conditional-ci.md +214 -0
  113. package/skills/cicd/github-actions/references/debugging.md +13 -0
  114. package/skills/cicd/github-actions/references/pipeline.md +87 -0
  115. package/skills/cicd/github-actions/references/post-deploy-and-publishing.md +33 -0
  116. package/skills/cicd/github-actions/references/runners.md +253 -0
  117. package/skills/cicd/github-actions/references/security.md +29 -0
  118. package/skills/cicd/github-actions/references/speed-and-audit.md +115 -0
  119. package/skills/cicd/github-actions/references/two-pass-reading.md +92 -0
  120. package/skills/cicd/github-actions/release-check.mjs +93 -0
  121. package/skills/cicd/github-actions/scripts/ci-run-audit-core.mjs +612 -0
  122. package/skills/cicd/github-actions/scripts/ci-run-audit.mjs +4 -0
  123. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +637 -0
  124. package/skills/cicd/github-flow/LICENSE +3 -0
  125. package/skills/cicd/github-flow/SKILL.md +123 -0
  126. package/skills/cicd/github-flow/references/ci-conventions.md +14 -0
  127. package/skills/cicd/github-flow/release-check.mjs +93 -0
  128. package/skills/cicd/github-flow/scripts/flow-core.mjs +105 -0
  129. package/skills/cicd/github-flow/scripts/flow.mjs +81 -0
  130. package/skills/cicd/github-flow/scripts/flow.test.mjs +114 -0
  131. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +117 -0
  132. package/skills/cicd/github-flow/scripts/persist-conventions.mjs +49 -0
  133. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +108 -0
  134. package/skills/cicd/google-cloud/LICENSE +3 -0
  135. package/skills/cicd/google-cloud/SKILL.md +119 -0
  136. package/skills/cicd/google-cloud/references/gcs.md +469 -0
  137. package/skills/cicd/google-cloud/references/iam.md +451 -0
  138. package/skills/cicd/google-cloud/references/project.md +349 -0
  139. package/skills/cicd/google-cloud/references/secrets.md +336 -0
  140. package/skills/cicd/google-cloud/references/vpc.md +312 -0
  141. package/skills/cicd/google-cloud/release-check.mjs +93 -0
  142. package/skills/cicd/google-cloud/scripts/create-sa.sh +36 -0
  143. package/skills/cicd/google-cloud/scripts/gcp-config.sh +31 -0
  144. package/skills/cicd/google-cloud/scripts/grant-iap.sh +41 -0
  145. package/skills/cicd/google-cloud/scripts/setup-secrets.sh +48 -0
  146. package/skills/cicd/mergify/LICENSE +3 -0
  147. package/skills/cicd/mergify/SKILL.md +102 -0
  148. package/skills/cicd/mergify/assets/templates/mergify.yml +215 -0
  149. package/skills/cicd/mergify/assets/templates/ruleset.json +46 -0
  150. package/skills/cicd/mergify/references/branch-protection.md +282 -0
  151. package/skills/cicd/mergify/references/configuration.md +274 -0
  152. package/skills/cicd/mergify/references/diagnosis.md +109 -0
  153. package/skills/cicd/mergify/references/stacks.md +84 -0
  154. package/skills/cicd/mergify/references/traps.md +119 -0
  155. package/skills/cicd/mergify/references/watch-contract.md +274 -0
  156. package/skills/cicd/mergify/release-check.mjs +93 -0
  157. package/skills/cicd/mergify/scripts/audit-core.mjs +208 -0
  158. package/skills/cicd/mergify/scripts/audit.mjs +4 -0
  159. package/skills/cicd/mergify/scripts/audit.test.mjs +201 -0
  160. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +106 -0
  161. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +30 -0
  162. package/skills/cicd/mergify/scripts/stack-push-core.mjs +258 -0
  163. package/skills/cicd/mergify/scripts/stack-push.mjs +4 -0
  164. package/skills/cicd/mergify/scripts/stack-push.test.mjs +233 -0
  165. package/skills/cicd/mergify/scripts/watch-pr-delivery-core.mjs +1065 -0
  166. package/skills/cicd/mergify/scripts/watch-pr-delivery.mjs +4 -0
  167. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +1826 -0
  168. package/skills/cicd/podman/LICENSE +3 -0
  169. package/skills/cicd/podman/SKILL.md +71 -0
  170. package/skills/cicd/podman/agents/openai.yaml +4 -0
  171. package/skills/cicd/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  172. package/skills/cicd/podman/assets/templates/podman-service-override.conf +3 -0
  173. package/skills/cicd/podman/references/compose-compatibility.md +70 -0
  174. package/skills/cicd/podman/references/networking-and-ports.md +74 -0
  175. package/skills/cicd/podman/references/rootless-services-and-sockets.md +156 -0
  176. package/skills/cicd/podman/references/troubleshooting.md +98 -0
  177. package/skills/cicd/podman/release-check.mjs +93 -0
  178. package/skills/content/article-review/LICENSE +3 -0
  179. package/skills/content/article-review/SKILL.md +66 -0
  180. package/skills/content/article-review/references/report-format.md +68 -0
  181. package/skills/content/article-review/references/template-guide.md +79 -0
  182. package/skills/content/article-review/references/template-wechat-payments.md +125 -0
  183. package/skills/content/article-review/release-check.mjs +93 -0
  184. package/skills/content/article-to-image/LICENSE +3 -0
  185. package/skills/content/article-to-image/SKILL.md +91 -0
  186. package/skills/content/article-to-image/references/formats.md +53 -0
  187. package/skills/content/article-to-image/release-check.mjs +93 -0
  188. package/skills/content/flowchart-svg/LICENSE +3 -0
  189. package/skills/content/flowchart-svg/SKILL.md +43 -0
  190. package/skills/content/flowchart-svg/release-check.mjs +93 -0
  191. package/skills/content/flowchart-svg/scripts/export_png.sh +8 -0
  192. package/skills/content/image-gen/LICENSE +3 -0
  193. package/skills/content/image-gen/SKILL.md +192 -0
  194. package/skills/content/image-gen/release-check.mjs +93 -0
  195. package/skills/content/image-gen/scripts/credentials.mjs +217 -0
  196. package/skills/content/image-gen/scripts/credentials.test.mjs +212 -0
  197. package/skills/content/image-gen/scripts/generate.mjs +240 -0
  198. package/skills/content/image-gen/scripts/options.mjs +293 -0
  199. package/skills/content/image-gen/scripts/options.test.mjs +245 -0
  200. package/skills/content/mermaid/LICENSE +3 -0
  201. package/skills/content/mermaid/SKILL.md +635 -0
  202. package/skills/content/mermaid/release-check.mjs +93 -0
  203. package/skills/content/wechat-article-writer/LICENSE +3 -0
  204. package/skills/content/wechat-article-writer/SKILL.md +92 -0
  205. package/skills/content/wechat-article-writer/references/style-selector.md +133 -0
  206. package/skills/content/wechat-article-writer/references/writing-rules.md +152 -0
  207. package/skills/content/wechat-article-writer/release-check.mjs +93 -0
  208. package/skills/content/wechat-article-writer/templates/wechat-comparison.md +52 -0
  209. package/skills/content/wechat-article-writer/templates/wechat-howto.md +51 -0
  210. package/skills/content/wechat-article-writer/templates/wechat-insight.md +50 -0
  211. package/skills/content/wechat-article-writer/templates/wechat-pitfall.md +52 -0
  212. package/skills/content/wechat-publisher/LICENSE +3 -0
  213. package/skills/content/wechat-publisher/SKILL.md +171 -0
  214. package/skills/content/wechat-publisher/references/api.md +105 -0
  215. package/skills/content/wechat-publisher/references/layout-rules.md +57 -0
  216. package/skills/content/wechat-publisher/references/wechat-safe-template.html +86 -0
  217. package/skills/content/wechat-publisher/release-check.mjs +93 -0
  218. package/skills/content/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
  219. package/skills/content/wechat-publisher/scripts/publish.py +239 -0
  220. package/skills/content/wechat-search/LICENSE +3 -0
  221. package/skills/content/wechat-search/SKILL.md +211 -0
  222. package/skills/content/wechat-search/release-check.mjs +93 -0
  223. package/skills/content/wechat-search/scripts/start-chrome.sh +83 -0
  224. package/skills/content/xai-search/LICENSE +3 -0
  225. package/skills/content/xai-search/SKILL.md +79 -0
  226. package/skills/content/xai-search/release-check.mjs +93 -0
  227. package/skills/content/xai-search/scripts/xai_search.sh +187 -0
  228. package/skills/content/xai-search/tests/smoke_test.sh +36 -0
  229. package/skills/core/distill-writing/LICENSE +3 -0
  230. package/skills/core/distill-writing/SKILL.md +74 -0
  231. package/skills/core/distill-writing/references/extraction.md +95 -0
  232. package/skills/core/distill-writing/references/updating.md +46 -0
  233. package/skills/core/distill-writing/release-check.mjs +93 -0
  234. package/skills/core/doc-to-md/LICENSE +3 -0
  235. package/skills/core/doc-to-md/SKILL.md +78 -0
  236. package/skills/core/doc-to-md/release-check.mjs +93 -0
  237. package/skills/core/doc-to-md/scripts/convert.py +157 -0
  238. package/skills/core/editorial-review/LICENSE +3 -0
  239. package/skills/core/editorial-review/SKILL.md +79 -0
  240. package/skills/core/editorial-review/references/checklist.md +94 -0
  241. package/skills/core/editorial-review/release-check.mjs +93 -0
  242. package/skills/core/house-style/LICENSE +3 -0
  243. package/skills/core/house-style/SKILL.md +33 -0
  244. package/skills/core/house-style/references/defaults.md +66 -0
  245. package/skills/core/house-style/references/rules.md +267 -0
  246. package/skills/core/house-style/references/sources.md +27 -0
  247. package/skills/core/house-style/references/style-en.md +131 -0
  248. package/skills/core/house-style/references/style-zh.md +141 -0
  249. package/skills/core/house-style/references/terminology-zh.md +223 -0
  250. package/skills/core/house-style/release-check.mjs +93 -0
  251. package/skills/core/house-style/templates/brd.md +103 -0
  252. package/skills/core/house-style/templates/commercial-proposal.md +93 -0
  253. package/skills/core/house-style/templates/compliance-policy.md +109 -0
  254. package/skills/core/house-style/templates/gap-analysis.md +105 -0
  255. package/skills/core/house-style/templates/general.md +63 -0
  256. package/skills/core/house-style/templates/integration-guide.md +98 -0
  257. package/skills/core/house-style/templates/prd.md +81 -0
  258. package/skills/core/house-style/templates/risk-operations-procedure.md +94 -0
  259. package/skills/core/house-style/templates/technical-spec.md +96 -0
  260. package/skills/core/polish-cn/LICENSE +3 -0
  261. package/skills/core/polish-cn/SKILL.md +102 -0
  262. package/skills/core/polish-cn/references/brief.md +93 -0
  263. package/skills/core/polish-cn/references/models.md +76 -0
  264. package/skills/core/polish-cn/release-check.mjs +93 -0
  265. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +244 -0
  266. package/skills/core/polish-cn/scripts/polish_blocks.py +489 -0
  267. package/skills/core/reg-parser/LICENSE +3 -0
  268. package/skills/core/reg-parser/SKILL.md +212 -0
  269. package/skills/core/reg-parser/references/checklist-schema.md +223 -0
  270. package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
  271. package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
  272. package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
  273. package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
  274. package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
  275. package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
  276. package/skills/core/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
  277. package/skills/core/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
  278. package/skills/core/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
  279. package/skills/core/reg-parser/references/mandates/visa-vars.md +205 -0
  280. package/skills/core/reg-parser/release-check.mjs +93 -0
  281. package/skills/core/reg-parser/scripts/compare_checklist.py +230 -0
  282. package/skills/core/reg-parser/scripts/dump_checklist.py +361 -0
  283. package/skills/core/reg-parser/scripts/preprocess-mandate.sh +187 -0
  284. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +165 -0
  285. package/skills/core/reg-parser/scripts/test_quarantine.sh +105 -0
  286. package/skills/core/reg-parser/scripts/validate_checklist.py +563 -0
  287. package/skills/core/reg-parser/scripts/validate_mandate.py +273 -0
  288. package/skills/core/terminology/LICENSE +3 -0
  289. package/skills/core/terminology/SKILL.md +62 -0
  290. package/skills/core/terminology/references/format.md +50 -0
  291. package/skills/core/terminology/references/init.md +32 -0
  292. package/skills/core/terminology/release-check.mjs +93 -0
  293. package/skills/core/write-doc/LICENSE +3 -0
  294. package/skills/core/write-doc/SKILL.md +141 -0
  295. package/skills/core/write-doc/references/bilingual.md +60 -0
  296. package/skills/core/write-doc/release-check.mjs +93 -0
  297. package/skills/engineering/better-review/LICENSE +3 -0
  298. package/skills/engineering/better-review/SKILL.md +249 -0
  299. package/skills/engineering/better-review/agents/openai.yaml +9 -0
  300. package/skills/engineering/better-review/references/domain-hazards.md +53 -0
  301. package/skills/engineering/better-review/references/passes.md +147 -0
  302. package/skills/engineering/better-review/references/result.md +151 -0
  303. package/skills/engineering/better-review/release-check.mjs +93 -0
  304. package/skills/engineering/better-review/scripts/pr-context.mjs +212 -0
  305. package/skills/engineering/better-review/scripts/pr-context.test.mjs +168 -0
  306. package/skills/engineering/better-review/scripts/rereview-scope.mjs +503 -0
  307. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +587 -0
  308. package/skills/engineering/better-review/scripts/scan-diff.mjs +365 -0
  309. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +414 -0
  310. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +96 -0
  311. package/skills/engineering/e2e-test/LICENSE +3 -0
  312. package/skills/engineering/e2e-test/SKILL.md +98 -0
  313. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +24 -0
  314. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  315. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +73 -0
  316. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +144 -0
  317. package/skills/engineering/e2e-test/references/ci-integration.md +25 -0
  318. package/skills/engineering/e2e-test/references/ci-playwright-container.md +290 -0
  319. package/skills/engineering/e2e-test/references/presentation-sweep.md +104 -0
  320. package/skills/engineering/e2e-test/references/reviewing.md +41 -0
  321. package/skills/engineering/e2e-test/references/route-manifest.md +51 -0
  322. package/skills/engineering/e2e-test/release-check.mjs +93 -0
  323. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  324. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  325. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +106 -0
  326. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  327. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +117 -0
  328. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  329. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +69 -0
  330. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  331. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +98 -0
  332. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +147 -0
  333. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +71 -0
  334. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  335. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +59 -0
  336. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +82 -0
  337. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +76 -0
  338. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  339. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  340. package/skills/engineering/engineering-runtime/SKILL.md +11 -0
  341. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  342. package/skills/engineering/engineering-runtime/coherence/workflow.json +597 -0
  343. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +68 -0
  344. package/skills/engineering/engineering-runtime/release-check.mjs +93 -0
  345. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  346. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  347. package/skills/engineering/engineering-runtime/scripts/invocation-policy.mjs +187 -0
  348. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +95 -0
  349. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +79 -0
  350. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +288 -0
  351. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  352. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  353. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  354. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  355. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +96 -0
  356. package/skills/engineering/engineering-runtime/scripts/review-completion.mjs +56 -0
  357. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +595 -0
  358. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +189 -0
  359. package/skills/engineering/engineering-runtime/scripts/yaml-mapping.mjs +18 -0
  360. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  361. package/skills/engineering/ensure-coverage/SKILL.md +129 -0
  362. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +92 -0
  363. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +85 -0
  364. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  365. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  366. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  367. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +19 -0
  368. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  369. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +57 -0
  370. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +39 -0
  371. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +44 -0
  372. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +110 -0
  373. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +35 -0
  374. package/skills/engineering/ensure-coverage/references/presentation/axis.md +64 -0
  375. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +27 -0
  376. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  377. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +56 -0
  378. package/skills/engineering/ensure-coverage/release-check.mjs +93 -0
  379. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +67 -0
  380. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +140 -0
  381. package/skills/engineering/ensure-coverage/scripts/README.md +99 -0
  382. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +182 -0
  383. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +96 -0
  384. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +105 -0
  385. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +47 -0
  386. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +137 -0
  387. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +86 -0
  388. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +674 -0
  389. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +447 -0
  390. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  391. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +552 -0
  392. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +282 -0
  393. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +289 -0
  394. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  395. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +119 -0
  396. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  397. package/skills/engineering/golden-path/LICENSE +3 -0
  398. package/skills/engineering/golden-path/SKILL.md +230 -0
  399. package/skills/engineering/golden-path/agents/openai.yaml +3 -0
  400. package/skills/engineering/golden-path/assets/golden-paths.example.json +57 -0
  401. package/skills/engineering/golden-path/references/contract.md +239 -0
  402. package/skills/engineering/golden-path/references/migration.md +111 -0
  403. package/skills/engineering/golden-path/release-check.mjs +93 -0
  404. package/skills/engineering/golden-path/scripts/golden-path-core.mjs +1779 -0
  405. package/skills/engineering/golden-path/scripts/golden-path.mjs +6 -0
  406. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +1457 -0
  407. package/skills/engineering/harness-runtime/LICENSE +3 -0
  408. package/skills/engineering/harness-runtime/SKILL.md +19 -0
  409. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  410. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  411. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  412. package/skills/engineering/harness-runtime/contracts.md +15 -0
  413. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  414. package/skills/engineering/harness-runtime/discover-models.test.mjs +363 -0
  415. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  416. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  417. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +967 -0
  418. package/skills/engineering/harness-runtime/index.mjs +767 -0
  419. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +829 -0
  420. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  421. package/skills/engineering/harness-runtime/model-catalog.test.mjs +366 -0
  422. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  423. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +408 -0
  424. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  425. package/skills/engineering/harness-runtime/release-check.mjs +93 -0
  426. package/skills/engineering/resolve-issues/LICENSE +3 -0
  427. package/skills/engineering/resolve-issues/SKILL.md +250 -0
  428. package/skills/engineering/resolve-issues/agents/openai.yaml +9 -0
  429. package/skills/engineering/resolve-issues/plugin-hooks.json +17 -0
  430. package/skills/engineering/resolve-issues/references/canary.md +28 -0
  431. package/skills/engineering/resolve-issues/references/ci.md +105 -0
  432. package/skills/engineering/resolve-issues/references/done.md +25 -0
  433. package/skills/engineering/resolve-issues/references/dropbox.md +90 -0
  434. package/skills/engineering/resolve-issues/references/expire.md +318 -0
  435. package/skills/engineering/resolve-issues/references/exploration.md +202 -0
  436. package/skills/engineering/resolve-issues/references/implementation.md +303 -0
  437. package/skills/engineering/resolve-issues/references/preflight.md +445 -0
  438. package/skills/engineering/resolve-issues/references/review.md +228 -0
  439. package/skills/engineering/resolve-issues/references/shared-modules.md +44 -0
  440. package/skills/engineering/resolve-issues/references/task-plan.md +264 -0
  441. package/skills/engineering/resolve-issues/release-check.mjs +93 -0
  442. package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +364 -0
  443. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +280 -0
  444. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  445. package/skills/engineering/resolve-issues/scripts/dropbox.mjs +473 -0
  446. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +346 -0
  447. package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +98 -0
  448. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +88 -0
  449. package/skills/engineering/resolve-issues/scripts/github-reviews.mjs +384 -0
  450. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +264 -0
  451. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +135 -0
  452. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +79 -0
  453. package/skills/engineering/resolve-issues/scripts/issue-tree.mjs +162 -0
  454. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +107 -0
  455. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +14 -0
  456. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  457. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +149 -0
  458. package/skills/engineering/resolve-issues/scripts/next-actions.mjs +856 -0
  459. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +915 -0
  460. package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +269 -0
  461. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +156 -0
  462. package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +736 -0
  463. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +448 -0
  464. package/skills/engineering/resolve-issues/scripts/ready-check.mjs +185 -0
  465. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +217 -0
  466. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +98 -0
  467. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +91 -0
  468. package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +509 -0
  469. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +1060 -0
  470. package/skills/engineering/resolve-issues/scripts/size-check.mjs +492 -0
  471. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +458 -0
  472. package/skills/engineering/resolve-release/LICENSE +3 -0
  473. package/skills/engineering/resolve-release/SKILL.md +215 -0
  474. package/skills/engineering/resolve-release/agents/openai.yaml +8 -0
  475. package/skills/engineering/resolve-release/flows/cloud-run-canary.md +13 -0
  476. package/skills/engineering/resolve-release/flows/cloud-run.md +7 -0
  477. package/skills/engineering/resolve-release/flows/root-tag-npm.md +13 -0
  478. package/skills/engineering/resolve-release/flows/workspace-npm.md +22 -0
  479. package/skills/engineering/resolve-release/references/canary.md +51 -0
  480. package/skills/engineering/resolve-release/references/candidate.md +41 -0
  481. package/skills/engineering/resolve-release/references/cut.md +131 -0
  482. package/skills/engineering/resolve-release/references/dark-launch.md +66 -0
  483. package/skills/engineering/resolve-release/references/flows.md +150 -0
  484. package/skills/engineering/resolve-release/references/preflight.md +154 -0
  485. package/skills/engineering/resolve-release/references/prepared-metadata.md +260 -0
  486. package/skills/engineering/resolve-release/references/promotion.md +82 -0
  487. package/skills/engineering/resolve-release/references/publish.md +232 -0
  488. package/skills/engineering/resolve-release/references/workspace-npm.md +103 -0
  489. package/skills/engineering/resolve-release/release-check.mjs +93 -0
  490. package/skills/engineering/resolve-release/scripts/apply.mjs +212 -0
  491. package/skills/engineering/resolve-release/scripts/apply.test.mjs +99 -0
  492. package/skills/engineering/resolve-release/scripts/dark-flags.mjs +134 -0
  493. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +100 -0
  494. package/skills/engineering/resolve-release/scripts/flow.mjs +500 -0
  495. package/skills/engineering/resolve-release/scripts/flow.test.mjs +340 -0
  496. package/skills/engineering/resolve-release/scripts/green-head.mjs +231 -0
  497. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +101 -0
  498. package/skills/engineering/resolve-release/scripts/npm-version-absence.mjs +138 -0
  499. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +426 -0
  500. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +659 -0
  501. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +317 -0
  502. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +291 -0
  503. package/skills/engineering/resolve-release/scripts/plan.mjs +806 -0
  504. package/skills/engineering/resolve-release/scripts/plan.test.mjs +428 -0
  505. package/skills/engineering/rush-issues/LICENSE +3 -0
  506. package/skills/engineering/rush-issues/SKILL.md +58 -0
  507. package/skills/engineering/rush-issues/agents/openai.yaml +8 -0
  508. package/skills/engineering/rush-issues/rehydrate.mjs +10 -0
  509. package/skills/engineering/rush-issues/release-check.mjs +93 -0
  510. package/skills/engineering/rush-release/LICENSE +3 -0
  511. package/skills/engineering/rush-release/SKILL.md +35 -0
  512. package/skills/engineering/rush-release/agents/openai.yaml +8 -0
  513. package/skills/engineering/rush-release/release-check.mjs +93 -0
  514. package/skills/engineering/test-quality/LICENSE +3 -0
  515. package/skills/engineering/test-quality/SKILL.md +79 -0
  516. package/skills/engineering/test-quality/references/audit.md +80 -0
  517. package/skills/engineering/test-quality/references/lint.md +24 -0
  518. package/skills/engineering/test-quality/references/principles.md +143 -0
  519. package/skills/engineering/test-quality/references/testing-conventions.md +9 -0
  520. package/skills/engineering/test-quality/release-check.mjs +93 -0
  521. package/skills/engineering/test-quality/scripts/lint-tests.mjs +574 -0
  522. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +591 -0
  523. package/skills/engineering/test-quality/scripts/mock-policy-config.mjs +176 -0
  524. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +119 -0
  525. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +117 -0
  526. package/skills/engineering/test-quality/scripts/persist-conventions.mjs +17 -0
  527. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +104 -0
  528. package/skills/risk-compliance/compliance-audit/LICENSE +3 -0
  529. package/skills/risk-compliance/compliance-audit/SKILL.md +439 -0
  530. package/skills/risk-compliance/compliance-audit/references/findings-schema.md +293 -0
  531. package/skills/risk-compliance/compliance-audit/references/normalization-rules.md +45 -0
  532. package/skills/risk-compliance/compliance-audit/references/vars-checklist.md +1068 -0
  533. package/skills/risk-compliance/compliance-audit/references/vars-findings-schema.md +398 -0
  534. package/skills/risk-compliance/compliance-audit/release-check.mjs +93 -0
  535. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +14 -0
  536. package/skills/risk-compliance/compliance-audit/scripts/export_tracker_xlsx.py +221 -0
  537. package/skills/risk-compliance/compliance-audit/scripts/reconcile_tracker.py +458 -0
  538. package/skills/risk-compliance/compliance-audit/scripts/test_export_tracker_xlsx.py +97 -0
  539. package/skills/risk-compliance/compliance-audit/scripts/test_validate_tracker.py +157 -0
  540. package/skills/risk-compliance/compliance-audit/scripts/validate_tracker.py +829 -0
  541. package/skills/risk-compliance/contract-compare/LICENSE +3 -0
  542. package/skills/risk-compliance/contract-compare/SKILL.md +199 -0
  543. package/skills/risk-compliance/contract-compare/references/comparison-dimensions.md +209 -0
  544. package/skills/risk-compliance/contract-compare/release-check.mjs +93 -0
  545. package/skills/risk-compliance/contract-review/LICENSE +3 -0
  546. package/skills/risk-compliance/contract-review/SKILL.md +240 -0
  547. package/skills/risk-compliance/contract-review/references/review-criteria.md +223 -0
  548. package/skills/risk-compliance/contract-review/release-check.mjs +93 -0
  549. package/skills/risk-compliance/doc-authenticity/LICENSE +3 -0
  550. package/skills/risk-compliance/doc-authenticity/SKILL.md +160 -0
  551. package/skills/risk-compliance/doc-authenticity/agents/openai.yaml +8 -0
  552. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +12 -0
  553. package/skills/risk-compliance/doc-authenticity/references/agreement.md +66 -0
  554. package/skills/risk-compliance/doc-authenticity/references/bank-statement.md +150 -0
  555. package/skills/risk-compliance/doc-authenticity/references/forensic-signals.md +157 -0
  556. package/skills/risk-compliance/doc-authenticity/references/image-signals.md +114 -0
  557. package/skills/risk-compliance/doc-authenticity/references/invoice.md +66 -0
  558. package/skills/risk-compliance/doc-authenticity/references/issuer-profiles.md +86 -0
  559. package/skills/risk-compliance/doc-authenticity/references/verdict-and-report.md +95 -0
  560. package/skills/risk-compliance/doc-authenticity/references/verdict.schema.json +81 -0
  561. package/skills/risk-compliance/doc-authenticity/release-check.mjs +93 -0
  562. package/skills/risk-compliance/doc-authenticity/scripts/image_forensics.py +294 -0
  563. package/skills/risk-compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
  564. package/skills/risk-compliance/doc-authenticity/scripts/validate_codes.py +224 -0
  565. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
  566. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +212 -0
  567. package/skills/risk-compliance/fraud-analysis/LICENSE +3 -0
  568. package/skills/risk-compliance/fraud-analysis/SKILL.md +114 -0
  569. package/skills/risk-compliance/fraud-analysis/references/archetypes/authorized-payment-scam.md +41 -0
  570. package/skills/risk-compliance/fraud-analysis/references/archetypes/first-party-fraud.md +44 -0
  571. package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +27 -0
  572. package/skills/risk-compliance/fraud-analysis/references/contexts/bank-transfer.md +24 -0
  573. package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +30 -0
  574. package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +20 -0
  575. package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +20 -0
  576. package/skills/risk-compliance/fraud-analysis/references/feature-engineering.md +158 -0
  577. package/skills/risk-compliance/fraud-analysis/references/mechanisms/account-takeover.md +36 -0
  578. package/skills/risk-compliance/fraud-analysis/references/report-rationale.md +45 -0
  579. package/skills/risk-compliance/fraud-analysis/references/report-template.md +190 -0
  580. package/skills/risk-compliance/fraud-analysis/references/review-checklist.md +175 -0
  581. package/skills/risk-compliance/fraud-analysis/references/taxonomy.md +79 -0
  582. package/skills/risk-compliance/fraud-analysis/references/terminology.md +108 -0
  583. package/skills/risk-compliance/fraud-analysis/references/workflow.md +175 -0
  584. package/skills/risk-compliance/fraud-analysis/release-check.mjs +93 -0
  585. package/skills/risk-compliance/payment-analysis/LICENSE +3 -0
  586. package/skills/risk-compliance/payment-analysis/SKILL.md +180 -0
  587. package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +30 -0
  588. package/skills/risk-compliance/payment-analysis/references/chargebacks.md +208 -0
  589. package/skills/risk-compliance/payment-analysis/references/chinese-polish.md +51 -0
  590. package/skills/risk-compliance/payment-analysis/references/event-layers.md +127 -0
  591. package/skills/risk-compliance/payment-analysis/references/fx.md +59 -0
  592. package/skills/risk-compliance/payment-analysis/references/journey.md +79 -0
  593. package/skills/risk-compliance/payment-analysis/references/metrics.md +67 -0
  594. package/skills/risk-compliance/payment-analysis/references/report-template.md +119 -0
  595. package/skills/risk-compliance/payment-analysis/references/terminology.md +95 -0
  596. package/skills/risk-compliance/payment-analysis/references/visualization.md +56 -0
  597. package/skills/risk-compliance/payment-analysis/release-check.mjs +93 -0
  598. package/skills/risk-compliance/report-writing/LICENSE +3 -0
  599. package/skills/risk-compliance/report-writing/SKILL.md +220 -0
  600. package/skills/risk-compliance/report-writing/references/audit-report-template.md +121 -0
  601. package/skills/risk-compliance/report-writing/references/external-assessment-template.md +167 -0
  602. package/skills/risk-compliance/report-writing/references/management-report-template.md +145 -0
  603. package/skills/risk-compliance/report-writing/references/vars-report-template.md +224 -0
  604. package/skills/risk-compliance/report-writing/release-check.mjs +93 -0
  605. package/skills/risk-compliance/similar-domain/LICENSE +3 -0
  606. package/skills/risk-compliance/similar-domain/SKILL.md +203 -0
  607. package/skills/risk-compliance/similar-domain/references/entities.md +67 -0
  608. package/skills/risk-compliance/similar-domain/references/fingerprints.md +134 -0
  609. package/skills/risk-compliance/similar-domain/references/pivots.md +82 -0
  610. package/skills/risk-compliance/similar-domain/references/report-template.md +93 -0
  611. package/skills/risk-compliance/similar-domain/references/scoring.md +130 -0
  612. package/skills/risk-compliance/similar-domain/references/terminology.md +41 -0
  613. package/skills/risk-compliance/similar-domain/references/workflow.md +85 -0
  614. package/skills/risk-compliance/similar-domain/release-check.mjs +93 -0
  615. package/skills/risk-compliance/tld-detection/LICENSE +3 -0
  616. package/skills/risk-compliance/tld-detection/SKILL.md +245 -0
  617. package/skills/risk-compliance/tld-detection/agents/openai.yaml +9 -0
  618. package/skills/risk-compliance/tld-detection/references/chinese-polish.md +55 -0
  619. package/skills/risk-compliance/tld-detection/references/clean.md +51 -0
  620. package/skills/risk-compliance/tld-detection/references/clustering.md +235 -0
  621. package/skills/risk-compliance/tld-detection/references/decision.md +189 -0
  622. package/skills/risk-compliance/tld-detection/references/fx-conversion.md +232 -0
  623. package/skills/risk-compliance/tld-detection/references/report-template.md +272 -0
  624. package/skills/risk-compliance/tld-detection/references/streams.md +141 -0
  625. package/skills/risk-compliance/tld-detection/references/terminology.md +95 -0
  626. package/skills/risk-compliance/tld-detection/references/workflow.md +95 -0
  627. package/skills/risk-compliance/tld-detection/release-check.mjs +93 -0
  628. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +390 -0
  629. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +14 -0
  630. package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +650 -0
  631. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +14 -0
  632. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +134 -0
  633. package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +520 -0
@@ -0,0 +1,1065 @@
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
+ if (Array.isArray(pr.failedChecks) && pr.failedChecks.length > 0) {
108
+ return {
109
+ terminal: true,
110
+ outcome: 'STALLED',
111
+ reason: 'admission-checks-failed',
112
+ detail: 'PR admission failed before active Mergify queue membership was observed.',
113
+ failedChecks: pr.failedChecks,
114
+ active,
115
+ seenQueued: seenQueued === true,
116
+ everActive: false,
117
+ preAdmission: true,
118
+ }
119
+ }
120
+ // Seeded seenQueued with explicit everActive=false: still pre-admission for THIS process.
121
+ // Keep seenQueued so phases/callers retain the resume hint, but do not start the
122
+ // lost-membership counter until membership is observed.
123
+ return { terminal: false, outcome: 'NOT_YET_QUEUED', active, seenQueued: seenQueued === true, everActive: false }
124
+ }
125
+
126
+ // The ordered list of lifecycle phases this watch has actually passed through — the
127
+ // progression the contract is about, made visible to the caller.
128
+ //
129
+ // Why it is worth carrying: the terminal outcome alone is ambiguous in exactly the place the
130
+ // contract is subtle. `STALLED` after `["NOT_YET_QUEUED"]` means the PR was NEVER admitted —
131
+ // on a manual-delivery repo that almost always means nobody enqueued it, and the fix is an
132
+ // enqueue, not an investigation. `STALLED` after `["NOT_YET_QUEUED","QUEUED"]` means it was
133
+ // admitted and then wedged INSIDE the queue, where the fix is reading the blocking condition.
134
+ // Same verdict string, opposite diagnosis; `seenQueued` carries the bit but not the shape, and
135
+ // a caller reconstructing the shape from the heartbeat stream has to have kept every record.
136
+ //
137
+ // `WAITING` is deliberately not a phase. It is the re-read suspicion state that sits INSIDE
138
+ // `QUEUED` (membership missing once, not yet settled — see `classifyObservation`), so recording
139
+ // it would make a healthy queued PR oscillate QUEUED/WAITING/QUEUED and turn this into noise
140
+ // rather than a progression. Anything not in `LIFECYCLE_PHASES` leaves the list untouched.
141
+ export const LIFECYCLE_PHASES = new Set(['NOT_YET_QUEUED', 'QUEUED', 'MERGED', 'DEQUEUED', 'CLOSED_UNMERGED', 'STALLED'])
142
+
143
+ export function nextPhases(phases = [], outcome) {
144
+ const list = Array.isArray(phases) ? phases : []
145
+ if (!LIFECYCLE_PHASES.has(outcome)) return list
146
+ if (list[list.length - 1] === outcome) return list
147
+ return [...list, outcome]
148
+ }
149
+
150
+ export function observationKey({ pr = {}, queue = {} } = {}) {
151
+ const labels = Array.isArray(pr.labels) ? [...pr.labels].sort().join(',') : ''
152
+ const checks = [...(pr.failedChecks || []), ...(pr.pendingChecks || [])]
153
+ .map((check) => `${check.name}:${check.status}`)
154
+ .sort()
155
+ .join(',')
156
+ return JSON.stringify([
157
+ pr.state ?? null,
158
+ pr.mergedAt ?? null,
159
+ pr.headRefOid ?? null,
160
+ labels,
161
+ checks,
162
+ queue.queued ?? null,
163
+ queue.queueState ?? queue.state ?? null,
164
+ queue.position ?? null,
165
+ queue.batchPrNumber ?? null,
166
+ ])
167
+ }
168
+
169
+ function stallPrSnapshot(pr = {}) {
170
+ return {
171
+ state: pr.state ?? null,
172
+ mergedAt: pr.mergedAt ?? null,
173
+ headRefOid: pr.headRefOid ?? null,
174
+ labels: Array.isArray(pr.labels) ? [...pr.labels] : [],
175
+ failedChecks: Array.isArray(pr.failedChecks) ? pr.failedChecks.map(({ name, status }) => ({ name, status })) : [],
176
+ pendingChecks: Array.isArray(pr.pendingChecks) ? pr.pendingChecks.map(({ name, status }) => ({ name, status })) : [],
177
+ }
178
+ }
179
+
180
+ function stallQueueSnapshot(queue = {}) {
181
+ return {
182
+ queued: queue.queued ?? null,
183
+ queueState: queue.queueState ?? queue.state ?? null,
184
+ position: queue.position ?? null,
185
+ batchPrNumber: queue.batchPrNumber ?? null,
186
+ }
187
+ }
188
+
189
+ // Retain each source's last readable contribution independently. A failed read carries no
190
+ // replacement state, while a readable sibling can still move the effective stall snapshot.
191
+ export function nextStallObservation({ cache = {}, pr = {}, queue = {} } = {}) {
192
+ const previous = cache && typeof cache === 'object' ? cache : {}
193
+ const prReadable = pr.readable !== false
194
+ const queueReadable = queue.readable !== false
195
+ const nextPr = prReadable ? stallPrSnapshot(pr) : (previous.pr ?? null)
196
+ const nextQueue = queueReadable ? stallQueueSnapshot(queue) : (previous.queue ?? null)
197
+
198
+ if (!prReadable && !queueReadable) {
199
+ return {
200
+ cache: { pr: nextPr, queue: nextQueue, key: previous.key ?? null },
201
+ key: previous.key ?? null,
202
+ changed: false,
203
+ }
204
+ }
205
+
206
+ const key = observationKey({ pr: nextPr ?? {}, queue: nextQueue ?? {} })
207
+ return {
208
+ cache: { pr: nextPr, queue: nextQueue, key },
209
+ key,
210
+ changed: key !== (previous.key ?? null),
211
+ }
212
+ }
213
+
214
+ // A failed subprocess read (`readable: false`) carries no information about queue membership
215
+ // — it is not evidence the PR is still queued, and it is not evidence it left the queue
216
+ // either. Only a clean read that reports absence should move the counter that eventually
217
+ // settles `lost-queue-membership` in `classifyObservation`. Fixes #611 defect 1: two
218
+ // consecutive transient `gh`/`mergify` failures were previously indistinguishable from two
219
+ // consecutive genuine "not queued" reads, and both incremented the same counter.
220
+ export function nextMissingActiveReads({
221
+ active,
222
+ // Prefer everActive when supplied. Fall back to seenQueued for older callers so a
223
+ // genuine post-entry loss still counts; new callers pass everActive=false until
224
+ // membership is observed in-process (closes the --seen-queued pre-admission false DEQUEUE).
225
+ everActive = null,
226
+ seenQueued = false,
227
+ missingActiveReads = 0,
228
+ readable = true,
229
+ } = {}) {
230
+ if (active) return 0
231
+ const mayCount = everActive === null ? seenQueued === true : everActive === true
232
+ if (!mayCount) return missingActiveReads
233
+ return readable ? missingActiveReads + 1 : missingActiveReads
234
+ }
235
+
236
+ export function stallVerdict({ lastChangeMs, nowMs, stallMinutes }) {
237
+ const bound = Number(stallMinutes)
238
+ if (!Number.isFinite(bound) || bound <= 0) return { stalled: false, reason: 'invalid stall bound' }
239
+ const stalledForMs = nowMs - lastChangeMs
240
+ return stalledForMs >= bound * 60_000
241
+ ? { stalled: true, stalledForMs, reason: `no observable Mergify delivery-state change for ${Math.round(stalledForMs / 60_000)}m` }
242
+ : { stalled: false, stalledForMs }
243
+ }
244
+
245
+ // Phase-aware no-change bound. Pre-admission (`everActive` still false) uses the shorter
246
+ // admission stall — queue entry after a real enqueue should appear quickly, and burning the
247
+ // full post-entry bound on a dead admission path is pure wait cost. Once this process has
248
+ // observed membership, use the full stall bound: batch wait / bisect can sit still for longer
249
+ // while still being healthy. Admission is never allowed to exceed the post-entry stall, so a
250
+ // caller that shortens `--stall-minutes` for tests or a suspect PR keeps both phases tight.
251
+ // Before admission, a pending non-Mergify check means CI is running: Mergify admits the PR when
252
+ // it passes, so the wait is progress, not a dead admission path, and gets the full bound
253
+ // (2026-10-16: three enqueued PRs ended admission-stall after 3m while their CI waited on busy
254
+ // runners). A failing check still ends the watch at once (admission-checks-failed).
255
+ export function effectiveStallMinutes({
256
+ everActive = false,
257
+ stallMinutes = DEFAULT_STALL_MINUTES,
258
+ admissionStallMinutes = DEFAULT_ADMISSION_STALL_MINUTES,
259
+ ciPending = false,
260
+ } = {}) {
261
+ const post = Number(stallMinutes)
262
+ if (!Number.isFinite(post) || post <= 0) return { minutes: NaN, phase: 'invalid' }
263
+ if (everActive === true) return { minutes: post, phase: 'queued' }
264
+ if (ciPending === true) return { minutes: post, phase: 'admission-ci' }
265
+ const admission = Number(admissionStallMinutes)
266
+ const pre = Number.isFinite(admission) && admission > 0 ? Math.min(admission, post) : post
267
+ return { minutes: pre, phase: 'admission' }
268
+ }
269
+
270
+ // Fixes #611 defect 2: `execFileSync` had no `timeout`, so a stalled TLS handshake or hung
271
+ // `gh`/`mergify` invocation blocked the watcher past both `--stall-minutes` and
272
+ // `--max-minutes` — those bounds are only evaluated after a poll returns. The bound is a
273
+ // small multiple of the poll interval: generous enough that normal network latency (measured
274
+ // ~4.5s wall for a healthy `gh pr view`) never trips it, but bounded well under the absolute
275
+ // ceiling so a hung child cannot outlive it. Floors at 10s so a very short poll interval
276
+ // cannot turn this into a near-instant timeout — but that floor is itself clamped against
277
+ // `boundMinutes` (the caller passes the effective phase stall bound capped by `--max-minutes`)
278
+ // so a single read's own timeout can never exceed the watch's own stall/ceiling bounds;
279
+ // otherwise a small `--stall-minutes` / `--admission-stall-minutes` on a slow/misconfigured
280
+ // `--interval-seconds` could let one hung read alone outlive the bound it is meant to sit
281
+ // inside of. Pre-admission uses the shorter admission bound; post-entry uses the full stall.
282
+ //
283
+ // Issue #615 item 1 (round-2 NOTE-2 on #612): clamping to the *whole* bound is only correct
284
+ // for a watcher that makes ONE read per poll. This watcher makes TWO (`gh pr view` +
285
+ // `mergify queue show`), and the stall/ceiling bounds are evaluated only after both reads in
286
+ // a poll return — so clamping each read to the full bound let a single poll take up to
287
+ // `2 x min(effectiveStall, max)` before the loop ever checked it, a 2x overrun the reviewer
288
+ // measured live as 58s against a 12s stall bound. Dividing the clamp by `readsPerPoll` keeps
289
+ // the *sum* of a poll's reads inside the bound: this watcher passes 2 (`READS_PER_POLL`
290
+ // below), the sibling (`watch-delivery.mjs`, one read per poll) passes 1 and is unaffected
291
+ // (dividing by 1 is a no-op) — matching `references/watch-contract.md`'s claim, made true
292
+ // rather than aspirational.
293
+ // Shared by `parseArgs` and `readTimeoutMs`'s defensive fallback, so the two can never drift
294
+ // apart and make the "falls back to the documented default" claim quietly false.
295
+ export const DEFAULT_INTERVAL_SECONDS = 90
296
+ // A queue run is one long CI run with no queue-state change until it ends: 20-48 min on
297
+ // the self-hosted fleet (2026-10-10), and the queue sets no checks timeout. A 10-minute
298
+ // bound ended healthy watches STALLED mid-run.
299
+ export const DEFAULT_STALL_MINUTES = 45
300
+ // Pre-admission only: never observed membership this process. After enqueue, queue entry
301
+ // should show up well inside a few minutes; waiting the full post-entry stall on a dead
302
+ // admission path (stale Mergify status, unreadable queue API, forgotten enqueue) is pure cost.
303
+ export const DEFAULT_ADMISSION_STALL_MINUTES = 3
304
+ export const DEFAULT_MAX_MINUTES = 60
305
+ // CI profiling: a single job that runs longer than this is reported as slow. Five minutes is
306
+ // the budget a PR-lane job should fit in; a job over it is where queue wall time goes.
307
+ export const DEFAULT_SLOW_JOB_MINUTES = 5
308
+
309
+ // A hard ceiling on the absolute ceiling, enforced in `runCli` rather than left to the caller's
310
+ // judgement. `--max-minutes` is the backstop that bounds a watch whose queue keeps reporting
311
+ // change — the case `--stall-minutes` deliberately cannot catch, because movement resets it. So
312
+ // it is the only bound standing between "the queue is busy" and a watch that outlives the
313
+ // session that launched it, and a value nobody sanity-checks is exactly how that happens.
314
+ //
315
+ // One hour is chosen against what the bound is actually protecting: a batch takes minutes, so an
316
+ // hour of continuous movement without finishing is already a queue worth handing back to a human
317
+ // rather than waiting out. Handing back at 60m and relaunching is strictly better than a blind
318
+ // 120m wait — the caller gets a `phases`-carrying verdict it can route on, an hour sooner.
319
+ //
320
+ // Over-ceiling values are REFUSED (exit 2), not silently clamped. Clamping would let a caller
321
+ // believe it had a four-hour watch while getting one hour, and the two streams here are already
322
+ // committed: stdout is exactly one terminal JSON verdict, stderr is one NDJSON heartbeat per
323
+ // poll, so there is no channel for a warning that would not break a documented contract. An
324
+ // exit-2 usage error is loud, immediate, and matches how every other bad bound is handled.
325
+ export const MAX_MINUTES_CEILING = 60
326
+
327
+ export function readTimeoutMs(intervalSeconds, boundMinutes, readsPerPoll = 1) {
328
+ const seconds = Number(intervalSeconds)
329
+ const bounded = Number.isFinite(seconds) && seconds > 0 ? seconds : DEFAULT_INTERVAL_SECONDS
330
+ const computed = Math.max(bounded * 2000, 10_000)
331
+ const bound = Number(boundMinutes)
332
+ const reads = Number.isFinite(readsPerPoll) && readsPerPoll > 0 ? readsPerPoll : 1
333
+ return Number.isFinite(bound) && bound > 0 ? Math.min(computed, (bound * 60_000) / reads) : computed
334
+ }
335
+
336
+ // Both subprocess reads this watcher makes per poll — `gh pr view` in `prObservation`,
337
+ // `mergify queue show` in `queueObservation`. See `readTimeoutMs` above.
338
+ const READS_PER_POLL = 2
339
+
340
+ export function run(cmd, args, options = {}) {
341
+ try {
342
+ return { ok: true, stdout: execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...options }) }
343
+ } catch (error) {
344
+ return { ok: false, stdout: error.stdout?.toString?.() || '', stderr: error.stderr?.toString?.() || error.message || '', error }
345
+ }
346
+ }
347
+
348
+ // Issue #615 items 3-4 (round-2 NOTE-1 on #612): `JSON.parse(text || '{}')` treated an
349
+ // empty string as a valid, truthy `{}` object rather than the honest "not JSON" answer —
350
+ // the same manufactured-informative-read shape `queueObservation`'s BLOCK-1 fix closed for
351
+ // the mergify-CLI path. `prObservation` calls this on `result.ok === true` output only, so
352
+ // the live BLOCK-1 defect never reached it (a failed/timed-out `gh` is caught earlier by
353
+ // `if (!result.ok)`), but a `gh` that exits 0 with empty stdout — unreachable by any
354
+ // current caller, never reproduced against a real `gh` — would have manufactured a
355
+ // `readable: true` observation from nothing. Dropping the fallback closes the class at its
356
+ // root for both call sites: `JSON.parse('')` throwing is the honest answer, matching the
357
+ // sibling watcher's raw `JSON.parse` (`watch-delivery.mjs`'s `observe()`), which was already
358
+ // safe by construction for exactly this reason.
359
+ function parseJson(text) {
360
+ try { return JSON.parse(text) } catch { return null }
361
+ }
362
+
363
+ const MERGIFY_CHECKS = new Set(['Mergify Merge Protections', 'Mergify Merge Queue'])
364
+ const FAILED_CHECK_STATES = new Set(['ACTION_REQUIRED', 'CANCELLED', 'ERROR', 'FAILURE', 'STARTUP_FAILURE', 'TIMED_OUT'])
365
+ const PENDING_CHECK_STATES = new Set(['EXPECTED', 'IN_PROGRESS', 'PENDING', 'QUEUED', 'REQUESTED', 'WAITING'])
366
+
367
+ export function summarizeStatusChecks(rollup) {
368
+ const failedChecks = []
369
+ const pendingChecks = []
370
+ for (const check of Array.isArray(rollup) ? rollup : []) {
371
+ const name = check?.name || check?.context || 'unnamed-check'
372
+ if (MERGIFY_CHECKS.has(name)) continue
373
+ const status = String(check?.conclusion || check?.state || check?.status || 'UNKNOWN').toUpperCase()
374
+ const summary = {
375
+ name,
376
+ status,
377
+ detailsUrl: check?.detailsUrl || check?.targetUrl || null,
378
+ }
379
+ if (FAILED_CHECK_STATES.has(status)) failedChecks.push(summary)
380
+ else if (PENDING_CHECK_STATES.has(status)) pendingChecks.push(summary)
381
+ }
382
+ return { failedChecks, pendingChecks }
383
+ }
384
+
385
+ // ---- CI profiling -----------------------------------------------------------------------
386
+ //
387
+ // The watcher already reads `statusCheckRollup` every poll, and each CheckRun in it carries
388
+ // `startedAt`/`completedAt`, so per-job timing costs no extra read. Mergify's own checks are
389
+ // excluded: their "duration" is queue wait, not CI work, and so are jobs that did no work
390
+ // (SKIPPED, NEUTRAL). A job is running only when GitHub says it is not COMPLETED; it is then
391
+ // timed up to `nowMs`, so a slow job is flagged while it is still slow. Timestamps alone do not
392
+ // decide "running": GitHub records a skipped job with `completedAt` a few seconds BEFORE
393
+ // `startedAt` (seen on PR #1483), which a timestamp test read as "still running" and timed
394
+ // for the whole watch. A completed job whose timestamps are out of order counts as 0s.
395
+ // Profiling is observation only: it never changes a lifecycle verdict or an exit code.
396
+ const NO_WORK_CONCLUSIONS = new Set(['SKIPPED', 'NEUTRAL'])
397
+ export function checkTimings(rollup, { source = 'pr', nowMs = Date.now() } = {}) {
398
+ const jobs = []
399
+ for (const check of Array.isArray(rollup) ? rollup : []) {
400
+ const name = check?.name || check?.context || 'unnamed-check'
401
+ if (MERGIFY_CHECKS.has(name)) continue
402
+ if (NO_WORK_CONCLUSIONS.has(String(check?.conclusion || '').toUpperCase())) continue
403
+ const startedMs = Date.parse(check?.startedAt || '')
404
+ if (!Number.isFinite(startedMs) || startedMs <= 0) continue
405
+ const completedMs = Date.parse(check?.completedAt || '')
406
+ const statusText = String(check?.status || '').toUpperCase()
407
+ const running = statusText
408
+ ? statusText !== 'COMPLETED'
409
+ : !Number.isFinite(completedMs) || completedMs <= 0
410
+ jobs.push({
411
+ source,
412
+ name,
413
+ workflow: check?.workflowName || null,
414
+ status: String(check?.conclusion || check?.status || check?.state || 'UNKNOWN').toUpperCase(),
415
+ running,
416
+ startedAt: check.startedAt,
417
+ completedAt: running ? null : check.completedAt,
418
+ durationMs: running ? Math.max(0, nowMs - startedMs) : Math.max(0, (Number.isFinite(completedMs) ? completedMs : startedMs) - startedMs),
419
+ detailsUrl: check?.detailsUrl || check?.targetUrl || null,
420
+ })
421
+ }
422
+ return jobs
423
+ }
424
+
425
+ // One entry per job run (source + name + start), so a rerun of the same job is a separate
426
+ // entry and the latest poll's reading of a running job replaces its earlier one.
427
+ export function mergeCiJobs(previous = {}, jobs = []) {
428
+ const next = { ...(previous && typeof previous === 'object' ? previous : {}) }
429
+ for (const job of Array.isArray(jobs) ? jobs : []) next[`${job.source}|${job.name}|${job.startedAt}`] = job
430
+ return next
431
+ }
432
+
433
+ // Jobs that have crossed the threshold and were not warned about yet. Each job run is warned
434
+ // about once, so a long job does not repeat its warning on every poll.
435
+ export function newSlowJobs(jobsByKey = {}, { thresholdMs, warned = new Set() } = {}) {
436
+ const fresh = []
437
+ for (const [key, job] of Object.entries(jobsByKey || {})) {
438
+ if (job.durationMs > thresholdMs && !warned.has(key)) fresh.push({ key, job })
439
+ }
440
+ return fresh
441
+ }
442
+
443
+ export function slowJobWarning(job, { thresholdMs, nowIso }) {
444
+ return JSON.stringify({
445
+ warning: 'slow-ci-job',
446
+ name: job.name,
447
+ workflow: job.workflow,
448
+ source: job.source,
449
+ running: job.running,
450
+ durationSec: Math.round(job.durationMs / 1000),
451
+ thresholdSec: Math.round(thresholdMs / 1000),
452
+ detailsUrl: job.detailsUrl,
453
+ at: nowIso,
454
+ })
455
+ }
456
+
457
+ // Record the first time each lifecycle phase appeared, so the verdict can say how long the
458
+ // PR waited for admission, sat in the queue, and so on.
459
+ export function nextPhaseTimings(timings = [], phases = [], nowMs = Date.now()) {
460
+ const list = Array.isArray(timings) ? [...timings] : []
461
+ for (const phase of Array.isArray(phases) ? phases.slice(list.length) : []) list.push({ phase, enteredMs: nowMs })
462
+ return list
463
+ }
464
+
465
+ export function ciProfile(jobsByKey = {}, { thresholdMs, phaseTimings = [], startedMs, endMs } = {}) {
466
+ const jobs = Object.values(jobsByKey || {}).sort((a, b) => b.durationMs - a.durationMs)
467
+ const sec = (ms) => Math.round(ms / 1000)
468
+ const view = (job) => ({
469
+ source: job.source,
470
+ name: job.name,
471
+ workflow: job.workflow,
472
+ status: job.status,
473
+ running: job.running,
474
+ durationSec: sec(job.durationMs),
475
+ detailsUrl: job.detailsUrl,
476
+ })
477
+ const slow = jobs.filter((job) => job.durationMs > thresholdMs)
478
+ const phases = (Array.isArray(phaseTimings) ? phaseTimings : []).map((entry, index, all) => {
479
+ const next = all[index + 1]
480
+ const terminal = !next && ['MERGED', 'DEQUEUED', 'CLOSED_UNMERGED', 'STALLED'].includes(entry.phase)
481
+ return {
482
+ phase: entry.phase,
483
+ atSec: Number.isFinite(startedMs) ? sec(entry.enteredMs - startedMs) : null,
484
+ durationSec: terminal ? null : sec((next ? next.enteredMs : endMs) - entry.enteredMs),
485
+ }
486
+ })
487
+ return {
488
+ verdict: jobs.length === 0 ? 'no-timing-data' : (slow.length > 0 ? 'slow' : 'ok'),
489
+ slowJobMinutes: thresholdMs / 60_000,
490
+ jobCount: jobs.length,
491
+ slowestJob: jobs[0] ? view(jobs[0]) : null,
492
+ slowJobs: slow.map(view),
493
+ jobs: jobs.map(view),
494
+ phases,
495
+ }
496
+ }
497
+
498
+ // One compact line naming the outcome and the slow jobs, printed on stderr AFTER the verdict so
499
+ // it is the last line of a combined log. A background runner that reports only an exit code and
500
+ // a short log tail (Pi's bg_task_status / bg_task_log, 10 lines by default) otherwise shows the
501
+ // tail of the pretty-printed verdict — the end of `phases` — and the profile is never read
502
+ // (identity #894, 2026-09-29: a 338s job warned about, never seen). Capped at three named slow
503
+ // jobs so the line stays short; the full list is on the verdict.
504
+ export function ciProfileSummary(result, profile) {
505
+ const shown = (profile?.slowJobs || []).slice(0, 3).map((job) => `${job.name} ${job.durationSec}s`)
506
+ const hidden = (profile?.slowJobs || []).length - shown.length
507
+ return JSON.stringify({
508
+ ciProfileSummary: true,
509
+ outcome: result?.outcome ?? null,
510
+ verdict: profile?.verdict ?? 'no-timing-data',
511
+ jobCount: profile?.jobCount ?? 0,
512
+ slowJobMinutes: profile?.slowJobMinutes ?? null,
513
+ slowest: profile?.slowestJob ? `${profile.slowestJob.name} ${profile.slowestJob.durationSec}s` : null,
514
+ slowJobs: hidden > 0 ? [...shown, `+${hidden} more`] : shown,
515
+ })
516
+ }
517
+
518
+ // Issue #629: the one piece of history a single `labels` snapshot cannot carry — WHEN a
519
+ // `dequeued` label was applied. `gh pr view --json` has no `timelineItems` field in the CLI
520
+ // version this was built against (`gh pr view --json timelineItems` errors "Unknown JSON
521
+ // field"), so this reads the other source the issue names: Mergify's own status comment.
522
+ //
523
+ // BLOCK-1/2 fix (independent review on PR #637): the first version of this function read
524
+ // `mergifyComments[mergifyComments.length - 1]` — "the last mergify-authored comment" — on the
525
+ // premise that it "reflects the most recent cycle". That premise is false: Mergify also
526
+ // authors merge-protection summaries and command acknowledgements from the same account, and
527
+ // those interleave with (and are very often more recent than) the actual queue-status
528
+ // comments. Verified against the REAL payload of PR #620 — the PR issue #629 reproduces on —
529
+ // via `gh pr view --json comments`: of its 4 mergify-authored comments, the one carrying
530
+ // `Left the queue — 2026-07-28 11:04 UTC` is NOT the last one; the last one is a
531
+ // `> queue` / "Command ignored because it is already running" acknowledgement with no payload
532
+ // at all. The shipped helper returned `null` against this exact real data, which made guard 2
533
+ // (the freshness check below) dead code in production: for a GENUINE dequeue the watcher never
534
+ // saw `queued` for, neither this path nor the `missingActiveReads`/`absentStreak` fallback
535
+ // (which requires having observed `queued` first) can fire, so the watch ran to
536
+ // `--max-minutes` instead of settling (reproduced: `reason: 'conditions-unmet',
537
+ // observations: 7`).
538
+ //
539
+ // Fix: every Mergify status comment opens with a machine-readable, HTML-commented payload —
540
+ // `<!--- DO NOT EDIT -*- Mergify Payload -*- {"version":1,"state":"dequeued",...} -*- Mergify
541
+ // Payload End -*- -->` (NOTE-3) — which is exactly the discriminator "is this comment a queue-
542
+ // status comment at all" that the length-based pick was missing. Scan ALL mergify comments,
543
+ // parse each one's payload, and select the LAST one whose `state` is `"dequeued"`.
544
+ //
545
+ // Deliberate choice, because getting it wrong the other way reproduces the same class of bug:
546
+ // this reads for the most recent comment whose STATE HAPPENS TO BE "dequeued" — not simply
547
+ // "the most recent status comment, whatever its state" (on #620 that is the `state:"merged"`
548
+ // comment, and keying off it would read a freshly-MERGED PR as still dequeued). The `dequeued`
549
+ // label lingers on the PR without being cleared across a later re-enqueue, so the label's own
550
+ // claim is "the last attempt that dequeued was THIS one" — the comment that actually made that
551
+ // claim is the one this function must find. If a later attempt is active or merged, the
552
+ // `active`/merged branches in the callers below already win before this function is even
553
+ // consulted for a terminal verdict.
554
+ //
555
+ // The payload has no millisecond-precision exit timestamp of its own (only `queued_at`, the
556
+ // ENTRY time for that attempt) — so the actual "Left the queue" instant still comes from that
557
+ // selected comment's rendered text (kept as NOTE-3's suggested fallback source, not replaced
558
+ // by it). That text is minute-precision with no timezone beyond a literal `UTC` suffix, and
559
+ // it is Mergify's SERVER clock, compared below against this machine's local `Date.now()`
560
+ // (`startedMs`) — a locally-slow clock could in principle make a stale dequeue read as fresh;
561
+ // low risk on an NTP-synced host, called out here rather than silently assumed away (NOTE-2).
562
+ //
563
+ // Parse the selected dequeue comment once and retain all provider evidence used by the
564
+ // terminal verdict. The rendered comment is also the only surviving source after Mergify's
565
+ // queue API drops active membership and its synthetic PR relationship.
566
+ const MERGIFY_PAYLOAD_RE = /-\*- Mergify Payload -\*-\s*\n(\{[\s\S]*?\})\s*\n-\*- Mergify Payload End -\*-/
567
+ const LEFT_QUEUE_RE = /Left the queue\*\*[^`\n]*`(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}) UTC`/g
568
+ const BATCH_PR_RE = /\bon draft #(\d+)\b/i
569
+ const REASON_RE = /^## Reason\s*\n+([^\n]+)/m
570
+ const PROVIDER_CHECK_RE = /^-\s*(❌|🟠|🛑)\s*\[([^\]]+)\]\(([^)]+)\)/gmu
571
+
572
+ export function dequeueInfoFromComments(comments) {
573
+ const list = Array.isArray(comments) ? comments : []
574
+ const mergifyComments = list.filter((c) => c && c.author && c.author.login === 'mergify')
575
+ let target = null
576
+ for (const comment of mergifyComments) {
577
+ const body = typeof comment.body === 'string' ? comment.body : ''
578
+ const payloadMatch = MERGIFY_PAYLOAD_RE.exec(body)
579
+ if (!payloadMatch) continue
580
+ let payload
581
+ try { payload = JSON.parse(payloadMatch[1]) } catch { continue }
582
+ if (payload && payload.state === 'dequeued') target = comment // last match wins — comments are chronological
583
+ }
584
+ if (!target) return { dequeuedAtMs: null, rawReason: null, batchPrNumber: null, providerFailedChecks: [] }
585
+ const body = typeof target.body === 'string' ? target.body : ''
586
+ let match
587
+ let last = null
588
+ LEFT_QUEUE_RE.lastIndex = 0
589
+ while ((match = LEFT_QUEUE_RE.exec(body))) last = match
590
+ const dequeuedAtMs = last
591
+ ? Date.UTC(Number(last[1]), Number(last[2]) - 1, Number(last[3]), Number(last[4]), Number(last[5]))
592
+ : null
593
+ const reasonMatch = REASON_RE.exec(body)
594
+ const batchMatch = BATCH_PR_RE.exec(body)
595
+ const providerFailedChecks = []
596
+ PROVIDER_CHECK_RE.lastIndex = 0
597
+ while ((match = PROVIDER_CHECK_RE.exec(body))) {
598
+ providerFailedChecks.push({
599
+ name: match[2],
600
+ status: match[1] === '🟠' ? 'CANCELLED' : (match[1] === '🛑' ? 'TIMED_OUT' : 'FAILURE'),
601
+ detailsUrl: match[3] || null,
602
+ })
603
+ }
604
+ return {
605
+ dequeuedAtMs,
606
+ rawReason: reasonMatch?.[1]?.trim() || null,
607
+ batchPrNumber: batchMatch ? Number(batchMatch[1]) : null,
608
+ providerFailedChecks,
609
+ }
610
+ }
611
+
612
+ export function dequeuedAtFromComments(comments) {
613
+ return dequeueInfoFromComments(comments).dequeuedAtMs
614
+ }
615
+
616
+ // Exported so tests can pin that the live read timeout uses the same phase-aware bound the
617
+ // stall clock will fire on — a hung pre-admission poll must not outlive admission-stall, and
618
+ // a post-entry poll must not be clamped to the shorter admission bound forever.
619
+ export function readTimeoutForArgs({
620
+ intervalSeconds,
621
+ stallMinutes,
622
+ admissionStallMinutes,
623
+ maxMinutes,
624
+ everActive = false,
625
+ } = {}) {
626
+ const { minutes: phaseBound } = effectiveStallMinutes({
627
+ everActive,
628
+ stallMinutes,
629
+ admissionStallMinutes,
630
+ })
631
+ const bounds = [phaseBound, maxMinutes].filter((n) => Number.isFinite(n) && n > 0)
632
+ return readTimeoutMs(intervalSeconds, bounds.length ? Math.min(...bounds) : undefined, READS_PER_POLL)
633
+ }
634
+
635
+ function prObservation({ repo, pr, intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive = false }) {
636
+ // `comments` costs nothing extra to request in this same call and carries the one signal
637
+ // `dequeuedAtFromComments` needs — no additional subprocess read, so the READS_PER_POLL
638
+ // budget below is unaffected by issue #629's fix.
639
+ const result = run('gh', ['pr', 'view', String(pr), '--repo', repo, '--json', 'state,mergedAt,mergeCommit,headRefOid,labels,url,comments,statusCheckRollup'], { timeout: readTimeoutForArgs({ intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) })
640
+ if (!result.ok) return { readable: false, error: result.stderr || result.stdout }
641
+ const data = parseJson(result.stdout)
642
+ if (!data) return { readable: false, error: 'gh pr view returned invalid JSON' }
643
+ const checks = summarizeStatusChecks(data.statusCheckRollup)
644
+ const dequeueInfo = dequeueInfoFromComments(data.comments)
645
+ return {
646
+ readable: true,
647
+ state: data.state || null,
648
+ mergedAt: data.mergedAt || null,
649
+ mergeCommitOid: data.mergeCommit?.oid || null,
650
+ headRefOid: data.headRefOid || null,
651
+ labels: (data.labels || []).map((label) => label.name).filter(Boolean),
652
+ url: data.url || null,
653
+ dequeuedAtMs: dequeueInfo.dequeuedAtMs,
654
+ dequeueInfo,
655
+ ciJobs: checkTimings(data.statusCheckRollup, { source: 'pr' }),
656
+ ...checks,
657
+ }
658
+ }
659
+
660
+ export function mergifyQueueShowArgs({ pr, repo }) {
661
+ return ['queue', '--repository', repo, 'show', String(pr), '--json']
662
+ }
663
+
664
+ // Exported so the resolve-issues delivery watcher can use THIS reading of queue membership
665
+ // rather than keeping a second copy. That watcher is label-only today, which is why its
666
+ // dequeue resolution is weak: a `queued` label that lags or a `dequeued` label that never
667
+ // lands leaves it with no second opinion, while `mergify queue show --json` is authoritative
668
+ // for membership. Two copies of this inference would drift — and the `queued`-field trap
669
+ // below is exactly the kind of detail one copy would miss.
670
+ export function queueObservation({ pr, repo, intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive = false }) {
671
+ const result = run('mergify', mergifyQueueShowArgs({ pr, repo }), { timeout: readTimeoutForArgs({ intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) })
672
+ // BLOCK-1 fix (independent review on #612): `parseJson` falls back to `'{}'` on empty
673
+ // input, and `{}` is truthy — so a failed or timed-out `mergify` process (empty stdout,
674
+ // its message on stderr) parsed to a truthy `{}` and fell into the "successfully read"
675
+ // branch below, reporting `readable: true, queued: false`. That made `queueObservation`
676
+ // the one place in this file that DID treat an unreadable read as informative, defeating
677
+ // the exact fix `nextMissingActiveReads` exists for: two consecutive `mergify` failures
678
+ // read as two consecutive genuine "not queued" reads and settled a false DEQUEUED, and a
679
+ // timed-out `mergify` (defect 2's fix) fed the same false path, arriving *faster* than the
680
+ // stall bound rather than routing through it. `result.ok` (exit code) AND non-empty stdout
681
+ // are both required before the parsed body is trusted; a non-zero exit or empty stdout is
682
+ // exactly the shape of a failed/timed-out subprocess, not a real answer.
683
+ const stdoutText = typeof result.stdout === 'string' ? result.stdout.trim() : ''
684
+ const parsed = result.ok && stdoutText ? parseJson(stdoutText) : null
685
+ if (parsed) {
686
+ return {
687
+ readable: true,
688
+ queued: parsed.queued === true || Boolean(parsed.queue_rule_name || parsed.queue_rule),
689
+ queueState: parsed.status || parsed.state || parsed.mergeability_check?.state || null,
690
+ position: typeof parsed.position === 'number' ? parsed.position : null,
691
+ batchPrNumber: positivePrNumber(
692
+ parsed.mergeability_check?.queue_pull_request_number
693
+ ?? parsed.queue_pull_request_number
694
+ ?? parsed.speculative_check_pr,
695
+ ),
696
+ raw: parsed,
697
+ }
698
+ }
699
+ const text = `${result.stdout}\n${result.stderr}`.trim()
700
+ if (/not in the merge queue|not queued|not found/i.test(text)) return { readable: true, queued: false, queueState: 'not-queued', position: null, batchPrNumber: null, raw: text }
701
+ return { readable: false, queued: false, queueState: null, position: null, batchPrNumber: null, raw: text || 'mergify queue show failed' }
702
+ }
703
+
704
+ function positivePrNumber(value) {
705
+ const number = Number(value)
706
+ return Number.isInteger(number) && number > 0 ? number : null
707
+ }
708
+
709
+ // The batch relationship is only queried after a terminal dequeue verdict, so normal polls
710
+ // remain the documented two reads. This one-shot enrichment cannot influence lifecycle state.
711
+ function batchPrObservation({ repo, batchPrNumber, intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) {
712
+ const result = run('gh', ['pr', 'view', String(batchPrNumber), '--repo', repo, '--json', 'state,url,statusCheckRollup'], { timeout: readTimeoutForArgs({ intervalSeconds, stallMinutes, admissionStallMinutes, maxMinutes, everActive }) })
713
+ if (!result.ok) return { readable: false, failedChecks: [], error: result.stderr || result.stdout }
714
+ const data = parseJson(result.stdout)
715
+ if (!data) return { readable: false, failedChecks: [], error: 'gh pr view returned invalid JSON for synthetic batch PR' }
716
+ return { readable: true, state: data.state || null, url: data.url || null, ciJobs: checkTimings(data.statusCheckRollup, { source: 'queue-batch' }), ...summarizeStatusChecks(data.statusCheckRollup) }
717
+ }
718
+
719
+ export function classifyDequeue({ rawReason = null, failedChecks = [] } = {}) {
720
+ const statuses = new Set((Array.isArray(failedChecks) ? failedChecks : []).map((check) => String(check?.status || '').toUpperCase()))
721
+ if (['FAILURE', 'ERROR', 'TIMED_OUT', 'ACTION_REQUIRED', 'STARTUP_FAILURE'].some((status) => statuses.has(status))) return 'checks-failed'
722
+ if (statuses.has('CANCELLED')) return 'checks-cancelled'
723
+ const raw = String(rawReason || '').trim().toLowerCase()
724
+ if (/failing checks|checks failed/.test(raw)) return 'checks-failed'
725
+ if (/pull[- ]request.*updated|draft[- ]pull[- ]request.*changed/.test(raw)) return 'pull-request-updated'
726
+ if (/conflict/.test(raw)) return 'conflict'
727
+ if (/conditions[- ]unmet/.test(raw)) return 'conditions-unmet'
728
+ if (/frozen/.test(raw)) return 'frozen'
729
+ if (/manual|manually dequeued|dequeued by .*\bdequeue\b.*command/.test(raw)) return 'manual'
730
+ return 'unknown'
731
+ }
732
+
733
+ // Every poll's observation, as one compact NDJSON record — the watcher's heartbeat.
734
+ //
735
+ // Before this existed the loop's ONLY output sites were the three terminal ones (`MERGED`/
736
+ // `DEQUEUED`/`CLOSED_UNMERGED` below, plus the two `STALLED` paths), so a watch sitting in
737
+ // `NOT_YET_QUEUED` or `QUEUED` — the overwhelmingly common case, and the entire healthy path —
738
+ // wrote zero bytes for its whole life. Reproduced live on PR #1396: 25 minutes of watching a
739
+ // PR that was never queued produced a log containing only the harness's own spawn banner, and
740
+ // the watch was killed 4 minutes short of the then-30-minute stall bound, so it never printed
741
+ // even its terminal verdict. (The bound is 10 now, so that same watch would settle on its own
742
+ // well before a human gave up on it — but the heartbeat is what makes the wait legible while
743
+ // 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
744
+ // exactly the state the caller is trying to resolve, and the hand-written shell watchers this
745
+ // script is meant to replace all printed a per-poll status line. Losing that on the way to a
746
+ // correct state machine traded one failure (wrong answers) for another (no answers).
747
+ //
748
+ // Deliberately stderr, not stdout: stdout is the machine contract — exactly one pretty-printed
749
+ // terminal JSON object, which callers parse whole — and interleaving heartbeats into it would
750
+ // break every such caller. stderr is unparsed by the contract and is merged into the log by
751
+ // every process supervisor that would run this, which is the place a human actually looks.
752
+ // `terminal: false` and the separate stream both distinguish these from the real verdict.
753
+ export function progressRecord({ payload, changed, sinceChangeMs, nowIso }) {
754
+ return JSON.stringify({ ...payload, progress: true, changed, sinceChangeMs, at: nowIso })
755
+ }
756
+
757
+ // Defaults. `stallMinutes` is a NO-CHANGE bound, not an elapsed-time one — any observable
758
+ // movement (queue position, label, queue state) resets it — so 10 minutes of a completely
759
+ // frozen picture is already a long time to wait before handing back, and the earlier 30 (then
760
+ // 15) multiplied every dead watch's cost for no added signal. At a 90s poll, 10 minutes is
761
+ // still ~6 consecutive identical observations before the watch calls it — comfortably more
762
+ // than a transient blip. `intervalSeconds` at 90 also cuts the
763
+ // API budget: two reads per poll is 80/hour per watched PR, against 240/hour at the old 30s,
764
+ // which is what makes an epic-sized fan-out of concurrent watchers fit inside an authenticated
765
+ // token's 5,000/hour limit. The cost is at most 90s of extra merge-detection latency on a
766
+ // queue that takes minutes per batch.
767
+ export function parseArgs(argv) {
768
+ const args = {
769
+ intervalSeconds: DEFAULT_INTERVAL_SECONDS,
770
+ stallMinutes: DEFAULT_STALL_MINUTES,
771
+ admissionStallMinutes: DEFAULT_ADMISSION_STALL_MINUTES,
772
+ maxMinutes: DEFAULT_MAX_MINUTES,
773
+ slowJobMinutes: DEFAULT_SLOW_JOB_MINUTES,
774
+ }
775
+ for (let i = 0; i < argv.length; i += 1) {
776
+ const arg = argv[i]
777
+ if (arg === '--repo') args.repo = argv[++i]
778
+ else if (arg === '--pr') args.pr = argv[++i]
779
+ else if (arg === '--interval-seconds') args.intervalSeconds = Number(argv[++i])
780
+ else if (arg === '--stall-minutes') args.stallMinutes = Number(argv[++i])
781
+ else if (arg === '--admission-stall-minutes') args.admissionStallMinutes = Number(argv[++i])
782
+ else if (arg === '--max-minutes') args.maxMinutes = Number(argv[++i])
783
+ else if (arg === '--slow-job-minutes') args.slowJobMinutes = Number(argv[++i])
784
+ else if (arg === '--seen-queued') args.seenQueued = true
785
+ else if (arg === '--quiet') args.quiet = true
786
+ else if (arg === '--help' || arg === '-h') args.help = true
787
+ else args.unknown = arg
788
+ }
789
+ return args
790
+ }
791
+
792
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
793
+
794
+ function help() {
795
+ return `watch-pr-delivery --repo <owner/name> --pr <number> [options]
796
+
797
+ Observe a Mergify-queued PR until MERGED, DEQUEUED, CLOSED_UNMERGED, or STALLED.
798
+ The script observes only; it never queues, dequeues, merges, or records state.
799
+
800
+ Enqueue first. On a repo with manual delivery (no auto_merge_conditions, autoqueue: false)
801
+ an unqueued PR never enters the queue on its own, so this watch can only ever end STALLED.
802
+ Before admission, a completed failing non-Mergify GitHub check ends the watch as STALLED
803
+ with reason admission-checks-failed; after admission, Mergify owns failure/dequeue routing.
804
+
805
+ Options:
806
+ --interval-seconds <n> Poll interval, default 90
807
+ --stall-minutes <n> Post-entry stall bound (no-change, not elapsed), default 45
808
+ --admission-stall-minutes <n> Pre-admission stall bound (never observed membership, no CI
809
+ check pending), default 3. While a check is pending, the
810
+ post-entry bound applies.
811
+ --max-minutes <n> Absolute safety ceiling, default 60, hard maximum 60
812
+ --slow-job-minutes <n> Warn when one CI job runs longer than this, default 5
813
+ --seen-queued Resume hint only: keep phases/callers aware the PR was queued.
814
+ Does NOT seed lost-membership counting — membership must be
815
+ observed in this process (everActive) before DEQUEUED can fire.
816
+ --quiet Suppress the per-poll progress heartbeat on stderr
817
+
818
+ Output:
819
+ stdout exactly one pretty-printed terminal JSON verdict, at exit
820
+ stderr one compact JSON progress record per poll (progress:true), unless --quiet;
821
+ plus one {"warning":"slow-ci-job"} record per CI job that runs past
822
+ --slow-job-minutes (emitted even with --quiet); and, last of all, one
823
+ {"ciProfileSummary":true} line (outcome, profile verdict, slow jobs) so a
824
+ short log tail shows the profile (emitted even with --quiet)
825
+
826
+ The terminal verdict carries "ciProfile": every non-Mergify CI job's duration (PR checks
827
+ from each poll, plus the queue batch PR's checks read once at the end), the slow ones,
828
+ and how long each lifecycle phase lasted. Profiling never changes the verdict or exit code.
829
+
830
+ Both carry "phases": the lifecycle progression observed so far, in order — e.g.
831
+ ["NOT_YET_QUEUED"] ending STALLED reason admission-stall means never admitted (enqueue it),
832
+ while ["NOT_YET_QUEUED","QUEUED"] ending STALLED reason stall-bound means admitted then
833
+ wedged in the queue.
834
+
835
+ Exit codes:
836
+ 0 MERGED; stdout JSON includes mergedSha, which still needs ancestry proof
837
+ 4 terminal but not merged: DEQUEUED, CLOSED_UNMERGED, or STALLED
838
+ 2 usage error
839
+
840
+ Contract: this watches a progression, not a state. Once queue entry has been observed,
841
+ queued:false is a queue exit unless the PR merged.`
842
+ }
843
+
844
+ export async function runCli(argv) {
845
+ const args = parseArgs(argv)
846
+ if (args.help) {
847
+ console.log(help())
848
+ return 0
849
+ }
850
+ if (args.unknown || !args.repo || !args.pr) {
851
+ console.error(help())
852
+ return 2
853
+ }
854
+ if (!/^\d+$/.test(args.pr)) {
855
+ console.error(`--pr takes the bare PR number, not ${JSON.stringify(args.pr)}`)
856
+ return 2
857
+ }
858
+ if (!/^[\w.-]+\/[\w.-]+$/.test(args.repo)) {
859
+ console.error(`--repo takes owner/name, not ${JSON.stringify(args.repo)}`)
860
+ return 2
861
+ }
862
+ for (const key of ['intervalSeconds', 'stallMinutes', 'admissionStallMinutes', 'maxMinutes', 'slowJobMinutes']) {
863
+ if (!Number.isFinite(args[key]) || args[key] <= 0) {
864
+ console.error(`${key} must be a positive number`)
865
+ return 2
866
+ }
867
+ }
868
+ if (args.maxMinutes > MAX_MINUTES_CEILING) {
869
+ console.error(
870
+ `--max-minutes ${args.maxMinutes} exceeds the ${MAX_MINUTES_CEILING}-minute ceiling. ` +
871
+ 'A watch that outlives its session is worse than one that hands back: relaunch after a ' +
872
+ `${MAX_MINUTES_CEILING}m verdict instead. See MAX_MINUTES_CEILING for why.`,
873
+ )
874
+ return 2
875
+ }
876
+
877
+ const startedMs = Date.now()
878
+ // `--seen-queued` seeds resume intent only. everActive flips only after THIS process
879
+ // observes membership — required before lost-queue-membership can fire.
880
+ let seenQueued = args.seenQueued === true
881
+ let everActive = false
882
+ let missingActiveReads = 0
883
+ let stallObservationCache = null
884
+ let lastChangeMs = startedMs
885
+ let observations = 0
886
+ let phases = []
887
+ let lastBatchPrNumber = null
888
+ let phaseTimings = []
889
+ let ciJobs = {}
890
+ const warnedSlowJobs = new Set()
891
+ const slowThresholdMs = args.slowJobMinutes * 60_000
892
+ // Warn once per slow job run. Warnings go to stderr even under --quiet: they are the point
893
+ // of profiling, and --quiet only silences the routine heartbeat.
894
+ const warnSlowJobs = () => {
895
+ for (const { key, job } of newSlowJobs(ciJobs, { thresholdMs: slowThresholdMs, warned: warnedSlowJobs })) {
896
+ warnedSlowJobs.add(key)
897
+ console.error(slowJobWarning(job, { thresholdMs: slowThresholdMs, nowIso: new Date().toISOString() }))
898
+ }
899
+ }
900
+ // Every terminal path goes through here, so the profile is on every verdict. The queue
901
+ // batch PR (where the merge-queue CI actually runs) is read once, at the end, only if the
902
+ // dequeue path has not already read it — normal polls stay at two reads.
903
+ const finish = (result, { batchRead = null } = {}) => {
904
+ const endMs = Date.now()
905
+ let batch = batchRead
906
+ if (!batch && lastBatchPrNumber) batch = batchPrObservation({ ...args, everActive, batchPrNumber: lastBatchPrNumber })
907
+ if (batch?.readable && Array.isArray(batch.ciJobs)) ciJobs = mergeCiJobs(ciJobs, batch.ciJobs)
908
+ warnSlowJobs()
909
+ const timings = nextPhaseTimings(phaseTimings, result.phases || phases, endMs)
910
+ const profile = ciProfile(ciJobs, { thresholdMs: slowThresholdMs, phaseTimings: timings, startedMs, endMs })
911
+ console.log(JSON.stringify({ ...result, ciProfile: profile }, null, 2))
912
+ console.error(ciProfileSummary(result, profile))
913
+ }
914
+
915
+ for (;;) {
916
+ // Pass everActive into the reads so their per-child timeout tracks the phase bound that
917
+ // will actually fire STALLED this poll. Dropping it left admissionStallMinutes on args
918
+ // unused (destructuring stripped it) and hung pre-admission polls could outlive the 3m
919
+ // admission clock while still being clamped only to the 10m post-entry bound.
920
+ const pr = prObservation({ ...args, everActive })
921
+ const queue = queueObservation({ ...args, everActive })
922
+ observations += 1
923
+ if (pr.readable !== false && Array.isArray(pr.ciJobs)) {
924
+ ciJobs = mergeCiJobs(ciJobs, pr.ciJobs)
925
+ warnSlowJobs()
926
+ }
927
+ if (queue.readable !== false && queue.queued === true && queue.batchPrNumber) {
928
+ lastBatchPrNumber = queue.batchPrNumber
929
+ }
930
+ const active = (Array.isArray(pr.labels) && pr.labels.includes('queued')) || queue.queued === true
931
+ // Only the PR read can show a merge, so an absence counts only when it succeeded: Mergify
932
+ // also reads "not in the merge queue" for a PR that just merged (ai #1762).
933
+ const readable = pr.readable !== false
934
+ if (active) {
935
+ seenQueued = true
936
+ everActive = true
937
+ }
938
+ missingActiveReads = nextMissingActiveReads({ active, everActive, seenQueued, missingActiveReads, readable })
939
+
940
+ const verdict = classifyObservation({
941
+ pr,
942
+ queue,
943
+ seenQueued,
944
+ everActive,
945
+ missingActiveReads,
946
+ startedMs,
947
+ dequeuedAtMs: pr.dequeuedAtMs ?? null,
948
+ })
949
+ if (verdict.everActive === true) everActive = true
950
+ if (verdict.seenQueued === true) seenQueued = true
951
+ if (verdict.preAdmission === true) phases = nextPhases(phases, 'NOT_YET_QUEUED')
952
+ phases = nextPhases(phases, verdict.outcome)
953
+ phaseTimings = nextPhaseTimings(phaseTimings, phases)
954
+ let dequeue = null
955
+ if (verdict.outcome === 'DEQUEUED') {
956
+ const info = pr.dequeueInfo || { rawReason: null, batchPrNumber: null, providerFailedChecks: [] }
957
+ const batchPrNumber = lastBatchPrNumber ?? info.batchPrNumber ?? null
958
+ const batch = batchPrNumber ? batchPrObservation({ ...args, everActive, batchPrNumber }) : null
959
+ const failedChecks = batch?.readable && batch.failedChecks.length > 0
960
+ ? batch.failedChecks
961
+ : (info.providerFailedChecks || [])
962
+ const classifiedReason = classifyDequeue({ rawReason: info.rawReason, failedChecks })
963
+ dequeue = {
964
+ reason: classifiedReason === 'unknown' && !info.rawReason && failedChecks.length === 0
965
+ ? verdict.reason
966
+ : classifiedReason,
967
+ rawReason: info.rawReason || null,
968
+ batchPrNumber,
969
+ failedChecks,
970
+ batch,
971
+ }
972
+ }
973
+ const payload = {
974
+ outcome: verdict.outcome,
975
+ terminal: verdict.terminal,
976
+ reason: dequeue?.reason || verdict.reason || null,
977
+ rawReason: dequeue?.rawReason || null,
978
+ detail: verdict.detail || null,
979
+ mergedSha: verdict.mergedSha || null,
980
+ batchPrNumber: dequeue?.batchPrNumber || lastBatchPrNumber,
981
+ seenQueued,
982
+ everActive,
983
+ phases,
984
+ failedChecks: dequeue?.failedChecks || verdict.failedChecks || pr.failedChecks || [],
985
+ missingActiveReads,
986
+ observations,
987
+ elapsedMs: Date.now() - startedMs,
988
+ pr: {
989
+ state: pr.state || null,
990
+ mergedAt: pr.mergedAt || null,
991
+ headRefOid: pr.headRefOid || null,
992
+ labels: pr.labels || [],
993
+ url: pr.url || null,
994
+ readable: pr.readable !== false,
995
+ dequeuedAtMs: pr.dequeuedAtMs ?? null,
996
+ failedChecks: pr.failedChecks || [],
997
+ pendingChecks: pr.pendingChecks || [],
998
+ },
999
+ queue: {
1000
+ queued: queue.queued === true,
1001
+ queueState: queue.queueState || null,
1002
+ position: queue.position ?? null,
1003
+ batchPrNumber: queue.batchPrNumber || null,
1004
+ lastBatchPrNumber,
1005
+ readable: queue.readable !== false,
1006
+ },
1007
+ batchPr: dequeue?.batch ? {
1008
+ number: dequeue.batchPrNumber,
1009
+ state: dequeue.batch.state || null,
1010
+ url: dequeue.batch.url || null,
1011
+ readable: dequeue.batch.readable !== false,
1012
+ error: dequeue.batch.error || null,
1013
+ } : null,
1014
+ }
1015
+
1016
+ if (verdict.terminal) {
1017
+ finish(payload, { batchRead: dequeue?.batch || null })
1018
+ return verdict.outcome === 'MERGED' ? 0 : 4
1019
+ }
1020
+
1021
+ const stallObservation = nextStallObservation({ cache: stallObservationCache, pr, queue })
1022
+ stallObservationCache = stallObservation.cache
1023
+ const changed = stallObservation.changed
1024
+ if (changed) {
1025
+ lastChangeMs = Date.now()
1026
+ }
1027
+
1028
+ // Heartbeat before the bound checks below, so the last thing in the log before a STALLED
1029
+ // verdict is the unchanging observation that produced it — see `progressRecord`.
1030
+ if (!args.quiet) {
1031
+ const now = Date.now()
1032
+ console.error(progressRecord({ payload, changed, sinceChangeMs: now - lastChangeMs, nowIso: new Date(now).toISOString() }))
1033
+ }
1034
+
1035
+ const elapsedMs = Date.now() - startedMs
1036
+ if (elapsedMs >= args.maxMinutes * 60_000) {
1037
+ finish({ ...payload, terminal: true, outcome: 'STALLED', phases: nextPhases(phases, 'STALLED'), reason: 'max-minutes', detail: `watch exceeded ${args.maxMinutes}m absolute ceiling` })
1038
+ return 4
1039
+ }
1040
+ // Phase-aware bound: pre-admission uses --admission-stall-minutes (default 3); once this
1041
+ // process has observed membership, use the full --stall-minutes (default 45). Outcome
1042
+ // stays STALLED either way — only reason/detail (and wall time) change. Callers already
1043
+ // route on phases for never-admitted vs wedged-in-queue.
1044
+ const bound = effectiveStallMinutes({
1045
+ everActive,
1046
+ stallMinutes: args.stallMinutes,
1047
+ admissionStallMinutes: args.admissionStallMinutes,
1048
+ ciPending: (pr.pendingChecks || []).length > 0,
1049
+ })
1050
+ const stall = stallVerdict({ lastChangeMs, nowMs: Date.now(), stallMinutes: bound.minutes })
1051
+ if (stall.stalled) {
1052
+ // Same theme as `nextMissingActiveReads`, surfaced for a human/caller reading the
1053
+ // terminal payload: a stall reached while the most recent poll was unreadable is not
1054
+ // the same situation as a stall reached on clean, unchanging reads — the sibling
1055
+ // watcher (`watch-delivery.mjs`) already makes this distinction on its own stall path.
1056
+ // Readable pre-admission stalls are `admission-stall` so a dead admission path is not
1057
+ // mistaken for a queue that admitted and then froze.
1058
+ const reason = !readable ? 'unreadable' : (bound.phase === 'admission' ? 'admission-stall' : 'stall-bound')
1059
+ finish({ ...payload, terminal: true, outcome: 'STALLED', phases: nextPhases(phases, 'STALLED'), reason, detail: stall.reason })
1060
+ return 4
1061
+ }
1062
+
1063
+ await sleep(args.intervalSeconds * 1000)
1064
+ }
1065
+ }