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,1714 @@
1
+ import { createRequire as __createRequire } from 'module'; import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __dirnameFn } from 'path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __dirnameFn(__filename);
2
+
3
+ // src/models.ts
4
+ function provider(config) {
5
+ return config;
6
+ }
7
+ var providers = {
8
+ anthropic: provider({
9
+ displayName: "Anthropic",
10
+ envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
11
+ models: {
12
+ // OpenRouter serves claude-fable-5, but models.dev's OpenRouter mirror
13
+ // hasn't indexed it yet (shipped 2026-06-09), so the catalog drift gate
14
+ // can't validate an openRouterResolve. omit it until the mirror catches
15
+ // up; direct BYOK / Claude Code resolves anthropic/claude-fable-5 fine.
16
+ "claude-fable": {
17
+ displayName: "Claude Fable",
18
+ resolve: "anthropic/claude-fable-5",
19
+ preferred: true,
20
+ subagentModel: "claude-sonnet"
21
+ },
22
+ "claude-opus": {
23
+ displayName: "Claude Opus",
24
+ resolve: "anthropic/claude-opus-4-8",
25
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
26
+ subagentModel: "claude-sonnet"
27
+ },
28
+ "claude-sonnet": {
29
+ displayName: "Claude Sonnet",
30
+ resolve: "anthropic/claude-sonnet-4-6",
31
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
32
+ },
33
+ "claude-haiku": {
34
+ displayName: "Claude Haiku",
35
+ resolve: "anthropic/claude-haiku-4-5",
36
+ openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
37
+ }
38
+ }
39
+ }),
40
+ openai: provider({
41
+ displayName: "OpenAI",
42
+ envVars: ["OPENAI_API_KEY"],
43
+ managedCredentials: ["CODEX_AUTH_JSON"],
44
+ models: {
45
+ gpt: {
46
+ displayName: "GPT",
47
+ resolve: "openai/gpt-5.5",
48
+ openRouterResolve: "openrouter/openai/gpt-5.5",
49
+ preferred: true,
50
+ subagentModel: "gpt-5.4"
51
+ },
52
+ "gpt-pro": {
53
+ displayName: "GPT Pro",
54
+ resolve: "openai/gpt-5.5-pro",
55
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro",
56
+ subagentModel: "gpt"
57
+ },
58
+ // hidden subagent target — `gpt` lenses run against this. surfacing
59
+ // it in the picker would just confuse users (it's the prior-flagship,
60
+ // and they already have `gpt` and `gpt-mini` to choose from).
61
+ "gpt-5.4": {
62
+ displayName: "GPT 5.4",
63
+ resolve: "openai/gpt-5.4",
64
+ openRouterResolve: "openrouter/openai/gpt-5.4",
65
+ hidden: true
66
+ },
67
+ "gpt-mini": {
68
+ displayName: "GPT Mini",
69
+ resolve: "openai/gpt-5.4-mini",
70
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
71
+ },
72
+ // legacy aliases — openai unified the codex line into the main GPT family
73
+ // and is shutting down every "-codex" snapshot on 2026-07-23. transparently
74
+ // upgrade existing users via the fallback chain. UI display sites resolve
75
+ // to the terminal alias's label (so dropdown trigger + PR footers show
76
+ // "GPT" / "GPT Mini", not the historical name).
77
+ "gpt-codex": {
78
+ displayName: "GPT Codex",
79
+ resolve: "openai/gpt-5.3-codex",
80
+ openRouterResolve: "openrouter/openai/gpt-5.3-codex",
81
+ fallback: "openai/gpt"
82
+ },
83
+ "gpt-codex-mini": {
84
+ displayName: "GPT Codex Mini",
85
+ resolve: "openai/gpt-5.1-codex-mini",
86
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
87
+ fallback: "openai/gpt-mini"
88
+ },
89
+ o3: {
90
+ displayName: "O3",
91
+ resolve: "openai/o3",
92
+ openRouterResolve: "openrouter/openai/o3"
93
+ }
94
+ }
95
+ }),
96
+ google: provider({
97
+ displayName: "Google",
98
+ envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
99
+ models: {
100
+ "gemini-pro": {
101
+ displayName: "Gemini Pro",
102
+ resolve: "google/gemini-3.1-pro-preview",
103
+ openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
104
+ preferred: true
105
+ // Inherit (subagents stay on Pro). Google has no in-between tier;
106
+ // dropping to Flash for review work was a meaningful capability cliff
107
+ // (Flash missed the catastrophic camelCase/snake_case mismatch in
108
+ // the v4 e2e test). Pro is cost-effective enough to use for both
109
+ // orchestrator and lenses.
110
+ },
111
+ "gemini-flash": {
112
+ displayName: "Gemini Flash",
113
+ resolve: "google/gemini-3.5-flash",
114
+ openRouterResolve: "openrouter/google/gemini-3.5-flash"
115
+ }
116
+ }
117
+ }),
118
+ xai: provider({
119
+ displayName: "xAI",
120
+ envVars: ["XAI_API_KEY"],
121
+ models: {
122
+ grok: {
123
+ displayName: "Grok",
124
+ resolve: "xai/grok-4.3",
125
+ openRouterResolve: "openrouter/x-ai/grok-4.3",
126
+ preferred: true
127
+ },
128
+ // legacy aliases — xAI retired the entire fast/code-fast line on
129
+ // 2026-05-15 (https://docs.x.ai/developers/migration/may-15-deprecation)
130
+ // and now redirects every deprecated text-model slug to grok-4.3 at
131
+ // standard pricing. fall back to the live `xai/grok` so the alias
132
+ // chain resolves to grok-4.3 for both direct-key and OpenRouter users.
133
+ "grok-fast": {
134
+ displayName: "Grok Fast",
135
+ resolve: "xai/grok-4-1-fast",
136
+ openRouterResolve: "openrouter/x-ai/grok-4.3",
137
+ fallback: "xai/grok"
138
+ },
139
+ "grok-code-fast": {
140
+ displayName: "Grok Code Fast",
141
+ resolve: "xai/grok-code-fast-1",
142
+ openRouterResolve: "openrouter/x-ai/grok-4.3",
143
+ fallback: "xai/grok"
144
+ }
145
+ }
146
+ }),
147
+ deepseek: provider({
148
+ displayName: "DeepSeek",
149
+ envVars: ["DEEPSEEK_API_KEY"],
150
+ models: {
151
+ "deepseek-pro": {
152
+ displayName: "DeepSeek Pro",
153
+ resolve: "deepseek/deepseek-v4-pro",
154
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
155
+ preferred: true
156
+ },
157
+ "deepseek-flash": {
158
+ displayName: "DeepSeek Flash",
159
+ resolve: "deepseek/deepseek-v4-flash",
160
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-flash"
161
+ },
162
+ // legacy aliases — deepseek retires these on 2026-07-24; transparently
163
+ // upgrade existing users to the v4 family via the fallback chain.
164
+ "deepseek-reasoner": {
165
+ displayName: "DeepSeek Reasoner",
166
+ resolve: "deepseek/deepseek-reasoner",
167
+ openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
168
+ fallback: "deepseek/deepseek-pro"
169
+ },
170
+ "deepseek-chat": {
171
+ displayName: "DeepSeek Chat",
172
+ resolve: "deepseek/deepseek-chat",
173
+ openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
174
+ fallback: "deepseek/deepseek-flash"
175
+ }
176
+ }
177
+ }),
178
+ moonshotai: provider({
179
+ displayName: "Moonshot AI",
180
+ envVars: ["MOONSHOT_API_KEY"],
181
+ models: {
182
+ "kimi-k2": {
183
+ displayName: "Kimi K2",
184
+ resolve: "moonshotai/kimi-k2.6",
185
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6",
186
+ preferred: true
187
+ }
188
+ }
189
+ }),
190
+ opencode: provider({
191
+ displayName: "OpenCode",
192
+ envVars: ["OPENCODE_API_KEY"],
193
+ models: {
194
+ "big-pickle": {
195
+ displayName: "Big Pickle",
196
+ resolve: "opencode/big-pickle",
197
+ preferred: true,
198
+ envVars: [],
199
+ isFree: true
200
+ },
201
+ "claude-opus": {
202
+ displayName: "Claude Opus",
203
+ resolve: "opencode/claude-opus-4-8",
204
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
205
+ subagentModel: "claude-sonnet"
206
+ },
207
+ "claude-sonnet": {
208
+ displayName: "Claude Sonnet",
209
+ resolve: "opencode/claude-sonnet-4-6",
210
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
211
+ },
212
+ "claude-haiku": {
213
+ displayName: "Claude Haiku",
214
+ resolve: "opencode/claude-haiku-4-5",
215
+ openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
216
+ },
217
+ gpt: {
218
+ displayName: "GPT",
219
+ resolve: "opencode/gpt-5.5",
220
+ openRouterResolve: "openrouter/openai/gpt-5.5",
221
+ subagentModel: "gpt-5.4"
222
+ },
223
+ "gpt-pro": {
224
+ displayName: "GPT Pro",
225
+ resolve: "opencode/gpt-5.5-pro",
226
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro",
227
+ subagentModel: "gpt"
228
+ },
229
+ // hidden subagent target — see openai provider above for context.
230
+ "gpt-5.4": {
231
+ displayName: "GPT 5.4",
232
+ resolve: "opencode/gpt-5.4",
233
+ openRouterResolve: "openrouter/openai/gpt-5.4",
234
+ hidden: true
235
+ },
236
+ "gpt-mini": {
237
+ displayName: "GPT Mini",
238
+ resolve: "opencode/gpt-5.4-mini",
239
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
240
+ },
241
+ // legacy aliases — see openai provider above for context.
242
+ "gpt-codex": {
243
+ displayName: "GPT Codex",
244
+ resolve: "opencode/gpt-5.3-codex",
245
+ openRouterResolve: "openrouter/openai/gpt-5.3-codex",
246
+ fallback: "opencode/gpt"
247
+ },
248
+ "gpt-codex-mini": {
249
+ displayName: "GPT Codex Mini",
250
+ resolve: "opencode/gpt-5.1-codex-mini",
251
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
252
+ fallback: "opencode/gpt-mini"
253
+ },
254
+ "gemini-pro": {
255
+ displayName: "Gemini Pro",
256
+ resolve: "opencode/gemini-3.1-pro",
257
+ openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
258
+ // Inherit — see google/gemini-pro for rationale.
259
+ },
260
+ "gemini-flash": {
261
+ displayName: "Gemini Flash",
262
+ resolve: "opencode/gemini-3.5-flash",
263
+ openRouterResolve: "openrouter/google/gemini-3.5-flash"
264
+ },
265
+ "kimi-k2": {
266
+ displayName: "Kimi K2",
267
+ resolve: "opencode/kimi-k2.6",
268
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6"
269
+ },
270
+ "minimax-m2.5": {
271
+ displayName: "MiniMax M2.5",
272
+ resolve: "opencode/minimax-m2.5",
273
+ openRouterResolve: "openrouter/minimax/minimax-m2.5"
274
+ },
275
+ "gpt-5-nano": {
276
+ displayName: "GPT Nano",
277
+ resolve: "opencode/gpt-5-nano",
278
+ openRouterResolve: "openrouter/openai/gpt-5-nano"
279
+ },
280
+ "mimo-v2-pro-free": {
281
+ displayName: "MiMo V2 Pro",
282
+ resolve: "opencode/mimo-v2-pro-free",
283
+ envVars: [],
284
+ isFree: true,
285
+ fallback: "opencode/big-pickle"
286
+ },
287
+ "minimax-m2.5-free": {
288
+ displayName: "MiniMax M2.5",
289
+ resolve: "opencode/minimax-m2.5-free",
290
+ envVars: [],
291
+ isFree: true,
292
+ fallback: "opencode/big-pickle",
293
+ hidden: true
294
+ }
295
+ }
296
+ }),
297
+ bedrock: provider({
298
+ displayName: "Amazon Bedrock",
299
+ envVars: ["AWS_BEARER_TOKEN_BEDROCK", "AWS_REGION", "BEDROCK_MODEL_ID"],
300
+ models: {
301
+ // single routing entry — the actual Bedrock model ID is read from
302
+ // BEDROCK_MODEL_ID at run time. see ModelRouting docs for why we
303
+ // don't catalog individual Bedrock models.
304
+ byok: {
305
+ displayName: "Amazon Bedrock",
306
+ resolve: "bedrock",
307
+ routing: "bedrock"
308
+ }
309
+ }
310
+ }),
311
+ vertex: provider({
312
+ displayName: "Google Vertex AI",
313
+ envVars: [
314
+ "VERTEX_SERVICE_ACCOUNT_JSON",
315
+ "GOOGLE_CLOUD_PROJECT",
316
+ "VERTEX_LOCATION",
317
+ "VERTEX_MODEL_ID"
318
+ ],
319
+ models: {
320
+ // single routing entry — the actual Vertex AI model ID is read from
321
+ // VERTEX_MODEL_ID at run time. see ModelRouting docs for why we don't
322
+ // catalog individual Vertex models.
323
+ byok: {
324
+ displayName: "Google Vertex AI",
325
+ resolve: "vertex",
326
+ routing: "vertex"
327
+ }
328
+ }
329
+ }),
330
+ openrouter: provider({
331
+ displayName: "OpenRouter",
332
+ envVars: ["OPENROUTER_API_KEY"],
333
+ models: {
334
+ "claude-opus": {
335
+ displayName: "Claude Opus",
336
+ resolve: "openrouter/anthropic/claude-opus-4.8",
337
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
338
+ preferred: true,
339
+ subagentModel: "claude-sonnet"
340
+ },
341
+ "claude-sonnet": {
342
+ displayName: "Claude Sonnet",
343
+ resolve: "openrouter/anthropic/claude-sonnet-4.6",
344
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
345
+ },
346
+ "claude-haiku": {
347
+ displayName: "Claude Haiku",
348
+ resolve: "openrouter/anthropic/claude-haiku-4.5",
349
+ openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
350
+ },
351
+ gpt: {
352
+ displayName: "GPT",
353
+ resolve: "openrouter/openai/gpt-5.5",
354
+ openRouterResolve: "openrouter/openai/gpt-5.5",
355
+ subagentModel: "gpt-5.4"
356
+ },
357
+ "gpt-pro": {
358
+ displayName: "GPT Pro",
359
+ resolve: "openrouter/openai/gpt-5.5-pro",
360
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro",
361
+ subagentModel: "gpt"
362
+ },
363
+ // hidden subagent target — see openai provider above for context.
364
+ "gpt-5.4": {
365
+ displayName: "GPT 5.4",
366
+ resolve: "openrouter/openai/gpt-5.4",
367
+ openRouterResolve: "openrouter/openai/gpt-5.4",
368
+ hidden: true
369
+ },
370
+ "gpt-mini": {
371
+ displayName: "GPT Mini",
372
+ resolve: "openrouter/openai/gpt-5.4-mini",
373
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
374
+ },
375
+ // legacy aliases — see openai provider for context.
376
+ "gpt-codex": {
377
+ displayName: "GPT Codex",
378
+ resolve: "openrouter/openai/gpt-5.3-codex",
379
+ openRouterResolve: "openrouter/openai/gpt-5.3-codex",
380
+ fallback: "openrouter/gpt"
381
+ },
382
+ "gpt-codex-mini": {
383
+ displayName: "GPT Codex Mini",
384
+ resolve: "openrouter/openai/gpt-5.1-codex-mini",
385
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
386
+ fallback: "openrouter/gpt-mini"
387
+ },
388
+ "o4-mini": {
389
+ displayName: "O4 Mini",
390
+ resolve: "openrouter/openai/o4-mini",
391
+ openRouterResolve: "openrouter/openai/o4-mini"
392
+ },
393
+ "gemini-pro": {
394
+ displayName: "Gemini Pro",
395
+ resolve: "openrouter/google/gemini-3.1-pro-preview",
396
+ openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
397
+ // Inherit — see google/gemini-pro for rationale.
398
+ },
399
+ "gemini-flash": {
400
+ displayName: "Gemini Flash",
401
+ resolve: "openrouter/google/gemini-3.5-flash",
402
+ openRouterResolve: "openrouter/google/gemini-3.5-flash"
403
+ },
404
+ grok: {
405
+ displayName: "Grok",
406
+ resolve: "openrouter/x-ai/grok-4.3",
407
+ openRouterResolve: "openrouter/x-ai/grok-4.3"
408
+ },
409
+ "deepseek-pro": {
410
+ displayName: "DeepSeek Pro",
411
+ resolve: "openrouter/deepseek/deepseek-v4-pro",
412
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro"
413
+ },
414
+ "deepseek-flash": {
415
+ displayName: "DeepSeek Flash",
416
+ resolve: "openrouter/deepseek/deepseek-v4-flash",
417
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-flash"
418
+ },
419
+ // legacy alias — deepseek retires this on 2026-07-24; transparently
420
+ // upgrade existing users to the v4 family via the fallback chain.
421
+ "deepseek-chat": {
422
+ displayName: "DeepSeek Chat",
423
+ resolve: "openrouter/deepseek/deepseek-v3.2",
424
+ openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
425
+ fallback: "openrouter/deepseek-flash"
426
+ },
427
+ "kimi-k2": {
428
+ displayName: "Kimi K2",
429
+ resolve: "openrouter/moonshotai/kimi-k2.6",
430
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6"
431
+ },
432
+ "minimax-m2.5": {
433
+ displayName: "MiniMax M2.5",
434
+ resolve: "openrouter/minimax/minimax-m2.5",
435
+ openRouterResolve: "openrouter/minimax/minimax-m2.5"
436
+ }
437
+ }
438
+ })
439
+ };
440
+ function parseModel(slug) {
441
+ const slashIdx = slug.indexOf("/");
442
+ if (slashIdx === -1) {
443
+ throw new Error(`invalid model slug "${slug}" \u2014 expected "provider/model"`);
444
+ }
445
+ return { provider: slug.slice(0, slashIdx), model: slug.slice(slashIdx + 1) };
446
+ }
447
+ function getModelProvider(slug) {
448
+ return parseModel(slug).provider;
449
+ }
450
+ function getProviderDisplayName(slug) {
451
+ const parsed = parseModel(slug);
452
+ return providers[parsed.provider]?.displayName;
453
+ }
454
+ function getModelEnvVars(slug) {
455
+ const parsed = parseModel(slug);
456
+ const providerConfig = providers[parsed.provider];
457
+ if (!providerConfig) {
458
+ return [];
459
+ }
460
+ const modelConfig = providerConfig.models[parsed.model];
461
+ if (modelConfig?.envVars) {
462
+ return modelConfig.envVars.slice();
463
+ }
464
+ return providerConfig.envVars.slice();
465
+ }
466
+ function getModelManagedCredentials(slug) {
467
+ const parsed = parseModel(slug);
468
+ const providerConfig = providers[parsed.provider];
469
+ return providerConfig?.managedCredentials?.slice() ?? [];
470
+ }
471
+ var modelAliases = Object.entries(providers).flatMap(
472
+ ([providerKey, config]) => Object.entries(config.models).map(([modelId, def]) => ({
473
+ slug: `${providerKey}/${modelId}`,
474
+ provider: providerKey,
475
+ displayName: def.displayName,
476
+ resolve: def.resolve,
477
+ openRouterResolve: def.openRouterResolve,
478
+ preferred: def.preferred ?? false,
479
+ isFree: def.isFree ?? false,
480
+ fallback: def.fallback,
481
+ routing: def.routing,
482
+ // subagentModel is stored as an alias key local to the provider; expand
483
+ // here to a fully-qualified slug so callers can look up the target alias
484
+ // directly without re-deriving the provider.
485
+ subagentModel: def.subagentModel ? `${providerKey}/${def.subagentModel}` : void 0,
486
+ hidden: def.hidden ?? false
487
+ }))
488
+ );
489
+ var defaultProxyAlias = modelAliases.find((a) => a.slug === "deepseek/deepseek-pro");
490
+ if (!defaultProxyAlias?.openRouterResolve) {
491
+ throw new Error("DEFAULT_PROXY_MODEL: deepseek/deepseek-pro missing openRouterResolve");
492
+ }
493
+ var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
494
+ var defaultProxyDisplayName = defaultProxyAlias.displayName;
495
+ function getAutoSelectHintModel() {
496
+ return defaultProxyDisplayName;
497
+ }
498
+ function resolveModelSlug(slug) {
499
+ return modelAliases.find((a) => a.slug === slug)?.resolve;
500
+ }
501
+ var MAX_FALLBACK_DEPTH = 10;
502
+ function resolveDisplayAlias(slug) {
503
+ let current = slug;
504
+ const visited = /* @__PURE__ */ new Set();
505
+ for (let i = 0; i < MAX_FALLBACK_DEPTH; i++) {
506
+ if (visited.has(current)) return void 0;
507
+ visited.add(current);
508
+ const alias = modelAliases.find((a) => a.slug === current);
509
+ if (!alias) return void 0;
510
+ if (!alias.fallback) return alias;
511
+ current = alias.fallback;
512
+ }
513
+ return void 0;
514
+ }
515
+ function resolveCliModel(slug) {
516
+ return resolveDisplayAlias(slug)?.resolve;
517
+ }
518
+ function resolveOpenRouterModel(slug) {
519
+ return resolveDisplayAlias(slug)?.openRouterResolve;
520
+ }
521
+
522
+ // src/external.ts
523
+ var terramendMcpName = "terramend";
524
+ function formatMcpToolRef(agentId, toolName) {
525
+ switch (agentId) {
526
+ case "claude":
527
+ return `mcp__${terramendMcpName}__${toolName}`;
528
+ case "opencode":
529
+ return `${terramendMcpName}_${toolName}`;
530
+ default:
531
+ return agentId;
532
+ }
533
+ }
534
+
535
+ // src/agents/reviewer.ts
536
+ var REVIEWER_AGENT_NAME = "reviewfrog";
537
+
538
+ // src/reviewQuality.ts
539
+ var REVIEW_FINDING_PRECEDENTS = `### Finding precedents (false-positive control)
540
+
541
+ 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.
542
+
543
+ **Hard exclusions \u2014 never post:**
544
+
545
+ - 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".
546
+ - Theoretical race conditions or timing attacks. Post a race only when it is concretely reachable and concretely harmful.
547
+ - Memory-safety findings (buffer overflow, use-after-free, OOB) in memory-safe languages \u2014 Rust, Go, JS/TS, Python, Java, HCL.
548
+ - Security findings whose anchor is a documentation file \u2014 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.)
549
+ - "Lack of hardening" with no vulnerability: code is not required to implement every best practice, only to avoid concrete flaws.
550
+ - Vulnerable-dependency reports based on version strings alone \u2014 dependency scanning is a separate pipeline with its own remediation flow.
551
+
552
+ **General precedents:**
553
+
554
+ - Environment variables, CLI flags, and workflow-dispatch inputs are operator-trusted. An attack that requires controlling them is invalid.
555
+ - 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.
556
+ - React/Angular-class frameworks escape output by default \u2014 an XSS claim needs \`dangerouslySetInnerHTML\`, \`bypassSecurityTrustHtml\`, or an equivalent unsafe API in the diff.
557
+ - SSRF requires control of host or protocol; path-only control is not SSRF. Neither SSRF nor path traversal applies to purely client-side code.
558
+ - Command injection in shell scripts needs a named untrusted-input path; developer-invoked scripts taking developer-supplied arguments don't qualify.
559
+ - 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.
560
+ - UUIDs are unguessable; an attack that requires guessing one is invalid.
561
+
562
+ **Terraform / IaC precedents:**
563
+
564
+ - \`0.0.0.0/0\` **egress** is common and usually intentional \u2014 flag open **ingress**, or egress only with a concrete exfiltration concern attached.
565
+ - Values from \`*.tfvars\`, \`locals\`, and module input variables are operator-trusted; "what if this variable is malicious" is invalid without naming an untrusted writer.
566
+ - Missing encryption / versioning / access-logging on resources that demonstrably hold no sensitive data (short-retention log groups, test fixtures, scratch buckets) is \u2139\uFE0F at most, never \u{1F6A8}.
567
+ - Unpinned provider or module versions are style feedback for first-party modules; \u26A0\uFE0F only for third-party module sources, where the unpinned ref is supply-chain surface.
568
+ - Do not infer state drift, plan outcomes, or "this will destroy/replace the database" from static HCL \u2014 only \`terraform plan\` evidence supports those claims. Without plan evidence, phrase the concern as an open question, not a finding.
569
+ - Missing tags and naming-convention deviations are nitpicks, not findings.
570
+ - When a deterministic scanner rule covers the same issue (trivy \`AVD-*\`, checkov \`CKV_*\`, tflint), cite the rule id in the finding \u2014 that makes it \u2717\u2192\u2713 verifiable downstream instead of an unverifiable reviewer opinion.
571
+
572
+ **Signal-quality bar** \u2014 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?`;
573
+ var FINDING_VERIFICATION_PASS = `**Adversarial verification \u2014 required before posting any \u{1F6A8}/\u26A0\uFE0F 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 \u{1F6A8} critical or \u26A0\uFE0F important, dispatch one \`${REVIEWER_AGENT_NAME}\` verification subagent \u2014 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:
574
+
575
+ - \u2139\uFE0F informational findings and nitpicks (post on your own judgment), and
576
+ - findings whose evidence is deterministic tool output \u2014 a scanner concern id, a failing test, a compiler/type error. Those re-verify mechanically and need no judge.
577
+
578
+ Each verification dispatch contains, in order:
579
+ - the absolute \`diffPath\` (and \`incrementalDiffPath\` when available) named verbatim \u2014 the reviewer's baked-in system prompt selects its first action on this token;
580
+ - the single finding under test: file, line, intended severity, the claim, and the evidence you collected;
581
+ - the **Finding precedents** section \u2014 plus any \`### Finding precedents \u2014 org addendum\` section from your instructions \u2014 included verbatim (the subagent cannot see your prompt);
582
+ - this charge: "Attempt to REFUTE this finding. Read the actual code \u2014 do not trust the claim's description of it. Apply the finding precedents. Report a verdict (\`confirmed\` / \`refuted\` / \`uncertain\`), a confidence score 1\u201310, and a 2\u20133 sentence justification quoting the code that decides it. When the attack or failure path is theoretical rather than demonstrated, bias toward \`refuted\`."
583
+
584
+ 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 \u2014 the subagent is judging one claim, not exploring.
585
+
586
+ Gate on what comes back:
587
+ - \`refuted\` at confidence \u2265 7 \u2192 suppress the finding and record it for the audit trail.
588
+ - \`uncertain\`, or \`refuted\` at lower confidence \u2192 re-read the decisive code yourself; either downgrade to \u2139\uFE0F with the uncertainty stated, or suppress. Do not post it at \u{1F6A8}/\u26A0\uFE0F.
589
+ - \`confirmed\` \u2192 post it; fold the verifier's justification into the comment's technical-details block when it adds evidence.
590
+ - errored / timed out / nothing usable \u2192 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.
591
+
592
+ Suppressed findings are recorded, never silently deleted \u2014 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.`;
593
+
594
+ // src/modes.ts
595
+ var PR_SUMMARY_FORMAT = `### Default format
596
+
597
+ The body has at most four parts in this exact order:
598
+
599
+ 1. **Reviewed changes preamble** \u2014 one bolded inline lead-in describing what was reviewed in this run, a bullet list of the substantive changes, and an HTML comment carrying review metadata for downstream agents.
600
+ 2. **Cross-cutting issue sections** (zero or more) \u2014 one \`### \` heading per concern, with a human-readable problem write-up and a collapsed \`<details>Technical details</details>\` block underneath.
601
+ 3. **\`### \u2139\uFE0F Nitpicks\`** (only if there are nits worth surfacing in the body) \u2014 a flat bullet list, no technical-details block.
602
+ 4. **\`Suppressed findings\` collapsed block** at the very bottom (only when the adversarial verification pass suppressed at least one \u{1F6A8}/\u26A0\uFE0F candidate) \u2014 the one-line-per-finding audit trail for the false-positive filter.
603
+
604
+ Inline-vs-body split: concerns that anchor to a specific line go inline (use the \`comments\` parameter). Body \`### \` sections are reserved for concerns that **have no line to anchor to** \u2014 typically because the concern is about *absence* (something the diff should have done but didn't), *sequencing* (rollout / deletion / migration order), *design decisions only the human can make*, or *scope questions the diff implicitly raises but doesn't address*. A concern that anchors to a line but has broad implications still goes inline (use the technical-details block there to capture the implications \u2014 see Inline technical details below). If you found no non-anchorable concerns, the body has zero \`### \` issue sections \u2014 just the preamble + metadata.
605
+
606
+ ## 1. Reviewed changes preamble
607
+
608
+ Open with a single bolded inline lead-in followed immediately by the bullet list (no \`### Key changes\` heading, no \`<b>TL;DR</b>\`):
609
+
610
+ \`\`\`
611
+ **Reviewed changes** \u2014 one sentence on what was reviewed in this run. For Review (initial), this is what the PR does and why. For IncrementalReview, this is what changed since the prior terramend review. Focus on intent, not mechanics.
612
+
613
+ - **Short human-readable title** \u2014 1 sentence per substantive change. Write a short prose phrase; when you name a file, type, or function, put that name in backticks (e.g. **Add \\\`TodoTracker\\\` for live checklists**). A reviewer should understand the full reviewed scope from this list alone \u2014 this IS the dispassionate "what was reviewed and what changed" overview, so cover the substantive changes, not just the loudest ones.
614
+
615
+ <!--
616
+ Terramend review metadata \u2014 for any agent (or human-with-agent) reading this
617
+ review. Incorporate the fields below into your understanding of the context
618
+ this review was made in. The findings below were written against
619
+ {head_sha_short}; if new commits have landed on {head_ref} since this review
620
+ was submitted, treat any specific bug, file, or line callout as POTENTIALLY
621
+ STALE \u2014 re-diff against {head_sha_short} (or trigger a fresh review) and
622
+ factor commits past {head_sha_short} into your understanding of the current
623
+ state before acting on findings.
624
+
625
+ - Mode: Review (initial) or IncrementalReview (delta against prior terramend review)
626
+ - Files reviewed: {file_count}
627
+ - Commits reviewed: {commit_count}
628
+ - Base: {base_ref} ({base_sha_short})
629
+ - Head: {head_ref} ({head_sha_short})
630
+ - Reviewed commits:
631
+ - {sha_short} \u2014 {commit_subject}
632
+ - ...
633
+ - Prior terramend review: none or {prior_sha_short} ({prior_review_html_url})
634
+ - Submitted at: {iso_timestamp}
635
+ -->
636
+ \`\`\`
637
+
638
+ Pull every metadata field from the \`checkout_pr\` tool's response \u2014 file count, commit count, base/head ref + SHA, the commit list. For \`IncrementalReview\` runs, populate \`Prior terramend review\` with the prior review's commit_id (short SHA) and \`html_url\` from \`list_pull_request_reviews\`.
639
+
640
+ ## 2. Cross-cutting issue sections (zero or more)
641
+
642
+ For each cross-cutting concern, one \`### \` section. Use this exact shape:
643
+
644
+ \`\`\`
645
+ ### {emoji} {short, descriptive title \u2014 what's wrong, not what to do}
646
+
647
+ {Human-readable problem write-up. Describes the PROBLEM only \u2014 what's broken, what the symptom is, what the blast radius is. NO asks, NO suggested fixes, NO "the right thing to do is...". Asks and fixes live in the technical-details block below; the visible part is for the human to *understand* the problem, not to implement it.}
648
+
649
+ <details><summary>Technical details</summary>
650
+
651
+ \\\`\\\`\\\`\\\`markdown
652
+ # {title repeated}
653
+
654
+ ## Affected sites
655
+ - {file path:line} \u2014 {what's wrong there}
656
+ - ...
657
+
658
+ ## Required outcome
659
+ - {what the fix needs to achieve, not how to achieve it}
660
+ - ...
661
+
662
+ ## Suggested approach (optional)
663
+ {When the fix shape is non-obvious, sketch one or more reasonable directions. Skip when the outcome alone makes the fix obvious.}
664
+
665
+ ## Open questions for the human (optional)
666
+ - {Any decision an implementing agent shouldn't make unilaterally \u2014 pricing thresholds, breaking-change policy, naming, scope of follow-up.}
667
+ \\\`\\\`\\\`\\\`
668
+
669
+ </details>
670
+ \`\`\`
671
+
672
+ Concrete example of the visible part of a non-anchored section (technical-details block unchanged from the template above):
673
+
674
+ \`\`\`
675
+ ### \u2139\uFE0F Legacy \`opencode.ts\` has no documented deletion plan
676
+
677
+ The v2 harness lands alongside the v1 file and imports one helper from it. Worth a follow-up issue or a TODO so the next maintainer doesn't have to re-derive the cleanup plan.
678
+ \`\`\`
679
+
680
+ The example's value is its *shape*: a finding about absence (no deletion plan), not a line-anchored bug. Body sections live or die on whether the concern genuinely doesn't fit on a line.
681
+
682
+ **Heading severity emoji** \u2014 every \`### \` heading carries one:
683
+
684
+ - \u{1F6A8} critical \u2014 blocks merge (data loss, security, broken core flow)
685
+ - \u26A0\uFE0F important \u2014 must address before merging (regression, missing validation, incorrect behavior)
686
+ - \u2139\uFE0F informational \u2014 surfaced for awareness; mergeable as-is
687
+
688
+ **Visible problem write-up rules:**
689
+
690
+ - **No asks, no suggested fixes** in the visible part. The visible portion describes the problem; the technical-details block describes the fix shape and any open questions. The exception: a fix so self-evident that NOT stating it would be weird (e.g. "the typo is missing an 'r'") \u2014 in that case, fold it into the problem statement and skip the suggested-approach block in technical details too.
691
+ - **Never two successive plain paragraphs.** Every transition between block-level elements must alternate prose with structure: paragraph \u2192 bullet list \u2192 paragraph; paragraph \u2192 code fence \u2192 bullet list; paragraph \u2192 table \u2192 paragraph. Two consecutive paragraphs in a row create a wall of text that's impossible to digest. If you catch yourself writing one, find a way to split it: pull a list out of it, drop a 2-3 line code fence between them, or merge them into a single tighter paragraph.
692
+ - **Per-paragraph budget:** ~3 sentences max. Past that, you're explaining where you should be structuring.
693
+ - **Identifier discipline still applies** in the visible part. Lead with behavior in plain English; name an identifier only when it's the subject of the concern or a public surface a reader would recognize. The technical-details block is where dense identifier references belong.
694
+
695
+ **Technical-details block rules:**
696
+
697
+ - Wrapped in a 4-backtick markdown fence (\`\\\`\\\`\\\`\\\`markdown ... \\\`\\\`\\\`\\\`\`) so it's visually distinct, one-click copyable, and can contain its own 3-backtick code fences without escape gymnastics. The contents are agent-readable \u2014 a fix-agent will pull the body down and use this block as the brief.
698
+ - File paths and \`file:line\` refs are encouraged (and necessary) \u2014 the next agent uses these to navigate. Identifier density is fine here.
699
+ - Slightly more verbose than the absolute minimum is OK when it materially helps the next agent: a small code snippet showing the symptom, a short table of mismatched key/column pairs, a one-paragraph "why CI doesn't catch it" note. Skip massive regression-test scaffolding or full route rewrites \u2014 the implementing agent writes those.
700
+ - Use the four standard sections (\`Affected sites\`, \`Required outcome\`, optional \`Suggested approach\`, optional \`Open questions for the human\`). Skip the optional sections when they wouldn't add anything.
701
+
702
+ ## Inline technical details
703
+
704
+ Inline comments are short (~2-3 sentences) by default. When an inline finding has broader implications worth recording for a fix-agent \u2014 e.g. a localized bug whose proper fix requires touching several files, or where the right fix depends on a design decision the human needs to make \u2014 append a collapsed \`<details><summary>Technical details</summary>\` block to the inline comment's body. Same shape as the body-section technical-details block (4-backtick fenced markdown, \`## Affected sites\` / \`## Required outcome\` / optional \`## Suggested approach\` / optional \`## Open questions for the human\`).
705
+
706
+ GitHub renders the same markdown parser in inline comments as in the review body, so the collapsed-details affordance works the same way. The visible part of the inline comment stays scannable; the depth is one click away for any agent that needs it.
707
+
708
+ ## 3. \`### \u2139\uFE0F Nitpicks\` (optional, last content section)
709
+
710
+ Only when there are nits that for some reason can't be inlined. Filepaths in nit text are fine \u2014 these are simple enough that a human or agent reads once and acts. No technical-details block.
711
+
712
+ \`\`\`
713
+ ### \u2139\uFE0F Nitpicks
714
+
715
+ - {nit, with file path inline if useful, \u2264 ~200 chars}
716
+ - ...
717
+ \`\`\`
718
+
719
+ ## 4. \`Suppressed findings\` (optional, very last)
720
+
721
+ Only when the adversarial verification pass (see the checklist) suppressed at least one \u{1F6A8}/\u26A0\uFE0F candidate. One collapsed block, always the last element in the body, with the count in the summary line:
722
+
723
+ \`\`\`
724
+ <details><summary>\u{1F5D1}\uFE0F Suppressed findings (2)</summary>
725
+
726
+ - \u26A0\uFE0F \`networking/main.tf:42\` \u2014 claimed the subnet exposes the DB publicly \u2014 refuted: \`publicly_accessible = false\` at \`db.tf:18\`.
727
+ - \u{1F6A8} \`api/auth.ts:77\` \u2014 claimed JWT signature bypass \u2014 refuted: the unverified decode is dev-only, gated by the \`NODE_ENV\` check two lines above.
728
+
729
+ </details>
730
+ \`\`\`
731
+
732
+ One bullet per suppressed finding: severity emoji, \`file:line\`, the claim in a few words, the refutation in a few words. One line each \u2014 the block exists for auditability (a human catching the false-positive filter being wrong), not re-litigation. Omit the block entirely when nothing was suppressed.
733
+
734
+ ## Inline comment shape
735
+
736
+ Inline comments use the same severity framing as body \`### \` sections, scaled down for line-anchored use:
737
+
738
+ - **Lead with a 1-2 sentence problem statement.** The reader is looking at the line in question, so don't restate what the line says \u2014 describe what's wrong with it. Optionally prefix the visible line with a severity emoji (\u{1F6A8} / \u26A0\uFE0F / \u2139\uFE0F) when severity isn't obvious from context.
739
+ - **Optional \`<details><summary>Technical details</summary>...</details>\` collapsible** for findings whose technical context (longer file:line references, related-code snippets, suggested approach, regression-risk notes) would overwhelm the human-readable lead-in. Same agent-readable purpose, same 4-backtick fence shape, and same 4-section structure as the body's technical-details block \u2014 see *Inline technical details* above. Encouraged whenever the depth helps a downstream fix-agent; don't force one when the inline lead-in already says everything.
740
+ - **Visible portion \u2264 2-3 sentences.** If you find yourself writing more, that's the cue to split the depth into the \`Technical details\` collapsible.
741
+
742
+ ## Body-wide rules
743
+
744
+ - **Inline-vs-body discipline (repeated for emphasis):** anything that anchors to a specific line goes inline (with a \`<details>Technical details</details>\` block when the implications are broad). The body is for non-anchorable concerns only \u2014 absence, sequencing, design decisions, scope questions, architectural risk.
745
+ - **No \`### Issues found\` heading** above the issue sections \u2014 each \`### \` heading IS the issue.
746
+ - **Severity emoji on every \`### \` heading** (\u{1F6A8} / \u26A0\uFE0F / \u2139\uFE0F). No emoji on the preamble lead-in or anywhere else.
747
+ - **GitHub block-level rendering**: GitHub's markdown parser requires a blank line between ALL block-level elements (HTML tags like \`<br/>\`, \`<sub>\`, \`<details>\`, \`<b>\` and markdown syntax like headings, lists, blockquotes, code fences, paragraphs). Without a blank line, GitHub treats following content as a continuation of the HTML block and renders markdown syntax as literal text. ALWAYS separate block-level elements with a blank line.
748
+ - **Backtick-wrap** every variable, identifier, or file name when you mention one (in either visible or technical-details portions).
749
+ - **Don't repeat diff content**, don't include raw \`+123 / -45\` stats, don't include a changelog section, don't use horizontal rules (\`---\`).
750
+ - **Pull file/commit counts from \`checkout_pr\` metadata** \u2014 never count manually.
751
+ - **Legacy headings REMOVED.** Do not use \`### Key changes\`, \`### Issues found\`, \`<b>TL;DR</b>\`, or \`<sub><b>Summary</b>\`. The new structure subsumes them.`;
752
+ var REMEDIATION_PR_FORMAT = `### Remediation PR format
753
+
754
+ **Minimum (ALWAYS include, even under tight budget):** a one-paragraph plain-English summary of *what was wrong and what you changed*, then a \`## What changed\` list with one *Was / Changed / Safe because* note per concern, then the \`## Validation (\u2717 \u2192 \u2713)\` list. If you produce nothing else, produce these three \u2014 a PR a human can't understand from its body alone has failed its job. Everything below enriches this minimum; it does not replace it.
755
+
756
+ Build the PR body in this EXACT order. Every line is backed by a tool result \u2014 never write a status you didn't get from a tool. Omit a whole section only when its tool didn't run (e.g. no plan without cloud creds); never fabricate it. Keep a blank line between every block-level element (GitHub needs it to render).
757
+
758
+ #### 1. Status banner (first line)
759
+
760
+ One GitHub alert blockquote that sets the reviewer's expectation, picked from the verification evidence:
761
+
762
+ - \`> [!CAUTION]\` \u2014 a \`needs-human\` signal fired: a regression (\`has_regressions\`), a stateful destroy/replace, a high blast radius, a non-deterministic plan, or a cost escalation. One sentence naming the reason.
763
+ - \`> [!WARNING]\` \u2014 verified but with a caveat (medium blast radius, a still-\`remaining\` concern, baseline-unavailable cost).
764
+ - \`> [!NOTE]\` \u2014 clean: \`verified: true\`, no regressions, low blast radius. One sentence: what was hardened.
765
+
766
+ #### 2. Title line + badges
767
+
768
+ A single bolded sentence naming the file/group and what was fixed, then a one-line badge row built from the tool results (drop any badge whose tool didn't run):
769
+
770
+ \`\`\`
771
+ **Hardened \\\`main.tf\\\` \u2014 S3 encryption + public-access block.**
772
+
773
+ \`Confidence: high\` \xB7 \`Blast radius: low (1 resource)\` \xB7 \`Plan: +0 ~1 -0\` \xB7 \`Idempotent: yes\` \xB7 \`Cost: +$0.00/mo\`
774
+ \`\`\`
775
+
776
+ Render \`Confidence\` verbatim from \`terraform_verify_remediation.confidence\` \u2014 never inflate it. Use \`\xB7\` separators, backtick-wrap each badge.
777
+
778
+ #### 3. \`## What changed\`
779
+
780
+ One \`### \` subsection per resolved concern (or one per rule for a by-rule group), each with the \xA75.17 three-line micro-template and the rule linked to its docs (\`doc_url\`, else \`remediation_hint\`):
781
+
782
+ \`\`\`
783
+ ### \u{1F512} [\\\`trivy:AVD-AWS-0088\\\`](https://avd.aquasec.com/misconfig/avd-aws-0088) \u2014 S3 bucket not encrypted
784
+
785
+ - **Was** \u2014 {the scanner's \`evidence\`, in plain English}.
786
+ - **Changed** \u2014 {what the fix did, one sentence}.
787
+ - **Safe because** \u2014 {why it's correct and non-breaking}.
788
+ \`\`\`
789
+
790
+ Lead each heading with a severity emoji (\u{1F6A8} critical \xB7 \u26A0\uFE0F high \xB7 \u{1F512} security \xB7 \u2139\uFE0F low/info). Backtick-wrap every identifier. No raw diff dumps \u2014 the Files tab shows the diff.
791
+
792
+ #### 4. \`## Validation (\u2717 \u2192 \u2713)\`
793
+
794
+ Built ONLY from \`terraform_verify_remediation\`'s result \u2014 this is the proof, not a self-report. One line per id in \`resolved\`, then any still-open id honestly:
795
+
796
+ \`\`\`
797
+ ## Validation (\u2717 \u2192 \u2713)
798
+
799
+ - \u2717 \u2192 \u2713 \\\`trivy:AVD-AWS-0088\\\` resolved
800
+ - \u2717 \u2192 \u2713 \\\`checkov:CKV_AWS_19\\\` resolved
801
+ - \u26A0\uFE0F still open: \\\`tflint:...\\\` \u2014 {why it couldn't be cleared}
802
+ \`\`\`
803
+
804
+ If \`has_regressions\` is true, add a \`> [!CAUTION]\` **Regression** callout listing each new concern id BEFORE this list, and ensure the \`needs-human\` label is set. Never mark an id \u2713 unless the tool returned it in \`resolved\`.
805
+
806
+ #### 5. \`<details><summary>Plan</summary>\` (when \`terraform_plan\` ran)
807
+
808
+ Attach the full \`plan_text\` in a collapsed code block so a reviewer sees the exact change without re-running it:
809
+
810
+ \`\`\`
811
+ <details><summary>Terraform plan</summary>
812
+
813
+ \\\`\\\`\\\`
814
+ {plan_text}
815
+ \\\`\\\`\\\`
816
+
817
+ </details>
818
+ \`\`\`
819
+
820
+ When \`needs_human\` is true, surface \`needs_human_reasons\` as a visible bullet list above the \`<details>\` \u2014 don't bury an escalation in a collapsed block.
821
+
822
+ #### 6. \`## \u{1F6E1}\uFE0F Prevent recurrence\` (optional follow-up)
823
+
824
+ From the scan's \`prevention\` map \u2014 the CI guardrail that stops this class of concern coming back. Clearly marked **not part of this PR's diff**: a short intro sentence then the \`mechanism\` + a fenced \`snippet\`. One entry per distinct rule.
825
+
826
+ #### 7. \`## Compliance\` (optional, when a crosswalk was run)
827
+
828
+ When \`terraform_compliance_crosswalk\` was called, add a short auditor-facing note: the frameworks/controls this fix touches (from \`by_framework\`), prefixed "Indicative alignment (crosswalk v{version}) \u2014 not an audit verdict." Skip entirely when the crosswalk wasn't run.
829
+
830
+ #### Body-wide rules
831
+
832
+ - **Evidence-built, never self-reported** \u2014 every badge, \u2713, and count comes from a tool result. If a tool didn't run, omit its section; don't guess.
833
+ - **Blank line between ALL block-level elements** (callouts, headings, lists, code fences, \`<details>\`) \u2014 GitHub renders markdown as literal text otherwise.
834
+ - **Backtick-wrap** every file, rule id, resource address, and identifier.
835
+ - **No raw \`+N/-M\` diff stats, no horizontal rules (\`---\`), no changelog section.** The footer is appended automatically \u2014 don't add your own.
836
+ - **One scoped group per PR.** The body describes this group's fix only.`;
837
+ function computeModes(agentId) {
838
+ const t = (toolName) => formatMcpToolRef(agentId, toolName);
839
+ return [
840
+ {
841
+ name: "Build",
842
+ description: "Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
843
+ prompt: `### Checklist
844
+
845
+ 1. **task list**: create your task list for this run as your first action.
846
+
847
+ 2. **plan** (optional, for complex tasks): analyze requirements, read AGENTS.md and relevant code, produce a step-by-step implementation plan.
848
+
849
+ 3. **setup**: checkout or create the branch:
850
+ - **PR event, modifying the existing PR**: call \`${t("checkout_pr")}\`
851
+ - **new branch**: use \`${t("git")}\` to create a branch (\`git checkout -b terramend/branch-name\`)
852
+
853
+ 4. **build**: implement changes using your native file and shell tools:
854
+ - follow the plan (if you ran a plan phase)
855
+ - plan your approach before writing code: identify which files need to change, key design decisions, and edge cases. for non-trivial changes, consider whether there's a more elegant approach.
856
+ - run relevant tests/lints before committing
857
+
858
+ 5. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
859
+
860
+ Skip self-review (commit directly) when the diff is **genuinely trivial**:
861
+ - doc typos, comment-only edits, whitespace/format-only, import reordering
862
+ - lockfile or generated-code regeneration, mechanical rename whose only effect is import-path updates (size of diff is irrelevant \u2014 read the *shape*, not the line count)
863
+ - low-risk dep patch bump from a trusted source
864
+
865
+ Run self-review when the diff has **any behavioral surface, however small**:
866
+ - 1-line changes to SQL operators / comparison logic / regexes / redirects / HTTP methods / response codes
867
+ - any change to money / tax / currency / billing / fee / refund / payout calculations or constants
868
+ - any change to auth / permissions / roles / sessions / tokens / signature verification
869
+ - any change to feature-flag defaults, retry counts, timeouts, rate limits, batch sizes
870
+ - new endpoints, new code paths, new error branches \u2014 even small ones
871
+ - mixed diffs (whitespace + a single semantic line) \u2014 the semantic line still triggers self-review
872
+ - anything you're uncertain about
873
+
874
+ Tie-breaker: when in doubt, run self-review. One false-positive subagent dispatch costs cents; one false-negative shipped bug costs much more. There's no value in dispatching for a typo, but there's also no excuse for skipping on a 1-line change to a billing path.
875
+
876
+ Otherwise delegate the \`${REVIEWER_AGENT_NAME}\` subagent to review your diff with fresh eyes against YOUR TASK. The subagent's baked-in system prompt enforces a non-mutative + non-recursive contract: read-only file/search/web tools and read-only MCP queries only; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch. Enforcement is prose-only \u2014 restate the constraint in your dispatch instructions and do not relax it.
877
+
878
+ Before dispatching, ensure \`origin/<base>\` is locally available \u2014 the runner is often a shallow single-branch \`actions/checkout\` (depth=1, head-only refspec), and the reviewer's \`git diff --merge-base origin/<base>\` will fail with \`ambiguous argument\` or \`no merge base\` otherwise. Run \`git fetch --no-tags --deepen=1000 origin <base>:refs/remotes/origin/<base>\` once (the explicit destination refspec is required \u2014 a shallow single-branch checkout configures a head-only refspec, so a bare \`origin <base>\` only updates \`FETCH_HEAD\` and never creates the \`origin/<base>\` tracking ref); it's a no-op if the ref already has enough history. (The reviewer is read-only by contract, so it cannot do this itself \u2014 fetching is the orchestrator's job.)
879
+
880
+ Compose your \`${REVIEWER_AGENT_NAME}\` dispatch prompt using this template verbatim, substituting the \`<...>\` placeholders. The preamble aligns the orchestrator side of the dispatch contract with the reviewer's baked-in system prompt \u2014 both ends say the same thing about where the work lives and what to do on an empty diff.
881
+
882
+ \`\`\`
883
+ ## What you're reviewing
884
+ This is a PRE-COMMIT Build-mode self-review. The work to review lives in the working tree (uncommitted), NOT in committed history.
885
+
886
+ Branch: <branch> (off <base>)
887
+ Canonical diff command: git diff --merge-base origin/<base>
888
+
889
+ Use \`--merge-base\` (single MCP \`git\` call, no shell substitution required). NOT bare \`git diff origin/<base>\` or two-dot \`git diff origin/<base>..HEAD\` \u2014 the symmetric forms include the inverse of every commit landed on \`<base>\` since this branch forked, which is noise (and the git tool will reject those forms when the divergence is detected). \`origin/<base>...HEAD\` (three-dot) and \`--cached\` both miss the uncommitted edits self-review runs on, so they're also wrong here.
890
+
891
+ If the merge-base diff returns empty, treat it as "no changes \u2014 nothing to review" and stop per your system prompt. Do not search for the work elsewhere.
892
+
893
+ ## Your task
894
+ <YOUR TASK content>
895
+
896
+ ## Build-phase failures
897
+ <tight summary \u2014 what broke, root cause, the fix \u2014 or "no build-phase failures">
898
+ \`\`\`
899
+
900
+ Follow the template with the diff content (\`git diff --merge-base origin/<base-branch>\` \u2014 single MCP \`git\` call, captures committed + staged + unstaged, excludes base-branch progress) and your task brief. Instruct the subagent to flag bugs, logic errors, missing edge cases, gaps between request and diff, and unintended changes.
901
+
902
+ Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
903
+ - Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
904
+ - Do NOT curate a reading list of files. Let the subagent discover scope from the diff and codebase.
905
+ - Do NOT pre-shape output with a severity / category schema. That leaks your hypotheses; severity is your call during evaluation.
906
+ - Do NOT defect-hunt the diff yourself in parallel with the subagent. Your role is dispatch + evaluation; doing the review yourself reintroduces the implementation bias the subagent is meant to mitigate.
907
+ - For diffs that rely on third-party API contracts, SDK semantics, framework directives, or DB engine specifics, instruct the subagent to verify load-bearing claims via web search and quote source URLs rather than trust training data \u2014 this is the single most common review-quality failure mode.
908
+
909
+ Be **discerning** about what comes back. The reviewer is an AI subagent and is fallible \u2014 treat every finding as a hypothesis, not a directive, and **verify each one yourself** against the diff and the code before deciding whether to apply. You are searching for a solution that is **complete, minimal, and elegant** \u2014 you may need to think hard to find it. Do not over-engineer, do not be over-defensive, **do not write AI slop**. Reviewers bias toward *recommending additions*, and that bias has a recognizable slop texture: defensive checks for cases that cannot happen, extra logging, new abstractions used once, comments restating code, tests asserting tautologies, "just-in-case" guards, error handlers for cases the type system already rules out. Reject those. For each surviving finding, ask: would applying it leave the code more sound, correct, AND elegant? Two-out-of-three means look harder for a fix that gets all three before settling. After applying the fixes you accept, re-read your diff and be discerning about what *you just changed*: if any fix turned out to be bloat in context, revert it. Then verify only intended changes are present, no debug artifacts or commented-out code remain, no unrelated files were modified. Commit locally via shell (\`git add . && git commit -m "..."\`).
910
+
911
+ 6. **finalize**:
912
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
913
+ - create a PR via \`${t("create_pull_request")}\`
914
+ - call \`${t("report_progress")}\` with the PR link or the exact error if push/PR failed
915
+
916
+ ### Notes
917
+
918
+ For simple, well-defined tasks, skip the plan phase and go straight to build.`
919
+ },
920
+ {
921
+ name: "AddressReviews",
922
+ description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
923
+ prompt: `### Checklist
924
+
925
+ 1. **task list**: create your task list for this run as your first action.
926
+
927
+ 2. Checkout the PR branch via \`${t("checkout_pr")}\`.
928
+
929
+ 3. Fetch review comments via \`${t("get_review_comments")}\`.
930
+
931
+ 4. For each comment:
932
+ - understand the feedback
933
+ - **verify the finding yourself** against the actual code before deciding whether to apply \u2014 every comment (human or agent) is a hypothesis, not a directive. agent reviewers especially are fallible.
934
+ - you are searching for a solution that is **complete, minimal, and elegant** \u2014 you may need to think hard to find it. do not over-engineer, do not be over-defensive, **do not write AI slop**. reviewers bias toward *recommending additions*, and that bias has a recognizable slop texture: defensive checks for impossible cases, extra abstractions used once, comments restating obvious code, tests asserting tautologies, "just-in-case" guards, error handlers for cases the type system already rules out. reject those. evaluate whether applying the finding would leave the code more **sound, correct, AND elegant**; two-out-of-three is a signal to look harder for a fix that gets all three. if a request would add bloat \u2014 ceremony without commensurate correctness benefit \u2014 push back in your reply rather than mechanically applying it.
935
+ - if the request stands, make the code change using your native tools; otherwise reply explaining why
936
+ - record what was done (or why nothing was done)
937
+
938
+ 5. Quality check:
939
+ - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
940
+ - commit locally via shell (\`git add . && git commit -m "..."\`)
941
+
942
+ 6. Finalize. Reply + resolve are paired write actions: do BOTH or NEITHER for each thread.
943
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
944
+ - **if push fails**, call \`${t("report_progress")}\` with the exact error and STOP \u2014 do NOT reply or resolve any thread until the fix is live on the remote. Resolving a thread without the fix landing misleads the reviewer.
945
+ - **on push success**, for each thread you acted on:
946
+ - reply ONCE via \`${t("reply_to_review_comment")}\`. The \`comment_id\` parameter takes the root comment's numeric \`id=\` (from the first \`comment author=...\` tag in the \`${t("get_review_comments")}\` output) \u2014 NOT the \`thread=\` value; that's a separate GraphQL ID used by resolve. The runtime dedupes identical bodies within a session.
947
+ - **immediately** call \`${t("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
948
+ - call \`${t("report_progress")}\` with a brief summary`
949
+ },
950
+ // Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
951
+ // 0 lenses (orchestrator handles the review solo). Multi-lens (2+
952
+ // reviewfrog subagents in parallel) only fires for substantive PRs or
953
+ // high-stakes-subsystem touches — and when it fires, ALL lenses must
954
+ // dispatch in a single assistant turn or the parallelism win disappears.
955
+ // We never dispatch exactly one lens: a single lens is just a worse,
956
+ // slower version of doing the work yourself.
957
+ //
958
+ // Build mode self-review is a different problem shape: the orchestrator
959
+ // wrote the code, so bias-mitigation comes from delegating to one
960
+ // fresh-eyes subagent that doesn't share the implementation context. A
961
+ // single subagent there is appropriate; the 0-or-2+ rule applies only to
962
+ // the Review/IncrementalReview lens fan-out where independence between
963
+ // perspectives is what's being purchased.
964
+ //
965
+ // Severity categorization is split across two surfaces: the opening
966
+ // callout (CAUTION/IMPORTANT/ℹ️/✅) sets the review's overall tier, and
967
+ // per-bullet emoji prefixes (🚨/⚠️/ℹ️ in PR_SUMMARY_FORMAT) tag
968
+ // individual points inside summary sections — scoping severity to the
969
+ // specific bullet rather than the whole section keeps a section that
970
+ // mixes a 🚨 and an ℹ️ from being mislabeled by either of them.
971
+ {
972
+ name: "Review",
973
+ description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
974
+ prompt: `### Checklist
975
+
976
+ 1. **task list**: create your task list for this run as your first action.
977
+
978
+ 2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
979
+
980
+ 3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). pull as much context as you need to render a confident, well-grounded review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths, fetch related GitHub state. **you are the synthesizer** \u2014 never delegate understanding to subagents.
981
+
982
+ if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
983
+
984
+ "Genuinely trivial" (skip):
985
+ - single-word doc typo, whitespace/format-only, comment-only across any number of files
986
+ - lockfile or generated-code regeneration (size of diff is irrelevant \u2014 read the *shape*)
987
+ - mechanical rename whose only effect is import-path updates
988
+ - low-risk dep patch bump
989
+
990
+ "Looks trivial but isn't" (do **NOT** skip \u2014 small diff, big blast radius):
991
+ - any 1-line change to SQL / regex / auth / billing / permission / signature-verification code
992
+ - flipping a feature-flag default, default config value, or retry/timeout constant
993
+ - changing a money/tax/currency/fee constant by any amount
994
+ - changing an HTTP method, redirect URL, response code, or status enum
995
+ - tightening or loosening a comparison operator (\`<\` \u2194 \`<=\`, \`==\` \u2194 \`!=\`)
996
+ - renaming a public API surface (still trivial in shape, but needs an impact lens)
997
+ - adding a new direct dependency (supply-chain surface)
998
+ - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
999
+ - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
1000
+
1001
+ 4. **lens decision \u2014 0 or 2+, NEVER 1**.
1002
+
1003
+ The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
1004
+
1005
+ Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
1006
+ - the PR is substantive (>5 files changed AND >200 net lines), OR touches a high-stakes subsystem (auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling)
1007
+ - you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
1008
+ - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
1009
+
1010
+ **NEVER dispatch exactly one lens.** A single lens is just a more expensive version of doing the work yourself with a worse model \u2014 it adds wall time and a context-handoff for no orthogonality benefit. Either you have at least two genuinely independent failure-mode hypotheses (dispatch all in one turn), or you don't (do the review yourself).
1011
+
1012
+ When you do go multi-lens, lens framings come in two flavors:
1013
+ - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
1014
+ - **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
1015
+
1016
+ starter menu (combine, omit, or invent your own):
1017
+ - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
1018
+ - **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
1019
+ - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. **only pick when the PR's correctness depends on the contract behaving a specific way** \u2014 not when the API is merely used. The bar is "if the third-party contract differs from what the diff assumes, the PR is incorrect." When dispatched, the subagent must verify load-bearing claims via web search and quote source URLs.
1020
+ - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
1021
+ - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
1022
+ - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
1023
+ - **integration & cross-cutting** \u2014 API contracts between modules, backward-compat of public surfaces, multi-service ordering
1024
+ - **test integrity** \u2014 meaningful coverage for the changed behavior; deterministic; no shared-state pollution
1025
+ - **performance** \u2014 N+1 queries, hot-path allocation, latency budgets, index coverage
1026
+ - **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
1027
+ - **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
1028
+
1029
+ The only subagent type is \`${REVIEWER_AGENT_NAME}\` \u2014 used for lens judgment work ("is this safe / correct / well-tested?"), runs on a mid-tier model.
1030
+
1031
+ 5. **fan out (only if step 4 said 2+ lenses)**: dispatch every \`${REVIEWER_AGENT_NAME}\` subagent for this run **IN A SINGLE ASSISTANT TURN, AS MULTIPLE PARALLEL TASK TOOL_USE BLOCKS IN ONE MESSAGE.**
1032
+
1033
+ \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
1034
+ The default tool-call behavior of Claude Code (and most agent runtimes) is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them. If you find yourself emitting one Task call, then thinking about the result, then emitting another \u2014 STOP and re-issue them all together. The whole point of going multi-lens is the wall-clock speedup from parallel execution; serial dispatch defeats it entirely.
1035
+
1036
+ \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
1037
+ \u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B) \u2192 turn 3 (after B's result) = Task(lens C). This is the failure mode. Do not do this.
1038
+
1039
+ You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches \u2014 concurrent context-pulling on the orchestrator side runs in parallel with the lens fan-out and costs zero extra wall time.
1040
+
1041
+ if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip the fan-out entirely on a single subagent failure. each subagent gets:
1042
+ - **the absolute \`diffPath\` (and \`incrementalDiffPath\` if available) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt** (e.g. \`diffPath: /tmp/terramend-XXXX/pr-NNN-SHA.diff\`). the reviewer's baked-in system prompt selects its FIRST action on this token \u2014 paraphrasing ("review the diff", "look at this PR") sends it down the \`git diff origin/<base>\` fallback, which fails on shallow GHA checkouts. the subagent \`read\`s those files for scope; it must NOT re-derive the diff via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of any commits that landed on \`<base>\` since the branch forked \u2014 pure noise, and the git tool rejects it). reading and codebase exploration are still its job.
1043
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
1044
+ - **a Task \`description\` set to the lens name** (e.g. \`"security"\`, \`"correctness"\`, \`"billing-subsystem"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
1045
+ - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search rather than trust training data, and to quote source URLs in its reasoning. action runs are non-interactive \u2014 there's no human in the loop to catch "I'm pretty sure Stripe does X."
1046
+ - ask the subagent to report findings with file paths and NEW line numbers from the diff so you can anchor inline comments without re-reading the entire diff.
1047
+
1048
+ delegation discipline:
1049
+ - do NOT summarize the PR for them (biases toward a validation frame)
1050
+ - do NOT hand them a curated reading list (let them discover scope)
1051
+ - do NOT pre-shape their output with a finding schema
1052
+ - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
1053
+
1054
+ 6. **aggregate & draft**: when the fan-out lands, merge findings; de-dup overlaps (two lenses catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
1055
+
1056
+ Apply the **Finding precedents** (defined after this checklist, before the body format) to every candidate \u2014 a precedent match means drop, unless you have specific evidence the precedent does not apply here.
1057
+
1058
+ **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the diff replaces or shadows; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the diff implies but doesn't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the diff opens up that aren't a single-line bug. On substantial PRs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
1059
+
1060
+ ${FINDING_VERIFICATION_PASS}
1061
+
1062
+ for surviving findings, draft inline comments with NEW line numbers from the diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
1063
+
1064
+ 7. **submit**: ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
1065
+
1066
+ note: the first create_pull_request_review submission may error with a one-time diff-coverage nudge listing unread TOC regions. retry the same call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session.
1067
+
1068
+ The review body is structured as: \`[optional alert blockquote]\` \u2192 \`[PR summary using the default format below]\`. Inline comments are passed via the \`comments\` parameter, not in the body.
1069
+
1070
+ The opening callout is what the author sees first \u2014 pick the one that matches what you want them to do. Five tiers, from loudest to friendliest:
1071
+
1072
+ - \`[!CAUTION]\` \u2014 large red banner. Reads as "this will break something."
1073
+ - \`[!IMPORTANT]\` \u2014 large purple banner. Reads as "you need to look at this before merging."
1074
+ - \`> \u2139\uFE0F ...\` \u2014 informational blockquote. Reads as "minor suggestions, nothing blocking."
1075
+ - \`> \u2705 ...\` \u2014 green friendly blockquote. Reads as "no concerns, mergeable."
1076
+
1077
+ Two reinforcing levers: callout intensity (above) and \`approved\` (which gates the footer Fix-button affordance \u2014 Fix renders on every non-approving review, so \`approved: true\` suppresses it). Wrapping mergeable feedback in \`[!IMPORTANT]\` trains users to click Fix on reviews that don't need fixing. Pick the tier the author's actual next action justifies.
1078
+
1079
+ - **critical issues** (blocks merge \u2014 bugs, security, data loss, broken core flows):
1080
+ \`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
1081
+ - **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
1082
+ \`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
1083
+ - **minor suggestions only** (single-line nits, doc/comment polish, defer-able observations, "rough edges"):
1084
+ \`approved: false\`. Body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` followed by the PR summary. Include all inline comments via \`comments\`. Vary the wording after the emoji to fit the review (e.g. "Minor suggestions only.", "Two rough edges worth a look."), but always keep the \u2139\uFE0F prefix and keep it short.
1085
+ - **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, surfacing a minor stale doc reference, calling out something noteworthy without recommending a change):
1086
+ \`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary. Do NOT include inline \`comments\` \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
1087
+ - **no actionable issues**:
1088
+ \`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary.
1089
+
1090
+ ${REVIEW_FINDING_PRECEDENTS}
1091
+
1092
+ ${PR_SUMMARY_FORMAT}`
1093
+ },
1094
+ // IncrementalReview shares Review's 0-or-2+ lens pattern AND its body
1095
+ // format (PR_SUMMARY_FORMAT), scoped to the incremental delta against the
1096
+ // prior terramend review. The "issues must be NEW since the last Terramend
1097
+ // review" filter lives at aggregation time (step 8), NOT in the subagent
1098
+ // prompt — pushing the filter into subagents matches the canonical anneal
1099
+ // anti-pattern of "list known pre-existing failures — don't flag these"
1100
+ // and suppresses signal on regressions the new commits amplified. A
1101
+ // separate "Prior review feedback" checklist would duplicate the rolling
1102
+ // PR summary snapshot's record of what earlier runs already addressed and
1103
+ // add noise to the user-facing body. Same opening-callout + per-bullet
1104
+ // emoji severity split as Review.
1105
+ {
1106
+ name: "IncrementalReview",
1107
+ description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
1108
+ prompt: `### Checklist
1109
+
1110
+ 1. **task list**: create your task list for this run as your first action.
1111
+
1112
+ 2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
1113
+
1114
+ 3. **incremental scope**: if \`incrementalDiffPath\` is present, read it to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. if not present, fall back to reviewing the full PR diff and determine what changed since Terramend's most recent review.
1115
+
1116
+ 4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`, then call \`${t("get_review_comments")}\` on each prior Terramend review. Each thread renders as a section whose first line is a fenced tag \`comment author=<login> id=<fullDatabaseId> review=<reviewId> thread=<graphqlId>\`; section headers carry \`[RESOLVED]\` / \`[OUTDATED]\` when relevant. For every **open, Terramend-originated** thread, decide and act:
1117
+
1118
+ - **Terramend-originated** means the FIRST \`comment author=...\` tag in the section is \`author=terramend[bot]\`. The \`*\` marker on individual comments is unrelated \u2014 it flags whether a comment belongs to the queried review, not whether it is the thread root.
1119
+ - **addressed?** read the file at the thread's anchor and judge whether the substantive concern is now resolved by the new commits. Lines being modified isn't enough: reformatting, renaming, or moving the same code elsewhere doesn't address a concern. If the comment raised multiple distinct concerns, ALL must be addressed. The \`[OUTDATED]\` tag means GitHub moved the anchor (line shift, force-push, rename) \u2014 it does NOT mean the concern was addressed; re-read the code at its new location before deciding.
1120
+ - **if addressed**: call \`${t("reply_to_review_comment")}\` with the root tag's numeric \`id=\` as \`comment_id\` (NOT the \`thread=\` value \u2014 that's a separate GraphQL ID used only by resolve) and a one-line body (e.g. \`Addressed in <short-sha>.\`), then call \`${t("resolve_review_thread")}\` with the root tag's \`thread=\` value as \`thread_id\`. Do this BEFORE drafting the new review so the GitHub thread state aligns with the new review by the time it lands.
1121
+ - **if uncertain or partially addressed**: leave open. False-positive resolutions erode trust faster than false negatives.
1122
+ - **scope**: only retire Terramend-originated threads. Threads from human reviewers belong to those humans to resolve, even if the commit happened to address them.
1123
+ - **reaction signal**: comment tags may carry \`reactions=\u{1F44D}N,\u{1F44E}N\` \u2014 human votes on the finding. A \u{1F44E} on a Terramend-originated root comment is the human telling you the finding was wrong or unwanted: do NOT re-raise that finding class in this review, and treat it as false-positive feedback \u2014 when a LEARNINGS file is configured, record the rejected finding class (rule/pattern + why the human rejected it, if discernible) so future reviews stop flagging it. A \u{1F44D} confirms the finding class is valued; no action needed beyond noting it. Reactions never override the addressed/unaddressed judgment for retiring threads \u2014 a \u{1F44D} on an unfixed finding does not make it addressed.
1124
+
1125
+ The remaining open threads feed step 8's dedup filter \u2014 anything already flagged and unchanged by the new commits should not be re-raised. The rolling PR summary snapshot is the durable record of retire activity; you don't need to surface it in the review body.
1126
+
1127
+ 5. **triage**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces. pull as much context as you need to render a confident review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths. **you are the synthesizer.**
1128
+
1129
+ if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 10's non-substantive path (do NOT submit a review).
1130
+
1131
+ "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
1132
+ "Looks trivial but isn't" (do NOT skip \u2014 same anti-patterns as Review mode): 1-line changes to SQL/regex/auth/billing/permissions/signature-verification code; flipping feature-flag defaults or retry/timeout constants; money/tax/HTTP-method/redirect changes; tightening or loosening a comparison operator; mixed diffs with a semantic line buried in formatting.
1133
+ When unsure, treat as non-trivial.
1134
+
1135
+ 6. **lens decision \u2014 0 or 2+, NEVER 1**.
1136
+
1137
+ The default is **0 lenses**: handle the re-review yourself end-to-end. Most incremental reviews land here \u2014 especially thread-reply re-reviews where the user is asking "did you address X?" rather than "review the diff again."
1138
+
1139
+ Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
1140
+ - the incremental changes are substantive (>5 files changed AND >200 net new lines), OR touch a high-stakes subsystem (auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling)
1141
+ - you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
1142
+ - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
1143
+
1144
+ **NEVER dispatch exactly one lens.** Single-lens dispatch adds wall time and cost for no orthogonality benefit. Either go multi-lens (\u22652 in parallel) or do the re-review yourself.
1145
+
1146
+ Lens framing follows Review mode: themed lenses (correctness, security, etc.) and subsystem lenses (auth, billing, schema-migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens.
1147
+
1148
+ 7. **fan out (only if step 6 said 2+ lenses)**: dispatch every \`${REVIEWER_AGENT_NAME}\` subagent for this run **IN A SINGLE ASSISTANT TURN, AS MULTIPLE PARALLEL TASK TOOL_USE BLOCKS IN ONE MESSAGE.**
1149
+
1150
+ \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
1151
+ Default tool-call behavior is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them.
1152
+
1153
+ \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
1154
+ \u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
1155
+
1156
+ You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
1157
+
1158
+ if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body. each subagent gets:
1159
+ - **the absolute diff path(s) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt.** when \`incrementalDiffPath\` is present, name BOTH (\`incrementalDiffPath: /tmp/.../pr-NNN-SHA-incremental.diff\` then \`diffPath: /tmp/.../pr-NNN-SHA.diff\`) \u2014 the reviewer's baked-in prompt reads incremental first and uses full for context; when only \`diffPath\` exists, name it alone. the subagent \`read\`s those files; it must NOT re-derive via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of base-branch progress \u2014 pure noise, and the git tool rejects it), and paraphrasing ("review the new commits") sends it down that fallback, which also fails on shallow GHA checkouts. do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 8), not in the subagent prompt.
1160
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
1161
+ - **a Task \`description\` set to the lens name** \u2014 the harness reads this field to label log lines so parallel runs can be told apart.
1162
+ - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
1163
+ - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
1164
+
1165
+ delegation discipline:
1166
+ - do NOT summarize the changes for them (biases toward validation frame)
1167
+ - do NOT hand them a curated reading list (let them discover scope)
1168
+ - do NOT pre-shape their output with a finding schema
1169
+ - do NOT mention the other lenses (independence is the point)
1170
+
1171
+ 8. **aggregate, draft, self-critique**: merge findings (yours + any subagent output if you went multi-lens); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Terramend review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
1172
+
1173
+ Apply the **Finding precedents** (defined after this checklist, before the body format) to every candidate \u2014 a precedent match means drop, unless you have specific evidence the precedent does not apply here.
1174
+
1175
+ **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the new commits replace or shadow; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the new commits imply but don't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the new commits open up that aren't a single-line bug. On substantial incremental diffs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
1176
+
1177
+ ${FINDING_VERIFICATION_PASS}
1178
+
1179
+ draft inline comments with NEW line numbers from the full PR diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part.
1180
+
1181
+ 9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections + optional \`### \u2139\uFE0F Nitpicks\` + optional suppressed-findings block) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior terramend review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Terramend's most recent review yourself before drafting bullets.
1182
+
1183
+ 10. Submit \u2014 every run must end with EXACTLY ONE of \`${t("create_pull_request_review")}\` (substantive review) or \`${t("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
1184
+
1185
+ Same callout ladder as Review mode \u2014 \`[!CAUTION]\` (red, "will break") \u2192 \`[!IMPORTANT]\` (purple, "must address before merging") \u2192 \`> \u2139\uFE0F ...\` (informational, "minor suggestions only") \u2192 \`> \u2705 ...\` (green friendly, "no concerns"). Same Fix-button lever: the footer renders a Fix button on every non-approving review, so \`approved: true\` suppresses it. Wrapping mergeable feedback in \`[!IMPORTANT]\` trains users to click Fix on reviews that don't need fixing \u2014 pick the tier the author's actual next action justifies.
1186
+
1187
+ Follow these rules:
1188
+ - note: the first create_pull_request_review submission may error with a one-time diff-coverage nudge listing unread TOC regions. retry the same call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session.
1189
+ - IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t("report_progress")}\` with a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
1190
+ - ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary using the default format below.
1191
+ - ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary using the default format below. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
1192
+ - ELSE IF NEW MINOR SUGGESTIONS ONLY (single-line nits, doc/comment polish, defer-able observations, "rough edges"): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
1193
+ - ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly with one minor stale doc reference, or a noteworthy positive observation): call \`${t("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> \u2705 No new issues found.\\n\\n\` (or similar friendly green opener), followed by the PR summary using the default format below. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor.
1194
+ - ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, set \`approved: true\`. body opens with \`> \u2705 No new issues found.\\n\\n\`, followed by the PR summary using the default format below.
1195
+
1196
+ ${REVIEW_FINDING_PRECEDENTS}
1197
+
1198
+ ${PR_SUMMARY_FORMAT}`
1199
+ },
1200
+ {
1201
+ name: "Plan",
1202
+ description: "Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
1203
+ prompt: `### Checklist
1204
+
1205
+ 1. **task list**: create your task list for this run as your first action.
1206
+
1207
+ 2. Analyze the task and gather context:
1208
+ - read AGENTS.md and relevant codebase files
1209
+ - understand the architecture and constraints
1210
+
1211
+ 3. Produce a structured, actionable plan with clear milestones.
1212
+
1213
+ 4. Call \`${t("report_progress")}\` with the plan body. Do NOT set \`target_plan_comment\` \u2014 that flag is exclusively for revising an existing plan, and \`${t("select_mode")}\` will route you to a separate PlanEdit checklist when a prior plan comment exists for this issue.`
1214
+ },
1215
+ {
1216
+ name: "Fix",
1217
+ description: "Fix CI failures; debug failing tests or builds; investigate and resolve check suite failures",
1218
+ prompt: `### Checklist
1219
+
1220
+ 1. **task list**: create your task list for this run as your first action.
1221
+
1222
+ 2. Checkout the PR branch via \`${t("checkout_pr")}\`.
1223
+
1224
+ 3. Fetch check suite logs via \`${t("get_check_suite_logs")}\`.
1225
+
1226
+ 4. **CRITICAL**: verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
1227
+
1228
+ 5. Diagnose and fix:
1229
+ - read the workflow file, reproduce locally with the EXACT same commands CI runs
1230
+ - fix the issue using your native file and shell tools
1231
+ - verify the fix by re-running the exact CI command
1232
+ - review the diff before committing \u2014 verify only the fix is present, no debug artifacts, no unrelated changes. the fix should be clean enough that a senior engineer would approve without hesitation.
1233
+ - commit locally via shell (\`git add . && git commit -m "..."\`)
1234
+
1235
+ 6. Finalize:
1236
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
1237
+ - call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
1238
+ },
1239
+ {
1240
+ name: "ResolveConflicts",
1241
+ description: "Resolve merge conflicts in a PR branch against the base branch",
1242
+ prompt: `### Checklist
1243
+
1244
+ 1. **task list**: create your task list for this run as your first action.
1245
+
1246
+ 2. **Setup**:
1247
+ - Call \`${t("checkout_pr")}\` to get the PR branch.
1248
+ - Call \`${t("get_pull_request")}\` to identify the base branch (e.g., 'main').
1249
+ - Call \`${t("git_fetch")}\` to fetch the base branch.
1250
+
1251
+ 3. **Merge Attempt**:
1252
+ - Run \`git merge origin/<base_branch>\` via shell.
1253
+ - If it succeeds automatically, confirm a clean working tree, push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 4\u20135.**
1254
+ - If it fails (conflicts), resolve them manually (continue to steps 4\u20135).
1255
+
1256
+ 4. **Resolve Conflicts**:
1257
+ - Run \`git status\` or parse the merge output to find the list of conflicting files.
1258
+ - For each conflicting file: read it, find the conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`), understand the code context, and rewrite the file with the correct resolution. Remove all markers.
1259
+ - Verify the file syntax is correct after resolution.
1260
+
1261
+ 5. **Finalize**:
1262
+ - Run a final verification (build/test) to ensure the resolution works.
1263
+ - \`git add . && git commit -m "resolve merge conflicts"\`
1264
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
1265
+ - Call \`${t("report_progress")}\` with a summary of what was resolved (or the exact push error if push failed)`
1266
+ },
1267
+ {
1268
+ name: "Remediate",
1269
+ description: "Bring a repository's Terraform up to best practice: scan with the deterministic check tools, then open one scoped, reviewable PR per concern that fixes it and proves it fixed (\u2717\u2192\u2713).",
1270
+ prompt: `### Checklist
1271
+
1272
+ 1. **task list**: create your task list for this run as your first action.
1273
+
1274
+ 2. **scan**: call \`${t("terraform_scan")}\` to get the best-practice \`concerns\` plus \`groups\` \u2014 one group per file, each with a stable \`id\`, its \`file\`, the group \`severity\` (highest in the file), the distinct \`rule_ids\`, and the \`concern_ids\` it covers. If it reports zero concerns, call \`${t("report_progress")}\` with "Terraform already follows best practice \u2014 nothing to remediate." and **stop**.
1275
+
1276
+ *Alternative concern source:* if the Assessor (terraform-reviewer) has already produced a \`findings.json\` **or a SARIF report** (Trivy/Checkov/tflint \`-o sarif\`) for this repo (one exists in the workspace, or \`$TERRAMEND_FINDINGS_PATH\` is set), call \`${t("read_findings")}\` instead \u2014 it auto-detects the format and returns the **same** \`{concerns, groups}\` shape, so the rest of this checklist is unchanged. Default to \`${t("terraform_scan")}\`; only use \`${t("read_findings")}\` when such a file is present (it returns \`found: false\` otherwise). Note that reviewer-exclusive findings (source \`reviewer\`) can't be re-verified by Terramend's scanners \u2014 see the prove-it step.
1277
+
1278
+ *Multi-root repos (\xA7MCP):* \`${t("terraform_validate")}\` and \`${t("terraform_plan")}\` are now **multi-root aware** \u2014 they automatically validate/plan **every** Terraform root in the repo (e.g. hepcare's \`terraform/\` + \`terraform/core/\`) and aggregate the result (see \`roots_validated\` / \`roots_planned\`), so you do **not** need to loop per-root yourself. Call \`${t("terraform_roots")}\` only when you want to see the root layout (or fix a concern that lives in one specific root).
1279
+
1280
+ 3. **pick scope**: act on **one group per PR** (a group is all of one file's concerns \u2014 different scanners flag the same defect under different rules, so fixing per-file avoids a flood of near-duplicate PRs). Take the **highest-severity group first**. Unless the task explicitly asks for more, open **at most one PR this run**. Skip groups whose severity is only \`info\` unless asked. Use the \`terraform-best-practices\` skill for how to read each concern and apply the *minimal* fix.
1281
+
1282
+ **Autonomy (\xA73.9)**: each group carries an \`autonomy\` field \u2014 \`auto\` (fix and open a normal PR) or \`needs-human\` (a security finding at/above the \`autonomy_threshold\`, or \u2014 once plan runs \u2014 a high blast radius). You still fix and open a PR for a \`needs-human\` group, but you MUST add the \`needs-human\` label (\`${t("add_labels")}\`), open the PR with \`approved: false\` framing, and put a prominent **\u26A0\uFE0F Needs human review** callout at the top of the body listing the group's \`autonomy_reasons\`. Never batch a \`needs-human\` group with others.
1283
+
1284
+ **Dependency order & environment twins (\xA724 / \xA722)**: when a repo has local modules, \`${t("terraform_module_graph")}\` returns \`dependency_order\` \u2014 fix a shared/depended-on module BEFORE its dependents so sequenced PRs don't conflict. \`${t("terraform_roots")}\` returns \`environment_twins\` \u2014 parallel \`dev\`/\`staging\`/\`prod\` (or per-region) stacks that differ only by an environment segment; when the fixed file is one twin, note in the PR that the same fix should be offered for its twins (a separate PR per twin, honouring \`max_prs\`).
1285
+
1286
+ **Grouping & batching (\xA73.11 / \xA73.10)**: groups default to one-per-file. When a single rule dominates across many files (e.g. "add \`tags\` everywhere"), re-scan with \`group_by: "rule"\` so it becomes ONE coherent group/PR instead of many. The scan's \`batch_plan\` tells you which low-risk groups are \`batchable\` (combine them into the single \`batch_plan.batch_branch\` PR when \`max_prs\` would otherwise be exceeded) and which are \`isolated\` (each gets its own PR for independent review/revert). Still honour \`max_prs\` and never batch a \`needs-human\` group.
1287
+
1288
+ **Comment command (\xA73.12)**: when this run was triggered by a \`@terramend fix \u2026\` comment, the triggering body is in your prompt \u2014 honour the requested scope INSTEAD of "highest-severity group": \`fix #<concern-id>\` \u2192 act only on the group containing that concern id; \`fix all <severity>-severity\` \u2192 set the scan \`severity_threshold\` to that level and act on those groups (up to \`max_prs\`); \`fix <file>.tf\` \u2192 act on that file's group; \`fix all\` \u2192 act on the highest-severity groups up to \`max_prs\`. If the comment isn't a recognised fix command, fall back to the default scope. A **strategy suffix** \u2014 \`fix #<concern-id> with strategy B\` (or a bare \`strategy B\` reply on a proposal thread) \u2014 additionally tells you **which** fix to apply: see \xA726 in step 4.
1289
+
1290
+ 4. **for the chosen group**:
1291
+ - **base branch**: this run's base branch is resolved deterministically \u2014 \`${t("create_pull_request")}\` targets the \`base_branch\` input if set, else the repository's default branch (\`main\`, or \`master\`). You do not choose it; just **omit** the \`base\` argument when opening the PR (below) and it is filled in.
1292
+ - **idempotency**: the remediation branch is \`remediate/<group-id>\`. Before doing anything, check whether that branch or an open PR for it already exists (\`${t("git")}\` / \`${t("get_pull_request")}\`). If one exists, update it rather than opening a duplicate.
1293
+ - **branch**: create \`remediate/<group-id>\` from the **current HEAD** (the checkout that was just scanned) via \`${t("git")}\` (\`git checkout -b remediate/<group-id>\`). Do NOT switch to a different base first \u2014 branching from the scanned checkout keeps the PR diff to exactly your fix.
1294
+ - **honest refusal (\xA729 \u2014 decide BEFORE fixing)**: if the group's concerns appear in the scan's \`refusal_candidates\` (the fix needs a human decision \u2014 narrowing an IAM wildcard, a KMS key policy, a real ingress CIDR), do **not** guess a fix that could break the stack. Instead open a structured issue (\`${t("create_issue")}\`) describing the concern, why it isn't auto-fixed, and what a human should do, and skip the PR for that group. A proven fix or an honest refusal \u2014 never a guessed, unverifiable PR.
1295
+ - **propose, then let me steer (\xA726 \u2014 when there's no single right fix)**: distinct from \xA729 (which refuses a fix a human must *decide*), \xA726 is for a finding with **2\u20133 genuinely distinct, defensible fixes** that differ in trade-offs, not correctness (e.g. encrypt with an AWS-managed key **vs** a customer-managed KMS key; a narrow security-group rule **vs** a prefix list **vs** a VPC endpoint). When such a fork exists **and the triggering comment did not already select a strategy**, do **not** silently pick for the reviewer: via \`${t("create_issue_comment")}\` post one short comment listing the options as **A / B / C** \u2014 each a single line (what it does + its trade-off) \u2014 and ask the reviewer to reply \`@terramend fix #<concern-id> with strategy <A|B|C>\`. Then **skip the PR for this group** this run and note it in your final report (it resumes when the reviewer replies). When the comment **did** select one (\`fix #<id> with strategy B\`, or a bare \`strategy B\` reply on the proposal thread), apply **exactly** that strategy \u2014 don't second-guess it. Reserve this for real forks in the road; a fix with one obvious correct answer just gets made.
1296
+ - **fix**: edit the group's file(s), using your native file tools. For a by-file group that's the single \`file\`; for a **by-rule group (\xA73.11)** it's every entry in \`files\` (fix the one rule everywhere it fires). Resolve **every** concern in the group \u2014 when the scan's \`co_located\` shows several scanners flagged the same \`file:line\` (\xA730), they're one underlying defect: write ONE canonical fix and one explanation, not separate edits. **Only touch \`*.tf\` / \`*.tfvars\` files.** Make the smallest changes that clear the concerns \u2014 do NOT reformat or refactor unrelated code (see *SYSTEM* surgical-change rules). **Module-source awareness (\xA74.14):** call \`${t("terraform_module_graph")}\` first \u2014 if the concern's file is inside a \`local_module_dir\`, fix it ONCE at the module source (it propagates to all callers; note them in the PR); if the fix would require editing a registry/git/remote module, you can't fix it here \u2014 report it (open an issue naming the upstream module + version) instead. **Approved modules (\xA74.14):** call \`${t("list_modules")}\` and prefer a catalogue module (registry or house, pinned) when the fix is genuinely a module swap \u2014 but for a one-line fix on an existing raw resource, fix it in place. **Provider-major awareness (\xA74.15):** before introducing an argument or block, check \`terraform_validate\`'s \`providers\` list for the pinned \`major\` \u2014 argument names and valid blocks differ across majors. After the dir is init-ed (validate/plan ran), you can **verify an argument exists** for the installed provider with \`${t("terraform_provider_schema")}\` (pass the resource type + the arg names you added; it returns any \`unknown_args\` that would break \`plan\`). **Reusing a module?** call \`${t("terraform_module_interface")}\` on its dir to get its real \`variable\` names + which are required, so the \`module\` block you write is correct.
1297
+ - **keep the module's tests/examples consistent (\xA728 \u2014 only when you fixed a reusable module)**: if the file(s) you changed live inside a \`local_module_dir\` (from \`${t("terraform_module_graph")}\`) AND your fix changed the module's public interface (added/removed/renamed a \`variable\`, tightened a type), call \`${t("terraform_module_tests")}\` with that module dir. It returns the module's existing \`examples/\` fixtures + \`terraform test\` (\`*.tftest.hcl\`) / Go Terratest files and the \`drift\` per asset \u2014 \`missing_required\` (a variable the asset must now set) and \`unknown_set\` (a variable the asset references that no longer exists). Update **exactly** the drifting assets so they match the new interface; **never weaken, delete, or comment out an assertion just to make a test pass** \u2014 a fix that breaks a module's contract is the test doing its job, so correct the fix or the fixture, not the assertion. \`examples/\` are \`*.tf\` (always within the push allow-list); native \`*.tftest.hcl\` / Go \`*_test.go\` files are only pushable when the \`terratest\` input is enabled \u2014 when it isn't and only those drift, note the needed test update in the PR body for a human rather than leaving the module's own tests broken. Skip this entirely for a one-off raw-resource fix that doesn't touch a module interface.
1298
+ - **validate**: call \`${t("terraform_validate")}\`. If it does not pass, fix what it reports or abandon this group \u2014 **never open a PR whose validate did not pass**. Its \`providers\` field carries the pinned provider majors (use them as above). It also returns \`unknown_arguments\` (\xA74.15-next): arguments you wrote that are NOT in the installed provider's schema and would break \`plan\` \u2014 treat any entry as a must-fix (correct the argument for the pinned major) even though \`passed\` doesn't gate on it. \`schema_checked: false\` means the schema wasn't available (rely on \`${t("terraform_plan")}\` then).
1299
+ - **policy gate (optional, \xA73.5)**: if the repo ships policy-as-code (a \`policy/\`, \`policies/\`, or \`.conftest\` dir of Rego), call \`${t("policy_check")}\` \u2014 it runs \`conftest\` against the plan JSON. It degrades green (\`ok: false\`) when conftest or a policy dir is absent. When it returns \`passed: false\`, treat it exactly like a failed validate: fix the violation (listed in \`failures\`) or label the PR \`needs-human\` and surface it \u2014 never push past a policy denial.
1300
+ - **plan (safety gate \u2014 do this BEFORE pushing)**: call \`${t("terraform_plan")}\`. It auto-skips (returns \`ran: false\`) when no cloud credentials / the terraform CLI are present, or init/plan can't complete \u2014 then carry on. When it returns \`ran: true\`, add a one-line **Plan** note to the PR body (e.g. \`Plan: +0 ~1 -0\`) and act on three signals:
1301
+ - **destroy/replace (\`has_destroy_or_replace\`)**: treat it as a stop sign \u2014 a best-practice remediation should rarely destroy or replace a resource. Any entry in \`stateful_destructive\` (a data-bearing resource: RDS, S3, EBS, a SQL database, \u2026) will be **hard-blocked at \`${t("push_branch")}\`** by a code-level guardrail unless the operator set the \`allow_replace\` input for that address \u2014 so do not rely on narration: if the change would destroy/replace a stateful resource and that is not clearly intended, **abandon this group** and report it rather than attempting the push. List the \`destructive\` resources in your report either way.
1302
+ - **blast radius (\`blast_radius.tier\`)**: add it to the PR body (e.g. \`Blast radius: low (1 resource)\`). When the tier is \`high\` (more than 10 resources, or the change spans more than one module), add a prominent **\u26A0\uFE0F Large blast radius \u2014 review carefully** callout to the PR body so a reviewer knows this is not a one-line change.
1303
+ - **idempotency (\`idempotent\`)**: when it is \`false\`, the second plan disagreed with the first \u2014 a perpetual-diff smell (a non-deterministic value such as \`timestamp()\`/\`uuid()\`/an unkeyed \`random_*\`). **Prefer to fix the non-determinism or abandon the group**; if you still open the PR, surface \`idempotency_warning\` prominently as a **\u26A0\uFE0F Non-deterministic plan** caveat. (Note: this catches in-config non-determinism only \u2014 Terramend never applies, so a provider-normalisation perpetual diff can't be detected here.)
1304
+ - **needs-human (\`needs_human\`, \xA72.6\u2192\xA73.9)**: when \`true\`, the plan crossed a deterministic escalation line (high blast radius, a stateful destroy/replace, or a non-deterministic plan \u2014 see \`needs_human_reasons\`). Add the \`needs-human\` label (\`${t("add_labels")}\`) and a louder callout.
1305
+ - **full plan (\`plan_text\`, \xA71.2)**: when present, attach it to the PR body as a collapsed \`<details><summary>Plan</summary>\\n\\n\\\`\\\`\\\`\\n\u2026\\n\\\`\\\`\\\`\\n</details>\` block so a reviewer can see the exact planned change without re-running it.
1306
+ - **commit + push**: \`git add\` only the file you changed, commit with a message naming the file and the key rules (e.g. \`fix(tf): harden main.tf \u2014 S3 encryption + block public access\`), then \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*).
1307
+ - **open PR \u2014 with a COMPLETE body (MANDATORY)**: \`${t("create_pull_request")}\` (omit \`base\` \u2014 it resolves to the run's base branch above). The PR body is the primary deliverable a human reviews \u2014 open the PR **with a full body**, never a placeholder you intend to fill in later. At an absolute minimum the body MUST explain, in plain English: (a) **what was wrong** \u2014 each concern by \`rule_id\` + its \`evidence\`; and (b) **what you changed** to fix each one, and why it's safe. Build it with the **Remediation PR format** at the end of this checklist (status banner \u2192 title + badges \u2192 \`## What changed\` with the \xA75.17 *Was / Changed / Safe because* note per concern). \u26A0\uFE0F \`${t("report_progress")}\` writes the GitHub Actions **job summary**, which is NOT the PR body \u2014 a good job summary does **not** substitute for a complete PR body. If you only have time/budget for one, the PR body wins.
1308
+ - **prove it (\u2717\u2192\u2713)**: call \`${t("terraform_verify_remediation")}\` with the group's \`concern_ids\`. It re-runs the scanners and returns the authoritative \`resolved\` / \`remaining\` sets and a \`verified\` flag \u2014 this is the proof, do NOT eyeball a scan or self-report. Then \`${t("update_pull_request_body")}\` to add a "Validation" section built **from that result**: one \`\u2717 \u2192 \u2713 <rule_id> resolved\` line per id in \`resolved\`, and list every id in \`remaining\` honestly as still-open. Never mark a concern \u2713 unless the tool returned it in \`resolved\`. Act on two more fields it returns:
1309
+ - **regressions (\xA71.4)**: when \`has_regressions\` is true, the fix INTRODUCED new concerns (listed in \`regressions\`) that weren't there before \u2014 it traded one defect for another. Add a prominent **\u26A0\uFE0F Regression** callout listing them, add the \`needs-human\` label (\`${t("add_labels")}\`), and prefer reworking the fix to remove the regression before relying on the PR.
1310
+ - **confidence (\xA75.19)**: render the returned \`confidence\` (high/medium/low) as a one-line badge in the PR body (e.g. \`Confidence: high\`) with its \`confidence_reasons\`. It is computed deterministically from the verification evidence (verified + no regressions + plan idempotency + blast radius + cost) \u2014 report it verbatim, do NOT inflate it.
1311
+ - **per-finding explanation (\xA75.17)**: in the PR body, give each resolved concern a short three-line note \u2014 **Was** (what the scanner flagged, from its \`evidence\`), **Changed** (what your fix did), **Safe because** (why it's correct/non-breaking) \u2014 and hyperlink the \`rule_id\` to its documentation. The scan output carries a \`doc_url\` per concern (and \`doc_urls\` per group); use it, falling back to the concern's \`remediation_hint\` when no \`doc_url\` is present.
1312
+ - **compliance crosswalk (optional, \xA723)**: for a security-relevant fix, call \`${t("terraform_compliance_crosswalk")}\` with the group's \`concerns\` to get the UK/general frameworks + controls it touches (NCSC Cloud Principles, Cyber Essentials, NHS DSPT, Secure by Design, CIS, SOC 2). Add a short **## Compliance** note from \`by_framework\`, prefixed "Indicative alignment (crosswalk v{version}) \u2014 not an audit verdict." Skip when nothing maps.
1313
+ - **prevent recurrence (\xA721)**: the scan's \`prevention\` map gives a CI guardrail per \`rule_id\` (a Checkov hard-fail entry, a tflint rule, a \`trivy config\` gate, an \`fmt -check\` step). Add a short **\u{1F6E1}\uFE0F Prevent recurrence** note to the PR body with the suggested \`mechanism\` + \`snippet\` so the team can stop this class of concern coming back \u2014 clearly marked as an optional follow-up, not part of this PR's diff.
1314
+ - **cost impact (optional)**: call \`${t("infracost_diff")}\` to estimate the monthly cost change the fix introduces. It auto-skips (returns \`ran: false\`) when \`INFRACOST_API_KEY\` or the infracost CLI is absent \u2014 in that case add nothing. When it returns \`ran: true\`, add a one-line **Cost impact** note to the PR body from its result: e.g. \`\u{1F4B0} Cost impact: +$12.40/mo\` for an increase, \`-$3.10/mo\` for a decrease, \`no change\` when \`monthly_delta\` is 0, or \`~$X/mo (baseline unavailable)\` when \`monthly_delta\` is null. When it returns \`needs_human: true\` (\xA74.16 \u2014 the increase crossed the \`cost_increase_block_usd\` threshold), add the \`needs-human\` label (\`${t("add_labels")}\`) and surface \`cost_escalation_reason\` prominently so a large spend increase isn't merged blindly.
1315
+
1316
+ 5. **guardrails** (always): one scoped PR per group, never a mega-PR spanning multiple files; **never auto-merge** and always leave the PR for human review; never modify files outside \`*.tf\` / \`*.tfvars\`.
1317
+
1318
+ 6. **finalize**: call \`${t("report_progress")}\` once with a summary \u2014 which file/group was fixed, the PR link, and the \u2717\u2192\u2713 result (or the exact tool error if push/PR creation failed).
1319
+
1320
+ **SARIF for code-scanning (optional, \xA73.5)**: when the workflow has a SARIF upload step (it grants \`security-events: write\` and runs \`github/codeql-action/upload-sarif\` on a \`terramend.sarif\`), call \`${t("terraform_emit_sarif")}\` once at the end so the full scan also lands in the repo's Security tab \u2014 complementary to the fix PR, not a replacement for it.
1321
+
1322
+ ${REMEDIATION_PR_FORMAT}`
1323
+ },
1324
+ {
1325
+ name: "RefreshRemediation",
1326
+ description: "Self-heal stale Terramend remediation PRs (\xA727): when a remediation PR's base branch has moved, re-derive the fix on the current base and force-update it, or close it if the concern is already resolved upstream. Best run on a schedule.",
1327
+ prompt: `### Checklist
1328
+
1329
+ This mode keeps already-open Terramend remediation PRs healthy. A remediation PR is "the current base + a minimal, proven fix"; when the base advances, the PR's diff is computed against an old base and the concern may have been resolved upstream. This sweep re-derives each stale fix on the **current** base (it never \`git merge\`s the base in \u2014 re-deriving avoids conflict resolution and keeps the PR diff to exactly the fix). The run starts checked out on the base/default branch.
1330
+
1331
+ 1. **task list**: create your task list for this run as your first action.
1332
+
1333
+ 2. **find stale PRs**: call \`${t("list_remediation_prs")}\`. It returns each open \`remediate/<id>\` / \`terramend/generate-<slug>\` PR with a \`recommended_action\`:
1334
+ - \`skip\` \u2014 the base hasn't moved; the fix is still current. Do nothing.
1335
+ - \`escalate\` \u2014 a human pushed commits to the branch; auto-refresh would overwrite their work. Add the \`needs-human\` label (\`${t("add_labels")}\`) and post ONE short comment (\`${t("create_issue_comment")}\`) noting the base moved and a manual rebase is needed. Never touch the branch.
1336
+ - \`refresh\` \u2014 the base advanced; act on it in step 4.
1337
+
1338
+ If there are zero \`refresh\`/\`escalate\` PRs, call \`${t("report_progress")}\` with "No stale Terramend remediation PRs \u2014 all open fixes are current." and **stop**.
1339
+
1340
+ 3. **scan the current base once**: call \`${t("terraform_scan")}\` (the checkout is on the base). This is the authoritative current-base concern set \u2014 you use it both to decide whether a stale PR's concern is already resolved and to re-derive the fix. Note each group's stable \`id\` (it matches the PR branch's \`<id>\`).
1341
+
1342
+ 4. **for each \`refresh\` PR** (act on at most \`max_prs\` of them this run \u2014 process the highest-severity groups first; report the rest as deferred):
1343
+ - **do NOT \`${t("checkout_pr")}\`** the stale PR \u2014 you are re-deriving from the current base, not editing the old branch. Read the PR with \`${t("get_pull_request")}\` if you need its body/number.
1344
+ - **resolved-on-base? \u2192 close it**: look for a group in step 3's scan whose \`id\` equals the PR's \`group_id\` (for a by-rule/batch PR, match on the concern ids it covered). If **no** current group/concern corresponds, the concern was already fixed on the base (a human fix, or a base change removed the file) \u2014 the PR is redundant. Call \`${t("close_pull_request")}\` with a one-line \`comment\` explaining it's resolved on the base. Do not push anything for this PR.
1345
+ - **still present \u2192 re-derive the fix on the current base**:
1346
+ - **branch**: recreate the remediation branch at the current base HEAD via \`${t("git")}\` (\`git checkout -B remediate/<id>\`) \u2014 \`-B\` force-resets it to the just-scanned base so the diff is only your fix.
1347
+ - **fix \u2192 validate \u2192 plan \u2192 keep tests consistent (\xA728) \u2192 prove it**: apply the minimal fix for that group exactly as in **Remediate** step 4 (same \`${t("terraform_validate")}\`, \`${t("terraform_plan")}\`, \`${t("terraform_module_tests")}\`, and \`${t("terraform_verify_remediation")}\` gates, and the same guardrails \u2014 never open/keep a PR whose validate didn't pass, abandon a group that would destroy a stateful resource, etc.).
1348
+ - **force-update the PR branch**: \`${t("push_branch")}\` with \`force: true\` (the PR already exists; force-updating its branch refreshes it in place \u2014 do NOT open a second PR). The Terraform-only / secret / destroy guardrails still run at push time.
1349
+ - **refresh the body**: \`${t("update_pull_request_body")}\` rebuilt from the fresh \`${t("terraform_verify_remediation")}\` result (the Remediation PR format below), and add a one-line note that it was rebased onto the current base (\`<short-sha>\`) on this run.
1350
+ - Honour every Remediate guardrail: one scoped group per PR, never auto-merge, never modify non-\`*.tf\`/\`*.tfvars\` files.
1351
+
1352
+ 5. **finalize**: call \`${t("report_progress")}\` once with a summary \u2014 how many PRs were refreshed, closed-as-resolved, or escalated, with their links (or the exact tool error if a push/close failed).
1353
+
1354
+ ${REMEDIATION_PR_FORMAT}`
1355
+ },
1356
+ {
1357
+ name: "GenerateTerraform",
1358
+ description: "Generate new Terraform from a plain-English requirement (or a description of desired infrastructure) so it starts best-practice: secure defaults, pinned versions, parameterised, validated, and opened as one reviewable PR.",
1359
+ prompt: `### Checklist
1360
+
1361
+ 1. **task list**: create your task list for this run as your first action.
1362
+
1363
+ 2. **understand the requirement**: read the prompt / issue body and restate, in your task list, exactly what infrastructure is being asked for (provider, resources, environment, constraints). If something that materially changes the Terraform is ambiguous (region, sizing, public vs private, naming), make the **most secure, conventional** choice and record the assumption for the PR body \u2014 do NOT invent unrequested resources or scope-creep.
1364
+
1365
+ 3. **branch**: create \`terramend/generate-<short-slug>\` from the **current HEAD** via \`${t("git")}\` (\`git checkout -b terramend/generate-<short-slug>\`; slug from the requirement, e.g. \`terramend/generate-s3-static-site\`). The PR's base branch is resolved deterministically by \`${t("create_pull_request")}\` (the \`base_branch\` input, else the repository's default branch \u2014 \`main\`, or \`master\`) \u2014 you do not choose it; just omit the \`base\` argument when opening the PR.
1366
+
1367
+ 4. **generate (best practice from the first line)**: write the Terraform with your native file tools, guided by the \`terraform-best-practices\` skill. Requirements:
1368
+ - **Only create \`*.tf\` / \`*.tfvars\` files** (a code-level guardrail blocks the push otherwise). Split conventionally \u2014 \`main.tf\` (resources), \`variables.tf\`, \`outputs.tf\`, \`versions.tf\` (pin \`required_version\` + \`required_providers\` with version constraints). Match the repo's existing layout when one exists.
1369
+ - **Secure by default, not as an afterthought**: encryption at rest and in transit, block public access, least-privilege IAM (no wildcard actions/resources), IMDSv2, private networking, logging/versioning where supported. No \`0.0.0.0/0\` to admin/database ports. No plaintext secrets.
1370
+ - **Parameterise**: expose environment-specific / likely-to-change values as typed \`variable\`s with \`description\`s and safe defaults; use \`locals\` for derived values. Never hardcode magic values.
1371
+ - **Prefer modules**: call \`${t("list_modules")}\` first \u2014 it returns the operator's \`module_catalogue\` AND \`discovered_house_modules\` (local modules already used in this repo). Build on those (registry, a private git module library, or a house module), using their exact variable names and pinning the \`version\` (a git module's pin is its \`?ref=\`). Otherwise use a well-maintained public registry module (e.g. the \`terraform-aws-modules\` collection, pinned) over hand-rolled resources when one cleanly fits.
1372
+ - **Module layout (\xA728)**: when you generate a **reusable module**, follow the standard layout (\`main.tf\` / \`variables.tf\` (typed + \`description\` + validation) / \`outputs.tf\` / \`versions.tf\` / \`README.md\`). Do **not** generate \`examples/\` fixtures. When building a \`module\` block that consumes an EXISTING module, call \`${t("terraform_module_interface")}\` on its dir first to use its real variable names. When the \`terratest\` input is enabled, also call \`${t("scaffold_terratest")}\` (module name + its dir; pass the module's variables from \`${t("terraform_module_interface")}\`) to generate a plan-only Go Terratest test **and** a Terraform-native \`*.tftest.hcl\` \u2014 both plan the module directly \u2014 and write the returned files. Terramend never runs the tests (no cloud creds) \u2014 note in the PR that they should be run in the user's pipeline.
1373
+ - **Tag** resources consistently where the provider supports it.
1374
+
1375
+ 5. **validate (must pass before any PR)**: call \`${t("terraform_validate")}\` (fmt + validate + tflint). Fix whatever it reports \u2014 **never open a PR whose validate did not pass.**
1376
+
1377
+ 6. **self-scan (the generated code must itself be clean)**: call \`${t("terraform_scan")}\`. Generated Terraform is meant to START best-practice, so the scan should report zero concerns in the files you wrote. If it surfaces any, fix them and re-scan until clean \u2014 or, if a concern is a deliberate, justified exception, call it out explicitly in the PR body. Do not ship generated code that trips the scanners.
1378
+
1379
+ 7. **cost (optional)**: call \`${t("infracost_diff")}\` to estimate the monthly cost of what you're creating. It auto-skips when \`INFRACOST_API_KEY\` / the infracost CLI is absent. When it returns \`ran: true\`, add a one-line **\u{1F4B0} Cost impact** note to the PR body (for new infra the \`monthly_delta\` is the full cost of the addition).
1380
+
1381
+ 8. **finalize**:
1382
+ - confirm a clean working tree (only your new \`*.tf\`/\`*.tfvars\` files), then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*).
1383
+ - open a PR via \`${t("create_pull_request")}\` (omit \`base\` \u2014 it resolves to the run's base branch above). Use the **Remediation PR format** conventions (the same status banner + badge row + \`## What changed\` shape, and the body-wide rules) ADAPTED for generation: the body states the requirement, what was generated, the key best-practice choices (security defaults, parameters, modules, pinned versions) and any assumptions; the badge row carries \`Plan\`/\`Cost\` when those tools ran; and in place of the \`## Validation (\u2717 \u2192 \u2713)\` section put a \`## Validation\` line stating \`terraform_validate\` passed and \`terraform_scan\` is clean (self-scan: 0 concerns).
1384
+ - **never auto-merge** \u2014 leave the PR for human review.
1385
+ - call \`${t("report_progress")}\` once with the PR link (or the exact tool error if push/PR creation failed).
1386
+
1387
+ ${REMEDIATION_PR_FORMAT}`
1388
+ },
1389
+ {
1390
+ name: "Task",
1391
+ description: "General-purpose tasks that don't fit other modes: answering questions, adding comments, labeling, running ad-hoc commands, or any direct request",
1392
+ prompt: `### Checklist
1393
+
1394
+ 1. **task list**: create your task list for this run as your first action.
1395
+
1396
+ 2. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly \u2014 but your answer only reaches the user through \`${t("report_progress")}\` (step 4); raw assistant text is discarded.
1397
+
1398
+ 3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
1399
+ - plan your approach before starting
1400
+ - use native file and shell tools for local operations
1401
+ - use ${terramendMcpName} MCP tools for GitHub/git operations
1402
+ - if code changes are needed: review your own diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
1403
+
1404
+ 4. Finalize:
1405
+ - if code changes were made, push to a pull request (new or existing) using \`${t("push_branch")}\` and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
1406
+ - call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
1407
+ - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
1408
+ }
1409
+ ];
1410
+ }
1411
+ var modes = computeModes("opencode");
1412
+ var BUILTIN_MODE_NAMES = modes.map((m) => m.name);
1413
+
1414
+ // src/utils/buildTerramendFooter.ts
1415
+ var TERRAMEND_DIVIDER = "<!-- TERRAMEND_DIVIDER_DO_NOT_REMOVE_PLZ -->";
1416
+ function providerDisplayName(slug) {
1417
+ try {
1418
+ const key = getModelProvider(slug);
1419
+ const meta = providers[key];
1420
+ return meta?.displayName ?? key;
1421
+ } catch {
1422
+ return slug;
1423
+ }
1424
+ }
1425
+ function formatModelLabel(params) {
1426
+ const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
1427
+ // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
1428
+ // a stored alias slug, find the alias whose resolve target matches so we
1429
+ // still render a friendly display name.
1430
+ modelAliases.find((a) => a.resolve === params.model || a.openRouterResolve === params.model);
1431
+ const displayName = alias?.displayName ?? params.model;
1432
+ const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
1433
+ if (!params.fallbackFrom) return base;
1434
+ return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} not configured)`;
1435
+ }
1436
+ function buildTerramendFooter(params) {
1437
+ const parts = [];
1438
+ if (params.customParts) {
1439
+ parts.push(...params.customParts);
1440
+ }
1441
+ if (params.triggeredBy) {
1442
+ parts.push("via Terramend");
1443
+ }
1444
+ if (params.model) {
1445
+ parts.push(
1446
+ `Using ${formatModelLabel({ model: params.model, fallbackFrom: params.fallbackFrom })}`
1447
+ );
1448
+ }
1449
+ return `
1450
+
1451
+ ${TERRAMEND_DIVIDER}
1452
+ <sup>${parts.join(" \uFF5C ")}</sup>`;
1453
+ }
1454
+ function stripExistingFooter(body) {
1455
+ const dividerIndex = body.indexOf(TERRAMEND_DIVIDER);
1456
+ if (dividerIndex === -1) {
1457
+ return body;
1458
+ }
1459
+ return body.substring(0, dividerIndex).trimEnd();
1460
+ }
1461
+
1462
+ // src/utils/codexOAuth.ts
1463
+ var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1464
+ var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
1465
+ var OAuthInvalidGrantError = class extends Error {
1466
+ status;
1467
+ constructor(status, body) {
1468
+ super(`Codex token refresh failed: ${status} ${body}`);
1469
+ this.name = "OAuthInvalidGrantError";
1470
+ this.status = status;
1471
+ }
1472
+ };
1473
+ async function refreshCodexAuthBody(body) {
1474
+ const response = await fetch(CODEX_OAUTH_TOKEN_URL, {
1475
+ method: "POST",
1476
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1477
+ body: new URLSearchParams({
1478
+ grant_type: "refresh_token",
1479
+ refresh_token: body.tokens.refresh_token,
1480
+ client_id: CODEX_OAUTH_CLIENT_ID
1481
+ }).toString(),
1482
+ signal: AbortSignal.timeout(1e4)
1483
+ });
1484
+ if (!response.ok) {
1485
+ const text = await response.text().catch(() => "");
1486
+ if (response.status >= 400 && response.status < 500) {
1487
+ throw new OAuthInvalidGrantError(response.status, text);
1488
+ }
1489
+ throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
1490
+ }
1491
+ const tokens = await response.json();
1492
+ const idToken = tokens.id_token ?? body.tokens.id_token;
1493
+ const accountId = body.tokens.account_id;
1494
+ return {
1495
+ auth_mode: "chatgpt",
1496
+ tokens: {
1497
+ access_token: tokens.access_token,
1498
+ refresh_token: tokens.refresh_token,
1499
+ ...idToken ? { id_token: idToken } : {},
1500
+ ...accountId ? { account_id: accountId } : {}
1501
+ },
1502
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString()
1503
+ };
1504
+ }
1505
+ function decodeJwtExpMs(token) {
1506
+ const parts = token.split(".");
1507
+ if (parts.length !== 3) return null;
1508
+ let payload;
1509
+ try {
1510
+ payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1511
+ } catch {
1512
+ return null;
1513
+ }
1514
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
1515
+ return payload.exp * 1e3;
1516
+ }
1517
+ function parseCodexAuthBody(raw) {
1518
+ let parsed;
1519
+ try {
1520
+ parsed = JSON.parse(raw);
1521
+ } catch {
1522
+ return null;
1523
+ }
1524
+ if (!parsed || typeof parsed !== "object") return null;
1525
+ const v = parsed;
1526
+ if (v.auth_mode !== "chatgpt") return null;
1527
+ const tokens = v.tokens;
1528
+ if (!tokens || typeof tokens !== "object") return null;
1529
+ const t = tokens;
1530
+ if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
1531
+ if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
1532
+ return {
1533
+ auth_mode: "chatgpt",
1534
+ tokens: {
1535
+ access_token: t.access_token,
1536
+ refresh_token: t.refresh_token,
1537
+ ...typeof t.id_token === "string" ? { id_token: t.id_token } : {},
1538
+ ...typeof t.account_id === "string" ? { account_id: t.account_id } : {}
1539
+ },
1540
+ ...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
1541
+ };
1542
+ }
1543
+ function stringifyCodexAuthBody(body) {
1544
+ return `${JSON.stringify(body, null, 2)}
1545
+ `;
1546
+ }
1547
+
1548
+ // src/utils/leapingComment.ts
1549
+ var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
1550
+ function isLeapingIntoActionCommentBody(body) {
1551
+ const content = stripExistingFooter(body).trimStart();
1552
+ const firstLine = content.split(/\r?\n/, 1)[0]?.trimEnd() ?? "";
1553
+ return new RegExp(`(^|\\s)${LEAPING_INTO_ACTION_PREFIX}(\\.\\.\\.)?$`).test(firstLine);
1554
+ }
1555
+
1556
+ // src/utils/learningsTruncate.ts
1557
+ var MAX_LEARNINGS_LENGTH = 1e5;
1558
+ var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
1559
+ function truncateAtLineBoundary(body, cap) {
1560
+ if (body.length <= cap) return body;
1561
+ const head = body.slice(0, cap);
1562
+ const lastNewline = head.lastIndexOf("\n");
1563
+ if (lastNewline <= 0) return head;
1564
+ if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
1565
+ return head.slice(0, lastNewline);
1566
+ }
1567
+
1568
+ // src/utils/progressComment.ts
1569
+ async function getProgressComment(ctx, comment) {
1570
+ const result = await (comment.type === "review" ? ctx.octokit.rest.pulls.getReviewComment({
1571
+ owner: ctx.owner,
1572
+ repo: ctx.repo,
1573
+ comment_id: comment.id
1574
+ }) : ctx.octokit.rest.issues.getComment({
1575
+ owner: ctx.owner,
1576
+ repo: ctx.repo,
1577
+ comment_id: comment.id
1578
+ }));
1579
+ return {
1580
+ id: result.data.id,
1581
+ body: result.data.body ?? void 0,
1582
+ html_url: result.data.html_url
1583
+ };
1584
+ }
1585
+ async function updateProgressComment(ctx, comment, body) {
1586
+ const result = await (comment.type === "review" ? ctx.octokit.rest.pulls.updateReviewComment({
1587
+ owner: ctx.owner,
1588
+ repo: ctx.repo,
1589
+ comment_id: comment.id,
1590
+ body
1591
+ }) : ctx.octokit.rest.issues.updateComment({
1592
+ owner: ctx.owner,
1593
+ repo: ctx.repo,
1594
+ comment_id: comment.id,
1595
+ body
1596
+ }));
1597
+ return {
1598
+ id: result.data.id,
1599
+ body: result.data.body ?? void 0,
1600
+ html_url: result.data.html_url,
1601
+ node_id: result.data.node_id
1602
+ };
1603
+ }
1604
+ async function deleteProgressCommentApi(ctx, comment) {
1605
+ if (comment.type === "review") {
1606
+ await ctx.octokit.rest.pulls.deleteReviewComment({
1607
+ owner: ctx.owner,
1608
+ repo: ctx.repo,
1609
+ comment_id: comment.id
1610
+ });
1611
+ return;
1612
+ }
1613
+ await ctx.octokit.rest.issues.deleteComment({
1614
+ owner: ctx.owner,
1615
+ repo: ctx.repo,
1616
+ comment_id: comment.id
1617
+ });
1618
+ }
1619
+ async function createLeapingProgressComment(ctx, target, body) {
1620
+ if (target.kind === "reviewReply") {
1621
+ try {
1622
+ const result2 = await ctx.octokit.rest.pulls.createReplyForReviewComment({
1623
+ owner: ctx.owner,
1624
+ repo: ctx.repo,
1625
+ pull_number: target.pullNumber,
1626
+ comment_id: target.replyToCommentId,
1627
+ body
1628
+ });
1629
+ return {
1630
+ comment: { id: result2.data.id, type: "review" },
1631
+ body: result2.data.body ?? void 0,
1632
+ html_url: result2.data.html_url
1633
+ };
1634
+ } catch (error) {
1635
+ console.warn(
1636
+ `[progressComment] review reply failed (parent ${target.replyToCommentId} on PR #${target.pullNumber}), falling back to issue comment:`,
1637
+ error
1638
+ );
1639
+ const fallback = await ctx.octokit.rest.issues.createComment({
1640
+ owner: ctx.owner,
1641
+ repo: ctx.repo,
1642
+ issue_number: target.pullNumber,
1643
+ body
1644
+ });
1645
+ return {
1646
+ comment: { id: fallback.data.id, type: "issue" },
1647
+ body: fallback.data.body ?? void 0,
1648
+ html_url: fallback.data.html_url
1649
+ };
1650
+ }
1651
+ }
1652
+ const result = await ctx.octokit.rest.issues.createComment({
1653
+ owner: ctx.owner,
1654
+ repo: ctx.repo,
1655
+ issue_number: target.issueNumber,
1656
+ body
1657
+ });
1658
+ return {
1659
+ comment: { id: result.data.id, type: "issue" },
1660
+ body: result.data.body ?? void 0,
1661
+ html_url: result.data.html_url
1662
+ };
1663
+ }
1664
+
1665
+ // src/utils/time.ts
1666
+ var TIMEOUT_DISABLED = "none";
1667
+ var TIME_STRING_REGEX = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/;
1668
+ function parseTimeString(input) {
1669
+ const match = input.match(TIME_STRING_REGEX);
1670
+ if (!match || !match[1] && !match[2] && !match[3]) return null;
1671
+ const hours = parseInt(match[1] || "0", 10);
1672
+ const minutes = parseInt(match[2] || "0", 10);
1673
+ const seconds = parseInt(match[3] || "0", 10);
1674
+ return (hours * 3600 + minutes * 60 + seconds) * 1e3;
1675
+ }
1676
+ function isValidTimeString(input) {
1677
+ return parseTimeString(input) !== null;
1678
+ }
1679
+ export {
1680
+ DEFAULT_PROXY_MODEL,
1681
+ LEAPING_INTO_ACTION_PREFIX,
1682
+ MAX_LEARNINGS_LENGTH,
1683
+ OAuthInvalidGrantError,
1684
+ TERRAMEND_DIVIDER,
1685
+ TIMEOUT_DISABLED,
1686
+ buildTerramendFooter,
1687
+ createLeapingProgressComment,
1688
+ decodeJwtExpMs,
1689
+ deleteProgressCommentApi,
1690
+ getAutoSelectHintModel,
1691
+ getModelEnvVars,
1692
+ getModelManagedCredentials,
1693
+ getModelProvider,
1694
+ getProgressComment,
1695
+ getProviderDisplayName,
1696
+ isLeapingIntoActionCommentBody,
1697
+ isValidTimeString,
1698
+ modelAliases,
1699
+ modes,
1700
+ parseCodexAuthBody,
1701
+ parseModel,
1702
+ parseTimeString,
1703
+ providers,
1704
+ refreshCodexAuthBody,
1705
+ resolveCliModel,
1706
+ resolveDisplayAlias,
1707
+ resolveModelSlug,
1708
+ resolveOpenRouterModel,
1709
+ stringifyCodexAuthBody,
1710
+ stripExistingFooter,
1711
+ terramendMcpName,
1712
+ truncateAtLineBoundary,
1713
+ updateProgressComment
1714
+ };