infraweaver 0.1.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 (524) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +244 -0
  3. package/dist/agents/claude.d.ts +113 -0
  4. package/dist/agents/claudePretoolGate.d.ts +137 -0
  5. package/dist/agents/dispatchMetrics.d.ts +77 -0
  6. package/dist/agents/gateServer.d.ts +7 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/nativeFsDenies.d.ts +46 -0
  9. package/dist/agents/opencode.d.ts +284 -0
  10. package/dist/agents/opencodePlugin.d.ts +96 -0
  11. package/dist/agents/opencodeShared.d.ts +40 -0
  12. package/dist/agents/postRun.d.ts +126 -0
  13. package/dist/agents/reviewer.d.ts +41 -0
  14. package/dist/agents/sessionLabeler.d.ts +97 -0
  15. package/dist/agents/shared.d.ts +246 -0
  16. package/dist/agents/subagentModels.d.ts +19 -0
  17. package/dist/agents/tokenQuota.d.ts +118 -0
  18. package/dist/agents/writeGateSource.d.ts +22 -0
  19. package/dist/agents/writePolicy.d.ts +79 -0
  20. package/dist/brand.d.ts +67 -0
  21. package/dist/cli.mjs +247846 -0
  22. package/dist/external.d.ts +227 -0
  23. package/dist/i18n/scaffolding.d.ts +59 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +247061 -0
  26. package/dist/internal/index.d.ts +18 -0
  27. package/dist/internal.js +2398 -0
  28. package/dist/lifecycle.d.ts +2 -0
  29. package/dist/main.d.ts +8 -0
  30. package/dist/mcp/arkConfig.d.ts +1 -0
  31. package/dist/mcp/assess.d.ts +157 -0
  32. package/dist/mcp/capabilityContext.d.ts +71 -0
  33. package/dist/mcp/changeSummary.d.ts +52 -0
  34. package/dist/mcp/checkSuite.d.ts +27 -0
  35. package/dist/mcp/checkout.d.ts +92 -0
  36. package/dist/mcp/comment.d.ts +127 -0
  37. package/dist/mcp/commitInfo.d.ts +11 -0
  38. package/dist/mcp/crosswalk.d.ts +178 -0
  39. package/dist/mcp/crosswalkDigest.d.ts +1 -0
  40. package/dist/mcp/cyberEssentials.d.ts +24 -0
  41. package/dist/mcp/dashboard.d.ts +125 -0
  42. package/dist/mcp/dependencies.d.ts +12 -0
  43. package/dist/mcp/frameworks.d.ts +74 -0
  44. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  45. package/dist/mcp/git.d.ts +60 -0
  46. package/dist/mcp/guardrails.d.ts +222 -0
  47. package/dist/mcp/issue.d.ts +20 -0
  48. package/dist/mcp/issueComments.d.ts +11 -0
  49. package/dist/mcp/issueEvents.d.ts +11 -0
  50. package/dist/mcp/issueInfo.d.ts +11 -0
  51. package/dist/mcp/labels.d.ts +14 -0
  52. package/dist/mcp/localContext.d.ts +26 -0
  53. package/dist/mcp/moduleExtraction.d.ts +79 -0
  54. package/dist/mcp/moduleTests.d.ts +106 -0
  55. package/dist/mcp/modules.d.ts +198 -0
  56. package/dist/mcp/output.d.ts +16 -0
  57. package/dist/mcp/pathSafety.d.ts +14 -0
  58. package/dist/mcp/policy.d.ts +50 -0
  59. package/dist/mcp/pr.d.ts +64 -0
  60. package/dist/mcp/prInfo.d.ts +11 -0
  61. package/dist/mcp/providerSchema.d.ts +52 -0
  62. package/dist/mcp/review.d.ts +212 -0
  63. package/dist/mcp/reviewComments.d.ts +245 -0
  64. package/dist/mcp/roots.d.ts +60 -0
  65. package/dist/mcp/scope.d.ts +26 -0
  66. package/dist/mcp/selectMode.d.ts +20 -0
  67. package/dist/mcp/server.d.ts +61 -0
  68. package/dist/mcp/shared.d.ts +62 -0
  69. package/dist/mcp/shell.d.ts +58 -0
  70. package/dist/mcp/staleFix.d.ts +138 -0
  71. package/dist/mcp/terraform/azurePrices.d.ts +8 -0
  72. package/dist/mcp/terraform/baseline.d.ts +57 -0
  73. package/dist/mcp/terraform/concernResult.d.ts +38 -0
  74. package/dist/mcp/terraform/cost.d.ts +55 -0
  75. package/dist/mcp/terraform/cspm.d.ts +80 -0
  76. package/dist/mcp/terraform/currency.d.ts +110 -0
  77. package/dist/mcp/terraform/decisions.d.ts +178 -0
  78. package/dist/mcp/terraform/eidas.d.ts +100 -0
  79. package/dist/mcp/terraform/evidence.d.ts +343 -0
  80. package/dist/mcp/terraform/findings.d.ts +76 -0
  81. package/dist/mcp/terraform/fixMemory.d.ts +81 -0
  82. package/dist/mcp/terraform/governance.d.ts +56 -0
  83. package/dist/mcp/terraform/hcl.d.ts +115 -0
  84. package/dist/mcp/terraform/iacLanguages.d.ts +120 -0
  85. package/dist/mcp/terraform/idleFloor.d.ts +78 -0
  86. package/dist/mcp/terraform/keyless.d.ts +68 -0
  87. package/dist/mcp/terraform/moduleDocs.d.ts +56 -0
  88. package/dist/mcp/terraform/nativeRules.d.ts +38 -0
  89. package/dist/mcp/terraform/nativeScan.d.ts +11 -0
  90. package/dist/mcp/terraform/noiseProfile.d.ts +100 -0
  91. package/dist/mcp/terraform/normalization.d.ts +52 -0
  92. package/dist/mcp/terraform/oscal.d.ts +103 -0
  93. package/dist/mcp/terraform/packs/azurerm-3-4.d.ts +2 -0
  94. package/dist/mcp/terraform/packs/cloudflare-4-5.d.ts +2 -0
  95. package/dist/mcp/terraform/paths.d.ts +28 -0
  96. package/dist/mcp/terraform/plan.d.ts +157 -0
  97. package/dist/mcp/terraform/planDrift.d.ts +199 -0
  98. package/dist/mcp/terraform/policyAuthor.d.ts +191 -0
  99. package/dist/mcp/terraform/prowlerOcsf.d.ts +136 -0
  100. package/dist/mcp/terraform/refactor.d.ts +183 -0
  101. package/dist/mcp/terraform/registry.d.ts +108 -0
  102. package/dist/mcp/terraform/risk.d.ts +41 -0
  103. package/dist/mcp/terraform/scanSession.d.ts +116 -0
  104. package/dist/mcp/terraform/scannerCache.d.ts +15 -0
  105. package/dist/mcp/terraform/scannerEgress.d.ts +72 -0
  106. package/dist/mcp/terraform/scannerJson.d.ts +13 -0
  107. package/dist/mcp/terraform/scanners.d.ts +205 -0
  108. package/dist/mcp/terraform/signing.d.ts +61 -0
  109. package/dist/mcp/terraform/stateLocking.d.ts +18 -0
  110. package/dist/mcp/terraform/subprocess.d.ts +43 -0
  111. package/dist/mcp/terraform/suppressions.d.ts +90 -0
  112. package/dist/mcp/terraform/taxonomy.d.ts +39 -0
  113. package/dist/mcp/terraform/tfquery.d.ts +47 -0
  114. package/dist/mcp/terraform/toolchainIntegrity.d.ts +82 -0
  115. package/dist/mcp/terraform/tools/authorPolicy.d.ts +59 -0
  116. package/dist/mcp/terraform/tools/detectPlanDrift.d.ts +20 -0
  117. package/dist/mcp/terraform/tools/emitCost.d.ts +11 -0
  118. package/dist/mcp/terraform/tools/emitSarif.d.ts +14 -0
  119. package/dist/mcp/terraform/tools/fixMemory.d.ts +19 -0
  120. package/dist/mcp/terraform/tools/infracostDiff.d.ts +22 -0
  121. package/dist/mcp/terraform/tools/ingestExternalFindings.d.ts +21 -0
  122. package/dist/mcp/terraform/tools/moduleLookup.d.ts +14 -0
  123. package/dist/mcp/terraform/tools/moduleSearch.d.ts +23 -0
  124. package/dist/mcp/terraform/tools/plan.d.ts +59 -0
  125. package/dist/mcp/terraform/tools/providerUpgrade.d.ts +55 -0
  126. package/dist/mcp/terraform/tools/readFindings.d.ts +17 -0
  127. package/dist/mcp/terraform/tools/scan.d.ts +114 -0
  128. package/dist/mcp/terraform/tools/validate.d.ts +31 -0
  129. package/dist/mcp/terraform/tools/verifyRemediation.d.ts +31 -0
  130. package/dist/mcp/terraform/tools/versionCurrency.d.ts +5 -0
  131. package/dist/mcp/terraform/tools.d.ts +16 -0
  132. package/dist/mcp/terraform/transformPacks.d.ts +267 -0
  133. package/dist/mcp/terraform/types.d.ts +226 -0
  134. package/dist/mcp/terraform/verification.d.ts +81 -0
  135. package/dist/mcp/terraform/vex.d.ts +115 -0
  136. package/dist/mcp/terraform/writeOnly.d.ts +67 -0
  137. package/dist/mcp/terraform.d.ts +51 -0
  138. package/dist/mcp/terratest.d.ts +85 -0
  139. package/dist/mcp/untrustedContent.d.ts +76 -0
  140. package/dist/mcp/upload.d.ts +8 -0
  141. package/dist/models.d.ts +181 -0
  142. package/dist/modes/address-reviews.d.ts +2 -0
  143. package/dist/modes/assess.d.ts +2 -0
  144. package/dist/modes/build.d.ts +2 -0
  145. package/dist/modes/compliance-audit.d.ts +2 -0
  146. package/dist/modes/cost-optimization.d.ts +2 -0
  147. package/dist/modes/drift-detect.d.ts +2 -0
  148. package/dist/modes/fix.d.ts +2 -0
  149. package/dist/modes/incremental-review.d.ts +2 -0
  150. package/dist/modes/index.d.ts +24 -0
  151. package/dist/modes/modernize-deprecated.d.ts +2 -0
  152. package/dist/modes/plan.d.ts +2 -0
  153. package/dist/modes/policy-gate.d.ts +2 -0
  154. package/dist/modes/prFormats.d.ts +3 -0
  155. package/dist/modes/refactor.d.ts +2 -0
  156. package/dist/modes/refresh-remediation.d.ts +2 -0
  157. package/dist/modes/remediate-and-refactor.d.ts +2 -0
  158. package/dist/modes/remediate.d.ts +2 -0
  159. package/dist/modes/resolve-conflicts.d.ts +2 -0
  160. package/dist/modes/review.d.ts +2 -0
  161. package/dist/modes/summarize-pr.d.ts +2 -0
  162. package/dist/modes/task.d.ts +2 -0
  163. package/dist/modes/terraform-code-review.d.ts +2 -0
  164. package/dist/modes/types.d.ts +9 -0
  165. package/dist/modes/update-dependencies.d.ts +2 -0
  166. package/dist/prep/index.d.ts +7 -0
  167. package/dist/prep/installNodeDependencies.d.ts +2 -0
  168. package/dist/prep/installPythonDependencies.d.ts +2 -0
  169. package/dist/prep/types.d.ts +31 -0
  170. package/dist/reviewQuality.d.ts +107 -0
  171. package/dist/skills/terraform-best-practices/SKILL.md +379 -0
  172. package/dist/toolState.d.ts +123 -0
  173. package/dist/utils/activity.d.ts +40 -0
  174. package/dist/utils/agent.d.ts +47 -0
  175. package/dist/utils/agentHangReport.d.ts +38 -0
  176. package/dist/utils/annotations.d.ts +17 -0
  177. package/dist/utils/apiFetch.d.ts +11 -0
  178. package/dist/utils/apiKeys.d.ts +48 -0
  179. package/dist/utils/apiUrl.d.ts +28 -0
  180. package/dist/utils/assets.d.ts +8 -0
  181. package/dist/utils/baseRefConfig.d.ts +121 -0
  182. package/dist/utils/billingErrors.d.ts +85 -0
  183. package/dist/utils/body.d.ts +34 -0
  184. package/dist/utils/buildInfraweaverFooter.d.ts +29 -0
  185. package/dist/utils/byokFallback.d.ts +85 -0
  186. package/dist/utils/changeImpact.d.ts +101 -0
  187. package/dist/utils/claudeSubscription.d.ts +30 -0
  188. package/dist/utils/cli.d.ts +10 -0
  189. package/dist/utils/cloudClient.d.ts +33 -0
  190. package/dist/utils/cloudFetchCore.d.ts +42 -0
  191. package/dist/utils/cloudReport.d.ts +114 -0
  192. package/dist/utils/codexHome.d.ts +29 -0
  193. package/dist/utils/codexOAuth.d.ts +60 -0
  194. package/dist/utils/diffCoverage.d.ts +63 -0
  195. package/dist/utils/env.d.ts +46 -0
  196. package/dist/utils/errorReport.d.ts +17 -0
  197. package/dist/utils/exitHandler.d.ts +8 -0
  198. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  199. package/dist/utils/gitAuth.d.ts +84 -0
  200. package/dist/utils/gitAuthServer.d.ts +24 -0
  201. package/dist/utils/github.d.ts +104 -0
  202. package/dist/utils/globals.d.ts +3 -0
  203. package/dist/utils/infraweaverConfig.d.ts +56 -0
  204. package/dist/utils/install.d.ts +37 -0
  205. package/dist/utils/instructions.d.ts +48 -0
  206. package/dist/utils/keylessOidc.d.ts +28 -0
  207. package/dist/utils/leapingComment.d.ts +11 -0
  208. package/dist/utils/learnings.d.ts +62 -0
  209. package/dist/utils/learningsTruncate.d.ts +25 -0
  210. package/dist/utils/lifecycle.d.ts +57 -0
  211. package/dist/utils/log.d.ts +111 -0
  212. package/dist/utils/modelResolver.d.ts +63 -0
  213. package/dist/utils/moduleFetch.d.ts +39 -0
  214. package/dist/utils/normalizeEnv.d.ts +30 -0
  215. package/dist/utils/openCodeModels.d.ts +11 -0
  216. package/dist/utils/openaiCompatible.d.ts +26 -0
  217. package/dist/utils/opentofuMcp.d.ts +50 -0
  218. package/dist/utils/overrides.d.ts +40 -0
  219. package/dist/utils/packageManager.d.ts +49 -0
  220. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  221. package/dist/utils/payload.d.ts +249 -0
  222. package/dist/utils/prSummary.d.ts +61 -0
  223. package/dist/utils/presets.d.ts +36 -0
  224. package/dist/utils/progressComment.d.ts +146 -0
  225. package/dist/utils/providerErrors.d.ts +31 -0
  226. package/dist/utils/proxyModel.d.ts +76 -0
  227. package/dist/utils/rangeDiff.d.ts +51 -0
  228. package/dist/utils/redact.d.ts +33 -0
  229. package/dist/utils/registryAuth.d.ts +52 -0
  230. package/dist/utils/remediationCommand.d.ts +58 -0
  231. package/dist/utils/retry.d.ts +38 -0
  232. package/dist/utils/reviewCleanup.d.ts +14 -0
  233. package/dist/utils/run.d.ts +9 -0
  234. package/dist/utils/runContext.d.ts +112 -0
  235. package/dist/utils/runContextData.d.ts +42 -0
  236. package/dist/utils/runErrorRenderer.d.ts +64 -0
  237. package/dist/utils/runLifecycle.d.ts +89 -0
  238. package/dist/utils/runStartupLog.d.ts +15 -0
  239. package/dist/utils/secrets.d.ts +34 -0
  240. package/dist/utils/setup.d.ts +90 -0
  241. package/dist/utils/shell.d.ts +44 -0
  242. package/dist/utils/skills.d.ts +10 -0
  243. package/dist/utils/subprocess.d.ts +81 -0
  244. package/dist/utils/terraformMcp.d.ts +46 -0
  245. package/dist/utils/time.d.ts +15 -0
  246. package/dist/utils/timer.d.ts +23 -0
  247. package/dist/utils/todoTracking.d.ts +16 -0
  248. package/dist/utils/token.d.ts +52 -0
  249. package/dist/utils/toolLicensing.d.ts +56 -0
  250. package/dist/utils/toolSelection.d.ts +73 -0
  251. package/dist/utils/toon.d.ts +16 -0
  252. package/dist/utils/version.d.ts +2 -0
  253. package/dist/utils/versioning.d.ts +7 -0
  254. package/dist/utils/vertex.d.ts +16 -0
  255. package/dist/utils/workflow.d.ts +13 -0
  256. package/package.json +132 -0
  257. package/src/agents/claude.ts +1588 -0
  258. package/src/agents/claudePretoolGate.ts +286 -0
  259. package/src/agents/dispatchMetrics.ts +162 -0
  260. package/src/agents/gateServer.ts +178 -0
  261. package/src/agents/index.ts +10 -0
  262. package/src/agents/nativeFsDenies.ts +164 -0
  263. package/src/agents/opencode.ts +1600 -0
  264. package/src/agents/opencodePlugin.ts +304 -0
  265. package/src/agents/opencodeShared.ts +134 -0
  266. package/src/agents/postRun.ts +615 -0
  267. package/src/agents/reviewer.ts +134 -0
  268. package/src/agents/sessionLabeler.ts +185 -0
  269. package/src/agents/shared.ts +377 -0
  270. package/src/agents/subagentModels.ts +40 -0
  271. package/src/agents/tokenQuota.ts +203 -0
  272. package/src/agents/writeGateSource.ts +148 -0
  273. package/src/agents/writePolicy.ts +132 -0
  274. package/src/brand.ts +84 -0
  275. package/src/cli.ts +117 -0
  276. package/src/commands/gha.ts +188 -0
  277. package/src/commands/mcp.ts +126 -0
  278. package/src/commands/verifyEvidence.ts +100 -0
  279. package/src/entry.ts +7 -0
  280. package/src/entryPost.ts +109 -0
  281. package/src/external.ts +308 -0
  282. package/src/i18n/scaffolding.ts +191 -0
  283. package/src/index.ts +7 -0
  284. package/src/internal/index.ts +74 -0
  285. package/src/lifecycle.ts +2 -0
  286. package/src/main.ts +815 -0
  287. package/src/mcp/__fixtures__/infraweaver-scratch-pr-49-review-3485940013.json +110 -0
  288. package/src/mcp/__fixtures__/infraweaver-scratch-pr-64-review-3531000326.json +14 -0
  289. package/src/mcp/__fixtures__/infraweaver-test-repo-pr-1.diff.json +67 -0
  290. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  291. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  292. package/src/mcp/arkConfig.ts +7 -0
  293. package/src/mcp/assess.ts +723 -0
  294. package/src/mcp/capabilityContext.ts +80 -0
  295. package/src/mcp/changeSummary.ts +145 -0
  296. package/src/mcp/checkSuite.ts +261 -0
  297. package/src/mcp/checkout.ts +1015 -0
  298. package/src/mcp/comment.ts +683 -0
  299. package/src/mcp/commitInfo.ts +57 -0
  300. package/src/mcp/crosswalk.ts +1253 -0
  301. package/src/mcp/crosswalkDigest.ts +5 -0
  302. package/src/mcp/cyberEssentials.ts +60 -0
  303. package/src/mcp/dashboard.ts +232 -0
  304. package/src/mcp/dependencies.ts +190 -0
  305. package/src/mcp/frameworks.ts +236 -0
  306. package/src/mcp/geminiSanitizer.ts +212 -0
  307. package/src/mcp/git.ts +1116 -0
  308. package/src/mcp/guardrails.ts +771 -0
  309. package/src/mcp/issue.ts +74 -0
  310. package/src/mcp/issueComments.ts +48 -0
  311. package/src/mcp/issueEvents.ts +100 -0
  312. package/src/mcp/issueInfo.ts +72 -0
  313. package/src/mcp/labels.ts +35 -0
  314. package/src/mcp/localContext.ts +65 -0
  315. package/src/mcp/localServer.ts +217 -0
  316. package/src/mcp/moduleExtraction.ts +368 -0
  317. package/src/mcp/moduleTests.ts +421 -0
  318. package/src/mcp/modules.ts +752 -0
  319. package/src/mcp/output.ts +71 -0
  320. package/src/mcp/pathSafety.ts +28 -0
  321. package/src/mcp/policy.ts +226 -0
  322. package/src/mcp/pr.ts +238 -0
  323. package/src/mcp/prInfo.ts +91 -0
  324. package/src/mcp/providerSchema.ts +175 -0
  325. package/src/mcp/review.ts +1081 -0
  326. package/src/mcp/reviewComments.ts +1137 -0
  327. package/src/mcp/roots.ts +217 -0
  328. package/src/mcp/scope.ts +82 -0
  329. package/src/mcp/selectMode.ts +207 -0
  330. package/src/mcp/server.ts +498 -0
  331. package/src/mcp/shared.ts +120 -0
  332. package/src/mcp/shell.ts +631 -0
  333. package/src/mcp/staleFix.ts +557 -0
  334. package/src/mcp/terraform/__snapshots__/moduleDocs.test.ts.snap +40 -0
  335. package/src/mcp/terraform/azurePrices.ts +38 -0
  336. package/src/mcp/terraform/baseline.ts +172 -0
  337. package/src/mcp/terraform/concernResult.ts +111 -0
  338. package/src/mcp/terraform/cost.ts +175 -0
  339. package/src/mcp/terraform/cspm.ts +326 -0
  340. package/src/mcp/terraform/currency.ts +354 -0
  341. package/src/mcp/terraform/decisions.ts +590 -0
  342. package/src/mcp/terraform/eidas.ts +134 -0
  343. package/src/mcp/terraform/evidence.ts +771 -0
  344. package/src/mcp/terraform/findings.ts +383 -0
  345. package/src/mcp/terraform/fixMemory.ts +253 -0
  346. package/src/mcp/terraform/governance.ts +231 -0
  347. package/src/mcp/terraform/hcl.ts +273 -0
  348. package/src/mcp/terraform/iacLanguages.ts +431 -0
  349. package/src/mcp/terraform/idleFloor.ts +304 -0
  350. package/src/mcp/terraform/keyless.ts +123 -0
  351. package/src/mcp/terraform/moduleDocs.ts +303 -0
  352. package/src/mcp/terraform/nativeRules.ts +152 -0
  353. package/src/mcp/terraform/nativeScan.ts +67 -0
  354. package/src/mcp/terraform/noiseProfile.ts +183 -0
  355. package/src/mcp/terraform/normalization.ts +265 -0
  356. package/src/mcp/terraform/oscal.ts +283 -0
  357. package/src/mcp/terraform/packs/azurerm-3-4.ts +382 -0
  358. package/src/mcp/terraform/packs/cloudflare-4-5.ts +114 -0
  359. package/src/mcp/terraform/paths.ts +59 -0
  360. package/src/mcp/terraform/plan.ts +377 -0
  361. package/src/mcp/terraform/planDrift.ts +520 -0
  362. package/src/mcp/terraform/policyAuthor.ts +886 -0
  363. package/src/mcp/terraform/prowlerOcsf.ts +380 -0
  364. package/src/mcp/terraform/refactor.ts +879 -0
  365. package/src/mcp/terraform/registry.ts +317 -0
  366. package/src/mcp/terraform/risk.ts +99 -0
  367. package/src/mcp/terraform/scanSession.ts +242 -0
  368. package/src/mcp/terraform/scannerCache.ts +86 -0
  369. package/src/mcp/terraform/scannerEgress.ts +141 -0
  370. package/src/mcp/terraform/scannerJson.ts +22 -0
  371. package/src/mcp/terraform/scanners.ts +1134 -0
  372. package/src/mcp/terraform/signing.ts +151 -0
  373. package/src/mcp/terraform/stateLocking.ts +106 -0
  374. package/src/mcp/terraform/subprocess.ts +102 -0
  375. package/src/mcp/terraform/suppressions.ts +551 -0
  376. package/src/mcp/terraform/taxonomy.ts +0 -0
  377. package/src/mcp/terraform/tfquery.ts +159 -0
  378. package/src/mcp/terraform/toolchainIntegrity.ts +151 -0
  379. package/src/mcp/terraform/tools/authorPolicy.ts +185 -0
  380. package/src/mcp/terraform/tools/detectPlanDrift.ts +279 -0
  381. package/src/mcp/terraform/tools/emitCost.ts +130 -0
  382. package/src/mcp/terraform/tools/emitSarif.ts +108 -0
  383. package/src/mcp/terraform/tools/fixMemory.ts +82 -0
  384. package/src/mcp/terraform/tools/infracostDiff.ts +110 -0
  385. package/src/mcp/terraform/tools/ingestExternalFindings.ts +259 -0
  386. package/src/mcp/terraform/tools/moduleLookup.ts +86 -0
  387. package/src/mcp/terraform/tools/moduleSearch.ts +46 -0
  388. package/src/mcp/terraform/tools/plan.ts +384 -0
  389. package/src/mcp/terraform/tools/providerUpgrade.ts +217 -0
  390. package/src/mcp/terraform/tools/readFindings.ts +138 -0
  391. package/src/mcp/terraform/tools/scan.ts +303 -0
  392. package/src/mcp/terraform/tools/validate.ts +160 -0
  393. package/src/mcp/terraform/tools/verifyRemediation.ts +190 -0
  394. package/src/mcp/terraform/tools/versionCurrency.ts +64 -0
  395. package/src/mcp/terraform/tools.ts +62 -0
  396. package/src/mcp/terraform/transformPacks.ts +594 -0
  397. package/src/mcp/terraform/types.ts +382 -0
  398. package/src/mcp/terraform/verification.ts +150 -0
  399. package/src/mcp/terraform/vex.ts +367 -0
  400. package/src/mcp/terraform/writeOnly.ts +252 -0
  401. package/src/mcp/terraform.ts +52 -0
  402. package/src/mcp/terratest.ts +215 -0
  403. package/src/mcp/untrustedContent.ts +126 -0
  404. package/src/mcp/upload.ts +123 -0
  405. package/src/models.ts +753 -0
  406. package/src/modes/address-reviews.ts +35 -0
  407. package/src/modes/assess.ts +27 -0
  408. package/src/modes/build.ts +86 -0
  409. package/src/modes/compliance-audit.ts +25 -0
  410. package/src/modes/cost-optimization.ts +36 -0
  411. package/src/modes/drift-detect.ts +31 -0
  412. package/src/modes/fix.ts +29 -0
  413. package/src/modes/incremental-review.ts +103 -0
  414. package/src/modes/index.ts +94 -0
  415. package/src/modes/modernize-deprecated.ts +36 -0
  416. package/src/modes/plan.ts +21 -0
  417. package/src/modes/policy-gate.ts +27 -0
  418. package/src/modes/prFormats.ts +325 -0
  419. package/src/modes/refactor.ts +74 -0
  420. package/src/modes/refresh-remediation.ts +40 -0
  421. package/src/modes/remediate-and-refactor.ts +42 -0
  422. package/src/modes/remediate.ts +78 -0
  423. package/src/modes/resolve-conflicts.ts +32 -0
  424. package/src/modes/review.ts +135 -0
  425. package/src/modes/summarize-pr.ts +42 -0
  426. package/src/modes/task.ts +26 -0
  427. package/src/modes/terraform-code-review.ts +68 -0
  428. package/src/modes/types.ts +17 -0
  429. package/src/modes/update-dependencies.ts +44 -0
  430. package/src/prep/index.ts +96 -0
  431. package/src/prep/installNodeDependencies.ts +251 -0
  432. package/src/prep/installPythonDependencies.ts +235 -0
  433. package/src/prep/types.ts +38 -0
  434. package/src/reviewQuality.ts +237 -0
  435. package/src/runCli.ts +335 -0
  436. package/src/skills/terraform-best-practices/SKILL.md +379 -0
  437. package/src/toolState.ts +241 -0
  438. package/src/utils/activity.ts +210 -0
  439. package/src/utils/agent.ts +245 -0
  440. package/src/utils/agentHangReport.ts +180 -0
  441. package/src/utils/annotations.ts +56 -0
  442. package/src/utils/apiFetch.ts +19 -0
  443. package/src/utils/apiKeys.ts +305 -0
  444. package/src/utils/apiUrl.ts +42 -0
  445. package/src/utils/assets.ts +123 -0
  446. package/src/utils/baseRefConfig.ts +254 -0
  447. package/src/utils/billingErrors.ts +210 -0
  448. package/src/utils/body.ts +168 -0
  449. package/src/utils/buildInfraweaverFooter.ts +104 -0
  450. package/src/utils/byokFallback.ts +137 -0
  451. package/src/utils/changeImpact.ts +330 -0
  452. package/src/utils/claudeSubscription.ts +93 -0
  453. package/src/utils/cli.ts +36 -0
  454. package/src/utils/cloudClient.ts +57 -0
  455. package/src/utils/cloudFetchCore.ts +139 -0
  456. package/src/utils/cloudReport.ts +316 -0
  457. package/src/utils/codexHome.ts +204 -0
  458. package/src/utils/codexOAuth.ts +154 -0
  459. package/src/utils/codexRefreshDetect.ts +36 -0
  460. package/src/utils/diffCoverage.ts +404 -0
  461. package/src/utils/env.ts +84 -0
  462. package/src/utils/errorReport.ts +94 -0
  463. package/src/utils/exitHandler.ts +35 -0
  464. package/src/utils/fixDoubleEscapedString.ts +9 -0
  465. package/src/utils/ghaCore.ts +13 -0
  466. package/src/utils/gitAuth.ts +268 -0
  467. package/src/utils/gitAuthServer.ts +186 -0
  468. package/src/utils/github.ts +676 -0
  469. package/src/utils/globals.ts +9 -0
  470. package/src/utils/humanEditCapture.ts +198 -0
  471. package/src/utils/infraweaverConfig.ts +202 -0
  472. package/src/utils/install.ts +279 -0
  473. package/src/utils/instructions.ts +640 -0
  474. package/src/utils/keylessOidc.ts +47 -0
  475. package/src/utils/leapingComment.ts +20 -0
  476. package/src/utils/learnings.ts +145 -0
  477. package/src/utils/learningsTruncate.ts +42 -0
  478. package/src/utils/lifecycle.ts +198 -0
  479. package/src/utils/log.ts +452 -0
  480. package/src/utils/modelResolver.ts +105 -0
  481. package/src/utils/moduleFetch.ts +138 -0
  482. package/src/utils/normalizeEnv.ts +106 -0
  483. package/src/utils/openCodeModels.ts +86 -0
  484. package/src/utils/openaiCompatible.ts +52 -0
  485. package/src/utils/opentofuMcp.ts +61 -0
  486. package/src/utils/overrides.ts +100 -0
  487. package/src/utils/packageManager.ts +257 -0
  488. package/src/utils/patchWorkflowRunFields.ts +173 -0
  489. package/src/utils/payload.ts +1206 -0
  490. package/src/utils/prSummary.ts +148 -0
  491. package/src/utils/presets.ts +105 -0
  492. package/src/utils/progressComment.ts +266 -0
  493. package/src/utils/providerErrors.ts +189 -0
  494. package/src/utils/proxyModel.ts +239 -0
  495. package/src/utils/rangeDiff.ts +182 -0
  496. package/src/utils/redact.ts +86 -0
  497. package/src/utils/registryAuth.ts +115 -0
  498. package/src/utils/remediationCommand.ts +144 -0
  499. package/src/utils/retry.ts +180 -0
  500. package/src/utils/reviewCleanup.ts +116 -0
  501. package/src/utils/run.ts +100 -0
  502. package/src/utils/runContext.ts +276 -0
  503. package/src/utils/runContextData.ts +121 -0
  504. package/src/utils/runErrorRenderer.ts +282 -0
  505. package/src/utils/runFixture.ts +91 -0
  506. package/src/utils/runLifecycle.ts +360 -0
  507. package/src/utils/runStartupLog.ts +67 -0
  508. package/src/utils/secrets.ts +208 -0
  509. package/src/utils/setup.ts +368 -0
  510. package/src/utils/shell.ts +132 -0
  511. package/src/utils/skills.ts +67 -0
  512. package/src/utils/subprocess.ts +474 -0
  513. package/src/utils/terraformMcp.ts +99 -0
  514. package/src/utils/time.ts +59 -0
  515. package/src/utils/timer.ts +72 -0
  516. package/src/utils/todoTracking.ts +168 -0
  517. package/src/utils/token.ts +294 -0
  518. package/src/utils/toolLicensing.ts +152 -0
  519. package/src/utils/toolSelection.ts +239 -0
  520. package/src/utils/toon.ts +76 -0
  521. package/src/utils/version.ts +10 -0
  522. package/src/utils/versioning.ts +44 -0
  523. package/src/utils/vertex.ts +94 -0
  524. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,1081 @@
1
+ import type { RestEndpointMethodTypes } from "@octokit/rest";
2
+ import { type } from "arktype";
3
+ import { PRODUCT_NAME } from "#app/brand";
4
+ import { formatMcpToolRef } from "#app/external";
5
+ import { chromeStrings } from "#app/i18n/scaffolding";
6
+ import type { GitHubWriteToolContext } from "#app/mcp/capabilityContext";
7
+ import { deleteProgressComment } from "#app/mcp/comment";
8
+ import { countOutstandingInfraweaverThreads } from "#app/mcp/reviewComments";
9
+ import { assertTargetInScope } from "#app/mcp/scope";
10
+ import { execute, tool } from "#app/mcp/shared";
11
+ import type { CommentableLines } from "#app/toolState";
12
+ import { getApiUrl } from "#app/utils/apiUrl";
13
+ import { buildInfraweaverFooter } from "#app/utils/buildInfraweaverFooter";
14
+ import { log } from "#app/utils/cli";
15
+ import {
16
+ countLinesInRanges,
17
+ getDiffCoverageBreakdown,
18
+ renderDiffCoverageBreakdown,
19
+ } from "#app/utils/diffCoverage";
20
+ import { fixDoubleEscapedString } from "#app/utils/fixDoubleEscapedString";
21
+ import { patchWorkflowRunFields } from "#app/utils/patchWorkflowRunFields";
22
+ import { redactSecrets } from "#app/utils/redact";
23
+ import { retry } from "#app/utils/retry";
24
+
25
+ export type { CommentableLines };
26
+
27
+ function getHttpStatus(err: unknown): number | undefined {
28
+ if (typeof err !== "object" || err === null) return undefined;
29
+ const status = (err as Record<string, unknown>).status;
30
+ return typeof status === "number" ? status : undefined;
31
+ }
32
+
33
+ /**
34
+ * detect GitHub's generic server-side 422 ("An internal error occurred,
35
+ * please try again.") that sometimes fires on `POST /pulls/{n}/reviews`.
36
+ *
37
+ * the body is stable across occurrences and distinct from every other 422
38
+ * cause we care about (anchor validation, body length, malformed suggestion
39
+ * blocks) — those all cite the specific problem. treating this as a
40
+ * transient server error unlocks bounded in-tool retry instead of surfacing
41
+ * it to the agent with the generic "likely causes (1)(2)(3)" prompt, which
42
+ * induces whack-a-mole comment dropping on content that was never the issue.
43
+ */
44
+ export function isTransientReviewError(err: unknown): boolean {
45
+ if (getHttpStatus(err) !== 422) return false;
46
+ const msg = err instanceof Error ? err.message : String(err);
47
+ return /internal error occurred, please try again/i.test(msg);
48
+ }
49
+
50
+ // backoff schedule for transient GitHub 422 "internal error" responses on the
51
+ // reviews endpoint. 3 attempts total (initial + 2 retries) with 1s/3s delays
52
+ // — most transient GH errors clear within a few seconds, and longer delays
53
+ // push review submission past agent-perceived responsiveness.
54
+ export const TRANSIENT_REVIEW_RETRY_DELAYS_MS = [1_000, 3_000];
55
+
56
+ type PullFile =
57
+ RestEndpointMethodTypes["pulls"]["listFiles"]["response"]["data"][number];
58
+
59
+ /**
60
+ * parse a PR file's patch to determine which line numbers on each side are
61
+ * valid anchors for inline comments. GitHub only accepts comments on lines
62
+ * inside a diff hunk: added/context lines on RIGHT, removed/context lines
63
+ * on LEFT.
64
+ */
65
+ export function commentableLinesForFile(
66
+ patch: string | undefined,
67
+ ): CommentableLines {
68
+ const right = new Set<number>();
69
+ const left = new Set<number>();
70
+ if (!patch) return { RIGHT: right, LEFT: left };
71
+
72
+ let oldLine = 0;
73
+ let newLine = 0;
74
+ for (const line of patch.split("\n")) {
75
+ const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
76
+ if (hunk) {
77
+ oldLine = parseInt(hunk[1]!, 10);
78
+ newLine = parseInt(hunk[2]!, 10);
79
+ continue;
80
+ }
81
+ const changeType = line[0];
82
+ if (changeType === "+") {
83
+ right.add(newLine);
84
+ newLine++;
85
+ } else if (changeType === "-") {
86
+ left.add(oldLine);
87
+ oldLine++;
88
+ } else if (changeType === " ") {
89
+ right.add(newLine);
90
+ left.add(oldLine);
91
+ newLine++;
92
+ oldLine++;
93
+ }
94
+ // "\" (no newline marker) and anything else: skip, don't advance counters
95
+ }
96
+ return { RIGHT: right, LEFT: left };
97
+ }
98
+
99
+ export async function buildCommentableMap(
100
+ ctx: GitHubWriteToolContext,
101
+ pullNumber: number,
102
+ ): Promise<Map<string, CommentableLines>> {
103
+ // prefer the snapshot captured by checkout_pr — it matches the diff GitHub
104
+ // will anchor to (commit_id=checkoutSha). refetching via listFiles at review
105
+ // time gives the LATEST PR state, which can drift from what the agent
106
+ // actually reviewed if the PR was updated mid-run.
107
+ //
108
+ // only reuse the cache if it was built for THIS pull request AND for the
109
+ // sha we will anchor the review to. a second checkout_pr that bumps
110
+ // checkoutSha but fails before repopulating the cache (e.g., listFiles 5xx)
111
+ // would otherwise leave a stale snapshot keyed to the right PR number but
112
+ // the wrong sha, silently mis-validating comments.
113
+ const cached = ctx.toolState.commentableLinesByFile;
114
+ const cachedFor = ctx.toolState.commentableLinesPullNumber;
115
+ const cachedSha = ctx.toolState.commentableLinesCheckoutSha;
116
+ const currentSha = ctx.toolState.checkoutSha;
117
+ if (
118
+ cached &&
119
+ cachedFor === pullNumber &&
120
+ cachedSha &&
121
+ cachedSha === currentSha
122
+ )
123
+ return cached;
124
+
125
+ const files: PullFile[] = await ctx.octokit.paginate(
126
+ ctx.octokit.rest.pulls.listFiles,
127
+ {
128
+ owner: ctx.repo.owner,
129
+ repo: ctx.repo.name,
130
+ pull_number: pullNumber,
131
+ per_page: 100,
132
+ },
133
+ );
134
+ const map = new Map<string, CommentableLines>();
135
+ for (const file of files) {
136
+ map.set(file.filename, commentableLinesForFile(file.patch));
137
+ }
138
+ return map;
139
+ }
140
+
141
+ export type ReviewCommentInput = NonNullable<
142
+ RestEndpointMethodTypes["pulls"]["createReview"]["parameters"]["comments"]
143
+ >[number];
144
+
145
+ export interface DroppedComment {
146
+ path: string;
147
+ line: number;
148
+ startLine?: number | undefined;
149
+ side: "LEFT" | "RIGHT";
150
+ reason: string;
151
+ }
152
+
153
+ export function validateInlineComments(
154
+ comments: ReviewCommentInput[],
155
+ map: Map<string, CommentableLines>,
156
+ ): { valid: ReviewCommentInput[]; dropped: DroppedComment[] } {
157
+ const valid: ReviewCommentInput[] = [];
158
+ const dropped: DroppedComment[] = [];
159
+ for (const c of comments) {
160
+ const side = c.side === "LEFT" ? "LEFT" : "RIGHT";
161
+ const line = c.line ?? 0;
162
+ const startLine = c.start_line ?? line;
163
+ const lines = map.get(c.path);
164
+ const record = (reason: string): void => {
165
+ const entry: DroppedComment = { path: c.path, line, side, reason };
166
+ if (c.start_line != null) entry.startLine = c.start_line;
167
+ dropped.push(entry);
168
+ };
169
+ if (!lines) {
170
+ record(`file not in PR diff`);
171
+ continue;
172
+ }
173
+ if (lines.LEFT.size === 0 && lines.RIGHT.size === 0) {
174
+ // file is in the PR but has no textual patch — usually binary, a
175
+ // pure rename with no content change, or a mode-only change. GitHub
176
+ // won't accept inline comments on these regardless of line number.
177
+ record(`file has no textual diff (binary, pure rename, or mode change)`);
178
+ continue;
179
+ }
180
+ const anchors = lines[side];
181
+ if (!anchors.has(line)) {
182
+ record(`line ${line} (${side}) is not inside a diff hunk`);
183
+ continue;
184
+ }
185
+ // GitHub requires start_line <= line. both anchors could be valid but
186
+ // inverted (e.g. start=44, line=42) — GitHub 422s with "invalid line
187
+ // numbers". catch it here so the agent sees a precise reason.
188
+ if (c.start_line != null && c.start_line > line) {
189
+ record(
190
+ `start_line ${c.start_line} is after line ${line} — ranges must satisfy start_line <= line`,
191
+ );
192
+ continue;
193
+ }
194
+ if (startLine !== line && !anchors.has(startLine)) {
195
+ record(`start_line ${startLine} (${side}) is not inside a diff hunk`);
196
+ continue;
197
+ }
198
+ valid.push(c);
199
+ }
200
+ return { valid, dropped };
201
+ }
202
+
203
+ // cap the detail list so a pathological run (agent emits hundreds of invalid
204
+ // comments on a huge PR) doesn't push the review body past GitHub's ~65KB
205
+ // limit and fail the whole submission with a body-too-long 422.
206
+ export const MAX_DROPPED_COMMENT_LINES = 50;
207
+
208
+ /**
209
+ * reason a create_pull_request_review call should be skipped without hitting
210
+ * GitHub. returned by reviewSkipDecision; null means submit normally.
211
+ */
212
+ export type ReviewSkipDecision =
213
+ | { kind: "no-issues"; reason: string }
214
+ | { kind: "empty-downgraded-approve"; reason: string }
215
+ | { kind: "empty-request-changes"; reason: string };
216
+
217
+ /**
218
+ * decision returned by duplicateReviewDecision when a session has already
219
+ * submitted a review and the current call would be a duplicate.
220
+ */
221
+ export type DuplicateReviewDecision = {
222
+ kind: "already-submitted";
223
+ reviewId: number;
224
+ reason: string;
225
+ };
226
+
227
+ /**
228
+ * decide whether a second create_pull_request_review call in the same session
229
+ * is a duplicate of an earlier submission.
230
+ *
231
+ * the agent is instructed to call create_pull_request_review exactly once per
232
+ * Review-mode session (see src/modes/review.ts), but in practice it sometimes
233
+ * submits twice — once with substantive feedback, then again with the
234
+ * canonical "No new issues found." body when the prompt's branch logic
235
+ * re-classifies non-blocking observations. the second submission is
236
+ * always redundant: the first review is the record, and the duplicate just
237
+ * adds noise to the PR.
238
+ *
239
+ * legitimate follow-up reviews after new commits ARE allowed: when the agent
240
+ * re-runs checkout_pr to fetch the new commits, checkoutSha advances past the
241
+ * previously reviewed sha, so any call where checkoutSha has moved past the
242
+ * prior reviewedSha is a real follow-up and goes through. anything else — same
243
+ * sha (including an immediate re-submit that skipped checkout_pr), or no
244
+ * checkoutSha to compare against — is a duplicate.
245
+ */
246
+ export function duplicateReviewDecision(params: {
247
+ existing: { id: number; reviewedSha: string | undefined } | undefined;
248
+ currentCheckoutSha: string | undefined;
249
+ }): DuplicateReviewDecision | null {
250
+ const existing = params.existing;
251
+ if (!existing) return null;
252
+ // checkoutSha advanced past the prior reviewed sha — legitimate follow-up
253
+ // (e.g. after checkout_pr re-fetched new commits the agent was nudged to
254
+ // pull). only treat as a duplicate when we cannot prove the SHA moved.
255
+ if (
256
+ params.currentCheckoutSha &&
257
+ existing.reviewedSha &&
258
+ params.currentCheckoutSha !== existing.reviewedSha
259
+ ) {
260
+ return null;
261
+ }
262
+ return {
263
+ kind: "already-submitted",
264
+ reviewId: existing.id,
265
+ reason: `review ${existing.id} was already submitted in this session; ignoring duplicate call (call \`checkout_pr\` again first if new commits were pushed)`,
266
+ };
267
+ }
268
+
269
+ /**
270
+ * decide whether to skip a review submission before any network call.
271
+ *
272
+ * GitHub rejects `event: "COMMENT"` reviews with no body and no inline comments
273
+ * with HTTP 422 "Unprocessable Entity". two paths produce that shape:
274
+ *
275
+ * 1. `!approved` + empty body/comments: agent's "no issues found" result.
276
+ * skipping preserves the agent's intent (nothing to post is a fine
277
+ * outcome for a review run) without a spurious 422.
278
+ * 2. `approved` + `!prApproveEnabled` + empty body/comments: the runtime
279
+ * downgrades APPROVE to COMMENT when prApproveEnabled is off, and the
280
+ * resulting empty-COMMENT is exactly the shape GitHub 422s. skipping
281
+ * here surfaces the cause (downgrade + nothing to say) instead of an
282
+ * opaque 422 the agent can't recover from.
283
+ *
284
+ * legitimate bare approvals (`approved` + `prApproveEnabled`, no body/comments)
285
+ * are never skipped — GitHub accepts empty APPROVE reviews and the approval
286
+ * stamp itself is the review's content.
287
+ *
288
+ * a contentless `request_changes` is always skipped: a blocking verdict with no
289
+ * body and no inline comments tells the author nothing about what must change,
290
+ * and (when downgraded to COMMENT) is the empty-COMMENT shape GitHub 422s.
291
+ */
292
+ export function reviewSkipDecision(params: {
293
+ approved: boolean;
294
+ requestChanges?: boolean;
295
+ body: string | null | undefined;
296
+ hasComments: boolean;
297
+ prApproveEnabled: boolean;
298
+ }): ReviewSkipDecision | null {
299
+ if (params.body || params.hasComments) return null;
300
+ if (params.requestChanges) {
301
+ return {
302
+ kind: "empty-request-changes",
303
+ reason:
304
+ "request_changes needs a body or inline comments describing what must change — nothing to post",
305
+ };
306
+ }
307
+ if (!params.approved) {
308
+ return {
309
+ kind: "no-issues",
310
+ reason: "no issues found — nothing to post",
311
+ };
312
+ }
313
+ if (!params.prApproveEnabled) {
314
+ return {
315
+ kind: "empty-downgraded-approve",
316
+ reason:
317
+ "approve requested but prApproveEnabled is disabled; no feedback body or comments to post as a COMMENT review instead",
318
+ };
319
+ }
320
+ return null;
321
+ }
322
+
323
+ export function formatDroppedCommentsNote(dropped: DroppedComment[]): string {
324
+ const renderEntry = (d: DroppedComment): string => {
325
+ const range =
326
+ d.startLine != null && d.startLine !== d.line
327
+ ? `${d.startLine}-${d.line}`
328
+ : `${d.line}`;
329
+ return `- \`${d.path}:${range}\` (${d.side}) — ${d.reason}`;
330
+ };
331
+ const shown = dropped.slice(0, MAX_DROPPED_COMMENT_LINES).map(renderEntry);
332
+ const remainder = dropped.length - shown.length;
333
+ if (remainder > 0)
334
+ shown.push(`- …and ${remainder} more dropped comment(s) not shown`);
335
+ return (
336
+ `\n\n---\n\n` +
337
+ `**Note:** ${dropped.length} inline comment(s) dropped because they did not anchor to lines inside the PR diff:\n` +
338
+ shown.join("\n")
339
+ );
340
+ }
341
+
342
+ // one-shot review tool
343
+ export const CreatePullRequestReview = type({
344
+ pull_number: type.number.describe("The pull request number to review"),
345
+ body: type.string
346
+ .describe(
347
+ "1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array.",
348
+ )
349
+ .optional(),
350
+ approved: type.boolean
351
+ .describe(
352
+ `Set to true to submit as an approval. Use for \`> ✅ No new issues found.\` reviews where the PR is mergeable as-is and nothing in the body warrants code changes — approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for \`> ℹ️ ...\` (minor suggestions inline). Defaults to false (comment-only review). Mutually exclusive with request_changes. Approval is REJECTED while any unresolved ${PRODUCT_NAME} review thread remains open on the PR (not just the latest commit's diff): resolve the threads the current code addresses (reply + resolve_review_thread) first, or submit a non-approving review if a real issue remains.`,
353
+ )
354
+ .optional(),
355
+ request_changes: type.boolean
356
+ .describe(
357
+ "Set to true to submit as a blocking REQUEST_CHANGES review — a binding verdict that a required-reviews branch-protection gate treats as 'changes requested'. Use for `> [!IMPORTANT]` (recommended changes) and `> [!CAUTION]` (critical, security/state-destroying) reviews where the PR must not merge as-is. Requires a body or inline comments explaining what must change. Mutually exclusive with approved. Like approved, it is governed by the run's approval capability — when disabled it downgrades to a COMMENT review so the feedback still posts.",
358
+ )
359
+ .optional(),
360
+ commit_id: type.string
361
+ .describe(
362
+ "Optional SHA of the commit being reviewed. Defaults to latest. Must be the FULL 40-character SHA — abbreviated SHAs are rejected by GitHub with `422 Unprocessable Entity`. The PR-synchronize event payload's `head_sha` is already full-length.",
363
+ )
364
+ .optional(),
365
+ comments: type({
366
+ path: type.string.describe(
367
+ "The file path to comment on (relative to repo root). Must be a file that appears in the PR diff.",
368
+ ),
369
+ line: type.number.describe(
370
+ "Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format. Must sit inside a `@@` hunk in the PR diff — anchors on context-only or untouched lines are dropped silently (the rest of the review still posts; dropped entries are reported under `droppedComments` in the response).",
371
+ ),
372
+ side: type
373
+ .enumerated("LEFT", "RIGHT")
374
+ .describe(
375
+ "Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT.",
376
+ )
377
+ .optional(),
378
+ body: type.string
379
+ .describe("Explanatory comment text (optional if suggestion is provided)")
380
+ .optional(),
381
+ suggestion: type.string
382
+ .describe(
383
+ "Full replacement code for the line range [start_line, line]. MUST preserve the exact indentation of the original code.",
384
+ )
385
+ .optional(),
386
+ start_line: type.number
387
+ .describe(
388
+ "Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces. Both `start_line` and `line` must sit inside the same `@@` hunk — a `start_line` outside the hunk causes the whole comment to be dropped even when `line` is valid. If you need to comment on context just above/below a hunk, shrink the range to a single line that is provably modified.",
389
+ )
390
+ .optional(),
391
+ })
392
+ .array()
393
+ .describe(
394
+ "Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead.",
395
+ )
396
+ .optional(),
397
+ });
398
+
399
+ export function CreatePullRequestReviewTool(ctx: GitHubWriteToolContext) {
400
+ return tool({
401
+ mutates: true,
402
+ name: "create_pull_request_review",
403
+ description:
404
+ "Submit a review for an existing pull request. " +
405
+ 'Example: `create_pull_request_review({ pull_number: 1234, body: "LGTM", approved: true, comments: [{ path: "src/api.ts", line: 42, body: "nit: rename" }] })`. ' +
406
+ "Each call creates a permanent, visible review on the PR — NEVER submit test or diagnostic reviews. " +
407
+ "Reviews with no body AND no comments are silently skipped (nothing to post). " +
408
+ "IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. " +
409
+ "Only use 'body' for a 1-2 sentence summary with urgency and critical callouts. " +
410
+ "Use 'suggestion' to propose replacement code - MUST preserve exact indentation of original code. " +
411
+ "The first submission may error once with a one-time diff-coverage nudge listing unread TOC regions — retry with the same arguments and the pre-flight will not block again. " +
412
+ "Example replacing lines 42-44 (3 lines) with 5 lines: " +
413
+ `{ path: 'src/api.ts', start_line: 42, line: 44, suggestion: ' const result = await fetch(url);\\n if (!result.ok) {\\n log.error(result.status);\\n throw new Error("request failed");\\n }' }` +
414
+ " CONSTRAINT: Inline comments can ONLY target files and lines that appear in the PR diff." +
415
+ " Comments anchored outside a diff hunk are dropped automatically (with a note appended to the review body) — the rest of the review still posts.",
416
+ parameters: CreatePullRequestReview,
417
+ execute: execute(async (reviewInput) => {
418
+ const {
419
+ pull_number,
420
+ approved,
421
+ request_changes,
422
+ commit_id,
423
+ comments = [],
424
+ } = reviewInput;
425
+ // body is reassigned below (fix-up + dropped-comments note), so bind it mutable
426
+ let { body } = reviewInput;
427
+ // SECURITY: a review (especially an APPROVE, which can satisfy a
428
+ // required-reviews branch-protection gate) must target the PR this run is
429
+ // scoped to or one it opened — never an arbitrary PR an injected agent
430
+ // names. mirrors the cross-PR guard on push_branch (mcp/git.ts).
431
+ assertTargetInScope(ctx, pull_number, "submit a review on");
432
+
433
+ // approve and request_changes are opposite verdicts — a review can't be
434
+ // both. reject the contradiction rather than silently picking one.
435
+ if (approved && request_changes) {
436
+ throw new Error(
437
+ "`approved` and `request_changes` are mutually exclusive — a review is either an approval or a blocking change request, not both. Pick one (or omit both for a comment-only review).",
438
+ );
439
+ }
440
+
441
+ if (body) body = fixDoubleEscapedString(body);
442
+
443
+ // set issue context (PRs are issues)
444
+ ctx.toolState.issueNumber = pull_number;
445
+
446
+ // guard against duplicate review submissions in the same session.
447
+ // see duplicateReviewDecision for the rationale — short version: the
448
+ // agent occasionally submits twice (substantive review + canonical
449
+ // "no issues found" follow-up) and the second is always redundant.
450
+ // legit re-reviews after new commits are still allowed because
451
+ // checkout_pr advances toolState.checkoutSha past the prior reviewedSha.
452
+ const dup = duplicateReviewDecision({
453
+ existing: ctx.toolState.review,
454
+ currentCheckoutSha: ctx.toolState.checkoutSha,
455
+ });
456
+ if (dup) {
457
+ log.info(`skipping duplicate review submission: ${dup.reason}`);
458
+ return {
459
+ success: true,
460
+ skipped: true,
461
+ reason: dup.reason,
462
+ reviewId: dup.reviewId,
463
+ };
464
+ }
465
+
466
+ // INVARIANT: never approve a PR while ANY outstanding, unaddressed
467
+ // Infraweaver finding remains open — even on an incremental commit that
468
+ // introduces no NEW issues. The approval verdict is otherwise just the
469
+ // agent's `approved` boolean with zero verification, and the
470
+ // incremental-review framing biases the agent toward the latest diff, so a
471
+ // prior unresolved thread outside the delta could slip an approval through.
472
+ // Enforce it mechanically against the FULL set of open Infraweaver-originated
473
+ // review threads. Applies regardless of prApproveEnabled — a downgrade to
474
+ // COMMENT would still leave a misleading "no issues" body on a PR that has
475
+ // unresolved findings. see #990.
476
+ if (approved) {
477
+ const outstanding = await countOutstandingInfraweaverThreads({
478
+ octokit: ctx.octokit,
479
+ owner: ctx.repo.owner,
480
+ name: ctx.repo.name,
481
+ pullNumber: pull_number,
482
+ });
483
+ if (outstanding > 0) {
484
+ const listRef = formatMcpToolRef(ctx.agentId, "get_review_comments");
485
+ const resolveRef = formatMcpToolRef(
486
+ ctx.agentId,
487
+ "resolve_review_thread",
488
+ );
489
+ throw new Error(
490
+ `cannot approve: ${outstanding} unresolved ${PRODUCT_NAME} review thread(s) still open on this PR. ` +
491
+ `approval requires every prior ${PRODUCT_NAME} finding to be resolved, not just the latest commits to be clean. ` +
492
+ `inspect them with \`${listRef}\`; for each thread the current code genuinely addresses, reply then call \`${resolveRef}\`, and retry this approval. ` +
493
+ `if any thread is a real outstanding issue, do NOT approve — submit a non-approving review (omit \`approved\`, or set \`request_changes\` for a blocking one) that covers it instead.`,
494
+ );
495
+ }
496
+ }
497
+
498
+ // skip empty COMMENT reviews before any GitHub call. see reviewSkipDecision
499
+ // for the cases (no-issues vs empty-downgraded-approve) and why GitHub 422s
500
+ // the shape we'd otherwise POST.
501
+ const skip = reviewSkipDecision({
502
+ approved: approved ?? false,
503
+ requestChanges: request_changes ?? false,
504
+ body,
505
+ hasComments: comments.length > 0,
506
+ prApproveEnabled: ctx.prApproveEnabled,
507
+ });
508
+ if (skip) {
509
+ log.info(`skipping review submission: ${skip.reason}`);
510
+ return { success: true, skipped: true, reason: skip.reason };
511
+ }
512
+
513
+ // enforce prApproveEnabled: downgrade a binding verdict (APPROVE or the
514
+ // blocking REQUEST_CHANGES) to COMMENT when the run's approval capability
515
+ // is disabled — both satisfy/block a required-reviews branch-protection
516
+ // gate, so a blocking verdict must be at least as governed as an approval.
517
+ // by this point we already returned if the downgrade would produce an
518
+ // empty COMMENT (the skip above), so every downgrade that reaches here
519
+ // carries either a body or inline comments.
520
+ let event: "APPROVE" | "COMMENT" | "REQUEST_CHANGES" = approved
521
+ ? "APPROVE"
522
+ : request_changes
523
+ ? "REQUEST_CHANGES"
524
+ : "COMMENT";
525
+ if (
526
+ (event === "APPROVE" || event === "REQUEST_CHANGES") &&
527
+ !ctx.prApproveEnabled
528
+ ) {
529
+ log.info(
530
+ `prApproveEnabled is disabled — downgrading ${event} to COMMENT`,
531
+ );
532
+ event = "COMMENT";
533
+ }
534
+
535
+ const params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"] =
536
+ {
537
+ owner: ctx.repo.owner,
538
+ repo: ctx.repo.name,
539
+ pull_number,
540
+ event,
541
+ };
542
+ let latestHeadSha: string | undefined;
543
+ if (commit_id) {
544
+ params.commit_id = commit_id;
545
+ } else {
546
+ const pr = await ctx.octokit.rest.pulls.get({
547
+ owner: ctx.repo.owner,
548
+ repo: ctx.repo.name,
549
+ pull_number,
550
+ });
551
+ latestHeadSha = pr.data.head.sha;
552
+ // anchor to checkout sha so line numbers match the diff the agent analyzed
553
+ params.commit_id = ctx.toolState.checkoutSha ?? latestHeadSha;
554
+ if (
555
+ ctx.toolState.checkoutSha &&
556
+ latestHeadSha !== ctx.toolState.checkoutSha
557
+ ) {
558
+ log.info(
559
+ `anchoring review to checkout ${ctx.toolState.checkoutSha.slice(0, 7)} ` +
560
+ `(HEAD is now ${latestHeadSha.slice(0, 7)})`,
561
+ );
562
+ }
563
+ }
564
+
565
+ runDiffCoveragePreflight({ ctx });
566
+
567
+ type ReviewComment = NonNullable<typeof params.comments>[number];
568
+ const reviewComments = comments.map((comment) => {
569
+ let commentBody = fixDoubleEscapedString(comment.body || "");
570
+ if (comment.suggestion !== undefined) {
571
+ const suggestionBlock = `\`\`\`suggestion\n${comment.suggestion}\n\`\`\``;
572
+ commentBody = commentBody
573
+ ? `${commentBody}\n\n${suggestionBlock}`
574
+ : suggestionBlock;
575
+ }
576
+ const side = comment.side || "RIGHT";
577
+ const reviewComment: ReviewComment = {
578
+ path: comment.path,
579
+ line: comment.line,
580
+ // LLM02: redact secrets echoed from repo content before they land in a
581
+ // durable review comment (covers both submitReview + createReview paths).
582
+ body: redactSecrets(commentBody),
583
+ side,
584
+ };
585
+ if (comment.start_line != null && comment.start_line !== comment.line) {
586
+ reviewComment.start_line = comment.start_line;
587
+ reviewComment.start_side = side;
588
+ }
589
+ return reviewComment;
590
+ });
591
+
592
+ // pre-validate inline comments against the current PR diff. drop any
593
+ // comment that does not anchor to a line inside a hunk, rather than
594
+ // letting GitHub 422 and sink the whole review.
595
+ let droppedComments: DroppedComment[] = [];
596
+ if (reviewComments.length > 0) {
597
+ const commentableMap = await buildCommentableMap(ctx, pull_number);
598
+ const validation = validateInlineComments(
599
+ reviewComments,
600
+ commentableMap,
601
+ );
602
+ droppedComments = validation.dropped;
603
+ if (droppedComments.length > 0) {
604
+ log.info(
605
+ `dropping ${droppedComments.length}/${reviewComments.length} inline comment(s) that do not anchor to PR diff lines`,
606
+ );
607
+ }
608
+ // always reassign so all-dropped reviews leave params.comments empty
609
+ // instead of carrying the original invalid set (which would 422).
610
+ params.comments = validation.valid;
611
+ }
612
+
613
+ // if we dropped comments, surface them in the review body so the
614
+ // author (and the agent, on retry) can see what was skipped. this is
615
+ // deliberate: an all-dropped review still posts the note (see the
616
+ // "posts the dropped-comments note as the review body" test) so the
617
+ // drop is visible and the agent can re-anchor on retry.
618
+ if (droppedComments.length > 0) {
619
+ const note = formatDroppedCommentsNote(droppedComments);
620
+ body = body ? body + note : note.replace(/^\n\n/, "");
621
+ }
622
+
623
+ // after dropping, an empty non-approve review has nothing left to post.
624
+ if (!approved && !body && !params.comments?.length) {
625
+ log.info(
626
+ "review has no body and all inline comments were dropped — skipping submission",
627
+ );
628
+ return {
629
+ success: true,
630
+ skipped: true,
631
+ reason: "all inline comments were invalid — nothing to post",
632
+ droppedComments,
633
+ };
634
+ }
635
+
636
+ // no body → single-step createReview (no footer needed)
637
+ // has body → pending + submit so we can build footer with Fix links using review ID
638
+ //
639
+ // wrap the submission in `retry` so GitHub's transient 422 "internal
640
+ // error" body (distinct from anchor / body-length / suggestion 422s,
641
+ // which all cite the specific cause) clears on its own instead of
642
+ // surfacing through the generic 422 handler — that framing sent the
643
+ // agent dropping valid inline comments chasing a non-issue.
644
+ // `shouldRetry` scopes retries to the transient body only, so real
645
+ // validation 422s still fail fast.
646
+ let result: Awaited<
647
+ ReturnType<typeof ctx.octokit.rest.pulls.createReview>
648
+ >;
649
+ try {
650
+ result = await retry(
651
+ () =>
652
+ body
653
+ ? createAndSubmitWithFooter(ctx, params, {
654
+ body,
655
+ approved: approved ?? false,
656
+ hasComments: (params.comments?.length ?? 0) > 0,
657
+ })
658
+ : createReviewWithStrandedRecovery(ctx, params),
659
+ {
660
+ delaysMs: TRANSIENT_REVIEW_RETRY_DELAYS_MS,
661
+ shouldRetry: isTransientReviewError,
662
+ label: "review submission",
663
+ },
664
+ );
665
+ } catch (err: unknown) {
666
+ // GitHub's transient 422 "internal error" is distinct from anchor /
667
+ // body-length / suggestion validation failures — framing it with the
668
+ // generic "likely causes (1)(2)(3)" prompt sends the agent dropping
669
+ // comments that were never the problem. after bounded in-tool retry
670
+ // we surface a dedicated message that tells the agent to wait-and-
671
+ // retry or fall back to a body-only review.
672
+ if (isTransientReviewError(err)) {
673
+ const rawMsg = err instanceof Error ? err.message : String(err);
674
+ throw new Error(
675
+ `GitHub returned a transient 422 "internal error" on the reviews endpoint after ${TRANSIENT_REVIEW_RETRY_DELAYS_MS.length + 1} attempts. ` +
676
+ `This is a GitHub-side issue, not a problem with your review content. ` +
677
+ `Do NOT modify or drop inline comments — their content is not the cause. ` +
678
+ `Wait ~30 seconds and call this tool once more with the SAME arguments. ` +
679
+ `If it still fails, submit a body-only review (move all inline feedback into \`body\` as text) so nothing is lost. ` +
680
+ `GitHub said: ${rawMsg}`,
681
+ { cause: err },
682
+ );
683
+ }
684
+ if (getHttpStatus(err) !== 422 || !params.comments?.length) throw err;
685
+
686
+ const details = params.comments.map((c) => {
687
+ const line = c.line ?? 0;
688
+ const startLine = c.start_line ?? line;
689
+ const range = startLine !== line ? `${startLine}-${line}` : `${line}`;
690
+ return `${c.path}:${range} (${c.side ?? "RIGHT"})`;
691
+ });
692
+ // a 422 on createReview-with-comments is USUALLY about comment
693
+ // anchors, but could also be about body length, invalid suggestion
694
+ // blocks, etc. include the verbatim GitHub error so the agent can
695
+ // diagnose non-anchor 422s without us having to enumerate every
696
+ // possible GitHub validation rule.
697
+ const rawMsg = err instanceof Error ? err.message : String(err);
698
+ const checkoutRef = formatMcpToolRef(ctx.agentId, "checkout_pr");
699
+ throw new Error(
700
+ `GitHub rejected the review with 422 even after pre-validation. ` +
701
+ `Likely causes (check "GitHub said" below to narrow down): ` +
702
+ `(1) new commits pushed after pre-validation — call \`${checkoutRef}\` again to refresh the diff snapshot, then resubmit; ` +
703
+ `(2) the review body exceeded GitHub's ~65KB limit — shorten it and retry; ` +
704
+ `(3) a \`suggestion\` block is malformed (missing backticks, extra backticks, or wrong indentation) — inspect the affected comments below. ` +
705
+ `If none apply, move the failing comments into the review body as text so the rest still posts. ` +
706
+ `Affected comments: ${details.join(", ")}. ` +
707
+ `GitHub said: ${rawMsg}`,
708
+ { cause: err },
709
+ );
710
+ }
711
+ log.debug(`createReview response: ${JSON.stringify(result.data)}`);
712
+ if (!result.data.id) {
713
+ throw new Error(
714
+ `createReview returned invalid data: ${JSON.stringify(result.data)}`,
715
+ );
716
+ }
717
+ const reviewId = result.data.id;
718
+ const reviewNodeId = result.data.node_id;
719
+ log.info(`» created review ${reviewId} on pull request #${pull_number}`);
720
+
721
+ // reviewedSha = what the agent actually reviewed (checkout SHA), not the
722
+ // submission anchor (current HEAD). this ensures postReviewCleanup dispatches
723
+ // a follow-up if the agent doesn't handle new commits inline.
724
+ const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
725
+ ctx.toolState.review = {
726
+ id: reviewId,
727
+ nodeId: reviewNodeId,
728
+ reviewedSha: actuallyReviewedSha,
729
+ };
730
+
731
+ ctx.toolState.wasUpdated = true;
732
+
733
+ // a submitted review obsoletes the progress comment — the review IS the
734
+ // durable artifact. owned here (not in main.ts) so cleanup is atomic with
735
+ // submission and survives any path out of the run (success, timeout,
736
+ // crash). deleteProgressComment sets progressComment = null, so a later
737
+ // report_progress call short-circuits to a no-op.
738
+ // best-effort: a cleanup failure must not turn a successful review into
739
+ // a tool-call failure visible to the agent.
740
+ await deleteProgressComment(ctx).catch((err) => {
741
+ log.debug(`progress comment cleanup after review failed: ${err}`);
742
+ });
743
+
744
+ // detect commits pushed since checkout and guide the agent to review them
745
+ // inline instead of dispatching a separate workflow run
746
+ if (
747
+ ctx.toolState.checkoutSha &&
748
+ latestHeadSha &&
749
+ latestHeadSha !== ctx.toolState.checkoutSha
750
+ ) {
751
+ const fromSha = ctx.toolState.checkoutSha;
752
+ const toSha = latestHeadSha;
753
+ // store the just-reviewed sha as beforeSha so the next checkout_pr computes
754
+ // an incremental diff (fromSha..newHEAD).
755
+ ctx.toolState.beforeSha = fromSha;
756
+ // deliberately do NOT advance checkoutSha here. checkoutSha means "the sha
757
+ // we actually checked out and analyzed", and the agent has not re-checked
758
+ // out the new commits yet. leaving it at fromSha keeps reviewedSha ===
759
+ // checkoutSha, so an immediate re-submit that skips the checkout_pr below is
760
+ // caught by the duplicate guard instead of posting a second review against
761
+ // an un-refetched diff. a real checkout_pr advances checkoutSha to the new
762
+ // sha and unblocks the legitimate follow-up review.
763
+
764
+ log.info(
765
+ `new commits detected during review: ${fromSha.slice(0, 7)}..${toSha.slice(0, 7)}`,
766
+ );
767
+
768
+ return {
769
+ success: true,
770
+ reviewId,
771
+ html_url: result.data.html_url,
772
+ state: result.data.state,
773
+ user: result.data.user?.login,
774
+ submitted_at: result.data.submitted_at,
775
+ droppedComments:
776
+ droppedComments.length > 0 ? droppedComments : undefined,
777
+ newCommits: {
778
+ from: fromSha,
779
+ to: toSha,
780
+ instructions:
781
+ `new commits were pushed while you were reviewing. ` +
782
+ `call \`${formatMcpToolRef(ctx.agentId, "checkout_pr")}\` again to fetch the latest version — it will compute the incremental diff automatically. ` +
783
+ `submit another review covering only the new changes. do not repeat feedback from your previous review.`,
784
+ },
785
+ };
786
+ }
787
+
788
+ return {
789
+ success: true,
790
+ reviewId,
791
+ html_url: result.data.html_url,
792
+ state: result.data.state,
793
+ user: result.data.user?.login,
794
+ submitted_at: result.data.submitted_at,
795
+ droppedComments:
796
+ droppedComments.length > 0 ? droppedComments : undefined,
797
+ };
798
+ }),
799
+ });
800
+ }
801
+
802
+ function runDiffCoveragePreflight(params: {
803
+ ctx: GitHubWriteToolContext;
804
+ }): void {
805
+ const coverageState = params.ctx.toolState.diffCoverage;
806
+ if (!coverageState) {
807
+ log.debug(
808
+ "diff coverage pre-flight skipped: no diffCoverage state present in toolState",
809
+ );
810
+ return;
811
+ }
812
+ if (coverageState.coveragePreflightRan) {
813
+ log.debug("diff coverage pre-flight skipped: already ran in this session");
814
+ return;
815
+ }
816
+
817
+ coverageState.coveragePreflightRan = true;
818
+ log.debug(
819
+ `diff coverage pre-flight start: diffPath=${coverageState.diffPath}, totalLines=${coverageState.totalLines}, tocEntries=${coverageState.tocEntries.length}, coveredRanges=${coverageState.coveredRanges.length}`,
820
+ );
821
+ const breakdown = getDiffCoverageBreakdown({ state: coverageState });
822
+ const unread: Array<{ path: string; ranges: string; unreadLines: number }> =
823
+ [];
824
+ let unreadLines = 0;
825
+ for (const file of breakdown.files) {
826
+ if (file.unreadRanges.length === 0) continue;
827
+ const rangesText = file.unreadRanges
828
+ .map((range) => `${range.startLine}-${range.endLine}`)
829
+ .join(", ");
830
+ const fileUnreadLines = countLinesInRanges({ ranges: file.unreadRanges });
831
+ unread.push({
832
+ path: file.filename,
833
+ ranges: rangesText,
834
+ unreadLines: fileUnreadLines,
835
+ });
836
+ unreadLines += fileUnreadLines;
837
+ }
838
+ coverageState.lastBreakdown = renderDiffCoverageBreakdown({
839
+ diffPath: coverageState.diffPath,
840
+ breakdown,
841
+ });
842
+ log.debug(
843
+ `diff coverage pre-flight breakdown: coveredLines=${breakdown.coveredLines}, unreadLines=${unreadLines}`,
844
+ );
845
+
846
+ if (unreadLines === 0) {
847
+ log.debug("diff coverage pre-flight passed: no unread regions");
848
+ return;
849
+ }
850
+
851
+ log.info(
852
+ `diff coverage pre-flight nudge: unread lines=${unreadLines}, unread files=${unread.length}`,
853
+ );
854
+ const unreadText = unread
855
+ .map(
856
+ (entry) =>
857
+ `- ${entry.path} (${entry.unreadLines} lines, ${entry.ranges})`,
858
+ )
859
+ .join("\n");
860
+ throw new Error(
861
+ `diff coverage pre-flight: some TOC regions were not read before review submission. ` +
862
+ `this is a one-time nudge — read the ranges below from ${coverageState.diffPath} on a best-effort basis, then call create_pull_request_review again. ` +
863
+ `you are NOT obligated to read generated artifacts (lockfiles like pnpm-lock.yaml / package-lock.json / yarn.lock / Cargo.lock; codegen output like *.gen.*, *.pb.go, *.generated.*; snapshot/fixture dirs like __snapshots__/; migration metadata like drizzle/meta/, prisma migration SQL). ` +
864
+ `if every unread region is generated, retry immediately without reading. ` +
865
+ `this pre-flight will not block again in this review session.\n\n` +
866
+ `unread TOC regions:\n${unreadText}\n\n` +
867
+ `${coverageState.lastBreakdown}`,
868
+ );
869
+ }
870
+
871
+ type FooterOpts = { body: string; approved: boolean; hasComments: boolean };
872
+
873
+ /**
874
+ * clear a pending review draft stranded on the PR by a prior hard-killed run
875
+ * (workflow timeout, OOM) so the next createReview can succeed.
876
+ *
877
+ * GitHub enforces one-pending-review-per-user-per-PR. if the previous process
878
+ * died between createReview(PENDING) and submitReview, the draft remains and
879
+ * the next run's createReview 422s with "already has a pending review".
880
+ * listReviews only exposes PENDING reviews to their author, so filtering on
881
+ * state === "PENDING" is already scoped to the authed token's own draft.
882
+ *
883
+ * if `originalErr` is not a pending-review 422, or no leftover is found, this
884
+ * function rethrows `originalErr` so the caller surfaces the original failure.
885
+ * delete failures with 404 (draft already gone) or 422 (draft submitted by a
886
+ * concurrent caller) are swallowed — the caller's retry will succeed in both
887
+ * cases. any other delete error is rethrown unchanged.
888
+ *
889
+ * known limitation: if two runs on the SAME PR share the authed token and
890
+ * overlap in time, the loser's createReview 422s on the winner's still-active
891
+ * draft. recovery would then delete the winner's active draft and the
892
+ * winner's submitReview would 404. this is not distinguishable from a
893
+ * genuinely-stranded draft via the review object alone (PENDING reviews
894
+ * expose no created_at timestamp, and both reviews are authored by the same
895
+ * bot user). rely on workflow-level concurrency controls (e.g. a concurrency
896
+ * key keyed to the PR number) to prevent overlap.
897
+ */
898
+ export async function clearStrandedPendingReview(
899
+ ctx: GitHubWriteToolContext,
900
+ params: {
901
+ owner: string;
902
+ repo: string;
903
+ pull_number: number;
904
+ originalErr: unknown;
905
+ },
906
+ ): Promise<void> {
907
+ const originalErr = params.originalErr;
908
+ const msg =
909
+ originalErr instanceof Error ? originalErr.message.toLowerCase() : "";
910
+ if (getHttpStatus(originalErr) !== 422 || !msg.includes("pending review"))
911
+ throw originalErr;
912
+ // if listReviews itself fails (5xx, rate limit, etc), surface the ORIGINAL
913
+ // 422 rather than the listing failure — "pending review conflict" is the
914
+ // real blocker the caller needs to see. hiding it behind a transient 502
915
+ // sent agents chasing phantom server errors instead of retrying the
916
+ // conflict. log the listing failure for diagnosis but do not mask.
917
+ const reviews = await ctx.octokit
918
+ .paginate(ctx.octokit.rest.pulls.listReviews, {
919
+ owner: params.owner,
920
+ repo: params.repo,
921
+ pull_number: params.pull_number,
922
+ per_page: 100,
923
+ })
924
+ .catch((listErr: unknown) => {
925
+ // surface at info so operators not running at debug still see that
926
+ // recovery was attempted (and why) before the original 422 bubbles up.
927
+ log.info(
928
+ `» listReviews failed during pending-review cleanup, surfacing original 422: ${listErr instanceof Error ? listErr.message : String(listErr)}`,
929
+ );
930
+ throw originalErr;
931
+ });
932
+ const leftover = reviews.find((r) => r.state === "PENDING");
933
+ if (!leftover?.id) throw originalErr;
934
+ log.info(
935
+ `» clearing leftover pending review ${leftover.id} (likely stranded by a killed prior run)`,
936
+ );
937
+ try {
938
+ await ctx.octokit.rest.pulls.deletePendingReview({
939
+ owner: params.owner,
940
+ repo: params.repo,
941
+ pull_number: params.pull_number,
942
+ review_id: leftover.id,
943
+ });
944
+ } catch (cleanupErr) {
945
+ const cleanupStatus = getHttpStatus(cleanupErr);
946
+ if (cleanupStatus !== 404 && cleanupStatus !== 422) throw cleanupErr;
947
+ log.debug(
948
+ `» delete of leftover pending ${leftover.id} no-op (status ${cleanupStatus})`,
949
+ );
950
+ }
951
+ }
952
+
953
+ /**
954
+ * single-step createReview (event != PENDING) with stranded-draft recovery.
955
+ * the body path goes through createAndSubmitWithFooter which already recovers
956
+ * from a stranded PENDING draft at its own createReview call. the no-body path
957
+ * used to call createReview directly with no recovery — so a PR whose previous
958
+ * body-path run crashed between createReview(PENDING) and submitReview would
959
+ * permanently 422 any subsequent no-body review (approve-with-no-feedback or
960
+ * comments-only) until a body-path run happened to clear the draft.
961
+ */
962
+ export async function createReviewWithStrandedRecovery(
963
+ ctx: GitHubWriteToolContext,
964
+ params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"],
965
+ ): Promise<Awaited<ReturnType<typeof ctx.octokit.rest.pulls.createReview>>> {
966
+ try {
967
+ return await ctx.octokit.rest.pulls.createReview(params);
968
+ } catch (err) {
969
+ await clearStrandedPendingReview(ctx, {
970
+ owner: params.owner,
971
+ repo: params.repo,
972
+ pull_number: params.pull_number,
973
+ originalErr: err,
974
+ });
975
+ return await ctx.octokit.rest.pulls.createReview(params);
976
+ }
977
+ }
978
+
979
+ async function createAndSubmitWithFooter(
980
+ ctx: GitHubWriteToolContext,
981
+ params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"],
982
+ opts: FooterOpts,
983
+ ) {
984
+ // create as PENDING (strip event) so we get the review ID before publishing
985
+ const { event: _, ...pendingParams } = params;
986
+ let pending: Awaited<ReturnType<typeof ctx.octokit.rest.pulls.createReview>>;
987
+ try {
988
+ pending = await ctx.octokit.rest.pulls.createReview(pendingParams);
989
+ } catch (err) {
990
+ await clearStrandedPendingReview(ctx, {
991
+ owner: params.owner,
992
+ repo: params.repo,
993
+ pull_number: params.pull_number,
994
+ originalErr: err,
995
+ });
996
+ pending = await ctx.octokit.rest.pulls.createReview(pendingParams);
997
+ }
998
+ if (!pending.data.id) {
999
+ throw new Error(
1000
+ `createReview returned invalid data: ${JSON.stringify(pending.data)}`,
1001
+ );
1002
+ }
1003
+
1004
+ // once the pending draft exists, GitHub only allows one pending review per
1005
+ // user per PR — so ANY failure between here and successful submit must
1006
+ // clean up, not just a submitReview throw. getApiUrl() can throw if
1007
+ // API_URL is misconfigured, and future footer-building changes could
1008
+ // introduce new throw paths. keep the whole body wrapped.
1009
+ try {
1010
+ // Fix buttons are suppressed on approving reviews — those are mergeable
1011
+ // by definition (the `> ✅ No new issues found.` tier, with no inline
1012
+ // comments), so dispatching a fix run would be a UX trap.
1013
+ const customParts: string[] = [];
1014
+ if (!opts.approved) {
1015
+ const apiUrl = getApiUrl();
1016
+ const labels = chromeStrings(ctx.payload.language);
1017
+ if (opts.hasComments) {
1018
+ const fixAllUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix&review_id=${pending.data.id}`;
1019
+ const fixApprovedUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix-approved&review_id=${pending.data.id}`;
1020
+ customParts.push(
1021
+ `[${labels.fixAll} ➔](${fixAllUrl})`,
1022
+ `[${labels.fixApproved} ➔](${fixApprovedUrl})`,
1023
+ );
1024
+ } else {
1025
+ const fixUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix&review_id=${pending.data.id}`;
1026
+ customParts.push(`[${labels.fixIt} ➔](${fixUrl})`);
1027
+ }
1028
+ }
1029
+
1030
+ const footer = buildInfraweaverFooter({
1031
+ customParts,
1032
+ model: ctx.toolState.model,
1033
+ fallbackFrom: ctx.toolState.modelFallback?.from,
1034
+ language: ctx.payload.language,
1035
+ });
1036
+
1037
+ return await ctx.octokit.rest.pulls.submitReview({
1038
+ owner: params.owner,
1039
+ repo: params.repo,
1040
+ pull_number: params.pull_number,
1041
+ review_id: pending.data.id,
1042
+ event: params.event!,
1043
+ body: redactSecrets(opts.body) + footer, // LLM02: scrub the review summary body
1044
+ });
1045
+ } catch (err) {
1046
+ // anything failed after the pending draft was created. leaving the draft
1047
+ // on the PR would cause the agent's retry to fail with "already has a
1048
+ // pending review" (GitHub's one-pending-per-user-per-PR limit). best-effort
1049
+ // cleanup so retries start from a clean slate. the cleanup itself may
1050
+ // 404/422 (review already submitted by a concurrent caller, or the PR
1051
+ // was closed mid-flight) — log and swallow those so the original error
1052
+ // isn't masked.
1053
+ try {
1054
+ await ctx.octokit.rest.pulls.deletePendingReview({
1055
+ owner: params.owner,
1056
+ repo: params.repo,
1057
+ pull_number: params.pull_number,
1058
+ review_id: pending.data.id,
1059
+ });
1060
+ log.debug(
1061
+ `» deleted leftover pending review ${pending.data.id} after failure`,
1062
+ );
1063
+ } catch (cleanupErr) {
1064
+ log.debug(
1065
+ `» failed to delete pending review ${pending.data.id}: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`,
1066
+ );
1067
+ }
1068
+ throw err;
1069
+ }
1070
+ }
1071
+
1072
+ /**
1073
+ * report the review node ID so the WorkflowRun is marked as "review submitted".
1074
+ * exported for use in main.ts post-agent cleanup.
1075
+ */
1076
+ export async function reportReviewNodeId(
1077
+ ctx: GitHubWriteToolContext,
1078
+ params: { nodeId: string },
1079
+ ): Promise<void> {
1080
+ await patchWorkflowRunFields(ctx, { reviewNodeId: params.nodeId });
1081
+ }