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,95 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderRunError } from "#app/utils/runErrorRenderer";
3
+
4
+ const repo = { owner: "acme", name: "widget" };
5
+
6
+ describe("renderRunError BYOK provider billing exhausted (#835)", () => {
7
+ const deepseekRaw =
8
+ '» provider error detected (provider billing exhausted): ERROR providerID=deepseek modelID=deepseek-v4-pro error={"name":"AI_APICallError","message":"Insufficient Balance"}';
9
+
10
+ const anthropicRaw =
11
+ "APIError: Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.";
12
+
13
+ const opencodeZenRaw = "CreditsError: account out of free usage";
14
+
15
+ it("renders DeepSeek billing-exhausted with provider-specific dashboard link", () => {
16
+ const result = renderRunError({
17
+ errorMessage: deepseekRaw,
18
+ repo,
19
+ agentDiagnostic: undefined,
20
+ });
21
+ expect(result.summary).toContain("`deepseek` account is out of credit");
22
+ expect(result.summary).toContain("https://platform.deepseek.com/top_up");
23
+ expect(result.summary).toContain("### ❌ Terramend failed");
24
+ expect(result.comment).toContain("`deepseek` account is out of credit");
25
+ expect(result.comment).not.toContain("### ❌ Terramend failed");
26
+ });
27
+
28
+ it("matches Anthropic 'credit balance is too low' (#835 Anthropic case)", () => {
29
+ const result = renderRunError({
30
+ errorMessage: anthropicRaw,
31
+ repo,
32
+ agentDiagnostic: undefined,
33
+ });
34
+ expect(result.comment).toContain("out of credit");
35
+ });
36
+
37
+ it("matches OpenCode Zen CreditsError shape", () => {
38
+ const result = renderRunError({
39
+ errorMessage: opencodeZenRaw,
40
+ repo,
41
+ agentDiagnostic: undefined,
42
+ });
43
+ expect(result.comment).toContain("out of credit");
44
+ });
45
+
46
+ it("falls through to a generic CTA when providerID cannot be parsed", () => {
47
+ const result = renderRunError({
48
+ errorMessage: "Insufficient balance — provider response with no providerID tag",
49
+ repo,
50
+ agentDiagnostic: undefined,
51
+ });
52
+ expect(result.comment).toContain("Your provider account is out of credit");
53
+ expect(result.comment).not.toContain("Your your");
54
+ expect(result.comment).toContain("Top up your provider account");
55
+ });
56
+ });
57
+
58
+ describe("renderRunError ProviderModelNotFoundError (#816)", () => {
59
+ const staleFreeRaw =
60
+ 'ProviderModelNotFoundError: {"providerID":"opencode","modelID":"retired-free-model","suggestions":["deepseek-v4-flash-free"]}';
61
+
62
+ const bigPickleRaw =
63
+ 'ProviderModelNotFoundError: {"providerID":"opencode","modelID":"big-pickle","suggestions":[]}';
64
+
65
+ it("renders actionable copy for a stale free fallback model id", () => {
66
+ const result = renderRunError({
67
+ errorMessage: staleFreeRaw,
68
+ repo,
69
+ agentDiagnostic: undefined,
70
+ });
71
+ expect(result.summary).toContain("Terramend's free fallback model is no longer available");
72
+ expect(result.summary).toContain("`acme/widget`");
73
+ expect(result.summary).toContain("retired-free-model");
74
+ expect(result.comment).toBe(result.summary);
75
+ });
76
+
77
+ it("renders the same classifier when big-pickle is missing from opencode catalog", () => {
78
+ const result = renderRunError({
79
+ errorMessage: bigPickleRaw,
80
+ repo,
81
+ agentDiagnostic: undefined,
82
+ });
83
+ expect(result.summary).toContain("Terramend's free fallback model is no longer available");
84
+ expect(result.summary).toContain("big-pickle");
85
+ });
86
+
87
+ it("does not misclassify unrelated failures as fallback-catalog errors", () => {
88
+ const result = renderRunError({
89
+ errorMessage: "activity timeout after 900s",
90
+ repo,
91
+ agentDiagnostic: undefined,
92
+ });
93
+ expect(result.summary).not.toContain("free fallback model is no longer available");
94
+ });
95
+ });
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Classify + render the error thrown out of the main run try-block into a
3
+ * pair of user-facing markdown bodies — one for the GitHub Actions job
4
+ * summary tab, one for the PR progress comment.
5
+ *
6
+ * Classifications, in dispatch order (first match wins; the api-key
7
+ * branch additionally folds in the activity-timeout hang body as a
8
+ * sub-source so a hang masking an api-key error still surfaces the api-key
9
+ * CTA):
10
+ *
11
+ * 1. `BillingError` — either the proxy-token mint already threw one (402
12
+ * handled inline) or the agent runtime surfaced an OpenRouter
13
+ * "key budget exhausted" string mid-run. Both render via
14
+ * `formatBillingErrorSummary` so the user sees actionable copy.
15
+ *
16
+ * 2. BYOK provider billing-exhausted (#835) — DeepSeek "Insufficient
17
+ * Balance", Anthropic "credit balance is too low", OpenCode Zen
18
+ * `CreditsError`, Gemini "spending cap". Checked before api-key auth
19
+ * because billing-exhausted responses often carry 401 status codes
20
+ * that `isApiKeyAuthError` would otherwise mis-classify.
21
+ *
22
+ * 3. API-key auth error — `isApiKeyAuthError` sniffs the raw error string
23
+ * (or the activity-timeout hang body when present, since that's where
24
+ * the underlying provider error often lands); `formatApiKeyErrorSummary`
25
+ * renders provider + console-link copy.
26
+ *
27
+ * 4. ProviderModelNotFoundError — stale free-fallback model id no longer
28
+ * in the OpenCode catalog; renders a nudge to add a BYOK key.
29
+ *
30
+ * 5. Activity-timeout hang — `errorMessage` starts with
31
+ * `"activity timeout"` or `"agent still pending"` AND none of the
32
+ * above matched. The harness keeps structured diagnostic state on
33
+ * `toolState.agentDiagnostic`; `formatAgentHangBody` renders that into
34
+ * the job summary. The PR comment instead collapses to a one-line
35
+ * `**Run failed.** [View the logs →]` — the watchdog jargon, event
36
+ * counts, and benign stderr tail are operator-grade detail that only
37
+ * alarm the average user. The one exception is a hang masking billing
38
+ * exhaustion (#778), where `formatAgentHangBody` emits an actionable
39
+ * top-up CTA that the comment keeps verbatim.
40
+ *
41
+ * 6. Default — the job summary gets a plain-English lead sentence plus the
42
+ * raw error in a fenced code block under the `### ❌ Terramend failed`
43
+ * banner; the PR comment collapses to the same one-line logs link as
44
+ * the hang case, since the raw internal string helps nobody on the PR.
45
+ *
46
+ * Net: the actionable classifications (billing, API-key, model-not-found)
47
+ * render identical bodies on both surfaces; the non-actionable ones (hang,
48
+ * generic) keep the forensics in the Actions job summary and show a calm
49
+ * one-liner in the PR comment, whose footer already carries Terramend
50
+ * branding + rerun links.
51
+ */
52
+
53
+ import type { AgentDiagnostic } from "#app/utils/agentHangReport";
54
+ import { formatAgentHangBody } from "#app/utils/agentHangReport";
55
+ import { formatApiKeyErrorSummary, isApiKeyAuthError } from "#app/utils/apiKeys";
56
+ import { BillingError, formatBillingErrorSummary } from "#app/utils/billingErrors";
57
+ import {
58
+ extractProviderId,
59
+ isProviderBillingExhausted,
60
+ isRouterKeylimitExhaustedError,
61
+ } from "#app/utils/providerErrors";
62
+
63
+ export type RenderedRunError = {
64
+ summary: string;
65
+ comment: string;
66
+ };
67
+
68
+ function isProviderModelNotFoundError(message: string): boolean {
69
+ return message.includes("ProviderModelNotFoundError");
70
+ }
71
+
72
+ /**
73
+ * Generic failure copy for any shape not caught by a more specific classifier
74
+ * (billing / api-key / hang / model-not-found). A plain-English lead sentence
75
+ * so the user isn't staring at a raw internal string like
76
+ * `opencode prompt failed: fetch failed`, followed by the actual error in a
77
+ * fenced code block for anyone who needs the detail. Shared by both surfaces;
78
+ * the job summary adds the `### ❌ Terramend failed` banner on top.
79
+ */
80
+ function formatGenericFailure(errorMessage: string): string {
81
+ return [
82
+ "Terramend ran into an unexpected error and couldn't finish this run. The underlying error is below — re-trigger Terramend to try again, and reach out to support if it keeps happening.",
83
+ "",
84
+ "```",
85
+ errorMessage,
86
+ "```",
87
+ ].join("\n");
88
+ }
89
+
90
+ /**
91
+ * Minimal PR-comment body for non-actionable failures (hangs, unexpected
92
+ * errors). The forensic detail (event counts, stderr tail, raw error) stays
93
+ * in the Actions job summary; the comment the average user sees is one calm
94
+ * line plus a link to the logs. The footer appended by `reportErrorToComment`
95
+ * already carries rerun / model context.
96
+ */
97
+ function formatMinimalFailureComment(repo: { owner: string; name: string }): string {
98
+ const runId = process.env.GITHUB_RUN_ID;
99
+ if (!runId) return "**Run failed.**";
100
+ const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
101
+ const url = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
102
+ return `**Run failed.** [View the logs →](${url})`;
103
+ }
104
+
105
+ /**
106
+ * Best-known billing top-up URL per provider. Conservative list: only
107
+ * providers we've actually classified billing-exhaustion shapes for in
108
+ * `providerErrors.ts`. Unknown providers fall through to a generic CTA.
109
+ */
110
+ const PROVIDER_BILLING_URLS: Record<string, string> = {
111
+ deepseek: "https://platform.deepseek.com/top_up",
112
+ anthropic: "https://console.anthropic.com/settings/billing",
113
+ openai: "https://platform.openai.com/account/billing",
114
+ google: "https://aistudio.google.com/usage",
115
+ opencode: "https://opencode.ai/zen",
116
+ };
117
+
118
+ /**
119
+ * `extractProviderId` only fires when the harness emits `providerID=...`
120
+ * (OpenCode log shape). Direct-provider errors (e.g. Anthropic SDK throwing
121
+ * `"Your credit balance is too low to access the Anthropic API"`) carry no
122
+ * such tag, so map their distinctive copy to a provider id here so the
123
+ * dashboard link is reachable.
124
+ *
125
+ * Pattern is intentionally tight (Anthropic-specific phrasing only) to
126
+ * avoid mis-tagging non-Anthropic billing-exhausted errors that happen to
127
+ * mention `"Anthropic API"` in passing — the broader phrase appears in
128
+ * fallback-chain agent prompt text and OpenCode harness logs.
129
+ */
130
+ function detectProviderId(message: string): string | null {
131
+ const harnessId = extractProviderId(message);
132
+ if (harnessId) return harnessId;
133
+ if (/credit balance is too low/i.test(message)) return "anthropic";
134
+ return null;
135
+ }
136
+
137
+ function formatProviderBillingExhausted(input: { errorMessage: string }): string {
138
+ const providerId = detectProviderId(input.errorMessage);
139
+ const dashboardUrl = providerId ? PROVIDER_BILLING_URLS[providerId] : undefined;
140
+
141
+ const headline = providerId
142
+ ? `**Your \`${providerId}\` account is out of credit.**`
143
+ : "**Your provider account is out of credit.**";
144
+ const cta = dashboardUrl
145
+ ? `[Top up \`${providerId}\` →](${dashboardUrl})`
146
+ : "Top up your provider account, then re-trigger Terramend.";
147
+
148
+ return [
149
+ headline,
150
+ "",
151
+ "Terramend detected a billing-exhausted response from your provider — the agent stopped before completing this run.",
152
+ "",
153
+ cta,
154
+ "",
155
+ `\`\`\`\n${input.errorMessage}\n\`\`\``,
156
+ ].join("\n");
157
+ }
158
+
159
+ function formatProviderModelNotFoundSummary(input: {
160
+ owner: string;
161
+ name: string;
162
+ raw: string;
163
+ }): string {
164
+ return (
165
+ `Terramend's free fallback model is no longer available in OpenCode's catalog. ` +
166
+ `Add an API key for your configured model in the Terramend console for \`${input.owner}/${input.name}\`, ` +
167
+ `or contact support if this persists.\n\n` +
168
+ `\`\`\`\n${input.raw}\n\`\`\``
169
+ );
170
+ }
171
+
172
+ export function renderRunError(input: {
173
+ errorMessage: string;
174
+ repo: { owner: string; name: string };
175
+ agentDiagnostic: AgentDiagnostic | undefined;
176
+ }): RenderedRunError {
177
+ // reclassify mid-run OpenRouter "key budget exhausted" as BillingError so
178
+ // the user gets the same actionable copy as a /api/proxy-token 402.
179
+ const billingError = isRouterKeylimitExhaustedError(input.errorMessage)
180
+ ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" })
181
+ : null;
182
+
183
+ if (billingError) {
184
+ const body = formatBillingErrorSummary(billingError, input.repo.owner);
185
+ return { summary: body, comment: body };
186
+ }
187
+
188
+ // gated on isHang because the harness sets `agentDiagnostic` on entry, so
189
+ // any non-hang throw that hits the outer catch (e.g. post-success
190
+ // output_schema validator, or a late cleanup throw after the run already
191
+ // succeeded) would otherwise render "Terramend failed" with stale event
192
+ // counts and silently drop the real errorMessage.
193
+ const isHang =
194
+ input.errorMessage.startsWith("activity timeout") ||
195
+ input.errorMessage.startsWith("agent still pending");
196
+ const hangBody = isHang
197
+ ? formatAgentHangBody({
198
+ diagnostic: input.agentDiagnostic,
199
+ isHang: true,
200
+ errorMessage: input.errorMessage,
201
+ })
202
+ : null;
203
+
204
+ // BYOK provider billing-exhausted (DeepSeek "Insufficient Balance",
205
+ // Anthropic "credit balance is too low", OpenCode Zen `CreditsError` /
206
+ // `FreeUsageLimitError`, Gemini "spending cap"). distinct from the Router
207
+ // billing branches above — Router uses `BillingError`, this uses the agent
208
+ // log payload classified by `isProviderBillingExhausted`. see #835.
209
+ //
210
+ // checked BEFORE api-key auth: providers commonly return 401 (DeepSeek,
211
+ // Gemini) or include `"API Error: 401"` in the error body for billing
212
+ // exhaustion, which `isApiKeyAuthError` would otherwise match — surfacing
213
+ // a "rotate your key" CTA when the actual fix is "top up credits".
214
+ if (isProviderBillingExhausted(input.errorMessage)) {
215
+ const body = formatProviderBillingExhausted({ errorMessage: input.errorMessage });
216
+ return { summary: `### ❌ Terramend failed\n\n${body}`, comment: body };
217
+ }
218
+
219
+ const apiKeySource = hangBody ?? input.errorMessage;
220
+ const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource)
221
+ ? formatApiKeyErrorSummary({
222
+ owner: input.repo.owner,
223
+ name: input.repo.name,
224
+ raw: apiKeySource,
225
+ })
226
+ : null;
227
+
228
+ if (apiKeyErrorSummary) {
229
+ return { summary: apiKeyErrorSummary, comment: apiKeyErrorSummary };
230
+ }
231
+
232
+ if (isProviderModelNotFoundError(input.errorMessage)) {
233
+ const body = formatProviderModelNotFoundSummary({
234
+ owner: input.repo.owner,
235
+ name: input.repo.name,
236
+ raw: input.errorMessage,
237
+ });
238
+ return { summary: body, comment: body };
239
+ }
240
+
241
+ if (hangBody) {
242
+ // a hang masking billing exhaustion (#778) renders an actionable top-up
243
+ // CTA inside `hangBody` — keep that in the comment. every other hang is
244
+ // non-actionable noise for the average user, so the comment collapses to
245
+ // a one-liner and the diagnostic stays in the Actions job summary.
246
+ const isBillingExhausted =
247
+ input.agentDiagnostic?.lastProviderError === "provider billing exhausted";
248
+ return {
249
+ summary: `### ❌ Terramend failed\n\n${hangBody}`,
250
+ comment: isBillingExhausted ? hangBody : formatMinimalFailureComment(input.repo),
251
+ };
252
+ }
253
+
254
+ const genericBody = formatGenericFailure(input.errorMessage);
255
+ return {
256
+ summary: `### ❌ Terramend failed\n\n${genericBody}`,
257
+ comment: formatMinimalFailureComment(input.repo),
258
+ };
259
+ }
@@ -0,0 +1,76 @@
1
+ // in-process fixture runner used by `dev-run.ts` (and any future host-side
2
+ // runner). does NOT know about Docker — that's `docker.ts`'s job. when run
3
+ // inside the local docker container, this is what executes after the entrypoint.
4
+ import { execSync } from "node:child_process";
5
+ import { mkdtemp } from "node:fs/promises";
6
+ import { devNull, tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import type { AgentResult } from "#app/agents/shared";
9
+ import { type Inputs, main } from "#app/main";
10
+ import { log } from "#app/utils/cli";
11
+ import { ensureGitHubToken } from "#app/utils/github";
12
+ import { setupTestRepo } from "#app/utils/setup";
13
+
14
+ export async function run(inputsOrPrompt: Inputs | string): Promise<AgentResult> {
15
+ await ensureGitHubToken();
16
+
17
+ // dev-run.ts is a CI-emulator — isolate it from the developer's user- and
18
+ // system-scope gitconfig so checks like `validatePushDestination` see the
19
+ // raw stored remote URL instead of values mutated by `url.*.insteadOf`
20
+ // rewrites (a common SSH-auth convenience on dev boxes). CI runners have
21
+ // empty gitconfigs so this is a no-op there; locally it makes `pnpm dev:run`
22
+ // and real runs produce identical git state. `os.devNull` canonicalizes
23
+ // the null device across Unix (`/dev/null`) and Windows (`\\.\nul`).
24
+ process.env.GIT_CONFIG_GLOBAL = devNull;
25
+ process.env.GIT_CONFIG_SYSTEM = devNull;
26
+
27
+ const tempParent = await mkdtemp(join(tmpdir(), "terramend-dev-run-"));
28
+ const tempDir = join(tempParent, "repo");
29
+ const originalCwd = process.cwd();
30
+
31
+ try {
32
+ setupTestRepo({ tempDir });
33
+ process.chdir(tempDir);
34
+
35
+ // optional pre-agent setup (e.g. seed symlinks for adversarial fixtures).
36
+ if (process.env.TERRAMEND_TEST_REPO_SETUP) {
37
+ log.info("» running repo setup commands...");
38
+ execSync(process.env.TERRAMEND_TEST_REPO_SETUP, { cwd: tempDir, stdio: "pipe" });
39
+ }
40
+
41
+ // tell main() to use the cloned tempDir instead of the GHA workspace path.
42
+ process.env.GITHUB_WORKSPACE = tempDir;
43
+
44
+ const inputs: Inputs =
45
+ typeof inputsOrPrompt === "string" ? { prompt: inputsOrPrompt } : inputsOrPrompt;
46
+
47
+ for (const [key, value] of Object.entries(inputs)) {
48
+ if (value !== undefined && value !== null) {
49
+ process.env[`INPUT_${key.toUpperCase()}`] = String(value);
50
+ }
51
+ }
52
+
53
+ const result: AgentResult = await main();
54
+ process.chdir(originalCwd);
55
+
56
+ if (result.success) {
57
+ log.success("Action completed successfully");
58
+ return { success: true, output: result.output || undefined, error: undefined };
59
+ }
60
+ log.error(`Action failed: ${result.error || "Unknown error"}`);
61
+ return { success: false, error: result.error || undefined, output: undefined };
62
+ } catch (err) {
63
+ const errorMessage = (err as Error).message;
64
+ log.error(`Error: ${errorMessage}`);
65
+ return { success: false, error: errorMessage, output: undefined };
66
+ } finally {
67
+ process.chdir(originalCwd);
68
+ // sandbox isolation may create files with non-host ownership; rmSync
69
+ // can't always delete those, so escalate.
70
+ try {
71
+ execSync(`sudo rm -rf "${tempParent}"`, { stdio: "ignore" });
72
+ } catch {
73
+ // best-effort cleanup.
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * End-of-run cleanup phases extracted out of `main.ts`. Three shapes:
3
+ *
4
+ * - `persistRunArtifacts`: best-effort post-review cleanup + summary +
5
+ * learnings persistence. Shared by both the success path and the
6
+ * error-catch path; idempotent (each step has its own guard against
7
+ * double-execution).
8
+ *
9
+ * - `finalizeSuccessRun`: success-only — calls `persistRunArtifacts`
10
+ * first, then surfaces harness-side failures in the progress comment,
11
+ * deletes stranded progress comments, writes the GitHub Actions job
12
+ * summary, and emits the structured output marker.
13
+ *
14
+ * - `writeRunErrorOutputs`: error-only — writes the rendered error
15
+ * summary to the Actions summary tab and mirrors it to the PR
16
+ * progress comment. The catch path calls this and then
17
+ * `persistRunArtifacts` separately so the rendered error lands before
18
+ * the persistence calls, in case the latter throw.
19
+ *
20
+ * All three swallow their own non-fatal errors (`log.debug` or empty
21
+ * `catch {}`) so a cleanup failure can't flip an already-decided run
22
+ * outcome.
23
+ */
24
+
25
+ import { writeFile } from "node:fs/promises";
26
+ import { join } from "node:path";
27
+ import * as core from "@actions/core";
28
+ import type { AgentResult } from "#app/agents/shared";
29
+ import { deleteProgressComment } from "#app/mcp/comment";
30
+ import type { ToolContext } from "#app/mcp/server";
31
+ import { buildSarifReport } from "#app/mcp/terraform/findings";
32
+ import type { ToolState } from "#app/toolState";
33
+ import { formatUsageSummary, log, writeSummary } from "#app/utils/cli";
34
+ import { reportErrorToComment } from "#app/utils/errorReport";
35
+ import { persistLearnings } from "#app/utils/learnings";
36
+ import { persistSummary } from "#app/utils/prSummary";
37
+ import { postReviewCleanup } from "#app/utils/reviewCleanup";
38
+ import { type RenderedRunError, renderRunError } from "#app/utils/runErrorRenderer";
39
+
40
+ /**
41
+ * Best-effort cleanup shared by both run-end paths:
42
+ * 1. post-review cleanup (dispatch follow-up re-review on submitted reviews)
43
+ * 2. persist the agent-edited PR summary tmpfile
44
+ * 3. persist the agent-edited repo-level learnings tmpfile
45
+ *
46
+ * Each step is idempotent and swallows its own errors. Safe to call from
47
+ * both `main()`'s success path and its catch path.
48
+ */
49
+ export async function persistRunArtifacts(toolContext: ToolContext): Promise<void> {
50
+ await postReviewCleanup(toolContext).catch((error) => {
51
+ log.debug(`post-review cleanup failed: ${error}`);
52
+ });
53
+ await persistSummary(toolContext);
54
+ await persistLearnings(toolContext);
55
+ }
56
+
57
+ /**
58
+ * Run the success-path cleanup waterfall:
59
+ *
60
+ * 1. shared best-effort cleanup via `persistRunArtifacts`
61
+ * 2. when the harness returned `success=false` (e.g. unsubmitted-review
62
+ * gate exhausted retries, stop-hook persistently failing), render via
63
+ * `renderRunError` and surface the error in BOTH the progress comment
64
+ * (rendered.comment) and the Actions job summary (rendered.summary,
65
+ * prepended below in step 4) — same classifier as the catch path so
66
+ * the user sees it instead of a deleted-comment void / empty summary
67
+ * tab
68
+ * 3. when the run succeeded, some write landed (`wasUpdated`), but the
69
+ * progress comment was never finalized via `report_progress`, delete
70
+ * the stranded comment (abandoned checklist, or a substantive artifact
71
+ * written via another MCP write tool that skipped report_progress). a
72
+ * run where NO write landed keeps its comment for handleAgentResult to
73
+ * salvage into — see the `wasUpdated` guard below and #868
74
+ * 4. write the GitHub Actions step summary (best-effort — a write
75
+ * failure must not throw past this point because we'd hit the outer
76
+ * catch and clobber any progress comment we just wrote)
77
+ * 5. emit the structured output marker for tests + workflow consumers
78
+ */
79
+ export async function finalizeSuccessRun(input: {
80
+ toolContext: ToolContext;
81
+ toolState: ToolState;
82
+ result: AgentResult;
83
+ repo: { owner: string; name: string };
84
+ }): Promise<void> {
85
+ await persistRunArtifacts(input.toolContext);
86
+
87
+ // shared rendering for the !success branch — same classifier as the
88
+ // outer catch path (BillingError reclassify → hang → BYOK billing →
89
+ // api-key → generic), so a harness-returned `{success: false}` lands an
90
+ // actionable error block in the job summary alongside the matching body
91
+ // in the progress comment. hang and generic get the `### ❌ Terramend
92
+ // failed` H3 banner; BillingError, BYOK billing, and api-key render
93
+ // their own provider-specific framing (no banner). renders once; reused
94
+ // for both surfaces below.
95
+ const rendered = !input.result.success
96
+ ? renderRunError({
97
+ errorMessage: input.result.error || "agent run failed",
98
+ repo: input.repo,
99
+ agentDiagnostic: input.toolState.agentDiagnostic,
100
+ })
101
+ : null;
102
+
103
+ // `createIfMissing: true` is load-bearing for silent triggers
104
+ // (IncrementalReview / pull_request_synchronize / auto-label) that have
105
+ // no progress comment to update — without it, terminal failures like
106
+ // BYOK billing exhaustion land only in the GH job summary, which most
107
+ // users never open. `reportErrorToComment` no-ops when both progress
108
+ // comment AND issue context are absent. see #835.
109
+ if (rendered) {
110
+ await reportErrorToComment({
111
+ toolState: input.toolState,
112
+ error: rendered.comment,
113
+ createIfMissing: true,
114
+ }).catch((error) => {
115
+ log.debug(`failure error report failed: ${error}`);
116
+ });
117
+ }
118
+
119
+ // create_pull_request_review owns its own deletion (see mcp/review.ts), so
120
+ // progressComment is already null by the time we get here for that path.
121
+ // uses finalSummaryWritten (not todoTracker.enabled or wasUpdated) so
122
+ // cleanup survives API failures in report_progress where cancel() ran but
123
+ // the write didn't succeed, and isn't fooled by writes to *other* artifacts.
124
+ //
125
+ // the extra `wasUpdated` guard preserves the comment when NO write landed at
126
+ // all: that's the case handleAgentResult salvages (raw-text answer / failed
127
+ // report_progress) by writing the agent's result into this very comment, or
128
+ // — when there's nothing to salvage — reports the failure into it. deleting
129
+ // here would strand the user with a vanished "Leaping into action" comment
130
+ // and a no-op error report (see #868).
131
+ if (
132
+ input.result.success &&
133
+ input.toolState.progressComment &&
134
+ input.toolState.wasUpdated &&
135
+ !input.toolState.finalSummaryWritten
136
+ ) {
137
+ await deleteProgressComment(input.toolContext).catch((error) => {
138
+ log.debug(`stranded progress comment cleanup failed: ${error}`);
139
+ });
140
+ }
141
+
142
+ try {
143
+ const usageSummary = formatUsageSummary(input.toolState.usageEntries);
144
+ const body = input.toolState.lastProgressBody || input.result.output;
145
+ const parts = [rendered?.summary, body, usageSummary].filter(Boolean);
146
+ if (parts.length > 0) {
147
+ await writeSummary(parts.join("\n\n"));
148
+ }
149
+ } catch (error) {
150
+ log.debug(`job summary write failed: ${error}`);
151
+ }
152
+
153
+ if (input.toolState.output) {
154
+ log.info(`::terramend-output::${Buffer.from(input.toolState.output).toString("base64")}`);
155
+ core.setOutput("result", input.toolState.output);
156
+ }
157
+
158
+ await emitFindingsOutputs(input.toolState);
159
+ }
160
+
161
+ /**
162
+ * §5.4 — when a deterministic scan ran this run (`terraform_scan` populated
163
+ * `lastScanConcerns`), expose its reported concern set to downstream workflow
164
+ * steps: the `findings-count` / `findings-sarif-path` action outputs, plus a
165
+ * safety-net SARIF write so modes that don't prompt the agent to call
166
+ * `terraform_emit_sarif` (Review) — or a Remediate run where the agent skipped
167
+ * that optional step — still surface a Code-Scanning artifact.
168
+ *
169
+ * When the agent already emitted a SARIF via `terraform_emit_sarif`
170
+ * (`toolState.emittedSarifPath` set), this defers to that file — points the
171
+ * output at it, does not rewrite — so an agent report at a lower threshold or
172
+ * custom path is never clobbered. Otherwise it writes the safety-net file using
173
+ * the canonical `buildSarifReport` (the same builder the tool uses), so there is
174
+ * only ever one SARIF format.
175
+ *
176
+ * Unset outputs when no scan ran; `0` + a results-free SARIF when the scan was
177
+ * clean — so a workflow gate can distinguish "clean scan" from "not scanned".
178
+ * Best-effort like every other finalize step: an emit failure must not flip the
179
+ * run outcome.
180
+ */
181
+ async function emitFindingsOutputs(toolState: ToolState): Promise<void> {
182
+ const concerns = toolState.lastScanConcerns;
183
+ if (!concerns) return;
184
+ try {
185
+ core.setOutput("findings-count", String(concerns.length));
186
+ if (toolState.emittedSarifPath) {
187
+ core.setOutput("findings-sarif-path", toolState.emittedSarifPath);
188
+ log.info(
189
+ `» findings output: ${concerns.length} concern(s), SARIF at ${toolState.emittedSarifPath} (agent-emitted)`,
190
+ );
191
+ return;
192
+ }
193
+ const sarifPath = join(process.env.GITHUB_WORKSPACE ?? process.cwd(), "terramend.sarif");
194
+ await writeFile(sarifPath, `${JSON.stringify(buildSarifReport(concerns), null, 2)}\n`);
195
+ core.setOutput("findings-sarif-path", sarifPath);
196
+ log.info(`» findings output: ${concerns.length} concern(s), SARIF at ${sarifPath}`);
197
+ } catch (error) {
198
+ log.debug(`findings output emit failed: ${error}`);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Write the rendered error to the GitHub Actions job summary tab + mirror
204
+ * to the PR progress comment when one exists. Catch path only.
205
+ *
206
+ * `lastProgressBody` and the usage table are appended to the summary so the
207
+ * partial work the agent did before failing isn't lost.
208
+ *
209
+ * `createIfMissing: true` is symmetric with `finalizeSuccessRun` — silent
210
+ * triggers (IncrementalReview / pull_request_synchronize / auto-label) that
211
+ * throw past `finalizeSuccessRun` (e.g. timeout race kills the agent
212
+ * mid-billing-exhausted-retry) reach this catch path with no progress
213
+ * comment to update, and without `createIfMissing` the terminal error
214
+ * lands only in the GH job summary that most users never open. see #835.
215
+ */
216
+ export async function writeRunErrorOutputs(input: {
217
+ rendered: RenderedRunError;
218
+ toolState: ToolState;
219
+ }): Promise<void> {
220
+ try {
221
+ const usageSummary = formatUsageSummary(input.toolState.usageEntries);
222
+ const parts = [input.rendered.summary, input.toolState.lastProgressBody, usageSummary].filter(
223
+ Boolean,
224
+ );
225
+ await writeSummary(parts.join("\n\n"));
226
+ } catch {}
227
+
228
+ try {
229
+ await reportErrorToComment({
230
+ toolState: input.toolState,
231
+ error: input.rendered.comment,
232
+ createIfMissing: true,
233
+ });
234
+ } catch {
235
+ // error reporting failed, but don't let it mask the original error
236
+ }
237
+ }