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
package/src/main.ts ADDED
@@ -0,0 +1,712 @@
1
+ // changes to tool permissions should be reflected in wiki/granular-tools.md
2
+
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { agents } from "#app/agents/index";
7
+ import { reportProgress } from "#app/mcp/comment";
8
+ import { startInstallation } from "#app/mcp/dependencies";
9
+ import { startMcpHttpServer, type ToolContext } from "#app/mcp/server";
10
+ import { computeModes } from "#app/modes";
11
+ import { mergeReviewModeInstructions } from "#app/reviewQuality";
12
+ import { initToolState } from "#app/toolState";
13
+ import {
14
+ type ActivityTimeout,
15
+ AGENT_ACTIVITY_TIMEOUT_MS,
16
+ createProcessOutputActivityTimeout,
17
+ DEFAULT_ACTIVITY_CHECK_INTERVAL_MS,
18
+ } from "#app/utils/activity";
19
+ import { resolveAgent, resolveModel } from "#app/utils/agent";
20
+ import { validateAgentApiKey } from "#app/utils/apiKeys";
21
+ import { isBackendConfigured } from "#app/utils/apiUrl";
22
+ import { resolveBody } from "#app/utils/body";
23
+ import {
24
+ buildUnavailableModelError,
25
+ hasProviderKeyForModel,
26
+ selectFallbackModelIfNeeded,
27
+ } from "#app/utils/byokFallback";
28
+ import { log } from "#app/utils/cli";
29
+ import { installCodexAuth, TERRAMEND_DATA_DIR } from "#app/utils/codexHome";
30
+ import { recordDiffReadFromToolUse } from "#app/utils/diffCoverage";
31
+ import { onExitSignal } from "#app/utils/exitHandler";
32
+ import { resolveGit, setGitAuthServer } from "#app/utils/gitAuth";
33
+ import { startGitAuthServer } from "#app/utils/gitAuthServer";
34
+ import { createOctokit, writeGitHubUsageSummaryToFile } from "#app/utils/github";
35
+ import { resolveInstructions } from "#app/utils/instructions";
36
+ import { persistLearnings, seedLearningsFile } from "#app/utils/learnings";
37
+ import { describeSetupFailure, executeLifecycleHook } from "#app/utils/lifecycle";
38
+ import { normalizeEnv } from "#app/utils/normalizeEnv";
39
+ import {
40
+ captureAuthorizedModels,
41
+ captureBaselineModels,
42
+ getAuthorizedModels,
43
+ } from "#app/utils/openCodeModels";
44
+ import { applyOverrides } from "#app/utils/overrides";
45
+ import {
46
+ ensurePackageManager,
47
+ packageManagerBinDir,
48
+ resolvePackageManagerSpec,
49
+ } from "#app/utils/packageManager";
50
+ import { aggregateUsage, patchWorkflowRunFields } from "#app/utils/patchWorkflowRunFields";
51
+ import { resolveOutputSchema, resolvePayload, resolvePromptInput } from "#app/utils/payload";
52
+ import { fetchPreviousSnapshot, persistSummary, seedSummaryFile } from "#app/utils/prSummary";
53
+ import { handleAgentResult } from "#app/utils/run";
54
+ import { resolveRunContextData } from "#app/utils/runContextData";
55
+ import { renderRunError } from "#app/utils/runErrorRenderer";
56
+ import {
57
+ finalizeSuccessRun,
58
+ persistRunArtifacts,
59
+ writeRunErrorOutputs,
60
+ } from "#app/utils/runLifecycle";
61
+ import { logRunStartup } from "#app/utils/runStartupLog";
62
+ import { setEnvAllowlist } from "#app/utils/secrets";
63
+ import { createTempDirectory, setupGit, wipeRunnerLeakSurface } from "#app/utils/setup";
64
+ import { killTrackedChildren } from "#app/utils/subprocess";
65
+ import { resolveTimeoutMs, TIMEOUT_DISABLED } from "#app/utils/time";
66
+ import { Timer } from "#app/utils/timer";
67
+ import { createTodoTracker } from "#app/utils/todoTracking";
68
+ import { getJobToken, resolveTokens } from "#app/utils/token";
69
+ import {
70
+ cleanupVertexCredentials,
71
+ materializeVertexCredentials,
72
+ type VertexCredentials,
73
+ } from "#app/utils/vertex";
74
+ import { resolveRun } from "#app/utils/workflow";
75
+
76
+ export { Inputs } from "#app/utils/payload";
77
+
78
+ export interface MainResult {
79
+ success: boolean;
80
+ output?: string | undefined;
81
+ error?: string | undefined;
82
+ result?: string | undefined;
83
+ }
84
+
85
+ export async function main(): Promise<MainResult> {
86
+ // normalize env var names to uppercase (handles case-insensitive workflow files)
87
+ normalizeEnv();
88
+
89
+ // apply caller-supplied env overrides — JSON object forwarded as the
90
+ // UNSAFE_OVERRIDES env var (NOT a `with:` input). gated by `actions:write`
91
+ // on the repo and refuses integrity-critical names; see utils/overrides.ts
92
+ // for the deny-list and wiki/e2e-testing.md for usage + threat model.
93
+ // the `unsafe` prefix is intentional: GH echoes the env-block value in the
94
+ // step-header log, so the raw JSON is visible to anyone with `actions:read`.
95
+ const overridesRaw = process.env.UNSAFE_OVERRIDES ?? "";
96
+ if (overridesRaw.trim()) {
97
+ const result = applyOverrides({ raw: overridesRaw, env: process.env });
98
+ if (result.applied.length > 0) {
99
+ log.info(`» applied ${result.applied.length} env override(s): ${result.applied.join(", ")}`);
100
+ }
101
+ if (result.denied.length > 0) {
102
+ log.warning(
103
+ `» refused to override ${result.denied.length} protected env var(s): ${result.denied.join(", ")}`,
104
+ );
105
+ }
106
+ }
107
+
108
+ // write usage summary on SIGINT/SIGTERM so the worker can read it after sandbox.exec
109
+ const usageSummaryPath = process.env.TERRAMEND_USAGE_SUMMARY_PATH;
110
+ if (usageSummaryPath) {
111
+ onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
112
+ }
113
+
114
+ const timer = new Timer();
115
+ let activityTimeout: ActivityTimeout | null = null;
116
+ let safetyNetTimer: NodeJS.Timeout | undefined;
117
+
118
+ // parse prompt early to extract progressComment for toolState
119
+ const resolvedPromptInput = resolvePromptInput();
120
+
121
+ const toolState = initToolState({
122
+ progressComment:
123
+ typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressComment : undefined,
124
+ });
125
+
126
+ // resolve and fingerprint git binary before any agent code runs
127
+ resolveGit();
128
+
129
+ // get job token for initial API calls
130
+ const jobToken = getJobToken();
131
+ const initialOctokit = createOctokit(jobToken);
132
+ const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
133
+ timer.checkpoint("runContextData");
134
+
135
+ // Called for its side effect: `createTempDirectory()` sets TERRAMEND_TEMP_DIR,
136
+ // which `installFromNpmTarball` reads when the opencode CLI install runs below
137
+ // for BYOK introspection, and which agent + mcp server setup further down also
138
+ // consume. The returned path isn't needed directly here.
139
+ createTempDirectory();
140
+
141
+ // install OpenCode + capture the BASELINE model set BEFORE Codex auth.json
142
+ // is in scope. this is the set of models OpenCode can route from the runner's
143
+ // pre-existing environment alone (workflow `env:` block + GH Actions secrets).
144
+ // install is fs-cached, so the duplicate call inside the opencode agent's
145
+ // run() is a no-op.
146
+ const opencodeCliPath = await agents.opencode.install();
147
+ captureBaselineModels(opencodeCliPath);
148
+
149
+ // materialize Codex auth.json (idempotent — opencode agent re-calls inside
150
+ // run() and writes the same file). this has to land BEFORE
151
+ // captureAuthorizedModels so OpenCode's model introspection sees the
152
+ // OAuth-routed openai/* models.
153
+ installCodexAuth();
154
+
155
+ // capture the AUTHORIZED model set after Codex auth.json is applied. this is
156
+ // the authoritative source for the BYOK fallback decision and the
157
+ // opencode-agent path of validateAgentApiKey — strictly more accurate than
158
+ // the static envVars/managedCredentials catalog, which can miss new auth
159
+ // shapes.
160
+ captureAuthorizedModels(opencodeCliPath);
161
+
162
+ // configure env allowlist for subprocess filtering
163
+ if (runContext.repoSettings.envAllowlist) {
164
+ setEnvAllowlist(runContext.repoSettings.envAllowlist);
165
+ }
166
+
167
+ // resolve payload to determine shell permission
168
+ const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
169
+ toolState.model = payload.model;
170
+ if (payload.event.trigger === "pull_request_synchronize") {
171
+ toolState.beforeSha = payload.event.before_sha;
172
+ }
173
+
174
+ // resolve tokens first — acquireNewToken needs OIDC env vars for token exchange
175
+ await using tokenRef = await resolveTokens({ push: payload.push });
176
+
177
+ // wipe the GHA runner's known credential leak surface inside $RUNNER_TEMP
178
+ // before the agent spawns. our installation token is already in memory
179
+ // (tokenRef above), and setupGit's includeIf strip handles the matching
180
+ // dangling references in the user's .git/config. see wipeRunnerLeakSurface
181
+ // for the leak inventory and threat model.
182
+ wipeRunnerLeakSurface();
183
+
184
+ // clear OIDC env vars in restricted mode to prevent agent from minting tokens
185
+ if (payload.shell !== "enabled") {
186
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
187
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
188
+ }
189
+
190
+ // create octokit with MCP token for GitHub API calls
191
+ const octokit = createOctokit(tokenRef.mcpToken);
192
+
193
+ const runInfo = await resolveRun({ octokit });
194
+ let toolContext: ToolContext | undefined;
195
+ let progressCallbackDisabled = false;
196
+ let todoTracker: ReturnType<typeof createTodoTracker> | undefined;
197
+ let vertexCredentials: VertexCredentials | undefined;
198
+
199
+ try {
200
+ if (payload.cwd && process.cwd() !== payload.cwd) {
201
+ process.chdir(payload.cwd);
202
+ }
203
+
204
+ const tmpdir = createTempDirectory();
205
+
206
+ // resolve body - fetches body_html and converts to markdown if images present
207
+ // this ensures agents receive markdown with working signed image URLs
208
+ const originalBody = payload.event.body;
209
+ const resolvedBody = await resolveBody({
210
+ event: payload.event,
211
+ octokit,
212
+ repo: runContext.repo,
213
+ tmpdir,
214
+ githubToken: tokenRef.mcpToken,
215
+ });
216
+ if (resolvedBody !== originalBody) {
217
+ payload.event.body = resolvedBody;
218
+ // also update prompt if original body was included there
219
+ if (originalBody && payload.prompt.includes(originalBody)) {
220
+ payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
221
+ }
222
+ }
223
+
224
+ await using gitAuthServer = await startGitAuthServer(tmpdir);
225
+ setGitAuthServer(gitAuthServer);
226
+
227
+ const initialResolvedModel = resolveModel({ slug: payload.model });
228
+
229
+ // BYOK model gate. Three outcomes (see `selectFallbackModelIfNeeded`):
230
+ // - use-resolved: run the configured model as-is.
231
+ // - fallback: NO provider key present → swap to the free OpenCode model
232
+ // so the run still produces value. Without this, the agent launches
233
+ // with no key, the LLM provider 401s, and the run dies in seconds with
234
+ // a synthetic "Invalid API key" — the silent-churn pattern that took
235
+ // out 15 accounts before this landed.
236
+ // - unavailable: a provider key IS present but the configured model is
237
+ // not one the key can route. Fail loudly with the authorized list
238
+ // rather than silently downgrading to free (which hides a wrong model
239
+ // id — see PR #2, where a Google key was set but the slug was invalid
240
+ // and the run silently used big-pickle).
241
+ const authorized = getAuthorizedModels();
242
+ // the gate needs the agent to spare claude-harness runs (own auth,
243
+ // invisible to `opencode models`) from a spurious downgrade/failure.
244
+ const decision = selectFallbackModelIfNeeded({
245
+ resolvedModel: initialResolvedModel,
246
+ authorized,
247
+ providerKeyPresent: initialResolvedModel
248
+ ? hasProviderKeyForModel(initialResolvedModel)
249
+ : false,
250
+ agentName: resolveAgent({ model: initialResolvedModel }).name,
251
+ });
252
+ if (decision.kind === "unavailable") {
253
+ throw new Error(buildUnavailableModelError({ model: decision.model, authorized }));
254
+ }
255
+ // when fallback engages we bypass `resolveModel` for the new slug —
256
+ // `TERRAMEND_MODEL` has higher priority than the slug arg inside that
257
+ // helper and would otherwise re-override back to the unkeyed model.
258
+ // the free fallback slug is already a CLI-ready specifier, so using
259
+ // it verbatim is correct and avoids the override.
260
+ const effectiveSlug = decision.kind === "fallback" ? decision.to : payload.model;
261
+ const resolvedModel = decision.kind === "fallback" ? decision.to : initialResolvedModel;
262
+ if (decision.kind === "fallback") {
263
+ log.warning(
264
+ `» fell back from ${decision.from} to ${decision.to} — no provider key present in runner env. add a provider key in repo secrets to use ${decision.from} instead.`,
265
+ );
266
+ toolState.modelFallback = { from: decision.from };
267
+ }
268
+
269
+ vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
270
+
271
+ const agent = resolveAgent({ model: resolvedModel });
272
+
273
+ // surface the effective model in comment/review footers. payload.model is
274
+ // just the stored slug (often undefined when the agent auto-selects).
275
+ // matching priority with resolveModelForLog so the "Using `…`" badge
276
+ // reflects what actually ran.
277
+ toolState.model = resolvedModel ?? effectiveSlug;
278
+
279
+ // skip validation when fallback engaged: the effective model is the
280
+ // free fallback (`opencode/big-pickle`) and the fallback gate already
281
+ // authoritatively decided "this model is OK to run". re-validating
282
+ // would spuriously throw if `opencode models` doesn't list big-pickle.
283
+ if (decision.kind !== "fallback") {
284
+ validateAgentApiKey({
285
+ agent,
286
+ model: resolvedModel ?? effectiveSlug,
287
+ authorized,
288
+ owner: runContext.repo.owner,
289
+ name: runContext.repo.name,
290
+ });
291
+ }
292
+
293
+ await setupGit({
294
+ gitToken: tokenRef.gitToken,
295
+ owner: runContext.repo.owner,
296
+ name: runContext.repo.name,
297
+ octokit,
298
+ toolState,
299
+ shell: payload.shell,
300
+ postCheckoutScript: runContext.repoSettings.postCheckoutScript,
301
+ });
302
+ timer.checkpoint("git");
303
+
304
+ // pin the project's package manager via corepack BEFORE the setup hook
305
+ // runs. without this, a customer setup script like `npm i -g pnpm &&
306
+ // pnpm install` installs whatever pnpm is "latest" today and writes
307
+ // unrelated lockfile drift (e.g. the `packageManagerDependencies`
308
+ // block pnpm 11.3 added) into our commit — see #844. resolution
309
+ // honors pnpm 11+ precedence (`devEngines.packageManager` over
310
+ // `packageManager`); failure is non-fatal — we fall back to whatever
311
+ // is on PATH with a warning. the shim lands in a private bin dir
312
+ // (prepended to PATH), not the node bin dir, so a setup `npm i -g pnpm`
313
+ // can't collide with it.
314
+ const pmSpec = await resolvePackageManagerSpec(process.cwd());
315
+ if (pmSpec) {
316
+ await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir) });
317
+ }
318
+ timer.checkpoint("packageManager");
319
+
320
+ // execute the setup lifecycle hook (runs once at initialization). best-effort:
321
+ // a failure no longer aborts the run — we warn the operator and surface it to
322
+ // the agent via the SETUP HOOK FAILED banner so it can verify the env and adapt.
323
+ const setupHook = await executeLifecycleHook({
324
+ event: "setup",
325
+ script: runContext.repoSettings.setupScript,
326
+ normalizeWorkingTreeAfter: true,
327
+ });
328
+ if (setupHook.warning) {
329
+ log.warning(setupHook.warning);
330
+ }
331
+ timer.checkpoint("lifecycleHooks::setup");
332
+
333
+ const agentId = agent.name;
334
+ const modes = [...computeModes(agentId), ...runContext.repoSettings.modes];
335
+
336
+ const outputSchema = resolveOutputSchema();
337
+
338
+ // mcpServerUrl and tmpdir are set after server starts
339
+ toolContext = {
340
+ agentId,
341
+ repo: runContext.repo,
342
+ payload,
343
+ octokit,
344
+ githubInstallationToken: tokenRef.mcpToken,
345
+ gitToken: tokenRef.gitToken,
346
+ apiToken: runContext.apiToken,
347
+ modes,
348
+ postCheckoutScript: runContext.repoSettings.postCheckoutScript,
349
+ prepushScript: runContext.repoSettings.prepushScript,
350
+ prApproveEnabled: runContext.repoSettings.prApproveEnabled,
351
+ // §5.5 — workflow-file review policy / FP precedents compose with the
352
+ // backend-provided per-mode instructions (both owner-controlled).
353
+ modeInstructions: mergeReviewModeInstructions(runContext.repoSettings.modeInstructions, {
354
+ reviewInstructions: payload.reviewInstructions,
355
+ fpFilteringInstructions: payload.fpFilteringInstructions,
356
+ }),
357
+ toolState,
358
+ runId: runInfo.runId,
359
+ mcpServerUrl: "",
360
+ mcpServerToken: "",
361
+ tmpdir,
362
+ oss: runContext.oss,
363
+ plan: runContext.plan,
364
+ resolvedModel,
365
+ };
366
+ await using mcpHttpServer = await startMcpHttpServer(toolContext, { outputSchema });
367
+ toolContext.mcpServerUrl = mcpHttpServer.url;
368
+ toolContext.mcpServerToken = mcpHttpServer.token;
369
+ log.info(`» MCP server started at ${mcpHttpServer.url}`);
370
+ timer.checkpoint("mcpServer");
371
+
372
+ // seed the rolling repo-level learnings tmpfile. the agent reads the file
373
+ // at startup (path is surfaced in the LEARNINGS section of the prompt) and
374
+ // may edit it during the post-run reflection turn. persistLearnings reads
375
+ // it back at end-of-run and PATCHes any changes to Repo.learnings.
376
+ //
377
+ // gated on a configured backend: learnings only have somewhere to persist
378
+ // when a real backend is wired (hosted SaaS / local dev). in a standalone
379
+ // BYOK run there is no store, so seeding the file would only provoke the
380
+ // post-run reflection turn (an extra billed LLM turn) to edit a file whose
381
+ // changes get dropped — and persistLearnings would then PATCH the default
382
+ // marketing host and warn on its 404. leaving learningsFilePath unset makes
383
+ // every downstream consumer (`persistLearnings`, agent harnesses,
384
+ // `resolveInstructions`) treat this as "no learnings affordance this run".
385
+ //
386
+ // wrapped in best-effort try/catch: an unwrapped filesystem failure
387
+ // (ENOSPC, EACCES, hostile sandbox) would unwind into the outer main()
388
+ // catch and flip an otherwise-successful run to "❌ Terramend failed".
389
+ if (isBackendConfigured()) {
390
+ try {
391
+ const learningsPath = await seedLearningsFile({
392
+ tmpdir,
393
+ current: runContext.repoSettings.learnings,
394
+ });
395
+ toolState.learningsFilePath = learningsPath;
396
+ // file on disk is the verbatim DB body, so the seed used for
397
+ // change-detection is just `current ?? ""` (trimmed). persistLearnings
398
+ // byte-compares against the trimmed read-back to skip no-op PATCHes.
399
+ toolState.learningsSeed = (runContext.repoSettings.learnings ?? "").trim();
400
+ log.info(
401
+ `» learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`,
402
+ );
403
+ const ctxForExit = toolContext;
404
+ onExitSignal(() => persistLearnings(ctxForExit));
405
+ } catch (err) {
406
+ log.warning(
407
+ `» learnings seed failed: ${err instanceof Error ? err.message : String(err)} — continuing without learnings file`,
408
+ );
409
+ }
410
+ } else {
411
+ log.debug(
412
+ "no backend configured (API_URL unset) — skipping learnings seed + reflection turn",
413
+ );
414
+ }
415
+
416
+ // seed the rolling PR summary tmpfile when the dispatcher requested it.
417
+ // gated on event being a PR — issue/workflow_dispatch runs have no
418
+ // summarySnapshot to maintain. file path is exposed to the agent via
419
+ // the select_mode response addendum (action/mcp/selectMode.ts).
420
+ if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
421
+ const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
422
+ const filePath = await seedSummaryFile({ tmpdir, previousSnapshot });
423
+ toolState.summaryFilePath = filePath;
424
+ // capture the exact bytes the agent will see at startup. used by
425
+ // the post-run retry loop to detect the agent forgetting to edit
426
+ // the file (byte-identical to seed → nudge once via resume turn)
427
+ // and by persistSummary to skip the DB write when nothing changed.
428
+ try {
429
+ toolState.summarySeed = await readFile(filePath, "utf8");
430
+ } catch {
431
+ // intentionally empty — summarySeed stays undefined
432
+ }
433
+ log.info(
434
+ `» summary snapshot seeded at ${filePath} (previous=${previousSnapshot ? "yes" : "no"})`,
435
+ );
436
+ // on SIGINT/SIGTERM we still want to persist whatever the agent has
437
+ // written so far. handler is best-effort: any failure inside is
438
+ // swallowed by Promise.allSettled in exitHandler.ts, and the
439
+ // summaryPersistAttempted guard prevents double-execution if the
440
+ // signal arrives after the normal path already persisted. capture a
441
+ // narrowed reference so the closure doesn't depend on the outer
442
+ // `toolContext` variable being defined later.
443
+ const ctxForExit = toolContext;
444
+ onExitSignal(() => persistSummary(ctxForExit));
445
+ }
446
+
447
+ startInstallation(toolContext);
448
+
449
+ logRunStartup({ payload, resolvedModel, agentName: agent.name });
450
+
451
+ const instructions = resolveInstructions({
452
+ payload,
453
+ repo: runContext.repo,
454
+ modes,
455
+ agentId,
456
+ outputSchema,
457
+ learningsFilePath: toolState.learningsFilePath ?? null,
458
+ learningsHeadings: runContext.repoSettings.learningsHeadings,
459
+ setupHookFailure: describeSetupFailure(setupHook.failure),
460
+ });
461
+ const logParts = [
462
+ instructions.eventInstructions
463
+ ? `EVENT-LEVEL INSTRUCTIONS:\n${instructions.eventInstructions}`
464
+ : null,
465
+ instructions.user ? `USER REQUEST:\n${instructions.user}` : null,
466
+ instructions.event,
467
+ ].filter(Boolean);
468
+ log.box(logParts.join("\n\n---\n\n"), {
469
+ title: "Instructions",
470
+ });
471
+ log.group("View full prompt", () => {
472
+ log.info(instructions.full);
473
+ });
474
+
475
+ // OpenCode loads .opencode/plugin/ files at startup. if the repo has any,
476
+ // eagerly await dependency installation so plugin imports can resolve.
477
+ if (agentId === "opencode") {
478
+ const pluginDir = join(process.cwd(), ".opencode", "plugin");
479
+ const hasPlugins =
480
+ existsSync(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
481
+ if (hasPlugins && toolState.dependencyInstallation?.promise) {
482
+ log.info(
483
+ "» .opencode/plugin/ detected — awaiting dependency installation before agent start",
484
+ );
485
+ await toolState.dependencyInstallation.promise.catch(() => {});
486
+ timer.checkpoint("awaitDepsForPlugins");
487
+ }
488
+ }
489
+
490
+ // run agent, optionally with timeout enforcement
491
+ activityTimeout = createProcessOutputActivityTimeout({
492
+ timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
493
+ checkIntervalMs: DEFAULT_ACTIVITY_CHECK_INTERVAL_MS,
494
+ });
495
+ activityTimeout.promise.catch(() => {}); // prevent unhandled rejection if agent wins race
496
+ todoTracker = createTodoTracker(async (body) => {
497
+ if (progressCallbackDisabled || !toolContext) return;
498
+ try {
499
+ // liveProgress: this is the auto-rendered checklist, not a deliberate
500
+ // final answer — must not flip wasUpdated / lastProgressBody (see #868).
501
+ await reportProgress(toolContext, { body, liveProgress: true });
502
+ } catch (err) {
503
+ log.debug(`progress update failed: ${err}`);
504
+ }
505
+ });
506
+ toolState.todoTracker = todoTracker;
507
+
508
+ // on cancellation, stop scheduling new tracker writes immediately. without this, a
509
+ // debounced write queued just before SIGTERM could land at GitHub *after* the
510
+ // workflow_run.completed webhook has already replaced the comment with the
511
+ // "This run was cancelled" body, clobbering it back to the task list. we can't
512
+ // await in-flight writes (the process is exiting), but cancelling the timer
513
+ // shrinks the race window.
514
+ onExitSignal(() => {
515
+ todoTracker?.cancel();
516
+ });
517
+
518
+ // when the agent subprocess is killed for inner activity timeout, stop
519
+ // the MCP HTTP server so mcp-proxy's SSE reconnect attempts don't keep
520
+ // the outer activity timer alive. start a short safety-net timer — if
521
+ // the agent promise hasn't resolved within 5min after the inner kill,
522
+ // force-reject the outer timer so the run can exit.
523
+ let innerTimeoutFired = false;
524
+ const onInnerActivityTimeout = () => {
525
+ if (innerTimeoutFired) return;
526
+ innerTimeoutFired = true;
527
+ log.info(
528
+ "» inner activity timeout fired — stopping MCP server and starting 5min safety-net timer",
529
+ );
530
+ // fire and forget — the server's dispose is idempotent so the
531
+ // `await using` cleanup at block exit is still safe.
532
+ mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
533
+ log.debug(
534
+ `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`,
535
+ );
536
+ });
537
+ safetyNetTimer = setTimeout(
538
+ () => {
539
+ activityTimeout?.forceReject(
540
+ "agent still pending 5min after inner activity kill — forcing exit",
541
+ );
542
+ },
543
+ 5 * 60 * 1000,
544
+ );
545
+ safetyNetTimer.unref?.();
546
+ };
547
+
548
+ const agentPromise = agent.run({
549
+ payload,
550
+ resolvedModel,
551
+ mcpServerUrl: mcpHttpServer.url,
552
+ mcpServerToken: mcpHttpServer.token,
553
+ tmpdir,
554
+ // TERRAMEND_DATA_DIR (/var/lib/terramend) holds codex auth.json + any
555
+ // future terramend-managed on-disk secrets. bash via MCP tmpfs-overlays
556
+ // it; agent native FS tools deny it via the same secretDenyPaths plumbing
557
+ // used for vertex creds. see wiki/security.md "Filesystem Sandbox".
558
+ secretDenyPaths: [
559
+ TERRAMEND_DATA_DIR,
560
+ ...(vertexCredentials ? [vertexCredentials.secretDir] : []),
561
+ ],
562
+ instructions,
563
+ todoTracker,
564
+ stopScript: runContext.repoSettings.stopScript,
565
+ toolState,
566
+ apiToken: runContext.apiToken,
567
+ onActivityTimeout: onInnerActivityTimeout,
568
+ onToolUse: (event) => {
569
+ const wasTracked = recordDiffReadFromToolUse({
570
+ state: toolState.diffCoverage,
571
+ toolName: event.toolName,
572
+ input: event.input,
573
+ cwd: process.cwd(),
574
+ });
575
+ if (!wasTracked) return;
576
+ const trackedRanges = toolState.diffCoverage?.coveredRanges ?? [];
577
+ log.debug(
578
+ `» diff coverage tracked from tool ${event.toolName} (${trackedRanges.length} merged range${trackedRanges.length === 1 ? "" : "s"})`,
579
+ );
580
+ },
581
+ });
582
+ // symmetric with the activityTimeout/timeoutPromise catches below: if a
583
+ // timeout wins the race, agentPromise is stranded and its later rejection
584
+ // becomes an unhandled rejection. node 15+ terminates the process on
585
+ // unhandled rejection by default, which would kill main() mid-cleanup and
586
+ // lose the error-reporting / usage-summary work that follows. the race
587
+ // still sees the rejection (the original promise is shared); this catch
588
+ // only keeps node from treating a post-race rejection as unobserved.
589
+ agentPromise.catch(() => {});
590
+
591
+ // timeout enforcement: default is 1 hour, but can be overridden via flags in the prompt:
592
+ // - --timeout=2h (or any duration like "--timeout=30m", "--timeout=1h30m") to set a custom timeout
593
+ // - --notimeout to disable timeout entirely
594
+ let result: Awaited<typeof agentPromise>;
595
+ if (payload.timeout === TIMEOUT_DISABLED) {
596
+ result = await Promise.race([agentPromise, activityTimeout.promise]);
597
+ } else {
598
+ // resolveTimeoutMs rejects unparseable / zero / setTimeout-overflow inputs
599
+ // so a bad string can't silently resolve to an instant timeout. fall back
600
+ // to the 1h default with a warning — users who want runtime measured in
601
+ // weeks should use --notimeout.
602
+ const usable = resolveTimeoutMs(payload.timeout);
603
+ if (payload.timeout && usable === null) {
604
+ log.warning(`invalid timeout "${payload.timeout}" (use --notimeout to disable), using 1h`);
605
+ }
606
+ const timeoutMs = usable ?? 3600000;
607
+ const actualTimeout = usable !== null ? payload.timeout : "1h";
608
+ let timeoutId: NodeJS.Timeout | undefined;
609
+ const timeoutPromise = new Promise<never>((_, reject) => {
610
+ timeoutId = setTimeout(() => {
611
+ reject(new Error(`agent run timed out after ${actualTimeout}`));
612
+ }, timeoutMs);
613
+ });
614
+ timeoutPromise.catch(() => {}); // prevent unhandled rejection if agent wins race
615
+ try {
616
+ result = await Promise.race([agentPromise, timeoutPromise, activityTimeout.promise]);
617
+ } finally {
618
+ clearTimeout(timeoutId);
619
+ }
620
+ }
621
+
622
+ // accumulate top-level agent usage
623
+ if (result.usage) {
624
+ toolState.usageEntries.push(result.usage);
625
+ }
626
+
627
+ // validate this before writing job summary to avoid masking the error
628
+ if (outputSchema && !toolState.output) {
629
+ throw new Error(
630
+ "output_schema was provided but agent did not call set_output — structured output is required",
631
+ );
632
+ }
633
+
634
+ // success-path cleanup: postReview → persistSummary → persistLearnings →
635
+ // failure-error-report → stranded-comment cleanup → job summary → output
636
+ // marker. each step is best-effort; see `finalizeSuccessRun` for ordering
637
+ // rationale (notably: progress-comment deletion lives in
638
+ // create_pull_request_review for review-mode runs, so deletion here
639
+ // covers the non-review success paths).
640
+ await finalizeSuccessRun({ toolContext, toolState, result, repo: runContext.repo });
641
+
642
+ return await handleAgentResult({
643
+ result,
644
+ toolContext,
645
+ silent: payload.event.silent ?? false,
646
+ });
647
+ } catch (error) {
648
+ const errorMessage = error instanceof Error ? error.message : "unknown error occurred";
649
+ progressCallbackDisabled = true;
650
+ todoTracker?.cancel();
651
+ killTrackedChildren();
652
+ log.error(errorMessage);
653
+
654
+ // classify (BillingError reclassification + hang detection + API-key auth
655
+ // detection) and render to {summary, comment} markdown bodies.
656
+ const rendered = renderRunError({
657
+ errorMessage,
658
+ repo: runContext.repo,
659
+ agentDiagnostic: toolState.agentDiagnostic,
660
+ });
661
+ await writeRunErrorOutputs({ rendered, toolState });
662
+
663
+ // best-effort cleanup: review dispatch, summary persist, learnings persist.
664
+ // a partial edit before the crash is still worth keeping.
665
+ if (toolContext) {
666
+ await persistRunArtifacts(toolContext);
667
+ }
668
+
669
+ return {
670
+ success: false,
671
+ error: errorMessage,
672
+ };
673
+ } finally {
674
+ activityTimeout?.stop();
675
+ if (safetyNetTimer) clearTimeout(safetyNetTimer);
676
+ // also reap on the success-with-failure path (agent returned
677
+ // `{success: false}`) which skips the catch above and would otherwise hang
678
+ // ~60s on the eager dep install. idempotent SIGKILL. see #862.
679
+ killTrackedChildren();
680
+ if (usageSummaryPath) {
681
+ // a write error here (ENOSPC, EACCES, dirname removed) must not mask
682
+ // either the try's successful return or the catch's error return.
683
+ // the summary is informational — log and move on.
684
+ try {
685
+ await writeGitHubUsageSummaryToFile(usageSummaryPath);
686
+ } catch (err) {
687
+ log.debug(
688
+ `failed to write usage summary to ${usageSummaryPath}: ${err instanceof Error ? err.message : String(err)}`,
689
+ );
690
+ }
691
+ }
692
+
693
+ // persist aggregated token + cost usage to the WorkflowRun row.
694
+ // this is the single shared cleanup path across every agent implementation:
695
+ // each agent harness returns a single AgentUsage from agent.run() that
696
+ // already aggregates its internal retries via mergeAgentUsage, and the
697
+ // success branch above pushes that entry into toolState.usageEntries.
698
+ // aggregateUsage sums across those entries (one per agent.run()).
699
+ //
700
+ // caveat: if the agent promise rejected (timeout or uncaught throw) the
701
+ // usage was never pushed, so nothing gets persisted for that run. runs
702
+ // that returned AgentResult with success=false still report their partial
703
+ // usage because the harness populates AgentUsage before returning.
704
+ if (toolContext) {
705
+ const patch = aggregateUsage(toolState.usageEntries);
706
+ if (Object.keys(patch).length > 0) {
707
+ await patchWorkflowRunFields(toolContext, patch);
708
+ }
709
+ }
710
+ cleanupVertexCredentials(vertexCredentials);
711
+ }
712
+ }