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
@@ -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 {};
@@ -31,7 +31,7 @@ export function classifyReviewsForDisplay(reviews, seenMap) {
31
31
  *
32
32
  * Human-authored CR reviews continue to flow through the standard seen-gate.
33
33
  */
34
- export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botUsernames, repeatBotReviews = true) {
34
+ export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botUsernames, repeatBotReviews = true, deniedDismissalIds = new Set()) {
35
35
  const visible = [];
36
36
  const toMarkSeen = [];
37
37
  for (const review of reviews) {
@@ -39,8 +39,9 @@ export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botU
39
39
  const cls = classifyItem(review.id, review.body, seenMap);
40
40
  if (isBot) {
41
41
  if (cls === "unchanged") {
42
- if (repeatBotReviews)
42
+ if (repeatBotReviews && !deniedDismissalIds.has(review.id)) {
43
43
  visible.push({ ...review, staleBotCr: true });
44
+ }
44
45
  }
45
46
  else if (cls === "edited") {
46
47
  visible.push({ ...review, edited: true });
@@ -3,11 +3,12 @@ import { loadConfig } from "../config/load.mjs";
3
3
  import { sleep } from "../util/sleep.mjs";
4
4
  export async function waitForSha(pr, repo, expectedSha) {
5
5
  const { intervalMs: SHA_POLL_INTERVAL_MS, maxAttempts: SHA_POLL_MAX_ATTEMPTS } = loadConfig().resolve.shaPoll;
6
+ let lastSha;
6
7
  for (let attempt = 0; attempt < SHA_POLL_MAX_ATTEMPTS; attempt++) {
7
8
  try {
8
9
  // eslint-disable-next-line no-await-in-loop
9
- const currentSha = await getPrHeadSha(pr, repo.owner, repo.name);
10
- if (currentSha === expectedSha)
10
+ lastSha = await getPrHeadSha(pr, repo.owner, repo.name);
11
+ if (lastSha === expectedSha)
11
12
  return;
12
13
  }
13
14
  catch (err) {
@@ -19,5 +20,8 @@ export async function waitForSha(pr, repo, expectedSha) {
19
20
  await sleep(SHA_POLL_INTERVAL_MS);
20
21
  }
21
22
  }
22
- throw new Error(`Timeout: GitHub PR #${pr} head SHA has not updated to ${expectedSha} after ${((SHA_POLL_MAX_ATTEMPTS - 1) * SHA_POLL_INTERVAL_MS) / 1000}s. Push may still be in transit — retry shortly.`);
23
+ // Iterate prints a `--require-sha` that reads the current checkout's HEAD. Name that cause,
24
+ // so a run from another checkout can recover from this error alone.
25
+ const current = lastSha ? ` (currently ${lastSha})` : "";
26
+ throw new Error(`Timeout: GitHub PR #${pr} head SHA has not updated to ${expectedSha} after ${((SHA_POLL_MAX_ATTEMPTS - 1) * SHA_POLL_INTERVAL_MS) / 1000}s. Push may still be in transit — retry shortly. If this command ran outside the PR head checkout, rerun it with \`--require-sha\` set to the PR head SHA you pushed${current}.`);
23
27
  }
@@ -23,9 +23,10 @@ function classifyFirstLookThread(thread, seenMap, firstLookStatus) {
23
23
  export function classifyThreadVisibility(threads, seenMap, botUsernames = new Set(), repeatableThreadIds, resolveOtherHumanThreads = "none") {
24
24
  const shouldRepeat = (thread) => repeatableThreadIds?.has(thread.id) ?? true;
25
25
  const isOrdinaryHuman = (thread) => isHumanAuthor(thread) && !isConfiguredBotAuthor(thread, botUsernames);
26
- const unresolvedThreads = threads.filter((t) => !t.isResolved);
26
+ const unknownStatusThreads = threads.filter((thread) => thread.isResolved === undefined || thread.isOutdated === undefined);
27
+ const unresolvedThreads = threads.filter((thread) => thread.isResolved === false && thread.isOutdated !== undefined);
27
28
  const activeThreads = unresolvedThreads
28
- .filter((t) => !t.isOutdated && !t.isMinimized)
29
+ .filter((t) => t.isOutdated === false && t.isMinimized !== true)
29
30
  .flatMap((t) => {
30
31
  if (threadEndedByShepherd(t))
31
32
  return [];
@@ -56,23 +57,27 @@ export function classifyThreadVisibility(threads, seenMap, botUsernames = new Se
56
57
  });
57
58
  const firstLookThreads = [
58
59
  ...threads.flatMap((t) => {
59
- if (!t.isOutdated)
60
+ if (t.isOutdated !== true || t.isResolved === undefined)
60
61
  return [];
61
62
  const visible = classifyFirstLookThread(t, seenMap, "outdated");
62
63
  return visible ? [visible] : [];
63
64
  }),
64
65
  ...threads.flatMap((t) => {
65
- if (!t.isResolved || t.isOutdated)
66
+ if (t.isResolved !== true || t.isOutdated !== false)
66
67
  return [];
67
68
  const visible = classifyFirstLookThread(t, seenMap, "resolved");
68
69
  return visible ? [visible] : [];
69
70
  }),
70
71
  ...threads.flatMap((t) => {
71
- if (!t.isMinimized || t.isResolved || t.isOutdated)
72
+ if (!t.isMinimized || t.isResolved !== false || t.isOutdated !== false)
72
73
  return [];
73
74
  const visible = classifyFirstLookThread(t, seenMap, "minimized");
74
75
  return visible ? [visible] : [];
75
76
  }),
77
+ ...unknownStatusThreads.flatMap((thread) => {
78
+ const visible = classifyFirstLookThread(thread, seenMap, "unknown");
79
+ return visible ? [visible] : [];
80
+ }),
76
81
  ];
77
82
  const seenMarkerIds = new Set();
78
83
  const toMarkSeen = [...activeThreads, ...resolutionOnlyThreads, ...firstLookThreads].filter((thread) => {
@@ -0,0 +1,11 @@
1
+ import type { RepoInfo } from "../github/client.mts";
2
+ import { type ReplyEvidence } from "../github/reply-recovery-read.mts";
3
+ interface ReplyContext {
4
+ repo: RepoInfo;
5
+ pr: number;
6
+ }
7
+ export declare function trackAdoptedReplyThreads(result: object, ids: readonly string[]): void;
8
+ export declare function adoptedReplyThreads(result: object): readonly string[];
9
+ export declare function assertReplyOutcomeKnown(context: ReplyContext, ids: string[], message: string): Promise<string[]>;
10
+ export declare function rememberUncertainReplies(context: ReplyContext, ids: string[], message: string, evidence?: Map<string, ReplyEvidence>): Promise<void>;
11
+ export {};