@oddessentials/odd-ai-reviewers 1.0.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 (370) hide show
  1. package/README.md +190 -0
  2. package/dist/__tests__/hermetic-setup.d.ts +55 -0
  3. package/dist/__tests__/hermetic-setup.d.ts.map +1 -0
  4. package/dist/__tests__/hermetic-setup.js +62 -0
  5. package/dist/__tests__/hermetic-setup.js.map +1 -0
  6. package/dist/__tests__/test-utils/hermetic.d.ts +84 -0
  7. package/dist/__tests__/test-utils/hermetic.d.ts.map +1 -0
  8. package/dist/__tests__/test-utils/hermetic.js +147 -0
  9. package/dist/__tests__/test-utils/hermetic.js.map +1 -0
  10. package/dist/agents/ai_semantic_review.d.ts +12 -0
  11. package/dist/agents/ai_semantic_review.d.ts.map +1 -0
  12. package/dist/agents/ai_semantic_review.js +317 -0
  13. package/dist/agents/ai_semantic_review.js.map +1 -0
  14. package/dist/agents/control_flow/budget.d.ts +162 -0
  15. package/dist/agents/control_flow/budget.d.ts.map +1 -0
  16. package/dist/agents/control_flow/budget.js +331 -0
  17. package/dist/agents/control_flow/budget.js.map +1 -0
  18. package/dist/agents/control_flow/cfg-builder.d.ts +26 -0
  19. package/dist/agents/control_flow/cfg-builder.d.ts.map +1 -0
  20. package/dist/agents/control_flow/cfg-builder.js +776 -0
  21. package/dist/agents/control_flow/cfg-builder.js.map +1 -0
  22. package/dist/agents/control_flow/cfg-types.d.ts +186 -0
  23. package/dist/agents/control_flow/cfg-types.d.ts.map +1 -0
  24. package/dist/agents/control_flow/cfg-types.js +114 -0
  25. package/dist/agents/control_flow/cfg-types.js.map +1 -0
  26. package/dist/agents/control_flow/finding-generator.d.ts +118 -0
  27. package/dist/agents/control_flow/finding-generator.d.ts.map +1 -0
  28. package/dist/agents/control_flow/finding-generator.js +354 -0
  29. package/dist/agents/control_flow/finding-generator.js.map +1 -0
  30. package/dist/agents/control_flow/index.d.ts +39 -0
  31. package/dist/agents/control_flow/index.d.ts.map +1 -0
  32. package/dist/agents/control_flow/index.js +270 -0
  33. package/dist/agents/control_flow/index.js.map +1 -0
  34. package/dist/agents/control_flow/logger.d.ts +333 -0
  35. package/dist/agents/control_flow/logger.d.ts.map +1 -0
  36. package/dist/agents/control_flow/logger.js +607 -0
  37. package/dist/agents/control_flow/logger.js.map +1 -0
  38. package/dist/agents/control_flow/mitigation-detector.d.ts +207 -0
  39. package/dist/agents/control_flow/mitigation-detector.d.ts.map +1 -0
  40. package/dist/agents/control_flow/mitigation-detector.js +625 -0
  41. package/dist/agents/control_flow/mitigation-detector.js.map +1 -0
  42. package/dist/agents/control_flow/mitigation-patterns.d.ts +53 -0
  43. package/dist/agents/control_flow/mitigation-patterns.d.ts.map +1 -0
  44. package/dist/agents/control_flow/mitigation-patterns.js +620 -0
  45. package/dist/agents/control_flow/mitigation-patterns.js.map +1 -0
  46. package/dist/agents/control_flow/path-analyzer.d.ts +287 -0
  47. package/dist/agents/control_flow/path-analyzer.d.ts.map +1 -0
  48. package/dist/agents/control_flow/path-analyzer.js +695 -0
  49. package/dist/agents/control_flow/path-analyzer.js.map +1 -0
  50. package/dist/agents/control_flow/pattern-validator.d.ts +132 -0
  51. package/dist/agents/control_flow/pattern-validator.d.ts.map +1 -0
  52. package/dist/agents/control_flow/pattern-validator.js +420 -0
  53. package/dist/agents/control_flow/pattern-validator.js.map +1 -0
  54. package/dist/agents/control_flow/timeout-regex.d.ts +144 -0
  55. package/dist/agents/control_flow/timeout-regex.d.ts.map +1 -0
  56. package/dist/agents/control_flow/timeout-regex.js +339 -0
  57. package/dist/agents/control_flow/timeout-regex.js.map +1 -0
  58. package/dist/agents/control_flow/types.d.ts +782 -0
  59. package/dist/agents/control_flow/types.d.ts.map +1 -0
  60. package/dist/agents/control_flow/types.js +428 -0
  61. package/dist/agents/control_flow/types.js.map +1 -0
  62. package/dist/agents/control_flow/vulnerability-detector.d.ts +85 -0
  63. package/dist/agents/control_flow/vulnerability-detector.d.ts.map +1 -0
  64. package/dist/agents/control_flow/vulnerability-detector.js +493 -0
  65. package/dist/agents/control_flow/vulnerability-detector.js.map +1 -0
  66. package/dist/agents/date-utils.d.ts +19 -0
  67. package/dist/agents/date-utils.d.ts.map +1 -0
  68. package/dist/agents/date-utils.js +29 -0
  69. package/dist/agents/date-utils.js.map +1 -0
  70. package/dist/agents/index.d.ts +25 -0
  71. package/dist/agents/index.d.ts.map +1 -0
  72. package/dist/agents/index.js +50 -0
  73. package/dist/agents/index.js.map +1 -0
  74. package/dist/agents/json-utils.d.ts +34 -0
  75. package/dist/agents/json-utils.d.ts.map +1 -0
  76. package/dist/agents/json-utils.js +62 -0
  77. package/dist/agents/json-utils.js.map +1 -0
  78. package/dist/agents/local_llm.d.ts +24 -0
  79. package/dist/agents/local_llm.d.ts.map +1 -0
  80. package/dist/agents/local_llm.js +566 -0
  81. package/dist/agents/local_llm.js.map +1 -0
  82. package/dist/agents/metadata.d.ts +57 -0
  83. package/dist/agents/metadata.d.ts.map +1 -0
  84. package/dist/agents/metadata.js +45 -0
  85. package/dist/agents/metadata.js.map +1 -0
  86. package/dist/agents/opencode.d.ts +18 -0
  87. package/dist/agents/opencode.d.ts.map +1 -0
  88. package/dist/agents/opencode.js +364 -0
  89. package/dist/agents/opencode.js.map +1 -0
  90. package/dist/agents/path-filter.d.ts +25 -0
  91. package/dist/agents/path-filter.d.ts.map +1 -0
  92. package/dist/agents/path-filter.js +43 -0
  93. package/dist/agents/path-filter.js.map +1 -0
  94. package/dist/agents/pr_agent.d.ts +3 -0
  95. package/dist/agents/pr_agent.d.ts.map +1 -0
  96. package/dist/agents/pr_agent.js +312 -0
  97. package/dist/agents/pr_agent.js.map +1 -0
  98. package/dist/agents/retry.d.ts +12 -0
  99. package/dist/agents/retry.d.ts.map +1 -0
  100. package/dist/agents/retry.js +65 -0
  101. package/dist/agents/retry.js.map +1 -0
  102. package/dist/agents/reviewdog.d.ts +24 -0
  103. package/dist/agents/reviewdog.d.ts.map +1 -0
  104. package/dist/agents/reviewdog.js +259 -0
  105. package/dist/agents/reviewdog.js.map +1 -0
  106. package/dist/agents/security.d.ts +49 -0
  107. package/dist/agents/security.d.ts.map +1 -0
  108. package/dist/agents/security.js +302 -0
  109. package/dist/agents/security.js.map +1 -0
  110. package/dist/agents/semgrep.d.ts +8 -0
  111. package/dist/agents/semgrep.d.ts.map +1 -0
  112. package/dist/agents/semgrep.js +157 -0
  113. package/dist/agents/semgrep.js.map +1 -0
  114. package/dist/agents/types.d.ts +450 -0
  115. package/dist/agents/types.d.ts.map +1 -0
  116. package/dist/agents/types.js +127 -0
  117. package/dist/agents/types.js.map +1 -0
  118. package/dist/budget.d.ts +59 -0
  119. package/dist/budget.d.ts.map +1 -0
  120. package/dist/budget.js +82 -0
  121. package/dist/budget.js.map +1 -0
  122. package/dist/cache/key.d.ts +49 -0
  123. package/dist/cache/key.d.ts.map +1 -0
  124. package/dist/cache/key.js +71 -0
  125. package/dist/cache/key.js.map +1 -0
  126. package/dist/cache/store.d.ts +47 -0
  127. package/dist/cache/store.d.ts.map +1 -0
  128. package/dist/cache/store.js +328 -0
  129. package/dist/cache/store.js.map +1 -0
  130. package/dist/cli/commands/check.d.ts +60 -0
  131. package/dist/cli/commands/check.d.ts.map +1 -0
  132. package/dist/cli/commands/check.js +163 -0
  133. package/dist/cli/commands/check.js.map +1 -0
  134. package/dist/cli/commands/index.d.ts +12 -0
  135. package/dist/cli/commands/index.d.ts.map +1 -0
  136. package/dist/cli/commands/index.js +12 -0
  137. package/dist/cli/commands/index.js.map +1 -0
  138. package/dist/cli/commands/local-review.d.ts +149 -0
  139. package/dist/cli/commands/local-review.d.ts.map +1 -0
  140. package/dist/cli/commands/local-review.js +755 -0
  141. package/dist/cli/commands/local-review.js.map +1 -0
  142. package/dist/cli/config-wizard.d.ts +87 -0
  143. package/dist/cli/config-wizard.d.ts.map +1 -0
  144. package/dist/cli/config-wizard.js +240 -0
  145. package/dist/cli/config-wizard.js.map +1 -0
  146. package/dist/cli/dependencies/catalog.d.ts +44 -0
  147. package/dist/cli/dependencies/catalog.d.ts.map +1 -0
  148. package/dist/cli/dependencies/catalog.js +89 -0
  149. package/dist/cli/dependencies/catalog.js.map +1 -0
  150. package/dist/cli/dependencies/checker.d.ts +42 -0
  151. package/dist/cli/dependencies/checker.d.ts.map +1 -0
  152. package/dist/cli/dependencies/checker.js +240 -0
  153. package/dist/cli/dependencies/checker.js.map +1 -0
  154. package/dist/cli/dependencies/index.d.ts +16 -0
  155. package/dist/cli/dependencies/index.d.ts.map +1 -0
  156. package/dist/cli/dependencies/index.js +16 -0
  157. package/dist/cli/dependencies/index.js.map +1 -0
  158. package/dist/cli/dependencies/messages.d.ts +58 -0
  159. package/dist/cli/dependencies/messages.d.ts.map +1 -0
  160. package/dist/cli/dependencies/messages.js +183 -0
  161. package/dist/cli/dependencies/messages.js.map +1 -0
  162. package/dist/cli/dependencies/platform.d.ts +25 -0
  163. package/dist/cli/dependencies/platform.d.ts.map +1 -0
  164. package/dist/cli/dependencies/platform.js +42 -0
  165. package/dist/cli/dependencies/platform.js.map +1 -0
  166. package/dist/cli/dependencies/schemas.d.ts +65 -0
  167. package/dist/cli/dependencies/schemas.d.ts.map +1 -0
  168. package/dist/cli/dependencies/schemas.js +42 -0
  169. package/dist/cli/dependencies/schemas.js.map +1 -0
  170. package/dist/cli/dependencies/types.d.ts +112 -0
  171. package/dist/cli/dependencies/types.d.ts.map +1 -0
  172. package/dist/cli/dependencies/types.js +6 -0
  173. package/dist/cli/dependencies/types.js.map +1 -0
  174. package/dist/cli/dependencies/version.d.ts +67 -0
  175. package/dist/cli/dependencies/version.d.ts.map +1 -0
  176. package/dist/cli/dependencies/version.js +125 -0
  177. package/dist/cli/dependencies/version.js.map +1 -0
  178. package/dist/cli/git-context.d.ts +105 -0
  179. package/dist/cli/git-context.d.ts.map +1 -0
  180. package/dist/cli/git-context.js +313 -0
  181. package/dist/cli/git-context.js.map +1 -0
  182. package/dist/cli/interactive-prompts.d.ts +126 -0
  183. package/dist/cli/interactive-prompts.d.ts.map +1 -0
  184. package/dist/cli/interactive-prompts.js +128 -0
  185. package/dist/cli/interactive-prompts.js.map +1 -0
  186. package/dist/cli/options/index.d.ts +7 -0
  187. package/dist/cli/options/index.d.ts.map +1 -0
  188. package/dist/cli/options/index.js +11 -0
  189. package/dist/cli/options/index.js.map +1 -0
  190. package/dist/cli/options/local-review-options.d.ts +221 -0
  191. package/dist/cli/options/local-review-options.d.ts.map +1 -0
  192. package/dist/cli/options/local-review-options.js +332 -0
  193. package/dist/cli/options/local-review-options.js.map +1 -0
  194. package/dist/cli/output/colors.d.ts +154 -0
  195. package/dist/cli/output/colors.d.ts.map +1 -0
  196. package/dist/cli/output/colors.js +255 -0
  197. package/dist/cli/output/colors.js.map +1 -0
  198. package/dist/cli/output/errors.d.ts +157 -0
  199. package/dist/cli/output/errors.d.ts.map +1 -0
  200. package/dist/cli/output/errors.js +266 -0
  201. package/dist/cli/output/errors.js.map +1 -0
  202. package/dist/cli/output/index.d.ts +12 -0
  203. package/dist/cli/output/index.d.ts.map +1 -0
  204. package/dist/cli/output/index.js +15 -0
  205. package/dist/cli/output/index.js.map +1 -0
  206. package/dist/cli/output/progress.d.ts +237 -0
  207. package/dist/cli/output/progress.d.ts.map +1 -0
  208. package/dist/cli/output/progress.js +405 -0
  209. package/dist/cli/output/progress.js.map +1 -0
  210. package/dist/cli/signals.d.ts +145 -0
  211. package/dist/cli/signals.d.ts.map +1 -0
  212. package/dist/cli/signals.js +223 -0
  213. package/dist/cli/signals.js.map +1 -0
  214. package/dist/cli/validation-report.d.ts +106 -0
  215. package/dist/cli/validation-report.d.ts.map +1 -0
  216. package/dist/cli/validation-report.js +108 -0
  217. package/dist/cli/validation-report.js.map +1 -0
  218. package/dist/config/index.d.ts +9 -0
  219. package/dist/config/index.d.ts.map +1 -0
  220. package/dist/config/index.js +12 -0
  221. package/dist/config/index.js.map +1 -0
  222. package/dist/config/mitigation-config.d.ts +94 -0
  223. package/dist/config/mitigation-config.d.ts.map +1 -0
  224. package/dist/config/mitigation-config.js +430 -0
  225. package/dist/config/mitigation-config.js.map +1 -0
  226. package/dist/config/providers.d.ts +118 -0
  227. package/dist/config/providers.d.ts.map +1 -0
  228. package/dist/config/providers.js +229 -0
  229. package/dist/config/providers.js.map +1 -0
  230. package/dist/config/schemas.d.ts +278 -0
  231. package/dist/config/schemas.d.ts.map +1 -0
  232. package/dist/config/schemas.js +111 -0
  233. package/dist/config/schemas.js.map +1 -0
  234. package/dist/config/zero-config.d.ts +126 -0
  235. package/dist/config/zero-config.d.ts.map +1 -0
  236. package/dist/config/zero-config.js +243 -0
  237. package/dist/config/zero-config.js.map +1 -0
  238. package/dist/config.d.ts +110 -0
  239. package/dist/config.d.ts.map +1 -0
  240. package/dist/config.js +302 -0
  241. package/dist/config.js.map +1 -0
  242. package/dist/diff.d.ts +224 -0
  243. package/dist/diff.d.ts.map +1 -0
  244. package/dist/diff.js +832 -0
  245. package/dist/diff.js.map +1 -0
  246. package/dist/git-validators.d.ts +106 -0
  247. package/dist/git-validators.d.ts.map +1 -0
  248. package/dist/git-validators.js +224 -0
  249. package/dist/git-validators.js.map +1 -0
  250. package/dist/main.d.ts +61 -0
  251. package/dist/main.d.ts.map +1 -0
  252. package/dist/main.js +704 -0
  253. package/dist/main.js.map +1 -0
  254. package/dist/phases/execute.d.ts +60 -0
  255. package/dist/phases/execute.d.ts.map +1 -0
  256. package/dist/phases/execute.js +168 -0
  257. package/dist/phases/execute.js.map +1 -0
  258. package/dist/phases/index.d.ts +9 -0
  259. package/dist/phases/index.d.ts.map +1 -0
  260. package/dist/phases/index.js +9 -0
  261. package/dist/phases/index.js.map +1 -0
  262. package/dist/phases/preflight.d.ts +40 -0
  263. package/dist/phases/preflight.d.ts.map +1 -0
  264. package/dist/phases/preflight.js +122 -0
  265. package/dist/phases/preflight.js.map +1 -0
  266. package/dist/phases/report.d.ts +51 -0
  267. package/dist/phases/report.d.ts.map +1 -0
  268. package/dist/phases/report.js +152 -0
  269. package/dist/phases/report.js.map +1 -0
  270. package/dist/policy.d.ts +33 -0
  271. package/dist/policy.d.ts.map +1 -0
  272. package/dist/policy.js +34 -0
  273. package/dist/policy.js.map +1 -0
  274. package/dist/preflight.d.ts +181 -0
  275. package/dist/preflight.d.ts.map +1 -0
  276. package/dist/preflight.js +627 -0
  277. package/dist/preflight.js.map +1 -0
  278. package/dist/report/ado.d.ts +53 -0
  279. package/dist/report/ado.d.ts.map +1 -0
  280. package/dist/report/ado.js +411 -0
  281. package/dist/report/ado.js.map +1 -0
  282. package/dist/report/agent-icons.d.ts +36 -0
  283. package/dist/report/agent-icons.d.ts.map +1 -0
  284. package/dist/report/agent-icons.js +46 -0
  285. package/dist/report/agent-icons.js.map +1 -0
  286. package/dist/report/base.d.ts +30 -0
  287. package/dist/report/base.d.ts.map +1 -0
  288. package/dist/report/base.js +64 -0
  289. package/dist/report/base.js.map +1 -0
  290. package/dist/report/formats.d.ts +206 -0
  291. package/dist/report/formats.d.ts.map +1 -0
  292. package/dist/report/formats.js +481 -0
  293. package/dist/report/formats.js.map +1 -0
  294. package/dist/report/github.d.ts +44 -0
  295. package/dist/report/github.d.ts.map +1 -0
  296. package/dist/report/github.js +409 -0
  297. package/dist/report/github.js.map +1 -0
  298. package/dist/report/line-resolver.d.ts +208 -0
  299. package/dist/report/line-resolver.d.ts.map +1 -0
  300. package/dist/report/line-resolver.js +578 -0
  301. package/dist/report/line-resolver.js.map +1 -0
  302. package/dist/report/resolution.d.ts +158 -0
  303. package/dist/report/resolution.d.ts.map +1 -0
  304. package/dist/report/resolution.js +272 -0
  305. package/dist/report/resolution.js.map +1 -0
  306. package/dist/report/sanitize.d.ts +32 -0
  307. package/dist/report/sanitize.d.ts.map +1 -0
  308. package/dist/report/sanitize.js +84 -0
  309. package/dist/report/sanitize.js.map +1 -0
  310. package/dist/report/terminal.d.ts +440 -0
  311. package/dist/report/terminal.d.ts.map +1 -0
  312. package/dist/report/terminal.js +840 -0
  313. package/dist/report/terminal.js.map +1 -0
  314. package/dist/reviewignore.d.ts +125 -0
  315. package/dist/reviewignore.d.ts.map +1 -0
  316. package/dist/reviewignore.js +335 -0
  317. package/dist/reviewignore.js.map +1 -0
  318. package/dist/security-logger.d.ts +178 -0
  319. package/dist/security-logger.d.ts.map +1 -0
  320. package/dist/security-logger.js +256 -0
  321. package/dist/security-logger.js.map +1 -0
  322. package/dist/telemetry/backends/console.d.ts +24 -0
  323. package/dist/telemetry/backends/console.d.ts.map +1 -0
  324. package/dist/telemetry/backends/console.js +54 -0
  325. package/dist/telemetry/backends/console.js.map +1 -0
  326. package/dist/telemetry/backends/jsonl.d.ts +31 -0
  327. package/dist/telemetry/backends/jsonl.d.ts.map +1 -0
  328. package/dist/telemetry/backends/jsonl.js +121 -0
  329. package/dist/telemetry/backends/jsonl.js.map +1 -0
  330. package/dist/telemetry/emitter.d.ts +43 -0
  331. package/dist/telemetry/emitter.d.ts.map +1 -0
  332. package/dist/telemetry/emitter.js +83 -0
  333. package/dist/telemetry/emitter.js.map +1 -0
  334. package/dist/telemetry/hook.d.ts +53 -0
  335. package/dist/telemetry/hook.d.ts.map +1 -0
  336. package/dist/telemetry/hook.js +118 -0
  337. package/dist/telemetry/hook.js.map +1 -0
  338. package/dist/telemetry/index.d.ts +58 -0
  339. package/dist/telemetry/index.d.ts.map +1 -0
  340. package/dist/telemetry/index.js +143 -0
  341. package/dist/telemetry/index.js.map +1 -0
  342. package/dist/telemetry/types.d.ts +139 -0
  343. package/dist/telemetry/types.d.ts.map +1 -0
  344. package/dist/telemetry/types.js +133 -0
  345. package/dist/telemetry/types.js.map +1 -0
  346. package/dist/trust.d.ts +65 -0
  347. package/dist/trust.d.ts.map +1 -0
  348. package/dist/trust.js +78 -0
  349. package/dist/trust.js.map +1 -0
  350. package/dist/types/assert-never.d.ts +30 -0
  351. package/dist/types/assert-never.d.ts.map +1 -0
  352. package/dist/types/assert-never.js +32 -0
  353. package/dist/types/assert-never.js.map +1 -0
  354. package/dist/types/branded.d.ts +172 -0
  355. package/dist/types/branded.d.ts.map +1 -0
  356. package/dist/types/branded.js +262 -0
  357. package/dist/types/branded.js.map +1 -0
  358. package/dist/types/errors.d.ts +320 -0
  359. package/dist/types/errors.d.ts.map +1 -0
  360. package/dist/types/errors.js +551 -0
  361. package/dist/types/errors.js.map +1 -0
  362. package/dist/types/index.d.ts +37 -0
  363. package/dist/types/index.d.ts.map +1 -0
  364. package/dist/types/index.js +77 -0
  365. package/dist/types/index.js.map +1 -0
  366. package/dist/types/result.d.ts +323 -0
  367. package/dist/types/result.d.ts.map +1 -0
  368. package/dist/types/result.js +423 -0
  369. package/dist/types/result.js.map +1 -0
  370. package/package.json +63 -0
package/dist/main.js ADDED
@@ -0,0 +1,704 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AI Review Router - Main Entry Point
4
+ *
5
+ * Orchestrates multi-pass AI code review using phase modules.
6
+ * This is the orchestrator - actual logic lives in ./phases/*.ts
7
+ *
8
+ * To run tests against this module, use the exported `run()` function
9
+ * with a custom ExitHandler to avoid process.exit() calls.
10
+ */
11
+ import { Command } from 'commander';
12
+ import { fileURLToPath } from 'url';
13
+ import { realpathSync } from 'fs';
14
+ import { loadConfig } from './config.js';
15
+ import { checkTrust, buildADOPRContext } from './trust.js';
16
+ import { checkBudget, estimateTokens } from './budget.js';
17
+ import { getDiff, filterFiles, buildCombinedDiff, resolveReviewRefs, getGitHubCheckHeadSha, } from './diff.js';
18
+ import { loadReviewIgnore, shouldIgnoreFile } from './reviewignore.js';
19
+ import { startCheckRun } from './report/github.js';
20
+ import { buildRouterEnv } from './agents/security.js';
21
+ import { hashConfig } from './cache/key.js';
22
+ import { runPreflightChecks, executeAllPasses, processFindings, dispatchReport, checkGating, } from './phases/index.js';
23
+ /**
24
+ * Default exit handler that calls process.exit
25
+ */
26
+ export const defaultExitHandler = (code) => {
27
+ process.exit(code);
28
+ };
29
+ function exitSuccess(exitHandler) {
30
+ exitHandler(0);
31
+ }
32
+ // =============================================================================
33
+ // CLI Program
34
+ // =============================================================================
35
+ const program = new Command();
36
+ program
37
+ .name('ai-review')
38
+ .description('AI Code Review Router')
39
+ .version('1.0.0')
40
+ // Enable positional options mode to prevent global options from being parsed
41
+ // before subcommand options. This fixes the conflict between:
42
+ // - Root program's `--base <ref>` (for shorthand `ai-review .` usage)
43
+ // - Review subcommand's `--base <sha>` (for CI usage)
44
+ .enablePositionalOptions();
45
+ program
46
+ .command('review')
47
+ .description('Run AI review on a PR or commit range')
48
+ .requiredOption('--repo <path>', 'Path to repository')
49
+ .requiredOption('--base <sha>', 'Base commit SHA')
50
+ .requiredOption('--head <sha>', 'Head commit SHA')
51
+ .option('--pr <number>', 'PR number', parseInt)
52
+ .option('--owner <owner>', 'Repository owner (for GitHub API)')
53
+ .option('--repo-name <name>', 'Repository name (for GitHub API)')
54
+ .option('--dry-run', 'Run without posting results')
55
+ .action(async (options) => {
56
+ try {
57
+ await runReview(options);
58
+ }
59
+ catch (error) {
60
+ console.error('[router] Fatal error:', error);
61
+ defaultExitHandler(1);
62
+ }
63
+ });
64
+ program
65
+ .command('validate')
66
+ .description('Validate configuration file')
67
+ .requiredOption('--repo <path>', 'Path to repository')
68
+ .option('--json', 'Output validation result as JSON')
69
+ .action(async (options) => {
70
+ const { formatValidationReport, printValidationReport } = await import('./cli/validation-report.js');
71
+ try {
72
+ // T029: Call runPreflightChecks for validation
73
+ const config = await loadConfig(options.repo);
74
+ // Build minimal agent context for preflight checks
75
+ const env = process.env;
76
+ // Create minimal AgentContext for validation (no diff needed)
77
+ // T016 (FR-003): Use placeholder - preflight will resolve the actual model
78
+ const minimalContext = {
79
+ repoPath: options.repo,
80
+ diff: {
81
+ files: [],
82
+ totalAdditions: 0,
83
+ totalDeletions: 0,
84
+ baseSha: '',
85
+ headSha: '',
86
+ contextLines: 3,
87
+ source: 'local-git',
88
+ },
89
+ files: [],
90
+ config,
91
+ diffContent: '',
92
+ prNumber: undefined,
93
+ env,
94
+ effectiveModel: '', // Placeholder - preflight resolves the actual model
95
+ provider: null,
96
+ };
97
+ // T029: Run preflight checks
98
+ const preflightResult = runPreflightChecks(config, minimalContext, env, options.repo);
99
+ // T030: Format validation report
100
+ const report = formatValidationReport(preflightResult);
101
+ if (options.json) {
102
+ // JSON output for programmatic consumption
103
+ console.log(JSON.stringify(report, null, 2));
104
+ }
105
+ else {
106
+ // T031: Print human-readable report
107
+ printValidationReport(report);
108
+ }
109
+ // T032: Exit 1 on errors, 0 on warnings-only or success
110
+ defaultExitHandler(report.valid ? 0 : 1);
111
+ }
112
+ catch (error) {
113
+ // Config loading failed - this is an error
114
+ const errorMessage = error instanceof Error ? error.message : String(error);
115
+ console.error(`✗ ERROR: Failed to load configuration: ${errorMessage}`);
116
+ defaultExitHandler(1);
117
+ }
118
+ });
119
+ // Check command (Feature 001-local-deps-setup - Dependency validation)
120
+ program
121
+ .command('check')
122
+ .description('Check external dependency availability')
123
+ .option('--verbose', 'Show additional details (minimum version, docs URL)')
124
+ .option('--json', 'Output results in JSON format')
125
+ .action(async (options) => {
126
+ const { runCheck, formatCheckOutput, formatCheckOutputJson } = await import('./cli/commands/check.js');
127
+ const result = runCheck({ verbose: options.verbose, json: options.json });
128
+ if (options.json) {
129
+ console.log(formatCheckOutputJson(result.results));
130
+ }
131
+ else {
132
+ console.log(formatCheckOutput(result.results, { verbose: options.verbose ?? false }));
133
+ }
134
+ defaultExitHandler(result.exitCode);
135
+ });
136
+ // Config init command (Feature 015 - Interactive Configuration Wizard)
137
+ const configCommand = program.command('config').description('Configuration management commands');
138
+ configCommand
139
+ .command('init')
140
+ .description('Generate a new .ai-review.yml configuration file')
141
+ .option('--defaults', 'Use default settings without prompts')
142
+ .option('--yes', 'Alias for --defaults')
143
+ .option('--provider <provider>', 'LLM provider (openai, anthropic, azure-openai, ollama)')
144
+ .option('--platform <platform>', 'Platform (github, ado)', 'github')
145
+ .option('--output <path>', 'Output file path', '.ai-review.yml')
146
+ .action(async (options) => {
147
+ const configWizard = await import('./cli/config-wizard.js');
148
+ const { writeFile } = await import('fs/promises');
149
+ const { existsSync } = await import('fs');
150
+ const { createReadlineInterface, promptSelect, promptConfirm } = await import('./cli/interactive-prompts.js');
151
+ const { formatValidationReport, printValidationReport } = await import('./cli/validation-report.js');
152
+ const useDefaults = options.defaults || options.yes;
153
+ let provider;
154
+ let platform;
155
+ let agents;
156
+ const outputPath = options.output;
157
+ const defaultAgentsByProvider = {
158
+ openai: ['semgrep', 'opencode'],
159
+ anthropic: ['semgrep', 'opencode'],
160
+ 'azure-openai': ['semgrep', 'pr_agent'],
161
+ ollama: ['semgrep', 'local_llm'],
162
+ };
163
+ const allowedAgentsByProvider = {
164
+ openai: ['semgrep', 'reviewdog', 'opencode', 'pr_agent', 'ai_semantic_review', 'local_llm'],
165
+ anthropic: [
166
+ 'semgrep',
167
+ 'reviewdog',
168
+ 'opencode',
169
+ 'pr_agent',
170
+ 'ai_semantic_review',
171
+ 'local_llm',
172
+ ],
173
+ 'azure-openai': ['semgrep', 'reviewdog', 'pr_agent', 'ai_semantic_review', 'local_llm'],
174
+ ollama: ['semgrep', 'reviewdog', 'local_llm'],
175
+ };
176
+ // Check TTY for interactive mode (T022)
177
+ if (!useDefaults && !configWizard.isInteractiveTerminal()) {
178
+ console.error('Error: Interactive mode requires a TTY terminal.');
179
+ console.error('Use --defaults or --yes flag with --provider and --platform options.');
180
+ console.error('');
181
+ console.error('Example: ai-review config init --defaults --provider openai --platform github');
182
+ defaultExitHandler(1);
183
+ return;
184
+ }
185
+ // Interactive mode (T017-T021)
186
+ if (!useDefaults) {
187
+ const rl = createReadlineInterface();
188
+ // Handle Ctrl+C gracefully (T021)
189
+ rl.on('close', () => {
190
+ // If closed unexpectedly, exit 0 (user cancellation)
191
+ });
192
+ try {
193
+ console.log('Welcome to ai-review configuration wizard!\n');
194
+ // Platform selection (T017)
195
+ const platformOptions = configWizard.AVAILABLE_PLATFORMS.map((p) => ({
196
+ label: p.name,
197
+ value: p.id,
198
+ description: p.description,
199
+ }));
200
+ const platformResult = await promptSelect(rl, 'Select your platform:', platformOptions);
201
+ if (platformResult.status === 'cancelled') {
202
+ console.log('\nConfiguration cancelled.');
203
+ rl.close();
204
+ defaultExitHandler(0);
205
+ return;
206
+ }
207
+ // T039 (FR-011): Pass 'both' directly to generate dual reporting blocks
208
+ platform = platformResult.value;
209
+ // Provider selection (T018)
210
+ const providerOptions = configWizard.AVAILABLE_PROVIDERS.map((p) => ({
211
+ label: p.name,
212
+ value: p.id,
213
+ description: p.description,
214
+ }));
215
+ const providerResult = await promptSelect(rl, 'Select your LLM provider:', providerOptions);
216
+ if (providerResult.status === 'cancelled') {
217
+ console.log('\nConfiguration cancelled.');
218
+ rl.close();
219
+ defaultExitHandler(0);
220
+ return;
221
+ }
222
+ provider = providerResult.value;
223
+ // Agent selection with provider-appropriate defaults (T019)
224
+ const defaultAgents = defaultAgentsByProvider[provider];
225
+ const allowedAgents = allowedAgentsByProvider[provider];
226
+ const agentOptions = configWizard.AVAILABLE_AGENTS.filter((agent) => allowedAgents.includes(agent.id));
227
+ console.log('\nSelect agents to enable (press Enter to accept defaults):');
228
+ const selectedAgents = [];
229
+ for (const agent of agentOptions) {
230
+ const isRecommended = defaultAgents.includes(agent.id);
231
+ const descriptor = agent.description ? ` (${agent.description})` : '';
232
+ const label = `${agent.name}${descriptor}${isRecommended ? ' [recommended]' : ''}`;
233
+ const include = await promptConfirm(rl, `Include ${label}`, !isRecommended);
234
+ if (include) {
235
+ selectedAgents.push(agent.id);
236
+ }
237
+ }
238
+ if (selectedAgents.length === 0) {
239
+ console.log(`\nNo agents selected. Using recommended: ${defaultAgents.join(', ')}`);
240
+ agents = defaultAgents;
241
+ }
242
+ else {
243
+ agents = selectedAgents;
244
+ }
245
+ // Overwrite confirmation (T020)
246
+ if (existsSync(outputPath)) {
247
+ const overwrite = await promptConfirm(rl, `\nFile ${outputPath} exists. Overwrite?`, true);
248
+ if (!overwrite) {
249
+ console.log('Configuration cancelled.');
250
+ rl.close();
251
+ defaultExitHandler(0);
252
+ return;
253
+ }
254
+ }
255
+ rl.close();
256
+ }
257
+ catch {
258
+ // Ctrl+C or other error - exit 0 (T021)
259
+ rl.close();
260
+ console.log('\nConfiguration cancelled.');
261
+ defaultExitHandler(0);
262
+ return;
263
+ }
264
+ }
265
+ else {
266
+ // Non-interactive mode with --defaults
267
+ // Validate provider option
268
+ const validProviders = ['openai', 'anthropic', 'azure-openai', 'ollama'];
269
+ provider = (options.provider || 'openai');
270
+ if (!validProviders.includes(provider)) {
271
+ console.error(`[config init] Invalid provider: ${options.provider}`);
272
+ console.error(`Valid providers: ${validProviders.join(', ')}`);
273
+ defaultExitHandler(1);
274
+ return;
275
+ }
276
+ // Validate platform option (including 'both' for dual reporting)
277
+ const validPlatforms = ['github', 'ado', 'both'];
278
+ platform = (options.platform || 'github');
279
+ if (!validPlatforms.includes(platform)) {
280
+ console.error(`[config init] Invalid platform: ${options.platform}`);
281
+ console.error(`Valid platforms: ${validPlatforms.join(', ')}`);
282
+ defaultExitHandler(1);
283
+ return;
284
+ }
285
+ // Default agents based on provider
286
+ agents = defaultAgentsByProvider[provider];
287
+ // Check if output file already exists (no prompt in defaults mode)
288
+ if (existsSync(outputPath)) {
289
+ console.error(`[config init] File already exists: ${outputPath}`);
290
+ console.error('Remove the existing file or specify a different --output path.');
291
+ defaultExitHandler(1);
292
+ return;
293
+ }
294
+ }
295
+ // Generate config
296
+ const yaml = configWizard.generateConfigYaml({
297
+ provider,
298
+ platform,
299
+ agents,
300
+ useDefaults: true,
301
+ });
302
+ // Write to file
303
+ await writeFile(outputPath, yaml, 'utf-8');
304
+ console.log(`\n✓ Configuration written to ${outputPath}`);
305
+ // Run validation and show summary (US3 integration - T037-T040)
306
+ console.log('\nValidating configuration...');
307
+ try {
308
+ // Parse the generated YAML and merge with defaults to match loadConfig behavior.
309
+ // This ensures validation uses the same merged config that runtime will see.
310
+ const { parse: parseYaml } = await import('yaml');
311
+ const { ConfigSchema } = await import('./config/schemas.js');
312
+ const { loadDefaults, deepMerge } = await import('./config.js');
313
+ const generatedConfig = parseYaml(yaml);
314
+ const defaults = await loadDefaults();
315
+ const mergedConfig = deepMerge(defaults, generatedConfig);
316
+ const config = ConfigSchema.parse(mergedConfig);
317
+ const env = process.env;
318
+ // T030 (FR-009, FR-010): Build minimal AgentContext same pattern as validate command
319
+ // This fixes the P2 bug where undefined was passed, causing validation to crash
320
+ const minimalContext = {
321
+ repoPath: process.cwd(),
322
+ diff: {
323
+ files: [],
324
+ totalAdditions: 0,
325
+ totalDeletions: 0,
326
+ baseSha: '',
327
+ headSha: '',
328
+ contextLines: 3,
329
+ source: 'local-git',
330
+ },
331
+ files: [],
332
+ config,
333
+ diffContent: '',
334
+ prNumber: undefined,
335
+ env,
336
+ effectiveModel: '', // Placeholder - preflight resolves the actual model
337
+ provider: null,
338
+ };
339
+ // T031: Use same pattern as validate command
340
+ // Use outputPath (where config was written) for accurate reporting
341
+ const preflightResult = runPreflightChecks(config, minimalContext, env, outputPath);
342
+ const report = formatValidationReport(preflightResult);
343
+ printValidationReport(report);
344
+ // Show next steps
345
+ console.log('\nNext steps:');
346
+ if (provider === 'azure-openai') {
347
+ console.log(' 1. Set AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT');
348
+ console.log(' 2. Set MODEL=<your-deployment-name>');
349
+ console.log(" 3. Run 'ai-review review --repo .' to test");
350
+ }
351
+ else if (provider === 'openai') {
352
+ console.log(' 1. Set OPENAI_API_KEY environment variable');
353
+ console.log(" 2. Run 'ai-review review --repo .' to test");
354
+ }
355
+ else if (provider === 'anthropic') {
356
+ console.log(' 1. Set ANTHROPIC_API_KEY environment variable');
357
+ console.log(" 2. Run 'ai-review review --repo .' to test");
358
+ }
359
+ else if (provider === 'ollama') {
360
+ console.log(' 1. Ensure Ollama is running (or set OLLAMA_BASE_URL)');
361
+ console.log(" 2. Run 'ai-review review --repo .' to test");
362
+ }
363
+ // Exit based on validation result (T040)
364
+ defaultExitHandler(report.valid ? 0 : 1);
365
+ }
366
+ catch {
367
+ // Config validation failed but file was written
368
+ console.log('\n⚠ Could not validate config (this is expected for new projects)');
369
+ defaultExitHandler(0);
370
+ }
371
+ });
372
+ // =============================================================================
373
+ // Local Review Command (Phase 407 - T097-T113)
374
+ // =============================================================================
375
+ program
376
+ .command('local')
377
+ .alias('local-review') // T015 (US1): Add alias for command discoverability
378
+ .description('Run AI review on local changes (uncommitted/staged)')
379
+ .argument('[path]', 'Path to repository (default: current directory)', '.')
380
+ .option('--base <ref>', 'Base reference for comparison (auto-detected if not specified)')
381
+ .option('--head <ref>', 'Head reference (default: HEAD)')
382
+ .option('--range <range>', 'Git range (e.g., main...HEAD, HEAD~3..)\n' +
383
+ ' Operators: ... (default) = merge-base, .. = direct comparison')
384
+ .option('--staged', 'Review only staged changes')
385
+ .option('--uncommitted', 'Include uncommitted changes (default when no --base/--range)')
386
+ .option('--pass <name>', 'Run specific pass only')
387
+ .option('--agent <id>', 'Run specific agent only')
388
+ .option('--format <fmt>', 'Output format: pretty, json, sarif (default: pretty)', 'pretty')
389
+ .option('--no-color', 'Disable colored output')
390
+ .option('--quiet', 'Minimal output (errors only)')
391
+ .option('--verbose', 'Show debug information')
392
+ .option('--dry-run', 'Show what would be reviewed without running agents')
393
+ .option('--cost-only', 'Estimate cost without running agents')
394
+ .option('-c, --config <path>', 'Path to config file')
395
+ .action(async (path, options) => {
396
+ // Dynamically import to avoid circular dependencies
397
+ const { runLocalReview, createDefaultDependencies } = await import('./cli/commands/local-review.js');
398
+ const deps = createDefaultDependencies();
399
+ // Build raw options object matching RawLocalReviewOptions
400
+ const rawOptions = {
401
+ path,
402
+ base: options.base,
403
+ head: options.head,
404
+ range: options.range,
405
+ staged: options.staged,
406
+ uncommitted: options.uncommitted,
407
+ pass: options.pass,
408
+ agent: options.agent,
409
+ format: options.format,
410
+ noColor: options.noColor,
411
+ color: options.color, // Commander sets color=false for --no-color
412
+ quiet: options.quiet,
413
+ verbose: options.verbose,
414
+ dryRun: options.dryRun,
415
+ costOnly: options.costOnly,
416
+ config: options.config,
417
+ };
418
+ try {
419
+ const result = await runLocalReview(rawOptions, deps);
420
+ deps.exitHandler(result.exitCode);
421
+ }
422
+ catch (error) {
423
+ console.error('[local] Fatal error:', error);
424
+ deps.exitHandler(1);
425
+ }
426
+ });
427
+ // Alias: `ai-review .` as shorthand for `ai-review local .`
428
+ // This provides the zero-friction experience: npx @oddessentials/odd-ai-reviewers .
429
+ program
430
+ .argument('[path]', 'Path to repository for local review')
431
+ .option('--base <ref>', 'Base reference for comparison')
432
+ .option('--head <ref>', 'Head reference (default: HEAD)')
433
+ .option('--range <range>', 'Git range (e.g., main...HEAD, HEAD~3..)\n' +
434
+ ' Operators: ... (default) = merge-base, .. = direct comparison')
435
+ .option('--staged', 'Review only staged changes')
436
+ .option('--uncommitted', 'Include uncommitted changes (default when no --base/--range)')
437
+ .option('--pass <name>', 'Run specific pass only')
438
+ .option('--agent <id>', 'Run specific agent only')
439
+ .option('--format <fmt>', 'Output format: pretty, json, sarif')
440
+ .option('--no-color', 'Disable colored output')
441
+ .option('--quiet', 'Minimal output (errors only)')
442
+ .option('--verbose', 'Show debug information')
443
+ .option('--dry-run', 'Show what would be reviewed without running agents')
444
+ .option('--cost-only', 'Estimate cost without running agents')
445
+ .option('-c, --config <path>', 'Path to config file')
446
+ .action(async (path, options) => {
447
+ // Skip if no path provided and no relevant options - let Commander show help
448
+ // This handles the case where user runs `ai-review` with no arguments
449
+ if (!path && !options.base && !options.staged && !options.range) {
450
+ program.outputHelp();
451
+ return;
452
+ }
453
+ const { runLocalReview, createDefaultDependencies } = await import('./cli/commands/local-review.js');
454
+ const deps = createDefaultDependencies();
455
+ const rawOptions = {
456
+ path: path ?? '.',
457
+ base: options.base,
458
+ head: options.head,
459
+ range: options.range,
460
+ staged: options.staged,
461
+ uncommitted: options.uncommitted,
462
+ pass: options.pass,
463
+ agent: options.agent,
464
+ format: options.format,
465
+ noColor: options.noColor,
466
+ color: options.color,
467
+ quiet: options.quiet,
468
+ verbose: options.verbose,
469
+ dryRun: options.dryRun,
470
+ costOnly: options.costOnly,
471
+ config: options.config,
472
+ };
473
+ try {
474
+ const result = await runLocalReview(rawOptions, deps);
475
+ deps.exitHandler(result.exitCode);
476
+ }
477
+ catch (error) {
478
+ console.error('[local] Fatal error:', error);
479
+ deps.exitHandler(1);
480
+ }
481
+ });
482
+ /**
483
+ * Detect the CI platform from environment variables
484
+ */
485
+ export function detectPlatform(env) {
486
+ if (env['GITHUB_ACTIONS'] === 'true')
487
+ return 'github';
488
+ if (env['TF_BUILD'] === 'True' || env['SYSTEM_TEAMFOUNDATIONCOLLECTIONURI'])
489
+ return 'ado';
490
+ return 'unknown';
491
+ }
492
+ /**
493
+ * Main review orchestration function.
494
+ *
495
+ * Flow:
496
+ * 1. Load config and build contexts
497
+ * 2. Check trust, get diff, filter files
498
+ * 3. Run preflight validation
499
+ * 4. Execute agent passes
500
+ * 5. Process and report findings
501
+ * 6. Check gating
502
+ *
503
+ * @param options - Review options (repo, base, head, etc.)
504
+ * @param deps - Injectable dependencies for testing
505
+ */
506
+ export async function runReview(options, deps = {}) {
507
+ const env = deps.env ?? process.env;
508
+ const exitHandler = deps.exitHandler ?? defaultExitHandler;
509
+ console.log('[router] Starting AI Review');
510
+ console.log(`[router] Repository: ${options.repo}`);
511
+ console.log(`[router] Diff: ${options.base}...${options.head}`);
512
+ // === PHASE 1: Setup & Context Building ===
513
+ const routerEnv = buildRouterEnv(env);
514
+ const config = await loadConfig(options.repo);
515
+ console.log(`[router] Loaded config with ${config.passes.length} passes`);
516
+ const configHash = hashConfig(config);
517
+ // Load .reviewignore patterns
518
+ const reviewIgnoreResult = await loadReviewIgnore(options.repo);
519
+ const reviewIgnorePatterns = reviewIgnoreResult.patterns;
520
+ const platform = detectPlatform(routerEnv);
521
+ console.log(`[router] Detected platform: ${platform}`);
522
+ // Build PR context based on platform
523
+ let prContext;
524
+ if (platform === 'ado') {
525
+ const adoContext = buildADOPRContext(routerEnv);
526
+ if (!adoContext) {
527
+ console.log('[router] Not running in ADO PR context - skipping review');
528
+ exitSuccess(exitHandler);
529
+ return;
530
+ }
531
+ prContext = adoContext;
532
+ }
533
+ else {
534
+ const headRepo = routerEnv['GITHUB_HEAD_REPO'];
535
+ const baseRepo = routerEnv['GITHUB_REPOSITORY'];
536
+ const isFork = headRepo !== undefined && headRepo !== '' && headRepo !== baseRepo;
537
+ prContext = {
538
+ number: options.pr ?? 0,
539
+ headRepo: options.owner && options.repoName ? `${options.owner}/${options.repoName}` : '',
540
+ baseRepo: options.owner && options.repoName ? `${options.owner}/${options.repoName}` : '',
541
+ author: routerEnv['GITHUB_ACTOR'] ?? 'unknown',
542
+ isFork,
543
+ isDraft: routerEnv['GITHUB_EVENT_NAME'] === 'pull_request' &&
544
+ routerEnv['GITHUB_EVENT_PULL_REQUEST_DRAFT'] === 'true',
545
+ };
546
+ }
547
+ // === PHASE 2: Trust & Diff ===
548
+ const trustResult = checkTrust(prContext, config);
549
+ if (!trustResult.trusted) {
550
+ console.log(`[router] Skipping review: ${trustResult.reason}`);
551
+ exitSuccess(exitHandler);
552
+ return;
553
+ }
554
+ console.log('[router] Resolving review refs...');
555
+ // Resolve base/head refs to SHAs for stable cache keys and accurate diff mapping.
556
+ const reviewRefs = resolveReviewRefs(options.repo, options.base, options.head);
557
+ if (reviewRefs.headSource === 'merge-parent') {
558
+ console.log(`[router] Using PR head SHA ${reviewRefs.headSha} for review`);
559
+ }
560
+ const githubHeadSha = getGitHubCheckHeadSha(reviewRefs);
561
+ if (platform === 'github' && reviewRefs.headSource === 'merge-parent') {
562
+ console.log(`[router] Using merge commit SHA ${githubHeadSha} for GitHub checks`);
563
+ }
564
+ console.log('[router] Extracting diff...');
565
+ const diff = getDiff(options.repo, reviewRefs.baseSha, reviewRefs.headSha);
566
+ console.log(`[router] Found ${diff.files.length} changed files (${diff.totalAdditions}+ / ${diff.totalDeletions}-)`);
567
+ // Count .reviewignore exclusions separately (count-only pre-pass)
568
+ const ignoredByReviewIgnore = reviewIgnorePatterns.length > 0
569
+ ? diff.files.filter((f) => shouldIgnoreFile(f.path, reviewIgnorePatterns)).length
570
+ : 0;
571
+ // Combine path_filters from config with .reviewignore patterns
572
+ // Filter precedence (applied in filterFiles):
573
+ // 1. .reviewignore patterns (excludes matching files)
574
+ // 2. path_filters.exclude (excludes additional files)
575
+ // 3. path_filters.include (if set, only keeps matching files - whitelist)
576
+ const pathFilter = {
577
+ ...config.path_filters,
578
+ reviewIgnorePatterns,
579
+ };
580
+ const filteredFiles = filterFiles(diff.files, pathFilter);
581
+ // Calculate path_filters exclusions (approximate if there's overlap)
582
+ const totalExcluded = diff.files.length - filteredFiles.length;
583
+ const ignoredByPathFilters = Math.max(0, totalExcluded - ignoredByReviewIgnore);
584
+ // Log filtering results with breakdown
585
+ console.log(`[router] ${filteredFiles.length} files after filtering`);
586
+ if (ignoredByReviewIgnore > 0) {
587
+ console.log(`[router] - ${ignoredByReviewIgnore} excluded by .reviewignore`);
588
+ }
589
+ if (ignoredByPathFilters > 0) {
590
+ console.log(`[router] - ${ignoredByPathFilters} excluded by path_filters`);
591
+ }
592
+ if (filteredFiles.length === 0) {
593
+ console.log('[router] No files to review after filtering');
594
+ exitSuccess(exitHandler);
595
+ return;
596
+ }
597
+ // Start GitHub check run (in_progress state)
598
+ let checkRunId;
599
+ const reportingMode = config.reporting.github?.mode ?? 'checks_and_comments';
600
+ const shouldUseChecks = reportingMode === 'checks_only' || reportingMode === 'checks_and_comments';
601
+ if (shouldUseChecks &&
602
+ !options.dryRun &&
603
+ options.owner &&
604
+ options.repoName &&
605
+ routerEnv['GITHUB_TOKEN']) {
606
+ try {
607
+ checkRunId = await startCheckRun({
608
+ owner: options.owner,
609
+ repo: options.repoName,
610
+ headSha: githubHeadSha,
611
+ token: routerEnv['GITHUB_TOKEN'],
612
+ });
613
+ }
614
+ catch (error) {
615
+ console.warn('[router] Failed to start check run:', error);
616
+ }
617
+ }
618
+ // === PHASE 3: Budget & Agent Context ===
619
+ const diffContent = buildCombinedDiff(filteredFiles, config.limits.max_diff_lines);
620
+ const estimatedTokenCount = estimateTokens(diffContent);
621
+ const budgetContext = {
622
+ fileCount: filteredFiles.length,
623
+ diffLines: diff.totalAdditions + diff.totalDeletions,
624
+ estimatedTokens: estimatedTokenCount,
625
+ };
626
+ const budgetCheck = checkBudget(budgetContext, config.limits);
627
+ if (!budgetCheck.allowed) {
628
+ console.warn(`[router] Budget exceeded: ${budgetCheck.reason}`);
629
+ }
630
+ const agentContext = {
631
+ repoPath: options.repo,
632
+ diff,
633
+ files: filteredFiles,
634
+ config,
635
+ diffContent,
636
+ prNumber: options.pr,
637
+ env: routerEnv,
638
+ // T016 (FR-003): Use placeholder - preflight will resolve the actual model
639
+ effectiveModel: '',
640
+ provider: null, // Resolved per-agent in execute phase
641
+ };
642
+ // === PHASE 4: Preflight Validation ===
643
+ const preflightResult = runPreflightChecks(config, agentContext, env);
644
+ if (!preflightResult.valid) {
645
+ console.error('[router] ❌ Preflight validation failed:');
646
+ for (const error of preflightResult.errors) {
647
+ console.error(`[router] - ${error}`);
648
+ }
649
+ exitHandler(1);
650
+ return; // For type safety when exitHandler doesn't terminate
651
+ }
652
+ // T015 (FR-002, FR-004): Update agentContext with resolved model from preflight
653
+ // This is the single source of truth - no re-resolution after preflight
654
+ if (preflightResult.resolved) {
655
+ agentContext.effectiveModel = preflightResult.resolved.model;
656
+ }
657
+ // === PHASE 5: Execute Agent Passes ===
658
+ const executeResult = await executeAllPasses(config, agentContext, routerEnv, budgetCheck, {
659
+ pr: options.pr,
660
+ head: reviewRefs.headSha,
661
+ configHash,
662
+ });
663
+ // === PHASE 6: Process & Report Findings ===
664
+ // (012-fix-agent-result-regressions) - Now passing completeFindings and partialFindings separately
665
+ const { sorted, partialSorted } = processFindings(executeResult.completeFindings, executeResult.partialFindings, executeResult.allResults, executeResult.skippedAgents);
666
+ await dispatchReport(platform, sorted, partialSorted, config, diff.files, routerEnv, prContext.number, {
667
+ dryRun: options.dryRun,
668
+ owner: options.owner,
669
+ repoName: options.repoName,
670
+ pr: options.pr,
671
+ head: reviewRefs.headSha,
672
+ githubHeadSha,
673
+ checkRunId,
674
+ });
675
+ // === PHASE 7: Gating ===
676
+ // FR-008: checkGating receives only completeFindings (sorted) - partial findings don't affect gating
677
+ checkGating(config, sorted);
678
+ console.log('[router] Review complete');
679
+ exitSuccess(exitHandler);
680
+ }
681
+ // Only parse arguments when run directly (not when imported for testing)
682
+ // This allows tests to import runReview without triggering CLI parsing
683
+ // Use realpathSync to resolve npm bin shims (symlinks) to their real paths
684
+ function isMainModule() {
685
+ if (!process.argv[1])
686
+ return false;
687
+ try {
688
+ const scriptPath = fileURLToPath(import.meta.url);
689
+ const argvPath = process.argv[1];
690
+ // Try resolving symlinks for both paths
691
+ const realScriptPath = realpathSync(scriptPath);
692
+ const realArgvPath = realpathSync(argvPath);
693
+ return realScriptPath === realArgvPath;
694
+ }
695
+ catch {
696
+ // If realpathSync fails (e.g., path doesn't exist during testing),
697
+ // fall back to direct comparison
698
+ return fileURLToPath(import.meta.url) === process.argv[1];
699
+ }
700
+ }
701
+ if (isMainModule()) {
702
+ program.parse();
703
+ }
704
+ //# sourceMappingURL=main.js.map