@humanbased/crosscheck 0.14.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 (381) hide show
  1. package/AGENT.md +207 -0
  2. package/ISSUE.md +234 -0
  3. package/LICENSE +21 -0
  4. package/README.md +234 -0
  5. package/README.zh.md +169 -0
  6. package/assets/logo.png +0 -0
  7. package/assets/screenshot-watch-timing.png +0 -0
  8. package/assets/screenshot-watch-timing.svg +1 -0
  9. package/assets/screenshot-watch.png +0 -0
  10. package/crosscheck.config.example.yml +214 -0
  11. package/dist/__tests__/annotation.test.d.ts +2 -0
  12. package/dist/__tests__/annotation.test.d.ts.map +1 -0
  13. package/dist/__tests__/annotation.test.js +134 -0
  14. package/dist/__tests__/annotation.test.js.map +1 -0
  15. package/dist/__tests__/backtrace.test.d.ts +2 -0
  16. package/dist/__tests__/backtrace.test.d.ts.map +1 -0
  17. package/dist/__tests__/backtrace.test.js +280 -0
  18. package/dist/__tests__/backtrace.test.js.map +1 -0
  19. package/dist/__tests__/board.test.d.ts +2 -0
  20. package/dist/__tests__/board.test.d.ts.map +1 -0
  21. package/dist/__tests__/board.test.js +149 -0
  22. package/dist/__tests__/board.test.js.map +1 -0
  23. package/dist/__tests__/codex.test.d.ts +2 -0
  24. package/dist/__tests__/codex.test.d.ts.map +1 -0
  25. package/dist/__tests__/codex.test.js +92 -0
  26. package/dist/__tests__/codex.test.js.map +1 -0
  27. package/dist/__tests__/comment-bodies.test.d.ts +2 -0
  28. package/dist/__tests__/comment-bodies.test.d.ts.map +1 -0
  29. package/dist/__tests__/comment-bodies.test.js +75 -0
  30. package/dist/__tests__/comment-bodies.test.js.map +1 -0
  31. package/dist/__tests__/conflict-resolve.test.d.ts +2 -0
  32. package/dist/__tests__/conflict-resolve.test.d.ts.map +1 -0
  33. package/dist/__tests__/conflict-resolve.test.js +123 -0
  34. package/dist/__tests__/conflict-resolve.test.js.map +1 -0
  35. package/dist/__tests__/crosscheck-commit.test.d.ts +2 -0
  36. package/dist/__tests__/crosscheck-commit.test.d.ts.map +1 -0
  37. package/dist/__tests__/crosscheck-commit.test.js +13 -0
  38. package/dist/__tests__/crosscheck-commit.test.js.map +1 -0
  39. package/dist/__tests__/detector.test.d.ts +2 -0
  40. package/dist/__tests__/detector.test.d.ts.map +1 -0
  41. package/dist/__tests__/detector.test.js +112 -0
  42. package/dist/__tests__/detector.test.js.map +1 -0
  43. package/dist/__tests__/diagnose.test.d.ts +2 -0
  44. package/dist/__tests__/diagnose.test.d.ts.map +1 -0
  45. package/dist/__tests__/diagnose.test.js +164 -0
  46. package/dist/__tests__/diagnose.test.js.map +1 -0
  47. package/dist/__tests__/diff-hash.test.d.ts +2 -0
  48. package/dist/__tests__/diff-hash.test.d.ts.map +1 -0
  49. package/dist/__tests__/diff-hash.test.js +126 -0
  50. package/dist/__tests__/diff-hash.test.js.map +1 -0
  51. package/dist/__tests__/durations.test.d.ts +2 -0
  52. package/dist/__tests__/durations.test.d.ts.map +1 -0
  53. package/dist/__tests__/durations.test.js +26 -0
  54. package/dist/__tests__/durations.test.js.map +1 -0
  55. package/dist/__tests__/event-fields.test.d.ts +2 -0
  56. package/dist/__tests__/event-fields.test.d.ts.map +1 -0
  57. package/dist/__tests__/event-fields.test.js +50 -0
  58. package/dist/__tests__/event-fields.test.js.map +1 -0
  59. package/dist/__tests__/filter.test.d.ts +2 -0
  60. package/dist/__tests__/filter.test.d.ts.map +1 -0
  61. package/dist/__tests__/filter.test.js +21 -0
  62. package/dist/__tests__/filter.test.js.map +1 -0
  63. package/dist/__tests__/fix.test.d.ts +2 -0
  64. package/dist/__tests__/fix.test.d.ts.map +1 -0
  65. package/dist/__tests__/fix.test.js +124 -0
  66. package/dist/__tests__/fix.test.js.map +1 -0
  67. package/dist/__tests__/github-client.test.d.ts +2 -0
  68. package/dist/__tests__/github-client.test.d.ts.map +1 -0
  69. package/dist/__tests__/github-client.test.js +22 -0
  70. package/dist/__tests__/github-client.test.js.map +1 -0
  71. package/dist/__tests__/github-scan-client.test.d.ts +2 -0
  72. package/dist/__tests__/github-scan-client.test.d.ts.map +1 -0
  73. package/dist/__tests__/github-scan-client.test.js +100 -0
  74. package/dist/__tests__/github-scan-client.test.js.map +1 -0
  75. package/dist/__tests__/is-fresh-review-comment.test.d.ts +2 -0
  76. package/dist/__tests__/is-fresh-review-comment.test.d.ts.map +1 -0
  77. package/dist/__tests__/is-fresh-review-comment.test.js +86 -0
  78. package/dist/__tests__/is-fresh-review-comment.test.js.map +1 -0
  79. package/dist/__tests__/issue.test.d.ts +2 -0
  80. package/dist/__tests__/issue.test.d.ts.map +1 -0
  81. package/dist/__tests__/issue.test.js +259 -0
  82. package/dist/__tests__/issue.test.js.map +1 -0
  83. package/dist/__tests__/kickass.test.d.ts +2 -0
  84. package/dist/__tests__/kickass.test.d.ts.map +1 -0
  85. package/dist/__tests__/kickass.test.js +268 -0
  86. package/dist/__tests__/kickass.test.js.map +1 -0
  87. package/dist/__tests__/loader.test.d.ts +2 -0
  88. package/dist/__tests__/loader.test.d.ts.map +1 -0
  89. package/dist/__tests__/loader.test.js +180 -0
  90. package/dist/__tests__/loader.test.js.map +1 -0
  91. package/dist/__tests__/onboard-preservation.test.d.ts +2 -0
  92. package/dist/__tests__/onboard-preservation.test.d.ts.map +1 -0
  93. package/dist/__tests__/onboard-preservation.test.js +506 -0
  94. package/dist/__tests__/onboard-preservation.test.js.map +1 -0
  95. package/dist/__tests__/optimize.test.d.ts +2 -0
  96. package/dist/__tests__/optimize.test.d.ts.map +1 -0
  97. package/dist/__tests__/optimize.test.js +101 -0
  98. package/dist/__tests__/optimize.test.js.map +1 -0
  99. package/dist/__tests__/post-review-comment.test.d.ts +2 -0
  100. package/dist/__tests__/post-review-comment.test.d.ts.map +1 -0
  101. package/dist/__tests__/post-review-comment.test.js +44 -0
  102. package/dist/__tests__/post-review-comment.test.js.map +1 -0
  103. package/dist/__tests__/pr-lock.test.d.ts +2 -0
  104. package/dist/__tests__/pr-lock.test.d.ts.map +1 -0
  105. package/dist/__tests__/pr-lock.test.js +115 -0
  106. package/dist/__tests__/pr-lock.test.js.map +1 -0
  107. package/dist/__tests__/pr-picker.test.d.ts +2 -0
  108. package/dist/__tests__/pr-picker.test.d.ts.map +1 -0
  109. package/dist/__tests__/pr-picker.test.js +57 -0
  110. package/dist/__tests__/pr-picker.test.js.map +1 -0
  111. package/dist/__tests__/pr-status-scan.test.d.ts +2 -0
  112. package/dist/__tests__/pr-status-scan.test.d.ts.map +1 -0
  113. package/dist/__tests__/pr-status-scan.test.js +92 -0
  114. package/dist/__tests__/pr-status-scan.test.js.map +1 -0
  115. package/dist/__tests__/pr-status.test.d.ts +2 -0
  116. package/dist/__tests__/pr-status.test.d.ts.map +1 -0
  117. package/dist/__tests__/pr-status.test.js +346 -0
  118. package/dist/__tests__/pr-status.test.js.map +1 -0
  119. package/dist/__tests__/repo-picker.test.d.ts +2 -0
  120. package/dist/__tests__/repo-picker.test.d.ts.map +1 -0
  121. package/dist/__tests__/repo-picker.test.js +115 -0
  122. package/dist/__tests__/repo-picker.test.js.map +1 -0
  123. package/dist/__tests__/review-comment-body.test.d.ts +2 -0
  124. package/dist/__tests__/review-comment-body.test.d.ts.map +1 -0
  125. package/dist/__tests__/review-comment-body.test.js +54 -0
  126. package/dist/__tests__/review-comment-body.test.js.map +1 -0
  127. package/dist/__tests__/review-models.test.d.ts +2 -0
  128. package/dist/__tests__/review-models.test.d.ts.map +1 -0
  129. package/dist/__tests__/review-models.test.js +39 -0
  130. package/dist/__tests__/review-models.test.js.map +1 -0
  131. package/dist/__tests__/review-status.test.d.ts +2 -0
  132. package/dist/__tests__/review-status.test.d.ts.map +1 -0
  133. package/dist/__tests__/review-status.test.js +95 -0
  134. package/dist/__tests__/review-status.test.js.map +1 -0
  135. package/dist/__tests__/runner.test.d.ts +2 -0
  136. package/dist/__tests__/runner.test.d.ts.map +1 -0
  137. package/dist/__tests__/runner.test.js +204 -0
  138. package/dist/__tests__/runner.test.js.map +1 -0
  139. package/dist/__tests__/scan-cache.test.d.ts +2 -0
  140. package/dist/__tests__/scan-cache.test.d.ts.map +1 -0
  141. package/dist/__tests__/scan-cache.test.js +59 -0
  142. package/dist/__tests__/scan-cache.test.js.map +1 -0
  143. package/dist/__tests__/scan-client.test.d.ts +2 -0
  144. package/dist/__tests__/scan-client.test.d.ts.map +1 -0
  145. package/dist/__tests__/scan-client.test.js +30 -0
  146. package/dist/__tests__/scan-client.test.js.map +1 -0
  147. package/dist/__tests__/scan.test.d.ts +2 -0
  148. package/dist/__tests__/scan.test.d.ts.map +1 -0
  149. package/dist/__tests__/scan.test.js +115 -0
  150. package/dist/__tests__/scan.test.js.map +1 -0
  151. package/dist/__tests__/scopes.test.d.ts +2 -0
  152. package/dist/__tests__/scopes.test.d.ts.map +1 -0
  153. package/dist/__tests__/scopes.test.js +101 -0
  154. package/dist/__tests__/scopes.test.js.map +1 -0
  155. package/dist/__tests__/sha-cache.test.d.ts +2 -0
  156. package/dist/__tests__/sha-cache.test.d.ts.map +1 -0
  157. package/dist/__tests__/sha-cache.test.js +40 -0
  158. package/dist/__tests__/sha-cache.test.js.map +1 -0
  159. package/dist/__tests__/smart-switch.test.d.ts +2 -0
  160. package/dist/__tests__/smart-switch.test.d.ts.map +1 -0
  161. package/dist/__tests__/smart-switch.test.js +145 -0
  162. package/dist/__tests__/smart-switch.test.js.map +1 -0
  163. package/dist/ck.d.ts +3 -0
  164. package/dist/ck.d.ts.map +1 -0
  165. package/dist/ck.js +8 -0
  166. package/dist/ck.js.map +1 -0
  167. package/dist/cli.d.ts +3 -0
  168. package/dist/cli.d.ts.map +1 -0
  169. package/dist/cli.js +132 -0
  170. package/dist/cli.js.map +1 -0
  171. package/dist/commands/diagnose.d.ts +54 -0
  172. package/dist/commands/diagnose.d.ts.map +1 -0
  173. package/dist/commands/diagnose.js +294 -0
  174. package/dist/commands/diagnose.js.map +1 -0
  175. package/dist/commands/impact.d.ts +38 -0
  176. package/dist/commands/impact.d.ts.map +1 -0
  177. package/dist/commands/impact.js +210 -0
  178. package/dist/commands/impact.js.map +1 -0
  179. package/dist/commands/init.d.ts +12 -0
  180. package/dist/commands/init.d.ts.map +1 -0
  181. package/dist/commands/init.js +183 -0
  182. package/dist/commands/init.js.map +1 -0
  183. package/dist/commands/issue.d.ts +25 -0
  184. package/dist/commands/issue.d.ts.map +1 -0
  185. package/dist/commands/issue.js +445 -0
  186. package/dist/commands/issue.js.map +1 -0
  187. package/dist/commands/kickass.d.ts +59 -0
  188. package/dist/commands/kickass.d.ts.map +1 -0
  189. package/dist/commands/kickass.js +288 -0
  190. package/dist/commands/kickass.js.map +1 -0
  191. package/dist/commands/onboard.d.ts +70 -0
  192. package/dist/commands/onboard.d.ts.map +1 -0
  193. package/dist/commands/onboard.js +883 -0
  194. package/dist/commands/onboard.js.map +1 -0
  195. package/dist/commands/optimize.d.ts +16 -0
  196. package/dist/commands/optimize.d.ts.map +1 -0
  197. package/dist/commands/optimize.js +244 -0
  198. package/dist/commands/optimize.js.map +1 -0
  199. package/dist/commands/review.d.ts +2 -0
  200. package/dist/commands/review.d.ts.map +1 -0
  201. package/dist/commands/review.js +118 -0
  202. package/dist/commands/review.js.map +1 -0
  203. package/dist/commands/run.d.ts +13 -0
  204. package/dist/commands/run.d.ts.map +1 -0
  205. package/dist/commands/run.js +243 -0
  206. package/dist/commands/run.js.map +1 -0
  207. package/dist/commands/scan.d.ts +94 -0
  208. package/dist/commands/scan.d.ts.map +1 -0
  209. package/dist/commands/scan.js +276 -0
  210. package/dist/commands/scan.js.map +1 -0
  211. package/dist/commands/serve.d.ts +9 -0
  212. package/dist/commands/serve.d.ts.map +1 -0
  213. package/dist/commands/serve.js +402 -0
  214. package/dist/commands/serve.js.map +1 -0
  215. package/dist/commands/status.d.ts +2 -0
  216. package/dist/commands/status.d.ts.map +1 -0
  217. package/dist/commands/status.js +89 -0
  218. package/dist/commands/status.js.map +1 -0
  219. package/dist/commands/watch.d.ts +9 -0
  220. package/dist/commands/watch.d.ts.map +1 -0
  221. package/dist/commands/watch.js +902 -0
  222. package/dist/commands/watch.js.map +1 -0
  223. package/dist/config/loader.d.ts +47 -0
  224. package/dist/config/loader.d.ts.map +1 -0
  225. package/dist/config/loader.js +334 -0
  226. package/dist/config/loader.js.map +1 -0
  227. package/dist/config/schema.d.ts +814 -0
  228. package/dist/config/schema.d.ts.map +1 -0
  229. package/dist/config/schema.js +152 -0
  230. package/dist/config/schema.js.map +1 -0
  231. package/dist/github/client.d.ts +139 -0
  232. package/dist/github/client.d.ts.map +1 -0
  233. package/dist/github/client.js +711 -0
  234. package/dist/github/client.js.map +1 -0
  235. package/dist/github/detector.d.ts +12 -0
  236. package/dist/github/detector.d.ts.map +1 -0
  237. package/dist/github/detector.js +120 -0
  238. package/dist/github/detector.js.map +1 -0
  239. package/dist/github/merge.d.ts +9 -0
  240. package/dist/github/merge.d.ts.map +1 -0
  241. package/dist/github/merge.js +33 -0
  242. package/dist/github/merge.js.map +1 -0
  243. package/dist/github/review-status.d.ts +6 -0
  244. package/dist/github/review-status.d.ts.map +1 -0
  245. package/dist/github/review-status.js +51 -0
  246. package/dist/github/review-status.js.map +1 -0
  247. package/dist/github/webhook.d.ts +41 -0
  248. package/dist/github/webhook.d.ts.map +1 -0
  249. package/dist/github/webhook.js +50 -0
  250. package/dist/github/webhook.js.map +1 -0
  251. package/dist/lib/annotation.d.ts +23 -0
  252. package/dist/lib/annotation.d.ts.map +1 -0
  253. package/dist/lib/annotation.js +103 -0
  254. package/dist/lib/annotation.js.map +1 -0
  255. package/dist/lib/backtrace.d.ts +40 -0
  256. package/dist/lib/backtrace.d.ts.map +1 -0
  257. package/dist/lib/backtrace.js +169 -0
  258. package/dist/lib/backtrace.js.map +1 -0
  259. package/dist/lib/board.d.ts +74 -0
  260. package/dist/lib/board.d.ts.map +1 -0
  261. package/dist/lib/board.js +640 -0
  262. package/dist/lib/board.js.map +1 -0
  263. package/dist/lib/clone.d.ts +12 -0
  264. package/dist/lib/clone.d.ts.map +1 -0
  265. package/dist/lib/clone.js +30 -0
  266. package/dist/lib/clone.js.map +1 -0
  267. package/dist/lib/comment-bodies.d.ts +17 -0
  268. package/dist/lib/comment-bodies.d.ts.map +1 -0
  269. package/dist/lib/comment-bodies.js +51 -0
  270. package/dist/lib/comment-bodies.js.map +1 -0
  271. package/dist/lib/crosscheck-commit.d.ts +2 -0
  272. package/dist/lib/crosscheck-commit.d.ts.map +1 -0
  273. package/dist/lib/crosscheck-commit.js +4 -0
  274. package/dist/lib/crosscheck-commit.js.map +1 -0
  275. package/dist/lib/diff-hash.d.ts +16 -0
  276. package/dist/lib/diff-hash.d.ts.map +1 -0
  277. package/dist/lib/diff-hash.js +71 -0
  278. package/dist/lib/diff-hash.js.map +1 -0
  279. package/dist/lib/durations.d.ts +5 -0
  280. package/dist/lib/durations.d.ts.map +1 -0
  281. package/dist/lib/durations.js +39 -0
  282. package/dist/lib/durations.js.map +1 -0
  283. package/dist/lib/event-fields.d.ts +6 -0
  284. package/dist/lib/event-fields.d.ts.map +1 -0
  285. package/dist/lib/event-fields.js +20 -0
  286. package/dist/lib/event-fields.js.map +1 -0
  287. package/dist/lib/filter.d.ts +2 -0
  288. package/dist/lib/filter.d.ts.map +1 -0
  289. package/dist/lib/filter.js +4 -0
  290. package/dist/lib/filter.js.map +1 -0
  291. package/dist/lib/fortune.d.ts +2 -0
  292. package/dist/lib/fortune.d.ts.map +1 -0
  293. package/dist/lib/fortune.js +26 -0
  294. package/dist/lib/fortune.js.map +1 -0
  295. package/dist/lib/languages.d.ts +3 -0
  296. package/dist/lib/languages.d.ts.map +1 -0
  297. package/dist/lib/languages.js +26 -0
  298. package/dist/lib/languages.js.map +1 -0
  299. package/dist/lib/log-analysis.d.ts +17 -0
  300. package/dist/lib/log-analysis.d.ts.map +1 -0
  301. package/dist/lib/log-analysis.js +72 -0
  302. package/dist/lib/log-analysis.js.map +1 -0
  303. package/dist/lib/logger.d.ts +14 -0
  304. package/dist/lib/logger.d.ts.map +1 -0
  305. package/dist/lib/logger.js +84 -0
  306. package/dist/lib/logger.js.map +1 -0
  307. package/dist/lib/port.d.ts +2 -0
  308. package/dist/lib/port.d.ts.map +1 -0
  309. package/dist/lib/port.js +21 -0
  310. package/dist/lib/port.js.map +1 -0
  311. package/dist/lib/pr-lock.d.ts +4 -0
  312. package/dist/lib/pr-lock.d.ts.map +1 -0
  313. package/dist/lib/pr-lock.js +91 -0
  314. package/dist/lib/pr-lock.js.map +1 -0
  315. package/dist/lib/pr-picker.d.ts +10 -0
  316. package/dist/lib/pr-picker.d.ts.map +1 -0
  317. package/dist/lib/pr-picker.js +80 -0
  318. package/dist/lib/pr-picker.js.map +1 -0
  319. package/dist/lib/pr-status.d.ts +206 -0
  320. package/dist/lib/pr-status.d.ts.map +1 -0
  321. package/dist/lib/pr-status.js +613 -0
  322. package/dist/lib/pr-status.js.map +1 -0
  323. package/dist/lib/repo-picker.d.ts +23 -0
  324. package/dist/lib/repo-picker.d.ts.map +1 -0
  325. package/dist/lib/repo-picker.js +411 -0
  326. package/dist/lib/repo-picker.js.map +1 -0
  327. package/dist/lib/review-models.d.ts +7 -0
  328. package/dist/lib/review-models.d.ts.map +1 -0
  329. package/dist/lib/review-models.js +32 -0
  330. package/dist/lib/review-models.js.map +1 -0
  331. package/dist/lib/runner.d.ts +65 -0
  332. package/dist/lib/runner.d.ts.map +1 -0
  333. package/dist/lib/runner.js +710 -0
  334. package/dist/lib/runner.js.map +1 -0
  335. package/dist/lib/scan-cache.d.ts +31 -0
  336. package/dist/lib/scan-cache.d.ts.map +1 -0
  337. package/dist/lib/scan-cache.js +112 -0
  338. package/dist/lib/scan-cache.js.map +1 -0
  339. package/dist/lib/scopes.d.ts +16 -0
  340. package/dist/lib/scopes.d.ts.map +1 -0
  341. package/dist/lib/scopes.js +37 -0
  342. package/dist/lib/scopes.js.map +1 -0
  343. package/dist/lib/sha-cache.d.ts +7 -0
  344. package/dist/lib/sha-cache.d.ts.map +1 -0
  345. package/dist/lib/sha-cache.js +44 -0
  346. package/dist/lib/sha-cache.js.map +1 -0
  347. package/dist/lib/smart-switch.d.ts +44 -0
  348. package/dist/lib/smart-switch.d.ts.map +1 -0
  349. package/dist/lib/smart-switch.js +145 -0
  350. package/dist/lib/smart-switch.js.map +1 -0
  351. package/dist/lib/verdict.d.ts +9 -0
  352. package/dist/lib/verdict.d.ts.map +1 -0
  353. package/dist/lib/verdict.js +52 -0
  354. package/dist/lib/verdict.js.map +1 -0
  355. package/dist/lib/workflow.d.ts +85 -0
  356. package/dist/lib/workflow.d.ts.map +1 -0
  357. package/dist/lib/workflow.js +116 -0
  358. package/dist/lib/workflow.js.map +1 -0
  359. package/dist/reviewers/address.d.ts +5 -0
  360. package/dist/reviewers/address.d.ts.map +1 -0
  361. package/dist/reviewers/address.js +87 -0
  362. package/dist/reviewers/address.js.map +1 -0
  363. package/dist/reviewers/claude.d.ts +12 -0
  364. package/dist/reviewers/claude.d.ts.map +1 -0
  365. package/dist/reviewers/claude.js +78 -0
  366. package/dist/reviewers/claude.js.map +1 -0
  367. package/dist/reviewers/codex.d.ts +9 -0
  368. package/dist/reviewers/codex.d.ts.map +1 -0
  369. package/dist/reviewers/codex.js +121 -0
  370. package/dist/reviewers/codex.js.map +1 -0
  371. package/dist/reviewers/conflict-resolve.d.ts +15 -0
  372. package/dist/reviewers/conflict-resolve.d.ts.map +1 -0
  373. package/dist/reviewers/conflict-resolve.js +219 -0
  374. package/dist/reviewers/conflict-resolve.js.map +1 -0
  375. package/dist/reviewers/fix.d.ts +7 -0
  376. package/dist/reviewers/fix.d.ts.map +1 -0
  377. package/dist/reviewers/fix.js +197 -0
  378. package/dist/reviewers/fix.js.map +1 -0
  379. package/get-started.md +1271 -0
  380. package/get-started.zh.md +1208 -0
  381. package/package.json +75 -0
@@ -0,0 +1,710 @@
1
+ import { execSync, execFileSync } from 'child_process';
2
+ import { randomUUID } from 'crypto';
3
+ import { readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import chalk from 'chalk';
6
+ import { runCodexReview } from '../reviewers/codex.js';
7
+ import { runClaudeReview } from '../reviewers/claude.js';
8
+ import { runFixStep } from '../reviewers/fix.js';
9
+ import { runConflictResolveStep, findConflictedFiles } from '../reviewers/conflict-resolve.js';
10
+ import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING } from '../lib/verdict.js';
11
+ import { createGithubClient, postReviewComment, getLastCrossCheckCommentId, getLastCrossCheckReviewComment } from '../github/client.js';
12
+ import { acquireRemoteLock, releaseRemoteLock } from '../github/review-status.js';
13
+ import { log as fileLog, logError } from '../lib/logger.js';
14
+ import { buildCommitTrailers } from '../lib/annotation.js';
15
+ import { resolveClaudeModel } from '../lib/review-models.js';
16
+ import { buildStepIdentityFields } from '../lib/event-fields.js';
17
+ import { buildFixAppliedCommentBody, buildConflictResolvedCommentBody } from '../lib/comment-bodies.js';
18
+ import { loadWorkflow, evaluateWhen } from '../lib/workflow.js';
19
+ const MAX_CROSSCHECK_COMMITS = 5;
20
+ const FIX_RETRY_DELAY_MS = 2 * 60 * 1000;
21
+ // Auth failures are operator issues that won't self-heal — everything else is worth a retry.
22
+ export function isRetryableFixError(err) {
23
+ const msg = err instanceof Error ? err.message : String(err);
24
+ return !/auth failure|not logged in|claude auth/i.test(msg);
25
+ }
26
+ // When a PR has already been reviewed, subsequent webhook runs treat every
27
+ // 'review' step as a 'recheck' so the first review's CR result is preserved.
28
+ export function getEffectiveStepType(stepType, isRecheckRun) {
29
+ return stepType === 'review' && isRecheckRun ? 'recheck' : stepType;
30
+ }
31
+ // Counts crosscheck-authored commits unique to this PR (ahead of base) rather
32
+ // than the branch's full history. Long-lived integration branches like
33
+ // `staging` accumulate [crosscheck] commits from many merged PRs — counting
34
+ // those would trip the per-PR fix-loop guard immediately and skip fix/recheck.
35
+ //
36
+ // Fails closed: when `origin/<base>` isn't available (e.g. clone fetched the
37
+ // base ref with `base_branch_fetch_skipped`), fall back to the full-history
38
+ // count rather than returning 0. Over-counting can stop fix early; returning 0
39
+ // would silently disable the cap and let runaway fix loops keep pushing.
40
+ export function countCrosscheckCommitsForPR(tmpDir, baseRef) {
41
+ const runLog = (args) => execFileSync('git', ['log', '--oneline', ...args], { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
42
+ const count = (out) => out.split('\n').filter(l => l.includes('[crosscheck]')).length;
43
+ try {
44
+ return count(runLog([`origin/${baseRef}..HEAD`]));
45
+ }
46
+ catch {
47
+ // Scoped range unavailable — fall back to full history so the cap still
48
+ // applies. May over-count when the branch has prior merged crosscheck
49
+ // commits, but that's preferable to bypassing the safety guard.
50
+ try {
51
+ return count(runLog([]));
52
+ }
53
+ catch {
54
+ return 0;
55
+ }
56
+ }
57
+ }
58
+ export function buildWorkflowCompleteEvent(inputs) {
59
+ const lastVerdict = Object.values(inputs.results).reverse().find(r => r.verdict !== undefined)?.verdict ?? null;
60
+ const lastStep = inputs.stepsRun.length > 0 ? inputs.stepsRun[inputs.stepsRun.length - 1] : null;
61
+ const endedReason = inputs.workflowFailed ? 'error' : 'completed';
62
+ const now = inputs.now ?? Date.now();
63
+ return {
64
+ level: inputs.workflowFailed ? 'warn' : 'info',
65
+ event: 'workflow_complete',
66
+ repo: `${inputs.owner}/${inputs.repoName}`,
67
+ pr: inputs.prNumber,
68
+ workflow_id: inputs.workflowId,
69
+ steps_run: inputs.stepsRun,
70
+ last_step: lastStep,
71
+ last_verdict: lastVerdict,
72
+ ended_reason: endedReason,
73
+ total_duration_ms: now - inputs.workflowStart,
74
+ ...(inputs.round !== undefined && { round: inputs.round }),
75
+ };
76
+ }
77
+ // Returns true when fix/recheck steps should be skipped because the configured
78
+ // max_rounds cap has been reached. The review step (even when coerced to recheck)
79
+ // is never skipped — it always produces a verdict for the current push.
80
+ export function exceedsMaxRounds(effectiveType, originalStepType, maxRounds, round) {
81
+ if (round === undefined)
82
+ return false;
83
+ if (effectiveType === 'fix')
84
+ return round > maxRounds;
85
+ if (effectiveType === 'conflict-resolve')
86
+ return round > maxRounds;
87
+ // Recheck step from the workflow (not a review coerced to recheck) is gated
88
+ if (effectiveType === 'recheck' && originalStepType !== 'review')
89
+ return round > maxRounds;
90
+ return false;
91
+ }
92
+ function countComments(reviewText) {
93
+ const bullets = (reviewText.match(/^[-*•]\s/gm) ?? []).length;
94
+ const numbered = (reviewText.match(/^\d+\.\s/gm) ?? []).length;
95
+ return bullets + numbered;
96
+ }
97
+ function resolveReviewer(reviewer, origin, config, fallback) {
98
+ if (reviewer === 'origin') {
99
+ if (origin === 'claude' && config.vendors.claude.enabled)
100
+ return 'claude';
101
+ if (origin === 'codex' && config.vendors.codex.enabled)
102
+ return 'codex';
103
+ return fallback && config.vendors[fallback].enabled ? fallback : null;
104
+ }
105
+ if (reviewer === 'auto') {
106
+ if (origin === 'claude' && config.vendors.codex.enabled)
107
+ return 'codex';
108
+ if (origin === 'codex' && config.vendors.claude.enabled)
109
+ return 'claude';
110
+ if (config.vendors.codex.enabled)
111
+ return 'codex';
112
+ if (config.vendors.claude.enabled)
113
+ return 'claude';
114
+ return null;
115
+ }
116
+ if (reviewer === 'claude')
117
+ return config.vendors.claude.enabled ? 'claude' : (fallback && config.vendors[fallback].enabled ? fallback : null);
118
+ if (reviewer === 'codex')
119
+ return config.vendors.codex.enabled ? 'codex' : (fallback && config.vendors[fallback].enabled ? fallback : null);
120
+ return null;
121
+ }
122
+ export async function runWorkflow(ctx) {
123
+ const { owner, repoName, prNumber, pr, tmpDir, token, config, origin, log, onPhaseChange } = ctx;
124
+ const steps = ctx.steps ?? loadWorkflow(process.cwd());
125
+ const results = {};
126
+ // SHAs the workflow pushed AND set a `crosscheck/review` pending status on.
127
+ // Each one must be released in the finally below — otherwise the pending
128
+ // status would stay forever on GitHub (the 15-min staleness check is
129
+ // internal to crosscheck's lock detection and does not clear the status,
130
+ // which can block PRs in repos where `crosscheck/review` is required).
131
+ //
132
+ // Use the caller's array if provided so the command-layer signal handler
133
+ // can iterate the same list and release these shas if SIGINT/SIGTERM fires
134
+ // mid-workflow (process.exit there bypasses our finally below).
135
+ const pushedShasNeedingRelease = ctx.pushedShas ?? [];
136
+ let workflowFailed = false;
137
+ // workflow_complete event accumulators. Each step the runner dispatches is
138
+ // recorded in stepsRun (including ones that get logged as step_skipped —
139
+ // the event records the workflow's declared shape, the per-step skip
140
+ // reasons live in their own step_skipped log entries).
141
+ const workflowId = randomUUID();
142
+ const workflowStart = Date.now();
143
+ const stepsRun = [];
144
+ try {
145
+ for (const step of steps) {
146
+ stepsRun.push(step.name);
147
+ const effectiveType = getEffectiveStepType(step.type, ctx.isRecheckRun === true);
148
+ if (exceedsMaxRounds(effectiveType, step.type, step.max_rounds, ctx.round)) {
149
+ fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'max_rounds' });
150
+ results[step.name] = { skipped: true };
151
+ if (effectiveType === 'fix')
152
+ onPhaseChange('', { phase: 'fixed', fixCount: 0 });
153
+ else if (effectiveType === 'recheck')
154
+ onPhaseChange('', { phase: 'rechecked' });
155
+ else if (effectiveType === 'conflict-resolve')
156
+ onPhaseChange('', { phase: 'fixed', fixCount: 0 });
157
+ continue;
158
+ }
159
+ // Evaluate when condition — skip step if false
160
+ if (step.when && !evaluateWhen(step.when, results)) {
161
+ fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'when_condition' });
162
+ results[step.name] = { skipped: true };
163
+ if (effectiveType === 'fix')
164
+ onPhaseChange('', { phase: 'fixed', fixCount: 0 });
165
+ else if (effectiveType === 'recheck')
166
+ onPhaseChange('', { phase: 'rechecked' });
167
+ else if (effectiveType === 'conflict-resolve')
168
+ onPhaseChange('', { phase: 'fixed', fixCount: 0 });
169
+ continue;
170
+ }
171
+ if (effectiveType === 'review' || effectiveType === 'recheck') {
172
+ const isRecheck = effectiveType === 'recheck';
173
+ const reviewer = resolveReviewer(step.reviewer, origin, config, ctx.smartSwitchFallback);
174
+ if (!reviewer) {
175
+ fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'no_reviewer' });
176
+ results[step.name] = { skipped: true };
177
+ continue;
178
+ }
179
+ const stepIdentity = buildStepIdentityFields(effectiveType, step.name);
180
+ fileLog({ level: 'info', event: 'review_started', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, ...stepIdentity, ...(ctx.round !== undefined && { round: ctx.round }) });
181
+ const startPhase = isRecheck ? 'rechecking' : 'reviewing';
182
+ const donePhase = isRecheck ? 'rechecked' : 'reviewed';
183
+ onPhaseChange(`${reviewer} ${isRecheck ? 'rechecking' : 'reviewing'}...`, { phase: startPhase });
184
+ // Per-step start timestamp. The shared ctx.reviewStart is set once at
185
+ // workflow start and never reset, so a recheck's duration_ms would
186
+ // otherwise include the prior review and fix wall time.
187
+ const stepStart = Date.now();
188
+ let rawReview;
189
+ let tokensUsed;
190
+ let model = 'default';
191
+ if (reviewer === 'codex') {
192
+ ;
193
+ ({ review: rawReview, tokensUsed, model } = await runCodexReview(tmpDir, pr.base.ref, pr.title, config.quality, config.vendors.codex, step.instructions));
194
+ }
195
+ else {
196
+ ;
197
+ ({ review: rawReview, tokensUsed, model } = await runClaudeReview(tmpDir, pr.base.ref, pr.title, config.quality, config.vendors.claude, config.budget.per_review_usd, step.instructions));
198
+ }
199
+ const { verdict, clean } = parseVerdict(rawReview);
200
+ if (verdict === null) {
201
+ fileLog({ level: 'warn', event: 'verdict_parse_failed', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, ...stepIdentity, output_length: rawReview.length });
202
+ }
203
+ const commentBody = verdict === null
204
+ ? `${NULL_VERDICT_WARNING}\n\n${clean}`
205
+ : prependVerdictToComment(clean, verdict);
206
+ const commentCount = countComments(rawReview);
207
+ fileLog({ level: 'info', event: 'review_complete', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, model, ...stepIdentity, verdict, duration_ms: Date.now() - stepStart, tokens_used: tokensUsed, ...(ctx.round !== undefined && { round: ctx.round }) });
208
+ // Recheck verdict is stored separately to preserve the original review's commentCount on the board
209
+ const phaseUpdate = isRecheck
210
+ ? { recheckVerdict: verdict, phase: donePhase, recheckTokens: tokensUsed, recheckReviewer: reviewer, qualityTier: config.quality.tier }
211
+ : { verdict, commentCount, phase: donePhase, crTokens: tokensUsed, crReviewer: reviewer, qualityTier: config.quality.tier };
212
+ if (ctx.dryRun) {
213
+ onPhaseChange('dry-run — comment not posted', phaseUpdate);
214
+ log(chalk.dim(`\n--- dry-run: comment that would be posted ---\n${commentBody}\n--- end ---`));
215
+ results[step.name] = { verdict, commentBody };
216
+ }
217
+ else {
218
+ onPhaseChange(isRecheck ? 'posting recheck...' : 'posting comment...', phaseUpdate);
219
+ const octokit = createGithubClient(token);
220
+ // For rechecks: look up the original review comment ID so the recheck
221
+ // can link back to it. Check in-run results first (single-run pipelines),
222
+ // then fall back to GitHub (cross-run: recheck triggered by a new push).
223
+ let priorReviewId;
224
+ if (isRecheck) {
225
+ priorReviewId = Object.values(results).reverse().find(r => r.commentId !== undefined)?.commentId;
226
+ if (priorReviewId === undefined) {
227
+ priorReviewId = await getLastCrossCheckCommentId(owner, repoName, prNumber, token);
228
+ }
229
+ }
230
+ const commentId = await postReviewComment(octokit, owner, repoName, prNumber, commentBody, reviewer, config.brand, origin, verdict ?? undefined, priorReviewId, isRecheck, model, effectiveType, ctx.round ?? 1, pr.head.sha);
231
+ const commentUrl = `github.com/${owner}/${repoName}/pull/${prNumber}`;
232
+ fileLog({ level: 'info', event: 'comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, url: `https://${commentUrl}` });
233
+ results[step.name] = { verdict, commentBody, commentUrl, commentId };
234
+ }
235
+ }
236
+ else if (effectiveType === 'fix') {
237
+ const skipFix = (reason) => {
238
+ onPhaseChange('', { phase: 'fixed', fixCount: 0 });
239
+ results[step.name] = { skipped: true };
240
+ fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason });
241
+ };
242
+ if (ctx.dryRun) {
243
+ skipFix('dry_run');
244
+ continue;
245
+ }
246
+ // Migration gate: honor legacy opt-out fields while users migrate to workflow.yml.
247
+ const legacyDisabled = config.post_review.auto_fix.enabled === false
248
+ || config.post_review.auto_fix.trigger === 'never';
249
+ if (legacyDisabled) {
250
+ log(chalk.yellow(`⚠ auto_fix.enabled/trigger are deprecated — remove them from config and add a "when:" condition to the fix step in workflow.yml instead`));
251
+ skipFix('legacy_auto_fix_disabled');
252
+ continue;
253
+ }
254
+ // Find the most recent review result that has a comment body. A fix-only
255
+ // invocation (used by kickass) has no in-memory review result, so seed it
256
+ // from the latest fresh crosscheck review comment on GitHub.
257
+ const reviewResult = Object.values(results).reverse().find(r => r.commentBody);
258
+ let reviewCommentBody = reviewResult?.commentBody;
259
+ let reviewCommentId = reviewResult?.commentId;
260
+ if (!reviewCommentBody) {
261
+ reviewCommentBody = ctx.initialReviewComment?.body;
262
+ reviewCommentId = ctx.initialReviewComment?.id;
263
+ }
264
+ if (!reviewCommentBody) {
265
+ const latestReviewComment = await getLastCrossCheckReviewComment(owner, repoName, prNumber, token);
266
+ reviewCommentBody = latestReviewComment?.body;
267
+ reviewCommentId = latestReviewComment?.id;
268
+ }
269
+ if (!reviewCommentBody) {
270
+ skipFix('no_review_comment');
271
+ continue;
272
+ }
273
+ // Vendor is resolved from the workflow step's reviewer field, same as review/recheck steps.
274
+ // Use 'origin' to fix with the same vendor that authored the PR (recommended default).
275
+ const vendor = resolveReviewer(step.reviewer, origin, config, ctx.smartSwitchFallback);
276
+ if (!vendor) {
277
+ skipFix('no_vendor');
278
+ continue;
279
+ }
280
+ // Codex fix not yet implemented — skip gracefully
281
+ if (vendor === 'codex') {
282
+ skipFix('codex_fix_unsupported');
283
+ continue;
284
+ }
285
+ const fixModel = resolveClaudeModel(config.quality);
286
+ // Guard: don't push more than MAX_CROSSCHECK_COMMITS per PR.
287
+ // Scope to commits ahead of base so long-lived branches (e.g. staging)
288
+ // don't count [crosscheck] commits from previously merged PRs.
289
+ const existingCount = countCrosscheckCommitsForPR(tmpDir, pr.base.ref);
290
+ if (existingCount >= MAX_CROSSCHECK_COMMITS) {
291
+ log(chalk.yellow(`⚠ PR #${prNumber}: ${MAX_CROSSCHECK_COMMITS} [crosscheck] commits already — stopping auto-fix`));
292
+ skipFix('commit_limit_reached');
293
+ continue;
294
+ }
295
+ onPhaseChange(`${vendor} fixing...`, { phase: 'fixing' });
296
+ // Per-step start timestamp covers attempt + retry + retry-delay wall time
297
+ // (the user perceives the whole interval as "the fix step").
298
+ const fixStepStart = Date.now();
299
+ let appliedCount = 0;
300
+ let fixTokensUsed;
301
+ let fixErr = undefined;
302
+ try {
303
+ ;
304
+ ({ appliedCount, tokensUsed: fixTokensUsed } = await runFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', config));
305
+ }
306
+ catch (err) {
307
+ logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'fix', attempt: 1 }, err);
308
+ fixErr = err;
309
+ }
310
+ if (fixErr !== undefined && isRetryableFixError(fixErr)) {
311
+ log(chalk.yellow(`⚠ fix step failed — retrying in 2 min...`));
312
+ onPhaseChange('fix retry in 2 min...', { phase: 'fixing' });
313
+ fileLog({ level: 'info', event: 'fix_retry_scheduled', repo: `${owner}/${repoName}`, pr: prNumber });
314
+ await new Promise(resolve => setTimeout(resolve, FIX_RETRY_DELAY_MS));
315
+ onPhaseChange(`${vendor} fixing (retry)...`, { phase: 'fixing' });
316
+ try {
317
+ ;
318
+ ({ appliedCount, tokensUsed: fixTokensUsed } = await runFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', config));
319
+ fileLog({ level: 'info', event: 'fix_retry_succeeded', repo: `${owner}/${repoName}`, pr: prNumber });
320
+ fixErr = undefined;
321
+ }
322
+ catch (retryErr) {
323
+ logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'fix', attempt: 2 }, retryErr);
324
+ fixErr = retryErr;
325
+ }
326
+ }
327
+ if (fixErr !== undefined) {
328
+ skipFix('fix_error');
329
+ // Only notify for transient failures — auth errors are operator issues, not PR author issues
330
+ if (isRetryableFixError(fixErr)) {
331
+ try {
332
+ const octokit = createGithubClient(token);
333
+ await octokit.rest.issues.createComment({
334
+ owner, repo: repoName, issue_number: prNumber,
335
+ body: `⚠️ **Auto-fix failed**\n\nThe fix step timed out after retrying. Push a new commit or run \`crosscheck run ${pr.html_url}\` to retry manually.\n\n<!-- crosscheck: fix_failed -->`,
336
+ });
337
+ fileLog({ level: 'info', event: 'fix_failed_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber });
338
+ }
339
+ catch { /* best-effort notification */ }
340
+ }
341
+ continue;
342
+ }
343
+ if (appliedCount === 0) {
344
+ onPhaseChange('', { phase: 'fixed', fixCount: 0, fixTokens: fixTokensUsed });
345
+ results[step.name] = { applied_count: 0 };
346
+ continue;
347
+ }
348
+ const isFork = pr.head.repo?.full_name !== pr.base.repo.full_name;
349
+ if (isFork) {
350
+ skipFix('fork_pr');
351
+ continue;
352
+ }
353
+ const deliveryMode = config.post_review.auto_fix.delivery.mode;
354
+ if (deliveryMode === 'commit') {
355
+ execSync('git add -A', { cwd: tmpDir });
356
+ execFileSync('git', [
357
+ 'commit',
358
+ '-m',
359
+ `[crosscheck] fix: apply ${appliedCount} fix${appliedCount !== 1 ? 'es' : ''} from code review — by Claude Code`,
360
+ '-m',
361
+ buildCommitTrailers({ reviewer: vendor, model: fixModel, step: 'fix', service: 'crosscheck' }),
362
+ ], { cwd: tmpDir });
363
+ const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
364
+ execSync(`git push origin HEAD:${pr.head.ref}`, {
365
+ cwd: tmpDir,
366
+ env: { ...process.env, GITHUB_TOKEN: token, GH_TOKEN: token },
367
+ });
368
+ ctx.crosscheckShas.add(newSha);
369
+ onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
370
+ fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor, applied_count: appliedCount, sha: newSha, delivery: 'commit', tokens_used: fixTokensUsed, duration_ms: Date.now() - fixStepStart });
371
+ // Post a summary comment so the silent commit push is visible on the timeline
372
+ // as a comment card. Best-effort — a failure here must not fail the run.
373
+ try {
374
+ const octokit = createGithubClient(token);
375
+ const body = buildFixAppliedCommentBody({
376
+ owner, repo: repoName, sha: newSha, appliedCount,
377
+ reviewCommentId,
378
+ });
379
+ await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
380
+ fileLog({ level: 'info', event: 'fix_applied_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });
381
+ }
382
+ catch (err) {
383
+ fileLog({ level: 'warn', event: 'fix_applied_comment_failed', repo: `${owner}/${repoName}`, pr: prNumber, error: err instanceof Error ? err.message : String(err) });
384
+ }
385
+ results[step.name] = { applied_count: appliedCount };
386
+ }
387
+ else if (deliveryMode === 'pull_request') {
388
+ // Create a fix branch and open a PR targeting the original branch
389
+ const fixBranch = `fix/cr-${prNumber}-review-issues`;
390
+ execSync(`git checkout -b ${fixBranch}`, { cwd: tmpDir });
391
+ execSync('git add -A', { cwd: tmpDir });
392
+ execFileSync('git', [
393
+ 'commit',
394
+ '-m',
395
+ `[crosscheck] fix: apply CR fixes from review of PR #${prNumber} — by Claude Code`,
396
+ '-m',
397
+ buildCommitTrailers({ reviewer: vendor, model: fixModel, step: 'fix', service: 'crosscheck' }),
398
+ ], { cwd: tmpDir });
399
+ const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
400
+ execSync(`git push origin HEAD:${fixBranch}`, {
401
+ cwd: tmpDir,
402
+ env: { ...process.env, GITHUB_TOKEN: token, GH_TOKEN: token },
403
+ });
404
+ ctx.crosscheckShas.add(newSha);
405
+ const octokit = createGithubClient(token);
406
+ const fixPrTitle = config.post_review.auto_fix.delivery.pr_title.replace('#{original_pr_title}', pr.title);
407
+ const { data: fixPr } = await octokit.rest.pulls.create({
408
+ owner,
409
+ repo: repoName,
410
+ head: fixBranch,
411
+ base: pr.head.ref,
412
+ title: fixPrTitle,
413
+ body: `Auto-fix by crosscheck for CR issues found in #${prNumber}.\n\nReview: https://github.com/${owner}/${repoName}/pull/${prNumber}`,
414
+ });
415
+ if (config.post_review.auto_fix.delivery.label) {
416
+ try {
417
+ await octokit.rest.issues.addLabels({
418
+ owner, repo: repoName, issue_number: fixPr.number, labels: [config.post_review.auto_fix.delivery.label],
419
+ });
420
+ }
421
+ catch { /* label may not exist in this repo — skip */ }
422
+ }
423
+ onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
424
+ fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor, applied_count: appliedCount, sha: newSha, delivery: 'pull_request', fix_pr: fixPr.number, tokens_used: fixTokensUsed, duration_ms: Date.now() - fixStepStart });
425
+ results[step.name] = { applied_count: appliedCount };
426
+ }
427
+ else {
428
+ // comment: post the diff as a suggested-fix comment, no code push needed (works for fork PRs too)
429
+ let patch = '';
430
+ try {
431
+ patch = execSync('git diff', { cwd: tmpDir, encoding: 'utf8' });
432
+ }
433
+ catch { /* ignore */ }
434
+ if (patch) {
435
+ const octokit = createGithubClient(token);
436
+ const body = `### Suggested fixes (crosscheck auto-fix)\n\n\`\`\`diff\n${patch.slice(0, 16000)}\n\`\`\``;
437
+ await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
438
+ }
439
+ onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
440
+ fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor, applied_count: appliedCount, delivery: 'comment', tokens_used: fixTokensUsed, duration_ms: Date.now() - fixStepStart });
441
+ results[step.name] = { applied_count: appliedCount };
442
+ }
443
+ }
444
+ else if (effectiveType === 'conflict-resolve') {
445
+ const skipConflictResolve = (reason) => {
446
+ onPhaseChange('', { phase: 'fixed', fixCount: 0 });
447
+ results[step.name] = { skipped: true };
448
+ fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason });
449
+ };
450
+ if (ctx.dryRun) {
451
+ skipConflictResolve('dry_run');
452
+ continue;
453
+ }
454
+ // Fast pre-check: GitHub's mergeable field tells us if the PR has conflicts without
455
+ // cloning. true = no conflicts (skip immediately); false = conflicts confirmed (proceed);
456
+ // null = GitHub is still computing — fall through to the git merge probe.
457
+ {
458
+ const octokit = createGithubClient(token);
459
+ const { data: prInfo } = await octokit.rest.pulls.get({ owner, repo: repoName, pull_number: prNumber });
460
+ if (prInfo.mergeable === true) {
461
+ skipConflictResolve('no_conflicts');
462
+ continue;
463
+ }
464
+ }
465
+ // P1: The clone only has the PR head checked out — no unmerged index entries exist
466
+ // until we actually attempt the merge. Attempt the merge first; if it succeeds
467
+ // cleanly (no conflicts) abort it and skip. If it fails, the working tree now has
468
+ // real conflict markers and UU entries that findConflictedFiles can detect.
469
+ let hasMergeConflicts = false;
470
+ try {
471
+ execSync(`git merge --no-commit origin/${pr.base.ref}`, { cwd: tmpDir, stdio: 'pipe' });
472
+ // Clean merge — undo the staged merge state and skip this step
473
+ try {
474
+ execSync('git merge --abort', { cwd: tmpDir });
475
+ }
476
+ catch { /* ignore */ }
477
+ }
478
+ catch {
479
+ hasMergeConflicts = true;
480
+ }
481
+ if (!hasMergeConflicts) {
482
+ skipConflictResolve('no_conflicts');
483
+ continue;
484
+ }
485
+ const conflictedFiles = findConflictedFiles(tmpDir);
486
+ if (conflictedFiles.length === 0) {
487
+ try {
488
+ execSync('git merge --abort', { cwd: tmpDir });
489
+ }
490
+ catch { /* ignore */ }
491
+ skipConflictResolve('no_conflicts');
492
+ continue;
493
+ }
494
+ const vendor = resolveReviewer(step.reviewer, origin, config, ctx.smartSwitchFallback);
495
+ if (!vendor) {
496
+ try {
497
+ execSync('git merge --abort', { cwd: tmpDir });
498
+ }
499
+ catch { /* ignore */ }
500
+ ;
501
+ skipConflictResolve('no_vendor');
502
+ continue;
503
+ }
504
+ if (vendor === 'codex') {
505
+ try {
506
+ execSync('git merge --abort', { cwd: tmpDir });
507
+ }
508
+ catch { /* ignore */ }
509
+ ;
510
+ skipConflictResolve('codex_conflict_resolve_unsupported');
511
+ continue;
512
+ }
513
+ const conflictResolveModel = resolveClaudeModel(config.quality);
514
+ const isFork = pr.head.repo?.full_name !== pr.base.repo.full_name;
515
+ if (isFork) {
516
+ try {
517
+ execSync('git merge --abort', { cwd: tmpDir });
518
+ }
519
+ catch { /* ignore */ }
520
+ ;
521
+ skipConflictResolve('fork_pr');
522
+ continue;
523
+ }
524
+ const existingCount = countCrosscheckCommitsForPR(tmpDir, pr.base.ref);
525
+ if (existingCount >= MAX_CROSSCHECK_COMMITS) {
526
+ try {
527
+ execSync('git merge --abort', { cwd: tmpDir });
528
+ }
529
+ catch { /* ignore */ }
530
+ log(chalk.yellow(`⚠ PR #${prNumber}: ${MAX_CROSSCHECK_COMMITS} [crosscheck] commits already — stopping conflict-resolve`));
531
+ skipConflictResolve('commit_limit_reached');
532
+ continue;
533
+ }
534
+ onPhaseChange(`${vendor} resolving conflicts...`, { phase: 'fixing' });
535
+ // Per-step start timestamp for conflict-resolve wall time.
536
+ const conflictResolveStepStart = Date.now();
537
+ let appliedCount = 0;
538
+ let resolvedPaths = [];
539
+ let resolveTokensUsed;
540
+ try {
541
+ ;
542
+ ({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed } = await runConflictResolveStep(tmpDir, pr.title, step.instructions ?? '', conflictResolveModel));
543
+ }
544
+ catch (err) {
545
+ logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'conflict-resolve', attempt: 1 }, err);
546
+ try {
547
+ execSync('git merge --abort', { cwd: tmpDir });
548
+ }
549
+ catch { /* ignore */ }
550
+ skipConflictResolve('resolve_error');
551
+ continue;
552
+ }
553
+ if (appliedCount === 0) {
554
+ try {
555
+ execSync('git merge --abort', { cwd: tmpDir });
556
+ }
557
+ catch { /* ignore */ }
558
+ onPhaseChange('', { phase: 'fixed', fixCount: 0, fixTokens: resolveTokensUsed });
559
+ results[step.name] = { applied_count: 0 };
560
+ continue;
561
+ }
562
+ // P2: Verify every conflict region was resolved before committing. Scope the
563
+ // check to the union of (originally-conflicted files) ∪ (files the resolver
564
+ // actually rewrote) — a repo-wide grep would false-positive on legitimate
565
+ // "=======" lines in docs (e.g. Markdown setext headings) and abort valid
566
+ // resolutions, but we still need to cover any path the resolver touched in
567
+ // case it ever edits outside the original conflict set. Read working-tree
568
+ // content directly so untrusted PR-controlled paths never reach a shell.
569
+ const MARKER_RE = /^(<<<<<<<|=======|>>>>>>>)( |$)/m;
570
+ const pathsToScan = Array.from(new Set([...conflictedFiles, ...resolvedPaths]));
571
+ const filesWithMarkers = [];
572
+ for (const f of pathsToScan) {
573
+ try {
574
+ const content = readFileSync(join(tmpDir, f), 'utf8');
575
+ if (MARKER_RE.test(content))
576
+ filesWithMarkers.push(f);
577
+ }
578
+ catch { /* unreadable (deleted side of modify/delete) — caught by U-filter below */ }
579
+ }
580
+ if (filesWithMarkers.length > 0) {
581
+ try {
582
+ execSync('git merge --abort', { cwd: tmpDir });
583
+ }
584
+ catch { /* ignore */ }
585
+ log(chalk.yellow(`⚠ PR #${prNumber}: ${filesWithMarkers.length} file(s) still contain conflict markers — skipping commit`));
586
+ fileLog({ level: 'warn', event: 'conflict_resolve_incomplete', repo: `${owner}/${repoName}`, pr: prNumber, paths: filesWithMarkers });
587
+ skipConflictResolve('incomplete_resolution');
588
+ continue;
589
+ }
590
+ // Stage only files the resolver actually rewrote — `git add -A` would
591
+ // otherwise silently stage non-text conflicts (binary, modify/delete) using
592
+ // the worktree side as an un-reviewed resolution. Staging also has to come
593
+ // BEFORE the unmerged-path check below: git keeps a path in the unmerged
594
+ // index until it is explicitly added, so checking earlier would always fail
595
+ // on the resolved files themselves. Use execFileSync (no shell) because
596
+ // resolvedPaths is derived from model output and PR-controlled filenames.
597
+ for (const p of resolvedPaths) {
598
+ try {
599
+ execFileSync('git', ['add', '--', p], { cwd: tmpDir, stdio: 'pipe' });
600
+ }
601
+ catch { /* skip */ }
602
+ }
603
+ // After staging the resolved files, anything still in U state is a conflict
604
+ // the resolver did not handle (binary, modify/delete, or a failed edit).
605
+ // Abort rather than commit a partial merge.
606
+ let unmergedPaths = [];
607
+ try {
608
+ const out = execSync('git diff --name-only --diff-filter=U', { cwd: tmpDir, encoding: 'utf8' });
609
+ unmergedPaths = out.trim().split('\n').filter(Boolean);
610
+ }
611
+ catch { /* ignore */ }
612
+ if (unmergedPaths.length > 0) {
613
+ try {
614
+ execSync('git merge --abort', { cwd: tmpDir });
615
+ }
616
+ catch { /* ignore */ }
617
+ log(chalk.yellow(`⚠ PR #${prNumber}: ${unmergedPaths.length} unmerged path(s) remain after resolve — skipping commit`));
618
+ fileLog({ level: 'warn', event: 'conflict_resolve_unmerged_paths', repo: `${owner}/${repoName}`, pr: prNumber, paths: unmergedPaths });
619
+ skipConflictResolve('unmerged_paths');
620
+ continue;
621
+ }
622
+ execFileSync('git', [
623
+ 'commit',
624
+ '-m',
625
+ `[crosscheck] resolve: resolve ${conflictedFiles.length} conflict${conflictedFiles.length !== 1 ? 's' : ''} — by Claude Code`,
626
+ '-m',
627
+ buildCommitTrailers({ reviewer: vendor, model: conflictResolveModel, step: 'conflict-resolve', service: 'crosscheck' }),
628
+ ], { cwd: tmpDir });
629
+ const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
630
+ execSync(`git push origin HEAD:${pr.head.ref}`, {
631
+ cwd: tmpDir,
632
+ env: { ...process.env, GITHUB_TOKEN: token, GH_TOKEN: token },
633
+ });
634
+ ctx.crosscheckShas.add(newSha);
635
+ // Move the in-flight pending status to newSha so watchers on other
636
+ // machines (which don't share crosscheckShas) see the PR as locked when
637
+ // they receive the synchronize event and skip duplicate review.
638
+ // Track the sha so the finally below releases the pending status —
639
+ // without that release the status would stay pending forever on GitHub.
640
+ try {
641
+ const lockOctokit = createGithubClient(token);
642
+ await acquireRemoteLock(lockOctokit, owner, repoName, newSha);
643
+ pushedShasNeedingRelease.push(newSha);
644
+ }
645
+ catch (err) {
646
+ fileLog({ level: 'warn', event: 'remote_lock_refresh_failed', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha, error: err instanceof Error ? err.message : String(err) });
647
+ }
648
+ onPhaseChange('conflicts resolved ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: resolveTokensUsed });
649
+ fileLog({ level: 'info', event: 'conflict_resolve_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor, conflicts_resolved: conflictedFiles.length, sha: newSha, tokens_used: resolveTokensUsed, duration_ms: Date.now() - conflictResolveStepStart });
650
+ // Post a summary comment so the silent merge-commit push is visible on the
651
+ // timeline as a comment card. Best-effort — a failure here must not fail the run.
652
+ // Prefer the resolver's actual rewrite set; fall back to the originally-conflicted
653
+ // list if the resolver didn't surface paths.
654
+ try {
655
+ const octokit = createGithubClient(token);
656
+ const body = buildConflictResolvedCommentBody({
657
+ owner, repo: repoName, sha: newSha,
658
+ conflictCount: conflictedFiles.length,
659
+ files: resolvedPaths.length > 0 ? resolvedPaths : conflictedFiles,
660
+ });
661
+ await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
662
+ fileLog({ level: 'info', event: 'conflict_resolved_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });
663
+ }
664
+ catch (err) {
665
+ fileLog({ level: 'warn', event: 'conflict_resolved_comment_failed', repo: `${owner}/${repoName}`, pr: prNumber, error: err instanceof Error ? err.message : String(err) });
666
+ }
667
+ results[step.name] = { applied_count: appliedCount };
668
+ }
669
+ }
670
+ const verdict = Object.values(results).reverse().find(r => r.verdict !== undefined)?.verdict ?? null;
671
+ return { verdict: verdict ?? null };
672
+ }
673
+ catch (err) {
674
+ workflowFailed = true;
675
+ throw err;
676
+ }
677
+ finally {
678
+ if (pushedShasNeedingRelease.length > 0) {
679
+ const lockOctokit = createGithubClient(token);
680
+ const outcome = workflowFailed ? 'failure' : 'success';
681
+ // Drain via shift() so each released sha is synchronously removed from
682
+ // the shared array. The command-layer SIGINT/SIGTERM handler iterates
683
+ // the same array — if a late signal arrives after this finally has
684
+ // already released a sha, the handler won't see it and won't overwrite
685
+ // the released status with 'failure'. Atomic shift gives clean per-sha
686
+ // ownership transfer even when both loops are draining concurrently.
687
+ while (pushedShasNeedingRelease.length > 0) {
688
+ const s = pushedShasNeedingRelease.shift();
689
+ try {
690
+ await releaseRemoteLock(lockOctokit, owner, repoName, s, outcome);
691
+ }
692
+ catch (err) {
693
+ fileLog({ level: 'warn', event: 'pushed_sha_release_failed', repo: `${owner}/${repoName}`, pr: prNumber, sha: s, error: err instanceof Error ? err.message : String(err) });
694
+ }
695
+ }
696
+ }
697
+ // workflow_complete fires exactly once per runWorkflow invocation, in the
698
+ // finally so it lands on both happy-path returns AND on caught exceptions.
699
+ // Closes the "no_followup vs crash" log ambiguity called out in
700
+ // prd.md:1145 §B (the analysis of 411 review_complete events on
701
+ // 2026-05-28 found 17.2% of initial reviews with no follow-up event —
702
+ // indistinguishable from a session crash without this event).
703
+ fileLog(buildWorkflowCompleteEvent({
704
+ owner, repoName, prNumber,
705
+ workflowId, workflowStart, stepsRun, results, workflowFailed,
706
+ round: ctx.round,
707
+ }));
708
+ }
709
+ }
710
+ //# sourceMappingURL=runner.js.map