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,292 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import type { AgentId } from "#app/external";
3
+ import type { ToolState } from "#app/toolState";
4
+ import { log } from "#app/utils/cli";
5
+ import type { ResolvedInstructions } from "#app/utils/instructions";
6
+ import type { ResolvedPayload } from "#app/utils/payload";
7
+ import type { TodoTracker } from "#app/utils/todoTracking";
8
+
9
+ // maximum number of stderr lines to keep in the rolling buffer during agent execution
10
+ export const MAX_STDERR_LINES = 20;
11
+
12
+ // ── post-run retry loop ────────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * how many times the post-run loop may resume the agent to fix a dirty tree
16
+ * or a failing stop hook before giving up.
17
+ */
18
+ export const MAX_POST_RUN_RETRIES = 3;
19
+
20
+ export function getGitStatus(): string {
21
+ try {
22
+ return execFileSync("git", ["status", "--porcelain"], {
23
+ encoding: "utf-8",
24
+ timeout: 10_000,
25
+ }).trim();
26
+ } catch {
27
+ return "";
28
+ }
29
+ }
30
+
31
+ export function buildCommitPrompt(status: string): string {
32
+ return [
33
+ `UNCOMMITTED CHANGES — the working tree is dirty. push all changes to a pull request (new or existing). \`git status\` must be clean before you finish.`,
34
+ "",
35
+ "```",
36
+ status,
37
+ "```",
38
+ ].join("\n");
39
+ }
40
+
41
+ export interface StopHookFailure {
42
+ exitCode: number;
43
+ output: string;
44
+ }
45
+
46
+ export interface SummaryStale {
47
+ /** absolute path to the seeded snapshot file the agent was meant to edit. */
48
+ filePath: string;
49
+ }
50
+
51
+ export interface PostRunIssues {
52
+ stopHook?: StopHookFailure;
53
+ dirtyTree?: string;
54
+ /** populated when the rolling PR summary file is byte-identical to its
55
+ * seed, i.e. the agent never touched it. soft gate — nudges once via a
56
+ * resume turn but never fails the run, parallel to dirtyTree semantics. */
57
+ summaryStale?: SummaryStale;
58
+ /**
59
+ * populated when the agent selected a review mode but the post-run check
60
+ * over toolState shows neither a `create_pull_request_review` submission
61
+ * nor a final `report_progress` write happened. derived inline from
62
+ * `toolState.selectedMode` + `toolState.review` + `toolState.finalSummaryWritten`
63
+ * via {@link getUnsubmittedReview} — no parallel toolState flag is stored.
64
+ * carries the mode name so the resume prompt can reference it. handled like
65
+ * `stopHook`: nudge via resume, hard-fail if still unsatisfied after
66
+ * `MAX_POST_RUN_RETRIES`.
67
+ */
68
+ unsubmittedReview?: "Review" | "IncrementalReview";
69
+ }
70
+
71
+ export function hasPostRunIssues(issues: PostRunIssues): boolean {
72
+ return (
73
+ issues.stopHook !== undefined ||
74
+ issues.dirtyTree !== undefined ||
75
+ issues.summaryStale !== undefined ||
76
+ issues.unsubmittedReview !== undefined
77
+ );
78
+ }
79
+
80
+ /**
81
+ * token/cost usage data from a single agent run.
82
+ *
83
+ * NOTE on semantics: `inputTokens` here is the *total* billable input for the
84
+ * run — non-cached input + cache read + cache write — matching the per-agent
85
+ * SDK conventions. This is what gets persisted to `WorkflowRun.inputTokens`.
86
+ *
87
+ * The stdout token table and markdown step summary display a different "Input"
88
+ * column that shows only the non-cached portion (derivable as
89
+ * `inputTokens - cacheReadTokens - cacheWriteTokens`) so humans can see the
90
+ * cache hit ratio at a glance. Dashboards that query `WorkflowRun.inputTokens`
91
+ * directly are seeing the full total, not the log column.
92
+ */
93
+ export interface AgentUsage {
94
+ agent: string;
95
+ /** full billable input: non-cached + cache read + cache write */
96
+ inputTokens: number;
97
+ outputTokens: number;
98
+ cacheReadTokens?: number | undefined;
99
+ cacheWriteTokens?: number | undefined;
100
+ costUsd?: number | undefined;
101
+ }
102
+
103
+ export interface AgentToolUseEvent {
104
+ toolName: string;
105
+ input: unknown;
106
+ }
107
+
108
+ /**
109
+ * Result returned by agent execution
110
+ */
111
+ export interface AgentResult {
112
+ success: boolean;
113
+ output?: string | undefined;
114
+ error?: string | undefined;
115
+ metadata?: Record<string, unknown>;
116
+ usage?: AgentUsage | undefined;
117
+ }
118
+
119
+ /**
120
+ * Env var that carries the per-run MCP bearer token to BOTH agent harnesses,
121
+ * which reference it from their MCP client config so the on-disk/in-config form
122
+ * holds only a placeholder, never the raw token:
123
+ * - Claude Code expands `${TERRAMEND_MCP_TOKEN}` in .mcp.json headers.
124
+ * - opencode expands `{env:TERRAMEND_MCP_TOKEN}` in remote-MCP headers.
125
+ * The `_TOKEN` suffix also makes filterEnv() strip it from the MCP shell
126
+ * sandbox. Set only on the agent/server spawn env (never process.env), so a
127
+ * co-located dependency-install subprocess never inherits it. See
128
+ * ToolContext.mcpServerToken and gateServer.ts for the same pattern.
129
+ */
130
+ export const MCP_SERVER_TOKEN_ENV = "TERRAMEND_MCP_TOKEN";
131
+
132
+ /**
133
+ * Context passed to agent.run() and threaded through the post-run loop.
134
+ *
135
+ * design rule: this is the single object that flows through the harness and
136
+ * downstream utilities by reference. derived predicates (e.g.
137
+ * `getUnsubmittedReview`), tmpfile paths, and seed bytes live on
138
+ * `toolState` — read them at the call site, do not duplicate them onto this
139
+ * interface. utilities that need run state should accept `ctx` whole, not
140
+ * destructure a narrow subset.
141
+ */
142
+ export interface AgentRunContext {
143
+ payload: ResolvedPayload;
144
+ resolvedModel?: string | undefined;
145
+ mcpServerUrl: string;
146
+ /** per-run bearer token the agent's MCP client presents to mcpServerUrl. See
147
+ * ToolContext.mcpServerToken — delivered to the client config out-of-band so
148
+ * it never lands in a readable file. */
149
+ mcpServerToken: string;
150
+ tmpdir: string;
151
+ /** harness-owned secret paths that agent filesystem tools must never read. */
152
+ secretDenyPaths?: string[] | undefined;
153
+ instructions: ResolvedInstructions;
154
+ todoTracker?: TodoTracker | undefined;
155
+ /**
156
+ * user-configured stop hook script. runs after the agent finishes each
157
+ * attempt; non-zero exit resumes the agent with the hook output as
158
+ * guidance. null when the repo has no stop hook configured.
159
+ */
160
+ stopScript?: string | null | undefined;
161
+ /**
162
+ * mutable per-run state shared with the MCP server (by reference). post-run
163
+ * gates read fresh values from it after each agent attempt — `summaryFilePath`,
164
+ * `summarySeed`, `selectedMode`, `review`, `finalSummaryWritten`,
165
+ * `hadProgressComment` are all consulted by `collectPostRunIssues`. see
166
+ * `action/toolState.ts` for the literal-state design rule.
167
+ */
168
+ toolState: ToolState;
169
+ /**
170
+ * called synchronously when the agent subprocess is killed for inner
171
+ * activity timeout. lets main.ts tear down shared resources (MCP HTTP
172
+ * server) so lingering SSE reconnects don't keep the outer timer alive.
173
+ */
174
+ onActivityTimeout?: (() => void) | undefined;
175
+ onToolUse?: ((event: AgentToolUseEvent) => void) | undefined;
176
+ /**
177
+ * Terramend API JWT scoped to this run. agents only need this when they
178
+ * have to write state back to Terramend mid-run (today: opencode.ts uses
179
+ * it to seed the post-hook's writeback envelope for Codex auth refresh).
180
+ * empty string when the run wasn't context-resolved (e.g. local dry-runs).
181
+ */
182
+ apiToken: string;
183
+ }
184
+
185
+ export interface Agent {
186
+ name: AgentId;
187
+ install: (token?: string) => Promise<string>;
188
+ run: (ctx: AgentRunContext) => Promise<AgentResult>;
189
+ }
190
+
191
+ export const agent = (input: Agent): Agent => {
192
+ return {
193
+ ...input,
194
+ run: async (ctx: AgentRunContext): Promise<AgentResult> => {
195
+ log.debug(`» payload: ${JSON.stringify(ctx.payload, null, 2)}`);
196
+ return input.run(ctx);
197
+ },
198
+ };
199
+ };
200
+
201
+ /** format a USD cost to 4 decimal places, always showing the leading zero */
202
+ export function formatCostUsd(costUsd: number): string {
203
+ return costUsd.toFixed(4);
204
+ }
205
+
206
+ /**
207
+ * merge two AgentUsage snapshots into one running total.
208
+ *
209
+ * both agent harnesses invoke their runner multiple times per `run()` when the
210
+ * post-run retry loop kicks in (MAX_POST_RUN_RETRIES). each invocation
211
+ * produces its own AgentUsage; we sum them so downstream callers (usage
212
+ * summary, WorkflowRun persistence) see the whole session — not just the
213
+ * final retry's slice.
214
+ *
215
+ * returns `undefined` when both sides are empty so callers can short-circuit
216
+ * without a special case. zero-valued cache / cost fields are dropped to
217
+ * `undefined` for symmetry with each harness's `buildUsage`.
218
+ */
219
+ export function mergeAgentUsage(
220
+ a: AgentUsage | undefined,
221
+ b: AgentUsage | undefined,
222
+ ): AgentUsage | undefined {
223
+ // always return a fresh object — callers treat AgentUsage as immutable, and
224
+ // returning `a` / `b` directly would leak that invariant to future callers
225
+ if (!a && !b) return undefined;
226
+ if (!a) return { ...(b as AgentUsage) };
227
+ if (!b) return { ...a };
228
+ const cacheRead = (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0);
229
+ const cacheWrite = (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0);
230
+ const cost = (a.costUsd ?? 0) + (b.costUsd ?? 0);
231
+ return {
232
+ agent: a.agent,
233
+ inputTokens: a.inputTokens + b.inputTokens,
234
+ outputTokens: a.outputTokens + b.outputTokens,
235
+ cacheReadTokens: cacheRead > 0 ? cacheRead : undefined,
236
+ cacheWriteTokens: cacheWrite > 0 ? cacheWrite : undefined,
237
+ costUsd: cost > 0 ? cost : undefined,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * unified per-run token table used by every agent harness.
243
+ *
244
+ * columns are kept stable across agents and models so downstream log parsers
245
+ * (scripts/token-usage.ts, cost dashboards) only have to understand one format:
246
+ *
247
+ * Input non-cached input tokens sent this run
248
+ * Cache Read input tokens served from prompt cache (Anthropic, etc.)
249
+ * Cache Write input tokens written to prompt cache this run
250
+ * Output assistant output tokens
251
+ * Total sum of the four columns — the real billable quantity
252
+ * Cost ($) USD cost reported by the provider (only rendered when known)
253
+ *
254
+ * models that don't report prompt caching leave Cache Read / Write at 0.
255
+ * OpenCode emits per-step `part.cost` sourced from models.dev (works across
256
+ * Anthropic, OpenAI, Google, xAI, DeepSeek, Moonshot, OpenRouter, etc.);
257
+ * Claude CLI emits `total_cost_usd` on its final `result` event. pass the
258
+ * accumulated value via `costUsd` to render the Cost column.
259
+ */
260
+ export function logTokenTable(t: {
261
+ input: number;
262
+ cacheRead: number;
263
+ cacheWrite: number;
264
+ output: number;
265
+ costUsd?: number | undefined;
266
+ }): void {
267
+ const total = t.input + t.cacheRead + t.cacheWrite + t.output;
268
+ // narrow costUsd to a concrete number so the render path doesn't need a cast
269
+ const costUsd = typeof t.costUsd === "number" && t.costUsd > 0 ? t.costUsd : undefined;
270
+
271
+ const headerRow: Array<{ data: string; header: true }> = [
272
+ { data: "Input", header: true },
273
+ { data: "Cache Read", header: true },
274
+ { data: "Cache Write", header: true },
275
+ { data: "Output", header: true },
276
+ { data: "Total", header: true },
277
+ ];
278
+ const dataRow: string[] = [
279
+ String(t.input),
280
+ String(t.cacheRead),
281
+ String(t.cacheWrite),
282
+ String(t.output),
283
+ String(total),
284
+ ];
285
+
286
+ if (costUsd !== undefined) {
287
+ headerRow.push({ data: "Cost ($)", header: true });
288
+ dataRow.push(formatCostUsd(costUsd));
289
+ }
290
+
291
+ log.table([headerRow, dataRow]);
292
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { deriveSubagentModels } from "#app/agents/subagentModels";
3
+
4
+ describe("deriveSubagentModels", () => {
5
+ it("returns no override when orchestrator is undefined", () => {
6
+ expect(deriveSubagentModels(undefined)).toEqual({ reviewer: undefined });
7
+ });
8
+
9
+ it("returns no override when orchestrator slug isn't registered", () => {
10
+ expect(deriveSubagentModels("nonexistent/model")).toEqual({ reviewer: undefined });
11
+ });
12
+
13
+ describe("anthropic family — opus → sonnet", () => {
14
+ it("direct anthropic opus", () => {
15
+ expect(deriveSubagentModels("anthropic/claude-opus-4-8")).toEqual({
16
+ reviewer: "anthropic/claude-sonnet-4-6",
17
+ });
18
+ });
19
+ it("opencode-vendored opus stays on opencode prefix", () => {
20
+ expect(deriveSubagentModels("opencode/claude-opus-4-8")).toEqual({
21
+ reviewer: "opencode/claude-sonnet-4-6",
22
+ });
23
+ });
24
+ it("openrouter-anthropic-opus-via-anthropic-direct hits anthropic alias's openRouterResolve", () => {
25
+ // both the anthropic alias and the opencode alias have the same
26
+ // openRouterResolve. first-match-wins by alias declaration order
27
+ // (anthropic declared first in providers).
28
+ expect(deriveSubagentModels("openrouter/anthropic/claude-opus-4.8")).toEqual({
29
+ reviewer: "openrouter/anthropic/claude-sonnet-4.6",
30
+ });
31
+ });
32
+ it("sonnet has no further downshift", () => {
33
+ expect(deriveSubagentModels("anthropic/claude-sonnet-4-6")).toEqual({ reviewer: undefined });
34
+ expect(deriveSubagentModels("opencode/claude-sonnet-4-6")).toEqual({ reviewer: undefined });
35
+ });
36
+ it("haiku has no downshift", () => {
37
+ expect(deriveSubagentModels("anthropic/claude-haiku-4-5")).toEqual({ reviewer: undefined });
38
+ });
39
+ });
40
+
41
+ describe("openai family", () => {
42
+ it("gpt-pro → gpt (direct)", () => {
43
+ expect(deriveSubagentModels("openai/gpt-5.5-pro")).toEqual({ reviewer: "openai/gpt-5.5" });
44
+ });
45
+ it("gpt → gpt-5.4 (direct)", () => {
46
+ expect(deriveSubagentModels("openai/gpt-5.5")).toEqual({ reviewer: "openai/gpt-5.4" });
47
+ });
48
+ it("gpt → gpt-5.4 (opencode-vendored)", () => {
49
+ expect(deriveSubagentModels("opencode/gpt-5.5")).toEqual({ reviewer: "opencode/gpt-5.4" });
50
+ });
51
+ it("gpt-pro → gpt (openrouter)", () => {
52
+ expect(deriveSubagentModels("openrouter/openai/gpt-5.5-pro")).toEqual({
53
+ reviewer: "openrouter/openai/gpt-5.5",
54
+ });
55
+ });
56
+ it("gpt → gpt-5.4 (openrouter)", () => {
57
+ expect(deriveSubagentModels("openrouter/openai/gpt-5.5")).toEqual({
58
+ reviewer: "openrouter/openai/gpt-5.4",
59
+ });
60
+ });
61
+ it("gpt-5.4 itself (the hidden subagent target) has no further downshift", () => {
62
+ expect(deriveSubagentModels("openai/gpt-5.4")).toEqual({ reviewer: undefined });
63
+ });
64
+ it("gpt-mini has no downshift", () => {
65
+ expect(deriveSubagentModels("openai/gpt-5.4-mini")).toEqual({ reviewer: undefined });
66
+ });
67
+ });
68
+
69
+ describe("google (gemini) — inherit (Pro for both orchestrator and lenses)", () => {
70
+ // pro → flash was a meaningful capability cliff (Flash missed catastrophic
71
+ // cross-file bugs the v4 e2e test surfaced); Pro is cost-effective enough
72
+ // to keep on for lenses too. Google has no in-between tier.
73
+ it("direct google pro inherits", () => {
74
+ expect(deriveSubagentModels("google/gemini-3.1-pro-preview")).toEqual({
75
+ reviewer: undefined,
76
+ });
77
+ });
78
+ it("opencode-vendored gemini-pro inherits", () => {
79
+ expect(deriveSubagentModels("opencode/gemini-3.1-pro")).toEqual({
80
+ reviewer: undefined,
81
+ });
82
+ });
83
+ it("openrouter gemini-pro inherits", () => {
84
+ expect(deriveSubagentModels("openrouter/google/gemini-3.1-pro-preview")).toEqual({
85
+ reviewer: undefined,
86
+ });
87
+ });
88
+ it("flash has no downshift", () => {
89
+ expect(deriveSubagentModels("google/gemini-3-flash-preview")).toEqual({
90
+ reviewer: undefined,
91
+ });
92
+ });
93
+ });
94
+
95
+ describe("providers / models without a subagentModel — inherit", () => {
96
+ it("xai grok (already cheap flagship)", () => {
97
+ expect(deriveSubagentModels("xai/grok-4.3")).toEqual({ reviewer: undefined });
98
+ });
99
+ it("deepseek", () => {
100
+ expect(deriveSubagentModels("deepseek/deepseek-v4-pro")).toEqual({ reviewer: undefined });
101
+ });
102
+ it("moonshot kimi", () => {
103
+ expect(deriveSubagentModels("moonshotai/kimi-k2.6")).toEqual({ reviewer: undefined });
104
+ });
105
+ it("opencode big-pickle", () => {
106
+ expect(deriveSubagentModels("opencode/big-pickle")).toEqual({ reviewer: undefined });
107
+ });
108
+ it("legacy fallback aliases (gpt-codex, deepseek-reasoner)", () => {
109
+ expect(deriveSubagentModels("openai/gpt-5.3-codex")).toEqual({ reviewer: undefined });
110
+ expect(deriveSubagentModels("deepseek/deepseek-reasoner")).toEqual({ reviewer: undefined });
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,40 @@
1
+ import { modelAliases } from "#app/models";
2
+
3
+ /**
4
+ * Derive a cheaper subagent model override from the orchestrator's resolved
5
+ * model spec.
6
+ *
7
+ * This is a pure registry lookup: every alias in `action/models.ts` declares
8
+ * its own `subagentModel` (alias key in the same provider). At runtime we
9
+ * reverse-lookup the orchestrator's resolved slug to find the alias that
10
+ * produced it, follow the `subagentModel` pointer, and return the target
11
+ * alias's resolve / openRouterResolve depending on which route the
12
+ * orchestrator was using.
13
+ *
14
+ * Returns `{ reviewer: undefined }` when the orchestrator's alias has no
15
+ * `subagentModel` (e.g. it's already at a sufficiently cheap tier, or its
16
+ * provider doesn't have a clean cheaper-but-capable sibling). See models.ts
17
+ * for the wiring + per-provider rationale.
18
+ */
19
+ export function deriveSubagentModels(orchestratorSpec: string | undefined): {
20
+ reviewer: string | undefined;
21
+ } {
22
+ if (!orchestratorSpec) return { reviewer: undefined };
23
+
24
+ // Reverse-lookup. The same resolve string appears in only one alias
25
+ // (within its provider), so first match wins. We track which field
26
+ // matched (resolve vs openRouterResolve) so we can pick the same field
27
+ // off the subagent target — keeping the orchestrator's route consistent.
28
+ for (const source of modelAliases) {
29
+ const matchedDirect = source.resolve === orchestratorSpec;
30
+ const matchedOR = source.openRouterResolve === orchestratorSpec;
31
+ if (!matchedDirect && !matchedOR) continue;
32
+ if (!source.subagentModel) return { reviewer: undefined };
33
+ const target = modelAliases.find((a) => a.slug === source.subagentModel);
34
+ if (!target) return { reviewer: undefined };
35
+ const reviewer = matchedOR ? target.openRouterResolve : target.resolve;
36
+ return { reviewer };
37
+ }
38
+
39
+ return { reviewer: undefined };
40
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const claudeSource = readFileSync(join(__dirname, "claude.ts"), "utf-8");
6
+ const opencodeSharedSource = readFileSync(join(__dirname, "opencodeShared.ts"), "utf-8");
7
+ const opencodeSource = readFileSync(join(__dirname, "opencode.ts"), "utf-8");
8
+
9
+ /**
10
+ * The Claude Code `--agents` JSON and OpenCode `agent` config block are the
11
+ * only places where per-subagent model overrides take effect. They're built
12
+ * by string-only helpers we don't export, so this test reads the source and
13
+ * asserts the literal model strings + agent names are wired in. A regression
14
+ * here means the next review run silently runs lenses on Opus instead of
15
+ * Sonnet.
16
+ */
17
+ describe("subagent registration source asserts", () => {
18
+ describe("claude.ts buildAgentsJson", () => {
19
+ it("registers review with sonnet model", () => {
20
+ expect(claudeSource).toMatch(
21
+ /\[REVIEWER_AGENT_NAME\]:\s*\{[^}]*model:\s*"claude-sonnet-4-6"/s,
22
+ );
23
+ });
24
+ it("imports the reviewer name constant", () => {
25
+ expect(claudeSource).toMatch(/REVIEWER_AGENT_NAME/);
26
+ });
27
+ });
28
+
29
+ describe("opencodeShared.ts buildReviewerAgentConfig", () => {
30
+ it("registers review with mode: subagent", () => {
31
+ expect(opencodeSharedSource).toMatch(/\[REVIEWER_AGENT_NAME\]:[^}]*mode:\s*"subagent"/s);
32
+ });
33
+ it("uses deriveSubagentModels for the reviewer model override", () => {
34
+ expect(opencodeSharedSource).toMatch(/deriveSubagentModels\(/);
35
+ expect(opencodeSharedSource).toMatch(/overrides\.reviewer/);
36
+ });
37
+ it("v2 runner passes orchestrator model to buildReviewerAgentConfig", () => {
38
+ expect(opencodeSource).toMatch(/buildReviewerAgentConfig\(model\)/);
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Single source of truth for MCP tools subagents are forbidden from calling.
3
+ *
4
+ * Subagents share the orchestrator's in-process git working tree, `toolState`,
5
+ * progress comment, and run-scoped pr/branch context. A subagent that calls
6
+ * `checkout_pr` switches the orchestrator's HEAD; one that calls `push_branch`
7
+ * pushes whatever the orchestrator happens to have committed. The 2026-05-18
8
+ * `zed-industries/cloud` incident hit exactly this: a `reviewfrog` lens
9
+ * dispatched `checkout_pr({2582})` mid-review, the orchestrator's next push
10
+ * clobbered an unrelated engineer's branch. PR #796 added runtime backstops
11
+ * inside `checkout_pr`/`push_branch`; this list is the upstream gate that
12
+ * stops the call from ever reaching MCP when it originates from a subagent.
13
+ *
14
+ * The gate is enforced at two pre-tool hooks:
15
+ * - opencode: `tool.execute.before` (action/agents/opencodePlugin.ts)
16
+ * - claude: `PreToolUse` settings hook (action/agents/claudePretoolGate.ts)
17
+ *
18
+ * Names are stored in their canonical bare form (the FastMCP tool `name`
19
+ * field). Each runtime presents them with a different prefix:
20
+ * - claude: `mcp__terramend__<name>`
21
+ * - opencode: `terramend_<name>`
22
+ * The hooks strip those prefixes before comparing.
23
+ *
24
+ * Read-only MCP tools (`get_*`, `list_*`, `git_fetch`, `get_check_suite_logs`,
25
+ * `await_dependency_installation`, etc.) and the `git`/`shell` tools stay off
26
+ * this list — denying them would make review work impossible. The reviewer system prompt
27
+ * (`action/agents/reviewer.ts`) already forbids state-changing shell/git
28
+ * subcommands as a prose constraint; this list is the belt-and-suspenders
29
+ * machine fence for the high-stakes mutations we can identify by name alone.
30
+ *
31
+ * When adding a state-changing MCP tool to `action/mcp/server.ts`, add its
32
+ * canonical name here too. Inclusions justified inline.
33
+ */
34
+ export const SUBAGENT_DENIED_TOOLS = [
35
+ // working-tree mutation: switches HEAD onto pr-N and registers a push remote
36
+ "checkout_pr",
37
+
38
+ // remote mutation: pushes commits / branches / tags / deletes a branch
39
+ "push_branch",
40
+ "push_tags",
41
+ "delete_branch",
42
+
43
+ // GitHub PR state mutation
44
+ "create_pull_request",
45
+ "update_pull_request_body",
46
+ // §27 — closes a (remediation) PR; a state-changing PR mutation.
47
+ "close_pull_request",
48
+
49
+ // GitHub comment / issue mutation
50
+ "create_issue",
51
+ "create_issue_comment",
52
+ "edit_issue_comment",
53
+ "reply_to_review_comment",
54
+
55
+ // GitHub review state mutation
56
+ "create_pull_request_review",
57
+ "resolve_review_thread",
58
+
59
+ // GitHub label mutation
60
+ "add_labels",
61
+
62
+ // run-state mutation: workflow output, progress comment, run mode select
63
+ "set_output",
64
+ "report_progress",
65
+ "select_mode",
66
+
67
+ // process / filesystem mutation outside the agent's intended scope
68
+ "start_dependency_installation",
69
+ "kill_background",
70
+ "upload_file",
71
+ ] as const;
72
+
73
+ export type SubagentDeniedTool = (typeof SUBAGENT_DENIED_TOOLS)[number];
74
+
75
+ /**
76
+ * Strip the runtime-specific MCP prefix from a tool name and return the
77
+ * canonical bare name (matching FastMCP's `name:` field). Returns the input
78
+ * unchanged if it doesn't carry a known prefix — keeping comparison simple
79
+ * for native (non-MCP) tools, which never appear on the deny list anyway.
80
+ */
81
+ export function stripMcpPrefix(toolName: string): string {
82
+ // claude: `mcp__terramend__checkout_pr` → `checkout_pr`
83
+ if (toolName.startsWith("mcp__terramend__")) return toolName.slice("mcp__terramend__".length);
84
+ // opencode: `terramend_checkout_pr` → `checkout_pr`
85
+ if (toolName.startsWith("terramend_")) return toolName.slice("terramend_".length);
86
+ return toolName;
87
+ }
88
+
89
+ /**
90
+ * Whether `toolName` (in any runtime's prefix style) names a tool that
91
+ * subagents must not call.
92
+ */
93
+ export function isSubagentDeniedTool(toolName: string): boolean {
94
+ const bare = stripMcpPrefix(toolName);
95
+ return (SUBAGENT_DENIED_TOOLS as readonly string[]).includes(bare);
96
+ }
97
+
98
+ /**
99
+ * Human-readable refusal surfaced to the model when a denied tool is gated.
100
+ * Phrased so a halfway-attentive subagent realises (a) the tool is denied to
101
+ * it specifically, (b) why (shared in-process state with the orchestrator),
102
+ * and (c) what to do instead (report findings; the orchestrator can call the
103
+ * tool directly).
104
+ */
105
+ export function buildSubagentDenyMessage(toolName: string): string {
106
+ const bare = stripMcpPrefix(toolName);
107
+ return (
108
+ `subagent attempted to call denied tool '${bare}'. ` +
109
+ `subagents share the orchestrator's in-process working tree and toolState; ` +
110
+ `state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, ` +
111
+ `report_progress, etc.) are reserved for the orchestrator. ` +
112
+ `report findings back to the orchestrator and let it perform the mutation.`
113
+ );
114
+ }