pr-shepherd 0.56.5 → 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 (423) 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 +40 -19
  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.d.mts +6 -5
  138. package/bin/commands/iterate/native-stack-rebase.mjs +24 -7
  139. package/bin/commands/iterate/render.d.mts +8 -2
  140. package/bin/commands/iterate/render.mjs +28 -42
  141. package/bin/commands/iterate/stack-trunk-conflict.d.mts +4 -2
  142. package/bin/commands/iterate/stack-trunk-conflict.mjs +44 -19
  143. package/bin/commands/iterate/stale-ancestry.mjs +4 -1
  144. package/bin/commands/iterate/stall.d.mts +5 -1
  145. package/bin/commands/iterate/stall.mjs +19 -7
  146. package/bin/commands/iterate/thread-mutation-routing.d.mts +1 -0
  147. package/bin/commands/iterate/thread-mutation-routing.mjs +4 -2
  148. package/bin/commands/iterate/unreported-required.mjs +5 -2
  149. package/bin/commands/mark-files-as-viewed.mjs +7 -0
  150. package/bin/commands/next-check.d.mts +31 -0
  151. package/bin/commands/next-check.mjs +46 -0
  152. package/bin/commands/playbook.d.mts +13 -0
  153. package/bin/commands/playbook.mjs +48 -0
  154. package/bin/commands/poll-mode.d.mts +8 -0
  155. package/bin/commands/poll-mode.mjs +14 -0
  156. package/bin/commands/poll-progress.d.mts +1 -0
  157. package/bin/commands/poll-progress.mjs +6 -3
  158. package/bin/commands/poll-quota.d.mts +3 -2
  159. package/bin/commands/poll-quota.mjs +7 -3
  160. package/bin/commands/poll-summary-explicit-instructions.d.mts +2 -0
  161. package/bin/commands/poll-summary-explicit-instructions.mjs +10 -1
  162. package/bin/commands/poll-summary-instructions.mjs +16 -17
  163. package/bin/commands/poll-summary.mjs +42 -17
  164. package/bin/commands/poll.d.mts +0 -1
  165. package/bin/commands/poll.mjs +11 -19
  166. package/bin/commands/quota-selection.d.mts +2 -1
  167. package/bin/commands/quota-selection.mjs +12 -7
  168. package/bin/commands/resolve-mutate.mjs +33 -5
  169. package/bin/commands/rest-stack-merge-readiness.d.mts +4 -0
  170. package/bin/commands/rest-stack-merge-readiness.mjs +74 -0
  171. package/bin/commands/rule-auto-resolve.mjs +9 -3
  172. package/bin/commands/shepherd-journal.d.mts +5 -4
  173. package/bin/commands/shepherd-journal.mjs +9 -8
  174. package/bin/commands/stack-drain.d.mts +1 -9
  175. package/bin/commands/stack-drain.mjs +50 -37
  176. package/bin/commands/stack-handoffs.d.mts +10 -0
  177. package/bin/commands/stack-handoffs.mjs +25 -0
  178. package/bin/commands/stack-stall.d.mts +5 -0
  179. package/bin/commands/stack-stall.mjs +11 -0
  180. package/bin/commands/stack-work.d.mts +5 -0
  181. package/bin/commands/stack-work.mjs +32 -0
  182. package/bin/comments/existing-reply-scan.d.mts +17 -0
  183. package/bin/comments/existing-reply-scan.mjs +41 -0
  184. package/bin/comments/rate-limit.mjs +2 -2
  185. package/bin/comments/resolve.d.mts +7 -1
  186. package/bin/comments/resolve.mjs +108 -18
  187. package/bin/comments/rest-reply.d.mts +9 -0
  188. package/bin/comments/rest-reply.mjs +95 -0
  189. package/bin/comments/rest-review-mutations.d.mts +22 -0
  190. package/bin/comments/rest-review-mutations.mjs +125 -0
  191. package/bin/comments/review-visibility.d.mts +1 -1
  192. package/bin/comments/review-visibility.mjs +3 -2
  193. package/bin/comments/sha-poll.mjs +7 -3
  194. package/bin/comments/thread-visibility.mjs +10 -5
  195. package/bin/comments/uncertain-replies.d.mts +11 -0
  196. package/bin/comments/uncertain-replies.mjs +121 -0
  197. package/bin/comments/uncertain-reply-guards.d.mts +10 -0
  198. package/bin/comments/uncertain-reply-guards.mjs +94 -0
  199. package/bin/comments/visible-comments.d.mts +1 -1
  200. package/bin/comments/visible-comments.mjs +4 -2
  201. package/bin/config/load.d.mts +37 -1
  202. package/bin/config/load.mjs +73 -1
  203. package/bin/config.json +6 -2
  204. package/bin/exit-codes.d.mts +3 -0
  205. package/bin/exit-codes.mjs +8 -0
  206. package/bin/github/api-telemetry-aggregate.d.mts +15 -4
  207. package/bin/github/api-telemetry-aggregate.mjs +57 -18
  208. package/bin/github/api-telemetry.d.mts +4 -0
  209. package/bin/github/api-telemetry.mjs +22 -8
  210. package/bin/github/apply-review-preflight.d.mts +20 -0
  211. package/bin/github/apply-review-preflight.mjs +61 -0
  212. package/bin/github/base-behind-cache.d.mts +15 -0
  213. package/bin/github/base-behind-cache.mjs +18 -0
  214. package/bin/github/batch-parse-suites.d.mts +4 -2
  215. package/bin/github/batch-parse-suites.mjs +6 -1
  216. package/bin/github/batch-parser-helpers.mjs +40 -2
  217. package/bin/github/batch-parsers.mjs +1 -0
  218. package/bin/github/batch-raw-rules.d.mts +26 -0
  219. package/bin/github/batch-raw-types.d.mts +8 -6
  220. package/bin/github/batch-stack-evidence.d.mts +28 -0
  221. package/bin/github/batch-stack-evidence.mjs +62 -0
  222. package/bin/github/batch.d.mts +15 -2
  223. package/bin/github/batch.mjs +34 -17
  224. package/bin/github/check-annotations-batch.d.mts +0 -5
  225. package/bin/github/check-annotations-batch.mjs +25 -4
  226. package/bin/github/check-annotations.mjs +3 -1
  227. package/bin/github/client.d.mts +8 -19
  228. package/bin/github/client.mjs +45 -13
  229. package/bin/github/fingerprint-fields.d.mts +8 -0
  230. package/bin/github/fingerprint-fields.mjs +6 -1
  231. package/bin/github/fingerprint.d.mts +4 -2
  232. package/bin/github/fingerprint.mjs +6 -38
  233. package/bin/github/github-fetch.d.mts +2 -0
  234. package/bin/github/github-fetch.mjs +19 -0
  235. package/bin/github/gql/base-behind.gql +3 -0
  236. package/bin/github/gql/batch-pr-page.gql +16 -0
  237. package/bin/github/gql/batch-pr.gql +57 -0
  238. package/bin/github/gql/commit-check-contexts.gql +17 -0
  239. package/bin/github/gql/get-pr-body.gql +1 -0
  240. package/bin/github/gql/poll-stack-summary.gql +1 -0
  241. package/bin/github/gql/poll-summary-check-contexts.gql +5 -0
  242. package/bin/github/gql/poll-summary-fragment.gql +65 -15
  243. package/bin/github/gql/stack-handoff-check-runs.gql +42 -0
  244. package/bin/github/gql/upper-layer-conflict-target.gql +0 -15
  245. package/bin/github/graphql-http.mjs +9 -4
  246. package/bin/github/graphql-internal-retry.d.mts +2 -0
  247. package/bin/github/graphql-internal-retry.mjs +1 -1
  248. package/bin/github/graphql-quota-probe.d.mts +6 -0
  249. package/bin/github/graphql-quota-probe.mjs +56 -0
  250. package/bin/github/graphql-session-refusal.d.mts +8 -0
  251. package/bin/github/graphql-session-refusal.mjs +14 -0
  252. package/bin/github/http-request.d.mts +1 -1
  253. package/bin/github/http-request.mjs +22 -6
  254. package/bin/github/http-utils.mjs +6 -0
  255. package/bin/github/merge-target-rules.d.mts +16 -3
  256. package/bin/github/merge-target-rules.mjs +79 -44
  257. package/bin/github/mutation-policy.d.mts +6 -0
  258. package/bin/github/mutation-policy.mjs +21 -0
  259. package/bin/github/poll-summary-annotation-probe.mjs +17 -10
  260. package/bin/github/poll-summary-check-blockers.d.mts +1 -1
  261. package/bin/github/poll-summary-check-blockers.mjs +2 -2
  262. package/bin/github/poll-summary-check-hydration.mjs +25 -10
  263. package/bin/github/poll-summary-checks.d.mts +2 -2
  264. package/bin/github/poll-summary-checks.mjs +9 -6
  265. package/bin/github/poll-summary-command.d.mts +2 -0
  266. package/bin/github/poll-summary-command.mjs +24 -0
  267. package/bin/github/poll-summary-fingerprint.d.mts +1 -1
  268. package/bin/github/poll-summary-fingerprint.mjs +4 -2
  269. package/bin/github/poll-summary-handoff-fields.d.mts +3 -0
  270. package/bin/github/poll-summary-handoff-fields.mjs +73 -0
  271. package/bin/github/poll-summary-projector.d.mts +1 -1
  272. package/bin/github/poll-summary-projector.mjs +17 -24
  273. package/bin/github/poll-summary-raw.d.mts +70 -10
  274. package/bin/github/poll-summary-readiness.mjs +4 -1
  275. package/bin/github/poll-summary-rest-queue.d.mts +3 -0
  276. package/bin/github/poll-summary-rest-queue.mjs +12 -0
  277. package/bin/github/poll-summary-review.d.mts +1 -1
  278. package/bin/github/poll-summary-review.mjs +23 -11
  279. package/bin/github/poll-summary-route.mjs +19 -3
  280. package/bin/github/poll-summary-stack-page.d.mts +11 -0
  281. package/bin/github/poll-summary-stack-page.mjs +27 -0
  282. package/bin/github/poll-summary-unreported.d.mts +6 -3
  283. package/bin/github/poll-summary-unreported.mjs +15 -7
  284. package/bin/github/poll-summary.d.mts +4 -1
  285. package/bin/github/poll-summary.mjs +48 -34
  286. package/bin/github/queries.d.mts +9 -5
  287. package/bin/github/queries.mjs +10 -6
  288. package/bin/github/rate-limit-sample.d.mts +7 -0
  289. package/bin/github/rate-limit-sample.mjs +12 -0
  290. package/bin/github/reply-recovery-read.d.mts +50 -0
  291. package/bin/github/reply-recovery-read.mjs +118 -0
  292. package/bin/github/reply-thread-transcripts.d.mts +0 -1
  293. package/bin/github/reply-thread-transcripts.mjs +24 -1
  294. package/bin/github/rest-annotation-read.d.mts +5 -0
  295. package/bin/github/rest-annotation-read.mjs +44 -0
  296. package/bin/github/rest-batch-read.d.mts +12 -0
  297. package/bin/github/rest-batch-read.mjs +57 -0
  298. package/bin/github/rest-batch-snapshot.d.mts +30 -0
  299. package/bin/github/rest-batch-snapshot.mjs +115 -0
  300. package/bin/github/rest-batch-unavailable.d.mts +5 -0
  301. package/bin/github/rest-batch-unavailable.mjs +16 -0
  302. package/bin/github/rest-check-read.d.mts +34 -0
  303. package/bin/github/rest-check-read.mjs +124 -0
  304. package/bin/github/rest-comment-authorship.d.mts +7 -0
  305. package/bin/github/rest-comment-authorship.mjs +11 -0
  306. package/bin/github/rest-conditional-scope.d.mts +20 -0
  307. package/bin/github/rest-conditional-scope.mjs +44 -0
  308. package/bin/github/rest-explicit-summary-read.d.mts +3 -0
  309. package/bin/github/rest-explicit-summary-read.mjs +8 -0
  310. package/bin/github/rest-feedback-read.d.mts +36 -0
  311. package/bin/github/rest-feedback-read.mjs +147 -0
  312. package/bin/github/rest-feedback-validation.d.mts +5 -0
  313. package/bin/github/rest-feedback-validation.mjs +28 -0
  314. package/bin/github/rest-http.d.mts +12 -0
  315. package/bin/github/rest-http.mjs +108 -23
  316. package/bin/github/rest-identities.d.mts +22 -0
  317. package/bin/github/rest-identities.mjs +134 -0
  318. package/bin/github/rest-merge-stack-guard.d.mts +14 -0
  319. package/bin/github/rest-merge-stack-guard.mjs +41 -0
  320. package/bin/github/rest-merge.d.mts +29 -0
  321. package/bin/github/rest-merge.mjs +56 -0
  322. package/bin/github/rest-merged-base-read.d.mts +9 -0
  323. package/bin/github/rest-merged-base-read.mjs +48 -0
  324. package/bin/github/rest-pr-core.d.mts +52 -0
  325. package/bin/github/rest-pr-core.mjs +60 -0
  326. package/bin/github/rest-reader-core.d.mts +16 -0
  327. package/bin/github/rest-reader-core.mjs +154 -0
  328. package/bin/github/rest-review-read.d.mts +11 -0
  329. package/bin/github/rest-review-read.mjs +47 -0
  330. package/bin/github/rest-rules-read.d.mts +9 -0
  331. package/bin/github/rest-rules-read.mjs +154 -0
  332. package/bin/github/rest-session-refusal.d.mts +5 -0
  333. package/bin/github/rest-session-refusal.mjs +21 -0
  334. package/bin/github/rest-snapshot-context.d.mts +10 -0
  335. package/bin/github/rest-snapshot-context.mjs +1 -0
  336. package/bin/github/rest-stack-read.d.mts +21 -0
  337. package/bin/github/rest-stack-read.mjs +47 -0
  338. package/bin/github/rest-stack-summary.d.mts +8 -0
  339. package/bin/github/rest-stack-summary.mjs +62 -0
  340. package/bin/github/rest-summary-read.d.mts +7 -0
  341. package/bin/github/rest-summary-read.mjs +112 -0
  342. package/bin/github/rest-terminal-batch.d.mts +14 -0
  343. package/bin/github/rest-terminal-batch.mjs +45 -0
  344. package/bin/github/rest-text.mjs +7 -3
  345. package/bin/github/rest-thread-read.d.mts +4 -0
  346. package/bin/github/rest-thread-read.mjs +35 -0
  347. package/bin/github/rest-viewer-read.d.mts +2 -0
  348. package/bin/github/rest-viewer-read.mjs +41 -0
  349. package/bin/github/stack-bottom.d.mts +3 -0
  350. package/bin/github/stack-bottom.mjs +14 -0
  351. package/bin/github/stack-handoff-context.d.mts +25 -0
  352. package/bin/github/stack-handoff-context.mjs +12 -0
  353. package/bin/github/stack-handoff-page.d.mts +17 -0
  354. package/bin/github/stack-handoff-page.mjs +306 -0
  355. package/bin/github/stack-handoff.d.mts +16 -0
  356. package/bin/github/stack-handoff.mjs +115 -0
  357. package/bin/github/stack-order.d.mts +6 -0
  358. package/bin/github/stack-order.mjs +13 -0
  359. package/bin/github/stack-read.d.mts +7 -3
  360. package/bin/github/stack-read.mjs +22 -11
  361. package/bin/github/stack-size-hint.d.mts +3 -0
  362. package/bin/github/stack-size-hint.mjs +14 -0
  363. package/bin/github/suggestion-thread.mjs +27 -1
  364. package/bin/github/thread-comments.mjs +9 -1
  365. package/bin/github/transport-evidence.d.mts +20 -0
  366. package/bin/github/transport-evidence.mjs +26 -0
  367. package/bin/github/transport-mode.d.mts +2 -0
  368. package/bin/github/transport-mode.mjs +5 -0
  369. package/bin/github/transport.d.mts +19 -0
  370. package/bin/github/transport.mjs +126 -0
  371. package/bin/github/unsupported-rest.d.mts +4 -0
  372. package/bin/github/unsupported-rest.mjs +7 -0
  373. package/bin/mcp/server.mjs +52 -3
  374. package/bin/merge-status/derive.d.mts +2 -0
  375. package/bin/merge-status/derive.mjs +17 -13
  376. package/bin/merge-status/required-check-requirements.d.mts +7 -0
  377. package/bin/merge-status/required-check-requirements.mjs +15 -0
  378. package/bin/merge-status/requirements-format.d.mts +5 -0
  379. package/bin/merge-status/requirements-format.mjs +41 -12
  380. package/bin/merge-status/requirements.mjs +18 -7
  381. package/bin/state/base.mjs +41 -0
  382. package/bin/state/durable-state.d.mts +6 -0
  383. package/bin/state/durable-state.mjs +41 -0
  384. package/bin/state/graphql-quota-policy.mjs +23 -16
  385. package/bin/state/merge-request.d.mts +25 -0
  386. package/bin/state/merge-request.mjs +145 -0
  387. package/bin/state/pr-fingerprint.d.mts +15 -0
  388. package/bin/state/pr-fingerprint.mjs +22 -7
  389. package/bin/state/rest-cache.d.mts +3 -0
  390. package/bin/state/rest-cache.mjs +1 -0
  391. package/bin/state/rest-snapshot-report.d.mts +21 -0
  392. package/bin/state/rest-snapshot-report.mjs +52 -0
  393. package/bin/state/seen-comments.d.mts +3 -0
  394. package/bin/state/seen-comments.mjs +18 -0
  395. package/bin/types/api-usage.d.mts +9 -1
  396. package/bin/types/escalate.d.mts +1 -1
  397. package/bin/types/github.d.mts +23 -9
  398. package/bin/types/iterate.d.mts +24 -2
  399. package/bin/types/merge-action.d.mts +1 -1
  400. package/bin/types/merge-requirements.d.mts +7 -7
  401. package/bin/types/next-check.d.mts +12 -0
  402. package/bin/types/next-check.mjs +1 -0
  403. package/bin/types/poll-summary.d.mts +21 -0
  404. package/bin/types/report.d.mts +13 -1
  405. package/bin/types/review-thread.d.mts +1 -1
  406. package/bin/types.d.mts +1 -0
  407. package/bin/types.mjs +1 -0
  408. package/package.json +3 -2
  409. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  410. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  411. package/plugins/pr-shepherd/.mcp.json +1 -1
  412. package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +1 -1
  413. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +14 -51
  414. package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +12 -16
  415. package/plugins/pr-shepherd/skills/pr-shepherd/references/cloud-event-loop.md +14 -0
  416. package/plugins/pr-shepherd/skills/pr-shepherd/references/create-pr.md +10 -0
  417. package/plugins/pr-shepherd/skills/pr-shepherd/references/fix-code-loop.md +10 -0
  418. package/plugins/pr-shepherd/skills/pr-shepherd/references/mcp-fallback.md +9 -0
  419. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +2 -0
  420. package/bin/github/gql/pr-fingerprint.gql +0 -75
  421. package/plugins/pr-shepherd/skills/pr-shepherd/references/journal.md +0 -7
  422. package/plugins/pr-shepherd/skills/pr-shepherd/references/review-mutations.md +0 -9
  423. package/plugins/pr-shepherd/skills/pr-shepherd/references/stack-merge.md +0 -7
@@ -1,3 +1,5 @@
1
+ import { buildPrShepherdCommand } from "../cli/runner.mjs";
2
+ import { formatPrUrl } from "../pr-reference.mjs";
1
3
  /**
2
4
  * The one-PR hold reason for a `READY` draft that only the disabled setting keeps in draft. The
3
5
  * probe re-reads the draft, so this phrase, not a bare `[WAIT]`, proves it is still clean.
@@ -30,9 +32,39 @@ export function splitStackWork(candidates, staleChildren) {
30
32
  */
31
33
  export function appendMarkReadyInstructions(instructions, layers) {
32
34
  for (const item of layers) {
35
+ if (!item.owned) {
36
+ instructions.push(`${instructions.length + 1}. PR #${item.pr} is not owned. Leave its draft state unchanged.`);
37
+ continue;
38
+ }
39
+ if (item.transport === "rest") {
40
+ const command = buildPrShepherdCommand([
41
+ "iterate",
42
+ formatPrUrl(item.repo, item.pr),
43
+ "--transport",
44
+ "rest",
45
+ ]).text;
46
+ instructions.push(`${instructions.length + 1}. Run \`${item.pollCommand}\` first for PR #${item.pr}. If it returns \`[WAIT]\` saying the draft is held only because automatic mark-ready is disabled, run \`${command}\` to authorize the CCR ready transition; otherwise follow the probe's instructions.`);
47
+ continue;
48
+ }
33
49
  instructions.push(`${instructions.length + 1}. Automatic mark-ready is disabled, so marking PR #${item.pr} ready for review is your step. Run \`${item.pollCommand}\` first. If it returns \`[WAIT]\` saying PR #${item.pr} stays in draft because ${AUTO_MARK_READY_DISABLED_HOLD}, run \`gh pr ready ${item.pr} -R ${item.repo}\`; otherwise complete its instructions and leave PR #${item.pr} in draft this round.`);
34
50
  }
35
51
  }
36
52
  function isProbed(item) {
37
53
  return item.pollProbe === true && item.pollCommand !== undefined;
38
54
  }
55
+ /**
56
+ * Work exists, but every routed layer belongs to another author: rerunning the stack selector
57
+ * cannot change anything, so the caller reports the overview and stops instead.
58
+ */
59
+ function onlyUnownedWork(layers) {
60
+ return layers.length > 0 && layers.every((item) => !item.owned);
61
+ }
62
+ /**
63
+ * The closing step after routed sessions: rerun the selector, or stop when nothing is owned.
64
+ * Returns the plan action: a stop is a handoff, so no rerun, quota cadence, or wake-up follows it.
65
+ */
66
+ export function appendStackRerunInstruction(instructions, layers, rerun) {
67
+ const stop = onlyUnownedWork(layers);
68
+ instructions.push(`${instructions.length + 1}. ${stop ? "No owned layer needs a session. Report this overview and stop." : rerun}`);
69
+ return stop ? "escalate" : "shepherd";
70
+ }
@@ -0,0 +1,17 @@
1
+ import type { RepoInfo } from "../github/client.mts";
2
+ import { type ReplyEvidence } from "../github/reply-recovery-read.mts";
3
+ /**
4
+ * Threads whose last comment is already this exact Shepherd reply from the authenticated viewer.
5
+ *
6
+ * A cloud session can lose its state directory between ticks, taking the uncertain-reply
7
+ * records with it. With no local record to consult, GitHub is the only evidence of whether a
8
+ * reply already landed, so the `apply review` commands Shepherd generates in durable-state
9
+ * sessions opt in (`--adopt-existing-replies`) to checking it before posting. Direct apply
10
+ * requests never run this scan. A failed read never blocks the reply: it only means this
11
+ * safeguard had nothing to say. A batch fully covered by `preloaded` evidence (the
12
+ * apply-review preflight, read with no wait since) skips its read.
13
+ */
14
+ export declare function findExistingReplies(context: {
15
+ repo: RepoInfo;
16
+ pr: number;
17
+ }, ids: readonly string[], message: string, preloaded?: Map<string, ReplyEvidence>): Promise<string[]>;
@@ -0,0 +1,41 @@
1
+ import { readReplyRecoveryEvidence } from "../github/reply-recovery-read.mjs";
2
+ import { addPrShepherdMarker } from "./marker.mjs";
3
+ const BATCH = 10;
4
+ /**
5
+ * Threads whose last comment is already this exact Shepherd reply from the authenticated viewer.
6
+ *
7
+ * A cloud session can lose its state directory between ticks, taking the uncertain-reply
8
+ * records with it. With no local record to consult, GitHub is the only evidence of whether a
9
+ * reply already landed, so the `apply review` commands Shepherd generates in durable-state
10
+ * sessions opt in (`--adopt-existing-replies`) to checking it before posting. Direct apply
11
+ * requests never run this scan. A failed read never blocks the reply: it only means this
12
+ * safeguard had nothing to say. A batch fully covered by `preloaded` evidence (the
13
+ * apply-review preflight, read with no wait since) skips its read.
14
+ */
15
+ export async function findExistingReplies(context, ids, message, preloaded) {
16
+ if (ids.length === 0)
17
+ return [];
18
+ const marked = addPrShepherdMarker(message);
19
+ const found = [];
20
+ for (let offset = 0; offset < ids.length; offset += BATCH) {
21
+ const batch = ids.slice(offset, offset + BATCH);
22
+ try {
23
+ const evidence = batch.every((id) => preloaded?.has(id))
24
+ ? preloaded
25
+ : await readReplyRecoveryEvidence(context.repo, context.pr, batch);
26
+ for (const id of batch) {
27
+ const item = evidence.get(id);
28
+ const last = item?.comments.at(-1);
29
+ if (item &&
30
+ last?.body === marked &&
31
+ last.author?.toLowerCase() === item.viewer.toLowerCase()) {
32
+ found.push(id);
33
+ }
34
+ }
35
+ }
36
+ catch {
37
+ /* Evidence is advisory here; the normal reply path still runs. */
38
+ }
39
+ }
40
+ return found;
41
+ }
@@ -17,7 +17,7 @@ export function rateLimitFromGraphQlResult(messages, meta) {
17
17
  if (message)
18
18
  return buildRateLimitStop(message, meta);
19
19
  if (meta.stopOnZeroRemaining === true && meta.rateLimit?.remaining === 0) {
20
- return buildRateLimitStop("GitHub GraphQL rate limit remaining is 0", meta);
20
+ return buildRateLimitStop(`GitHub ${meta.rateLimit?.resource === "core" ? "REST core" : "GraphQL"} rate limit remaining is 0`, meta);
21
21
  }
22
22
  return undefined;
23
23
  }
@@ -47,5 +47,5 @@ function finiteNumber(value) {
47
47
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
48
48
  }
49
49
  export function isRateLimitMessage(message) {
50
- return /rate limit|rate-limit|secondary limit|secondary rate/i.test(message);
50
+ return /rate limit|rate-limit|secondary limit|secondary rate|abuse detection/i.test(message);
51
51
  }
@@ -1,6 +1,7 @@
1
1
  import { type RepoInfo } from "../github/client.mts";
2
2
  import type { ResolveOptions } from "../types.mts";
3
3
  import { type ResolveRateLimitStop } from "./rate-limit.mts";
4
+ import { type ReplyEvidence } from "../github/reply-recovery-read.mts";
4
5
  export interface ResolveResult {
5
6
  repliedThreads: string[];
6
7
  resolvedThreads: string[];
@@ -26,12 +27,17 @@ export interface ResolveResult {
26
27
  /** @deprecated Direct apply requests now rely on GitHub's mutation response. */
27
28
  skippedUnauthorizedDismissals?: string[];
28
29
  rateLimit?: ResolveRateLimitStop;
30
+ sessionRefusal?: string;
31
+ instructions?: string[];
29
32
  unrepliedThreads?: string[];
30
33
  unresolvedThreads?: string[];
31
34
  unminimizedComments?: string[];
32
35
  undismissedReviews?: string[];
33
36
  }
34
- export declare function applyResolveOptions(pr: number, repo: RepoInfo, opts: ResolveOptions): Promise<ResolveResult>;
37
+ export declare function applyResolveOptions(pr: number, repo: RepoInfo, opts: ResolveOptions, preflight?: {
38
+ headSha?: string;
39
+ evidence: Map<string, ReplyEvidence>;
40
+ }): Promise<ResolveResult>;
35
41
  /** @deprecated Compatibility alias; use `autoResolveThreads`. */
36
42
  export declare function autoResolveOutdated(threadIds: string[]): Promise<{
37
43
  resolved: string[];
@@ -1,9 +1,18 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { graphqlWithRateLimit } from "../github/client.mjs";
3
3
  import { addPrShepherdMarker } from "./marker.mjs";
4
+ import { findExistingReplies } from "./existing-reply-scan.mjs";
4
5
  import { isRateLimitMessage, rateLimitFromError, rateLimitFromGraphQlResult, } from "./rate-limit.mjs";
5
6
  import { setPendingOps } from "./pending-ops.mjs";
6
7
  import { waitForSha } from "./sha-poll.mjs";
8
+ import { githubOperation, getGithubTransport } from "../github/transport.mjs";
9
+ import { resolveGraphqlThreadId } from "../github/rest-identities.mjs";
10
+ import { applyRestReviewChunk, isAmbiguousMutationError } from "./rest-review-mutations.mjs";
11
+ import { assertReplyOutcomeKnown, rememberUncertainReplies, trackAdoptedReplyThreads, } from "./uncertain-replies.mjs";
12
+ import { isRestSessionRefusal } from "../github/rest-session-refusal.mjs";
13
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
14
+ import { readReplyRecoveryEvidence } from "../github/reply-recovery-read.mjs";
15
+ import { GitHubRequestError, isRetryableGraphQlInternal, } from "../github/errors.mjs";
7
16
  const COMMENTED_DISMISS_ERROR_PATTERNS = [
8
17
  /can\s*not\s+dismiss[\s\S]*?commented pull request review/i,
9
18
  ];
@@ -30,7 +39,15 @@ function mutationErrorMessage(errors, alias) {
30
39
  .map((error) => error.message);
31
40
  return messages.length > 0 ? messages.join("; ") : undefined;
32
41
  }
33
- export async function applyResolveOptions(pr, repo, opts) {
42
+ function uncertainReplyIds(ids, confirmed, errors) {
43
+ return ids.filter((id, index) => {
44
+ if (confirmed.includes(id))
45
+ return false;
46
+ const applicable = errors.filter((error) => error.path === undefined || (Array.isArray(error.path) && error.path[0] === `p${index}`));
47
+ return applicable.length === 0 || isRetryableGraphQlInternal(applicable);
48
+ });
49
+ }
50
+ export async function applyResolveOptions(pr, repo, opts, preflight) {
34
51
  const resolveThreadIds = dedupeIds(opts.resolveThreadIds ?? []);
35
52
  const replyThreadIds = dedupeIds(opts.replyThreadIds ?? []);
36
53
  const minimizeCommentIds = opts.minimizeCommentIds ?? [];
@@ -45,12 +62,30 @@ export async function applyResolveOptions(pr, repo, opts) {
45
62
  if ((dismissReviewIds.length > 0 || replyThreadIds.length > 0) && !opts.dismissMessage) {
46
63
  throw new Error("--message is required when replying to threads or dismissing reviews");
47
64
  }
48
- if (opts.requireSha) {
65
+ // A preflight that already saw the required head stands in for the first poll; its
66
+ // recovery evidence is used only when no poll wait separated it from the mutation.
67
+ const headConfirmed = !opts.requireSha || preflight?.headSha === opts.requireSha;
68
+ const preloadedEvidence = headConfirmed ? preflight?.evidence : undefined;
69
+ if (opts.requireSha && !headConfirmed) {
49
70
  // Verify GitHub received the commit before resolving — prevents auto-merge
50
71
  // before reviewers see the fix.
51
72
  await waitForSha(pr, repo, opts.requireSha);
52
73
  }
53
- await bulkApply(replyThreadIds, resolveThreadIds, minimizeCommentIds, dismissReviewIds, opts.dismissMessage ?? "", result);
74
+ const known = await assertReplyOutcomeKnown({ repo, pr }, replyThreadIds, opts.dismissMessage ?? "");
75
+ const adopted = [
76
+ ...known,
77
+ ...(opts.adoptExistingReplies
78
+ ? await findExistingReplies({ repo, pr }, replyThreadIds.filter((id) => !known.includes(id)), opts.dismissMessage ?? "", preloadedEvidence)
79
+ : []),
80
+ ];
81
+ result.repliedThreads.push(...adopted);
82
+ trackAdoptedReplyThreads(result, adopted);
83
+ await bulkApply(replyThreadIds.filter((id) => !adopted.includes(id)), resolveThreadIds, minimizeCommentIds, dismissReviewIds, opts.dismissMessage ?? "", result, { repo, pr }, preloadedEvidence);
84
+ if (result.sessionRefusal)
85
+ result.instructions = [
86
+ "Restore GitHub access for this session using the proxy instructions above.",
87
+ "Retry only the pending IDs listed above.",
88
+ ];
54
89
  return result;
55
90
  }
56
91
  /** @deprecated Compatibility alias; use `autoResolveThreads`. */
@@ -68,6 +103,8 @@ export async function autoResolveThreads(threadIds) {
68
103
  errors: [],
69
104
  };
70
105
  await bulkApply([], threadIds, [], [], "", result);
106
+ if (result.sessionRefusal)
107
+ throw new ShepherdError(result.sessionRefusal, EXIT.NOPERM);
71
108
  return { resolved: result.resolvedThreads, errors: result.errors };
72
109
  }
73
110
  export async function autoMinimizeComments(minimizeIds) {
@@ -81,6 +118,8 @@ export async function autoMinimizeComments(minimizeIds) {
81
118
  errors: [],
82
119
  };
83
120
  await bulkApply([], [], minimizeIds, [], "", result);
121
+ if (result.sessionRefusal)
122
+ throw new ShepherdError(result.sessionRefusal, EXIT.NOPERM);
84
123
  return { minimized: result.minimizedComments, errors: result.errors };
85
124
  }
86
125
  // Keep mutation batches small so rate-limit stops leave a precise pending list.
@@ -102,7 +141,7 @@ function buildBulkMutation(replyIds, resolveIds, minimizeIds, dismissIds, dismis
102
141
  }
103
142
  return `mutation BulkApply {\n${ops.join("\n")}\n}`;
104
143
  }
105
- async function bulkApply(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage, result) {
144
+ async function bulkApply(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage, result, context, firstChunkEvidence) {
106
145
  const allOps = [
107
146
  ...replyIds.map((id) => ({ kind: "p", id })),
108
147
  ...resolveIds.map((id) => ({ kind: "r", id })),
@@ -112,34 +151,76 @@ async function bulkApply(replyIds, resolveIds, minimizeIds, dismissIds, dismissM
112
151
  for (let i = 0; i < allOps.length; i += BULK_CHUNK_SIZE) {
113
152
  const chunk = allOps.slice(i, i + BULK_CHUNK_SIZE);
114
153
  // eslint-disable-next-line no-await-in-loop
115
- const stopped = await bulkApplyChunk(chunk.filter((o) => o.kind === "r").map((o) => o.id), chunk.filter((o) => o.kind === "p").map((o) => o.id), chunk.filter((o) => o.kind === "m").map((o) => o.id), chunk.filter((o) => o.kind === "d").map((o) => o.id), dismissMessage, result, i + BULK_CHUNK_SIZE < allOps.length);
154
+ const stopped = await bulkApplyChunk(chunk.filter((o) => o.kind === "r").map((o) => o.id), chunk.filter((o) => o.kind === "p").map((o) => o.id), chunk.filter((o) => o.kind === "m").map((o) => o.id), chunk.filter((o) => o.kind === "d").map((o) => o.id), dismissMessage, result, i + BULK_CHUNK_SIZE < allOps.length, context, i === 0 ? firstChunkEvidence : undefined);
116
155
  if (stopped) {
117
156
  setPendingOps(result, allOps.slice(i));
118
157
  return;
119
158
  }
120
159
  }
121
160
  }
122
- async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dismissMessage, result, hasPendingAfter) {
123
- const doc = buildBulkMutation(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage);
161
+ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dismissMessage, result, hasPendingAfter, context, preloadedEvidence) {
124
162
  let data = {};
125
163
  let graphQlErrors = [];
126
164
  let rateLimitStop;
127
165
  let suppressCurrentChunkErrors = false;
166
+ let restStopped = false;
167
+ let graphqlMutationAttempted = false;
168
+ let replyEvidence = new Map();
169
+ if (context && replyIds.length && getGithubTransport() === "graphql") {
170
+ // Evidence is all-or-nothing per batch, as the standalone read is.
171
+ if (replyIds.every((id) => preloadedEvidence?.has(id)))
172
+ replyEvidence = new Map(replyIds.map((id) => [id, preloadedEvidence.get(id)]));
173
+ else
174
+ try {
175
+ replyEvidence = await readReplyRecoveryEvidence(context.repo, context.pr, replyIds);
176
+ }
177
+ catch {
178
+ /* Preserve explicit mutation eligibility when recovery evidence is unavailable. */
179
+ }
180
+ }
128
181
  try {
129
- const resp = await graphqlWithRateLimit(doc, {}, {
130
- allowPartialData: true,
131
- });
182
+ const resp = await githubOperation("BulkApply", async () => {
183
+ const [graphqlReplies, graphqlResolves] = await Promise.all([
184
+ Promise.all(replyIds.map(resolveGraphqlThreadId)),
185
+ Promise.all(resolveIds.map(resolveGraphqlThreadId)),
186
+ ]);
187
+ graphqlMutationAttempted = true;
188
+ return graphqlWithRateLimit(buildBulkMutation(graphqlReplies, graphqlResolves, minimizeIds, dismissIds, dismissMessage), {}, { allowPartialData: true });
189
+ }, () => applyRestReviewChunk({
190
+ ...context,
191
+ replyIds,
192
+ resolveIds,
193
+ minimizeIds,
194
+ dismissIds,
195
+ message: dismissMessage,
196
+ }), { mutation: true });
197
+ if ("restStopped" in resp)
198
+ restStopped = resp.restStopped === true;
199
+ if ("sessionRefusal" in resp && typeof resp.sessionRefusal === "string")
200
+ result.sessionRefusal = resp.sessionRefusal;
132
201
  data = resp.data;
133
202
  graphQlErrors = (resp.errors ?? []);
134
203
  const graphQlErrorMessages = graphQlErrors.map((e) => e.message);
135
- suppressCurrentChunkErrors = graphQlErrorMessages.some(isRateLimitMessage);
204
+ suppressCurrentChunkErrors = restStopped || graphQlErrorMessages.some(isRateLimitMessage);
136
205
  rateLimitStop = rateLimitFromGraphQlResult(graphQlErrorMessages, {
137
206
  rateLimit: resp.rateLimit,
138
- retryAfterSeconds: resp.retryAfterSeconds,
207
+ retryAfterSeconds: "retryAfterSeconds" in resp ? resp.retryAfterSeconds : undefined,
139
208
  stopOnZeroRemaining: hasPendingAfter,
140
209
  });
141
210
  }
142
211
  catch (err) {
212
+ if (isRestSessionRefusal(err))
213
+ throw err;
214
+ const uncertainIds = err instanceof GitHubRequestError && err.status === 200
215
+ ? uncertainReplyIds(replyIds, [], err.graphqlErrors ?? [])
216
+ : replyIds;
217
+ const ambiguous = isAmbiguousMutationError(err) ||
218
+ (graphqlMutationAttempted &&
219
+ err instanceof GitHubRequestError &&
220
+ (isRetryableGraphQlInternal(err.graphqlErrors) ||
221
+ (err.status === 200 && (!err.graphqlErrors?.length || uncertainIds.length > 0))));
222
+ if (context && graphqlMutationAttempted && ambiguous)
223
+ await rememberUncertainReplies(context, uncertainIds, dismissMessage, replyEvidence);
143
224
  const msg = err instanceof Error ? err.message : String(err);
144
225
  const stop = rateLimitFromError(err, msg);
145
226
  if (stop) {
@@ -155,7 +236,7 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
155
236
  result.errors.push(`${id}: ${msg}`);
156
237
  for (const id of dismissIds)
157
238
  result.errors.push(`${id}: ${msg}`);
158
- return false;
239
+ return ambiguous;
159
240
  }
160
241
  for (let i = 0; i < replyIds.length; i++) {
161
242
  const id = replyIds[i];
@@ -164,21 +245,21 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
164
245
  const p = data[`p${i}`];
165
246
  if (p?.comment?.id)
166
247
  result.repliedThreads.push(id);
167
- else if (!suppressCurrentChunkErrors)
248
+ else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `p${i}`))
168
249
  result.errors.push(`${id}: ${mutationErrorMessage(graphQlErrors, `p${i}`) ?? "reply returned null or comment not created"}`);
169
250
  }
170
251
  for (let i = 0; i < resolveIds.length; i++) {
171
252
  const r = data[`r${i}`];
172
253
  if (r?.thread?.isResolved === true)
173
254
  result.resolvedThreads.push(resolveIds[i]);
174
- else if (!suppressCurrentChunkErrors)
255
+ else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `r${i}`))
175
256
  result.errors.push(`${resolveIds[i]}: ${mutationErrorMessage(graphQlErrors, `r${i}`) ?? "resolve returned null or thread not resolved"}`);
176
257
  }
177
258
  for (let i = 0; i < minimizeIds.length; i++) {
178
259
  const m = data[`m${i}`];
179
260
  if (m?.minimizedComment?.isMinimized === true)
180
261
  result.minimizedComments.push(minimizeIds[i]);
181
- else if (!suppressCurrentChunkErrors)
262
+ else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `m${i}`))
182
263
  result.errors.push(`${minimizeIds[i]}: ${mutationErrorMessage(graphQlErrors, `m${i}`) ?? "minimize returned null or comment not minimized"}`);
183
264
  }
184
265
  const singleDismiss = dismissIds.length === 1;
@@ -198,17 +279,26 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
198
279
  const d = data[`d${i}`];
199
280
  if (d?.pullRequestReview != null)
200
281
  result.dismissedReviews.push(dismissIds[i]);
201
- else if (!suppressCurrentChunkErrors)
282
+ else if (!suppressCurrentChunkErrors || mutationErrorMessage(graphQlErrors, `d${i}`))
202
283
  result.errors.push(commentedDismissErrorIndexes.has(i) || (singleDismiss && hasUnmappedCommentedDismissError)
203
284
  ? dismissReviewNonDismissibleMessage(dismissIds[i])
204
285
  : `${dismissIds[i]}: ${mutationErrorMessage(graphQlErrors, `d${i}`) ?? "dismiss returned null"}`);
205
286
  }
287
+ const internalFailure = graphqlMutationAttempted && isRetryableGraphQlInternal(graphQlErrors);
288
+ const uncertainIds = graphqlMutationAttempted
289
+ ? uncertainReplyIds(replyIds, result.repliedThreads, graphQlErrors)
290
+ : [];
291
+ if (context && uncertainIds.length) {
292
+ await rememberUncertainReplies(context, uncertainIds, dismissMessage, replyEvidence);
293
+ }
206
294
  if (rateLimitStop) {
207
295
  result.errors.push(`rate limit: ${rateLimitStop.message}`);
208
296
  result.rateLimit = rateLimitStop;
209
297
  return true;
210
298
  }
211
- return false;
299
+ if (result.sessionRefusal)
300
+ return true;
301
+ return restStopped || internalFailure || uncertainIds.length > 0;
212
302
  }
213
303
  function dismissErrorAliasIndex(error) {
214
304
  if (!Array.isArray(error.path))
@@ -0,0 +1,9 @@
1
+ import type { RateLimitInfo } from "../github/http-utils.mts";
2
+ import type { RepoInfo } from "../github/client.mts";
3
+ interface ReplyResult {
4
+ id: string;
5
+ rateLimit?: RateLimitInfo;
6
+ }
7
+ export declare function isAmbiguousMutationError(error: unknown): boolean;
8
+ export declare function replyToThread(repo: RepoInfo, pr: number, root: string, body: string): Promise<ReplyResult>;
9
+ export {};
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { resolvePrStatePath } from "../state/base.mjs";
5
+ import { rest, restWithRateLimit } from "../github/http.mjs";
6
+ import { GitHubRequestError } from "../github/errors.mjs";
7
+ import { isTransportError } from "../github/http-utils.mjs";
8
+ import { readRestPages, restRepoPath } from "../github/rest-reader-core.mjs";
9
+ import { isRestSessionRefusal } from "../github/rest-session-refusal.mjs";
10
+ export function isAmbiguousMutationError(error) {
11
+ return isTransportError(error) || (error instanceof GitHubRequestError && error.status >= 500);
12
+ }
13
+ function replyPath(repo, pr, root, body) {
14
+ const hash = createHash("sha256").update(`${root}\n${body}`).digest("hex");
15
+ return resolvePrStatePath({ owner: repo.owner, repo: repo.name, pr }, "pending-replies", `${hash}.json`);
16
+ }
17
+ async function readComments(repo, pr) {
18
+ return readRestPages(`${restRepoPath(repo)}/pulls/${pr}/comments`);
19
+ }
20
+ async function findPendingReply(repo, pr, pending) {
21
+ if (!pending.viewer)
22
+ return undefined;
23
+ const existing = new Set(pending.beforeIds);
24
+ const response = await readComments(repo, pr);
25
+ const matches = response.nodes.filter((comment) => String(comment.in_reply_to_id) === pending.root &&
26
+ comment.body === pending.body &&
27
+ comment.user?.login === pending.viewer &&
28
+ !existing.has(comment.id));
29
+ return matches.length === 1
30
+ ? { id: String(matches[0].id), rateLimit: response.rateLimit }
31
+ : undefined;
32
+ }
33
+ export async function replyToThread(repo, pr, root, body) {
34
+ const path = replyPath(repo, pr, root, body);
35
+ let pending;
36
+ try {
37
+ pending = JSON.parse(await readFile(path, "utf8"));
38
+ }
39
+ catch (error) {
40
+ if (error.code !== "ENOENT")
41
+ throw new Error("Cannot read pending REST reply; cannot safely retry", { cause: error });
42
+ }
43
+ if (pending) {
44
+ if (pending.root !== root || pending.body !== body || !Array.isArray(pending.beforeIds))
45
+ throw new Error("Invalid pending REST reply; cannot safely retry");
46
+ const id = await findPendingReply(repo, pr, pending);
47
+ if (!id)
48
+ throw new Error("Previous reply outcome is uncertain; refresh the thread before retrying, no duplicate reply was sent");
49
+ await unlink(path);
50
+ return id;
51
+ }
52
+ // These reads provide the exact author and pre-write IDs needed after a lost response.
53
+ let viewer;
54
+ try {
55
+ viewer = (await rest("GET", "/user")).login;
56
+ }
57
+ catch (error) {
58
+ if (isRestSessionRefusal(error))
59
+ throw error;
60
+ if (!(error instanceof GitHubRequestError) || ![403, 404].includes(error.status))
61
+ throw error;
62
+ }
63
+ const comments = await readComments(repo, pr);
64
+ pending = {
65
+ root,
66
+ body,
67
+ ...(viewer && { viewer }),
68
+ beforeIds: comments.nodes.map((comment) => comment.id),
69
+ };
70
+ await mkdir(dirname(path), { recursive: true });
71
+ await writeFile(path, JSON.stringify(pending), { flag: "wx" });
72
+ try {
73
+ const result = await restWithRateLimit("POST", `${restRepoPath(repo)}/pulls/${pr}/comments/${root}/replies`, { body });
74
+ if (!Number.isSafeInteger(result.data?.id) || result.data.id <= 0)
75
+ throw new Error("Reply returned no valid comment ID; outcome is uncertain");
76
+ await unlink(path);
77
+ return { id: String(result.data.id), rateLimit: result.rateLimit };
78
+ }
79
+ catch (error) {
80
+ if (isAmbiguousMutationError(error)) {
81
+ try {
82
+ const id = await findPendingReply(repo, pr, pending);
83
+ if (id) {
84
+ await unlink(path);
85
+ return id;
86
+ }
87
+ }
88
+ catch { }
89
+ }
90
+ else if (error instanceof GitHubRequestError && error.status >= 400 && error.status < 500) {
91
+ await unlink(path);
92
+ }
93
+ throw error;
94
+ }
95
+ }
@@ -0,0 +1,22 @@
1
+ import type { RepoInfo } from "../github/client.mts";
2
+ import type { RateLimitInfo } from "../github/http-utils.mts";
3
+ export declare function applyRestReviewChunk(input: {
4
+ repo?: RepoInfo;
5
+ pr?: number;
6
+ replyIds: string[];
7
+ resolveIds: string[];
8
+ minimizeIds: string[];
9
+ dismissIds: string[];
10
+ message: string;
11
+ }): Promise<{
12
+ data: Record<string, unknown>;
13
+ errors: Array<{
14
+ message: string;
15
+ path: string[];
16
+ }>;
17
+ rateLimit?: RateLimitInfo;
18
+ retryAfterSeconds?: number;
19
+ restStopped?: true;
20
+ sessionRefusal?: string;
21
+ }>;
22
+ export { isAmbiguousMutationError } from "./rest-reply.mts";
@@ -0,0 +1,125 @@
1
+ import { replyToThread, isAmbiguousMutationError } from "./rest-reply.mjs";
2
+ import { restWithRateLimit } from "../github/http.mjs";
3
+ import { GitHubRequestError } from "../github/errors.mjs";
4
+ import { isCcrTransport } from "../github/transport.mjs";
5
+ import { UnsupportedRestOperationError } from "../github/unsupported-rest.mjs";
6
+ import { resolveRestIdentity, resolveRestThreadRoot } from "../github/rest-identities.mjs";
7
+ import { readRestPages, restRepoPath } from "../github/rest-reader-core.mjs";
8
+ import { addPrShepherdMarker } from "./marker.mjs";
9
+ import { rateLimitFromError } from "./rate-limit.mjs";
10
+ import { markMutationDenied } from "../state/seen-comments.mjs";
11
+ import { readRestFeedback } from "../github/rest-feedback-read.mjs";
12
+ import { threadTranscriptBody } from "../threads/transcript.mjs";
13
+ import { isRestSessionRefusal } from "../github/rest-session-refusal.mjs";
14
+ async function reviewNumericId(repo, pr, id) {
15
+ if (/^[1-9][0-9]*$/.test(id))
16
+ return id;
17
+ try {
18
+ const entry = await resolveRestIdentity(id, "review");
19
+ if (entry.pr === pr &&
20
+ entry.repo.owner.toLowerCase() === repo.owner.toLowerCase() &&
21
+ entry.repo.name.toLowerCase() === repo.name.toLowerCase())
22
+ return entry.numericId;
23
+ }
24
+ catch { }
25
+ const reviews = (await readRestPages(`${restRepoPath(repo)}/pulls/${pr}/reviews`)).nodes;
26
+ const target = reviews.find((review) => review.node_id === id);
27
+ if (!target)
28
+ throw new Error(`Cannot address review ${id} through REST in ${repo.owner}/${repo.name}#${pr}`);
29
+ return String(target.id);
30
+ }
31
+ async function rememberDenied(input, id, kind) {
32
+ let key;
33
+ try {
34
+ const identity = input.repo && input.pr !== undefined ? undefined : await resolveRestIdentity(id);
35
+ const repo = input.repo ?? identity.repo;
36
+ const pr = input.pr ?? identity.pr;
37
+ key = { owner: repo.owner, repo: repo.name, pr };
38
+ const feedback = await readRestFeedback(pr, repo);
39
+ const root = kind === "dismiss" ? undefined : await resolveRestThreadRoot(repo, pr, id);
40
+ const body = kind === "dismiss"
41
+ ? feedback.reviews.find((review) => review.node_id === id || String(review.id) === id)?.body
42
+ : feedback.threads.find((thread) => thread.id === id || thread.id === `rest-thread-${root}`);
43
+ if (body !== undefined && body !== null)
44
+ await markMutationDenied(key, id, typeof body === "string" ? body : threadTranscriptBody(body));
45
+ }
46
+ catch {
47
+ // Generated mutations already surfaced this revision; a failed refresh must not retry it.
48
+ if (key)
49
+ await markMutationDenied(key, id);
50
+ }
51
+ }
52
+ export async function applyRestReviewChunk(input) {
53
+ const result = { data: {}, errors: [] };
54
+ const ops = [
55
+ ...input.replyIds.map((id, index) => ({ id, alias: `p${index}`, kind: "reply" })),
56
+ ...input.resolveIds.map((id, index) => ({ id, alias: `r${index}`, kind: "resolve" })),
57
+ ...input.minimizeIds.map((id, index) => ({ id, alias: `m${index}`, kind: "minimize" })),
58
+ ...input.dismissIds.map((id, index) => ({ id, alias: `d${index}`, kind: "dismiss" })),
59
+ ];
60
+ for (const op of ops) {
61
+ try {
62
+ if (op.kind === "minimize")
63
+ throw new UnsupportedRestOperationError("minimizeComment");
64
+ let repo = input.repo;
65
+ let pr = input.pr;
66
+ if (!repo || pr === undefined) {
67
+ const identity = await resolveRestIdentity(op.id, op.kind === "dismiss" ? "review" : "thread");
68
+ repo = identity.repo;
69
+ pr = identity.pr;
70
+ }
71
+ const prefix = `${restRepoPath(repo)}/pulls/${pr}`;
72
+ if (op.kind === "reply") {
73
+ const root = await resolveRestThreadRoot(repo, pr, op.id);
74
+ const response = await replyToThread(repo, pr, root, addPrShepherdMarker(input.message));
75
+ result.rateLimit = response.rateLimit;
76
+ result.data[op.alias] = { comment: { id: response.id } };
77
+ }
78
+ else if (op.kind === "resolve") {
79
+ if (!isCcrTransport())
80
+ throw new UnsupportedRestOperationError("resolveReviewThread", "thread resolution requires the cloud CCR proxy");
81
+ const root = await resolveRestThreadRoot(repo, pr, op.id);
82
+ const response = await restWithRateLimit("POST", `${prefix}/ccr/comments/${root}/resolve`);
83
+ result.rateLimit = response.rateLimit;
84
+ if (response.data?.resolved !== true)
85
+ throw new Error("CCR resolve did not confirm the thread was resolved");
86
+ result.data[op.alias] = { thread: { isResolved: true } };
87
+ }
88
+ else {
89
+ const review = await reviewNumericId(repo, pr, op.id);
90
+ const response = await restWithRateLimit("PUT", `${prefix}/reviews/${review}/dismissals`, { message: input.message });
91
+ result.rateLimit = response.rateLimit;
92
+ if (response.data?.state !== "DISMISSED")
93
+ throw new Error("Review dismissal did not confirm DISMISSED state");
94
+ result.data[op.alias] = { pullRequestReview: { state: "DISMISSED" } };
95
+ }
96
+ if (result.rateLimit?.remaining === 0) {
97
+ result.errors.push({ message: "GitHub REST core rate limit remaining is 0", path: [] });
98
+ result.restStopped = true;
99
+ break;
100
+ }
101
+ }
102
+ catch (error) {
103
+ if (isRestSessionRefusal(error)) {
104
+ result.sessionRefusal = error.message;
105
+ result.restStopped = true;
106
+ break;
107
+ }
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ result.errors.push({ message, path: [op.alias] });
110
+ const stop = rateLimitFromError(error, message);
111
+ if (!stop && error instanceof GitHubRequestError && [403, 404].includes(error.status))
112
+ await rememberDenied(input, op.id, op.kind);
113
+ if (error instanceof GitHubRequestError) {
114
+ result.rateLimit = error.rateLimit;
115
+ result.retryAfterSeconds = error.retryAfterSeconds;
116
+ }
117
+ if (stop || isAmbiguousMutationError(error) || message.includes("outcome is uncertain")) {
118
+ result.restStopped = true;
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ return result;
124
+ }
125
+ export { isAmbiguousMutationError } from "./rest-reply.mjs";
@@ -24,5 +24,5 @@ export declare function classifyReviewsForDisplay(reviews: Review[], seenMap: Ma
24
24
  *
25
25
  * Human-authored CR reviews continue to flow through the standard seen-gate.
26
26
  */
27
- export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames, repeatBotReviews?: boolean): ReviewVisibility;
27
+ export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames, repeatBotReviews?: boolean, deniedDismissalIds?: ReadonlySet<string>): ReviewVisibility;
28
28
  export {};