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,1137 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Octokit } from "@octokit/rest";
4
+ import { type } from "arktype";
5
+ import type { GitHubToolContext } from "#app/mcp/capabilityContext";
6
+ import { assertTargetInScope } from "#app/mcp/scope";
7
+ import { execute, tool } from "#app/mcp/shared";
8
+ import { resolveBodyAssets } from "#app/utils/body";
9
+ import { stripExistingFooter } from "#app/utils/buildInfraweaverFooter";
10
+ import { getTempDir } from "#app/utils/env";
11
+ import { log } from "#app/utils/log";
12
+ import { isInfraweaver } from "#app/utils/payload";
13
+ import { isTransientServerError, retry } from "#app/utils/retry";
14
+
15
+ /**
16
+ * A GraphQL review-thread fetch worth retrying: an HTTP 5xx / network blip
17
+ * ({@link isTransientServerError}), OR GitHub's transient "Something went wrong
18
+ * while executing your query" — which surfaces both as an HTTP-200
19
+ * `GraphqlResponseError` (message only) and as an HTTP-502 `RequestError`. A
20
+ * blip here would otherwise drop a whole review delivery. Deterministic GraphQL
21
+ * validation errors (bad query, unknown field) don't match and fail fast. #997.
22
+ */
23
+ export function isTransientGraphqlError(error: unknown): boolean {
24
+ if (isTransientServerError(error)) return true;
25
+ const msg = error instanceof Error ? error.message : String(error);
26
+ return /something went wrong while executing your query/i.test(msg);
27
+ }
28
+
29
+ // GraphQL query to fetch all review threads for a PR with full comment history
30
+ export const REVIEW_THREADS_QUERY = `
31
+ query ($owner: String!, $name: String!, $prNumber: Int!) {
32
+ repository(owner: $owner, name: $name) {
33
+ pullRequest(number: $prNumber) {
34
+ reviewThreads(first: 100) {
35
+ nodes {
36
+ id
37
+ path
38
+ line
39
+ startLine
40
+ diffSide
41
+ isResolved
42
+ isOutdated
43
+ comments(first: 50) {
44
+ nodes {
45
+ fullDatabaseId
46
+ body
47
+ bodyHTML
48
+ createdAt
49
+ diffHunk
50
+ line
51
+ startLine
52
+ originalLine
53
+ originalStartLine
54
+ author { login }
55
+ pullRequestReview {
56
+ databaseId
57
+ author { login }
58
+ }
59
+ reactionGroups {
60
+ content
61
+ reactors(first: 10) {
62
+ totalCount
63
+ nodes {
64
+ ... on Actor { login }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ `;
76
+
77
+ /**
78
+ * Lightweight PAGINATED query for the approval gate. It fetches only each
79
+ * thread's resolved state and its ROOT author (oldest comment, hence
80
+ * `comments(first: 1)`) — everything {@link countOutstandingInfraweaverThreads}
81
+ * needs and nothing more. Distinct from {@link REVIEW_THREADS_QUERY} (a single
82
+ * `first: 100` page with full comment bodies) precisely because the invariant
83
+ * must hold on PRs with more than 100 threads: a silent first-page cap would let
84
+ * a finding beyond thread #100 slip an approval through.
85
+ */
86
+ export const OUTSTANDING_THREADS_QUERY = `
87
+ query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
88
+ repository(owner: $owner, name: $name) {
89
+ pullRequest(number: $prNumber) {
90
+ reviewThreads(first: 100, after: $cursor) {
91
+ pageInfo { hasNextPage endCursor }
92
+ nodes {
93
+ isResolved
94
+ comments(first: 1) { nodes { author { login } } }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ `;
101
+
102
+ export type OutstandingThreadsResponse = {
103
+ repository: {
104
+ pullRequest: {
105
+ reviewThreads: {
106
+ pageInfo: { hasNextPage: boolean; endCursor: string | null };
107
+ nodes:
108
+ | ({
109
+ isResolved: boolean;
110
+ comments: {
111
+ nodes: ({ author: { login: string } | null } | null)[] | null;
112
+ } | null;
113
+ } | null)[]
114
+ | null;
115
+ } | null;
116
+ } | null;
117
+ } | null;
118
+ };
119
+
120
+ /**
121
+ * Count unresolved review threads on a PR whose ROOT comment was authored by
122
+ * Infraweaver's bot — the full set of open Infraweaver findings on the PR. Basis
123
+ * for the never-approve-with-outstanding-findings invariant (see the approval
124
+ * gate in `create_pull_request_review`).
125
+ *
126
+ * It's a function of ALL open findings, not just the latest commit's delta, so
127
+ * an incremental review can't approve past a prior unresolved thread. Outdated-
128
+ * but-unresolved threads still count: GitHub marks a thread `[OUTDATED]` when the
129
+ * anchor line moved (reformat, rename, force-push), which is not the concern
130
+ * being addressed. Human-reviewer threads are excluded — they belong to those
131
+ * reviewers to resolve. Walks every page (bounded) so a long-lived PR with >100
132
+ * threads can't hide an outstanding finding. #990.
133
+ */
134
+ export async function countOutstandingInfraweaverThreads(input: {
135
+ octokit: Octokit;
136
+ owner: string;
137
+ name: string;
138
+ pullNumber: number;
139
+ }): Promise<number> {
140
+ let count = 0;
141
+ let cursor: string | null = null;
142
+ // bound the walk so a misbehaving cursor can't loop forever; 50 pages = 5000
143
+ // threads, orders of magnitude beyond any real PR.
144
+ for (let page = 0; page < 50; page += 1) {
145
+ const response: OutstandingThreadsResponse = await retry(
146
+ () =>
147
+ input.octokit.graphql<OutstandingThreadsResponse>(
148
+ OUTSTANDING_THREADS_QUERY,
149
+ {
150
+ owner: input.owner,
151
+ name: input.name,
152
+ prNumber: input.pullNumber,
153
+ cursor,
154
+ },
155
+ ),
156
+ {
157
+ delaysMs: [500, 2_000],
158
+ shouldRetry: isTransientGraphqlError,
159
+ label: "outstanding threads graphql",
160
+ },
161
+ );
162
+ const conn = response.repository?.pullRequest?.reviewThreads;
163
+ for (const thread of conn?.nodes ?? []) {
164
+ if (!thread || thread.isResolved) continue;
165
+ const root = thread.comments?.nodes?.find((c) => c != null);
166
+ if (root && isInfraweaver(root.author?.login)) count += 1;
167
+ }
168
+ if (!conn?.pageInfo.hasNextPage) break;
169
+ cursor = conn.pageInfo.endCursor;
170
+ }
171
+ return count;
172
+ }
173
+
174
+ export type ReviewThreadComment = {
175
+ fullDatabaseId: string | null;
176
+ body: string;
177
+ bodyHTML: string;
178
+ createdAt: string;
179
+ diffHunk: string;
180
+ line: number | null;
181
+ startLine: number | null;
182
+ originalLine: number | null;
183
+ originalStartLine: number | null;
184
+ author: { login: string } | null;
185
+ pullRequestReview: {
186
+ databaseId: number | null;
187
+ author: { login: string } | null;
188
+ } | null;
189
+ reactionGroups: Array<{
190
+ content: string;
191
+ reactors: {
192
+ totalCount?: number;
193
+ nodes: Array<{ login: string } | null> | null;
194
+ } | null;
195
+ }> | null;
196
+ };
197
+
198
+ export type ReviewThread = {
199
+ id: string;
200
+ path: string;
201
+ line: number | null;
202
+ startLine: number | null;
203
+ diffSide: "LEFT" | "RIGHT";
204
+ isResolved: boolean;
205
+ isOutdated: boolean;
206
+ comments: {
207
+ nodes: (ReviewThreadComment | null)[] | null;
208
+ } | null;
209
+ };
210
+
211
+ export type ReviewThreadsQueryResponse = {
212
+ repository: {
213
+ pullRequest: {
214
+ reviewThreads: {
215
+ nodes: (ReviewThread | null)[] | null;
216
+ } | null;
217
+ } | null;
218
+ } | null;
219
+ };
220
+
221
+ export function countLines(str: string): number {
222
+ let count = 1;
223
+ let index = -1;
224
+ // biome-ignore lint/suspicious/noAssignInExpressions: assignment in while condition is intentional for indexOf loop pattern
225
+ while ((index = str.indexOf("\n", index + 1)) !== -1) {
226
+ count++;
227
+ }
228
+ return count;
229
+ }
230
+
231
+ // extract exactly the commented line range from diffHunk, plus context
232
+ const CONTEXT_PADDING = 3;
233
+
234
+ function extractCommentedLines(
235
+ diffHunk: string,
236
+ startLine: number | null,
237
+ endLine: number | null,
238
+ side: "LEFT" | "RIGHT",
239
+ ): string {
240
+ const lines = diffHunk.split("\n");
241
+ if (lines.length <= 1) return diffHunk;
242
+
243
+ const header = lines[0] ?? "";
244
+ const contentLines = lines.slice(1);
245
+
246
+ // parse header: @@ -old_start,old_count +new_start,new_count @@
247
+ const headerMatch = header.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
248
+ if (!headerMatch) return diffHunk;
249
+
250
+ // destructure with defaults instead of `!` — the two `(\d+)` groups are required,
251
+ // so the defaults never apply; this carries the non-null in the binding, not an assertion.
252
+ const [, oldStr = "", newStr = ""] = headerMatch;
253
+ const hunkOldStart = parseInt(oldStr, 10);
254
+ const hunkNewStart = parseInt(newStr, 10);
255
+
256
+ // LEFT = old file (deletions), RIGHT = new file (additions)
257
+ const hunkStart = side === "LEFT" ? hunkOldStart : hunkNewStart;
258
+ const commentStart = startLine ?? endLine ?? hunkStart;
259
+ const commentEnd = endLine ?? commentStart;
260
+
261
+ // walk through diff lines, tracking line numbers for both old and new files
262
+ // - lines: old file only (LEFT)
263
+ // + lines: new file only (RIGHT)
264
+ // context lines: both files
265
+ type DiffLine = { text: string; lineNum: number | null };
266
+ const diffLines: DiffLine[] = [];
267
+ let oldLineNum = hunkOldStart;
268
+ let newLineNum = hunkNewStart;
269
+
270
+ for (const line of contentLines) {
271
+ const prefix = line[0];
272
+ if (prefix === "-") {
273
+ // deletion - only has old line number
274
+ diffLines.push({
275
+ text: line,
276
+ lineNum: side === "LEFT" ? oldLineNum : null,
277
+ });
278
+ oldLineNum++;
279
+ } else if (prefix === "+") {
280
+ // addition - only has new line number
281
+ diffLines.push({
282
+ text: line,
283
+ lineNum: side === "RIGHT" ? newLineNum : null,
284
+ });
285
+ newLineNum++;
286
+ } else {
287
+ // context - has both line numbers
288
+ diffLines.push({
289
+ text: line,
290
+ lineNum: side === "LEFT" ? oldLineNum : newLineNum,
291
+ });
292
+ oldLineNum++;
293
+ newLineNum++;
294
+ }
295
+ }
296
+
297
+ // find lines for comment range with context
298
+ const targetStart = commentStart - CONTEXT_PADDING;
299
+ const targetEnd = commentEnd;
300
+
301
+ const result: string[] = [];
302
+ let truncatedBefore = 0;
303
+
304
+ for (const [i, dl] of diffLines.entries()) {
305
+ // include if: within target range, OR it's an "other side" line adjacent to included lines
306
+ const inRange =
307
+ dl.lineNum !== null &&
308
+ dl.lineNum >= targetStart &&
309
+ dl.lineNum <= targetEnd;
310
+ // include opposite-side lines if they're between included lines
311
+ const adjacentOtherSide =
312
+ dl.lineNum === null && result.length > 0 && i < diffLines.length - 1;
313
+
314
+ if (inRange || adjacentOtherSide) {
315
+ result.push(dl.text);
316
+ } else if (result.length === 0) {
317
+ truncatedBefore++;
318
+ }
319
+ }
320
+
321
+ if (truncatedBefore > 0) {
322
+ return `${header}\n... (${truncatedBefore} lines above) ...\n${result.join("\n")}`;
323
+ }
324
+ return `${header}\n${result.join("\n")}`;
325
+ }
326
+
327
+ // parsed hunk from a unified diff
328
+ export type ParsedHunk = {
329
+ header: string;
330
+ oldStart: number;
331
+ oldCount: number;
332
+ newStart: number;
333
+ newCount: number;
334
+ content: string[];
335
+ };
336
+
337
+ // parse a full file patch into individual hunks
338
+ export function parseFilePatches(patch: string): ParsedHunk[] {
339
+ const hunks: ParsedHunk[] = [];
340
+ const lines = patch.split("\n");
341
+
342
+ let currentHunk: ParsedHunk | null = null;
343
+
344
+ for (const line of lines) {
345
+ // anchor to line start (mirrors commentableLinesForFile's `^@@`): an added,
346
+ // removed, or context line whose TEXT contains a hunk-header-shaped substring
347
+ // (e.g. `+ log.info("@@ -1,2 +3,4 @@")`) must not be mistaken for a real hunk
348
+ // boundary — that would mis-key downstream overlap math and show wrong context.
349
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
350
+ if (hunkMatch) {
351
+ if (currentHunk) hunks.push(currentHunk);
352
+ // destructure with defaults — `oldS`/`newS` are required `(\d+)` groups (the
353
+ // "" default never applies); `oldC`/`newC` are the optional counts, defaulting to
354
+ // "1" exactly as the prior `?? "1"` did.
355
+ const [, oldS = "", oldC = "1", newS = "", newC = "1"] = hunkMatch;
356
+ currentHunk = {
357
+ header: line,
358
+ oldStart: parseInt(oldS, 10),
359
+ oldCount: parseInt(oldC, 10),
360
+ newStart: parseInt(newS, 10),
361
+ newCount: parseInt(newC, 10),
362
+ content: [],
363
+ };
364
+ } else if (currentHunk) {
365
+ currentHunk.content.push(line);
366
+ }
367
+ }
368
+ if (currentHunk) hunks.push(currentHunk);
369
+
370
+ return hunks;
371
+ }
372
+
373
+ // find hunks that overlap with a line range (for LEFT or RIGHT side)
374
+ function findOverlappingHunks(
375
+ hunks: ParsedHunk[],
376
+ startLine: number,
377
+ endLine: number,
378
+ side: "LEFT" | "RIGHT",
379
+ ): ParsedHunk[] {
380
+ return hunks.filter((hunk) => {
381
+ const hunkStart = side === "LEFT" ? hunk.oldStart : hunk.newStart;
382
+ const hunkCount = side === "LEFT" ? hunk.oldCount : hunk.newCount;
383
+ const hunkEnd = hunkStart + hunkCount - 1;
384
+
385
+ // check for overlap: ranges overlap if start1 <= end2 && start2 <= end1
386
+ return startLine <= hunkEnd && hunkStart <= endLine;
387
+ });
388
+ }
389
+
390
+ // extract diff content from multiple hunks for a comment range
391
+ function extractFromFilePatches(
392
+ hunks: ParsedHunk[],
393
+ startLine: number,
394
+ endLine: number,
395
+ side: "LEFT" | "RIGHT",
396
+ ): string {
397
+ const overlapping = findOverlappingHunks(hunks, startLine, endLine, side);
398
+
399
+ if (overlapping.length === 0) {
400
+ return `(no diff hunks found for lines ${startLine}-${endLine})`;
401
+ }
402
+
403
+ // a length===1 array always has element 0, so this is `overlapping[0]` without
404
+ // the `!` — the guard carries the non-null.
405
+ const single = overlapping.length === 1 ? overlapping[0] : undefined;
406
+ if (single) {
407
+ // single hunk - use existing extraction logic
408
+ const fullHunk = `${single.header}\n${single.content.join("\n")}`;
409
+ return extractCommentedLines(fullHunk, startLine, endLine, side);
410
+ }
411
+
412
+ // multiple hunks - combine them with gap indicators
413
+ const result: string[] = [];
414
+ let prevHunkEnd = 0;
415
+
416
+ for (const [i, hunk] of overlapping.entries()) {
417
+ const hunkStart = side === "LEFT" ? hunk.oldStart : hunk.newStart;
418
+ const hunkCount = side === "LEFT" ? hunk.oldCount : hunk.newCount;
419
+ const hunkEnd = hunkStart + hunkCount - 1;
420
+
421
+ // add gap indicator if there's a gap between hunks
422
+ if (i > 0 && hunkStart > prevHunkEnd + 1) {
423
+ const gapSize = hunkStart - prevHunkEnd - 1;
424
+ result.push(`\n... (${gapSize} unchanged lines) ...\n`);
425
+ }
426
+
427
+ // add the hunk header and content
428
+ result.push(hunk.header);
429
+ result.push(...hunk.content);
430
+
431
+ prevHunkEnd = hunkEnd;
432
+ }
433
+
434
+ return result.join("\n");
435
+ }
436
+
437
+ export const GetReviewComments = type({
438
+ pull_number: type.number.describe("The pull request number"),
439
+ review_id: type.number.describe("The review ID to get comments for"),
440
+ });
441
+
442
+ function hasThumbsUpFrom(
443
+ comment: ReviewThreadComment,
444
+ username: string,
445
+ ): boolean {
446
+ if (!comment.reactionGroups) return false;
447
+ const thumbsUp = comment.reactionGroups.find(
448
+ (g) => g.content === "THUMBS_UP",
449
+ );
450
+ if (!thumbsUp?.reactors?.nodes) return false;
451
+ const needle = username.toLowerCase();
452
+ return thumbsUp.reactors.nodes.some(
453
+ (r) => r?.login?.toLowerCase() === needle,
454
+ );
455
+ }
456
+
457
+ function threadHasThumbsUpFrom(
458
+ thread: ReviewThread,
459
+ username: string,
460
+ ): boolean {
461
+ const comments = thread.comments?.nodes ?? [];
462
+ return comments.some((c) => c && hasThumbsUpFrom(c, username));
463
+ }
464
+
465
+ /**
466
+ * render 👍/👎 totals as a `reactions=` tag attribute so the agent sees
467
+ * human feedback on its findings inline with the thread (a 👎 on a infraweaver
468
+ * root comment is false-positive signal; see the IncrementalReview prompt).
469
+ * Only the thumbs contents — the other six reaction types carry no
470
+ * accept/reject semantics — and only when at least one is nonzero, so the
471
+ * common no-reaction case adds zero tokens.
472
+ */
473
+ export function formatReactionCounts(comment: ReviewThreadComment): string {
474
+ const count = (content: string): number =>
475
+ comment.reactionGroups?.find((g) => g.content === content)?.reactors
476
+ ?.totalCount ?? 0;
477
+ const up = count("THUMBS_UP");
478
+ const down = count("THUMBS_DOWN");
479
+ if (up === 0 && down === 0) return "";
480
+ const parts = [
481
+ up > 0 ? `👍${up}` : null,
482
+ down > 0 ? `👎${down}` : null,
483
+ ].filter(Boolean);
484
+ return ` reactions=${parts.join(",")}`;
485
+ }
486
+
487
+ /**
488
+ * formats thread blocks into markdown with TOC and line numbers.
489
+ * extracted for testability.
490
+ */
491
+ export function formatReviewThreads(
492
+ threadBlocks: Array<{ path: string; lineRange: string; content: string[] }>,
493
+ header: {
494
+ pullNumber: number;
495
+ reviewId: number;
496
+ reviewer: string;
497
+ reviewBody?: string;
498
+ },
499
+ ) {
500
+ // header section takes: title (1) + blank (1) + "## TOC" (1) + blank (1) + N TOC entries + blank (1) + "---" (1) + blank (1)
501
+ const tocHeaderLines = 4;
502
+ const tocFooterLines = 3;
503
+ let currentLine = tocHeaderLines + threadBlocks.length + tocFooterLines + 1;
504
+
505
+ // account for review body section if present
506
+ const reviewBodyLines: string[] = [];
507
+ if (header.reviewBody) {
508
+ reviewBodyLines.push("## Review Body", "", header.reviewBody, "");
509
+ currentLine += reviewBodyLines.reduce(
510
+ (sum, line) => sum + countLines(line),
511
+ 0,
512
+ );
513
+ }
514
+
515
+ const tocEntries: string[] = [];
516
+ const threadLines: string[] = [];
517
+
518
+ for (const block of threadBlocks) {
519
+ const startLine = currentLine;
520
+ const actualLineCount = block.content.reduce(
521
+ (sum, line) => sum + countLines(line),
522
+ 0,
523
+ );
524
+ const endLine = currentLine + actualLineCount - 1;
525
+ tocEntries.push(
526
+ `- ${block.path}:${block.lineRange} → lines ${startLine}-${endLine}`,
527
+ );
528
+ threadLines.push(...block.content);
529
+ currentLine += actualLineCount;
530
+ }
531
+
532
+ const lines: string[] = [];
533
+ lines.push(
534
+ `# Review Threads (${threadBlocks.length}) for PR #${header.pullNumber} - Review ${header.reviewId} by ${header.reviewer}`,
535
+ );
536
+ lines.push("");
537
+ if (threadBlocks.length > 0) {
538
+ lines.push("## TOC");
539
+ lines.push("");
540
+ lines.push(...tocEntries);
541
+ lines.push("");
542
+ }
543
+ lines.push(...reviewBodyLines);
544
+ lines.push("---");
545
+ lines.push("");
546
+ lines.push(...threadLines);
547
+
548
+ return {
549
+ toc: tocEntries.join("\n"),
550
+ content: lines.join("\n"),
551
+ };
552
+ }
553
+
554
+ /**
555
+ * builds thread blocks from review threads and file patches.
556
+ * extracted for testability.
557
+ */
558
+ export function buildThreadBlocks(
559
+ threads: ReviewThread[],
560
+ filePatchMap: Map<string, ParsedHunk[]>,
561
+ reviewId: number,
562
+ ) {
563
+ // sort threads by file path, then by line number
564
+ threads.sort((a, b) => {
565
+ const pathCmp = a.path.localeCompare(b.path);
566
+ if (pathCmp !== 0) return pathCmp;
567
+ const aLine = a.startLine ?? a.line ?? 0;
568
+ const bLine = b.startLine ?? b.line ?? 0;
569
+ return aLine - bLine;
570
+ });
571
+
572
+ const threadBlocks: Array<{
573
+ path: string;
574
+ lineRange: string;
575
+ content: string[];
576
+ }> = [];
577
+
578
+ for (const thread of threads) {
579
+ const allComments = (thread.comments?.nodes ?? []).filter(
580
+ (c): c is ReviewThreadComment => c !== null,
581
+ );
582
+ if (allComments.length === 0) continue;
583
+
584
+ // get line info from thread, or fall back to first comment's line info
585
+ const firstComment = allComments[0];
586
+ const line =
587
+ thread.line ??
588
+ firstComment?.line ??
589
+ firstComment?.originalLine ??
590
+ thread.startLine ??
591
+ 0;
592
+ const startLine =
593
+ thread.startLine ??
594
+ firstComment?.startLine ??
595
+ firstComment?.originalStartLine ??
596
+ line;
597
+ const lineRange = startLine === line ? `${line}` : `${startLine}-${line}`;
598
+ const block: string[] = [];
599
+
600
+ // header with file:line range and status
601
+ const status = thread.isResolved
602
+ ? " [RESOLVED]"
603
+ : thread.isOutdated
604
+ ? " [OUTDATED]"
605
+ : "";
606
+ block.push(`## ${thread.path}:${lineRange}${status}`);
607
+ block.push("");
608
+
609
+ // show all comments in the thread (full conversation history)
610
+ for (const comment of allComments) {
611
+ const author = comment.author?.login ?? "unknown";
612
+ const isTargetReview = comment.pullRequestReview?.databaseId === reviewId;
613
+ const marker = isTargetReview ? " *" : "";
614
+
615
+ block.push(
616
+ `\`\`\`\`comment author=${author} id=${comment.fullDatabaseId ?? "unknown"} review=${comment.pullRequestReview?.databaseId ?? "unknown"} thread=${thread.id}${formatReactionCounts(comment)}${marker}`,
617
+ );
618
+ block.push(comment.body || "(no comment body)");
619
+ block.push("````");
620
+ block.push("");
621
+ }
622
+
623
+ // diff context
624
+ const fileHunks = filePatchMap.get(thread.path);
625
+ const firstCommentWithHunk = allComments.find((c) => c.diffHunk);
626
+ let diffContent: string | null = null;
627
+
628
+ if (fileHunks && fileHunks.length > 0) {
629
+ const overlapping = findOverlappingHunks(
630
+ fileHunks,
631
+ startLine,
632
+ line,
633
+ thread.diffSide,
634
+ );
635
+ if (overlapping.length > 0) {
636
+ diffContent = extractFromFilePatches(
637
+ fileHunks,
638
+ startLine,
639
+ line,
640
+ thread.diffSide,
641
+ );
642
+ }
643
+ }
644
+
645
+ if (!diffContent && firstCommentWithHunk) {
646
+ diffContent = extractCommentedLines(
647
+ firstCommentWithHunk.diffHunk,
648
+ startLine,
649
+ line,
650
+ thread.diffSide,
651
+ );
652
+ }
653
+
654
+ if (diffContent) {
655
+ block.push(
656
+ `\`\`\`diff file=${thread.path} lines=${lineRange} side=${thread.diffSide}`,
657
+ );
658
+ block.push(diffContent);
659
+ block.push("```");
660
+ block.push("");
661
+ } else {
662
+ block.push(
663
+ `\`\`\`diff file=${thread.path} lines=${lineRange} side=${thread.diffSide}`,
664
+ );
665
+ block.push(`(no diff context available - comment on unchanged lines)`);
666
+ block.push("```");
667
+ block.push("");
668
+ }
669
+
670
+ threadBlocks.push({ path: thread.path, lineRange, content: block });
671
+ }
672
+
673
+ return threadBlocks;
674
+ }
675
+
676
+ /**
677
+ * `truncated` is true when GitHub returned a full page of threads (first: 100)
678
+ * or any thread returned a full page of comments (first: 50), meaning some
679
+ * review feedback is NOT in `threads`. Surfaced to the agent so it knows the
680
+ * thread set it's addressing may be incomplete rather than silently dropping
681
+ * the overflow (the cap is only logged for the operator).
682
+ */
683
+ async function getReviewThreads(
684
+ input: GetReviewDataInput,
685
+ ): Promise<{ threads: ReviewThread[]; truncated: boolean }> {
686
+ const response = await retry(
687
+ () =>
688
+ input.octokit.graphql<ReviewThreadsQueryResponse>(REVIEW_THREADS_QUERY, {
689
+ owner: input.owner,
690
+ name: input.name,
691
+ prNumber: input.pullNumber,
692
+ }),
693
+ {
694
+ delaysMs: [500, 2_000],
695
+ shouldRetry: isTransientGraphqlError,
696
+ label: "reviewThreads graphql",
697
+ },
698
+ );
699
+
700
+ const allThreads =
701
+ response.repository?.pullRequest?.reviewThreads?.nodes ?? [];
702
+
703
+ let truncated = false;
704
+ if (allThreads.length >= 100) {
705
+ truncated = true;
706
+ log.warning(
707
+ `PR ${input.owner}/${input.name}#${input.pullNumber}: reviewThreads returned 100 results (limit reached, some threads may be missing)`,
708
+ );
709
+ }
710
+ for (const thread of allThreads) {
711
+ if (thread?.comments?.nodes && thread.comments.nodes.length >= 50) {
712
+ truncated = true;
713
+ log.warning(
714
+ `PR ${input.owner}/${input.name}#${input.pullNumber}: review thread at ${thread.path}:${thread.line} has 50 comments (limit reached, some comments may be missing)`,
715
+ );
716
+ }
717
+ }
718
+
719
+ const threadsForReview = allThreads.filter(
720
+ (thread): thread is ReviewThread => {
721
+ if (!thread?.comments?.nodes) return false;
722
+ return thread.comments.nodes.some(
723
+ (c) => c?.pullRequestReview?.databaseId === input.reviewId,
724
+ );
725
+ },
726
+ );
727
+
728
+ if (!input.approvedBy) {
729
+ return { threads: threadsForReview, truncated };
730
+ }
731
+
732
+ const username = input.approvedBy;
733
+ return {
734
+ threads: threadsForReview.filter((thread) =>
735
+ threadHasThumbsUpFrom(thread, username),
736
+ ),
737
+ truncated,
738
+ };
739
+ }
740
+
741
+ interface GetReviewDataInput {
742
+ octokit: Octokit;
743
+ owner: string;
744
+ name: string;
745
+ pullNumber: number;
746
+ reviewId: number;
747
+ approvedBy?: string | undefined;
748
+ tmpdir: string;
749
+ githubToken: string;
750
+ }
751
+
752
+ // pure formatter: takes already-fetched GitHub responses and produces the
753
+ // review data the MCP tool returns. extracted from getReviewData so tests
754
+ // can drive it from checked-in fixtures without live API access.
755
+ //
756
+ // `prFiles` may be empty when `threads` is empty — callers that hit the
757
+ // network should skip the listFiles call in that case as a perf
758
+ // optimization. when both are empty and `review.body` is also empty, the
759
+ // formatter returns undefined just like getReviewData.
760
+ export interface FormatReviewDataInput {
761
+ review: ReviewResponse;
762
+ threads: ReviewThread[];
763
+ prFiles: ReviewPrFile[];
764
+ pullNumber: number;
765
+ reviewId: number;
766
+ }
767
+
768
+ export type ReviewResponse = {
769
+ body: string | null | undefined;
770
+ user: { login: string } | null | undefined;
771
+ };
772
+
773
+ export type ReviewPrFile = {
774
+ filename: string;
775
+ patch?: string | undefined;
776
+ };
777
+
778
+ export function formatReviewData(input: FormatReviewDataInput):
779
+ | {
780
+ threadBlocks: Array<{
781
+ path: string;
782
+ lineRange: string;
783
+ content: string[];
784
+ }>;
785
+ reviewer: string;
786
+ formatted: { toc: string; content: string };
787
+ }
788
+ | undefined {
789
+ const rawReviewBody = input.review.body;
790
+ const reviewBody = rawReviewBody ? stripExistingFooter(rawReviewBody) : "";
791
+ const reviewer = input.review.user?.login ?? "unknown";
792
+
793
+ if (input.threads.length === 0 && !reviewBody) return undefined;
794
+
795
+ let threadBlocks: Array<{
796
+ path: string;
797
+ lineRange: string;
798
+ content: string[];
799
+ }> = [];
800
+
801
+ if (input.threads.length > 0) {
802
+ const filePatchMap = new Map<string, ParsedHunk[]>();
803
+ for (const file of input.prFiles) {
804
+ if (file.patch) {
805
+ filePatchMap.set(file.filename, parseFilePatches(file.patch));
806
+ }
807
+ }
808
+ threadBlocks = buildThreadBlocks(
809
+ input.threads,
810
+ filePatchMap,
811
+ input.reviewId,
812
+ );
813
+ }
814
+
815
+ const formatted = formatReviewThreads(threadBlocks, {
816
+ pullNumber: input.pullNumber,
817
+ reviewId: input.reviewId,
818
+ reviewer,
819
+ reviewBody,
820
+ });
821
+
822
+ return { threadBlocks, reviewer, formatted };
823
+ }
824
+
825
+ export async function getReviewData(input: GetReviewDataInput): Promise<
826
+ | {
827
+ threadBlocks: Array<{
828
+ path: string;
829
+ lineRange: string;
830
+ content: string[];
831
+ }>;
832
+ reviewer: string;
833
+ formatted: { toc: string; content: string };
834
+ truncated: boolean;
835
+ }
836
+ | undefined
837
+ > {
838
+ const [review, threadsResult] = await Promise.all([
839
+ input.octokit.rest.pulls.getReview({
840
+ owner: input.owner,
841
+ repo: input.name,
842
+ pull_number: input.pullNumber,
843
+ review_id: input.reviewId,
844
+ headers: { accept: "application/vnd.github.full+json" },
845
+ }),
846
+ getReviewThreads(input),
847
+ ]);
848
+ const { threads, truncated } = threadsResult;
849
+
850
+ // skip listFiles when there are no threads — prFiles is only used for
851
+ // building thread blocks, and an empty array short-circuits below.
852
+ const prFiles =
853
+ threads.length > 0
854
+ ? await input.octokit.paginate(input.octokit.rest.pulls.listFiles, {
855
+ owner: input.owner,
856
+ repo: input.name,
857
+ pull_number: input.pullNumber,
858
+ per_page: 100,
859
+ })
860
+ : [];
861
+
862
+ if (review.data.body) {
863
+ review.data.body =
864
+ (await resolveBodyAssets({
865
+ body: review.data.body,
866
+ bodyHtml: review.data.body_html,
867
+ tmpdir: input.tmpdir,
868
+ githubToken: input.githubToken,
869
+ })) ?? review.data.body;
870
+ }
871
+
872
+ for (const thread of threads) {
873
+ for (const comment of thread.comments?.nodes ?? []) {
874
+ if (comment?.body) {
875
+ comment.body =
876
+ (await resolveBodyAssets({
877
+ body: comment.body,
878
+ bodyHtml: comment.bodyHTML,
879
+ tmpdir: input.tmpdir,
880
+ githubToken: input.githubToken,
881
+ })) ?? comment.body;
882
+ }
883
+ }
884
+ }
885
+
886
+ const formatted = formatReviewData({
887
+ review: review.data,
888
+ threads,
889
+ prFiles,
890
+ pullNumber: input.pullNumber,
891
+ reviewId: input.reviewId,
892
+ });
893
+ if (!formatted) return undefined;
894
+ return { ...formatted, truncated };
895
+ }
896
+
897
+ export function GetReviewCommentsTool(ctx: GitHubToolContext) {
898
+ return tool({
899
+ name: "get_review_comments",
900
+ description:
901
+ "Get review comments for a pull request review with full thread context. " +
902
+ "Example: `get_review_comments({ pull_number: 1234, review_id: 567890 })`. " +
903
+ "Automatically filters to approved comments when applicable. " +
904
+ "Returns a TOC and commentsPath pointing to a markdown file with full comment details.",
905
+ parameters: GetReviewComments,
906
+ execute: execute(async (params) => {
907
+ // auto-filter to approved comments when the event has approved_only set
908
+ const approvedBy =
909
+ ctx.payload.event.trigger === "fix_review" &&
910
+ ctx.payload.event.approved_only
911
+ ? ctx.payload.triggerer
912
+ : undefined;
913
+
914
+ const result = await getReviewData({
915
+ octokit: ctx.octokit,
916
+ owner: ctx.repo.owner,
917
+ name: ctx.repo.name,
918
+ pullNumber: params.pull_number,
919
+ reviewId: params.review_id,
920
+ approvedBy,
921
+ tmpdir: ctx.tmpdir,
922
+ githubToken: ctx.githubInstallationToken,
923
+ });
924
+
925
+ if (!result) {
926
+ return {
927
+ review_id: params.review_id,
928
+ pull_number: params.pull_number,
929
+ reviewer: "unknown",
930
+ threadCount: 0,
931
+ commentsPath: null,
932
+ toc: null,
933
+ instructions: approvedBy
934
+ ? `no threads with 👍 from ${approvedBy}`
935
+ : "no threads found for this review",
936
+ };
937
+ }
938
+
939
+ const { threadBlocks, reviewer, formatted, truncated } = result;
940
+
941
+ const tempDir = getTempDir();
942
+ const filename = `review-${params.review_id}-threads.md`;
943
+ const commentsPath = join(tempDir, filename);
944
+ writeFileSync(commentsPath, formatted.content);
945
+ log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
946
+
947
+ const truncationNote = truncated
948
+ ? ` WARNING: this PR has more review threads/comments than a single fetch returns, so the set above is INCOMPLETE — ` +
949
+ `some threads or comments are not included. Address what's here, but do not treat the list as exhaustive.`
950
+ : "";
951
+
952
+ return {
953
+ review_id: params.review_id,
954
+ pull_number: params.pull_number,
955
+ reviewer,
956
+ threadCount: threadBlocks.length,
957
+ truncated,
958
+ commentsPath,
959
+ toc: formatted.toc,
960
+ instructions:
961
+ `the file at commentsPath contains ${threadBlocks.length} review threads with full conversation history. ` +
962
+ `comments marked with * are from the target review (${params.review_id}). ` +
963
+ `the TOC shows each thread's file:line and the line number where it appears in the file. ` +
964
+ `to read a specific thread, use: grep -A 50 "^## <file:line>" ${commentsPath} ` +
965
+ `(replace <file:line> with the path from the TOC, e.g. "^## action/utils/foo.ts:42"). ` +
966
+ `address each thread in order, working through one file at a time.` +
967
+ truncationNote,
968
+ };
969
+ }),
970
+ });
971
+ }
972
+
973
+ export const ListPullRequestReviews = type({
974
+ pull_number: type.number.describe(
975
+ "The pull request number to list reviews for",
976
+ ),
977
+ });
978
+
979
+ export function ListPullRequestReviewsTool(ctx: GitHubToolContext) {
980
+ return tool({
981
+ name: "list_pull_request_reviews",
982
+ description:
983
+ "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments. " +
984
+ "Example: `list_pull_request_reviews({ pull_number: 1234 })`.",
985
+ parameters: ListPullRequestReviews,
986
+ execute: execute(async (params) => {
987
+ const reviews = await ctx.octokit.paginate(
988
+ ctx.octokit.rest.pulls.listReviews,
989
+ {
990
+ owner: ctx.repo.owner,
991
+ repo: ctx.repo.name,
992
+ pull_number: params.pull_number,
993
+ headers: { accept: "application/vnd.github.full+json" },
994
+ },
995
+ );
996
+
997
+ const processedReviews = await Promise.all(
998
+ reviews.map(async (review) => ({
999
+ id: review.id,
1000
+ node_id: review.node_id,
1001
+ body: await resolveBodyAssets({
1002
+ body: review.body,
1003
+ bodyHtml: review.body_html,
1004
+ tmpdir: ctx.tmpdir,
1005
+ githubToken: ctx.githubInstallationToken,
1006
+ }),
1007
+ state: review.state,
1008
+ user: review.user?.login,
1009
+ submitted_at: review.submitted_at,
1010
+ commit_id: review.commit_id,
1011
+ html_url: review.html_url,
1012
+ })),
1013
+ );
1014
+
1015
+ return {
1016
+ pull_number: params.pull_number,
1017
+ reviews: processedReviews,
1018
+ count: processedReviews.length,
1019
+ };
1020
+ }),
1021
+ });
1022
+ }
1023
+
1024
+ const RESOLVE_REVIEW_THREAD_MUTATION = `
1025
+ mutation($threadId: ID!) {
1026
+ resolveReviewThread(input: {threadId: $threadId}) {
1027
+ thread {
1028
+ id
1029
+ isResolved
1030
+ }
1031
+ }
1032
+ }
1033
+ `;
1034
+
1035
+ // resolve a thread node id to the PR it belongs to, so resolve_review_thread can
1036
+ // be scope-bound (a thread id is opaque, unlike a bare PR number).
1037
+ const REVIEW_THREAD_PR_QUERY = `
1038
+ query($id: ID!) {
1039
+ node(id: $id) {
1040
+ ... on PullRequestReviewThread {
1041
+ pullRequest { number }
1042
+ }
1043
+ }
1044
+ }
1045
+ `;
1046
+
1047
+ /**
1048
+ * SECURITY: bind resolve_review_thread to the run's scoped PR (or one it
1049
+ * created). The thread id is opaque, so look up its PR first. Skips the check
1050
+ * for standalone runs (no triggering issue/PR) and, deliberately, when the
1051
+ * lookup can't determine a PR number — resolving a thread is low-risk and
1052
+ * reversible, so a transient API/permission hiccup must not block a legitimate
1053
+ * resolve. A definite out-of-scope match still throws.
1054
+ */
1055
+ async function assertThreadInScope(
1056
+ ctx: GitHubToolContext,
1057
+ threadId: string,
1058
+ ): Promise<void> {
1059
+ if (ctx.payload?.event?.issue_number === undefined) return;
1060
+ let prNumber: number | undefined;
1061
+ try {
1062
+ const lookup = await ctx.octokit.graphql<{
1063
+ node?: { pullRequest?: { number?: number } } | null;
1064
+ }>(REVIEW_THREAD_PR_QUERY, { id: threadId });
1065
+ prNumber = lookup.node?.pullRequest?.number;
1066
+ } catch (e) {
1067
+ log.debug(
1068
+ `resolve_review_thread scope lookup failed (allowing): ${e instanceof Error ? e.message : String(e)}`,
1069
+ );
1070
+ return;
1071
+ }
1072
+ if (typeof prNumber === "number") {
1073
+ assertTargetInScope(ctx, prNumber, "resolve a review thread on");
1074
+ }
1075
+ }
1076
+
1077
+ export const ResolveReviewThread = type({
1078
+ thread_id: type.string.describe(
1079
+ "The GraphQL node ID of the review thread to resolve",
1080
+ ),
1081
+ });
1082
+
1083
+ export function ResolveReviewThreadTool(ctx: GitHubToolContext) {
1084
+ return tool({
1085
+ mutates: true,
1086
+ name: "resolve_review_thread",
1087
+ description:
1088
+ "Mark a review thread as resolved using GitHub's GraphQL API. " +
1089
+ "Only call this after addressing the review feedback, implementing fixes, testing them, and posting a reply. " +
1090
+ "Do not resolve threads that are already resolved, threads where no action was taken, or threads where you disagree with the feedback.",
1091
+ parameters: ResolveReviewThread,
1092
+ execute: execute(async (params) => {
1093
+ await assertThreadInScope(ctx, params.thread_id);
1094
+ try {
1095
+ const response = await ctx.octokit.graphql<{
1096
+ resolveReviewThread: {
1097
+ thread: {
1098
+ id: string;
1099
+ isResolved: boolean;
1100
+ };
1101
+ };
1102
+ }>(RESOLVE_REVIEW_THREAD_MUTATION, {
1103
+ threadId: params.thread_id,
1104
+ });
1105
+
1106
+ const thread = response.resolveReviewThread.thread;
1107
+ log.info(`» resolved review thread ${thread.id}`);
1108
+
1109
+ return {
1110
+ thread_id: thread.id,
1111
+ is_resolved: thread.isResolved,
1112
+ success: true,
1113
+ message: "Thread resolved successfully",
1114
+ };
1115
+ } catch (error) {
1116
+ // handle common error cases gracefully
1117
+ const errorMessage =
1118
+ error instanceof Error ? error.message : String(error);
1119
+ const isResolved =
1120
+ errorMessage.includes("already resolved") ||
1121
+ errorMessage.includes("isResolved");
1122
+
1123
+ const message = isResolved
1124
+ ? `thread ${params.thread_id} was already resolved`
1125
+ : `failed to resolve thread ${params.thread_id}: ${errorMessage}`;
1126
+ log.info(message);
1127
+
1128
+ return {
1129
+ thread_id: params.thread_id,
1130
+ is_resolved: isResolved,
1131
+ success: isResolved,
1132
+ message,
1133
+ };
1134
+ }
1135
+ }),
1136
+ });
1137
+ }