@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,640 @@
1
+ import chalk from 'chalk';
2
+ // ── Constants ─────────────────────────────────────────────────────────────────
3
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
+ const BAR_FILLED = '█';
5
+ const BAR_EMPTY = '░';
6
+ // ── Helpers ───────────────────────────────────────────────────────────────────
7
+ // Fixed-width timestamp: always "HH:MM:SS AM/PM" (zero-padded hour) so columns stay aligned
8
+ export function fmtTime(d = new Date()) {
9
+ return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true });
10
+ }
11
+ // Width of a fmtTime() result — constant regardless of time of day ("01:00:00 AM".length = 11)
12
+ export const FMT_TIME_WIDTH = 11;
13
+ // Format milliseconds as human duration: "45s", "4m05s", "1h02m"
14
+ function fmtDuration(ms) {
15
+ const totalSec = Math.floor(ms / 1000);
16
+ const h = Math.floor(totalSec / 3600);
17
+ const m = Math.floor((totalSec % 3600) / 60);
18
+ const s = totalSec % 60;
19
+ if (h > 0)
20
+ return `${h}h${String(m).padStart(2, '0')}m`;
21
+ if (m > 0)
22
+ return `${m}m${String(s).padStart(2, '0')}s`;
23
+ return `${s}s`;
24
+ }
25
+ // Short HH:MM timestamp (no seconds) for the "started" label
26
+ function fmtStartTime(epochMs) {
27
+ return new Date(epochMs).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
28
+ }
29
+ // Format token count as a compact suffix: "(900)", "(1.2K)", "(1.5M)". Returns '' when undefined.
30
+ export function fmtTokens(n) {
31
+ if (n == null)
32
+ return '';
33
+ if (n < 1_000)
34
+ return `(${n})`;
35
+ if (n < 1_000_000)
36
+ return `(${(n / 1_000).toFixed(1).replace(/\.0$/, '')}K)`;
37
+ return `(${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M)`;
38
+ }
39
+ // Raw token count without surrounding parens: "900", "1.2K", "1.5M". Returns '' when undefined.
40
+ function fmtTokensRaw(n) {
41
+ if (n == null)
42
+ return '';
43
+ if (n < 1_000)
44
+ return `${n}`;
45
+ if (n < 1_000_000)
46
+ return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}K`;
47
+ return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
48
+ }
49
+ // Strip ANSI escape codes for visible-width calculations
50
+ function stripAnsi(s) {
51
+ // eslint-disable-next-line no-control-regex
52
+ return s.replace(/\x1B\[[0-9;]*m/g, '');
53
+ }
54
+ function truncate(s, max) {
55
+ return s.length <= max ? s : s.slice(0, max - 1) + '…';
56
+ }
57
+ function makeBar(filled, total, fillFn, emptyFn) {
58
+ const f = Math.max(0, Math.min(total, Math.round(filled)));
59
+ return fillFn(BAR_FILLED.repeat(f)) + emptyFn(BAR_EMPTY.repeat(total - f));
60
+ }
61
+ // Format "codex · thorough" tag; returns '' when neither field is set.
62
+ function fmtReviewerTag(reviewer, tier) {
63
+ if (!reviewer && !tier)
64
+ return '';
65
+ if (reviewer && tier)
66
+ return `${reviewer} · ${tier}`;
67
+ return reviewer ?? tier ?? '';
68
+ }
69
+ function locToFilled(loc) {
70
+ if (loc <= 0)
71
+ return 0;
72
+ if (loc <= 10)
73
+ return 1;
74
+ if (loc <= 50)
75
+ return 2;
76
+ if (loc <= 150)
77
+ return 3;
78
+ if (loc <= 300)
79
+ return 5;
80
+ if (loc <= 600)
81
+ return 6;
82
+ if (loc <= 1000)
83
+ return 7;
84
+ if (loc <= 2000)
85
+ return 8;
86
+ if (loc <= 4000)
87
+ return 9;
88
+ return 10;
89
+ }
90
+ function commentCountToFilled(n) {
91
+ if (n === 0)
92
+ return 0;
93
+ if (n <= 2)
94
+ return 2;
95
+ if (n <= 5)
96
+ return 3;
97
+ if (n <= 9)
98
+ return 4;
99
+ if (n <= 14)
100
+ return 5;
101
+ if (n <= 20)
102
+ return 6;
103
+ if (n <= 30)
104
+ return 7;
105
+ return 8;
106
+ }
107
+ function fixCountToFilled(n) {
108
+ if (n === 0)
109
+ return 0;
110
+ if (n === 1)
111
+ return 1;
112
+ if (n <= 3)
113
+ return 2;
114
+ if (n <= 6)
115
+ return 3;
116
+ if (n <= 10)
117
+ return 4;
118
+ if (n <= 20)
119
+ return 5;
120
+ return 6;
121
+ }
122
+ function resolveColor(spec) {
123
+ if (spec === 'dim')
124
+ return chalk.dim;
125
+ if (spec === 'bold')
126
+ return chalk.bold;
127
+ if (spec.startsWith('#'))
128
+ return chalk.hex(spec);
129
+ const method = chalk[spec];
130
+ if (typeof method === 'function')
131
+ return method;
132
+ return chalk.white;
133
+ }
134
+ function buildTheme(cfg) {
135
+ const empty = resolveColor(cfg.bar_empty);
136
+ return {
137
+ spinner: chalk.greenBright,
138
+ success: chalk.green,
139
+ warning: chalk.yellow,
140
+ error: chalk.red,
141
+ dim: chalk.dim,
142
+ accent: chalk.cyan,
143
+ barPRFill: resolveColor(cfg.bar_fill),
144
+ barEmpty: empty,
145
+ barCRApprove: resolveColor(cfg.cr_approve),
146
+ barCRNeedsWork: resolveColor(cfg.cr_needs_work),
147
+ barCRBlock: resolveColor(cfg.cr_block),
148
+ barFixFill: resolveColor(cfg.fix_fill),
149
+ separator: chalk.dim,
150
+ };
151
+ }
152
+ // ── PRBoard ───────────────────────────────────────────────────────────────────
153
+ const CONN_LOG_MAX = 6; // max connectivity log lines kept in memory
154
+ const FOLD_THRESHOLD = 3; // when completed count exceeds this, fold all completed PRs to 1 line
155
+ const WORKSPACE_MAX = 25; // when total slots exceed this, evict oldest completed to scrollback
156
+ export class PRBoard {
157
+ slots = new Map();
158
+ frameIdx = 0;
159
+ timer = null;
160
+ liveLines = 0;
161
+ liveContent = '';
162
+ isTTY = Boolean(process.stdout.isTTY);
163
+ connLog = [];
164
+ stats = {
165
+ prsReceived: 0,
166
+ crsCompleted: 0,
167
+ fixesApplied: 0,
168
+ errorsOccurred: 0,
169
+ crTotalMs: 0,
170
+ sessionStart: Date.now(),
171
+ };
172
+ tunnel = {
173
+ type: 'none', url: null, alive: false,
174
+ };
175
+ config = null;
176
+ steps = [];
177
+ theme = buildTheme({
178
+ bar_fill: 'blue', bar_empty: 'dim',
179
+ cr_approve: 'green', cr_needs_work: 'yellow', cr_block: 'red',
180
+ fix_fill: 'cyan',
181
+ });
182
+ // ── Public API ─────────────────────────────────────────────────────────────
183
+ setConfig(config, steps) {
184
+ this.config = config;
185
+ this.steps = steps;
186
+ this.theme = buildTheme(config.display.theme);
187
+ }
188
+ setTunnel(type, url, alive) {
189
+ this.tunnel = { type, url, alive };
190
+ }
191
+ start() {
192
+ if (!this.isTTY)
193
+ return;
194
+ this.timer = setInterval(() => {
195
+ this.frameIdx = (this.frameIdx + 1) % FRAMES.length;
196
+ this.redraw();
197
+ }, 80);
198
+ }
199
+ stop() {
200
+ if (this.timer) {
201
+ clearInterval(this.timer);
202
+ this.timer = null;
203
+ }
204
+ this.eraseLive();
205
+ }
206
+ addPR(key, prNumber, repo, branch, round) {
207
+ this.slots.set(key, { prNumber, repo, branch, label: 'cloning...', startedAt: Date.now(), phase: 'queued', round: round ?? 1 });
208
+ this.stats.prsReceived++;
209
+ }
210
+ updatePR(key, updates) {
211
+ const slot = this.slots.get(key);
212
+ if (!slot)
213
+ return;
214
+ if (updates.label !== undefined)
215
+ slot.label = updates.label;
216
+ if (updates.prLoc !== undefined)
217
+ slot.prLoc = updates.prLoc;
218
+ if (updates.phase !== undefined)
219
+ slot.phase = updates.phase;
220
+ if (updates.verdict !== undefined)
221
+ slot.verdict = updates.verdict;
222
+ if (updates.commentCount !== undefined)
223
+ slot.commentCount = updates.commentCount;
224
+ if (updates.fixCount !== undefined)
225
+ slot.fixCount = updates.fixCount;
226
+ if (updates.recheckVerdict !== undefined)
227
+ slot.recheckVerdict = updates.recheckVerdict;
228
+ if (updates.crTokens !== undefined)
229
+ slot.crTokens = updates.crTokens;
230
+ if (updates.recheckTokens !== undefined)
231
+ slot.recheckTokens = updates.recheckTokens;
232
+ if (updates.fixTokens !== undefined)
233
+ slot.fixTokens = updates.fixTokens;
234
+ if (updates.round !== undefined)
235
+ slot.round = updates.round;
236
+ if (updates.crReviewer !== undefined)
237
+ slot.crReviewer = updates.crReviewer;
238
+ if (updates.recheckReviewer !== undefined)
239
+ slot.recheckReviewer = updates.recheckReviewer;
240
+ if (updates.qualityTier !== undefined)
241
+ slot.qualityTier = updates.qualityTier;
242
+ }
243
+ completePR(key, data) {
244
+ const slot = this.slots.get(key);
245
+ if (!slot)
246
+ return;
247
+ slot.completedAt = Date.now();
248
+ slot.url = data.url;
249
+ slot.label = 'done';
250
+ const verdict = slot.verdict ?? null;
251
+ const fixCount = slot.fixCount;
252
+ if (verdict !== null || slot.phase === 'reviewed' || slot.phase === 'rechecked' || slot.phase === 'fixed') {
253
+ this.stats.crsCompleted++;
254
+ this.stats.crTotalMs += data.elapsedMs;
255
+ }
256
+ if (fixCount !== undefined && fixCount > 0)
257
+ this.stats.fixesApplied++;
258
+ // Non-TTY has no live block to re-render — emit the folded line to scrollback and drop the slot.
259
+ if (!this.isTTY) {
260
+ process.stdout.write(this.renderPRSlotFolded(slot) + '\n');
261
+ this.slots.delete(key);
262
+ }
263
+ }
264
+ failPR(key, error) {
265
+ const slot = this.slots.get(key);
266
+ this.slots.delete(key);
267
+ this.stats.errorsOccurred++;
268
+ if (slot) {
269
+ const ts = fmtTime();
270
+ this.printStatic(`${chalk.dim(ts)} PR #${slot.prNumber} ${chalk.red('✗')} ${error}`);
271
+ }
272
+ }
273
+ /** Print 1–2 static lines to scrollback (above the live block). */
274
+ log(line1, line2) {
275
+ // Prepend a blank line for 2-line events so consecutive entries don't blur together
276
+ this.printStatic(line2 ? `\n${line1}\n${line2}` : line1);
277
+ }
278
+ /** Record a connectivity event in the live section (tunnel/webhook events). */
279
+ logConnectivity(line) {
280
+ const ts = chalk.dim(fmtTime());
281
+ this.connLog.push(` ${ts} ${line}`);
282
+ if (this.connLog.length > CONN_LOG_MAX)
283
+ this.connLog.shift();
284
+ }
285
+ // ── Private: display ───────────────────────────────────────────────────────
286
+ printStatic(content) {
287
+ this.eraseLive();
288
+ process.stdout.write(content + '\n');
289
+ }
290
+ countRenderedLines(content, columns) {
291
+ const w = columns || 80;
292
+ return content.split('\n').reduce((sum, line) => {
293
+ return sum + Math.max(1, Math.ceil(stripAnsi(line).length / w));
294
+ }, 0);
295
+ }
296
+ eraseLive() {
297
+ if (this.liveLines > 0 && this.isTTY) {
298
+ // Recompute against current width in case terminal was resized since last write
299
+ const lines = this.countRenderedLines(this.liveContent, process.stdout.columns || 80);
300
+ process.stdout.write(`\x1B[${lines}A\x1B[0J`);
301
+ this.liveLines = 0;
302
+ this.liveContent = '';
303
+ }
304
+ }
305
+ writeLive(content) {
306
+ this.eraseLive();
307
+ process.stdout.write(content + '\n');
308
+ const w = process.stdout.columns || 80;
309
+ this.liveContent = content;
310
+ this.liveLines = this.countRenderedLines(content, w);
311
+ }
312
+ // ── Private: theme helpers ─────────────────────────────────────────────────
313
+ verdictBadge(v) {
314
+ const t = this.theme;
315
+ if (v === 'APPROVE')
316
+ return t.success('✅ APPROVE');
317
+ if (v === 'NEEDS WORK')
318
+ return t.warning('⚠ NEEDS WORK');
319
+ if (v === 'BLOCK')
320
+ return t.error('🚫 BLOCK');
321
+ return t.dim('—');
322
+ }
323
+ crFillFn(verdict) {
324
+ const t = this.theme;
325
+ if (verdict === 'APPROVE')
326
+ return t.barCRApprove;
327
+ if (verdict === 'BLOCK')
328
+ return t.barCRBlock;
329
+ return t.barCRNeedsWork;
330
+ }
331
+ crLabelFn(verdict) {
332
+ const t = this.theme;
333
+ if (verdict === 'APPROVE')
334
+ return t.barCRApprove;
335
+ if (verdict === 'BLOCK')
336
+ return t.barCRBlock;
337
+ return t.barCRNeedsWork;
338
+ }
339
+ // ── Private: render ────────────────────────────────────────────────────────
340
+ uptime() {
341
+ const totalSec = Math.floor((Date.now() - this.stats.sessionStart) / 1000);
342
+ const h = Math.floor(totalSec / 3600);
343
+ const m = Math.floor((totalSec % 3600) / 60);
344
+ const s = totalSec % 60;
345
+ if (h > 0)
346
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
347
+ return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
348
+ }
349
+ statsRow() {
350
+ const { prsReceived, crsCompleted, fixesApplied, errorsOccurred, crTotalMs } = this.stats;
351
+ const avgCr = crsCompleted > 0
352
+ ? ` │ avg CR: ${fmtDuration(Math.round(crTotalMs / crsCompleted))}`
353
+ : '';
354
+ const errorPart = errorsOccurred > 0
355
+ ? ` · ${chalk.red(`errors: ${errorsOccurred}`)}`
356
+ : '';
357
+ return `PRs: ${prsReceived} · CRs: ${crsCompleted}${errorPart} · fixes: ${fixesApplied}${avgCr}`;
358
+ }
359
+ renderPRSlot(slot, frame) {
360
+ const t = this.theme;
361
+ const w = process.stdout.columns || 80;
362
+ const isCompleted = slot.completedAt !== undefined;
363
+ const totalElapsedMs = isCompleted
364
+ ? slot.completedAt - slot.startedAt
365
+ : Date.now() - slot.startedAt;
366
+ const eSuffix = fmtDuration(totalElapsedMs);
367
+ // ── Line 1: identity <pad> started·elapsed phase-label ────────────────
368
+ const branch = truncate(slot.branch, 22);
369
+ const icon = isCompleted ? t.success('✓') : t.spinner(frame);
370
+ const phaseLabel = this.phaseLine1Label(slot, frame);
371
+ const timePart = isCompleted
372
+ ? t.dim(eSuffix)
373
+ : `${t.dim('started ' + fmtStartTime(slot.startedAt))} ${t.dim(eSuffix)}`;
374
+ const rightPart = `${timePart} ${phaseLabel}`;
375
+ const identityPlain = ` #${slot.prNumber} ${slot.repo} ${branch}`;
376
+ const l1Pad = Math.max(2, w - stripAnsi(identityPlain).length - stripAnsi(rightPart).length - 2);
377
+ const prNum = isCompleted ? t.dim(`#${slot.prNumber}`) : chalk.bold(`#${slot.prNumber}`);
378
+ const repoStr = isCompleted ? t.dim(slot.repo) : chalk.white(slot.repo);
379
+ const l1 = ` ${icon} ${prNum} ${repoStr} ${t.dim(branch)}` +
380
+ ' '.repeat(l1Pad) + rightPart;
381
+ // ── Line 2: PR | CR | Fix | Recheck pipeline ────────────────────────────────
382
+ const pipe = t.dim(' | ');
383
+ const prSection = slot.prLoc !== undefined
384
+ ? `PR ${makeBar(locToFilled(slot.prLoc), 10, t.barPRFill, t.barEmpty)} ${t.dim(String(slot.prLoc) + 'loc')}`
385
+ : `PR ${makeBar(0, 10, t.barPRFill, t.barEmpty)} ${t.dim('—')}`;
386
+ const crSection = this.renderCRSection(slot, frame);
387
+ // URL line — only shown for completed slots that have a URL. Without this,
388
+ // expanded completions (≤ FOLD_THRESHOLD) never surface the PR link in the
389
+ // live block; the URL is otherwise only rendered in the folded form.
390
+ const urlLine = isCompleted && slot.url
391
+ ? `\n ${t.dim('→')} ${t.accent(slot.url)}`
392
+ : '';
393
+ // Round 2+: skip Fix, collapse into compact recheck display
394
+ const round = slot.round ?? 1;
395
+ if (round >= 2) {
396
+ const recheckSection = this.renderRecheckSection(slot, frame);
397
+ const parts = [prSection, crSection];
398
+ if (recheckSection !== null)
399
+ parts.push(recheckSection);
400
+ return `${l1}\n${parts.join(pipe)}${urlLine}`;
401
+ }
402
+ const fixSection = this.renderFixSection(slot, frame);
403
+ const recheckSection = this.renderRecheckSection(slot, frame);
404
+ const parts = [prSection, crSection, fixSection];
405
+ if (recheckSection !== null)
406
+ parts.push(recheckSection);
407
+ return `${l1}\n${parts.join(pipe)}${urlLine}`;
408
+ }
409
+ phaseLine1Label(slot, frame) {
410
+ const t = this.theme;
411
+ if (slot.completedAt !== undefined)
412
+ return t.dim('done');
413
+ switch (slot.phase) {
414
+ case 'reviewing':
415
+ case 'rechecking':
416
+ case 'fixing':
417
+ return `${t.spinner(frame)} ${t.dim(slot.label)}`;
418
+ default:
419
+ return t.dim(slot.label);
420
+ }
421
+ }
422
+ renderCRSection(slot, frame) {
423
+ const t = this.theme;
424
+ if (slot.phase === 'reviewing') {
425
+ return `CR ${makeBar(0, 8, t.barPRFill, t.barEmpty)} ${t.spinner(frame)} ${t.dim('reviewing…')}`;
426
+ }
427
+ // Round 2+: CR ran in a prior round — show as static completed, not as queued/error
428
+ if ((slot.round ?? 1) >= 2 && slot.verdict === undefined) {
429
+ return `CR ${makeBar(8, 8, t.dim, t.dim)} ${t.dim('·')}`;
430
+ }
431
+ if (slot.verdict === undefined) {
432
+ return `CR ${makeBar(0, 8, t.barPRFill, t.barEmpty)} ${t.dim('queued')}`;
433
+ }
434
+ if (slot.verdict === null) {
435
+ return `CR ${makeBar(0, 8, t.barEmpty, t.barEmpty)} ${t.warning('⚠ no verdict')}`;
436
+ }
437
+ const crFill = this.crFillFn(slot.verdict);
438
+ const crLabel = this.crLabelFn(slot.verdict);
439
+ const count = slot.commentCount ?? 0;
440
+ const tokRaw = fmtTokensRaw(slot.crTokens);
441
+ const label = tokRaw
442
+ ? `${count} issues (${slot.verdict}, ${tokRaw})`
443
+ : `${count} issues (${slot.verdict})`;
444
+ const reviewerTag = fmtReviewerTag(slot.crReviewer, slot.qualityTier);
445
+ return `CR ${makeBar(commentCountToFilled(count), 8, crFill, t.barEmpty)} ${crLabel(label)}${reviewerTag ? ' ' + t.dim(reviewerTag) : ''}`;
446
+ }
447
+ renderFixSection(slot, frame) {
448
+ const t = this.theme;
449
+ const hasFixStep = this.steps.some(s => s.type === 'fix' || s.type === 'conflict-resolve');
450
+ if (!hasFixStep)
451
+ return `Fix ${t.dim('—')}`;
452
+ if (slot.phase === 'fixing') {
453
+ return `Fix ${makeBar(0, 6, t.barFixFill, t.barEmpty)} ${t.spinner(frame)} ${t.dim('applying…')}`;
454
+ }
455
+ if (slot.fixCount !== undefined) {
456
+ if (slot.fixCount === 0)
457
+ return `Fix ${makeBar(0, 6, t.barFixFill, t.barEmpty)} ${t.dim('— skipped')}`;
458
+ const tokRaw = fmtTokensRaw(slot.fixTokens);
459
+ return `Fix ${makeBar(fixCountToFilled(slot.fixCount), 6, t.barFixFill, t.barEmpty)} ${t.success('✓')} ${t.accent(String(slot.fixCount) + ' applied')}${tokRaw ? ' ' + t.dim(`(${tokRaw})`) : ''}`;
460
+ }
461
+ return `Fix ${makeBar(0, 6, t.barFixFill, t.barEmpty)} ${t.dim('queued')}`;
462
+ }
463
+ renderRecheckSection(slot, frame) {
464
+ const t = this.theme;
465
+ const round = slot.round ?? 1;
466
+ // Round 2+: compact "N ROUNDS" display regardless of workflow steps
467
+ if (round >= 2) {
468
+ const roundsLabel = `${round} ROUNDS`;
469
+ if (slot.phase === 'rechecking' || slot.phase === 'reviewing') {
470
+ return `${roundsLabel} ${makeBar(0, 5, t.barPRFill, t.barEmpty)} ${t.spinner(frame)} ${t.dim(`round ${round}…`)}`;
471
+ }
472
+ if (slot.recheckVerdict !== undefined && slot.recheckVerdict !== null) {
473
+ const fill = this.crFillFn(slot.recheckVerdict);
474
+ const label = this.crLabelFn(slot.recheckVerdict);
475
+ const tokRaw = fmtTokensRaw(slot.recheckTokens);
476
+ const roundLabel = tokRaw ? `${slot.recheckVerdict}, ${tokRaw}` : slot.recheckVerdict;
477
+ return `${roundsLabel} ${makeBar(0, 5, fill, t.barEmpty)} ${label(roundLabel)}`;
478
+ }
479
+ return `${roundsLabel} ${makeBar(0, 5, t.barPRFill, t.barEmpty)} ${t.dim('queued')}`;
480
+ }
481
+ const hasRecheckStep = this.steps.some(s => s.type === 'recheck');
482
+ if (!hasRecheckStep)
483
+ return null;
484
+ if (slot.phase === 'rechecking') {
485
+ return `Recheck ${makeBar(0, 5, t.barPRFill, t.barEmpty)} ${t.spinner(frame)} ${t.dim('reviewing…')}`;
486
+ }
487
+ if (slot.phase === 'rechecked') {
488
+ if (slot.recheckVerdict === undefined) {
489
+ return `Recheck ${makeBar(0, 5, t.barFixFill, t.barEmpty)} ${t.dim('— skipped')}`;
490
+ }
491
+ if (slot.recheckVerdict === null) {
492
+ return `Recheck ${makeBar(0, 5, t.barEmpty, t.barEmpty)} ${t.warning('⚠ no verdict')}`;
493
+ }
494
+ const fill = this.crFillFn(slot.recheckVerdict);
495
+ const label = this.crLabelFn(slot.recheckVerdict);
496
+ const tokRaw = fmtTokensRaw(slot.recheckTokens);
497
+ const recheckLabel = tokRaw ? `${slot.recheckVerdict}, ${tokRaw}` : slot.recheckVerdict;
498
+ const reviewerTag = fmtReviewerTag(slot.recheckReviewer, slot.qualityTier);
499
+ return `Recheck ${makeBar(0, 5, fill, t.barEmpty)} ${label(recheckLabel)}${reviewerTag ? ' ' + t.dim(reviewerTag) : ''}`;
500
+ }
501
+ return `Recheck ${makeBar(0, 5, t.barPRFill, t.barEmpty)} ${t.dim('queued')}`;
502
+ }
503
+ render() {
504
+ if (!this.config)
505
+ return '';
506
+ // When the workspace overflows, evict oldest completed slots to scrollback.
507
+ this.evictOverflow();
508
+ const t = this.theme;
509
+ const w = process.stdout.columns || 80;
510
+ // Use w-1 to prevent the exact-terminal-width cursor wrap ambiguity that
511
+ // causes the first char of the next line to appear at the end of the separator.
512
+ const sep = t.separator('─'.repeat(w - 1));
513
+ const configLines = this.renderConfigPanel();
514
+ const statsLines = this.renderStatsPanel();
515
+ const prLines = this.renderPRWorkspace();
516
+ return [
517
+ ...configLines,
518
+ sep,
519
+ ...statsLines,
520
+ sep,
521
+ ...prLines,
522
+ sep,
523
+ ].join('\n');
524
+ }
525
+ // ── Panels ─────────────────────────────────────────────────────────────────
526
+ renderConfigPanel() {
527
+ const t = this.theme;
528
+ const cfg = this.config;
529
+ const lines = [];
530
+ lines.push(` ${chalk.greenBright('●')} ${chalk.bold('crosscheck')} ${t.dim(`${cfg.mode} · ${cfg.quality.tier}`)}`);
531
+ const stepFlow = this.steps.map(s => s.name).join(t.dim(' → '));
532
+ lines.push(` ${t.dim('workflow:')} ${stepFlow}`);
533
+ const vendors = [];
534
+ if (cfg.vendors.claude.enabled)
535
+ vendors.push('claude');
536
+ if (cfg.vendors.codex.enabled)
537
+ vendors.push('codex');
538
+ lines.push(` ${t.dim('vendors: ')} ${vendors.join(t.dim(' · '))}`);
539
+ return lines;
540
+ }
541
+ renderStatsPanel() {
542
+ const t = this.theme;
543
+ const lines = [];
544
+ lines.push(` ${this.statsRow()} ${t.dim('│')} ${t.dim('↑')} ${this.uptime()}`);
545
+ const { type: tunnelType, url, alive } = this.tunnel;
546
+ const tunnelLabel = tunnelType === 'serve' ? 'endpoint:' : 'tunnel: ';
547
+ const tunnelDisplay = url
548
+ ? `${url.replace(/^https?:\/\//, '')} ${alive ? t.success('✓') : t.warning('⚠')}`
549
+ : t.dim('connecting...');
550
+ lines.push(` ${t.dim(tunnelLabel)} ${tunnelDisplay}`);
551
+ // Connectivity log: already prefixed with timestamps + indent in logConnectivity()
552
+ const activeConn = this.connLog.filter(l => l.trim());
553
+ lines.push(...activeConn);
554
+ return lines;
555
+ }
556
+ renderPRWorkspace() {
557
+ const t = this.theme;
558
+ const frame = FRAMES[this.frameIdx];
559
+ if (this.slots.size === 0) {
560
+ return [t.dim(' waiting for PRs...')];
561
+ }
562
+ let completedCount = 0;
563
+ for (const slot of this.slots.values()) {
564
+ if (slot.completedAt !== undefined)
565
+ completedCount++;
566
+ }
567
+ const foldCompleted = completedCount > FOLD_THRESHOLD;
568
+ const lines = [];
569
+ let prevWasExpanded = false;
570
+ let first = true;
571
+ for (const slot of this.slots.values()) {
572
+ const isCompleted = slot.completedAt !== undefined;
573
+ const useFolded = foldCompleted && isCompleted;
574
+ if (useFolded) {
575
+ lines.push(this.renderPRSlotFolded(slot));
576
+ prevWasExpanded = false;
577
+ }
578
+ else {
579
+ if (!first && prevWasExpanded)
580
+ lines.push('');
581
+ lines.push(this.renderPRSlot(slot, frame));
582
+ prevWasExpanded = true;
583
+ }
584
+ first = false;
585
+ }
586
+ return lines;
587
+ }
588
+ // ── Folded PR slot ─────────────────────────────────────────────────────────
589
+ renderPRSlotFolded(slot) {
590
+ const t = this.theme;
591
+ const elapsedMs = (slot.completedAt ?? Date.now()) - slot.startedAt;
592
+ const elapsed = fmtDuration(elapsedMs);
593
+ const branch = truncate(slot.branch, 22);
594
+ const parts = [];
595
+ // CR verdict
596
+ if (slot.verdict !== undefined && slot.verdict !== null) {
597
+ const crFn = this.crLabelFn(slot.verdict);
598
+ parts.push(`CR: ${crFn(slot.verdict)}`);
599
+ }
600
+ else if (slot.verdict === null) {
601
+ parts.push(t.warning('CR: ⚠'));
602
+ }
603
+ else if ((slot.round ?? 1) >= 2) {
604
+ parts.push(t.dim('CR: prior-round'));
605
+ }
606
+ // Fix count (when fixes were applied)
607
+ if (slot.fixCount !== undefined && slot.fixCount > 0) {
608
+ parts.push(t.accent(`fix ${slot.fixCount}`));
609
+ }
610
+ // Recheck verdict
611
+ if (slot.recheckVerdict !== undefined && slot.recheckVerdict !== null) {
612
+ const rFn = this.crLabelFn(slot.recheckVerdict);
613
+ parts.push(`recheck ${rFn(slot.recheckVerdict)}`);
614
+ }
615
+ const urlPart = slot.url ? ` ${t.dim('→')} ${t.accent(slot.url)}` : '';
616
+ const partsStr = parts.length > 0 ? parts.join(t.dim(' · ')) : t.dim('—');
617
+ return ` ${t.success('✓')} ${t.dim(`#${slot.prNumber}`)} ${t.dim(slot.repo)} ${t.dim(branch)} ${partsStr} ${t.dim(`(${elapsed})`)}${urlPart}`;
618
+ }
619
+ // ── Overflow eviction ──────────────────────────────────────────────────────
620
+ evictOverflow() {
621
+ if (this.slots.size <= WORKSPACE_MAX)
622
+ return;
623
+ let toEvict = this.slots.size - WORKSPACE_MAX;
624
+ for (const [key, slot] of this.slots) {
625
+ if (toEvict <= 0)
626
+ break;
627
+ if (slot.completedAt === undefined)
628
+ continue; // never evict active
629
+ this.printStatic(this.renderPRSlotFolded(slot));
630
+ this.slots.delete(key);
631
+ toEvict--;
632
+ }
633
+ }
634
+ redraw() {
635
+ const content = this.render();
636
+ if (content)
637
+ this.writeLive(content);
638
+ }
639
+ }
640
+ //# sourceMappingURL=board.js.map