pr-shepherd 0.56.6 → 0.57.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 (420) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +44 -27
  3. package/bin/api.d.mts +25 -3
  4. package/bin/api.mjs +51 -13
  5. package/bin/checks/classify.d.mts +1 -0
  6. package/bin/checks/classify.mjs +1 -1
  7. package/bin/checks/conclusions.d.mts +5 -1
  8. package/bin/checks/conclusions.mjs +8 -2
  9. package/bin/checks/superseded.d.mts +8 -2
  10. package/bin/checks/superseded.mjs +24 -7
  11. package/bin/classify/loader.d.mts +2 -0
  12. package/bin/classify/loader.mjs +15 -0
  13. package/bin/classify/standard/author.d.mts +3 -0
  14. package/bin/classify/standard/author.mjs +10 -0
  15. package/bin/classify/standard/codecov-report.d.mts +4 -0
  16. package/bin/classify/standard/codecov-report.mjs +10 -0
  17. package/bin/classify/standard/coderabbit-notices.d.mts +8 -0
  18. package/bin/classify/standard/coderabbit-notices.mjs +38 -0
  19. package/bin/classify/standard/coderabbit-review-summary.d.mts +8 -0
  20. package/bin/classify/standard/coderabbit-review-summary.mjs +37 -0
  21. package/bin/classify/standard/codex-notices.d.mts +7 -0
  22. package/bin/classify/standard/codex-notices.mjs +36 -0
  23. package/bin/classify/standard/codex-review-summary.d.mts +7 -0
  24. package/bin/classify/standard/codex-review-summary.mjs +20 -0
  25. package/bin/classify/standard/dependabot-notices.d.mts +4 -0
  26. package/bin/classify/standard/dependabot-notices.mjs +15 -0
  27. package/bin/classify/standard/gemini-notices.d.mts +4 -0
  28. package/bin/classify/standard/gemini-notices.mjs +13 -0
  29. package/bin/classify/standard/index.d.mts +4 -0
  30. package/bin/classify/standard/index.mjs +22 -0
  31. package/bin/classify/standard/kilo-no-issues.d.mts +4 -0
  32. package/bin/classify/standard/kilo-no-issues.mjs +17 -0
  33. package/bin/classify/standard/sourcery-notices.d.mts +8 -0
  34. package/bin/classify/standard/sourcery-notices.mjs +20 -0
  35. package/bin/cli/api-usage-formatter.mjs +6 -1
  36. package/bin/cli/apply-merge-handler.d.mts +3 -0
  37. package/bin/cli/apply-merge-handler.mjs +73 -0
  38. package/bin/cli/arg-flags.d.mts +2 -0
  39. package/bin/cli/arg-flags.mjs +47 -0
  40. package/bin/cli/args.mjs +10 -41
  41. package/bin/cli/clean-handler.d.mts +1 -0
  42. package/bin/cli/clean-handler.mjs +71 -0
  43. package/bin/cli/default-poll.mjs +3 -0
  44. package/bin/cli/event-state-scope.d.mts +19 -0
  45. package/bin/cli/event-state-scope.mjs +60 -0
  46. package/bin/cli/fix-formatter.mjs +5 -11
  47. package/bin/cli/handlers.d.mts +0 -1
  48. package/bin/cli/handlers.mjs +8 -64
  49. package/bin/cli/help-command-pages.d.mts +62 -18
  50. package/bin/cli/help-command-pages.mjs +59 -15
  51. package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
  52. package/bin/cli/help-iterate-poll-pages.mjs +6 -0
  53. package/bin/cli/help-log-file-page.d.mts +1 -1
  54. package/bin/cli/help-log-file-page.mjs +4 -1
  55. package/bin/cli/help-top-page.d.mts +1 -1
  56. package/bin/cli/help-top-page.mjs +5 -0
  57. package/bin/cli/help.d.mts +63 -19
  58. package/bin/cli/help.mjs +2 -0
  59. package/bin/cli/iterate-branch-segment.d.mts +2 -0
  60. package/bin/cli/iterate-branch-segment.mjs +12 -7
  61. package/bin/cli/iterate-flags.d.mts +7 -1
  62. package/bin/cli/iterate-flags.mjs +38 -0
  63. package/bin/cli/iterate-formatter.d.mts +6 -6
  64. package/bin/cli/iterate-formatter.mjs +56 -22
  65. package/bin/cli/iterate-instructions.mjs +45 -15
  66. package/bin/cli/iterate-lean.mjs +39 -14
  67. package/bin/cli/iterate-merge-formatter.d.mts +6 -0
  68. package/bin/cli/iterate-merge-formatter.mjs +24 -14
  69. package/bin/cli/iterate-verbose.mjs +3 -6
  70. package/bin/cli/list-formatters.mjs +3 -1
  71. package/bin/cli/mutate-formatter.mjs +10 -5
  72. package/bin/cli/next-check-format.d.mts +7 -0
  73. package/bin/cli/next-check-format.mjs +10 -0
  74. package/bin/cli/playbook-handler.d.mts +5 -0
  75. package/bin/cli/playbook-handler.mjs +40 -0
  76. package/bin/cli/poll-handler.mjs +7 -6
  77. package/bin/cli/poll-summary-formatter.mjs +11 -2
  78. package/bin/cli/poll-targets.mjs +2 -0
  79. package/bin/cli/resolve-validators.mjs +2 -2
  80. package/bin/cli/runner.mjs +6 -1
  81. package/bin/cli/stack-layer-view.d.mts +33 -0
  82. package/bin/cli/stack-layer-view.mjs +1 -0
  83. package/bin/cli/stack-overview.d.mts +10 -28
  84. package/bin/cli/stack-overview.mjs +39 -14
  85. package/bin/cli/transport-args.d.mts +5 -0
  86. package/bin/cli/transport-args.mjs +23 -0
  87. package/bin/cli/transport-formatter.d.mts +9 -0
  88. package/bin/cli/transport-formatter.mjs +13 -0
  89. package/bin/cli-parser.mjs +62 -7
  90. package/bin/commands/apply-merge-options.d.mts +4 -0
  91. package/bin/commands/apply-merge-options.mjs +34 -0
  92. package/bin/commands/apply-merge-output.d.mts +7 -0
  93. package/bin/commands/apply-merge-output.mjs +20 -0
  94. package/bin/commands/apply-merge-pull.d.mts +20 -0
  95. package/bin/commands/apply-merge-pull.mjs +15 -0
  96. package/bin/commands/apply-merge.d.mts +18 -0
  97. package/bin/commands/apply-merge.mjs +126 -0
  98. package/bin/commands/apply-queue-removal.mjs +7 -0
  99. package/bin/commands/check-annotations.mjs +28 -18
  100. package/bin/commands/check-execution-context.d.mts +5 -0
  101. package/bin/commands/check-execution-context.mjs +16 -1
  102. package/bin/commands/check-fingerprint.d.mts +26 -2
  103. package/bin/commands/check-fingerprint.mjs +64 -28
  104. package/bin/commands/check-terminal-report.mjs +11 -1
  105. package/bin/commands/check-unreported.d.mts +6 -2
  106. package/bin/commands/check-unreported.mjs +32 -12
  107. package/bin/commands/check-wait-detectors.d.mts +35 -0
  108. package/bin/commands/check-wait-detectors.mjs +107 -0
  109. package/bin/commands/check.d.mts +4 -2
  110. package/bin/commands/check.mjs +130 -40
  111. package/bin/commands/event-instructions.d.mts +11 -0
  112. package/bin/commands/event-instructions.mjs +27 -0
  113. package/bin/commands/event-mode.d.mts +14 -0
  114. package/bin/commands/event-mode.mjs +138 -0
  115. package/bin/commands/iterate/api-usage.mjs +4 -2
  116. package/bin/commands/iterate/base.mjs +4 -0
  117. package/bin/commands/iterate/check-blocker-gate.mjs +14 -2
  118. package/bin/commands/iterate/check-instructions.d.mts +9 -10
  119. package/bin/commands/iterate/check-instructions.mjs +23 -22
  120. package/bin/commands/iterate/classify.mjs +19 -5
  121. package/bin/commands/iterate/escalate.mjs +13 -3
  122. package/bin/commands/iterate/fix-code.mjs +26 -9
  123. package/bin/commands/iterate/fix-instruction-threads.d.mts +0 -8
  124. package/bin/commands/iterate/fix-instruction-threads.mjs +0 -16
  125. package/bin/commands/iterate/fix-loop-instruction.d.mts +9 -0
  126. package/bin/commands/iterate/fix-loop-instruction.mjs +35 -0
  127. package/bin/commands/iterate/helpers.mjs +15 -2
  128. package/bin/commands/iterate/index.mjs +26 -4
  129. package/bin/commands/iterate/mark-ready.mjs +50 -10
  130. package/bin/commands/iterate/merge-plan.d.mts +19 -0
  131. package/bin/commands/iterate/merge-plan.mjs +88 -0
  132. package/bin/commands/iterate/merge-state.mjs +3 -0
  133. package/bin/commands/iterate/merge.d.mts +4 -15
  134. package/bin/commands/iterate/merge.mjs +18 -51
  135. package/bin/commands/iterate/merged-base-pull-requests.d.mts +2 -6
  136. package/bin/commands/iterate/merged-base-pull-requests.mjs +15 -11
  137. package/bin/commands/iterate/native-stack-rebase.mjs +11 -0
  138. package/bin/commands/iterate/render.d.mts +8 -2
  139. package/bin/commands/iterate/render.mjs +28 -42
  140. package/bin/commands/iterate/stack-trunk-conflict.mjs +17 -1
  141. package/bin/commands/iterate/stale-ancestry.mjs +4 -1
  142. package/bin/commands/iterate/stall.d.mts +5 -1
  143. package/bin/commands/iterate/stall.mjs +19 -7
  144. package/bin/commands/iterate/thread-mutation-routing.d.mts +1 -0
  145. package/bin/commands/iterate/thread-mutation-routing.mjs +4 -2
  146. package/bin/commands/iterate/unreported-required.mjs +5 -2
  147. package/bin/commands/mark-files-as-viewed.mjs +7 -0
  148. package/bin/commands/next-check.d.mts +31 -0
  149. package/bin/commands/next-check.mjs +46 -0
  150. package/bin/commands/playbook.d.mts +13 -0
  151. package/bin/commands/playbook.mjs +48 -0
  152. package/bin/commands/poll-mode.d.mts +8 -0
  153. package/bin/commands/poll-mode.mjs +14 -0
  154. package/bin/commands/poll-progress.d.mts +1 -0
  155. package/bin/commands/poll-progress.mjs +6 -3
  156. package/bin/commands/poll-quota.d.mts +3 -2
  157. package/bin/commands/poll-quota.mjs +7 -3
  158. package/bin/commands/poll-summary-explicit-instructions.d.mts +2 -0
  159. package/bin/commands/poll-summary-explicit-instructions.mjs +10 -1
  160. package/bin/commands/poll-summary-instructions.mjs +16 -17
  161. package/bin/commands/poll-summary.mjs +42 -17
  162. package/bin/commands/poll.d.mts +0 -1
  163. package/bin/commands/poll.mjs +11 -19
  164. package/bin/commands/quota-selection.d.mts +2 -1
  165. package/bin/commands/quota-selection.mjs +12 -7
  166. package/bin/commands/resolve-mutate.mjs +33 -5
  167. package/bin/commands/rest-stack-merge-readiness.d.mts +4 -0
  168. package/bin/commands/rest-stack-merge-readiness.mjs +74 -0
  169. package/bin/commands/rule-auto-resolve.mjs +9 -3
  170. package/bin/commands/shepherd-journal.d.mts +5 -4
  171. package/bin/commands/shepherd-journal.mjs +9 -8
  172. package/bin/commands/stack-drain.d.mts +1 -9
  173. package/bin/commands/stack-drain.mjs +50 -37
  174. package/bin/commands/stack-handoffs.d.mts +10 -0
  175. package/bin/commands/stack-handoffs.mjs +25 -0
  176. package/bin/commands/stack-stall.d.mts +5 -0
  177. package/bin/commands/stack-stall.mjs +11 -0
  178. package/bin/commands/stack-work.d.mts +5 -0
  179. package/bin/commands/stack-work.mjs +32 -0
  180. package/bin/comments/existing-reply-scan.d.mts +17 -0
  181. package/bin/comments/existing-reply-scan.mjs +41 -0
  182. package/bin/comments/rate-limit.mjs +2 -2
  183. package/bin/comments/resolve.d.mts +7 -1
  184. package/bin/comments/resolve.mjs +108 -18
  185. package/bin/comments/rest-reply.d.mts +9 -0
  186. package/bin/comments/rest-reply.mjs +95 -0
  187. package/bin/comments/rest-review-mutations.d.mts +22 -0
  188. package/bin/comments/rest-review-mutations.mjs +125 -0
  189. package/bin/comments/review-visibility.d.mts +1 -1
  190. package/bin/comments/review-visibility.mjs +3 -2
  191. package/bin/comments/sha-poll.mjs +7 -3
  192. package/bin/comments/thread-visibility.mjs +10 -5
  193. package/bin/comments/uncertain-replies.d.mts +11 -0
  194. package/bin/comments/uncertain-replies.mjs +121 -0
  195. package/bin/comments/uncertain-reply-guards.d.mts +10 -0
  196. package/bin/comments/uncertain-reply-guards.mjs +94 -0
  197. package/bin/comments/visible-comments.d.mts +1 -1
  198. package/bin/comments/visible-comments.mjs +4 -2
  199. package/bin/config/load.d.mts +37 -1
  200. package/bin/config/load.mjs +73 -1
  201. package/bin/config.json +6 -2
  202. package/bin/exit-codes.d.mts +3 -0
  203. package/bin/exit-codes.mjs +8 -0
  204. package/bin/github/api-telemetry-aggregate.d.mts +15 -4
  205. package/bin/github/api-telemetry-aggregate.mjs +57 -18
  206. package/bin/github/api-telemetry.d.mts +4 -0
  207. package/bin/github/api-telemetry.mjs +22 -8
  208. package/bin/github/apply-review-preflight.d.mts +20 -0
  209. package/bin/github/apply-review-preflight.mjs +61 -0
  210. package/bin/github/base-behind-cache.d.mts +15 -0
  211. package/bin/github/base-behind-cache.mjs +18 -0
  212. package/bin/github/batch-parse-suites.d.mts +4 -2
  213. package/bin/github/batch-parse-suites.mjs +6 -1
  214. package/bin/github/batch-parser-helpers.mjs +40 -2
  215. package/bin/github/batch-parsers.mjs +1 -0
  216. package/bin/github/batch-raw-rules.d.mts +26 -0
  217. package/bin/github/batch-raw-types.d.mts +8 -6
  218. package/bin/github/batch-stack-evidence.d.mts +28 -0
  219. package/bin/github/batch-stack-evidence.mjs +62 -0
  220. package/bin/github/batch.d.mts +15 -2
  221. package/bin/github/batch.mjs +34 -17
  222. package/bin/github/check-annotations-batch.d.mts +0 -5
  223. package/bin/github/check-annotations-batch.mjs +25 -4
  224. package/bin/github/check-annotations.mjs +3 -1
  225. package/bin/github/client.d.mts +8 -19
  226. package/bin/github/client.mjs +45 -13
  227. package/bin/github/fingerprint-fields.d.mts +8 -0
  228. package/bin/github/fingerprint-fields.mjs +6 -1
  229. package/bin/github/fingerprint.d.mts +4 -2
  230. package/bin/github/fingerprint.mjs +6 -38
  231. package/bin/github/github-fetch.d.mts +2 -0
  232. package/bin/github/github-fetch.mjs +19 -0
  233. package/bin/github/gql/base-behind.gql +3 -0
  234. package/bin/github/gql/batch-pr-page.gql +16 -0
  235. package/bin/github/gql/batch-pr.gql +57 -0
  236. package/bin/github/gql/commit-check-contexts.gql +17 -0
  237. package/bin/github/gql/get-pr-body.gql +1 -0
  238. package/bin/github/gql/poll-stack-summary.gql +1 -0
  239. package/bin/github/gql/poll-summary-check-contexts.gql +5 -0
  240. package/bin/github/gql/poll-summary-fragment.gql +65 -15
  241. package/bin/github/gql/stack-handoff-check-runs.gql +42 -0
  242. package/bin/github/graphql-http.mjs +9 -4
  243. package/bin/github/graphql-internal-retry.d.mts +2 -0
  244. package/bin/github/graphql-internal-retry.mjs +1 -1
  245. package/bin/github/graphql-quota-probe.d.mts +6 -0
  246. package/bin/github/graphql-quota-probe.mjs +56 -0
  247. package/bin/github/graphql-session-refusal.d.mts +8 -0
  248. package/bin/github/graphql-session-refusal.mjs +14 -0
  249. package/bin/github/http-request.d.mts +1 -1
  250. package/bin/github/http-request.mjs +22 -6
  251. package/bin/github/http-utils.mjs +6 -0
  252. package/bin/github/merge-target-rules.d.mts +12 -3
  253. package/bin/github/merge-target-rules.mjs +71 -34
  254. package/bin/github/mutation-policy.d.mts +6 -0
  255. package/bin/github/mutation-policy.mjs +21 -0
  256. package/bin/github/poll-summary-annotation-probe.mjs +17 -10
  257. package/bin/github/poll-summary-check-blockers.d.mts +1 -1
  258. package/bin/github/poll-summary-check-blockers.mjs +2 -2
  259. package/bin/github/poll-summary-check-hydration.mjs +25 -10
  260. package/bin/github/poll-summary-checks.d.mts +2 -2
  261. package/bin/github/poll-summary-checks.mjs +9 -6
  262. package/bin/github/poll-summary-command.d.mts +2 -0
  263. package/bin/github/poll-summary-command.mjs +24 -0
  264. package/bin/github/poll-summary-fingerprint.d.mts +1 -1
  265. package/bin/github/poll-summary-fingerprint.mjs +4 -2
  266. package/bin/github/poll-summary-handoff-fields.d.mts +3 -0
  267. package/bin/github/poll-summary-handoff-fields.mjs +73 -0
  268. package/bin/github/poll-summary-projector.d.mts +1 -1
  269. package/bin/github/poll-summary-projector.mjs +17 -24
  270. package/bin/github/poll-summary-raw.d.mts +70 -10
  271. package/bin/github/poll-summary-readiness.mjs +4 -1
  272. package/bin/github/poll-summary-rest-queue.d.mts +3 -0
  273. package/bin/github/poll-summary-rest-queue.mjs +12 -0
  274. package/bin/github/poll-summary-review.d.mts +1 -1
  275. package/bin/github/poll-summary-review.mjs +23 -11
  276. package/bin/github/poll-summary-route.mjs +19 -3
  277. package/bin/github/poll-summary-stack-page.d.mts +11 -0
  278. package/bin/github/poll-summary-stack-page.mjs +27 -0
  279. package/bin/github/poll-summary-unreported.d.mts +3 -1
  280. package/bin/github/poll-summary-unreported.mjs +6 -3
  281. package/bin/github/poll-summary.d.mts +4 -1
  282. package/bin/github/poll-summary.mjs +47 -33
  283. package/bin/github/queries.d.mts +7 -3
  284. package/bin/github/queries.mjs +8 -4
  285. package/bin/github/rate-limit-sample.d.mts +7 -0
  286. package/bin/github/rate-limit-sample.mjs +12 -0
  287. package/bin/github/reply-recovery-read.d.mts +50 -0
  288. package/bin/github/reply-recovery-read.mjs +118 -0
  289. package/bin/github/reply-thread-transcripts.d.mts +0 -1
  290. package/bin/github/reply-thread-transcripts.mjs +24 -1
  291. package/bin/github/rest-annotation-read.d.mts +5 -0
  292. package/bin/github/rest-annotation-read.mjs +44 -0
  293. package/bin/github/rest-batch-read.d.mts +12 -0
  294. package/bin/github/rest-batch-read.mjs +57 -0
  295. package/bin/github/rest-batch-snapshot.d.mts +30 -0
  296. package/bin/github/rest-batch-snapshot.mjs +115 -0
  297. package/bin/github/rest-batch-unavailable.d.mts +5 -0
  298. package/bin/github/rest-batch-unavailable.mjs +16 -0
  299. package/bin/github/rest-check-read.d.mts +34 -0
  300. package/bin/github/rest-check-read.mjs +124 -0
  301. package/bin/github/rest-comment-authorship.d.mts +7 -0
  302. package/bin/github/rest-comment-authorship.mjs +11 -0
  303. package/bin/github/rest-conditional-scope.d.mts +20 -0
  304. package/bin/github/rest-conditional-scope.mjs +44 -0
  305. package/bin/github/rest-explicit-summary-read.d.mts +3 -0
  306. package/bin/github/rest-explicit-summary-read.mjs +8 -0
  307. package/bin/github/rest-feedback-read.d.mts +36 -0
  308. package/bin/github/rest-feedback-read.mjs +147 -0
  309. package/bin/github/rest-feedback-validation.d.mts +5 -0
  310. package/bin/github/rest-feedback-validation.mjs +28 -0
  311. package/bin/github/rest-http.d.mts +12 -0
  312. package/bin/github/rest-http.mjs +108 -23
  313. package/bin/github/rest-identities.d.mts +22 -0
  314. package/bin/github/rest-identities.mjs +134 -0
  315. package/bin/github/rest-merge-stack-guard.d.mts +14 -0
  316. package/bin/github/rest-merge-stack-guard.mjs +41 -0
  317. package/bin/github/rest-merge.d.mts +29 -0
  318. package/bin/github/rest-merge.mjs +56 -0
  319. package/bin/github/rest-merged-base-read.d.mts +9 -0
  320. package/bin/github/rest-merged-base-read.mjs +48 -0
  321. package/bin/github/rest-pr-core.d.mts +52 -0
  322. package/bin/github/rest-pr-core.mjs +60 -0
  323. package/bin/github/rest-reader-core.d.mts +16 -0
  324. package/bin/github/rest-reader-core.mjs +154 -0
  325. package/bin/github/rest-review-read.d.mts +11 -0
  326. package/bin/github/rest-review-read.mjs +47 -0
  327. package/bin/github/rest-rules-read.d.mts +9 -0
  328. package/bin/github/rest-rules-read.mjs +154 -0
  329. package/bin/github/rest-session-refusal.d.mts +5 -0
  330. package/bin/github/rest-session-refusal.mjs +21 -0
  331. package/bin/github/rest-snapshot-context.d.mts +10 -0
  332. package/bin/github/rest-snapshot-context.mjs +1 -0
  333. package/bin/github/rest-stack-read.d.mts +21 -0
  334. package/bin/github/rest-stack-read.mjs +47 -0
  335. package/bin/github/rest-stack-summary.d.mts +8 -0
  336. package/bin/github/rest-stack-summary.mjs +62 -0
  337. package/bin/github/rest-summary-read.d.mts +7 -0
  338. package/bin/github/rest-summary-read.mjs +112 -0
  339. package/bin/github/rest-terminal-batch.d.mts +14 -0
  340. package/bin/github/rest-terminal-batch.mjs +45 -0
  341. package/bin/github/rest-text.mjs +7 -3
  342. package/bin/github/rest-thread-read.d.mts +4 -0
  343. package/bin/github/rest-thread-read.mjs +35 -0
  344. package/bin/github/rest-viewer-read.d.mts +2 -0
  345. package/bin/github/rest-viewer-read.mjs +41 -0
  346. package/bin/github/stack-bottom.d.mts +3 -0
  347. package/bin/github/stack-bottom.mjs +14 -0
  348. package/bin/github/stack-handoff-context.d.mts +25 -0
  349. package/bin/github/stack-handoff-context.mjs +12 -0
  350. package/bin/github/stack-handoff-page.d.mts +17 -0
  351. package/bin/github/stack-handoff-page.mjs +306 -0
  352. package/bin/github/stack-handoff.d.mts +16 -0
  353. package/bin/github/stack-handoff.mjs +115 -0
  354. package/bin/github/stack-order.d.mts +6 -0
  355. package/bin/github/stack-order.mjs +13 -0
  356. package/bin/github/stack-read.d.mts +7 -5
  357. package/bin/github/stack-read.mjs +22 -24
  358. package/bin/github/stack-size-hint.d.mts +3 -0
  359. package/bin/github/stack-size-hint.mjs +14 -0
  360. package/bin/github/suggestion-thread.mjs +27 -1
  361. package/bin/github/thread-comments.mjs +9 -1
  362. package/bin/github/transport-evidence.d.mts +20 -0
  363. package/bin/github/transport-evidence.mjs +26 -0
  364. package/bin/github/transport-mode.d.mts +2 -0
  365. package/bin/github/transport-mode.mjs +5 -0
  366. package/bin/github/transport.d.mts +19 -0
  367. package/bin/github/transport.mjs +126 -0
  368. package/bin/github/unsupported-rest.d.mts +4 -0
  369. package/bin/github/unsupported-rest.mjs +7 -0
  370. package/bin/mcp/server.mjs +52 -3
  371. package/bin/merge-status/derive.d.mts +2 -0
  372. package/bin/merge-status/derive.mjs +17 -13
  373. package/bin/merge-status/required-check-requirements.d.mts +7 -0
  374. package/bin/merge-status/required-check-requirements.mjs +15 -0
  375. package/bin/merge-status/requirements-format.d.mts +5 -0
  376. package/bin/merge-status/requirements-format.mjs +41 -12
  377. package/bin/merge-status/requirements.mjs +18 -7
  378. package/bin/state/base.mjs +41 -0
  379. package/bin/state/durable-state.d.mts +6 -0
  380. package/bin/state/durable-state.mjs +41 -0
  381. package/bin/state/graphql-quota-policy.mjs +23 -16
  382. package/bin/state/merge-request.d.mts +25 -0
  383. package/bin/state/merge-request.mjs +145 -0
  384. package/bin/state/pr-fingerprint.d.mts +15 -0
  385. package/bin/state/pr-fingerprint.mjs +22 -7
  386. package/bin/state/rest-cache.d.mts +3 -0
  387. package/bin/state/rest-cache.mjs +1 -0
  388. package/bin/state/rest-snapshot-report.d.mts +21 -0
  389. package/bin/state/rest-snapshot-report.mjs +52 -0
  390. package/bin/state/seen-comments.d.mts +3 -0
  391. package/bin/state/seen-comments.mjs +18 -0
  392. package/bin/types/api-usage.d.mts +9 -1
  393. package/bin/types/escalate.d.mts +1 -1
  394. package/bin/types/github.d.mts +23 -9
  395. package/bin/types/iterate.d.mts +24 -2
  396. package/bin/types/merge-action.d.mts +1 -1
  397. package/bin/types/merge-requirements.d.mts +7 -7
  398. package/bin/types/next-check.d.mts +12 -0
  399. package/bin/types/next-check.mjs +1 -0
  400. package/bin/types/poll-summary.d.mts +21 -0
  401. package/bin/types/report.d.mts +13 -1
  402. package/bin/types/review-thread.d.mts +1 -1
  403. package/bin/types.d.mts +1 -0
  404. package/bin/types.mjs +1 -0
  405. package/package.json +3 -2
  406. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  407. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  408. package/plugins/pr-shepherd/.mcp.json +1 -1
  409. package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +1 -1
  410. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +14 -51
  411. package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +12 -16
  412. package/plugins/pr-shepherd/skills/pr-shepherd/references/cloud-event-loop.md +14 -0
  413. package/plugins/pr-shepherd/skills/pr-shepherd/references/create-pr.md +10 -0
  414. package/plugins/pr-shepherd/skills/pr-shepherd/references/fix-code-loop.md +10 -0
  415. package/plugins/pr-shepherd/skills/pr-shepherd/references/mcp-fallback.md +9 -0
  416. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +2 -0
  417. package/bin/github/gql/pr-fingerprint.gql +0 -75
  418. package/plugins/pr-shepherd/skills/pr-shepherd/references/journal.md +0 -7
  419. package/plugins/pr-shepherd/skills/pr-shepherd/references/review-mutations.md +0 -9
  420. package/plugins/pr-shepherd/skills/pr-shepherd/references/stack-merge.md +0 -7
@@ -0,0 +1,56 @@
1
+ import { restWithRateLimit } from "./rest-http.mjs";
2
+ import { restRepoPath } from "./rest-reader-core.mjs";
3
+ import { GitHubRequestError } from "./errors.mjs";
4
+ import { sanitizeBody } from "./http-utils.mjs";
5
+ function parseRestMergeResponse(raw) {
6
+ if (raw === null || typeof raw !== "object")
7
+ throw new Error("Invalid asynchronous merge response");
8
+ const value = raw;
9
+ if (!["pending", "enqueued", "merged", "failed"].includes(String(value["status"])) ||
10
+ value["details"] === null ||
11
+ typeof value["details"] !== "object" ||
12
+ Array.isArray(value["details"])) {
13
+ throw new Error("Invalid asynchronous merge response");
14
+ }
15
+ return value;
16
+ }
17
+ /** This named mutation is never retried after a network failure or server error. */
18
+ export async function requestRestMerge(repo, pr, options) {
19
+ const { data, status, rateLimit } = await restWithRateLimit("PUT", `${restRepoPath(repo)}/pulls/${pr}/merge-async`, {
20
+ sha: options.requireSha,
21
+ merge_action: options.mergeAction,
22
+ bypass_rules: false,
23
+ ...(options.mergeAction === "direct_merge" && options.mergeMethod
24
+ ? { merge_method: options.mergeMethod }
25
+ : {}),
26
+ }, { acceptStatuses: [400, 409] });
27
+ try {
28
+ return parseRestMergeResponse(data);
29
+ }
30
+ catch (error) {
31
+ // Accepted 400/409 responses may contain an ordinary API error envelope,
32
+ // rather than merge state. Retain their definite refusal for intent recovery.
33
+ if (status !== undefined && status >= 400 && status < 500) {
34
+ const responseMessage = sanitizeBody(JSON.stringify(data) ?? "");
35
+ throw new GitHubRequestError(`Invalid asynchronous merge response: ${status} ${responseMessage}`, { status, rateLimit, responseMessage });
36
+ }
37
+ // A malformed acknowledgement may follow a successful submission.
38
+ throw error;
39
+ }
40
+ }
41
+ export async function getRestMerge(repo, pr, uuid) {
42
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid))
43
+ throw new Error("Invalid asynchronous merge UUID");
44
+ const { data } = await restWithRateLimit("GET", `${restRepoPath(repo)}/pulls/${pr}/merge-async/${uuid}`);
45
+ return parseRestMergeResponse(data);
46
+ }
47
+ /** Conflict requests must match the caller's guarded options before their UUID can be adopted. */
48
+ export function matchesRestMergeRequest(result, options) {
49
+ const details = result.details;
50
+ return (details.expected_head_sha === options.requireSha &&
51
+ details.merge_action === options.mergeAction &&
52
+ details.bypass_rules === false &&
53
+ (options.mergeMethod === undefined || details.merge_method === options.mergeMethod) &&
54
+ typeof details.uuid === "string" &&
55
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(details.uuid));
56
+ }
@@ -0,0 +1,9 @@
1
+ import type { MergedBasePullRequest } from "../types.mts";
2
+ export interface MergedBaseLookupInput {
3
+ owner: string;
4
+ repo: string;
5
+ baseRefName: string;
6
+ baseRefOid: string;
7
+ }
8
+ /** A reused branch name or a fork is insufficient evidence of a merged parent. */
9
+ export declare function readRestMergedBasePullRequests(input: MergedBaseLookupInput): Promise<MergedBasePullRequest[]>;
@@ -0,0 +1,48 @@
1
+ import { readRestPages, restRepoPath, restObject, restString, restNumber, malformedRest, } from "./rest-reader-core.mjs";
2
+ /** A reused branch name or a fork is insufficient evidence of a merged parent. */
3
+ export async function readRestMergedBasePullRequests(input) {
4
+ const repo = { owner: input.owner, name: input.repo };
5
+ const head = encodeURIComponent(`${input.owner}:${input.baseRefName}`);
6
+ const pulls = await readRestPages(`${restRepoPath(repo)}/pulls?state=closed&head=${head}&sort=updated&direction=desc`);
7
+ const candidates = [];
8
+ const ids = new Set();
9
+ for (const value of pulls.nodes) {
10
+ const pull = restObject(value, "closed pull request");
11
+ const number = restNumber(pull.number, "closed pull number");
12
+ if (number === 0 || ids.has(number))
13
+ malformedRest("closed pull number missing or repeated");
14
+ ids.add(number);
15
+ const state = restString(pull.state, "closed pull state");
16
+ if (!["open", "closed"].includes(state))
17
+ malformedRest("closed pull state");
18
+ if (pull.merged_at !== null && typeof pull.merged_at !== "string")
19
+ malformedRest("closed pull merged_at");
20
+ if (state !== "closed" || pull.merged_at === null)
21
+ continue;
22
+ if (!Number.isFinite(Date.parse(pull.merged_at)))
23
+ malformedRest("closed pull merged_at");
24
+ const source = restObject(pull.head, "closed pull head");
25
+ const headRefName = restString(source.ref, "closed pull head ref");
26
+ const headRefOid = restString(source.sha, "closed pull head SHA");
27
+ if (headRefName !== input.baseRefName || headRefOid !== input.baseRefOid)
28
+ continue;
29
+ if (source.repo === null)
30
+ continue;
31
+ const headRepository = restObject(source.repo, "closed pull head repository");
32
+ const nameWithOwner = restString(headRepository.full_name, "closed pull head repository name");
33
+ if (nameWithOwner.toLowerCase() !== `${input.owner}/${input.repo}`.toLowerCase())
34
+ continue;
35
+ const base = restObject(pull.base, "closed pull base");
36
+ candidates.push({
37
+ number,
38
+ url: restString(pull.html_url, "closed pull URL"),
39
+ state: "MERGED",
40
+ headRefName,
41
+ headRefOid,
42
+ baseRefName: restString(base.ref, "closed pull base ref"),
43
+ mergedAt: pull.merged_at,
44
+ headRepository: { nameWithOwner },
45
+ });
46
+ }
47
+ return candidates;
48
+ }
@@ -0,0 +1,52 @@
1
+ import type { RepoInfo } from "./client.mts";
2
+ export interface RestPull {
3
+ id: number;
4
+ node_id: string;
5
+ number: number;
6
+ title: string;
7
+ body: string | null;
8
+ html_url: string;
9
+ state: string;
10
+ merged_at: string | null;
11
+ draft: boolean;
12
+ mergeable: boolean | null;
13
+ mergeable_state: string;
14
+ updated_at: string;
15
+ user: {
16
+ login: string;
17
+ type?: string;
18
+ } | null;
19
+ head: {
20
+ ref: string;
21
+ sha: string;
22
+ repo: {
23
+ full_name: string;
24
+ } | null;
25
+ };
26
+ base: {
27
+ ref: string;
28
+ sha: string;
29
+ };
30
+ requested_reviewers: Array<{
31
+ login: string;
32
+ }>;
33
+ requested_teams: Array<{
34
+ name: string;
35
+ }>;
36
+ auto_merge?: {
37
+ enabled_by?: {
38
+ login: string;
39
+ };
40
+ merge_method: string;
41
+ } | null;
42
+ }
43
+ export declare function readRestPull(pr: number, repo: RepoInfo): Promise<RestPull>;
44
+ export declare function restPullRefs(pull: RestPull): {
45
+ number: number;
46
+ state: string;
47
+ headRefName: string;
48
+ headRefOid: string;
49
+ baseRefName: string;
50
+ baseRefOid: string;
51
+ };
52
+ export declare function restPullRevision(pull: RestPull): string;
@@ -0,0 +1,60 @@
1
+ import { readRest as rest } from "./rest-reader-core.mjs";
2
+ import { restRepoPath, restObject, restString, restNumber, restBoolean, restArray, malformedRest, } from "./rest-reader-core.mjs";
3
+ import { recordRestIdentity } from "./rest-identities.mjs";
4
+ export async function readRestPull(pr, repo) {
5
+ const value = await rest("GET", `${restRepoPath(repo)}/pulls/${pr}`);
6
+ const data = restObject(value, "pull request");
7
+ const head = restObject(data.head, "pull request head");
8
+ const base = restObject(data.base, "pull request base");
9
+ restNumber(data.id, "pull request id");
10
+ restString(data.node_id, "pull request node_id");
11
+ if (restNumber(data.number, "pull request number") !== pr)
12
+ throw new Error("REST pull request identity changed");
13
+ restString(data.title, "pull request title");
14
+ restString(data.html_url, "pull request html_url");
15
+ if (data.body !== null && typeof data.body !== "string")
16
+ malformedRest("pull request body");
17
+ if (data.merged_at !== null && typeof data.merged_at !== "string")
18
+ malformedRest("pull request merged_at");
19
+ restArray(data.requested_reviewers, "requested reviewers");
20
+ restArray(data.requested_teams, "requested teams");
21
+ restString(data.state, "pull request state");
22
+ restBoolean(data.draft, "pull request draft");
23
+ restString(head.sha, "pull request head SHA");
24
+ restString(head.ref, "pull request head ref");
25
+ restString(base.sha, "pull request base SHA");
26
+ restString(base.ref, "pull request base ref");
27
+ restString(data.updated_at, "pull request updated_at");
28
+ if (!Number.isFinite(Date.parse(data.updated_at)))
29
+ malformedRest("pull request revision timestamp");
30
+ restString(data.mergeable_state, "pull request mergeable_state");
31
+ if (data.mergeable !== null)
32
+ restBoolean(data.mergeable, "pull request mergeable");
33
+ await recordRestIdentity(repo, pr, data.node_id, String(data.id), "pull");
34
+ return data;
35
+ }
36
+ export function restPullRefs(pull) {
37
+ return {
38
+ number: pull.number,
39
+ state: pull.merged_at ? "MERGED" : pull.state.toUpperCase(),
40
+ headRefName: pull.head.ref,
41
+ headRefOid: pull.head.sha,
42
+ baseRefName: pull.base.ref,
43
+ baseRefOid: pull.base.sha,
44
+ };
45
+ }
46
+ export function restPullRevision(pull) {
47
+ return JSON.stringify([
48
+ pull.head.ref,
49
+ pull.head.sha,
50
+ pull.base.ref,
51
+ pull.base.sha,
52
+ pull.updated_at,
53
+ pull.state,
54
+ pull.draft,
55
+ pull.merged_at,
56
+ pull.mergeable,
57
+ pull.mergeable_state,
58
+ pull.auto_merge ?? null,
59
+ ]);
60
+ }
@@ -0,0 +1,16 @@
1
+ import type { RepoInfo, RateLimitInfo } from "./client.mts";
2
+ export declare function restRepoPath(repo: RepoInfo): string;
3
+ export declare function malformedRest(detail: string): never;
4
+ export declare function restObject(value: unknown, label: string): Record<string, unknown>;
5
+ export declare function restString(value: unknown, label: string): string;
6
+ export declare function restNumber(value: unknown, label: string): number;
7
+ export declare function restBoolean(value: unknown, label: string): boolean;
8
+ export declare function restArray(value: unknown, label: string): unknown[];
9
+ /** Counted REST envelopes must include the count as well as their list. */
10
+ export declare function restCollection(value: unknown, key: string): unknown[];
11
+ /** Complete REST connections; follow GitHub's Link, never infer completeness from a short page. */
12
+ export declare function readRestPages<T>(path: string, select?: (body: unknown) => T[]): Promise<{
13
+ nodes: T[];
14
+ rateLimit?: RateLimitInfo;
15
+ }>;
16
+ export declare function readRest<T>(method: string, path: string, body?: unknown): Promise<T>;
@@ -0,0 +1,154 @@
1
+ import { restWithRateLimit } from "./http.mjs";
2
+ import { GitHubRequestError } from "./errors.mjs";
3
+ import { EXIT } from "../exit-codes.mjs";
4
+ import { currentRestConditionalKey, recordRestConditionalRead } from "./rest-conditional-scope.mjs";
5
+ export function restRepoPath(repo) {
6
+ return `/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}`;
7
+ }
8
+ export function malformedRest(detail) {
9
+ throw new GitHubRequestError(`Malformed or incomplete GitHub REST response: ${detail}`, {
10
+ status: 200,
11
+ exitCodeOverride: EXIT.SOFTWARE,
12
+ });
13
+ }
14
+ export function restObject(value, label) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value))
16
+ malformedRest(label);
17
+ return value;
18
+ }
19
+ export function restString(value, label) {
20
+ if (typeof value !== "string" || !value)
21
+ malformedRest(label);
22
+ return value;
23
+ }
24
+ export function restNumber(value, label) {
25
+ if (!Number.isSafeInteger(value) || value < 0)
26
+ malformedRest(label);
27
+ return value;
28
+ }
29
+ export function restBoolean(value, label) {
30
+ if (typeof value !== "boolean")
31
+ malformedRest(label);
32
+ return value;
33
+ }
34
+ export function restArray(value, label) {
35
+ if (!Array.isArray(value))
36
+ malformedRest(label);
37
+ return value;
38
+ }
39
+ /** Counted REST envelopes must include the count as well as their list. */
40
+ export function restCollection(value, key) {
41
+ const record = restObject(value, key);
42
+ restNumber(record.total_count, `${key}.total_count`);
43
+ return restArray(record[key], key);
44
+ }
45
+ const REST_PAGE_SIZE = 100;
46
+ /** Complete REST connections; follow GitHub's Link, never infer completeness from a short page. */
47
+ export async function readRestPages(path, select = (body) => restArray(body, path)) {
48
+ let next = `${path}${path.includes("?") ? "&" : "?"}per_page=${REST_PAGE_SIZE}`;
49
+ const visited = new Set();
50
+ const nodes = [];
51
+ let rateLimit;
52
+ let total;
53
+ while (next) {
54
+ if (visited.has(next) || visited.size >= 1_000)
55
+ malformedRest(`pagination cycle or limit at ${next}`);
56
+ visited.add(next);
57
+ let result = await readRestRequest("GET", next);
58
+ let page = select(result.data);
59
+ if (!Array.isArray(page))
60
+ malformedRest(`missing page at ${next}`);
61
+ // A full page's body (and ETag) stays the same when an item is appended past it; only its
62
+ // Link gains rel="next". A 304 can't prove the cached terminal Link is still current, so a
63
+ // full page without a next link is re-read fresh rather than hiding items on a later page.
64
+ if (result.status === 304 &&
65
+ page.length >= REST_PAGE_SIZE &&
66
+ nextRestPage(result.link, path) === undefined) {
67
+ result = await readRestRequest("GET", next, undefined, true);
68
+ page = select(result.data);
69
+ if (!Array.isArray(page))
70
+ malformedRest(`missing page at ${next}`);
71
+ }
72
+ rateLimit = result.rateLimit ?? rateLimit;
73
+ nodes.push(...page);
74
+ if (result.data &&
75
+ typeof result.data === "object" &&
76
+ !Array.isArray(result.data) &&
77
+ "total_count" in result.data) {
78
+ const count = restNumber(result.data.total_count, `${path}.total_count`);
79
+ if (total !== undefined && total !== count)
80
+ malformedRest(`${path} changed during pagination`);
81
+ total = count;
82
+ }
83
+ next = nextRestPage(result.link, path);
84
+ if (next && page.length === 0)
85
+ malformedRest(`${path}: empty page with next link`);
86
+ }
87
+ if (total !== undefined && nodes.length !== total)
88
+ malformedRest(`${path}: ${nodes.length} of ${total} items`);
89
+ return { nodes, rateLimit };
90
+ }
91
+ function nextRestPage(link, original) {
92
+ if (!link)
93
+ return undefined;
94
+ const entries = link.split(",");
95
+ const nextEntries = entries.filter((entry) => /rel="next"/.test(entry));
96
+ if (nextEntries.length === 0)
97
+ return undefined;
98
+ if (nextEntries.length !== 1)
99
+ malformedRest(`multiple next links for ${original}`);
100
+ const match = /^\s*<([^>]+)>;\s*rel="next"\s*$/.exec(nextEntries[0]);
101
+ if (!match)
102
+ malformedRest(`invalid pagination link for ${original}`);
103
+ const url = new URL(match[1], "https://api.github.com");
104
+ if (url.origin !== "https://api.github.com" || url.pathname !== original.split("?")[0])
105
+ malformedRest(`unsafe pagination link for ${original}`);
106
+ if (url.searchParams.get("per_page") !== "100")
107
+ malformedRest(`changed page size for ${original}`);
108
+ const source = new URL(original, "https://api.github.com");
109
+ for (const key of new Set(source.searchParams.keys())) {
110
+ if (key !== "page" &&
111
+ key !== "per_page" &&
112
+ JSON.stringify(url.searchParams.getAll(key)) !==
113
+ JSON.stringify(source.searchParams.getAll(key)))
114
+ malformedRest(`changed pagination filter for ${original}`);
115
+ }
116
+ return `${url.pathname}${url.search}`;
117
+ }
118
+ /** A pull request whose mergeability GitHub is still computing must be re-read, never replayed. */
119
+ function isSettledBody(body) {
120
+ if (body === null || typeof body !== "object" || Array.isArray(body))
121
+ return true;
122
+ const record = body;
123
+ return record.mergeable !== null && record.mergeable_state !== "unknown";
124
+ }
125
+ let activeRequests = 0;
126
+ const pendingRequests = [];
127
+ /** Bound independent snapshots and nested connection reads to four HTTP requests together. */
128
+ async function readRestRequest(method, path, body, revalidate = false) {
129
+ if (activeRequests >= 4)
130
+ await new Promise((resolve) => pendingRequests.push(resolve));
131
+ else
132
+ activeRequests++;
133
+ try {
134
+ const key = method === "GET" ? currentRestConditionalKey() : undefined;
135
+ if (key === undefined)
136
+ return await restWithRateLimit(method, path, body);
137
+ const result = await restWithRateLimit(method, path, body, {
138
+ conditional: { key, name: path, shouldStore: isSettledBody, revalidate },
139
+ });
140
+ const unvalidated = result.status !== 304 && result.etag === undefined && isSettledBody(result.data);
141
+ recordRestConditionalRead(path, result.status === 304, result.etag, unvalidated ? result.data : undefined);
142
+ return result;
143
+ }
144
+ finally {
145
+ const waiting = pendingRequests.shift();
146
+ if (waiting)
147
+ waiting();
148
+ else
149
+ activeRequests--;
150
+ }
151
+ }
152
+ export async function readRest(method, path, body) {
153
+ return (await readRestRequest(method, path, body)).data;
154
+ }
@@ -0,0 +1,11 @@
1
+ import type { RestReview } from "./rest-feedback-read.mts";
2
+ export declare function validateRestReviews(reviews: RestReview[]): void;
3
+ /** Draft reviews carry no submitted decision and cannot replace earlier submitted evidence. */
4
+ export declare function restLatestReviews(reviews: RestReview[]): Map<string, {
5
+ login: string;
6
+ state: string;
7
+ }>;
8
+ export declare function restPendingReviews(reviews: RestReview[]): {
9
+ login: string;
10
+ state: string;
11
+ }[];
@@ -0,0 +1,47 @@
1
+ import { malformedRest, restString } from "./rest-reader-core.mjs";
2
+ const REVIEW_STATES = new Set([
3
+ "APPROVED",
4
+ "CHANGES_REQUESTED",
5
+ "COMMENTED",
6
+ "DISMISSED",
7
+ "PENDING",
8
+ ]);
9
+ export function validateRestReviews(reviews) {
10
+ for (const review of reviews) {
11
+ if (!REVIEW_STATES.has(restString(review.state, "review state")))
12
+ malformedRest("unknown review state");
13
+ restString(review.commit_id, "review commit_id");
14
+ if (review.state === "PENDING")
15
+ continue;
16
+ const submitted = restString(review.submitted_at, "review submitted_at");
17
+ if (!Number.isFinite(Date.parse(submitted)))
18
+ malformedRest("review submitted_at");
19
+ }
20
+ }
21
+ /** Draft reviews carry no submitted decision and cannot replace earlier submitted evidence. */
22
+ export function restLatestReviews(reviews) {
23
+ const latest = new Map();
24
+ const submitted = reviews.filter((review) => review.state !== "PENDING");
25
+ submitted.sort((a, b) => Date.parse(a.submitted_at) - Date.parse(b.submitted_at) || a.id - b.id);
26
+ for (const review of submitted) {
27
+ if (!review.user?.login)
28
+ continue;
29
+ latest.set(review.user.login.toLowerCase(), {
30
+ login: review.user.login,
31
+ state: review.state,
32
+ });
33
+ }
34
+ return latest;
35
+ }
36
+ export function restPendingReviews(reviews) {
37
+ const pending = new Map();
38
+ for (const review of reviews) {
39
+ if (review.state !== "PENDING" || !review.user?.login)
40
+ continue;
41
+ pending.set(review.user.login.toLowerCase(), {
42
+ login: review.user.login,
43
+ state: review.state,
44
+ });
45
+ }
46
+ return [...pending.values()];
47
+ }
@@ -0,0 +1,9 @@
1
+ import type { RepoInfo } from "./client.mts";
2
+ import type { RawBaseRef } from "./batch-raw-rules.mts";
3
+ export declare function readRestBranchRules(repo: RepoInfo, branch: string): Promise<{
4
+ baseRef?: RawBaseRef;
5
+ unavailable: Array<{
6
+ field: string;
7
+ reason: string;
8
+ }>;
9
+ }>;
@@ -0,0 +1,154 @@
1
+ import { readRest as rest } from "./rest-reader-core.mjs";
2
+ import { GitHubRequestError } from "./errors.mjs";
3
+ import { rateLimitKind } from "./rate-limit-kind.mjs";
4
+ import { isRestSessionRefusal } from "./rest-session-refusal.mjs";
5
+ import { recordRestConditionalRead } from "./rest-conditional-scope.mjs";
6
+ import { readRestPages, restRepoPath, restObject, restString, restArray, restNumber, } from "./rest-reader-core.mjs";
7
+ export async function readRestBranchRules(repo, branch) {
8
+ const path = `${restRepoPath(repo)}/branches/${encodeURIComponent(branch)}/protection`;
9
+ let classic = null;
10
+ const unavailable = [];
11
+ if (!(await classicProtectionDisabled(repo, branch))) {
12
+ try {
13
+ const data = restObject(await rest("GET", path), "branch protection");
14
+ const approvals = data.required_pull_request_reviews == null
15
+ ? null
16
+ : restObject(data.required_pull_request_reviews, "required reviews");
17
+ const checks = data.required_status_checks == null
18
+ ? null
19
+ : restObject(data.required_status_checks, "required checks");
20
+ classic = {
21
+ requiresApprovingReviews: approvals !== null,
22
+ requiredApprovingReviewCount: approvals
23
+ ? restNumber(approvals.required_approving_review_count, "required approval count")
24
+ : 0,
25
+ requiresConversationResolution: enabled(data.required_conversation_resolution),
26
+ requiresStatusChecks: checks !== null,
27
+ requiredStatusCheckContexts: checks
28
+ ? restArray(checks.contexts, "check contexts").map((context) => restString(context, "check context"))
29
+ : [],
30
+ requiresCodeOwnerReviews: approvals?.require_code_owner_reviews === true,
31
+ requireLastPushApproval: approvals?.require_last_push_approval === true,
32
+ requiresCommitSignatures: enabled(data.required_signatures),
33
+ requiresLinearHistory: enabled(data.required_linear_history),
34
+ requiresStrictStatusChecks: checks?.strict === true,
35
+ };
36
+ }
37
+ catch (error) {
38
+ if (!isUnavailablePolicy(error))
39
+ throw error;
40
+ // Only GitHub's explicit unprotected response proves absence. A generic
41
+ // 404 may hide protection from a token without administration access.
42
+ if (!isUnprotectedBranch(error))
43
+ unavailable.push({
44
+ field: "branchProtection",
45
+ reason: `Classic branch protection unavailable (HTTP ${error.status})`,
46
+ });
47
+ }
48
+ }
49
+ let rules;
50
+ try {
51
+ const data = await readRestPages(`${restRepoPath(repo)}/rules/branches/${encodeURIComponent(branch)}`);
52
+ rules = data.nodes.map((rule) => {
53
+ const type = restString(rule.type, "branch rule type").toUpperCase();
54
+ const p = rule.parameters == null ? {} : restObject(rule.parameters, "branch rule parameters");
55
+ return {
56
+ type,
57
+ parameters: {
58
+ ...(p.required_approving_review_count !== undefined && {
59
+ requiredApprovingReviewCount: restNumber(p.required_approving_review_count, "ruleset approval count"),
60
+ }),
61
+ ...(p.required_review_thread_resolution !== undefined && {
62
+ requiredReviewThreadResolution: p.required_review_thread_resolution === true,
63
+ }),
64
+ ...(p.require_code_owner_review !== undefined && {
65
+ requireCodeOwnerReview: p.require_code_owner_review === true,
66
+ }),
67
+ ...(p.require_last_push_approval !== undefined && {
68
+ requireLastPushApproval: p.require_last_push_approval === true,
69
+ }),
70
+ ...(p.strict_required_status_checks_policy !== undefined && {
71
+ strictRequiredStatusChecksPolicy: p.strict_required_status_checks_policy === true,
72
+ }),
73
+ ...(p.required_status_checks !== undefined && {
74
+ requiredStatusChecks: restArray(p.required_status_checks, "ruleset status checks").map((check) => ({
75
+ context: restString(restObject(check, "status check").context, "required context"),
76
+ })),
77
+ }),
78
+ ...(p.required_deployment_environments !== undefined && {
79
+ requiredDeploymentEnvironments: restArray(p.required_deployment_environments, "required deployments").map((environment) => restString(environment, "deployment environment")),
80
+ }),
81
+ },
82
+ };
83
+ });
84
+ }
85
+ catch (error) {
86
+ if (!isUnavailablePolicy(error))
87
+ throw error;
88
+ unavailable.push({
89
+ field: "branchRules",
90
+ reason: `Branch rules unavailable (HTTP ${error.status})`,
91
+ });
92
+ }
93
+ // Preserve partial known policy, but keep incompleteness explicit to READY consumers.
94
+ const baseRef = classic !== null || rules !== undefined
95
+ ? {
96
+ branchProtectionRule: classic,
97
+ rules: rules === undefined ? null : { pageInfo: { hasNextPage: false }, nodes: rules },
98
+ }
99
+ : undefined;
100
+ return { ...(baseRef && { baseRef }), unavailable };
101
+ }
102
+ /**
103
+ * The branch summary is conditional (a 304 costs no quota) while an unprotected branch's
104
+ * `/protection` 404 is charged on every read. Its `commit.sha` also makes a base push visible
105
+ * to REST snapshot reuse. `protection.enabled: false` is GitHub's own
106
+ * "Branch not protected" answer, so the charged read is skipped. Any other shape, including a
107
+ * summary hidden from the token, falls through to the authoritative `/protection` read.
108
+ */
109
+ async function classicProtectionDisabled(repo, branch) {
110
+ const path = `${restRepoPath(repo)}/branches/${encodeURIComponent(branch)}`;
111
+ let summary;
112
+ try {
113
+ summary = await rest("GET", path);
114
+ }
115
+ catch (error) {
116
+ if (!isUnavailablePolicy(error))
117
+ throw error;
118
+ // The summary's `commit.sha` is what tells snapshot reuse the base moved; without it, an
119
+ // unchanged pull body cannot prove mergeability, so the snapshot must not be replayed.
120
+ recordRestConditionalRead(path, false, undefined);
121
+ return false;
122
+ }
123
+ if (!summary || typeof summary !== "object" || Array.isArray(summary))
124
+ return false;
125
+ const protection = summary.protection;
126
+ if (!protection || typeof protection !== "object" || Array.isArray(protection))
127
+ return false;
128
+ return protection.enabled === false;
129
+ }
130
+ function isUnavailablePolicy(error) {
131
+ return (error instanceof GitHubRequestError &&
132
+ [403, 404].includes(error.status) &&
133
+ rateLimitKind(error) === null &&
134
+ !isRestSessionRefusal(error));
135
+ }
136
+ function isUnprotectedBranch(error) {
137
+ if (error.status !== 404)
138
+ return false;
139
+ try {
140
+ const body = JSON.parse(error.responseMessage ?? "");
141
+ return (body !== null &&
142
+ typeof body === "object" &&
143
+ !Array.isArray(body) &&
144
+ body["message"] === "Branch not protected");
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ }
150
+ function enabled(value) {
151
+ return (value !== null &&
152
+ value !== undefined &&
153
+ restObject(value, "protection requirement").enabled === true);
154
+ }
@@ -0,0 +1,5 @@
1
+ import { GitHubRequestError } from "./errors.mts";
2
+ /** A cloud proxy refusal says nothing about the viewer's GitHub authorization. */
3
+ export declare function isRestSessionRefusal(error: unknown): error is GitHubRequestError;
4
+ /** Inspect the response before normal error-message truncation hides its evidence. */
5
+ export declare function isRestSessionRefusalResponse(status: number, raw: string): boolean;
@@ -0,0 +1,21 @@
1
+ import { GitHubRequestError } from "./errors.mjs";
2
+ /** A cloud proxy refusal says nothing about the viewer's GitHub authorization. */
3
+ export function isRestSessionRefusal(error) {
4
+ return (error instanceof GitHubRequestError &&
5
+ isRestSessionRefusalResponse(error.status, error.responseMessage ?? error.message));
6
+ }
7
+ /** Inspect the response before normal error-message truncation hides its evidence. */
8
+ export function isRestSessionRefusalResponse(status, raw) {
9
+ if (status !== 403)
10
+ return false;
11
+ if (/not enabled for this session|sessions are bound/i.test(raw))
12
+ return true;
13
+ try {
14
+ const payload = JSON.parse(raw);
15
+ return (typeof payload?.documentation_url === "string" &&
16
+ new URL(payload.documentation_url).hostname === "docs.anthropic.com");
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
@@ -0,0 +1,10 @@
1
+ import type { readRestStackMembership } from "./rest-stack-read.mts";
2
+ import type { readRestBranchRules } from "./rest-rules-read.mts";
3
+ /** Shared evidence belongs to one stack summary read, never to a later polling tick. */
4
+ export interface RestSnapshotContext {
5
+ stack: Awaited<ReturnType<typeof readRestStackMembership>>;
6
+ repository: Record<string, unknown>;
7
+ viewerLogin: string | null;
8
+ readBranchRules(branch: string): ReturnType<typeof readRestBranchRules>;
9
+ recordPullRevision(pr: number, revision: string): void;
10
+ }
@@ -0,0 +1 @@
1
+ export {};