terramend 0.2.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 (406) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +145 -0
  3. package/dist/agents/claude.d.ts +73 -0
  4. package/dist/agents/claudePretoolGate.d.ts +99 -0
  5. package/dist/agents/gateServer.d.ts +7 -0
  6. package/dist/agents/index.d.ts +6 -0
  7. package/dist/agents/nativeFsDenies.d.ts +28 -0
  8. package/dist/agents/opencode.d.ts +231 -0
  9. package/dist/agents/opencodePlugin.d.ts +85 -0
  10. package/dist/agents/opencodeShared.d.ts +40 -0
  11. package/dist/agents/postRun.d.ts +132 -0
  12. package/dist/agents/reviewer.d.ts +38 -0
  13. package/dist/agents/sessionLabeler.d.ts +97 -0
  14. package/dist/agents/shared.d.ts +189 -0
  15. package/dist/agents/subagentModels.d.ts +19 -0
  16. package/dist/agents/subagentToolGates.d.ts +55 -0
  17. package/dist/cli.mjs +197426 -0
  18. package/dist/external.d.ts +227 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +196783 -0
  21. package/dist/internal/index.d.ts +18 -0
  22. package/dist/internal.js +1714 -0
  23. package/dist/lifecycle.d.ts +2 -0
  24. package/dist/main.d.ts +8 -0
  25. package/dist/mcp/arkConfig.d.ts +1 -0
  26. package/dist/mcp/checkSuite.d.ts +25 -0
  27. package/dist/mcp/checkout.d.ts +77 -0
  28. package/dist/mcp/comment.d.ts +119 -0
  29. package/dist/mcp/commitInfo.d.ts +9 -0
  30. package/dist/mcp/crosswalk.d.ts +105 -0
  31. package/dist/mcp/dependencies.d.ts +8 -0
  32. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  33. package/dist/mcp/git.d.ts +46 -0
  34. package/dist/mcp/guardrails.d.ts +104 -0
  35. package/dist/mcp/issue.d.ts +18 -0
  36. package/dist/mcp/issueComments.d.ts +9 -0
  37. package/dist/mcp/issueEvents.d.ts +9 -0
  38. package/dist/mcp/issueInfo.d.ts +9 -0
  39. package/dist/mcp/labels.d.ts +12 -0
  40. package/dist/mcp/localContext.d.ts +19 -0
  41. package/dist/mcp/moduleExtraction.d.ts +71 -0
  42. package/dist/mcp/moduleTests.d.ts +104 -0
  43. package/dist/mcp/modules.d.ts +179 -0
  44. package/dist/mcp/output.d.ts +12 -0
  45. package/dist/mcp/pathSafety.d.ts +14 -0
  46. package/dist/mcp/policy.d.ts +48 -0
  47. package/dist/mcp/pr.d.ts +49 -0
  48. package/dist/mcp/prInfo.d.ts +9 -0
  49. package/dist/mcp/providerSchema.d.ts +50 -0
  50. package/dist/mcp/review.d.ts +199 -0
  51. package/dist/mcp/reviewComments.d.ts +178 -0
  52. package/dist/mcp/roots.d.ts +58 -0
  53. package/dist/mcp/scope.d.ts +15 -0
  54. package/dist/mcp/selectMode.d.ts +18 -0
  55. package/dist/mcp/server.d.ts +48 -0
  56. package/dist/mcp/shared.d.ts +47 -0
  57. package/dist/mcp/shell.d.ts +37 -0
  58. package/dist/mcp/staleFix.d.ts +51 -0
  59. package/dist/mcp/terraform/cost.d.ts +55 -0
  60. package/dist/mcp/terraform/currency.d.ts +94 -0
  61. package/dist/mcp/terraform/decisions.d.ts +178 -0
  62. package/dist/mcp/terraform/findings.d.ts +75 -0
  63. package/dist/mcp/terraform/plan.d.ts +157 -0
  64. package/dist/mcp/terraform/scanners.d.ts +131 -0
  65. package/dist/mcp/terraform/tools.d.ts +63 -0
  66. package/dist/mcp/terraform/types.d.ts +172 -0
  67. package/dist/mcp/terraform.d.ts +22 -0
  68. package/dist/mcp/terratest.d.ts +83 -0
  69. package/dist/mcp/upload.d.ts +6 -0
  70. package/dist/models.d.ts +171 -0
  71. package/dist/modes.d.ts +26 -0
  72. package/dist/prep/index.d.ts +7 -0
  73. package/dist/prep/installNodeDependencies.d.ts +2 -0
  74. package/dist/prep/installPythonDependencies.d.ts +2 -0
  75. package/dist/prep/types.d.ts +31 -0
  76. package/dist/reviewQuality.d.ts +64 -0
  77. package/dist/skills/terraform-best-practices/SKILL.md +369 -0
  78. package/dist/toolState.d.ts +135 -0
  79. package/dist/utils/activity.d.ts +40 -0
  80. package/dist/utils/agent.d.ts +20 -0
  81. package/dist/utils/agentHangReport.d.ts +38 -0
  82. package/dist/utils/apiFetch.d.ts +19 -0
  83. package/dist/utils/apiKeys.d.ts +41 -0
  84. package/dist/utils/apiUrl.d.ts +20 -0
  85. package/dist/utils/assets.d.ts +8 -0
  86. package/dist/utils/billingErrors.d.ts +85 -0
  87. package/dist/utils/body.d.ts +34 -0
  88. package/dist/utils/buildTerramendFooter.d.ts +25 -0
  89. package/dist/utils/byokFallback.d.ts +85 -0
  90. package/dist/utils/claudeSubscription.d.ts +30 -0
  91. package/dist/utils/cli.d.ts +10 -0
  92. package/dist/utils/codexHome.d.ts +29 -0
  93. package/dist/utils/codexOAuth.d.ts +60 -0
  94. package/dist/utils/diffCoverage.d.ts +63 -0
  95. package/dist/utils/errorReport.d.ts +17 -0
  96. package/dist/utils/exitHandler.d.ts +8 -0
  97. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  98. package/dist/utils/gitAuth.d.ts +84 -0
  99. package/dist/utils/gitAuthServer.d.ts +24 -0
  100. package/dist/utils/github.d.ts +78 -0
  101. package/dist/utils/globals.d.ts +3 -0
  102. package/dist/utils/install.d.ts +60 -0
  103. package/dist/utils/instructions.d.ts +48 -0
  104. package/dist/utils/leapingComment.d.ts +11 -0
  105. package/dist/utils/learnings.d.ts +62 -0
  106. package/dist/utils/learningsTruncate.d.ts +25 -0
  107. package/dist/utils/lifecycle.d.ts +57 -0
  108. package/dist/utils/log.d.ts +111 -0
  109. package/dist/utils/normalizeEnv.d.ts +30 -0
  110. package/dist/utils/openCodeModels.d.ts +11 -0
  111. package/dist/utils/overrides.d.ts +40 -0
  112. package/dist/utils/packageManager.d.ts +49 -0
  113. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  114. package/dist/utils/payload.d.ts +105 -0
  115. package/dist/utils/prSummary.d.ts +61 -0
  116. package/dist/utils/progressComment.d.ts +146 -0
  117. package/dist/utils/providerErrors.d.ts +31 -0
  118. package/dist/utils/rangeDiff.d.ts +51 -0
  119. package/dist/utils/remediationCommand.d.ts +55 -0
  120. package/dist/utils/retry.d.ts +13 -0
  121. package/dist/utils/reviewCleanup.d.ts +14 -0
  122. package/dist/utils/run.d.ts +9 -0
  123. package/dist/utils/runContext.d.ts +60 -0
  124. package/dist/utils/runContextData.d.ts +23 -0
  125. package/dist/utils/runErrorRenderer.d.ts +64 -0
  126. package/dist/utils/runLifecycle.d.ts +86 -0
  127. package/dist/utils/runStartupLog.d.ts +15 -0
  128. package/dist/utils/secrets.d.ts +22 -0
  129. package/dist/utils/setup.d.ts +90 -0
  130. package/dist/utils/shell.d.ts +32 -0
  131. package/dist/utils/skills.d.ts +10 -0
  132. package/dist/utils/subprocess.d.ts +80 -0
  133. package/dist/utils/terraformMcp.d.ts +42 -0
  134. package/dist/utils/time.d.ts +15 -0
  135. package/dist/utils/timer.d.ts +23 -0
  136. package/dist/utils/todoTracking.d.ts +16 -0
  137. package/dist/utils/token.d.ts +39 -0
  138. package/dist/utils/version.d.ts +2 -0
  139. package/dist/utils/versioning.d.ts +7 -0
  140. package/dist/utils/vertex.d.ts +16 -0
  141. package/dist/utils/workflow.d.ts +13 -0
  142. package/package.json +119 -0
  143. package/src/agents/claude.test.ts +1016 -0
  144. package/src/agents/claude.ts +1246 -0
  145. package/src/agents/claudePretoolGate.test.ts +28 -0
  146. package/src/agents/claudePretoolGate.ts +173 -0
  147. package/src/agents/gateServer.test.ts +204 -0
  148. package/src/agents/gateServer.ts +124 -0
  149. package/src/agents/index.ts +10 -0
  150. package/src/agents/nativeFsDenies.ts +82 -0
  151. package/src/agents/opencode.test.ts +1440 -0
  152. package/src/agents/opencode.ts +1312 -0
  153. package/src/agents/opencodePlugin.ts +222 -0
  154. package/src/agents/opencodeShared.test.ts +34 -0
  155. package/src/agents/opencodeShared.ts +121 -0
  156. package/src/agents/postRun.test.ts +549 -0
  157. package/src/agents/postRun.ts +535 -0
  158. package/src/agents/reviewer.ts +104 -0
  159. package/src/agents/sessionLabeler.test.ts +247 -0
  160. package/src/agents/sessionLabeler.ts +178 -0
  161. package/src/agents/shared.test.ts +76 -0
  162. package/src/agents/shared.ts +292 -0
  163. package/src/agents/subagentModels.test.ts +113 -0
  164. package/src/agents/subagentModels.ts +40 -0
  165. package/src/agents/subagentRegistration.test.ts +41 -0
  166. package/src/agents/subagentToolGates.ts +114 -0
  167. package/src/cli.test.ts +129 -0
  168. package/src/cli.ts +105 -0
  169. package/src/commands/gha.test.ts +192 -0
  170. package/src/commands/gha.ts +188 -0
  171. package/src/commands/mcp.ts +122 -0
  172. package/src/config.ts +1 -0
  173. package/src/entry.ts +7 -0
  174. package/src/entryPost.stdlibOnly.test.ts +109 -0
  175. package/src/entryPost.ts +99 -0
  176. package/src/external.test.ts +16 -0
  177. package/src/external.ts +302 -0
  178. package/src/index.ts +11 -0
  179. package/src/internal/index.ts +71 -0
  180. package/src/lifecycle.ts +2 -0
  181. package/src/main.test.ts +873 -0
  182. package/src/main.ts +712 -0
  183. package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
  184. package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
  185. package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
  186. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  187. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  188. package/src/mcp/arkConfig.ts +7 -0
  189. package/src/mcp/checkSuite.test.ts +245 -0
  190. package/src/mcp/checkSuite.ts +255 -0
  191. package/src/mcp/checkout.test.ts +752 -0
  192. package/src/mcp/checkout.ts +886 -0
  193. package/src/mcp/comment.test.ts +772 -0
  194. package/src/mcp/comment.ts +582 -0
  195. package/src/mcp/commitInfo.test.ts +127 -0
  196. package/src/mcp/commitInfo.ts +61 -0
  197. package/src/mcp/crosswalk.test.ts +106 -0
  198. package/src/mcp/crosswalk.ts +339 -0
  199. package/src/mcp/dependencies.test.ts +309 -0
  200. package/src/mcp/dependencies.ts +189 -0
  201. package/src/mcp/geminiSanitizer.test.ts +287 -0
  202. package/src/mcp/geminiSanitizer.ts +207 -0
  203. package/src/mcp/git.test.ts +1083 -0
  204. package/src/mcp/git.ts +890 -0
  205. package/src/mcp/guardrails.test.ts +705 -0
  206. package/src/mcp/guardrails.ts +465 -0
  207. package/src/mcp/issue.test.ts +113 -0
  208. package/src/mcp/issue.ts +73 -0
  209. package/src/mcp/issueComments.test.ts +69 -0
  210. package/src/mcp/issueComments.ts +48 -0
  211. package/src/mcp/issueEvents.test.ts +134 -0
  212. package/src/mcp/issueEvents.ts +100 -0
  213. package/src/mcp/issueInfo.test.ts +104 -0
  214. package/src/mcp/issueInfo.ts +72 -0
  215. package/src/mcp/labels.test.ts +52 -0
  216. package/src/mcp/labels.ts +34 -0
  217. package/src/mcp/localContext.ts +28 -0
  218. package/src/mcp/localServer.test.ts +75 -0
  219. package/src/mcp/localServer.ts +131 -0
  220. package/src/mcp/moduleExtraction.test.ts +261 -0
  221. package/src/mcp/moduleExtraction.ts +313 -0
  222. package/src/mcp/moduleTests.test.ts +269 -0
  223. package/src/mcp/moduleTests.ts +421 -0
  224. package/src/mcp/modules.test.ts +640 -0
  225. package/src/mcp/modules.ts +696 -0
  226. package/src/mcp/output.test.ts +96 -0
  227. package/src/mcp/output.ts +70 -0
  228. package/src/mcp/pathSafety.test.ts +44 -0
  229. package/src/mcp/pathSafety.ts +28 -0
  230. package/src/mcp/policy.test.ts +282 -0
  231. package/src/mcp/policy.ts +199 -0
  232. package/src/mcp/pr.test.ts +387 -0
  233. package/src/mcp/pr.ts +194 -0
  234. package/src/mcp/prInfo.test.ts +96 -0
  235. package/src/mcp/prInfo.ts +91 -0
  236. package/src/mcp/providerSchema.test.ts +85 -0
  237. package/src/mcp/providerSchema.ts +175 -0
  238. package/src/mcp/review.test.ts +936 -0
  239. package/src/mcp/review.ts +923 -0
  240. package/src/mcp/reviewComments.test.ts +549 -0
  241. package/src/mcp/reviewComments.ts +896 -0
  242. package/src/mcp/roots.test.ts +175 -0
  243. package/src/mcp/roots.ts +217 -0
  244. package/src/mcp/scope.test.ts +59 -0
  245. package/src/mcp/scope.ts +65 -0
  246. package/src/mcp/security.test.ts +720 -0
  247. package/src/mcp/selectMode.test.ts +210 -0
  248. package/src/mcp/selectMode.ts +181 -0
  249. package/src/mcp/server.test.ts +292 -0
  250. package/src/mcp/server.ts +403 -0
  251. package/src/mcp/shared.ts +100 -0
  252. package/src/mcp/shell.test.ts +520 -0
  253. package/src/mcp/shell.ts +505 -0
  254. package/src/mcp/staleFix.test.ts +237 -0
  255. package/src/mcp/staleFix.ts +277 -0
  256. package/src/mcp/terraform/cost.ts +163 -0
  257. package/src/mcp/terraform/currency.test.ts +338 -0
  258. package/src/mcp/terraform/currency.ts +336 -0
  259. package/src/mcp/terraform/decisions.ts +527 -0
  260. package/src/mcp/terraform/findings.ts +333 -0
  261. package/src/mcp/terraform/plan.ts +348 -0
  262. package/src/mcp/terraform/scanners.ts +809 -0
  263. package/src/mcp/terraform/tools.test.ts +1071 -0
  264. package/src/mcp/terraform/tools.ts +908 -0
  265. package/src/mcp/terraform/types.ts +305 -0
  266. package/src/mcp/terraform.test.ts +1957 -0
  267. package/src/mcp/terraform.ts +23 -0
  268. package/src/mcp/terratest.test.ts +105 -0
  269. package/src/mcp/terratest.ts +196 -0
  270. package/src/mcp/toolFiltering.test.ts +85 -0
  271. package/src/mcp/upload.test.ts +180 -0
  272. package/src/mcp/upload.ts +112 -0
  273. package/src/models.test.ts +300 -0
  274. package/src/models.ts +708 -0
  275. package/src/modes.test.ts +107 -0
  276. package/src/modes.ts +880 -0
  277. package/src/prep/index.ts +43 -0
  278. package/src/prep/installNodeDependencies.test.ts +298 -0
  279. package/src/prep/installNodeDependencies.ts +196 -0
  280. package/src/prep/installPythonDependencies.test.ts +268 -0
  281. package/src/prep/installPythonDependencies.ts +199 -0
  282. package/src/prep/types.ts +38 -0
  283. package/src/reviewQuality.test.ts +63 -0
  284. package/src/reviewQuality.ts +134 -0
  285. package/src/runCli.test.ts +214 -0
  286. package/src/runCli.ts +282 -0
  287. package/src/skills/terraform-best-practices/SKILL.md +369 -0
  288. package/src/toolState.test.ts +45 -0
  289. package/src/toolState.ts +252 -0
  290. package/src/utils/activity.test.ts +188 -0
  291. package/src/utils/activity.ts +210 -0
  292. package/src/utils/agent.test.ts +251 -0
  293. package/src/utils/agent.ts +139 -0
  294. package/src/utils/agentHangReport.test.ts +203 -0
  295. package/src/utils/agentHangReport.ts +170 -0
  296. package/src/utils/apiFetch.test.ts +115 -0
  297. package/src/utils/apiFetch.ts +62 -0
  298. package/src/utils/apiKeys.test.ts +344 -0
  299. package/src/utils/apiKeys.ts +206 -0
  300. package/src/utils/apiUrl.test.ts +30 -0
  301. package/src/utils/apiUrl.ts +59 -0
  302. package/src/utils/assets.test.ts +153 -0
  303. package/src/utils/assets.ts +107 -0
  304. package/src/utils/billingErrors.test.ts +121 -0
  305. package/src/utils/billingErrors.ts +189 -0
  306. package/src/utils/body.test.ts +217 -0
  307. package/src/utils/body.ts +168 -0
  308. package/src/utils/buildTerramendFooter.test.ts +38 -0
  309. package/src/utils/buildTerramendFooter.ts +82 -0
  310. package/src/utils/byokFallback.test.ts +205 -0
  311. package/src/utils/byokFallback.ts +128 -0
  312. package/src/utils/claudeSubscription.test.ts +179 -0
  313. package/src/utils/claudeSubscription.ts +93 -0
  314. package/src/utils/cli.ts +31 -0
  315. package/src/utils/codexHome.test.ts +190 -0
  316. package/src/utils/codexHome.ts +191 -0
  317. package/src/utils/codexOAuth.ts +147 -0
  318. package/src/utils/codexRefreshDetect.test.ts +85 -0
  319. package/src/utils/codexRefreshDetect.ts +35 -0
  320. package/src/utils/diffCoverage.test.ts +468 -0
  321. package/src/utils/diffCoverage.ts +404 -0
  322. package/src/utils/errorReport.test.ts +135 -0
  323. package/src/utils/errorReport.ts +83 -0
  324. package/src/utils/exitHandler.ts +35 -0
  325. package/src/utils/fixDoubleEscapedString.ts +9 -0
  326. package/src/utils/ghaCore.ts +13 -0
  327. package/src/utils/gitAuth.test.ts +322 -0
  328. package/src/utils/gitAuth.ts +263 -0
  329. package/src/utils/gitAuthServer.test.ts +260 -0
  330. package/src/utils/gitAuthServer.ts +182 -0
  331. package/src/utils/github.test.ts +615 -0
  332. package/src/utils/github.ts +538 -0
  333. package/src/utils/globals.ts +9 -0
  334. package/src/utils/humanEditCapture.test.ts +100 -0
  335. package/src/utils/humanEditCapture.ts +193 -0
  336. package/src/utils/install.test.ts +768 -0
  337. package/src/utils/install.ts +492 -0
  338. package/src/utils/instructions.test.ts +240 -0
  339. package/src/utils/instructions.ts +543 -0
  340. package/src/utils/leapingComment.test.ts +51 -0
  341. package/src/utils/leapingComment.ts +18 -0
  342. package/src/utils/learnings.test.ts +87 -0
  343. package/src/utils/learnings.ts +138 -0
  344. package/src/utils/learningsTocRender.test.ts +116 -0
  345. package/src/utils/learningsTruncate.test.ts +39 -0
  346. package/src/utils/learningsTruncate.ts +42 -0
  347. package/src/utils/lifecycle.test.ts +195 -0
  348. package/src/utils/lifecycle.ts +198 -0
  349. package/src/utils/log.test.ts +402 -0
  350. package/src/utils/log.ts +432 -0
  351. package/src/utils/normalizeEnv.test.ts +91 -0
  352. package/src/utils/normalizeEnv.ts +106 -0
  353. package/src/utils/openCodeModels.ts +82 -0
  354. package/src/utils/overrides.test.ts +89 -0
  355. package/src/utils/overrides.ts +98 -0
  356. package/src/utils/packageManager.test.ts +321 -0
  357. package/src/utils/packageManager.ts +257 -0
  358. package/src/utils/patchWorkflowRunFields.test.ts +92 -0
  359. package/src/utils/patchWorkflowRunFields.ts +150 -0
  360. package/src/utils/payload.test.ts +497 -0
  361. package/src/utils/payload.ts +371 -0
  362. package/src/utils/postApiFetch.ts +51 -0
  363. package/src/utils/prSummary.test.ts +224 -0
  364. package/src/utils/prSummary.ts +147 -0
  365. package/src/utils/progressComment.ts +261 -0
  366. package/src/utils/providerErrors.test.ts +315 -0
  367. package/src/utils/providerErrors.ts +172 -0
  368. package/src/utils/rangeDiff.test.ts +236 -0
  369. package/src/utils/rangeDiff.ts +182 -0
  370. package/src/utils/remediationCommand.test.ts +163 -0
  371. package/src/utils/remediationCommand.ts +119 -0
  372. package/src/utils/retry.test.ts +153 -0
  373. package/src/utils/retry.ts +58 -0
  374. package/src/utils/reviewCleanup.ts +106 -0
  375. package/src/utils/run.ts +99 -0
  376. package/src/utils/runContext.ts +145 -0
  377. package/src/utils/runContextData.ts +58 -0
  378. package/src/utils/runErrorRenderer.test.ts +95 -0
  379. package/src/utils/runErrorRenderer.ts +259 -0
  380. package/src/utils/runFixture.ts +76 -0
  381. package/src/utils/runLifecycle.ts +237 -0
  382. package/src/utils/runStartupLog.ts +60 -0
  383. package/src/utils/secrets.test.ts +103 -0
  384. package/src/utils/secrets.ts +177 -0
  385. package/src/utils/setup.test.ts +509 -0
  386. package/src/utils/setup.ts +352 -0
  387. package/src/utils/shell.ts +103 -0
  388. package/src/utils/skills.test.ts +46 -0
  389. package/src/utils/skills.ts +67 -0
  390. package/src/utils/subprocess.test.ts +170 -0
  391. package/src/utils/subprocess.ts +438 -0
  392. package/src/utils/terraformMcp.test.ts +63 -0
  393. package/src/utils/terraformMcp.ts +83 -0
  394. package/src/utils/time.test.ts +105 -0
  395. package/src/utils/time.ts +59 -0
  396. package/src/utils/timer.test.ts +91 -0
  397. package/src/utils/timer.ts +72 -0
  398. package/src/utils/todoTracking.test.ts +223 -0
  399. package/src/utils/todoTracking.ts +167 -0
  400. package/src/utils/token.test.ts +239 -0
  401. package/src/utils/token.ts +186 -0
  402. package/src/utils/version.ts +10 -0
  403. package/src/utils/versioning.test.ts +34 -0
  404. package/src/utils/versioning.ts +44 -0
  405. package/src/utils/vertex.ts +85 -0
  406. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,527 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { CostDelta } from "#app/mcp/terraform/cost";
3
+ import {
4
+ type Autonomy,
5
+ type BlastTier,
6
+ type Concern,
7
+ type ConcernGroup,
8
+ SEVERITY_RANK,
9
+ type Severity,
10
+ } from "#app/mcp/terraform/types";
11
+
12
+ // --- grouping --------------------------------------------------------------
13
+
14
+ function groupId(file: string): string {
15
+ return createHash("sha1").update(`group|${file}`).digest("hex").slice(0, 12);
16
+ }
17
+
18
+ function ruleGroupId(ruleId: string): string {
19
+ return createHash("sha1").update(`rulegroup|${ruleId}`).digest("hex").slice(0, 12);
20
+ }
21
+
22
+ function maxSeverity(cs: Concern[]): Severity {
23
+ return cs.reduce<Severity>(
24
+ (max, c) => (SEVERITY_RANK[c.severity] > SEVERITY_RANK[max] ? c.severity : max),
25
+ "info",
26
+ );
27
+ }
28
+
29
+ function sortGroups(groups: ConcernGroup[]): ConcernGroup[] {
30
+ return groups.sort((a, b) => {
31
+ const sev = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
32
+ if (sev !== 0) return sev;
33
+ return a.id.localeCompare(b.id);
34
+ });
35
+ }
36
+
37
+ /** group concerns by file into scoped units, sorted by max severity. */
38
+ export function groupConcerns(concerns: Concern[]): ConcernGroup[] {
39
+ const byFile = new Map<string, Concern[]>();
40
+ for (const c of concerns) {
41
+ const arr = byFile.get(c.location.file) ?? [];
42
+ arr.push(c);
43
+ byFile.set(c.location.file, arr);
44
+ }
45
+ const groups: ConcernGroup[] = [];
46
+ for (const [file, cs] of byFile) {
47
+ groups.push({
48
+ id: groupId(file),
49
+ file,
50
+ files: [file],
51
+ grouping: "file",
52
+ severity: maxSeverity(cs),
53
+ concern_count: cs.length,
54
+ rule_ids: [...new Set(cs.map((c) => c.rule_id))].sort(),
55
+ concern_ids: cs.map((c) => c.id),
56
+ });
57
+ }
58
+ return sortGroups(groups);
59
+ }
60
+
61
+ /**
62
+ * §3.11 — group concerns by RULE across files instead of by file. When a single
63
+ * rule fires in many files ("add `tags` to every resource", "enable encryption
64
+ * on every bucket"), fixing it as ONE coherent change is far better than N
65
+ * near-identical per-file PRs. Each group covers one `rule_id` and lists every
66
+ * `file` it spans; the branch key (`remediate/<id>`) is rule-derived and stable.
67
+ * Opt-in (scan `group_by: "rule"`) — by-file stays the default because it keeps
68
+ * each PR's blast radius smaller; by-rule suits sweeping, low-risk rules.
69
+ */
70
+ export function groupConcernsByRule(concerns: Concern[]): ConcernGroup[] {
71
+ const byRule = new Map<string, Concern[]>();
72
+ for (const c of concerns) {
73
+ const arr = byRule.get(c.rule_id) ?? [];
74
+ arr.push(c);
75
+ byRule.set(c.rule_id, arr);
76
+ }
77
+ const groups: ConcernGroup[] = [];
78
+ for (const [ruleId, cs] of byRule) {
79
+ const files = [...new Set(cs.map((c) => c.location.file))].sort();
80
+ groups.push({
81
+ id: ruleGroupId(ruleId),
82
+ file: files.length === 1 ? files[0]! : `${files.length} files`,
83
+ files,
84
+ grouping: "rule",
85
+ severity: maxSeverity(cs),
86
+ concern_count: cs.length,
87
+ rule_ids: [ruleId],
88
+ concern_ids: cs.map((c) => c.id),
89
+ });
90
+ }
91
+ return sortGroups(groups);
92
+ }
93
+
94
+ /**
95
+ * §3.9 — annotate each group with an autonomy decision. Works for BOTH grouping
96
+ * modes: it resolves a group's concerns by `concern_ids` membership (not by
97
+ * `file`, which is just a label for by-rule groups), so the severity/category
98
+ * policy applies identically. Blast radius isn't known until terraform_plan
99
+ * runs, so it can only escalate a group later (the plan tool + prompt apply the
100
+ * `high`-blast override); at scan time autonomy is severity/category-driven.
101
+ */
102
+ export function annotateGroups(
103
+ groups: ConcernGroup[],
104
+ all: Concern[],
105
+ threshold: Severity,
106
+ ): ConcernGroup[] {
107
+ const byId = new Map(all.map((c) => [c.id, c]));
108
+ return groups.map((g) => {
109
+ const groupConcerns = g.concern_ids.map((id) => byId.get(id)).filter((c): c is Concern => !!c);
110
+ const decision = classifyAutonomy(groupConcerns, threshold);
111
+ return { ...g, autonomy: decision.autonomy, autonomy_reasons: decision.reasons };
112
+ });
113
+ }
114
+
115
+ // --- §3.10 atomic vs batched PRs -------------------------------------------
116
+
117
+ export interface BatchPlan {
118
+ /** group ids safe to combine into ONE low-risk PR (`remediate/batch-<hash>`). */
119
+ batchable: string[];
120
+ /** group ids that must each get their own PR (security / higher severity /
121
+ * needs-human / large blast). */
122
+ isolated: string[];
123
+ /** deterministic branch name for the batch (stable for the same member set). */
124
+ batch_branch: string | null;
125
+ }
126
+
127
+ /** a group is safe to batch when it's low-risk: severity `low`/`info` AND its
128
+ * autonomy decision is `auto` (no escalating security finding, no high blast). */
129
+ function isBatchable(g: ConcernGroup): boolean {
130
+ const lowRisk = g.severity === "low" || g.severity === "info";
131
+ return lowRisk && g.autonomy !== "needs-human";
132
+ }
133
+
134
+ /**
135
+ * §3.10 — split annotated groups into a single low-risk BATCH (merged into one
136
+ * easy-to-review PR) and the riskier groups that each stay ISOLATED in their own
137
+ * PR (so they can be reviewed/reverted independently). The batch branch name
138
+ * hashes the sorted member ids, so re-runs over the same set reuse the branch
139
+ * (idempotent). Returns `batch_branch: null` when fewer than two groups are
140
+ * batchable (one group is just a normal single-group PR, not a batch).
141
+ */
142
+ export function planBatches(groups: ConcernGroup[]): BatchPlan {
143
+ const batchable = groups
144
+ .filter(isBatchable)
145
+ .map((g) => g.id)
146
+ .sort();
147
+ const isolated = groups
148
+ .filter((g) => !isBatchable(g))
149
+ .map((g) => g.id)
150
+ .sort();
151
+ const batch_branch =
152
+ batchable.length >= 2
153
+ ? `remediate/batch-${createHash("sha1").update(batchable.join("|")).digest("hex").slice(0, 12)}`
154
+ : null;
155
+ return { batchable, isolated, batch_branch };
156
+ }
157
+
158
+ // --- §5.17 per-finding explanation (rule documentation links) --------------
159
+
160
+ /**
161
+ * Resolve the canonical documentation URL for a concern's rule, for the PR's
162
+ * per-finding explanation. Prefers the scanner's own `remediation_hint` when it
163
+ * is already a URL (checkov guideline, tflint rule link, trivy reference).
164
+ * Otherwise derives the well-known page deterministically: a trivy `AVD-*` rule
165
+ * maps to its Aqua Vulnerability Database page. Returns null when no canonical
166
+ * URL is known (the agent then explains from `evidence` alone).
167
+ */
168
+ export function ruleDocUrl(concern: Pick<Concern, "rule_id" | "remediation_hint">): string | null {
169
+ const hint = concern.remediation_hint?.trim();
170
+ if (hint && /^https?:\/\//i.test(hint)) return hint;
171
+ // trivy:AVD-AWS-0088 → https://avd.aquasec.com/misconfig/avd-aws-0088
172
+ const trivyMatch = concern.rule_id.match(/^trivy:(AVD-[A-Z0-9-]+)$/i);
173
+ if (trivyMatch) return `https://avd.aquasec.com/misconfig/${trivyMatch[1]!.toLowerCase()}`;
174
+ return null;
175
+ }
176
+
177
+ /** distinct rule→doc-url map for a group, for the PR body's per-finding links. */
178
+ export function docUrlsForGroup(g: ConcernGroup, all: Concern[]): Record<string, string> {
179
+ const byId = new Map(all.map((c) => [c.id, c]));
180
+ const out: Record<string, string> = {};
181
+ for (const id of g.concern_ids) {
182
+ const c = byId.get(id);
183
+ if (!c) continue;
184
+ const url = ruleDocUrl(c);
185
+ if (url) out[c.rule_id] = url;
186
+ }
187
+ return out;
188
+ }
189
+
190
+ // --- severity-driven autonomy (§3.9) ---------------------------------------
191
+
192
+ export interface AutonomyDecision {
193
+ autonomy: Autonomy;
194
+ /** human-readable reasons a group was escalated (empty for `auto`). */
195
+ reasons: string[];
196
+ }
197
+
198
+ /**
199
+ * Decide whether a group of concerns can be auto-fixed and opened as a normal
200
+ * PR (`auto`), or must be flagged for human review (`needs-human`). Trivial
201
+ * findings (style/correctness, deprecated args, missing tags, formatting) open
202
+ * as normal; high-severity SECURITY findings escalate by default, as does a
203
+ * `high` blast radius regardless of finding severity (§2.6 overrides upward).
204
+ *
205
+ * `threshold` is the minimum severity at which a *security* concern escalates
206
+ * (default `high`, so critical/high security → human; medium/low → auto). The
207
+ * decision is deterministic and computed from the `Concern` model's existing
208
+ * `severity` + `category` — no model self-assessment.
209
+ */
210
+ export function classifyAutonomy(
211
+ concerns: Pick<Concern, "severity" | "category">[],
212
+ threshold: Severity = "high",
213
+ blastTier?: BlastTier,
214
+ ): AutonomyDecision {
215
+ const reasons: string[] = [];
216
+ const minRank = SEVERITY_RANK[threshold];
217
+ const escalating = concerns.filter(
218
+ (c) => c.category === "security" && SEVERITY_RANK[c.severity] >= minRank,
219
+ );
220
+ if (escalating.length > 0) {
221
+ const top = escalating.reduce((max, c) =>
222
+ SEVERITY_RANK[c.severity] > SEVERITY_RANK[max.severity] ? c : max,
223
+ );
224
+ reasons.push(
225
+ `${escalating.length} security concern(s) at/above the ${threshold} autonomy threshold (highest: ${top.severity})`,
226
+ );
227
+ }
228
+ if (blastTier === "high") {
229
+ reasons.push(
230
+ "high blast radius — the fix touches more than 10 resources or spans more than one module",
231
+ );
232
+ }
233
+ return { autonomy: reasons.length > 0 ? "needs-human" : "auto", reasons };
234
+ }
235
+
236
+ // --- inline suggested changes (§5.18) --------------------------------------
237
+
238
+ export interface SuggestionDecision {
239
+ /** true ⇒ post a GitHub one-click `suggestion` instead of opening a full PR. */
240
+ suggest: boolean;
241
+ reason: string;
242
+ }
243
+
244
+ /**
245
+ * §5.18 — decide whether a fix is small/low-risk enough to post as a GitHub
246
+ * one-click **suggested change** (a ` ```suggestion ` block on the existing PR)
247
+ * rather than opening a whole `remediate/*` branch + PR. Much lower friction for
248
+ * trivial fixes. Only when ALL hold: there IS an existing PR context (a comment
249
+ * trigger on a PR); the group is `low`/`info` severity; the fix is a single hunk
250
+ * in a single file; and the blast radius (when known) is `low`. Anything bigger
251
+ * keeps full-PR mode.
252
+ */
253
+ export function shouldSuggestInline(opts: {
254
+ hasPrContext: boolean;
255
+ severity: Severity;
256
+ fileCount: number;
257
+ hunkCount: number;
258
+ blastTier?: BlastTier | undefined;
259
+ }): SuggestionDecision {
260
+ if (!opts.hasPrContext)
261
+ return { suggest: false, reason: "no existing PR to attach a suggestion to" };
262
+ if (opts.severity !== "low" && opts.severity !== "info") {
263
+ return {
264
+ suggest: false,
265
+ reason: `severity ${opts.severity} warrants a reviewable PR, not a one-click suggestion`,
266
+ };
267
+ }
268
+ if (opts.fileCount > 1 || opts.hunkCount > 1) {
269
+ return { suggest: false, reason: "multi-hunk / multi-file fix — open a full PR" };
270
+ }
271
+ if (opts.blastTier === "high" || opts.blastTier === "medium") {
272
+ return { suggest: false, reason: `blast radius ${opts.blastTier} — open a full PR` };
273
+ }
274
+ return {
275
+ suggest: true,
276
+ reason: "single-hunk low-risk fix on an existing PR — post as a one-click suggestion",
277
+ };
278
+ }
279
+
280
+ // --- confidence labeling (§5.19) -------------------------------------------
281
+
282
+ export type Confidence = "high" | "medium" | "low";
283
+
284
+ export interface ConfidenceSignals {
285
+ /** §1.1 — every targeted concern id was cleared by the re-scan. */
286
+ verified: boolean;
287
+ /** §1.4 — count of NEW concern ids the fix introduced (0 is good). */
288
+ regressionCount: number;
289
+ /** §1.3 — second plan matched the first. undefined when plan didn't run. */
290
+ idempotent?: boolean | undefined;
291
+ /** §2.6 — blast tier. undefined when plan didn't run. */
292
+ blastTier?: BlastTier | undefined;
293
+ /** §4.16 — cost direction. undefined when infracost didn't run. */
294
+ costDirection?: CostDelta["direction"] | undefined;
295
+ }
296
+
297
+ export interface ConfidenceResult {
298
+ level: Confidence;
299
+ reasons: string[];
300
+ }
301
+
302
+ /**
303
+ * Derive a fix's confidence DETERMINISTICALLY from the verification evidence
304
+ * already gathered — never a model self-assessment, which keeps it honest.
305
+ *
306
+ * - A fix that didn't verify (§1.1) or introduced a regression (§1.4) is `low`:
307
+ * the proof failed, full stop.
308
+ * - Otherwise it starts `high` and is capped to `medium` by any weaker signal:
309
+ * a non-deterministic plan (§1.3 `idempotent: false`), a `high` blast radius
310
+ * (§2.6), a cost increase (§4.16), or a signal that was *skipped* (plan /
311
+ * infracost didn't run, so we have less proof — `high` requires the full
312
+ * stack). A skipped signal lowers confidence but does not, by itself, make a
313
+ * verified, regression-free fix `low`.
314
+ */
315
+ export function computeConfidence(signals: ConfidenceSignals): ConfidenceResult {
316
+ const reasons: string[] = [];
317
+ if (!signals.verified) {
318
+ return {
319
+ level: "low",
320
+ reasons: ["the re-scan did not confirm every targeted concern was resolved (§1.1)"],
321
+ };
322
+ }
323
+ if (signals.regressionCount > 0) {
324
+ return {
325
+ level: "low",
326
+ reasons: [`the fix introduced ${signals.regressionCount} new concern(s) (§1.4 regression)`],
327
+ };
328
+ }
329
+ reasons.push(
330
+ "re-scan verified every targeted concern resolved (§1.1) with no regressions (§1.4)",
331
+ );
332
+
333
+ let level: Confidence = "high";
334
+ const capMedium = (reason: string) => {
335
+ if (level === "high") level = "medium";
336
+ reasons.push(reason);
337
+ };
338
+ if (signals.idempotent === false)
339
+ capMedium("plan is non-deterministic (§1.3) — a perpetual-diff smell");
340
+ if (signals.blastTier === "high") capMedium("high blast radius (§2.6) — review carefully");
341
+ if (signals.costDirection === "increase") capMedium("the fix increases monthly cost (§4.16)");
342
+ if (signals.idempotent === undefined || signals.blastTier === undefined) {
343
+ capMedium(
344
+ "no terraform plan evidence (no cloud credentials) — idempotency and blast radius unproven",
345
+ );
346
+ }
347
+ if (signals.costDirection === undefined) {
348
+ capMedium("no cost evidence (infracost did not run)");
349
+ }
350
+ return { level, reasons };
351
+ }
352
+
353
+ // --- honest refusal (§29) --------------------------------------------------
354
+
355
+ export interface RefusalDecision {
356
+ /** true ⇒ this concern needs a human decision; prefer a structured non-fix
357
+ * (an issue) over guessing a fix that could break the stack. */
358
+ refuse: boolean;
359
+ reason?: string;
360
+ }
361
+
362
+ // concern signatures whose correct fix needs information only a human has —
363
+ // auto-"fixing" them risks a narrow policy that breaks the stack or a wrong
364
+ // value. Matched against the rule id + evidence text (lower-cased).
365
+ const HUMAN_DECISION_SIGNATURES: { test: RegExp; reason: string }[] = [
366
+ {
367
+ test: /least[\s_-]?privilege|wildcard|iam.*("\*"|\*\s*action|action.*\*)|policy.*allows? all/i,
368
+ reason:
369
+ "narrowing an IAM policy needs the exact action/resource set the workload uses — a human decision",
370
+ },
371
+ {
372
+ test: /\bkms\b.*\bpolicy\b|key[\s_-]?policy|cmk.*polic/i,
373
+ reason: "a KMS/CMK key policy needs the real principals and grants — a human decision",
374
+ },
375
+ {
376
+ test: /allowed?[\s_-]?cidr|restrict.*cidr|specify.*cidr|known.*ip|real.*source/i,
377
+ reason: "tightening an ingress CIDR needs the real allowed source — a human decision",
378
+ },
379
+ ];
380
+
381
+ /**
382
+ * §29 — advisory check: would auto-fixing this concern require a judgement only
383
+ * a human can make? If so, the Remediate flow should post a STRUCTURED refusal
384
+ * (an issue describing the concern, why it won't auto-fix, and what a human
385
+ * should do) rather than guess a fix that could break the stack. Deterministic
386
+ * and conservative — it only flags the well-known human-decision classes.
387
+ */
388
+ export function classifyRefusal(concern: Pick<Concern, "rule_id" | "evidence">): RefusalDecision {
389
+ const text = `${concern.rule_id} ${concern.evidence}`.toLowerCase();
390
+ for (const sig of HUMAN_DECISION_SIGNATURES) {
391
+ if (sig.test.test(text)) return { refuse: true, reason: sig.reason };
392
+ }
393
+ return { refuse: false };
394
+ }
395
+
396
+ /**
397
+ * §29 — format a structured non-fix for a concern Terramend won't auto-fix. The
398
+ * output is a Markdown issue body: what's wrong, why it isn't auto-fixed, and
399
+ * the concrete next step for a human. Pure (string in → string out).
400
+ */
401
+ export function buildRefusalReport(input: {
402
+ concern: Pick<Concern, "rule_id" | "evidence" | "location">;
403
+ whyNoAutoFix: string;
404
+ humanAction: string;
405
+ }): string {
406
+ const { concern, whyNoAutoFix, humanAction } = input;
407
+ const loc = `${concern.location.file}${concern.location.line ? `:${concern.location.line}` : ""}`;
408
+ return [
409
+ `### Terramend won't auto-fix \`${concern.rule_id}\` (needs a human decision)`,
410
+ "",
411
+ `**Where:** \`${loc}\``,
412
+ "",
413
+ `**What's wrong:** ${concern.evidence}`,
414
+ "",
415
+ `**Why it isn't auto-fixed:** ${whyNoAutoFix}`,
416
+ "",
417
+ `**What a human should do:** ${humanAction}`,
418
+ "",
419
+ "_Terramend opens a PR only when it can prove the fix is correct; for this concern it can't, so it's surfaced here instead of guessing._",
420
+ ].join("\n");
421
+ }
422
+
423
+ // --- fix once, prevent forever (§21) ---------------------------------------
424
+
425
+ export interface PreventiveControl {
426
+ /** the mechanism that stops this class of concern recurring. */
427
+ mechanism: string;
428
+ /** a copy-pasteable config/CI snippet. */
429
+ snippet: string;
430
+ note: string;
431
+ }
432
+
433
+ /**
434
+ * §21 — alongside the patch, suggest the guardrail that stops the concern
435
+ * RECURRING: a CI gate keyed on the producing scanner. Deterministic by source
436
+ * (the scanner is the right enforcement point), parameterised by the rule id.
437
+ * Returns null for sources with no natural preventive gate.
438
+ */
439
+ export function preventiveControlFor(
440
+ concern: Pick<Concern, "source" | "rule_id">,
441
+ ): PreventiveControl | null {
442
+ // strip only the leading `<source>:` namespace (not every colon) so a rule
443
+ // name that itself contains a colon survives intact.
444
+ const prefix = `${concern.source}:`;
445
+ const bareRule = concern.rule_id.startsWith(prefix)
446
+ ? concern.rule_id.slice(prefix.length)
447
+ : concern.rule_id;
448
+ switch (concern.source) {
449
+ case "checkov":
450
+ return {
451
+ mechanism: "Checkov hard-fail in CI",
452
+ snippet: `# .checkov.yaml\nhard-fail-on:\n - ${bareRule}`,
453
+ note: `Add ${bareRule} to a Checkov hard-fail list so a PR that reintroduces it fails CI.`,
454
+ };
455
+ case "trivy":
456
+ return {
457
+ mechanism: "Trivy config scan gate in CI",
458
+ snippet: `# CI step\ntrivy config --exit-code 1 --severity HIGH,CRITICAL .`,
459
+ note: `Gate PRs on \`trivy config\` so ${bareRule} (and peers) can't be reintroduced.`,
460
+ };
461
+ case "tflint":
462
+ return {
463
+ mechanism: "tflint rule enforced in CI",
464
+ snippet: `# .tflint.hcl\nrule "${bareRule}" {\n enabled = true\n}`,
465
+ note: `Enable ${bareRule} in \`.tflint.hcl\` and run \`tflint\` in CI.`,
466
+ };
467
+ case "terraform-fmt":
468
+ return {
469
+ mechanism: "terraform fmt check in CI",
470
+ snippet: `# CI step\nterraform fmt -check -recursive`,
471
+ note: "Gate PRs on `terraform fmt -check` so formatting can't drift.",
472
+ };
473
+ case "terraform-validate":
474
+ return {
475
+ mechanism: "terraform validate in CI",
476
+ snippet: `# CI step\nterraform validate`,
477
+ note: "Run `terraform validate` in CI so this correctness error can't return.",
478
+ };
479
+ default:
480
+ return null;
481
+ }
482
+ }
483
+
484
+ // --- cross-tool co-location (§30) ------------------------------------------
485
+
486
+ export interface LocationCluster {
487
+ file: string;
488
+ line: number | null;
489
+ /** the concern ids at this exact location (likely the same underlying defect). */
490
+ concern_ids: string[];
491
+ /** the distinct scanners that flagged this location. */
492
+ sources: string[];
493
+ }
494
+
495
+ /**
496
+ * §30 — surface concerns that DIFFERENT scanners flagged at the same `file:line`
497
+ * — almost always the same underlying defect (trivy ∩ checkov overlap heavily on
498
+ * e.g. S3 encryption). Reported so the agent writes ONE canonical fix + ONE
499
+ * explanation for the cluster rather than treating each as separate work. This
500
+ * is purely advisory: it NEVER removes a concern from the verification set (a
501
+ * missing id must still provably clear), so it can't drop a real finding. Only
502
+ * clusters spanning more than one scanner are returned.
503
+ */
504
+ export function clusterByLocation(concerns: Concern[]): LocationCluster[] {
505
+ const byLoc = new Map<string, Concern[]>();
506
+ for (const c of concerns) {
507
+ if (c.location.line == null) continue; // a null line isn't a precise co-location
508
+ const key = `${c.location.file}|${c.location.line}`;
509
+ const arr = byLoc.get(key) ?? [];
510
+ arr.push(c);
511
+ byLoc.set(key, arr);
512
+ }
513
+ const clusters: LocationCluster[] = [];
514
+ for (const cs of byLoc.values()) {
515
+ const first = cs[0];
516
+ if (!first) continue; // empty group can't happen (map is populated), but keep the guard explicit
517
+ const sources = [...new Set(cs.map((c) => c.source))].sort();
518
+ if (sources.length < 2) continue; // single-scanner location isn't cross-tool overlap
519
+ clusters.push({
520
+ file: first.location.file,
521
+ line: first.location.line,
522
+ concern_ids: cs.map((c) => c.id).sort(),
523
+ sources,
524
+ });
525
+ }
526
+ return clusters.sort((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0));
527
+ }