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,134 @@
1
+ /**
2
+ * Review-quality controls for the Review / IncrementalReview modes: a curated
3
+ * false-positive precedents list and an adversarial per-finding verification
4
+ * pass. Both are embedded into the mode prompts (modes.ts) — the precedents
5
+ * also travel verbatim inside every verification dispatch, since subagents
6
+ * never see the orchestrator's prompt.
7
+ *
8
+ * Provenance: adapted from Anthropic's claude-code-security-review action
9
+ * (MIT) — its hard-exclusion rules, its LLM-judge "PRECEDENTS" list, and the
10
+ * /security-review slash command's refute-subagent pattern. See
11
+ * CLAUDE-CODE-SECURITY-REVIEW-VS-TERRAMEND.md (workspace root) §5.1–5.2 for
12
+ * the comparison that motivated this. Deliberate divergences from upstream:
13
+ * - CCSR judges findings via sequential direct API calls; we dispatch
14
+ * parallel read-only reviewfrog subagents (machine-gated, see
15
+ * agents/subagentToolGates.ts) so verification adds one round of wall
16
+ * time regardless of finding count.
17
+ * - CCSR's judge gates on a bare confidence number; we gate on an explicit
18
+ * verdict (confirmed/refuted/uncertain) plus confidence, because the
19
+ * verdict is what the orchestrator acts on and the number alone invites
20
+ * anchoring.
21
+ * - CCSR drops "secrets stored on disk" findings (handled by a separate
22
+ * pipeline there). Terramend reviews IaC where a hardcoded credential is
23
+ * a core finding, so that exclusion is intentionally NOT inherited.
24
+ * Kept as-is from upstream: fail-open semantics (a broken verifier must
25
+ * never silently swallow a true positive) and suppression auditability
26
+ * (excluded findings are listed, never deleted).
27
+ */
28
+
29
+ import { REVIEWER_AGENT_NAME } from "#app/agents/reviewer";
30
+
31
+ /**
32
+ * False-positive precedents applied at aggregation time and inside every
33
+ * verification dispatch. Each entry encodes a recurring FP class; a candidate
34
+ * matching one needs specific evidence that the precedent does not apply, or
35
+ * it gets dropped. Ordered: hard exclusions (never post) → general code
36
+ * precedents → Terraform/IaC precedents → the final signal-quality bar.
37
+ */
38
+ export const REVIEW_FINDING_PRECEDENTS = `### Finding precedents (false-positive control)
39
+
40
+ Apply these when deciding whether a candidate finding is worth posting, and include this whole section verbatim in every verification dispatch. Each precedent encodes a recurring false-positive class: a candidate that matches one is dropped unless you have specific evidence the precedent does not apply here.
41
+
42
+ **Hard exclusions — never post:**
43
+
44
+ - Denial-of-service / resource-exhaustion concerns without a concrete, cheap-to-trigger attack path: missing rate limiting, "unbounded" loops over trusted input, "could exhaust memory/CPU".
45
+ - Theoretical race conditions or timing attacks. Post a race only when it is concretely reachable and concretely harmful.
46
+ - Memory-safety findings (buffer overflow, use-after-free, OOB) in memory-safe languages — Rust, Go, JS/TS, Python, Java, HCL.
47
+ - Security findings whose anchor is a documentation file — a code snippet in \`.md\`/\`.mdx\` is not an attack surface. (Stale or incorrect docs remain valid *impact* findings; this exclusion is only for treating doc content as exploitable.)
48
+ - "Lack of hardening" with no vulnerability: code is not required to implement every best practice, only to avoid concrete flaws.
49
+ - Vulnerable-dependency reports based on version strings alone — dependency scanning is a separate pipeline with its own remediation flow.
50
+
51
+ **General precedents:**
52
+
53
+ - Environment variables, CLI flags, and workflow-dispatch inputs are operator-trusted. An attack that requires controlling them is invalid.
54
+ - A missing permission/auth check in client-side code is not a finding; the server is the enforcement boundary. The same applies to client-side input validation.
55
+ - React/Angular-class frameworks escape output by default — an XSS claim needs \`dangerouslySetInnerHTML\`, \`bypassSecurityTrustHtml\`, or an equivalent unsafe API in the diff.
56
+ - SSRF requires control of host or protocol; path-only control is not SSRF. Neither SSRF nor path traversal applies to purely client-side code.
57
+ - Command injection in shell scripts needs a named untrusted-input path; developer-invoked scripts taking developer-supplied arguments don't qualify.
58
+ - Un-sanitized user input reaching logs is log spoofing, not a vulnerability. A logging finding is valid only when it exposes secrets, credentials, or PII.
59
+ - UUIDs are unguessable; an attack that requires guessing one is invalid.
60
+
61
+ **Terraform / IaC precedents:**
62
+
63
+ - \`0.0.0.0/0\` **egress** is common and usually intentional — flag open **ingress**, or egress only with a concrete exfiltration concern attached.
64
+ - Values from \`*.tfvars\`, \`locals\`, and module input variables are operator-trusted; "what if this variable is malicious" is invalid without naming an untrusted writer.
65
+ - Missing encryption / versioning / access-logging on resources that demonstrably hold no sensitive data (short-retention log groups, test fixtures, scratch buckets) is ℹ️ at most, never 🚨.
66
+ - Unpinned provider or module versions are style feedback for first-party modules; ⚠️ only for third-party module sources, where the unpinned ref is supply-chain surface.
67
+ - Do not infer state drift, plan outcomes, or "this will destroy/replace the database" from static HCL — only \`terraform plan\` evidence supports those claims. Without plan evidence, phrase the concern as an open question, not a finding.
68
+ - Missing tags and naming-convention deviations are nitpicks, not findings.
69
+ - When a deterministic scanner rule covers the same issue (trivy \`AVD-*\`, checkov \`CKV_*\`, tflint), cite the rule id in the finding — that makes it ✗→✓ verifiable downstream instead of an unverifiable reviewer opinion.
70
+
71
+ **Signal-quality bar** — a surviving candidate must still answer yes to all three: Is there a concrete failure or attack path? Is it a real risk rather than a theoretical best practice? Could the author act on it exactly as written?`;
72
+
73
+ /**
74
+ * Adversarial verification pass, spliced into the aggregation step of Review
75
+ * and IncrementalReview (between the non-anchored-concern hunt and comment
76
+ * drafting). The 0-or-2+ lens rule does not apply here — that rule buys
77
+ * independence between discovery perspectives; verification is a per-claim
78
+ * judgment with no orthogonality to purchase, so one finding = one dispatch
79
+ * is correct even when there is exactly one finding.
80
+ */
81
+ export const FINDING_VERIFICATION_PASS = `**Adversarial verification — required before posting any 🚨/⚠️ finding.** A candidate finding is a hypothesis until an independent pass has tried to kill it; your own trace is not independent, because you found it. For every candidate you intend to post at 🚨 critical or ⚠️ important, dispatch one \`${REVIEWER_AGENT_NAME}\` verification subagent — ALL of them in a single assistant turn as parallel Task tool_use blocks. One candidate = one subagent, and dispatching exactly one is fine here: the 0-or-2+ rule governs discovery lenses, where independence between perspectives is the point; verification is per-claim judgment with no orthogonality to buy. Skip verification only for:
82
+
83
+ - ℹ️ informational findings and nitpicks (post on your own judgment), and
84
+ - findings whose evidence is deterministic tool output — a scanner concern id, a failing test, a compiler/type error. Those re-verify mechanically and need no judge.
85
+
86
+ Each verification dispatch contains, in order:
87
+ - the absolute \`diffPath\` (and \`incrementalDiffPath\` when available) named verbatim — the reviewer's baked-in system prompt selects its first action on this token;
88
+ - the single finding under test: file, line, intended severity, the claim, and the evidence you collected;
89
+ - the **Finding precedents** section — plus any \`### Finding precedents — org addendum\` section from your instructions — included verbatim (the subagent cannot see your prompt);
90
+ - this charge: "Attempt to REFUTE this finding. Read the actual code — do not trust the claim's description of it. Apply the finding precedents. Report a verdict (\`confirmed\` / \`refuted\` / \`uncertain\`), a confidence score 1–10, and a 2–3 sentence justification quoting the code that decides it. When the attack or failure path is theoretical rather than demonstrated, bias toward \`refuted\`."
91
+
92
+ Set the Task \`description\` to \`verify:<file>:<line>\` so parallel verifications are distinguishable in CI logs. Asking for a verdict schema is correct here and does not violate the discovery-lens "no finding schema" discipline — the subagent is judging one claim, not exploring.
93
+
94
+ Gate on what comes back:
95
+ - \`refuted\` at confidence ≥ 7 → suppress the finding and record it for the audit trail.
96
+ - \`uncertain\`, or \`refuted\` at lower confidence → re-read the decisive code yourself; either downgrade to ℹ️ with the uncertainty stated, or suppress. Do not post it at 🚨/⚠️.
97
+ - \`confirmed\` → post it; fold the verifier's justification into the comment's technical-details block when it adds evidence.
98
+ - errored / timed out / nothing usable → retry once; if it still fails, KEEP the finding and add \`verification unavailable\` to its technical details. Fail open: a broken verifier must never silently swallow a true positive, and must never block the review.
99
+
100
+ Suppressed findings are recorded, never silently deleted — list every one in the \`Suppressed findings\` block at the bottom of the review body (shape defined in the format below): severity, \`file:line\`, the claim in a few words, the refutation in a few words. An unaudited filter eats true positives invisibly; the audit trail is what lets a human catch the filter being wrong.`;
101
+
102
+ /**
103
+ * Heading under which `fp_filtering_instructions` (the action input carrying
104
+ * org-specific FP precedents) is appended to the Review mode instructions.
105
+ * FINDING_VERIFICATION_PASS names this heading when telling the orchestrator
106
+ * what to include verbatim in each verification dispatch — the two strings
107
+ * are a contract; change them together.
108
+ */
109
+ export const FP_PRECEDENTS_ADDENDUM_HEADING = "### Finding precedents — org addendum";
110
+
111
+ /**
112
+ * Merge the §5.5 action inputs into the per-mode user instructions that
113
+ * `select_mode` appends to the mode prompt (see buildOrchestratorGuidance in
114
+ * mcp/selectMode.ts). Both land on the "Review" key — IncrementalReview
115
+ * inherits Review's instructions via modeInstructionParent. Composes with
116
+ * (never replaces) backend-provided instructions: hosted settings and
117
+ * workflow-file inputs are both repo-owner-controlled surfaces.
118
+ */
119
+ export function mergeReviewModeInstructions(
120
+ base: Record<string, string>,
121
+ inputs: { reviewInstructions?: string | undefined; fpFilteringInstructions?: string | undefined },
122
+ ): Record<string, string> {
123
+ const review = inputs.reviewInstructions?.trim();
124
+ const fp = inputs.fpFilteringInstructions?.trim();
125
+ if (!review && !fp) return base;
126
+
127
+ const sections = [base.Review, review];
128
+ if (fp) {
129
+ sections.push(
130
+ `${FP_PRECEDENTS_ADDENDUM_HEADING}\n\nApply these alongside the built-in Finding precedents, and include this whole section verbatim in every verification dispatch.\n\n${fp}`,
131
+ );
132
+ }
133
+ return { ...base, Review: sections.filter(Boolean).join("\n\n") };
134
+ }
@@ -0,0 +1,214 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, existsSync, mkdtempSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { runTerramendCli } from "#app/runCli";
6
+ import actionPackageJson from "#package.json" with { type: "json" };
7
+
8
+ vi.mock("node:child_process", async (importOriginal) => {
9
+ const actual = await importOriginal<typeof import("node:child_process")>();
10
+ return { ...actual, execFileSync: vi.fn() };
11
+ });
12
+ vi.mock("node:fs", async (importOriginal) => {
13
+ const actual = await importOriginal<typeof import("node:fs")>();
14
+ return {
15
+ ...actual,
16
+ accessSync: vi.fn(),
17
+ existsSync: vi.fn(() => true),
18
+ mkdtempSync: vi.fn(() => "/tmp/terramend-bootstrap-xyz"),
19
+ };
20
+ });
21
+
22
+ const nodeBinDir = dirname(process.execPath);
23
+
24
+ /** make accessSync succeed only for paths matching the given predicate. */
25
+ function allowExecutables(predicate: (path: string) => boolean): void {
26
+ vi.mocked(accessSync).mockImplementation((path) => {
27
+ if (!predicate(String(path))) {
28
+ throw new Error("EACCES");
29
+ }
30
+ });
31
+ }
32
+
33
+ function execCall(index = 0): {
34
+ command: string;
35
+ args: string[];
36
+ options: Record<string, unknown>;
37
+ } {
38
+ const call = vi.mocked(execFileSync).mock.calls[index];
39
+ if (!call) throw new Error(`execFileSync call ${index} missing`);
40
+ return {
41
+ command: String(call[0]),
42
+ args: (call[1] ?? []) as string[],
43
+ options: (call[2] ?? {}) as Record<string, unknown>,
44
+ };
45
+ }
46
+
47
+ beforeEach(() => {
48
+ allowExecutables(() => true);
49
+ // pin the Windows executable-extension list so candidate paths are deterministic
50
+ vi.stubEnv("PATHEXT", ".CMD");
51
+ vi.stubEnv("GITHUB_WORKSPACE", "");
52
+ vi.stubEnv("GITHUB_ACTION_REF", "");
53
+ vi.stubEnv("GITHUB_ACTION_REPOSITORY", "");
54
+ vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "");
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.unstubAllEnvs();
59
+ vi.restoreAllMocks();
60
+ vi.clearAllMocks();
61
+ });
62
+
63
+ describe("runTerramendCli – npx bootstrap path", () => {
64
+ it("runs the exact-pinned npm package via npx in a fresh tmpdir", () => {
65
+ runTerramendCli({ cliArgs: ["gha", "token"] });
66
+
67
+ const { command, args, options } = execCall();
68
+ expect(command).toContain("npx");
69
+ expect(args).toEqual(["--yes", `terramend@${actionPackageJson.version}`, "gha", "token"]);
70
+ expect(options.cwd).toBe("/tmp/terramend-bootstrap-xyz");
71
+ expect(mkdtempSync).toHaveBeenCalledWith(expect.stringContaining("terramend-bootstrap-"));
72
+
73
+ const env = options.env as NodeJS.ProcessEnv;
74
+ expect(env.npm_config_registry).toBe("https://registry.npmjs.org");
75
+ expect(env.COREPACK_NPM_REGISTRY).toBe("https://registry.npmjs.org");
76
+ expect(env.npm_config_min_release_age).toBe("0");
77
+ expect(env.pnpm_config_minimum_release_age).toBe("0");
78
+ expect(env.PATH).toContain(nodeBinDir);
79
+ });
80
+
81
+ it("falls back to corepack pnpm dlx when npx is missing", () => {
82
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
83
+ allowExecutables((path) => path.includes("corepack"));
84
+
85
+ runTerramendCli({ cliArgs: ["gha"] });
86
+
87
+ expect(warnSpy).toHaveBeenCalledWith("» npx not found, using corepack pnpm dlx");
88
+ const { command, args } = execCall();
89
+ expect(command).toContain("corepack");
90
+ expect(args).toEqual(["pnpm", "dlx", `terramend@${actionPackageJson.version}`, "gha"]);
91
+ });
92
+
93
+ it("throws when neither npx nor corepack can be found", () => {
94
+ allowExecutables(() => false);
95
+
96
+ expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow(
97
+ /could not find npx or corepack on PATH/,
98
+ );
99
+ expect(execFileSync).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it("ignores PATH entries inside the customer workspace and relative entries", () => {
103
+ const workspace = join(nodeBinDir, "workspace-checkout");
104
+ vi.stubEnv("GITHUB_WORKSPACE", workspace);
105
+ vi.stubEnv("PATH", `${join(workspace, "bin")}${process.platform === "win32" ? ";" : ":"}bin`);
106
+ // every candidate is "accessible" — only the untrusted-path filter can reject
107
+ allowExecutables((path) => path.startsWith(workspace) || !path.includes(nodeBinDir));
108
+
109
+ expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow(
110
+ /could not find npx or corepack on PATH/,
111
+ );
112
+ });
113
+ });
114
+
115
+ describe("runTerramendCli – local CLI path", () => {
116
+ it("runs the checked-out cli.ts when TERRAMEND_FORCE_LOCAL_CLI=1", () => {
117
+ vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "1");
118
+
119
+ runTerramendCli({ cliArgs: ["gha"] });
120
+
121
+ const { command, args, options } = execCall();
122
+ expect(command).toBe(process.execPath);
123
+ expect(args).toEqual(["cli.ts", "gha"]);
124
+ expect(String(options.cwd)).toContain("src");
125
+ });
126
+
127
+ it("runs locally when the action ref is main on terramend/terramend", () => {
128
+ vi.stubEnv("GITHUB_ACTION_REF", "main");
129
+ vi.stubEnv("GITHUB_ACTION_REPOSITORY", "terramend/terramend");
130
+
131
+ runTerramendCli({ cliArgs: ["gha"] });
132
+
133
+ expect(execCall().command).toBe(process.execPath);
134
+ });
135
+
136
+ it("installs action dependencies via corepack pnpm when node_modules is missing", () => {
137
+ vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "1");
138
+ vi.mocked(existsSync).mockReturnValue(false);
139
+
140
+ runTerramendCli({ cliArgs: ["gha"] });
141
+
142
+ expect(execFileSync).toHaveBeenCalledTimes(2);
143
+ const install = execCall(0);
144
+ expect(install.command).toContain("corepack");
145
+ expect(install.args).toEqual(["pnpm", "install", "--frozen-lockfile", "--ignore-scripts"]);
146
+ expect(execCall(1).args).toEqual(["cli.ts", "gha"]);
147
+ });
148
+
149
+ it("throws a descriptive error when corepack is required but missing", () => {
150
+ vi.stubEnv("TERRAMEND_FORCE_LOCAL_CLI", "1");
151
+ vi.mocked(existsSync).mockReturnValue(false);
152
+ allowExecutables(() => false);
153
+
154
+ expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow(
155
+ /could not find corepack on PATH \(needed to install action dependencies via pnpm\)/,
156
+ );
157
+ });
158
+ });
159
+
160
+ function mockProcessExit() {
161
+ return vi.spyOn(process, "exit").mockImplementation(((code?: number | string | null) => {
162
+ throw new Error(`process.exit:${code}`);
163
+ }) as never);
164
+ }
165
+
166
+ describe("runTerramendCli – child exit propagation", () => {
167
+ let exitSpy: ReturnType<typeof mockProcessExit>;
168
+
169
+ beforeEach(() => {
170
+ exitSpy = mockProcessExit();
171
+ });
172
+
173
+ it("propagates a numeric child exit status silently", () => {
174
+ const childError = Object.assign(new Error("Command failed"), { status: 3 });
175
+ vi.mocked(execFileSync).mockImplementationOnce(() => {
176
+ throw childError;
177
+ });
178
+
179
+ expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow("process.exit:3");
180
+ expect(exitSpy).toHaveBeenCalledWith(3);
181
+ });
182
+
183
+ it("exits 1 when the child was killed by a signal", () => {
184
+ const childError = Object.assign(new Error("Command failed"), {
185
+ status: null,
186
+ signal: "SIGTERM",
187
+ });
188
+ vi.mocked(execFileSync).mockImplementationOnce(() => {
189
+ throw childError;
190
+ });
191
+
192
+ expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow("process.exit:1");
193
+ expect(exitSpy).toHaveBeenCalledWith(1);
194
+ });
195
+
196
+ it("rethrows genuine spawn failures", () => {
197
+ vi.mocked(execFileSync).mockImplementationOnce(() => {
198
+ throw new Error("spawn ENOENT");
199
+ });
200
+
201
+ expect(() => runTerramendCli({ cliArgs: ["gha"] })).toThrow("spawn ENOENT");
202
+ expect(exitSpy).not.toHaveBeenCalled();
203
+ });
204
+
205
+ it("swallows errors and warns when swallowErrors is set", () => {
206
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
207
+ vi.mocked(execFileSync).mockImplementationOnce(() => {
208
+ throw new Error("cleanup blew up");
209
+ });
210
+
211
+ expect(() => runTerramendCli({ cliArgs: ["gha"], swallowErrors: true })).not.toThrow();
212
+ expect(warnSpy).toHaveBeenCalledWith("» terramend cleanup bootstrap failed: cleanup blew up");
213
+ });
214
+ });
package/src/runCli.ts ADDED
@@ -0,0 +1,282 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, constants, existsSync, mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import actionPackageJson from "#package.json" with { type: "json" };
7
+
8
+ interface RunTerramendCliParams {
9
+ cliArgs: string[];
10
+ swallowErrors?: boolean;
11
+ }
12
+
13
+ interface RuntimeContext {
14
+ actionRef: string | undefined;
15
+ actionRepository: string | undefined;
16
+ actionRoot: string;
17
+ nodeBinDir: string;
18
+ env: NodeJS.ProcessEnv;
19
+ }
20
+
21
+ const NPM_REGISTRY = "https://registry.npmjs.org";
22
+ // Pin the EXACT version baked into this action ref, not a `^` range. A consumer
23
+ // who SHA-pins `uses: terramend/terramend@<sha>` is pinning this file; resolving
24
+ // `^x.y.z` at runtime would silently run a newer npm publish than the pinned
25
+ // ref, so a single malicious/compromised publish would reach every consumer
26
+ // despite their pin. Exact-pinning makes the executed code match the vetted ref.
27
+ const FALLBACK_PACKAGE_SPEC = `terramend@${actionPackageJson.version}`;
28
+
29
+ function getErrorMessage(error: unknown): string {
30
+ return error instanceof Error ? error.message : String(error);
31
+ }
32
+
33
+ function canAccessExecutable(path: string): boolean {
34
+ try {
35
+ accessSync(path, constants.X_OK);
36
+ return true;
37
+ } catch {
38
+ if (process.platform !== "win32") {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ try {
44
+ accessSync(path, constants.F_OK);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ // reject PATH entries that an attacker can plausibly write to before terramend
52
+ // runs. specifically: relative entries (., bin, etc., which resolve against
53
+ // cwd), and anything inside the customer's checkout. an attacker who can land
54
+ // a malicious `npx` in the repo and prepend `$GITHUB_WORKSPACE/bin` to
55
+ // `GITHUB_PATH` from a prior workflow step would otherwise get full code
56
+ // execution under our action token.
57
+ //
58
+ // on Windows the filesystem is case-insensitive but `resolve()` preserves
59
+ // input case, so we lowercase both sides before comparing — otherwise an
60
+ // attacker can bypass the filter by varying the case of GITHUB_WORKSPACE in
61
+ // their injected PATH entry (`d:\a\repo` vs `D:\a\repo`).
62
+ function normalizePathForCompare(path: string): string {
63
+ return process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path);
64
+ }
65
+
66
+ function isUntrustedPathEntry(entry: string, untrustedRoots: string[]): boolean {
67
+ if (!isAbsolute(entry)) return true;
68
+ const normalized = normalizePathForCompare(entry);
69
+ for (const root of untrustedRoots) {
70
+ if (normalized === root) return true;
71
+ if (normalized.startsWith(root + sep)) return true;
72
+ }
73
+ return false;
74
+ }
75
+
76
+ function getUntrustedPathRoots(env: NodeJS.ProcessEnv): string[] {
77
+ const roots: string[] = [];
78
+ const workspace = env.GITHUB_WORKSPACE;
79
+ if (workspace && isAbsolute(workspace)) roots.push(normalizePathForCompare(workspace));
80
+ return roots;
81
+ }
82
+
83
+ function resolveExecutable(params: { command: string; env: NodeJS.ProcessEnv }): string | null {
84
+ const pathValue = params.env.PATH ?? "";
85
+ const untrustedRoots = getUntrustedPathRoots(params.env);
86
+ const pathEntries = pathValue
87
+ .split(delimiter)
88
+ .filter(Boolean)
89
+ .filter((entry) => !isUntrustedPathEntry(entry, untrustedRoots));
90
+ const extensions =
91
+ process.platform === "win32"
92
+ ? (params.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
93
+ : [""];
94
+
95
+ for (const pathEntry of pathEntries) {
96
+ for (const extension of extensions) {
97
+ const candidate = join(pathEntry, `${params.command}${extension.toLowerCase()}`);
98
+ if (canAccessExecutable(candidate)) {
99
+ return candidate;
100
+ }
101
+ }
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ function createRuntimeContext(): RuntimeContext {
108
+ const actionRoot = dirname(fileURLToPath(import.meta.url));
109
+ const nodeBinDir = dirname(process.execPath);
110
+ const env: NodeJS.ProcessEnv = { ...process.env };
111
+ env.npm_config_registry = NPM_REGISTRY;
112
+ env.COREPACK_NPM_REGISTRY = NPM_REGISTRY;
113
+ // bypass customer-side release-age gates (npm's `min-release-age`, pnpm's
114
+ // `minimumReleaseAge`) so our bootstrap can resolve the latest publish.
115
+ // terramend's npm version is server-stamped from a SHA-pinned action ref the
116
+ // customer already vets at the action layer — not a customer-vetted dep, so
117
+ // the gate is the wrong affordance here. env beats .npmrc in both tools.
118
+ // npm uses `npm_config_*`; pnpm v11+ requires `pnpm_config_*` (the v10→v11
119
+ // migration renamed the prefix). tracked: #713
120
+ env.npm_config_min_release_age = "0";
121
+ env.pnpm_config_minimum_release_age = "0";
122
+ const currentPath = process.env.PATH ?? "";
123
+ env.PATH = currentPath ? `${nodeBinDir}${delimiter}${currentPath}` : nodeBinDir;
124
+
125
+ return {
126
+ actionRef: process.env.GITHUB_ACTION_REF,
127
+ actionRepository: process.env.GITHUB_ACTION_REPOSITORY,
128
+ actionRoot,
129
+ nodeBinDir,
130
+ env,
131
+ };
132
+ }
133
+
134
+ // $GITHUB_WORKSPACE is the customer's repo. running `npx --yes terramend@…`
135
+ // there makes npm read THEIR `package.json` first, which on npm v11+ enforces
136
+ // `devEngines.packageManager` and aborts the bootstrap with EBADDEVENGINES
137
+ // before the agent ever boots. our bootstrap doesn't need anything from the
138
+ // customer's tree — a freshly-created tmpdir is package.json-free and
139
+ // parent-less, so npm walks up to `/` finding nothing. see #837.
140
+ //
141
+ // `mkdtempSync` (vs raw `tmpdir()`): `$TMPDIR` is overridable from a prior
142
+ // `$GITHUB_ENV` step, and a customer-authored or compromised prior step
143
+ // could plant `node_modules/terramend/` in the resolved tmpdir to hijack
144
+ // `npx --yes terramend@<version>` resolution. a fresh per-invocation
145
+ // subdirectory is mode 0700 and not pre-writable by anything earlier in
146
+ // the job.
147
+ function runCommand(params: { context: RuntimeContext; command: string; args: string[] }): void {
148
+ execFileSync(params.command, params.args, {
149
+ cwd: mkdtempSync(join(tmpdir(), "terramend-bootstrap-")),
150
+ stdio: "inherit",
151
+ env: params.context.env,
152
+ });
153
+ }
154
+
155
+ // resolve a launcher binary by walking PATH (which already has the action
156
+ // runtime's nodeBinDir prepended). some hosted Node 24 runner pools ship
157
+ // `node` at `externals/node24/bin/node` without the sibling `npx`/`corepack`,
158
+ // so a hardcoded sibling path can't be relied on — fall back to whatever the
159
+ // runner image provides on PATH.
160
+ function requireExecutable(params: {
161
+ context: RuntimeContext;
162
+ command: string;
163
+ purpose: string;
164
+ }): string {
165
+ const resolved = resolveExecutable({ command: params.command, env: params.context.env });
166
+ if (!resolved) {
167
+ throw new Error(
168
+ `could not find ${params.command} on PATH (needed to ${params.purpose}); ` +
169
+ `runtime PATH was: ${params.context.env.PATH ?? "<empty>"}`,
170
+ );
171
+ }
172
+ return resolved;
173
+ }
174
+
175
+ function runPackageCli(context: RuntimeContext, packageSpec: string, cliArgs: string[]): void {
176
+ const npxPath = resolveExecutable({ command: "npx", env: context.env });
177
+ if (npxPath) {
178
+ runCommand({ context, command: npxPath, args: ["--yes", packageSpec, ...cliArgs] });
179
+ return;
180
+ }
181
+
182
+ const corepackPath = resolveExecutable({ command: "corepack", env: context.env });
183
+ if (corepackPath) {
184
+ console.warn("» npx not found, using corepack pnpm dlx");
185
+ runCommand({ context, command: corepackPath, args: ["pnpm", "dlx", packageSpec, ...cliArgs] });
186
+ return;
187
+ }
188
+
189
+ throw new Error(
190
+ `could not find npx or corepack on PATH to run ${packageSpec}; ` +
191
+ `runtime PATH was: ${context.env.PATH ?? "<empty>"}`,
192
+ );
193
+ }
194
+
195
+ function ensureActionDependencies(context: RuntimeContext): void {
196
+ const nodeModulesPath = join(context.actionRoot, "node_modules");
197
+ if (existsSync(nodeModulesPath)) {
198
+ return;
199
+ }
200
+
201
+ const corepackPath = requireExecutable({
202
+ context,
203
+ command: "corepack",
204
+ purpose: "install action dependencies via pnpm",
205
+ });
206
+ const adjacentCorepack = join(
207
+ context.nodeBinDir,
208
+ process.platform === "win32" ? "corepack.cmd" : "corepack",
209
+ );
210
+ if (corepackPath !== adjacentCorepack) {
211
+ // bad-runner case: GitHub's externals/node24/bin/ is missing the corepack
212
+ // sibling, so we resolved via PATH instead. logging this lets us correlate
213
+ // bootstrap path to runner pool when validating the fix.
214
+ console.warn(
215
+ `» nodeBinDir corepack missing (${adjacentCorepack}); using PATH-resolved ${corepackPath}`,
216
+ );
217
+ }
218
+ execFileSync(corepackPath, ["pnpm", "install", "--frozen-lockfile", "--ignore-scripts"], {
219
+ cwd: context.actionRoot,
220
+ stdio: "inherit",
221
+ env: context.env,
222
+ });
223
+ }
224
+
225
+ function runLocalCli(context: RuntimeContext, cliArgs: string[]): void {
226
+ ensureActionDependencies(context);
227
+ execFileSync(process.execPath, ["cli.ts", ...cliArgs], {
228
+ cwd: context.actionRoot,
229
+ stdio: "inherit",
230
+ env: context.env,
231
+ });
232
+ }
233
+
234
+ function runTerramendCliInner(context: RuntimeContext, cliArgs: string[]): void {
235
+ if (process.env.TERRAMEND_FORCE_LOCAL_CLI === "1") {
236
+ runLocalCli(context, cliArgs);
237
+ return;
238
+ }
239
+
240
+ if (context.actionRef === "main" && context.actionRepository === "terramend/terramend") {
241
+ runLocalCli(context, cliArgs);
242
+ return;
243
+ }
244
+
245
+ runPackageCli(context, FALLBACK_PACKAGE_SPEC, cliArgs);
246
+ }
247
+
248
+ // the inner CLI and the bootstrap install run with `stdio: "inherit"`, so on a
249
+ // non-zero exit they've already printed their own `##[error]` line. node turns
250
+ // that exit into a thrown `Error: Command failed…`; letting it bubble crashes
251
+ // the outer bootstrap with a `node:internal/errors` stack trace that buries the
252
+ // real failure (#862, #867). propagate the child's exit code silently instead;
253
+ // only genuine spawn failures (ENOENT, missing npx, …) still surface.
254
+ function propagateChildExit(error: unknown): never {
255
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
256
+ process.exit(error.status);
257
+ }
258
+ if (error instanceof Error && "signal" in error && error.signal != null) {
259
+ process.exit(1);
260
+ }
261
+ throw error;
262
+ }
263
+
264
+ export function runTerramendCli(params: RunTerramendCliParams): void {
265
+ const context = createRuntimeContext();
266
+
267
+ if (params.swallowErrors) {
268
+ try {
269
+ runTerramendCliInner(context, params.cliArgs);
270
+ } catch (error) {
271
+ console.warn(`» terramend cleanup bootstrap failed: ${getErrorMessage(error)}`);
272
+ // best-effort cleanup
273
+ }
274
+ return;
275
+ }
276
+
277
+ try {
278
+ runTerramendCliInner(context, params.cliArgs);
279
+ } catch (error) {
280
+ propagateChildExit(error);
281
+ }
282
+ }