infraweaver 0.1.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 (524) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +244 -0
  3. package/dist/agents/claude.d.ts +113 -0
  4. package/dist/agents/claudePretoolGate.d.ts +137 -0
  5. package/dist/agents/dispatchMetrics.d.ts +77 -0
  6. package/dist/agents/gateServer.d.ts +7 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/nativeFsDenies.d.ts +46 -0
  9. package/dist/agents/opencode.d.ts +284 -0
  10. package/dist/agents/opencodePlugin.d.ts +96 -0
  11. package/dist/agents/opencodeShared.d.ts +40 -0
  12. package/dist/agents/postRun.d.ts +126 -0
  13. package/dist/agents/reviewer.d.ts +41 -0
  14. package/dist/agents/sessionLabeler.d.ts +97 -0
  15. package/dist/agents/shared.d.ts +246 -0
  16. package/dist/agents/subagentModels.d.ts +19 -0
  17. package/dist/agents/tokenQuota.d.ts +118 -0
  18. package/dist/agents/writeGateSource.d.ts +22 -0
  19. package/dist/agents/writePolicy.d.ts +79 -0
  20. package/dist/brand.d.ts +67 -0
  21. package/dist/cli.mjs +247846 -0
  22. package/dist/external.d.ts +227 -0
  23. package/dist/i18n/scaffolding.d.ts +59 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +247061 -0
  26. package/dist/internal/index.d.ts +18 -0
  27. package/dist/internal.js +2398 -0
  28. package/dist/lifecycle.d.ts +2 -0
  29. package/dist/main.d.ts +8 -0
  30. package/dist/mcp/arkConfig.d.ts +1 -0
  31. package/dist/mcp/assess.d.ts +157 -0
  32. package/dist/mcp/capabilityContext.d.ts +71 -0
  33. package/dist/mcp/changeSummary.d.ts +52 -0
  34. package/dist/mcp/checkSuite.d.ts +27 -0
  35. package/dist/mcp/checkout.d.ts +92 -0
  36. package/dist/mcp/comment.d.ts +127 -0
  37. package/dist/mcp/commitInfo.d.ts +11 -0
  38. package/dist/mcp/crosswalk.d.ts +178 -0
  39. package/dist/mcp/crosswalkDigest.d.ts +1 -0
  40. package/dist/mcp/cyberEssentials.d.ts +24 -0
  41. package/dist/mcp/dashboard.d.ts +125 -0
  42. package/dist/mcp/dependencies.d.ts +12 -0
  43. package/dist/mcp/frameworks.d.ts +74 -0
  44. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  45. package/dist/mcp/git.d.ts +60 -0
  46. package/dist/mcp/guardrails.d.ts +222 -0
  47. package/dist/mcp/issue.d.ts +20 -0
  48. package/dist/mcp/issueComments.d.ts +11 -0
  49. package/dist/mcp/issueEvents.d.ts +11 -0
  50. package/dist/mcp/issueInfo.d.ts +11 -0
  51. package/dist/mcp/labels.d.ts +14 -0
  52. package/dist/mcp/localContext.d.ts +26 -0
  53. package/dist/mcp/moduleExtraction.d.ts +79 -0
  54. package/dist/mcp/moduleTests.d.ts +106 -0
  55. package/dist/mcp/modules.d.ts +198 -0
  56. package/dist/mcp/output.d.ts +16 -0
  57. package/dist/mcp/pathSafety.d.ts +14 -0
  58. package/dist/mcp/policy.d.ts +50 -0
  59. package/dist/mcp/pr.d.ts +64 -0
  60. package/dist/mcp/prInfo.d.ts +11 -0
  61. package/dist/mcp/providerSchema.d.ts +52 -0
  62. package/dist/mcp/review.d.ts +212 -0
  63. package/dist/mcp/reviewComments.d.ts +245 -0
  64. package/dist/mcp/roots.d.ts +60 -0
  65. package/dist/mcp/scope.d.ts +26 -0
  66. package/dist/mcp/selectMode.d.ts +20 -0
  67. package/dist/mcp/server.d.ts +61 -0
  68. package/dist/mcp/shared.d.ts +62 -0
  69. package/dist/mcp/shell.d.ts +58 -0
  70. package/dist/mcp/staleFix.d.ts +138 -0
  71. package/dist/mcp/terraform/azurePrices.d.ts +8 -0
  72. package/dist/mcp/terraform/baseline.d.ts +57 -0
  73. package/dist/mcp/terraform/concernResult.d.ts +38 -0
  74. package/dist/mcp/terraform/cost.d.ts +55 -0
  75. package/dist/mcp/terraform/cspm.d.ts +80 -0
  76. package/dist/mcp/terraform/currency.d.ts +110 -0
  77. package/dist/mcp/terraform/decisions.d.ts +178 -0
  78. package/dist/mcp/terraform/eidas.d.ts +100 -0
  79. package/dist/mcp/terraform/evidence.d.ts +343 -0
  80. package/dist/mcp/terraform/findings.d.ts +76 -0
  81. package/dist/mcp/terraform/fixMemory.d.ts +81 -0
  82. package/dist/mcp/terraform/governance.d.ts +56 -0
  83. package/dist/mcp/terraform/hcl.d.ts +115 -0
  84. package/dist/mcp/terraform/iacLanguages.d.ts +120 -0
  85. package/dist/mcp/terraform/idleFloor.d.ts +78 -0
  86. package/dist/mcp/terraform/keyless.d.ts +68 -0
  87. package/dist/mcp/terraform/moduleDocs.d.ts +56 -0
  88. package/dist/mcp/terraform/nativeRules.d.ts +38 -0
  89. package/dist/mcp/terraform/nativeScan.d.ts +11 -0
  90. package/dist/mcp/terraform/noiseProfile.d.ts +100 -0
  91. package/dist/mcp/terraform/normalization.d.ts +52 -0
  92. package/dist/mcp/terraform/oscal.d.ts +103 -0
  93. package/dist/mcp/terraform/packs/azurerm-3-4.d.ts +2 -0
  94. package/dist/mcp/terraform/packs/cloudflare-4-5.d.ts +2 -0
  95. package/dist/mcp/terraform/paths.d.ts +28 -0
  96. package/dist/mcp/terraform/plan.d.ts +157 -0
  97. package/dist/mcp/terraform/planDrift.d.ts +199 -0
  98. package/dist/mcp/terraform/policyAuthor.d.ts +191 -0
  99. package/dist/mcp/terraform/prowlerOcsf.d.ts +136 -0
  100. package/dist/mcp/terraform/refactor.d.ts +183 -0
  101. package/dist/mcp/terraform/registry.d.ts +108 -0
  102. package/dist/mcp/terraform/risk.d.ts +41 -0
  103. package/dist/mcp/terraform/scanSession.d.ts +116 -0
  104. package/dist/mcp/terraform/scannerCache.d.ts +15 -0
  105. package/dist/mcp/terraform/scannerEgress.d.ts +72 -0
  106. package/dist/mcp/terraform/scannerJson.d.ts +13 -0
  107. package/dist/mcp/terraform/scanners.d.ts +205 -0
  108. package/dist/mcp/terraform/signing.d.ts +61 -0
  109. package/dist/mcp/terraform/stateLocking.d.ts +18 -0
  110. package/dist/mcp/terraform/subprocess.d.ts +43 -0
  111. package/dist/mcp/terraform/suppressions.d.ts +90 -0
  112. package/dist/mcp/terraform/taxonomy.d.ts +39 -0
  113. package/dist/mcp/terraform/tfquery.d.ts +47 -0
  114. package/dist/mcp/terraform/toolchainIntegrity.d.ts +82 -0
  115. package/dist/mcp/terraform/tools/authorPolicy.d.ts +59 -0
  116. package/dist/mcp/terraform/tools/detectPlanDrift.d.ts +20 -0
  117. package/dist/mcp/terraform/tools/emitCost.d.ts +11 -0
  118. package/dist/mcp/terraform/tools/emitSarif.d.ts +14 -0
  119. package/dist/mcp/terraform/tools/fixMemory.d.ts +19 -0
  120. package/dist/mcp/terraform/tools/infracostDiff.d.ts +22 -0
  121. package/dist/mcp/terraform/tools/ingestExternalFindings.d.ts +21 -0
  122. package/dist/mcp/terraform/tools/moduleLookup.d.ts +14 -0
  123. package/dist/mcp/terraform/tools/moduleSearch.d.ts +23 -0
  124. package/dist/mcp/terraform/tools/plan.d.ts +59 -0
  125. package/dist/mcp/terraform/tools/providerUpgrade.d.ts +55 -0
  126. package/dist/mcp/terraform/tools/readFindings.d.ts +17 -0
  127. package/dist/mcp/terraform/tools/scan.d.ts +114 -0
  128. package/dist/mcp/terraform/tools/validate.d.ts +31 -0
  129. package/dist/mcp/terraform/tools/verifyRemediation.d.ts +31 -0
  130. package/dist/mcp/terraform/tools/versionCurrency.d.ts +5 -0
  131. package/dist/mcp/terraform/tools.d.ts +16 -0
  132. package/dist/mcp/terraform/transformPacks.d.ts +267 -0
  133. package/dist/mcp/terraform/types.d.ts +226 -0
  134. package/dist/mcp/terraform/verification.d.ts +81 -0
  135. package/dist/mcp/terraform/vex.d.ts +115 -0
  136. package/dist/mcp/terraform/writeOnly.d.ts +67 -0
  137. package/dist/mcp/terraform.d.ts +51 -0
  138. package/dist/mcp/terratest.d.ts +85 -0
  139. package/dist/mcp/untrustedContent.d.ts +76 -0
  140. package/dist/mcp/upload.d.ts +8 -0
  141. package/dist/models.d.ts +181 -0
  142. package/dist/modes/address-reviews.d.ts +2 -0
  143. package/dist/modes/assess.d.ts +2 -0
  144. package/dist/modes/build.d.ts +2 -0
  145. package/dist/modes/compliance-audit.d.ts +2 -0
  146. package/dist/modes/cost-optimization.d.ts +2 -0
  147. package/dist/modes/drift-detect.d.ts +2 -0
  148. package/dist/modes/fix.d.ts +2 -0
  149. package/dist/modes/incremental-review.d.ts +2 -0
  150. package/dist/modes/index.d.ts +24 -0
  151. package/dist/modes/modernize-deprecated.d.ts +2 -0
  152. package/dist/modes/plan.d.ts +2 -0
  153. package/dist/modes/policy-gate.d.ts +2 -0
  154. package/dist/modes/prFormats.d.ts +3 -0
  155. package/dist/modes/refactor.d.ts +2 -0
  156. package/dist/modes/refresh-remediation.d.ts +2 -0
  157. package/dist/modes/remediate-and-refactor.d.ts +2 -0
  158. package/dist/modes/remediate.d.ts +2 -0
  159. package/dist/modes/resolve-conflicts.d.ts +2 -0
  160. package/dist/modes/review.d.ts +2 -0
  161. package/dist/modes/summarize-pr.d.ts +2 -0
  162. package/dist/modes/task.d.ts +2 -0
  163. package/dist/modes/terraform-code-review.d.ts +2 -0
  164. package/dist/modes/types.d.ts +9 -0
  165. package/dist/modes/update-dependencies.d.ts +2 -0
  166. package/dist/prep/index.d.ts +7 -0
  167. package/dist/prep/installNodeDependencies.d.ts +2 -0
  168. package/dist/prep/installPythonDependencies.d.ts +2 -0
  169. package/dist/prep/types.d.ts +31 -0
  170. package/dist/reviewQuality.d.ts +107 -0
  171. package/dist/skills/terraform-best-practices/SKILL.md +379 -0
  172. package/dist/toolState.d.ts +123 -0
  173. package/dist/utils/activity.d.ts +40 -0
  174. package/dist/utils/agent.d.ts +47 -0
  175. package/dist/utils/agentHangReport.d.ts +38 -0
  176. package/dist/utils/annotations.d.ts +17 -0
  177. package/dist/utils/apiFetch.d.ts +11 -0
  178. package/dist/utils/apiKeys.d.ts +48 -0
  179. package/dist/utils/apiUrl.d.ts +28 -0
  180. package/dist/utils/assets.d.ts +8 -0
  181. package/dist/utils/baseRefConfig.d.ts +121 -0
  182. package/dist/utils/billingErrors.d.ts +85 -0
  183. package/dist/utils/body.d.ts +34 -0
  184. package/dist/utils/buildInfraweaverFooter.d.ts +29 -0
  185. package/dist/utils/byokFallback.d.ts +85 -0
  186. package/dist/utils/changeImpact.d.ts +101 -0
  187. package/dist/utils/claudeSubscription.d.ts +30 -0
  188. package/dist/utils/cli.d.ts +10 -0
  189. package/dist/utils/cloudClient.d.ts +33 -0
  190. package/dist/utils/cloudFetchCore.d.ts +42 -0
  191. package/dist/utils/cloudReport.d.ts +114 -0
  192. package/dist/utils/codexHome.d.ts +29 -0
  193. package/dist/utils/codexOAuth.d.ts +60 -0
  194. package/dist/utils/diffCoverage.d.ts +63 -0
  195. package/dist/utils/env.d.ts +46 -0
  196. package/dist/utils/errorReport.d.ts +17 -0
  197. package/dist/utils/exitHandler.d.ts +8 -0
  198. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  199. package/dist/utils/gitAuth.d.ts +84 -0
  200. package/dist/utils/gitAuthServer.d.ts +24 -0
  201. package/dist/utils/github.d.ts +104 -0
  202. package/dist/utils/globals.d.ts +3 -0
  203. package/dist/utils/infraweaverConfig.d.ts +56 -0
  204. package/dist/utils/install.d.ts +37 -0
  205. package/dist/utils/instructions.d.ts +48 -0
  206. package/dist/utils/keylessOidc.d.ts +28 -0
  207. package/dist/utils/leapingComment.d.ts +11 -0
  208. package/dist/utils/learnings.d.ts +62 -0
  209. package/dist/utils/learningsTruncate.d.ts +25 -0
  210. package/dist/utils/lifecycle.d.ts +57 -0
  211. package/dist/utils/log.d.ts +111 -0
  212. package/dist/utils/modelResolver.d.ts +63 -0
  213. package/dist/utils/moduleFetch.d.ts +39 -0
  214. package/dist/utils/normalizeEnv.d.ts +30 -0
  215. package/dist/utils/openCodeModels.d.ts +11 -0
  216. package/dist/utils/openaiCompatible.d.ts +26 -0
  217. package/dist/utils/opentofuMcp.d.ts +50 -0
  218. package/dist/utils/overrides.d.ts +40 -0
  219. package/dist/utils/packageManager.d.ts +49 -0
  220. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  221. package/dist/utils/payload.d.ts +249 -0
  222. package/dist/utils/prSummary.d.ts +61 -0
  223. package/dist/utils/presets.d.ts +36 -0
  224. package/dist/utils/progressComment.d.ts +146 -0
  225. package/dist/utils/providerErrors.d.ts +31 -0
  226. package/dist/utils/proxyModel.d.ts +76 -0
  227. package/dist/utils/rangeDiff.d.ts +51 -0
  228. package/dist/utils/redact.d.ts +33 -0
  229. package/dist/utils/registryAuth.d.ts +52 -0
  230. package/dist/utils/remediationCommand.d.ts +58 -0
  231. package/dist/utils/retry.d.ts +38 -0
  232. package/dist/utils/reviewCleanup.d.ts +14 -0
  233. package/dist/utils/run.d.ts +9 -0
  234. package/dist/utils/runContext.d.ts +112 -0
  235. package/dist/utils/runContextData.d.ts +42 -0
  236. package/dist/utils/runErrorRenderer.d.ts +64 -0
  237. package/dist/utils/runLifecycle.d.ts +89 -0
  238. package/dist/utils/runStartupLog.d.ts +15 -0
  239. package/dist/utils/secrets.d.ts +34 -0
  240. package/dist/utils/setup.d.ts +90 -0
  241. package/dist/utils/shell.d.ts +44 -0
  242. package/dist/utils/skills.d.ts +10 -0
  243. package/dist/utils/subprocess.d.ts +81 -0
  244. package/dist/utils/terraformMcp.d.ts +46 -0
  245. package/dist/utils/time.d.ts +15 -0
  246. package/dist/utils/timer.d.ts +23 -0
  247. package/dist/utils/todoTracking.d.ts +16 -0
  248. package/dist/utils/token.d.ts +52 -0
  249. package/dist/utils/toolLicensing.d.ts +56 -0
  250. package/dist/utils/toolSelection.d.ts +73 -0
  251. package/dist/utils/toon.d.ts +16 -0
  252. package/dist/utils/version.d.ts +2 -0
  253. package/dist/utils/versioning.d.ts +7 -0
  254. package/dist/utils/vertex.d.ts +16 -0
  255. package/dist/utils/workflow.d.ts +13 -0
  256. package/package.json +132 -0
  257. package/src/agents/claude.ts +1588 -0
  258. package/src/agents/claudePretoolGate.ts +286 -0
  259. package/src/agents/dispatchMetrics.ts +162 -0
  260. package/src/agents/gateServer.ts +178 -0
  261. package/src/agents/index.ts +10 -0
  262. package/src/agents/nativeFsDenies.ts +164 -0
  263. package/src/agents/opencode.ts +1600 -0
  264. package/src/agents/opencodePlugin.ts +304 -0
  265. package/src/agents/opencodeShared.ts +134 -0
  266. package/src/agents/postRun.ts +615 -0
  267. package/src/agents/reviewer.ts +134 -0
  268. package/src/agents/sessionLabeler.ts +185 -0
  269. package/src/agents/shared.ts +377 -0
  270. package/src/agents/subagentModels.ts +40 -0
  271. package/src/agents/tokenQuota.ts +203 -0
  272. package/src/agents/writeGateSource.ts +148 -0
  273. package/src/agents/writePolicy.ts +132 -0
  274. package/src/brand.ts +84 -0
  275. package/src/cli.ts +117 -0
  276. package/src/commands/gha.ts +188 -0
  277. package/src/commands/mcp.ts +126 -0
  278. package/src/commands/verifyEvidence.ts +100 -0
  279. package/src/entry.ts +7 -0
  280. package/src/entryPost.ts +109 -0
  281. package/src/external.ts +308 -0
  282. package/src/i18n/scaffolding.ts +191 -0
  283. package/src/index.ts +7 -0
  284. package/src/internal/index.ts +74 -0
  285. package/src/lifecycle.ts +2 -0
  286. package/src/main.ts +815 -0
  287. package/src/mcp/__fixtures__/infraweaver-scratch-pr-49-review-3485940013.json +110 -0
  288. package/src/mcp/__fixtures__/infraweaver-scratch-pr-64-review-3531000326.json +14 -0
  289. package/src/mcp/__fixtures__/infraweaver-test-repo-pr-1.diff.json +67 -0
  290. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  291. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  292. package/src/mcp/arkConfig.ts +7 -0
  293. package/src/mcp/assess.ts +723 -0
  294. package/src/mcp/capabilityContext.ts +80 -0
  295. package/src/mcp/changeSummary.ts +145 -0
  296. package/src/mcp/checkSuite.ts +261 -0
  297. package/src/mcp/checkout.ts +1015 -0
  298. package/src/mcp/comment.ts +683 -0
  299. package/src/mcp/commitInfo.ts +57 -0
  300. package/src/mcp/crosswalk.ts +1253 -0
  301. package/src/mcp/crosswalkDigest.ts +5 -0
  302. package/src/mcp/cyberEssentials.ts +60 -0
  303. package/src/mcp/dashboard.ts +232 -0
  304. package/src/mcp/dependencies.ts +190 -0
  305. package/src/mcp/frameworks.ts +236 -0
  306. package/src/mcp/geminiSanitizer.ts +212 -0
  307. package/src/mcp/git.ts +1116 -0
  308. package/src/mcp/guardrails.ts +771 -0
  309. package/src/mcp/issue.ts +74 -0
  310. package/src/mcp/issueComments.ts +48 -0
  311. package/src/mcp/issueEvents.ts +100 -0
  312. package/src/mcp/issueInfo.ts +72 -0
  313. package/src/mcp/labels.ts +35 -0
  314. package/src/mcp/localContext.ts +65 -0
  315. package/src/mcp/localServer.ts +217 -0
  316. package/src/mcp/moduleExtraction.ts +368 -0
  317. package/src/mcp/moduleTests.ts +421 -0
  318. package/src/mcp/modules.ts +752 -0
  319. package/src/mcp/output.ts +71 -0
  320. package/src/mcp/pathSafety.ts +28 -0
  321. package/src/mcp/policy.ts +226 -0
  322. package/src/mcp/pr.ts +238 -0
  323. package/src/mcp/prInfo.ts +91 -0
  324. package/src/mcp/providerSchema.ts +175 -0
  325. package/src/mcp/review.ts +1081 -0
  326. package/src/mcp/reviewComments.ts +1137 -0
  327. package/src/mcp/roots.ts +217 -0
  328. package/src/mcp/scope.ts +82 -0
  329. package/src/mcp/selectMode.ts +207 -0
  330. package/src/mcp/server.ts +498 -0
  331. package/src/mcp/shared.ts +120 -0
  332. package/src/mcp/shell.ts +631 -0
  333. package/src/mcp/staleFix.ts +557 -0
  334. package/src/mcp/terraform/__snapshots__/moduleDocs.test.ts.snap +40 -0
  335. package/src/mcp/terraform/azurePrices.ts +38 -0
  336. package/src/mcp/terraform/baseline.ts +172 -0
  337. package/src/mcp/terraform/concernResult.ts +111 -0
  338. package/src/mcp/terraform/cost.ts +175 -0
  339. package/src/mcp/terraform/cspm.ts +326 -0
  340. package/src/mcp/terraform/currency.ts +354 -0
  341. package/src/mcp/terraform/decisions.ts +590 -0
  342. package/src/mcp/terraform/eidas.ts +134 -0
  343. package/src/mcp/terraform/evidence.ts +771 -0
  344. package/src/mcp/terraform/findings.ts +383 -0
  345. package/src/mcp/terraform/fixMemory.ts +253 -0
  346. package/src/mcp/terraform/governance.ts +231 -0
  347. package/src/mcp/terraform/hcl.ts +273 -0
  348. package/src/mcp/terraform/iacLanguages.ts +431 -0
  349. package/src/mcp/terraform/idleFloor.ts +304 -0
  350. package/src/mcp/terraform/keyless.ts +123 -0
  351. package/src/mcp/terraform/moduleDocs.ts +303 -0
  352. package/src/mcp/terraform/nativeRules.ts +152 -0
  353. package/src/mcp/terraform/nativeScan.ts +67 -0
  354. package/src/mcp/terraform/noiseProfile.ts +183 -0
  355. package/src/mcp/terraform/normalization.ts +265 -0
  356. package/src/mcp/terraform/oscal.ts +283 -0
  357. package/src/mcp/terraform/packs/azurerm-3-4.ts +382 -0
  358. package/src/mcp/terraform/packs/cloudflare-4-5.ts +114 -0
  359. package/src/mcp/terraform/paths.ts +59 -0
  360. package/src/mcp/terraform/plan.ts +377 -0
  361. package/src/mcp/terraform/planDrift.ts +520 -0
  362. package/src/mcp/terraform/policyAuthor.ts +886 -0
  363. package/src/mcp/terraform/prowlerOcsf.ts +380 -0
  364. package/src/mcp/terraform/refactor.ts +879 -0
  365. package/src/mcp/terraform/registry.ts +317 -0
  366. package/src/mcp/terraform/risk.ts +99 -0
  367. package/src/mcp/terraform/scanSession.ts +242 -0
  368. package/src/mcp/terraform/scannerCache.ts +86 -0
  369. package/src/mcp/terraform/scannerEgress.ts +141 -0
  370. package/src/mcp/terraform/scannerJson.ts +22 -0
  371. package/src/mcp/terraform/scanners.ts +1134 -0
  372. package/src/mcp/terraform/signing.ts +151 -0
  373. package/src/mcp/terraform/stateLocking.ts +106 -0
  374. package/src/mcp/terraform/subprocess.ts +102 -0
  375. package/src/mcp/terraform/suppressions.ts +551 -0
  376. package/src/mcp/terraform/taxonomy.ts +0 -0
  377. package/src/mcp/terraform/tfquery.ts +159 -0
  378. package/src/mcp/terraform/toolchainIntegrity.ts +151 -0
  379. package/src/mcp/terraform/tools/authorPolicy.ts +185 -0
  380. package/src/mcp/terraform/tools/detectPlanDrift.ts +279 -0
  381. package/src/mcp/terraform/tools/emitCost.ts +130 -0
  382. package/src/mcp/terraform/tools/emitSarif.ts +108 -0
  383. package/src/mcp/terraform/tools/fixMemory.ts +82 -0
  384. package/src/mcp/terraform/tools/infracostDiff.ts +110 -0
  385. package/src/mcp/terraform/tools/ingestExternalFindings.ts +259 -0
  386. package/src/mcp/terraform/tools/moduleLookup.ts +86 -0
  387. package/src/mcp/terraform/tools/moduleSearch.ts +46 -0
  388. package/src/mcp/terraform/tools/plan.ts +384 -0
  389. package/src/mcp/terraform/tools/providerUpgrade.ts +217 -0
  390. package/src/mcp/terraform/tools/readFindings.ts +138 -0
  391. package/src/mcp/terraform/tools/scan.ts +303 -0
  392. package/src/mcp/terraform/tools/validate.ts +160 -0
  393. package/src/mcp/terraform/tools/verifyRemediation.ts +190 -0
  394. package/src/mcp/terraform/tools/versionCurrency.ts +64 -0
  395. package/src/mcp/terraform/tools.ts +62 -0
  396. package/src/mcp/terraform/transformPacks.ts +594 -0
  397. package/src/mcp/terraform/types.ts +382 -0
  398. package/src/mcp/terraform/verification.ts +150 -0
  399. package/src/mcp/terraform/vex.ts +367 -0
  400. package/src/mcp/terraform/writeOnly.ts +252 -0
  401. package/src/mcp/terraform.ts +52 -0
  402. package/src/mcp/terratest.ts +215 -0
  403. package/src/mcp/untrustedContent.ts +126 -0
  404. package/src/mcp/upload.ts +123 -0
  405. package/src/models.ts +753 -0
  406. package/src/modes/address-reviews.ts +35 -0
  407. package/src/modes/assess.ts +27 -0
  408. package/src/modes/build.ts +86 -0
  409. package/src/modes/compliance-audit.ts +25 -0
  410. package/src/modes/cost-optimization.ts +36 -0
  411. package/src/modes/drift-detect.ts +31 -0
  412. package/src/modes/fix.ts +29 -0
  413. package/src/modes/incremental-review.ts +103 -0
  414. package/src/modes/index.ts +94 -0
  415. package/src/modes/modernize-deprecated.ts +36 -0
  416. package/src/modes/plan.ts +21 -0
  417. package/src/modes/policy-gate.ts +27 -0
  418. package/src/modes/prFormats.ts +325 -0
  419. package/src/modes/refactor.ts +74 -0
  420. package/src/modes/refresh-remediation.ts +40 -0
  421. package/src/modes/remediate-and-refactor.ts +42 -0
  422. package/src/modes/remediate.ts +78 -0
  423. package/src/modes/resolve-conflicts.ts +32 -0
  424. package/src/modes/review.ts +135 -0
  425. package/src/modes/summarize-pr.ts +42 -0
  426. package/src/modes/task.ts +26 -0
  427. package/src/modes/terraform-code-review.ts +68 -0
  428. package/src/modes/types.ts +17 -0
  429. package/src/modes/update-dependencies.ts +44 -0
  430. package/src/prep/index.ts +96 -0
  431. package/src/prep/installNodeDependencies.ts +251 -0
  432. package/src/prep/installPythonDependencies.ts +235 -0
  433. package/src/prep/types.ts +38 -0
  434. package/src/reviewQuality.ts +237 -0
  435. package/src/runCli.ts +335 -0
  436. package/src/skills/terraform-best-practices/SKILL.md +379 -0
  437. package/src/toolState.ts +241 -0
  438. package/src/utils/activity.ts +210 -0
  439. package/src/utils/agent.ts +245 -0
  440. package/src/utils/agentHangReport.ts +180 -0
  441. package/src/utils/annotations.ts +56 -0
  442. package/src/utils/apiFetch.ts +19 -0
  443. package/src/utils/apiKeys.ts +305 -0
  444. package/src/utils/apiUrl.ts +42 -0
  445. package/src/utils/assets.ts +123 -0
  446. package/src/utils/baseRefConfig.ts +254 -0
  447. package/src/utils/billingErrors.ts +210 -0
  448. package/src/utils/body.ts +168 -0
  449. package/src/utils/buildInfraweaverFooter.ts +104 -0
  450. package/src/utils/byokFallback.ts +137 -0
  451. package/src/utils/changeImpact.ts +330 -0
  452. package/src/utils/claudeSubscription.ts +93 -0
  453. package/src/utils/cli.ts +36 -0
  454. package/src/utils/cloudClient.ts +57 -0
  455. package/src/utils/cloudFetchCore.ts +139 -0
  456. package/src/utils/cloudReport.ts +316 -0
  457. package/src/utils/codexHome.ts +204 -0
  458. package/src/utils/codexOAuth.ts +154 -0
  459. package/src/utils/codexRefreshDetect.ts +36 -0
  460. package/src/utils/diffCoverage.ts +404 -0
  461. package/src/utils/env.ts +84 -0
  462. package/src/utils/errorReport.ts +94 -0
  463. package/src/utils/exitHandler.ts +35 -0
  464. package/src/utils/fixDoubleEscapedString.ts +9 -0
  465. package/src/utils/ghaCore.ts +13 -0
  466. package/src/utils/gitAuth.ts +268 -0
  467. package/src/utils/gitAuthServer.ts +186 -0
  468. package/src/utils/github.ts +676 -0
  469. package/src/utils/globals.ts +9 -0
  470. package/src/utils/humanEditCapture.ts +198 -0
  471. package/src/utils/infraweaverConfig.ts +202 -0
  472. package/src/utils/install.ts +279 -0
  473. package/src/utils/instructions.ts +640 -0
  474. package/src/utils/keylessOidc.ts +47 -0
  475. package/src/utils/leapingComment.ts +20 -0
  476. package/src/utils/learnings.ts +145 -0
  477. package/src/utils/learningsTruncate.ts +42 -0
  478. package/src/utils/lifecycle.ts +198 -0
  479. package/src/utils/log.ts +452 -0
  480. package/src/utils/modelResolver.ts +105 -0
  481. package/src/utils/moduleFetch.ts +138 -0
  482. package/src/utils/normalizeEnv.ts +106 -0
  483. package/src/utils/openCodeModels.ts +86 -0
  484. package/src/utils/openaiCompatible.ts +52 -0
  485. package/src/utils/opentofuMcp.ts +61 -0
  486. package/src/utils/overrides.ts +100 -0
  487. package/src/utils/packageManager.ts +257 -0
  488. package/src/utils/patchWorkflowRunFields.ts +173 -0
  489. package/src/utils/payload.ts +1206 -0
  490. package/src/utils/prSummary.ts +148 -0
  491. package/src/utils/presets.ts +105 -0
  492. package/src/utils/progressComment.ts +266 -0
  493. package/src/utils/providerErrors.ts +189 -0
  494. package/src/utils/proxyModel.ts +239 -0
  495. package/src/utils/rangeDiff.ts +182 -0
  496. package/src/utils/redact.ts +86 -0
  497. package/src/utils/registryAuth.ts +115 -0
  498. package/src/utils/remediationCommand.ts +144 -0
  499. package/src/utils/retry.ts +180 -0
  500. package/src/utils/reviewCleanup.ts +116 -0
  501. package/src/utils/run.ts +100 -0
  502. package/src/utils/runContext.ts +276 -0
  503. package/src/utils/runContextData.ts +121 -0
  504. package/src/utils/runErrorRenderer.ts +282 -0
  505. package/src/utils/runFixture.ts +91 -0
  506. package/src/utils/runLifecycle.ts +360 -0
  507. package/src/utils/runStartupLog.ts +67 -0
  508. package/src/utils/secrets.ts +208 -0
  509. package/src/utils/setup.ts +368 -0
  510. package/src/utils/shell.ts +132 -0
  511. package/src/utils/skills.ts +67 -0
  512. package/src/utils/subprocess.ts +474 -0
  513. package/src/utils/terraformMcp.ts +99 -0
  514. package/src/utils/time.ts +59 -0
  515. package/src/utils/timer.ts +72 -0
  516. package/src/utils/todoTracking.ts +168 -0
  517. package/src/utils/token.ts +294 -0
  518. package/src/utils/toolLicensing.ts +152 -0
  519. package/src/utils/toolSelection.ts +239 -0
  520. package/src/utils/toon.ts +76 -0
  521. package/src/utils/version.ts +10 -0
  522. package/src/utils/versioning.ts +44 -0
  523. package/src/utils/vertex.ts +94 -0
  524. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,879 @@
1
+ /**
2
+ * Refactor equivalence proof (Phase A — the "safe floor").
3
+ *
4
+ * The `refactor` verb relocates resources behind a module boundary and asserts
5
+ * behaviour is unchanged. The PROOF that backs that assertion has two layers:
6
+ *
7
+ * - the keys-free STATIC proof in this file (no cloud creds): same managed
8
+ * resource set, identical per-resource argument names, and — the highest-risk
9
+ * invariant — EVERY relocated address covered by a `moved {}` block, plus
10
+ * `terraform validate` and `terraform fmt -check` clean. `computeEquivalence`
11
+ * hard-fails on a single uncovered move: a missed `moved {}` is a silent
12
+ * destroy/create on live infrastructure (a sin of commission), so the gate
13
+ * must refuse rather than warn.
14
+ * - the OPTIONAL stronger proof (cloud creds present): a real `terraform plan`
15
+ * that is a pure-move plan (`isPureMovePlan` in plan.ts). That is corroboration
16
+ * layered on top of this floor, never a substitute for it.
17
+ *
18
+ * Everything here is pure parsing + set arithmetic over file contents — the tool
19
+ * wrapper supplies the before-tree (via `git show <base>:<path>`) and the
20
+ * after-tree (the working tree on disk), mirroring moduleExtraction.ts.
21
+ *
22
+ * `moved {}` semantics anchor:
23
+ * https://developer.hashicorp.com/terraform/language/modules/develop/refactoring
24
+ * moved { from = aws_instance.example to = module.ec2.aws_instance.example }
25
+ */
26
+
27
+ import { readFileSync } from "node:fs";
28
+ import { join, posix } from "node:path";
29
+ import { type } from "arktype";
30
+ import { PRODUCT_NAME } from "#app/brand";
31
+ import type { LocalToolContext } from "#app/mcp/localContext";
32
+ import {
33
+ type ModuleSourceKind,
34
+ parseModuleBlocks,
35
+ walkTfFiles,
36
+ } from "#app/mcp/modules";
37
+ import { execute, tool, toolOk, toolSkip } from "#app/mcp/shared";
38
+ import { scanSessionOf } from "#app/mcp/terraform/scanSession";
39
+ import { scanFmt, scanValidate } from "#app/mcp/terraform/scanners";
40
+ import { run } from "#app/mcp/terraform/subprocess";
41
+ import { log } from "#app/utils/cli";
42
+ import { resolveModuleCredentialEnv } from "#app/utils/registryAuth";
43
+
44
+ // --- resource parsing with argument names (pure) -----------------------------
45
+
46
+ export interface ResourceRef {
47
+ /** full Terraform address, module-prefixed: `module.x.aws_s3_bucket.logs`. */
48
+ address: string;
49
+ /** the resource's logical identity, module-path-independent: `aws_s3_bucket.logs`. */
50
+ key: string;
51
+ type: string;
52
+ name: string;
53
+ /** sorted top-level argument names (attributes + nested block labels). */
54
+ argNames: string[];
55
+ }
56
+
57
+ export interface MovedBlock {
58
+ from: string;
59
+ to: string;
60
+ }
61
+
62
+ export interface SourceFile {
63
+ /** repo-relative path, posix-separated. */
64
+ path: string;
65
+ hcl: string;
66
+ }
67
+
68
+ // Meta-arguments are valid on every resource and never describe behaviour we
69
+ // compare for equivalence; mirrors RESOURCE_META_ARGUMENTS in scanners.ts.
70
+ const META_ARGUMENTS: ReadonlySet<string> = new Set([
71
+ "count",
72
+ "for_each",
73
+ "provider",
74
+ "depends_on",
75
+ "lifecycle",
76
+ "provisioner",
77
+ "connection",
78
+ ]);
79
+
80
+ /** strip a `"…"` string, `#`/`//` comment, or `<<EOF` heredoc starting at `i`;
81
+ * returns the index of the span's last char, or null when `i` isn't a span start.
82
+ * A compact local copy of scanners.ts's skipNonCode (not exported there) so the
83
+ * brace/argument scan can't be fooled by an interpolation brace or a commented `}`. */
84
+ function skipNonCode(s: string, i: number): number | null {
85
+ const ch = s[i];
86
+ if (ch === '"') {
87
+ let j = i + 1;
88
+ while (j < s.length && s[j] !== '"') {
89
+ if (s[j] === "\\") j++;
90
+ j++;
91
+ }
92
+ return j;
93
+ }
94
+ if (ch === "<" && s[i + 1] === "<") {
95
+ const m = /^<<[-~]?([A-Za-z_][A-Za-z0-9_]*)/.exec(s.slice(i, i + 80));
96
+ if (m) {
97
+ const openNl = s.indexOf("\n", i);
98
+ if (openNl === -1) return s.length - 1;
99
+ const cm = new RegExp(`\\n[ \\t]*${m[1]}[ \\t]*(?=\\n|$)`).exec(
100
+ s.slice(openNl),
101
+ );
102
+ return cm ? openNl + cm.index + cm[0].length - 1 : s.length - 1;
103
+ }
104
+ }
105
+ if (ch === "#" || (ch === "/" && s[i + 1] === "/")) {
106
+ const nl = s.indexOf("\n", i);
107
+ return nl === -1 ? s.length - 1 : nl;
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /** brace-match the body opened at `open`, string/comment/heredoc-aware. */
113
+ function matchBraceBody(
114
+ hcl: string,
115
+ open: number,
116
+ ): { text: string; end: number } | null {
117
+ let depth = 0;
118
+ for (let i = open; i < hcl.length; i++) {
119
+ const span = skipNonCode(hcl, i);
120
+ if (span !== null) {
121
+ i = span;
122
+ continue;
123
+ }
124
+ const ch = hcl[i];
125
+ if (ch === "{") depth++;
126
+ else if (ch === "}") {
127
+ depth--;
128
+ if (depth === 0) return { text: hcl.slice(open + 1, i), end: i };
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /** depth-0 argument names of a resource body (attributes `name =`, nested blocks
135
+ * `name {`, and a `dynamic "x"` block's generated label `x`), excluding
136
+ * meta-arguments. String/comment/heredoc-aware so an interpolation or comment
137
+ * can't fabricate a name. Returned sorted for stable comparison. */
138
+ function topLevelArgNames(body: string): string[] {
139
+ const names = new Set<string>();
140
+ let depth = 0;
141
+ let atStmtStart = true;
142
+ for (let i = 0; i < body.length; i++) {
143
+ const span = skipNonCode(body, i);
144
+ if (span !== null) {
145
+ // a comment/heredoc ends the line; a string does not. Approximate by
146
+ // checking whether the span consumed a newline.
147
+ atStmtStart = body.slice(i, span + 1).includes("\n");
148
+ i = span;
149
+ continue;
150
+ }
151
+ const ch = body[i]!;
152
+ if (ch === "{") {
153
+ depth++;
154
+ atStmtStart = false;
155
+ continue;
156
+ }
157
+ if (ch === "}") {
158
+ depth--;
159
+ atStmtStart = false;
160
+ continue;
161
+ }
162
+ if (ch === "\n") {
163
+ atStmtStart = true;
164
+ continue;
165
+ }
166
+ if (ch === " " || ch === "\t" || ch === "\r") continue;
167
+ if (depth === 0 && atStmtStart && /[A-Za-z_]/.test(ch)) {
168
+ let j = i;
169
+ while (j < body.length && /[A-Za-z0-9_-]/.test(body[j]!)) j++;
170
+ const ident = body.slice(i, j);
171
+ let k = j;
172
+ while (k < body.length && (body[k] === " " || body[k] === "\t")) k++;
173
+ const next = body[k];
174
+ if (next === "=" && body[k + 1] !== "=") {
175
+ if (!META_ARGUMENTS.has(ident)) names.add(ident);
176
+ } else if (next === "{") {
177
+ if (!META_ARGUMENTS.has(ident)) names.add(ident);
178
+ } else if (next === '"' && ident === "dynamic") {
179
+ const labelEnd = body.indexOf('"', k + 1);
180
+ if (labelEnd !== -1) names.add(body.slice(k + 1, labelEnd));
181
+ }
182
+ i = j - 1;
183
+ atStmtStart = false;
184
+ continue;
185
+ }
186
+ atStmtStart = false;
187
+ }
188
+ return [...names].sort();
189
+ }
190
+
191
+ /** parse every `resource "<type>" "<name>" { … }` block's type, name, and
192
+ * top-level argument names from one file. */
193
+ function parseResourcesWithArgs(
194
+ hcl: string,
195
+ ): { type: string; name: string; argNames: string[] }[] {
196
+ const out: { type: string; name: string; argNames: string[] }[] = [];
197
+ const re = /(?:^|\n)\s*resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/g;
198
+ let m: RegExpExecArray | null;
199
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex-exec iteration
200
+ while ((m = re.exec(hcl)) !== null) {
201
+ const [, resourceType = "", name = ""] = m;
202
+ const braceStart = hcl.indexOf("{", m.index);
203
+ if (braceStart === -1) break;
204
+ const body = matchBraceBody(hcl, braceStart);
205
+ if (!body) break;
206
+ re.lastIndex = body.end + 1;
207
+ out.push({
208
+ type: resourceType,
209
+ name,
210
+ argNames: topLevelArgNames(body.text),
211
+ });
212
+ }
213
+ return out;
214
+ }
215
+
216
+ // --- moved {} block parsing (pure) -------------------------------------------
217
+
218
+ /**
219
+ * Parse every top-level `moved { from = <addr> to = <addr> }` block from HCL.
220
+ * Addresses are unquoted Terraform traversals (`module.x.aws_s3_bucket.logs`,
221
+ * `aws_instance.example[0]`), so the value is captured raw up to the line end or
222
+ * a trailing comment. Brace-matched so a stray `{}` in a comment can't fool it.
223
+ * A block missing either `from` or `to` is skipped (incomplete, not a relocation).
224
+ */
225
+ export function parseMovedBlocks(hcl: string): MovedBlock[] {
226
+ const out: MovedBlock[] = [];
227
+ const re = /(?:^|\n)\s*moved\s*\{/g;
228
+ let m: RegExpExecArray | null;
229
+ // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex-exec iteration
230
+ while ((m = re.exec(hcl)) !== null) {
231
+ const braceStart = hcl.indexOf("{", m.index);
232
+ if (braceStart === -1) break;
233
+ const body = matchBraceBody(hcl, braceStart);
234
+ if (!body) break;
235
+ re.lastIndex = body.end + 1;
236
+ // capture the address TOKEN after `from =` / `to =` — a run of address
237
+ // characters terminated by whitespace, `}`, or a comment. Token-based (not
238
+ // line-based) so a single-line `moved { from = a to = b }` parses as well as
239
+ // the multi-line form. `\bfrom\b`/`\bto\b` anchor on the keyword, not a
240
+ // substring of an address.
241
+ const from = body.text.match(/\bfrom\b\s*=\s*([^\s}#]+)/)?.[1];
242
+ const to = body.text.match(/\bto\b\s*=\s*([^\s}#]+)/)?.[1];
243
+ if (from && to)
244
+ out.push({ from: normaliseAddress(from), to: normaliseAddress(to) });
245
+ }
246
+ return out;
247
+ }
248
+
249
+ /** collapse incidental whitespace so two spellings of the same address compare
250
+ * equal (`module.x . aws_s3_bucket.logs` is not valid HCL, but defensive). */
251
+ export function normaliseAddress(addr: string): string {
252
+ return addr
253
+ .trim()
254
+ .replace(/\s*\.\s*/g, ".")
255
+ .replace(/\s+/g, " ");
256
+ }
257
+
258
+ // --- module attribution (pure) -----------------------------------------------
259
+
260
+ /** the leaf `<type>.<name>` of an address, stripping leading `module.X.` hops and
261
+ * any `[index]`/`["key"]` instance suffix. `module.db.aws_db_instance.main[0]`
262
+ * → `aws_db_instance.main`. */
263
+ export function addressKey(address: string): string {
264
+ const noModules = address.replace(/^(?:module\.[^.]+\.)+/, "");
265
+ return noModules.replace(/\[[^\]]*\]$/, "");
266
+ }
267
+
268
+ interface ModuleCall {
269
+ /** repo-relative posix dir the local module's source resolves to. */
270
+ dir: string;
271
+ callName: string;
272
+ }
273
+
274
+ /** resolve every `module "<name>" { source = "./local/path" }` call to the dir
275
+ * its `.tf` files live in (local sources only — registry/git modules have no
276
+ * in-tree files to attribute). */
277
+ function localModuleCalls(files: SourceFile[]): ModuleCall[] {
278
+ const calls: ModuleCall[] = [];
279
+ for (const f of files) {
280
+ for (const block of parseModuleBlocks(f.hcl)) {
281
+ if (block.kind !== "local") continue;
282
+ const dir = posix
283
+ .normalize(posix.join(posix.dirname(f.path), block.source))
284
+ .replace(/\/+$/, "");
285
+ calls.push({ dir, callName: block.name });
286
+ }
287
+ }
288
+ return calls;
289
+ }
290
+
291
+ /** true when `file` lives inside module `dir` (or a nested file under it). */
292
+ function fileInDir(file: string, dir: string): boolean {
293
+ return dir === "."
294
+ ? !file.includes("/")
295
+ : file === dir || file.startsWith(`${dir}/`);
296
+ }
297
+
298
+ /**
299
+ * Attribute every resource in a file tree to its full Terraform address.
300
+ * A resource in a file under a single-caller local module dir is prefixed
301
+ * `module.<caller>.`; a resource at root keeps its bare `<type>.<name>`. A module
302
+ * dir with MULTIPLE callers (instantiated more than once) or NESTED module dirs
303
+ * can't be attributed to one address deterministically — those resources are
304
+ * returned in `ambiguous` so the caller degrades honestly rather than asserting a
305
+ * false address. Pure.
306
+ */
307
+ export function attributeAddresses(files: SourceFile[]): {
308
+ resources: ResourceRef[];
309
+ ambiguous: string[];
310
+ } {
311
+ const calls = localModuleCalls(files);
312
+ const callersByDir = new Map<string, string[]>();
313
+ for (const c of calls) {
314
+ callersByDir.set(c.dir, [...(callersByDir.get(c.dir) ?? []), c.callName]);
315
+ }
316
+ const resources: ResourceRef[] = [];
317
+ const ambiguous: string[] = [];
318
+ for (const f of files) {
319
+ // the longest module dir that contains this file (handles `modules/x` vs
320
+ // `modules/x/sub` — the deepest wins).
321
+ const owningDirs = [...callersByDir.keys()]
322
+ .filter((dir) => fileInDir(f.path, dir))
323
+ .sort((a, b) => b.length - a.length);
324
+ const owningDir = owningDirs[0];
325
+ for (const r of parseResourcesWithArgs(f.hcl)) {
326
+ const key = `${r.type}.${r.name}`;
327
+ if (!owningDir) {
328
+ resources.push({
329
+ address: key,
330
+ key,
331
+ type: r.type,
332
+ name: r.name,
333
+ argNames: r.argNames,
334
+ });
335
+ continue;
336
+ }
337
+ const callers = callersByDir.get(owningDir) ?? [];
338
+ // a nested module dir (file owned by a dir that is itself under another
339
+ // module dir) or a multi-instantiated module yields >1 viable prefix.
340
+ if (callers.length !== 1 || owningDirs.length > 1) {
341
+ ambiguous.push(`${owningDir}:${key}`);
342
+ continue;
343
+ }
344
+ const address = `module.${callers[0]}.${key}`;
345
+ resources.push({
346
+ address,
347
+ key,
348
+ type: r.type,
349
+ name: r.name,
350
+ argNames: r.argNames,
351
+ });
352
+ }
353
+ }
354
+ return { resources, ambiguous };
355
+ }
356
+
357
+ // --- the equivalence computation (pure) --------------------------------------
358
+
359
+ export interface EquivalenceResult {
360
+ /** the overall keys-free verdict: all sub-checks below passed. The push
361
+ * guardrail hard-blocks when this is false. */
362
+ equivalent: boolean;
363
+ /** same multiset of `<type>.<name>` across the whole tree before and after. */
364
+ resource_set_equivalent: boolean;
365
+ /** identical top-level argument names for every resource present in both. */
366
+ arg_names_equivalent: boolean;
367
+ /** every relocated address is covered by a `moved {}` block (zero uncovered).
368
+ * THE highest-risk invariant — a false here means a silent destroy/create. */
369
+ moves_exhaustive: boolean;
370
+ /** count of address relocations detected between before and after. */
371
+ moves_detected: number;
372
+ /** the relocations we detected (the authoritative move set). */
373
+ expected_moves: MovedBlock[];
374
+ /** relocations with NO matching `moved {}` block — the hard-fail set. */
375
+ uncovered_moves: MovedBlock[];
376
+ /** `moved {}` blocks that match no detected relocation (likely a typo'd
377
+ * `to`/`from` — surfaced, but not on its own a hard-fail). */
378
+ spurious_moved_blocks: MovedBlock[];
379
+ /** logical keys added / removed between before and after (must be empty for
380
+ * an equivalence-preserving refactor; a non-empty `added` is the b.4 "adopting
381
+ * a module adds a KMS key" signal — that's an ALTERING change, not equivalence). */
382
+ resource_set_diff: { added: string[]; removed: string[] };
383
+ /** per-resource argument-name drift. */
384
+ arg_diffs: { resource: string; added: string[]; removed: string[] }[];
385
+ /** addresses that couldn't be attributed deterministically (multi-instantiated
386
+ * or nested modules) — equivalence is reported but flagged incomplete. */
387
+ ambiguous: string[];
388
+ /** human-readable reasons the verdict is what it is. */
389
+ reasons: string[];
390
+ }
391
+
392
+ /** multiset of a list of keys → Map<key, count>. */
393
+ function multiset(keys: string[]): Map<string, number> {
394
+ const m = new Map<string, number>();
395
+ for (const k of keys) m.set(k, (m.get(k) ?? 0) + 1);
396
+ return m;
397
+ }
398
+
399
+ /**
400
+ * Compute the keys-free equivalence verdict from the before/after resource
401
+ * address sets and the `moved {}` blocks present in the after tree. Validate/fmt
402
+ * results are merged in by the tool wrapper (they need a subprocess); this pure
403
+ * core proves the three static invariants — resource-set preservation, argument
404
+ * preservation, and moved-block exhaustiveness.
405
+ */
406
+ export function computeEquivalence(
407
+ before: ResourceRef[],
408
+ after: ResourceRef[],
409
+ movedBlocks: MovedBlock[],
410
+ ambiguous: string[] = [],
411
+ ): EquivalenceResult {
412
+ const reasons: string[] = [];
413
+
414
+ // 1. resource-set preservation (multiset of logical keys).
415
+ const beforeKeys = multiset(before.map((r) => r.key));
416
+ const afterKeys = multiset(after.map((r) => r.key));
417
+ const added: string[] = [];
418
+ const removed: string[] = [];
419
+ for (const [k, n] of afterKeys)
420
+ if (n > (beforeKeys.get(k) ?? 0)) added.push(k);
421
+ for (const [k, n] of beforeKeys)
422
+ if (n > (afterKeys.get(k) ?? 0)) removed.push(k);
423
+ added.sort();
424
+ removed.sort();
425
+ const resourceSetEquivalent = added.length === 0 && removed.length === 0;
426
+ if (added.length > 0)
427
+ reasons.push(
428
+ `resource set GAINED ${added.length} resource(s): ${added.join(", ")}`,
429
+ );
430
+ if (removed.length > 0)
431
+ reasons.push(
432
+ `resource set LOST ${removed.length} resource(s): ${removed.join(", ")}`,
433
+ );
434
+
435
+ // 2. argument-name preservation per logical key (compare the union of
436
+ // arg-name sets for each key — robust to count>1 without false drift).
437
+ const argDiffs: EquivalenceResult["arg_diffs"] = [];
438
+ const argsByKey = (refs: ResourceRef[]) => {
439
+ const m = new Map<string, Set<string>>();
440
+ for (const r of refs) {
441
+ const set = m.get(r.key) ?? new Set<string>();
442
+ for (const a of r.argNames) set.add(a);
443
+ m.set(r.key, set);
444
+ }
445
+ return m;
446
+ };
447
+ const beforeArgs = argsByKey(before);
448
+ const afterArgs = argsByKey(after);
449
+ for (const key of new Set([...beforeArgs.keys(), ...afterArgs.keys()])) {
450
+ const b = beforeArgs.get(key) ?? new Set<string>();
451
+ const a = afterArgs.get(key) ?? new Set<string>();
452
+ const gained = [...a].filter((x) => !b.has(x)).sort();
453
+ const lost = [...b].filter((x) => !a.has(x)).sort();
454
+ if (gained.length > 0 || lost.length > 0)
455
+ argDiffs.push({ resource: key, added: gained, removed: lost });
456
+ }
457
+ const argNamesEquivalent = argDiffs.length === 0;
458
+ if (argDiffs.length > 0)
459
+ reasons.push(
460
+ `argument drift on ${argDiffs.length} resource(s): ${argDiffs.map((d) => d.resource).join(", ")}`,
461
+ );
462
+
463
+ // 3. moved-block exhaustiveness — pair each logical key's before/after
464
+ // addresses; a key whose address changed is a relocation that MUST be covered.
465
+ const addrsByKey = (refs: ResourceRef[]) => {
466
+ const m = new Map<string, string[]>();
467
+ for (const r of refs) m.set(r.key, [...(m.get(r.key) ?? []), r.address]);
468
+ return m;
469
+ };
470
+ const beforeAddrs = addrsByKey(before);
471
+ const afterAddrs = addrsByKey(after);
472
+ const expected: MovedBlock[] = [];
473
+ for (const [key, bAddrs] of beforeAddrs) {
474
+ const aAddrs = afterAddrs.get(key) ?? [];
475
+ // froms = before addresses not still present after; tos = new addresses.
476
+ const froms = bAddrs.filter((x) => !aAddrs.includes(x));
477
+ const tos = aAddrs.filter((x) => !bAddrs.includes(x));
478
+ // pair positionally (the 1:1 mechanical-extraction case is exact); any
479
+ // surplus on either side is an ambiguous relocation flagged below.
480
+ const pairs = Math.min(froms.length, tos.length);
481
+ for (let i = 0; i < pairs; i++)
482
+ expected.push({ from: froms[i]!, to: tos[i]! });
483
+ if (froms.length !== tos.length)
484
+ reasons.push(
485
+ `ambiguous relocation for ${key} (${froms.length} from / ${tos.length} to)`,
486
+ );
487
+ }
488
+ const moveKey = (mv: MovedBlock) => `${mv.from}=>${mv.to}`;
489
+ const movedSet = new Set(movedBlocks.map(moveKey));
490
+ const expectedSet = new Set(expected.map(moveKey));
491
+ const uncovered = expected.filter((mv) => !movedSet.has(moveKey(mv)));
492
+ const spurious = movedBlocks.filter((mv) => !expectedSet.has(moveKey(mv)));
493
+ const movesExhaustive = uncovered.length === 0;
494
+ if (uncovered.length > 0)
495
+ reasons.push(
496
+ `${uncovered.length} relocated resource(s) have NO \`moved {}\` block — ` +
497
+ `this would DESTROY and RECREATE them on apply: ${uncovered.map((u) => u.from).join(", ")}`,
498
+ );
499
+ if (spurious.length > 0)
500
+ reasons.push(
501
+ `${spurious.length} \`moved {}\` block(s) match no detected relocation (check from/to): ${spurious.map(moveKey).join(", ")}`,
502
+ );
503
+ if (ambiguous.length > 0)
504
+ reasons.push(
505
+ `${ambiguous.length} resource(s) in multi-instantiated/nested modules couldn't be attributed — equivalence is incomplete for: ${ambiguous.join(", ")}`,
506
+ );
507
+
508
+ const equivalent =
509
+ resourceSetEquivalent &&
510
+ argNamesEquivalent &&
511
+ movesExhaustive &&
512
+ ambiguous.length === 0;
513
+
514
+ return {
515
+ equivalent,
516
+ resource_set_equivalent: resourceSetEquivalent,
517
+ arg_names_equivalent: argNamesEquivalent,
518
+ moves_exhaustive: movesExhaustive,
519
+ moves_detected: expected.length,
520
+ expected_moves: expected,
521
+ uncovered_moves: uncovered,
522
+ spurious_moved_blocks: spurious,
523
+ resource_set_diff: { added, removed },
524
+ arg_diffs: argDiffs,
525
+ ambiguous,
526
+ reasons,
527
+ };
528
+ }
529
+
530
+ // --- moved {} block generation (pure) — A2 -----------------------------------
531
+
532
+ /**
533
+ * Generate one `moved {}` block per resource being relocated under a module call,
534
+ * and the authoritative `expected_moves` list (the denominator
535
+ * `computeEquivalence` checks coverage against). Each input address keeps its
536
+ * leaf `<type>.<name>[index]`; the new address prefixes it with `module.<to_module>.`.
537
+ * Works for root→module and module→module — the
538
+ * leaf is taken after stripping any existing `module.X.` hops.
539
+ */
540
+ export function generateMovedBlocks(
541
+ addresses: string[],
542
+ toModule: string,
543
+ ): { hcl: string; expected_moves: MovedBlock[] } {
544
+ const moves: MovedBlock[] = [];
545
+ for (const raw of addresses) {
546
+ const from = normaliseAddress(raw);
547
+ if (!from) continue;
548
+ const leaf = from.replace(/^(?:module\.[^.]+\.)+/, "");
549
+ moves.push({ from, to: `module.${toModule}.${leaf}` });
550
+ }
551
+ const hcl = moves
552
+ .map((mv) => `moved {\n from = ${mv.from}\n to = ${mv.to}\n}`)
553
+ .join("\n\n");
554
+ return { hcl, expected_moves: moves };
555
+ }
556
+
557
+ // --- module provenance (pure) — C3 -------------------------------------------
558
+
559
+ export interface ModuleProvenance {
560
+ /** the local `module "<name>"` call name. */
561
+ name: string;
562
+ source: string;
563
+ /** the pinned version (registry `version`, or a git `?ref=`), or null. */
564
+ version: string | null;
565
+ kind: ModuleSourceKind;
566
+ /** true when the source is pinned to an exact version/ref — the provenance
567
+ * discipline for adopting a third-party module (the analog to a SHA-pinned
568
+ * action). An unpinned external module is a supply-chain risk to flag. */
569
+ pinned: boolean;
570
+ }
571
+
572
+ /**
573
+ * Record the provenance of every EXTERNAL module the config calls (registry /
574
+ * git / remote — local modules have in-tree code, no provenance to record).
575
+ * Used by the refactor b.4 evidence: "adopted `<source>` at `<version>`",
576
+ * with `pinned` flagging an unpinned adoption. Pure over the file tree.
577
+ */
578
+ export function collectModuleProvenance(
579
+ files: SourceFile[],
580
+ ): ModuleProvenance[] {
581
+ const out: ModuleProvenance[] = [];
582
+ const seen = new Set<string>();
583
+ for (const f of files) {
584
+ for (const b of parseModuleBlocks(f.hcl)) {
585
+ if (b.kind === "local") continue;
586
+ const key = `${b.name}|${b.source}|${b.version ?? ""}`;
587
+ if (seen.has(key)) continue;
588
+ seen.add(key);
589
+ out.push({
590
+ name: b.name,
591
+ source: b.source,
592
+ version: b.version,
593
+ kind: b.kind,
594
+ pinned: b.version !== null,
595
+ });
596
+ }
597
+ }
598
+ return out;
599
+ }
600
+
601
+ /** read the workspace's `.tf`/`.tfvars` tree and record external-module
602
+ * provenance (the impure wrapper the evidence bundle calls). */
603
+ export function collectWorkspaceProvenance(cwd: string): ModuleProvenance[] {
604
+ return collectModuleProvenance(readWorkingTree(cwd));
605
+ }
606
+
607
+ // --- the `proposed — unproven` honesty primitive — C4 ------------------------
608
+
609
+ /** the GitHub label an UNPROVEN b.4 change carries so it is never mistaken for a
610
+ * proven one. Applied to the PR and surfaced in the evidence bundle. */
611
+ export const PROPOSED_UNPROVEN_LABEL = "proposed-unproven";
612
+
613
+ /**
614
+ * The unmissable PR-body banner for a behaviour-ALTERING b.4 change that ran
615
+ * WITHOUT opt-in read creds (the default): Infraweaver proposes the change but
616
+ * proves nothing about its runtime effect. It must never silently read as a
617
+ * proven/equivalence change. Used verbatim at the top of such a PR's body and
618
+ * mirrored as the `proposed_unproven` marker in the evidence bundle.
619
+ */
620
+ export const PROPOSED_UNPROVEN_BANNER =
621
+ "> [!CAUTION]\n" +
622
+ "> **proposed — unproven.** This change adopts a third-party module: it ALTERS behaviour " +
623
+ "(it can change defaults and add or remove resources), so equivalence is false and is NOT claimed. " +
624
+ `No \`terraform plan\` was run (no read-only state was supplied), so ${PRODUCT_NAME} asserts nothing about ` +
625
+ "its runtime effect — **review the plan yourself before merging.**";
626
+
627
+ // --- tool wiring -------------------------------------------------------------
628
+
629
+ /** read the `.tf`/`.tfvars` tree at a git ref into memory (the before-state).
630
+ * Returns null when the ref or git is unavailable (degrade to "no baseline"). */
631
+ function readTreeAtRef(cwd: string, ref: string): SourceFile[] | null {
632
+ const list = run("git", ["ls-tree", "-r", "--name-only", ref], cwd);
633
+ if (list.status !== 0) return null;
634
+ const files: SourceFile[] = [];
635
+ for (const raw of list.stdout.split("\n")) {
636
+ const path = raw.trim().replace(/\\/g, "/");
637
+ if (!path.endsWith(".tf") && !path.endsWith(".tfvars")) continue;
638
+ const show = run("git", ["show", `${ref}:${path}`], cwd);
639
+ if (show.status === 0) files.push({ path, hcl: show.stdout });
640
+ }
641
+ return files;
642
+ }
643
+
644
+ /** read the working-tree `.tf`/`.tfvars` files on disk (the after-state) —
645
+ * the working tree, including the agent's uncommitted edits. */
646
+ function readWorkingTree(cwd: string): SourceFile[] {
647
+ const files: SourceFile[] = [];
648
+ for (const rel of walkTfFiles(cwd)) {
649
+ if (!rel.endsWith(".tf") && !rel.endsWith(".tfvars")) continue;
650
+ try {
651
+ files.push({
652
+ path: rel.replace(/\\/g, "/"),
653
+ hcl: readFileSync(join(cwd, rel), "utf8"),
654
+ });
655
+ } catch {
656
+ /* skip unreadable file */
657
+ }
658
+ }
659
+ return files;
660
+ }
661
+
662
+ /** the mode that remediates AND refactors on one branch — its refactor half is
663
+ * measured against the COMMITTED fix, not the run-start commit (see below). */
664
+ const REMEDIATE_AND_REFACTOR_MODE = "RemediateAndRefactor";
665
+
666
+ /** resolve the commit the refactor's equivalence is measured against. Normally
667
+ * the run-start commit (mirrors guardrails.runStartSha), so the before-tree is
668
+ * exactly the state this run began from. In the combined Remediate+Refactor mode
669
+ * the remediation fix is COMMITTED before the refactor begins, so the baseline is
670
+ * that post-fix commit (the current HEAD): the fix's argument changes are the
671
+ * refactor's starting point and must NOT be counted against its equivalence. */
672
+ function resolveBaseSha(ctx: LocalToolContext, cwd: string): string | null {
673
+ if (ctx.toolState.selectedMode === REMEDIATE_AND_REFACTOR_MODE) {
674
+ const r = run("git", ["rev-parse", "HEAD"], cwd);
675
+ return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
676
+ }
677
+ const head = ctx.toolState.initialHead;
678
+ if (!head) return null;
679
+ const ref = head.kind === "branch" ? head.name : head.sha;
680
+ const r = run("git", ["rev-parse", ref], cwd);
681
+ return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
682
+ }
683
+
684
+ export const TerraformEquivalenceCheckParams = type({});
685
+
686
+ export function TerraformEquivalenceCheckTool(ctx: LocalToolContext) {
687
+ return tool({
688
+ name: "terraform_equivalence_check",
689
+ description:
690
+ "Keys-free EQUIVALENCE PROOF for a `refactor` PR (no cloud credentials). Compares the working tree " +
691
+ "against the run-start commit and proves the refactor preserved behaviour: (1) the same multiset of " +
692
+ "managed resources, (2) identical top-level argument names per resource, and (3) — the highest-risk " +
693
+ "invariant — that EVERY relocated address is covered by a `moved {}` block (zero uncovered moves), " +
694
+ "then runs `terraform validate` + `terraform fmt -check`. Returns `equivalent` (the overall verdict; " +
695
+ "the push guardrail HARD-BLOCKS when false), plus `uncovered_moves` (a missed `moved {}` = a silent " +
696
+ "destroy/recreate on apply — fix before pushing), `expected_moves`, `resource_set_diff`, and " +
697
+ "`arg_diffs`. For a behaviour-ALTERING change (b.4, adopting a third-party module) equivalence is " +
698
+ "expected to be false — do not claim it. Optional stronger proof: a real `terraform_plan` that is a " +
699
+ "pure-move plan (`refactor_safe: true`) corroborates this floor when cloud creds are present.",
700
+ parameters: TerraformEquivalenceCheckParams,
701
+ execute: execute(async () => {
702
+ const cwd = ctx.payload.cwd ?? process.cwd();
703
+ const base = resolveBaseSha(ctx, cwd);
704
+ if (!base) {
705
+ return toolSkip(
706
+ "no_baseline",
707
+ "could not resolve the run-start commit — an equivalence proof needs the before-state to diff against. Ensure the run started from a clean git checkout.",
708
+ );
709
+ }
710
+ // Combined Remediate+Refactor: the baseline is the COMMITTED fix, so the
711
+ // refactor edits must still be UNCOMMITTED for this to measure anything. A
712
+ // clean working tree here means the refactor wasn't made yet, or was
713
+ // committed before proving — either way diffing the baseline against an
714
+ // identical working tree would vacuously report `equivalent: true`. Refuse
715
+ // that false positive rather than emit a proof of nothing.
716
+ if (
717
+ ctx.toolState.selectedMode === REMEDIATE_AND_REFACTOR_MODE &&
718
+ changedFilesSinceBase(cwd, base).length === 0
719
+ ) {
720
+ return toolSkip(
721
+ "refactor_uncommitted_required",
722
+ "combined Remediate+Refactor: the equivalence proof diffs the working tree against the COMMITTED fix, but the working tree has no uncommitted changes. Commit the fix first, then make the refactor edits, then run this check BEFORE committing them.",
723
+ );
724
+ }
725
+ const beforeFiles = readTreeAtRef(cwd, base);
726
+ if (!beforeFiles) {
727
+ return toolSkip(
728
+ "no_baseline",
729
+ `could not read the Terraform tree at ${base.slice(0, 8)} — equivalence cannot be proven without the before-state.`,
730
+ );
731
+ }
732
+ const afterFiles = readWorkingTree(cwd);
733
+
734
+ const before = attributeAddresses(beforeFiles);
735
+ const after = attributeAddresses(afterFiles);
736
+ // `moved {}` blocks live in ROOT files (a module can't relocate its own
737
+ // caller's addresses), so collect them from after files NOT inside a module.
738
+ const afterCalls = new Set(
739
+ afterFiles.flatMap((f) =>
740
+ parseModuleBlocks(f.hcl)
741
+ .filter((b) => b.kind === "local")
742
+ .map((b) =>
743
+ posix
744
+ .normalize(posix.join(posix.dirname(f.path), b.source))
745
+ .replace(/\/+$/, ""),
746
+ ),
747
+ ),
748
+ );
749
+ const rootFiles = afterFiles.filter(
750
+ (f) => ![...afterCalls].some((dir) => fileInDir(f.path, dir)),
751
+ );
752
+ const movedBlocks = rootFiles.flatMap((f) => parseMovedBlocks(f.hcl));
753
+
754
+ const result = computeEquivalence(
755
+ before.resources,
756
+ after.resources,
757
+ movedBlocks,
758
+ [...new Set([...before.ambiguous, ...after.ambiguous])],
759
+ );
760
+
761
+ // validate + fmt (the keys-free proof's correctness gates). The credentials
762
+ // let a private cross-repo `git::` module or a private-registry module
763
+ // resolve during init, mirroring validate.
764
+ const terraformEnv = resolveModuleCredentialEnv(ctx.payload, {
765
+ sshKeyDir: ctx.tmpdir,
766
+ });
767
+ const fmtOutcome = scanFmt(cwd);
768
+ const validateOutcome = scanValidate(cwd, terraformEnv);
769
+ const validateRan = validateOutcome.ran;
770
+ const validatePassed =
771
+ validateRan &&
772
+ validateOutcome.concerns.length === 0 &&
773
+ (validateOutcome.unvalidated ?? 0) === 0;
774
+ // fmt-clean is scoped to the files this run changed — pre-existing drift in
775
+ // untouched files is not the refactor's to fix and must not fail its proof.
776
+ const changed = new Set(changedFilesSinceBase(cwd, base));
777
+ const fmtDriftFiles = [
778
+ ...new Set(
779
+ fmtOutcome.concerns
780
+ .map((c) => c.location.file)
781
+ .filter((f) => changed.size === 0 || changed.has(f)),
782
+ ),
783
+ ];
784
+ const fmtClean = fmtOutcome.ran && fmtDriftFiles.length === 0;
785
+
786
+ const reasons = [...result.reasons];
787
+ if (!validatePassed)
788
+ reasons.push(
789
+ validateRan
790
+ ? "`terraform validate` did not pass"
791
+ : "`terraform validate` did not run (terraform not installed) — the equivalence proof is incomplete",
792
+ );
793
+ if (!fmtClean && fmtOutcome.ran && fmtDriftFiles.length > 0)
794
+ reasons.push(
795
+ `\`terraform fmt\` reports drift in changed file(s): ${fmtDriftFiles.join(", ")}`,
796
+ );
797
+
798
+ const equivalent = result.equivalent && validatePassed && fmtClean;
799
+
800
+ // record for the push-time hard-fail guardrail (read in guardrails.ts) and
801
+ // the evidence bundle (read in terraform_emit_evidence).
802
+ scanSessionOf(ctx.toolState).refactorEquivalence = {
803
+ equivalent,
804
+ resource_set_equivalent: result.resource_set_equivalent,
805
+ arg_names_equivalent: result.arg_names_equivalent,
806
+ moves_exhaustive: result.moves_exhaustive,
807
+ moves_detected: result.moves_detected,
808
+ validate_passed: validatePassed,
809
+ fmt_clean: fmtClean,
810
+ uncovered_moves: result.uncovered_moves,
811
+ reasons,
812
+ };
813
+
814
+ log.info(
815
+ `» terraform_equivalence_check: equivalent=${equivalent} (moves ${result.expected_moves.length}, uncovered ${result.uncovered_moves.length}, resource-set ${result.resource_set_equivalent}, args ${result.arg_names_equivalent}, validate ${validatePassed}, fmt ${fmtClean})`,
816
+ );
817
+
818
+ return toolOk({
819
+ ...result,
820
+ equivalent,
821
+ validate_passed: validatePassed,
822
+ validate_ran: validateRan,
823
+ fmt_clean: fmtClean,
824
+ fmt_drift_files: fmtDriftFiles,
825
+ base: base.slice(0, 8),
826
+ reasons,
827
+ note: equivalent
828
+ ? "behaviour-preserving refactor PROVEN (keys-free). For the stronger proof, run terraform_plan — refactor_safe:true corroborates it."
829
+ : "NOT equivalent — the push will be blocked. Fix the issues in `reasons` (an uncovered move is the dangerous one — add the missing `moved {}` block).",
830
+ });
831
+ }),
832
+ });
833
+ }
834
+
835
+ /** files changed (tracked) between the base commit and the working tree. */
836
+ function changedFilesSinceBase(cwd: string, base: string): string[] {
837
+ const r = run("git", ["diff", "--name-only", base], cwd);
838
+ if (r.status !== 0) return [];
839
+ return r.stdout
840
+ .split("\n")
841
+ .map((l) => l.trim().replace(/\\/g, "/"))
842
+ .filter(Boolean);
843
+ }
844
+
845
+ export const TerraformGenerateMovedParams = type({
846
+ resources: type.string
847
+ .array()
848
+ .describe(
849
+ "the CURRENT addresses of the resources being relocated, e.g. ['aws_s3_bucket.logs', 'aws_s3_bucket_policy.logs'] (root) or ['module.old.aws_vpc.main'] (module→module).",
850
+ ),
851
+ to_module: type.string.describe(
852
+ 'the local name of the `module "<name>"` call the resources are moving INTO (the new address prefix).',
853
+ ),
854
+ });
855
+
856
+ export function TerraformGenerateMovedTool(ctx: LocalToolContext) {
857
+ return tool({
858
+ name: "terraform_generate_moved",
859
+ description:
860
+ "Generate the EXHAUSTIVE `moved {}` blocks for a resources→module refactor — one per relocated " +
861
+ "address (old → `module.<to_module>.<leaf>`), so the move set is complete by construction. Returns " +
862
+ "the ready-to-paste `hcl` (put it in a ROOT file) and `expected_moves` (the authoritative mapping " +
863
+ "`terraform_equivalence_check` verifies coverage against). A missing `moved {}` block is the single " +
864
+ "highest-risk refactor defect (a silent destroy/recreate) — generate them here rather than hand-" +
865
+ "writing so none is forgotten.",
866
+ parameters: TerraformGenerateMovedParams,
867
+ // pure string generation — no state change, safe for subagents.
868
+ execute: execute(async ({ resources, to_module }) => {
869
+ void ctx;
870
+ const { hcl, expected_moves } = generateMovedBlocks(resources, to_module);
871
+ return toolOk({
872
+ count: expected_moves.length,
873
+ expected_moves,
874
+ hcl,
875
+ note: "paste `hcl` into a root .tf file, then prove the refactor with terraform_equivalence_check (and terraform_plan when cloud creds are available).",
876
+ });
877
+ }),
878
+ });
879
+ }