infraweaver 0.3.7 → 0.3.8

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 (175) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/claude.d.ts +1 -1
  3. package/dist/agents/claudePretoolGate.d.ts +0 -36
  4. package/dist/agents/toolPlaneWatch.d.ts +31 -0
  5. package/dist/cli.mjs +31903 -18736
  6. package/dist/external.d.ts +2 -1
  7. package/dist/index.js +31987 -18749
  8. package/dist/internal.js +132 -52
  9. package/dist/mcp/baseBranch.d.ts +41 -0
  10. package/dist/mcp/changeSummary.d.ts +1 -1
  11. package/dist/mcp/findingAudit.d.ts +2 -0
  12. package/dist/mcp/guardrails.d.ts +38 -0
  13. package/dist/mcp/localContext.d.ts +1 -1
  14. package/dist/mcp/moduleExtraction.d.ts +5 -0
  15. package/dist/mcp/pr.d.ts +0 -31
  16. package/dist/mcp/reproveSuggestions.d.ts +27 -0
  17. package/dist/mcp/review.d.ts +126 -2
  18. package/dist/mcp/reviewCommentLimit.d.ts +17 -0
  19. package/dist/mcp/reviewComments.d.ts +9 -5
  20. package/dist/mcp/reviewDedup.d.ts +1 -0
  21. package/dist/mcp/reviewMarkers.d.ts +10 -0
  22. package/dist/mcp/reviewProvenance.d.ts +20 -6
  23. package/dist/mcp/shared.d.ts +15 -0
  24. package/dist/mcp/suggestionProver.d.ts +3 -2
  25. package/dist/mcp/terraform/checkovSeverities.d.ts +7 -0
  26. package/dist/mcp/terraform/consolidation.d.ts +26 -0
  27. package/dist/mcp/terraform/cost.d.ts +2 -2
  28. package/dist/mcp/terraform/currency.d.ts +11 -0
  29. package/dist/mcp/terraform/deltaSummary.d.ts +11 -1
  30. package/dist/mcp/terraform/emittedArtefacts.d.ts +11 -0
  31. package/dist/mcp/terraform/evidence.d.ts +10 -0
  32. package/dist/mcp/terraform/gateScan.d.ts +2 -16
  33. package/dist/mcp/terraform/hcl.d.ts +15 -0
  34. package/dist/mcp/terraform/initArtefacts.d.ts +21 -0
  35. package/dist/mcp/terraform/localModules.d.ts +29 -0
  36. package/dist/mcp/terraform/moduleFindings.d.ts +38 -0
  37. package/dist/mcp/terraform/refactor/attribution.d.ts +1 -1
  38. package/dist/mcp/terraform/refactor/equivalence.d.ts +7 -0
  39. package/dist/mcp/terraform/refactor/expressions.d.ts +62 -0
  40. package/dist/mcp/terraform/refactor/references.d.ts +15 -0
  41. package/dist/mcp/terraform/refactor/resources.d.ts +11 -0
  42. package/dist/mcp/terraform/refactor/substitution.d.ts +55 -37
  43. package/dist/mcp/terraform/scanDelta.d.ts +28 -0
  44. package/dist/mcp/terraform/scanSession.d.ts +13 -0
  45. package/dist/mcp/terraform/scanners.d.ts +67 -18
  46. package/dist/mcp/terraform/secretScan.d.ts +19 -0
  47. package/dist/mcp/terraform/standardsReport.d.ts +1 -1
  48. package/dist/mcp/terraform/tools/plan.d.ts +1 -1
  49. package/dist/mcp/terraform/tools/scan.d.ts +7 -7
  50. package/dist/mcp/terraform/tools/validate.d.ts +7 -2
  51. package/dist/mcp/terraform/tools/verifyRemediation.d.ts +1 -1
  52. package/dist/mcp/terraform/treeLayout.d.ts +57 -0
  53. package/dist/mcp/terraform/types.d.ts +1 -1
  54. package/dist/phases/applyPlan.d.ts +0 -2
  55. package/dist/toolState.d.ts +3 -0
  56. package/dist/utils/agent/codexOAuth.d.ts +1 -1
  57. package/dist/utils/cli.d.ts +1 -7
  58. package/dist/utils/cloud/cloudReport.d.ts +1 -1
  59. package/dist/utils/config/baseRefConfig.d.ts +1 -1
  60. package/dist/utils/config/infraweaverConfig.d.ts +1 -1
  61. package/dist/utils/config/payload.d.ts +5 -0
  62. package/dist/utils/markdownTable.d.ts +6 -0
  63. package/dist/utils/setup/toolLicensing.d.ts +1 -1
  64. package/dist/utils/unifiedDiff.d.ts +69 -0
  65. package/package.json +34 -28
  66. package/src/agents/claude.ts +48 -13
  67. package/src/agents/claudePretoolGate.ts +9 -17
  68. package/src/agents/sessionLabeler.ts +3 -3
  69. package/src/agents/toolPlaneWatch.ts +71 -0
  70. package/src/external.ts +3 -1
  71. package/src/mcp/assess.ts +8 -8
  72. package/src/mcp/baseBranch.ts +96 -0
  73. package/src/mcp/changeSummary.ts +10 -11
  74. package/src/mcp/checkSuite.ts +7 -10
  75. package/src/mcp/checkout.ts +13 -40
  76. package/src/mcp/crosswalk.ts +5 -0
  77. package/src/mcp/findingAudit.ts +1 -1
  78. package/src/mcp/git.ts +44 -2
  79. package/src/mcp/guardrails.ts +188 -36
  80. package/src/mcp/localContext.ts +2 -0
  81. package/src/mcp/localServer.ts +1 -0
  82. package/src/mcp/moduleExtraction.ts +17 -5
  83. package/src/mcp/moduleTests.ts +9 -2
  84. package/src/mcp/modules.ts +10 -7
  85. package/src/mcp/pr.ts +17 -65
  86. package/src/mcp/reproveSuggestions.ts +86 -0
  87. package/src/mcp/review.ts +249 -75
  88. package/src/mcp/reviewAnchor.ts +11 -23
  89. package/src/mcp/reviewCommentLimit.ts +79 -0
  90. package/src/mcp/reviewComments.ts +34 -81
  91. package/src/mcp/reviewDedup.ts +1 -1
  92. package/src/mcp/reviewMarkers.ts +25 -2
  93. package/src/mcp/reviewProvenance.ts +56 -21
  94. package/src/mcp/roots.ts +3 -2
  95. package/src/mcp/server.ts +2 -1
  96. package/src/mcp/shared.ts +40 -1
  97. package/src/mcp/shell.ts +27 -28
  98. package/src/mcp/suggestionProver.ts +8 -4
  99. package/src/mcp/terraform/checkovSeverities.ts +1156 -0
  100. package/src/mcp/terraform/concernResult.ts +2 -1
  101. package/src/mcp/terraform/consolidation.ts +232 -19
  102. package/src/mcp/terraform/cost.ts +16 -7
  103. package/src/mcp/terraform/cspm.ts +8 -4
  104. package/src/mcp/terraform/currency.ts +28 -12
  105. package/src/mcp/terraform/decisions.ts +1 -1
  106. package/src/mcp/terraform/deltaSummary.ts +50 -1
  107. package/src/mcp/terraform/emittedArtefacts.ts +63 -0
  108. package/src/mcp/terraform/evidence.ts +23 -3
  109. package/src/mcp/terraform/gateScan.ts +191 -128
  110. package/src/mcp/terraform/hcl.ts +27 -2
  111. package/src/mcp/terraform/idleFloor.ts +2 -1
  112. package/src/mcp/terraform/initArtefacts.ts +76 -0
  113. package/src/mcp/terraform/localModules.ts +112 -0
  114. package/src/mcp/terraform/moduleDocs.ts +2 -5
  115. package/src/mcp/terraform/moduleFindings.ts +102 -0
  116. package/src/mcp/terraform/moduleVersionConstraints.ts +1 -1
  117. package/src/mcp/terraform/planPairs.ts +2 -20
  118. package/src/mcp/terraform/policyGate.ts +0 -15
  119. package/src/mcp/terraform/refactor/attribution.ts +39 -5
  120. package/src/mcp/terraform/refactor/equivalence.ts +49 -10
  121. package/src/mcp/terraform/refactor/expressions.ts +323 -0
  122. package/src/mcp/terraform/refactor/references.ts +101 -0
  123. package/src/mcp/terraform/refactor/resources.ts +79 -7
  124. package/src/mcp/terraform/refactor/substitution.ts +852 -256
  125. package/src/mcp/terraform/scanDelta.ts +34 -0
  126. package/src/mcp/terraform/scanSession.ts +30 -0
  127. package/src/mcp/terraform/scannerCache.ts +4 -0
  128. package/src/mcp/terraform/scanners.ts +553 -233
  129. package/src/mcp/terraform/secretScan.ts +131 -0
  130. package/src/mcp/terraform/standardsReport.ts +14 -13
  131. package/src/mcp/terraform/tools/consolidationCandidates.ts +3 -0
  132. package/src/mcp/terraform/tools/emitEvidence.ts +13 -5
  133. package/src/mcp/terraform/tools/emitOscal.ts +3 -2
  134. package/src/mcp/terraform/tools/emitVex.ts +10 -4
  135. package/src/mcp/terraform/tools/equivalenceCheck.ts +139 -21
  136. package/src/mcp/terraform/tools/infracostDiff.ts +2 -2
  137. package/src/mcp/terraform/tools/plan.ts +30 -17
  138. package/src/mcp/terraform/tools/reviewScanDelta.ts +3 -0
  139. package/src/mcp/terraform/tools/scan.ts +5 -3
  140. package/src/mcp/terraform/tools/standardsReport.ts +1 -1
  141. package/src/mcp/terraform/tools/validate.ts +20 -5
  142. package/src/mcp/terraform/tools/verifyRemediation.ts +14 -7
  143. package/src/mcp/terraform/tools/versionCurrency.ts +3 -1
  144. package/src/mcp/terraform/treeLayout.ts +378 -0
  145. package/src/mcp/terraform/types.ts +4 -1
  146. package/src/mcp/terraform/versionRequirements.ts +11 -2
  147. package/src/modes/__snapshots__/assemblePrompt.test.ts.snap +2 -0
  148. package/src/modes/modernize-deprecated.ts +2 -2
  149. package/src/modes/refactor.ts +9 -9
  150. package/src/modes/remediate.ts +1 -1
  151. package/src/modes/terraform-code-review.ts +6 -3
  152. package/src/modes/update-dependencies.ts +6 -6
  153. package/src/phases/applyPlan.ts +33 -14
  154. package/src/phases/applySuggestions.ts +2 -2
  155. package/src/phases/runAgentWithWatchdogs.ts +2 -1
  156. package/src/toolState.ts +7 -0
  157. package/src/utils/agent/codexOAuth.ts +7 -7
  158. package/src/utils/agent/openCodeModels.ts +4 -6
  159. package/src/utils/cli.ts +1 -31
  160. package/src/utils/cloud/runContext.ts +1 -8
  161. package/src/utils/config/baseRefConfig.ts +6 -0
  162. package/src/utils/config/infraweaverConfig.ts +2 -0
  163. package/src/utils/config/payload.ts +41 -0
  164. package/src/utils/github/assets.ts +1 -6
  165. package/src/utils/github/github.ts +42 -150
  166. package/src/utils/github/token.ts +6 -11
  167. package/src/utils/log.ts +4 -5
  168. package/src/utils/markdownTable.ts +11 -0
  169. package/src/utils/prompt/changeImpact.ts +18 -26
  170. package/src/utils/prompt/instructions.ts +1 -1
  171. package/src/utils/prompt/promptDirectives.ts +1 -1
  172. package/src/utils/prompt/toolSelection.ts +11 -2
  173. package/src/utils/setup/toolLicensing.ts +7 -0
  174. package/src/utils/telemetry.ts +2 -1
  175. package/src/utils/unifiedDiff.ts +120 -0
package/dist/internal.js CHANGED
@@ -1,6 +1,6 @@
1
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
2
 
3
- // node_modules/.pnpm/@infraweaver-io+ci@0.1.7/node_modules/@infraweaver-io/ci/dist/progressComment.js
3
+ // node_modules/.pnpm/@infraweaver-io+ci@0.1.8/node_modules/@infraweaver-io/ci/dist/progressComment.js
4
4
  async function getProgressComment(ctx, comment) {
5
5
  const result = await (comment.type === "review" ? ctx.octokit.rest.pulls.getReviewComment({
6
6
  owner: ctx.owner,
@@ -94,7 +94,7 @@ async function createLeapingProgressComment(ctx, target, body) {
94
94
  };
95
95
  }
96
96
 
97
- // node_modules/.pnpm/@infraweaver-io+core@0.1.4/node_modules/@infraweaver-io/core/dist/time.js
97
+ // node_modules/.pnpm/@infraweaver-io+core@0.1.5/node_modules/@infraweaver-io/core/dist/time.js
98
98
  var TIMEOUT_DISABLED = "none";
99
99
  var TIME_STRING_REGEX = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/;
100
100
  function parseTimeString(input) {
@@ -110,7 +110,7 @@ function isValidTimeString(input) {
110
110
  return parseTimeString(input) !== null;
111
111
  }
112
112
 
113
- // node_modules/.pnpm/@infraweaver-io+models@0.1.5/node_modules/@infraweaver-io/models/dist/catalog.js
113
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/catalog.js
114
114
  function provider(config) {
115
115
  return config;
116
116
  }
@@ -121,32 +121,32 @@ var providers = {
121
121
  models: {
122
122
  // Claude Fable is access-gated on Anthropic: many BYOK keys can't invoke
123
123
  // it, so auto-selecting it as the Anthropic default 404s on the first model
124
- // call (matches the upstream pullfrog fix that demoted it). It's marked
125
- // deprecated via `fallback` → opus, so any fable request (auto or explicit)
126
- // resolves to the universally-available Opus 5 instead of dead-on-arrival.
127
- // Drop the `fallback` line to re-enable Fable once access is broad / for a
128
- // deployment whose BYOK keys are known to have it. Zen meters Fable like
129
- // any other model — `opencode/claude-fable` below reaches it today.
124
+ // call (matches the upstream pullfrog fix that demoted it). So it is
125
+ // `hidden` — never auto-selected and absent from the pickers — but NOT
126
+ // deprecated: a `fallback` would also turn an explicit `claude-fable`
127
+ // request into Opus without a word, and a run must use the model it was
128
+ // asked for. Unhide it once access is broad. Zen meters Fable like any
129
+ // other model — `opencode/claude-fable` below reaches it today.
130
130
  "claude-fable": {
131
131
  displayName: "Claude Fable",
132
132
  resolve: "anthropic/claude-fable-5-1",
133
133
  // rolling alias: OpenRouter's catalog lags brand-new pinned versions
134
134
  // (claude-fable-5-1 is not indexed), so track ~…-latest.
135
135
  openRouterResolve: "openrouter/~anthropic/claude-fable-latest",
136
- fallback: "anthropic/claude-opus",
136
+ hidden: true,
137
137
  subagentModel: "claude-sonnet"
138
138
  },
139
139
  "claude-opus": {
140
140
  displayName: "Claude Opus",
141
- resolve: "anthropic/claude-opus-5",
142
- openRouterResolve: "openrouter/anthropic/claude-opus-5",
141
+ resolve: "anthropic/claude-opus-5-5",
142
+ openRouterResolve: "openrouter/anthropic/claude-opus-5.5",
143
143
  preferred: true,
144
144
  subagentModel: "claude-sonnet"
145
145
  },
146
146
  "claude-sonnet": {
147
147
  displayName: "Claude Sonnet",
148
- resolve: "anthropic/claude-sonnet-5",
149
- openRouterResolve: "openrouter/anthropic/claude-sonnet-5"
148
+ resolve: "anthropic/claude-sonnet-5-5",
149
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-5.5"
150
150
  },
151
151
  "claude-haiku": {
152
152
  displayName: "Claude Haiku",
@@ -388,18 +388,19 @@ var providers = {
388
388
  // opencode/claude-opus-5` emitting nothing for 240s in CI as late as
389
389
  // 2026-09-03: opencode retries above the AI SDK with no part.updated, so
390
390
  // a run just produces nothing until it is killed. A raw-endpoint 200 is
391
- // not runtime availability.
391
+ // not runtime availability. The move to claude-opus-5-5 proves nothing
392
+ // either way: the fallback stays until a run completes on the new id.
392
393
  "claude-opus": {
393
394
  displayName: "Claude Opus",
394
- resolve: "opencode/claude-opus-5",
395
- openRouterResolve: "openrouter/anthropic/claude-opus-5",
395
+ resolve: "opencode/claude-opus-5-5",
396
+ openRouterResolve: "openrouter/anthropic/claude-opus-5.5",
396
397
  subagentModel: "claude-sonnet",
397
398
  fallback: "opencode/claude-sonnet"
398
399
  },
399
400
  "claude-sonnet": {
400
401
  displayName: "Claude Sonnet",
401
- resolve: "opencode/claude-sonnet-5",
402
- openRouterResolve: "openrouter/anthropic/claude-sonnet-5"
402
+ resolve: "opencode/claude-sonnet-5-5",
403
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-5.5"
403
404
  },
404
405
  "claude-haiku": {
405
406
  displayName: "Claude Haiku",
@@ -587,15 +588,15 @@ var providers = {
587
588
  models: {
588
589
  "claude-opus": {
589
590
  displayName: "Claude Opus",
590
- resolve: "openrouter/anthropic/claude-opus-5",
591
- openRouterResolve: "openrouter/anthropic/claude-opus-5",
591
+ resolve: "openrouter/anthropic/claude-opus-5.5",
592
+ openRouterResolve: "openrouter/anthropic/claude-opus-5.5",
592
593
  preferred: true,
593
594
  subagentModel: "claude-sonnet"
594
595
  },
595
596
  "claude-sonnet": {
596
597
  displayName: "Claude Sonnet",
597
- resolve: "openrouter/anthropic/claude-sonnet-5",
598
- openRouterResolve: "openrouter/anthropic/claude-sonnet-5"
598
+ resolve: "openrouter/anthropic/claude-sonnet-5.5",
599
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-5.5"
599
600
  },
600
601
  "claude-haiku": {
601
602
  displayName: "Claude Haiku",
@@ -745,13 +746,13 @@ var providers = {
745
746
  models: {
746
747
  "claude-opus": {
747
748
  displayName: "Claude Opus",
748
- resolve: "vercel/anthropic/claude-opus-5",
749
+ resolve: "vercel/anthropic/claude-opus-5.5",
749
750
  preferred: true,
750
751
  subagentModel: "claude-sonnet"
751
752
  },
752
753
  "claude-sonnet": {
753
754
  displayName: "Claude Sonnet",
754
- resolve: "vercel/anthropic/claude-sonnet-5"
755
+ resolve: "vercel/anthropic/claude-sonnet-5.5"
755
756
  },
756
757
  "claude-haiku": {
757
758
  displayName: "Claude Haiku",
@@ -1687,7 +1688,7 @@ ${PR_SUMMARY_FORMAT}`
1687
1688
  function modernizeDeprecatedMode(t) {
1688
1689
  return {
1689
1690
  name: "ModernizeDeprecated",
1690
- description: "Migrate deprecated Terraform patterns to their modern equivalents (aws_launch_configuration \u2192 aws_launch_template, the template_file data source \u2192 the templatefile() function, EOL provider pins), one PR per pattern class. These ALTER the resource set, so they are proven by a reviewed plan delta (or shipped `proposed \u2014 unproven`) with a `moved {}` block wherever an address can be preserved; equivalence is never claimed.",
1691
+ description: "Migrate deprecated Terraform patterns to their modern equivalents (aws_launch_configuration \u2192 aws_launch_template, the template_file data source \u2192 the templatefile() function, EOL provider pins), one PR per pattern class. These ALTER the resource set, so they are proven by a reviewed plan delta (or shipped proposed \u2014 unproven) with a `moved {}` block wherever an address can be preserved; equivalence is never claimed.",
1691
1692
  prompt: `### Checklist
1692
1693
 
1693
1694
  This mode migrates DEPRECATED Terraform patterns to modern ones. Unlike Refactor, these migrations swap a resource TYPE / data source / function and therefore ALTER the resource set \u2014 so equivalence is FALSE and must NEVER be claimed (the equivalence guard would block such a claim anyway). The proof is a reviewed plan delta when cloud credentials exist, otherwise the change ships labelled \`proposed \u2014 unproven\`. Author a \`moved {}\` block wherever an address genuinely carries over, and use \`create_before_destroy\` where a replace is unavoidable.
@@ -1704,7 +1705,7 @@ This mode migrates DEPRECATED Terraform patterns to modern ones. Unlike Refactor
1704
1705
 
1705
1706
  6. **prove via plan, never equivalence**: do NOT call \`${t("terraform_equivalence_check")}\` \u2014 this change is behaviour-altering and an equivalence claim would be false (and is hard-blocked at push). Instead:
1706
1707
  - **cloud credentials present**: call \`${t("terraform_plan")}\` and attach the **reviewed plan delta**. Treat \`has_destroy_or_replace\`/\`stateful_destructive\` exactly as Remediate \u2014 a stateful destroy/replace is hard-blocked at push unless \`allow_replace\` covers it; a high \`blast_radius.tier\` or \`needs_human\` adds the \`needs-human\` label (\`${t("add_labels")}\`) + a prominent callout.
1707
- - **no credentials**: ship \`proposed \u2014 unproven\` \u2014 add the label, put the proposed-unproven banner unmissably at the top of the body, and state plainly that this migration ALTERS resources and the plan must be reviewed before merge.
1708
+ - **no credentials**: ship it proposed \u2014 unproven \u2014 add the \`proposed-unproven\` label (exactly that name), put the proposed-unproven banner unmissably at the top of the body, and state plainly that this migration ALTERS resources and the plan must be reviewed before merge.
1708
1709
  - record the outcome with \`${t("terraform_emit_evidence")}\`.
1709
1710
 
1710
1711
  7. **keep module tests consistent** (if a local module changed): \`${t("terraform_module_tests")}\` \u2014 update drifting \`examples/\`/tests, never weaken an assertion.
@@ -1778,21 +1779,21 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
1778
1779
  2. **detect (two kinds of behaviour-preserving refactor \u2014 call BOTH)**:
1779
1780
  - \`${t("module_extraction_candidates")}\` \u2192 resource \`clusters\` (raw root resources that should likely be a module call), each with matched \`candidates\` (house modules by resource-type signature; catalogue modules by real signature when terraform init already fetched their content, else by keyword; plus 1\u20132-resource groups that EXACTLY duplicate an existing module) and per-candidate \`expected_moves\` (every cluster resource \u2192 its new module address).
1780
1781
  - \`${t("terraform_normalization_candidates")}\` \u2192 in-place idiomatic cleanups that change SYNTAX, not behaviour (redundant whole-string \`"\${expr}"\` interpolation; legacy HCL0.11 map-argument block syntax like \`vars {\` on a \`template_file\`). These relocate NOTHING \u2014 zero \`moved {}\` blocks \u2014 and are proven by the same equivalence check.
1781
- - \`${t("terraform_consolidation_candidates")}\` \u2192 the SAME resource shape declared in several root directories (\`env/dev\` + \`env/staging\` + \`env/prod\`), with the single parameterised module they could all call already derived: which attributes are identical (module content) and which differ (per-environment inputs). A member root of a group will ALSO show up as a zero-candidate cluster in \`${t("module_extraction_candidates")}\` \u2014 prefer the consolidation, which does that work once for every environment instead of once per environment.
1782
+ - \`${t("terraform_consolidation_candidates")}\` \u2192 the SAME resource shape declared in several root directories (\`env/dev\` + \`env/staging\` + \`env/prod\`), with the single parameterised module they could all call already derived: which attributes are identical (module content) and which differ (per-environment inputs). A member root of a group will ALSO show up as a zero-candidate cluster in \`${t("module_extraction_candidates")}\` \u2014 prefer the consolidation, which does that work once for every environment instead of once per environment. Its \`near_groups\` are the opposite case: roots recognisably the same stack that DIFFER (one carries a resource the other does not), where folding them together is a design decision this mode has no basis for. Do NOT extract a near-group root's shared resources into a module on its own either \u2014 a module one twin calls and the other does not widens exactly the drift the near-group reports. Leave those roots alone and name the near-group and its \`differences\` in your report.
1782
1783
 
1783
1784
  **Before picking, check the interface.** \`${t("module_extraction_candidates")}\` reports \`unmapped_attributes\` per candidate and a top-level \`partial_interfaces\` list: arguments the raw resources SET that the target module does not. Adopting such a module DROPS them silently \u2014 and the equivalence check cannot catch it, because once the argument is gone it is absent from both sides. Two of these are unrecoverable rather than merely wrong (\`object_lock_enabled\` cannot be set after creation; \`force_destroy\` changes deletion semantics). So: **decline the candidate, or extend the module to expose the argument and say so in the PR body.** Deleting the argument to make the shapes match is never the fix. A candidate whose \`unmapped_attributes\` is \`null\` was NOT checked \u2014 the module is external and its code is not in this repo \u2014 which is not the same as clean.
1784
1785
 
1785
- Pick the highest-value refactor across ALL THREE detectors. Only when they ALL come back empty is there genuinely nothing to do: call \`${t("report_progress")}\` with an ACCURATE message \u2014 e.g. "No behaviour-preserving refactor found: no extraction clusters and no idiomatic-normalisation candidates." \u2014 and **stop**. Two anti-patterns to avoid in that message: (i) never call a repo "already idiomatic" while normalisation candidates remain; (ii) when what's left is behaviour-CHANGING modernisation \u2014 a deprecated-resource swap (\`aws_launch_configuration\`\u2192\`aws_launch_template\`, \`aws_elb\`\u2192\`aws_lb\`, \`template_file\`\u2192\`templatefile()\`) \u2014 do NOT report "nothing to refactor": that is real work, but it ALTERS the resource set, so it belongs to Remediate. Say so explicitly rather than implying the code is clean.
1786
+ Pick the highest-value refactor across ALL THREE detectors. Only when they ALL come back empty \u2014 or all that remains is a cluster in a near-group root \u2014 is there genuinely nothing to do: call \`${t("report_progress")}\` with an ACCURATE message \u2014 e.g. "No behaviour-preserving refactor found: no extraction clusters and no idiomatic-normalisation candidates." \u2014 and **stop**. Two anti-patterns to avoid in that message: (i) never call a repo "already idiomatic" while normalisation candidates remain; (ii) when what's left is behaviour-CHANGING modernisation \u2014 a deprecated-resource swap (\`aws_launch_configuration\`\u2192\`aws_launch_template\`, \`aws_elb\`\u2192\`aws_lb\`, \`template_file\`\u2192\`templatefile()\`) \u2014 do NOT report "nothing to refactor": that is real work, but it ALTERS the resource set, so it belongs to Remediate. Say so explicitly rather than implying the code is clean.
1786
1787
 
1787
1788
  3. **pick scope \u2014 ONE refactor per PR**: take the largest / highest-cohesion extraction cluster, the file(s) with the most normalisation sites, or ONE consolidation group (which is one refactor spanning its member roots \u2014 see b.5), first. **Targeted directive override:** if YOUR TASK names specific files/resources, act only on those. Unless the task explicitly asks for more, open **at most one PR this run**.
1788
1789
 
1789
1790
  **If you chose an in-place NORMALISATION** (not a module extraction), nothing relocates: SKIP steps 4\u20136 (no module source, no wiring, no \`moved {}\` blocks). Apply the \`${t("terraform_normalization_candidates")}\` edits directly (rewrite \`"\${expr}"\` \u2192 \`expr\`, \`vars {\` \u2192 \`vars = {\`, etc.) \u2014 only the literal cleanups, never a resource-type swap \u2014 then jump to step 7. The equivalence check (step 8) proves the no-op: same resource set, identical argument names, zero moves.
1790
1791
 
1791
1792
  4. **resolve the module source (risk order \u2014 PRESERVING variants only)**: choose where the module code comes from. This mode ships only behaviour-preserving refactors; pick the lowest-risk viable source:
1792
- - **b.2 \u2014 module exists, same repo** (candidate \`kind: local\`): call it by its \`./modules/x\` path. No credential. *Prefer this.*
1793
+ - **b.2 \u2014 module exists, same repo** (candidate \`kind: local\`): write the candidate's \`call_source\` as the module block's \`source\` \u2014 the path from the calling root, e.g. \`../../modules/x\` from \`env/dev\`. The candidate's \`source\` is repo-relative and resolves only from the repo root. No credential. *Prefer this.*
1793
1794
  - **b.1 \u2014 module exists, separate org repo** (candidate \`kind: git\`): use a \`git::\u2026?ref=<pinned-sha>\` source (pin an exact commit, never a branch). The equivalence proof (step 8) attributes the module's resources from what \`terraform init\` fetched at that ref \u2014 the init runs inside \`${t("terraform_validate")}\` and the equivalence check itself \u2014 so a true relocation into an org module proves \`equivalent: true\` exactly like a local one. If the fetch fails (no access, offline), the module stays invisible to the proof and it DECLINES: fix access or abandon, never relabel. Access is via the GitHub App token (or the operator's read-only deploy key) and is **READ-ONLY** \u2014 \u26A0\uFE0F **NEVER open a write PR against the module repo.** If a concern lives inside the shared module, fix it at the CALL SITE via inputs, or note "fix needed in module repo \`<repo>\`" \u2014 do not edit vendored module code. A candidate's \`expected_moves\` for a git module is a TEMPLATE built from the root's own leaf names \u2014 correct each \`to\` against the module's REAL resource addresses (read the fetched module, or the equivalence check's \`uncovered_moves\`/\`resource_set_diff\` will name them).
1794
- - **b.3 \u2014 no module exists, author it** (no candidate): MECHANICAL extraction. Take the cluster's existing resource blocks, move them verbatim into \`./modules/<name>/\`, parameterise each hardcoded value into a \`variable\` (with a SECURE \`default\`, or **no default** so the caller must set it \u2014 NEVER preserve an insecure value as a default), expose the needed \`output\`s, and replace the root resources with a \`module\` block passing the ORIGINAL values. You do the naming + structure; the equivalence check (step 8) is the guardrail that proves you changed nothing.
1795
- - **b.5 \u2014 consolidation: one module, N environment roots** (a \`${t("terraform_consolidation_candidates")}\` group): the b.3 extraction done once for a shape several roots duplicate. Author \`./modules/<name>/\` from the group's resources, then convert EVERY member root in this same PR. Rules that are not optional: (i) move each resource block **verbatim, keeping its resource name** \u2014 the group's \`expected_moves_by_member\` assumes the leaf name survives, and renaming it to \`this\` invalidates every move; (ii) put each \`module_body_values\` entry in the module exactly as written \u2014 \`kind: "attribute"\` is a right-hand side (\`bucket = <value>\`), \`kind: "block"\` is a nested block's BODY and goes back inside its own braces (\`versioning_configuration { <value> }\`), never as \`versioning_configuration = { \u2026 }\`; (iii) replace ONLY the \`variables\` attributes with \`var.<name>\`, and copy each member's \`per_member.value\` into THAT member's call **verbatim \u2014 never retyped, never reformatted, never swapped between environments**. A \`classification: reference\` variable (identical text like \`var.bucket_name\` in every root) is still per-environment: the text matches, the value does not. Never fold differing values into one interpolated template (\`"acme-\${var.env}-logs"\`) \u2014 the job is to preserve N values, not to invent a rule that reproduces them. Paste each root's \`moved {}\` blocks, then prove ALL the roots with a single \`${t("terraform_equivalence_check")}\`.
1795
+ - **b.3 \u2014 no module exists, author it** (no candidate): MECHANICAL extraction. Take the cluster's existing resource blocks, move them verbatim into \`./modules/<name>/\`, parameterise each hardcoded value into a \`variable\` (with a SECURE \`default\`, or **no default** so the caller must set it \u2014 NEVER preserve an insecure value as a default), expose the needed \`output\`s, and replace the root resources with a \`module\` block passing the ORIGINAL values. Its \`source\` is relative to the root's directory: \`../../modules/<name>\` from \`env/dev\`, \`./modules/<name>\` only from the repo root. You do the naming + structure; the equivalence check (step 8) is the guardrail that proves you changed nothing.
1796
+ - **b.5 \u2014 consolidation: one module, N environment roots** (a \`${t("terraform_consolidation_candidates")}\` group): the b.3 extraction done once for a shape several roots duplicate. Author \`./modules/<name>/\` from the group's resources, then convert EVERY member root in this same PR; each member's call writes that member's \`call_source\` as its \`source\`. Rules that are not optional: (i) move each resource block **verbatim, keeping its resource name** \u2014 the group's \`expected_moves_by_member\` assumes the leaf name survives, and renaming it to \`this\` invalidates every move; a \`module\` entry in the group's \`resources\` is a module CALL the shape includes \u2014 move that call block too, keeping its name, and write its \`wrapped_calls[].source\` (relative to the NEW module, e.g. \`../s3\`) as its \`source\`, since the root's \`../../modules/s3\` no longer resolves from there; (ii) put each \`module_body_values\` entry in the module exactly as written \u2014 \`kind: "attribute"\` is a right-hand side (\`bucket = <value>\`), \`kind: "block"\` is a nested block's BODY and goes back inside its own braces (\`versioning_configuration { <value> }\`), never as \`versioning_configuration = { \u2026 }\`; (iii) replace ONLY the \`variables\` attributes with \`var.<name>\`, and copy each member's \`per_member.value\` into THAT member's call **verbatim \u2014 never retyped, never reformatted, never swapped between environments**. A \`classification: reference\` variable (identical text like \`var.bucket_name\` in every root) is still per-environment: the text matches, the value does not. Never fold differing values into one interpolated template (\`"acme-\${var.env}-logs"\`) \u2014 the job is to preserve N values, not to invent a rule that reproduces them. Paste each root's \`moved {}\` blocks, then prove ALL the roots with a single \`${t("terraform_equivalence_check")}\`.
1796
1797
  - **b.4 \u2014 third-party module (behaviour-ALTERING)**: adopting a community/registry module changes defaults and often adds resources, so equivalence is FALSE and must **never** be claimed. This is a DIFFERENT proof (a reviewed plan delta, opt-in; \`proposed \u2014 unproven\` by default) \u2014 follow the **b.4 path** at the end of this checklist INSTEAD of steps 5\u201312's equivalence gate. Reserve it for a genuine module adoption; a plain relocation of your own resources is b.1/b.2/b.3.
1797
1798
 
1798
1799
  5. **wire the module call**: get the chosen module's REAL interface so the \`module\` block passes its actual \`variable\` names (a missing required variable is a PR question for the reviewer, never a guessed value). For a LOCAL module dir (b.2/b.3) call \`${t("terraform_module_interface")}\`; for a REGISTRY-sourced module \u2014 a \`module_catalogue\` entry like \`terraform-aws-modules/vpc/aws\`, or the b.4 adoption \u2014 call \`${t("terraform_module_lookup")}\` to read its published \`required_inputs\`/\`outputs\` + resolved \`version\` from the registry. \`${t("terraform_module_graph")}\` shows existing local module dirs + callers. Apply the same secure-default discipline as Remediate: a parameterised value's default must be the secure choice or absent \u2014 never a hidden-insecure default.
@@ -1804,7 +1805,7 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
1804
1805
  8. **PROVE equivalence (the gate)**: call \`${t("terraform_equivalence_check")}\`. It compares the working tree against the run-start commit and must return \`equivalent: true\` \u2014 meaning: same resource multiset (\`resource_set_diff\` empty), identical per-resource argument names, **zero \`uncovered_moves\`**, and validate + fmt clean. If \`equivalent\` is false:
1805
1806
  - any \`uncovered_moves\` \u2192 add the missing \`moved {}\` block(s) for those addresses and re-run;
1806
1807
  - a non-empty \`resource_set_diff.added\`/\`removed\` or \`arg_diffs\` \u2192 the refactor changed behaviour; fix it back to a true no-op, or ABANDON the cluster (it may be a behaviour-altering adoption \u2014 see step 4).
1807
- - a non-empty \`value_mismatches\` (\`values_preserved: false\`) \u2192 a relocated argument's VALUE changed: the check resolved \`var.<x>\` back through the call site and it no longer yields what that root had. This is the b.5 failure nothing else can see \u2014 the resource set, the argument names, the moves, validate and even a plan are all clean, and the apply applies another environment's configuration. Each row names the root, the argument, the \`before\` value and what it now resolves to: put that root's OWN value back at its OWN call site, copied verbatim from the before tree.
1808
+ - a non-empty \`value_mismatches\` (\`values_preserved: false\`) \u2192 a VALUE changed: the check follows each value \u2014 \`var.<x>\` back through the call site, locals, module outputs, data sources, nested and \`dynamic\` blocks \u2014 and it no longer yields what the before tree had. This is the b.5 failure nothing else can see \u2014 the resource set, the argument names, the moves, validate and even a plan are all clean, and the apply applies another environment's configuration. Each row names the root, the resource, the argument path, the \`before\` value and what it now resolves to: put that root's OWN value back at its OWN call site, copied verbatim from the before tree. A row on a resource you did not move means a shared module's body or an untouched call site changed under it \u2014 undo that edit.
1808
1809
  - a \`verdict: "unverifiable"\` naming a repetition that moved between levels \u2192 the per-key \`moved {}\` blocks are the right shape and the proof will not evaluate the \`for_each\` expression to confirm the key set is complete. This is the ONE case a plan settles: run \`${t("terraform_plan")}\` and reach \`refactor_safe: true\` on the tree you are pushing, and the push is allowed with the verdict left at \`unverifiable\` (never restated as proven). Without cloud credentials, escalate the cluster rather than pushing.
1809
1810
  Do not proceed to push until \`equivalent: true\`, or until that one \`unverifiable\` is plan-corroborated. **Optional stronger proof:** when cloud credentials are present, call \`${t("terraform_plan")}\` \u2014 a \`refactor_safe: true\` (pure-move) plan corroborates the keys-free proof; attach the plan to the PR. Then **re-run \`${t("terraform_equivalence_check")}\`** so the plan's own reading of what moved folds into the recorded proof (\`plan_corroborated: true\`). Make no edits between the two calls \u2014 the pairing is tied to the exact tree the plan saw and is discarded if the tree changed.
1810
1811
 
@@ -1814,7 +1815,7 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
1814
1815
 
1815
1816
  11. **branch + commit + push**: create \`infraweaver/refactor-<slug>\` from the **current HEAD** (the scanned checkout) via \`${t("git")}\` \u2014 do NOT switch base first. \`git add\` only the files you changed, commit with a message naming the module + cluster (e.g. \`refactor(tf): extract S3 logging resources into module.logging\`), then \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*). The push guardrails enforce Terraform-only paths, no inlined secrets, AND the equivalence hard-fail \u2014 which refuses an unproven refactor AND one that recorded no proof at all, so skipping step 8 blocks the push rather than slipping past it.
1816
1817
 
1817
- 12. **open the PR (MANDATORY body)**: \`${t("create_pull_request")}\` (omit \`base\` \u2014 it resolves to the run's base branch). Build the body with the **Refactor PR format** at the end of this checklist \u2014 status banner \u2192 title \u2192 \`## What changed\` (include the adopted candidate's \`selection_reason\` VERBATIM; if you omit it, \`${t("create_pull_request")}\` appends it under \`### Module selection\`) \u2192 \`## Equivalence proof\` (built ONLY from \`${t("terraform_equivalence_check")}\`, the proof not a self-report) \u2192 composition note. Optionally call \`${t("terraform_emit_evidence")}\` \u2014 it records the equivalence statement with the proof behind it; when its result says \`signed: true\`, add a one-line evidence note to the PR body naming the artefact and the signing key fingerprint (\`key_fingerprint_sha256\`) so a reviewer can verify with \`infraweaver verify-evidence\`. Never auto-merge.
1818
+ 12. **open the PR (MANDATORY body)**: \`${t("create_pull_request")}\` (omit \`base\` \u2014 it resolves to the run's base branch). Build the body with the **Refactor PR format** at the end of this checklist \u2014 status banner \u2192 title \u2192 \`## What changed\` (include the adopted candidate's \`selection_reason\` VERBATIM; if you omit it, \`${t("create_pull_request")}\` appends it under \`### Module selection\`) \u2192 \`## Equivalence proof\` (built ONLY from \`${t("terraform_equivalence_check")}\`, the proof not a self-report) \u2192 composition note. Optionally call \`${t("terraform_emit_evidence")}\` \u2014 it records the equivalence statement with the proof behind it; when its result says \`signed: true\`, add a one-line evidence note to the PR body naming the artefact and the signing key fingerprint (\`key_fingerprint_sha256\`) so a reviewer can verify with \`infraweaver verify-evidence\`. Never auto-merge. If \`${t("create_pull_request")}\` refuses because an open PR already changes the same files, that PR is this refactor waiting for review: delete the branch you pushed and report it \u2014 never re-open under another branch name.
1818
1819
 
1819
1820
  13. **finalize**: call \`${t("report_progress")}\` once with a summary \u2014 which cluster was modularised, into which module, the PR link, and the equivalence verdict (or the exact tool error if a step failed).
1820
1821
 
@@ -1823,11 +1824,11 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
1823
1824
  Use this ONLY when the chosen source is a community/registry module that changes behaviour \u2014 equivalence is false here and must NEVER be claimed. The proof is a reviewed plan delta, gated behind opt-in read creds; by default the change ships labelled \`proposed \u2014 unproven\` and asserts nothing about runtime.
1824
1825
 
1825
1826
  - **Find + read the module**: when the operator's \`module_catalogue\` / \`refactor_source\` doesn't already name the module, call \`${t("terraform_module_search")}\` to find a published one \u2014 pass your org's \`namespace\` to prefer your OWN vetted modules over a stranger's. Search covers the PUBLIC registry only: a private-registry module must be named in \`module_catalogue\` / \`refactor_source\`, or looked up directly by its \`host/namespace/name/provider\` address. Then call \`${t("terraform_module_lookup")}\` on the chosen \`namespace/name/provider\` to read its \`required_inputs\` / \`outputs\` + latest \`version\`, so the \`module\` block wires the real interface and pins an exact version.
1826
- - **Scan with external-module resolution ON**: call \`${t("terraform_scan")}\` with \`download_external_modules: true\` (and verify with \`${t("terraform_verify_remediation")}\`'s \`download_external_modules: true\`) so the adopted module's INTERNALS stay in scope. Skipping it would let the module's findings vanish unfixed \u2014 a false \u2717\u2192\u2713 ("finding-laundering"). Non-negotiable.
1827
+ - **Scan with external-module resolution ON**: call \`${t("terraform_scan")}\` with \`download_external_modules: true\` (and verify with \`${t("terraform_verify_remediation")}\`'s \`download_external_modules: true\`) so the adopted module's INTERNALS stay in scope. Skipping it would let the module's findings vanish unfixed \u2014 a false \u2717\u2192\u2713 ("finding-laundering"). Non-negotiable. A finding inside the module is reported on the line of the \`module\` block that calls it, with the module resource it is about named in the evidence ("\u2014 inside the module: module.data.aws_s3_bucket.this (main.tf:25)").
1827
1828
  - **Remediate module-sourced findings via INPUTS**: set the hardening variable at the call site \u2014 never edit vendored module code. Where the module exposes no knob for a control, label it \`needs-fork\` and surface it; never a silent pass.
1828
1829
  - **Pin the exact version**: a registry \`version\` or a git \`?ref=<sha>\` (provenance discipline \u2014 the analog to a SHA-pinned action); it is recorded in the evidence bundle.
1829
1830
  - **Proof**:
1830
- - **Default (no read creds)**: do NOT run a plan. Call \`${t("terraform_emit_evidence")}\` with \`proposed_unproven: true\` **before you push** \u2014 the bundle records that the change asserts nothing about runtime, and \`${t("push_branch")}\` refuses a refactor that reaches a reviewer with neither a proof nor that marker. Then add the \`proposed-unproven\` label (\`${t("add_labels")}\`) and put the proposed-unproven banner unmissably at the top of the body. It must NEVER read as a proven / equivalence change.
1831
+ - **Default (no read creds)**: do NOT run a plan. Call \`${t("terraform_emit_evidence")}\` with \`proposed_unproven: true\` **before you push** \u2014 the bundle records that the change asserts nothing about runtime (commit the file it writes with the change, unedited: the push admits exactly those bytes), and \`${t("push_branch")}\` refuses a refactor that reaches a reviewer with neither a proof nor that marker. Then add the \`proposed-unproven\` label (\`${t("add_labels")}\`) and put the proposed-unproven banner unmissably at the top of the body. It must NEVER read as a proven / equivalence change.
1831
1832
  - **Opt-in (read-only backend/state supplied)**: call \`${t("terraform_plan")}\` **before you push**, then record the outcome with \`${t("terraform_emit_evidence")}\` passing \`reviewed_plan: true\` (refused if no plan ran) \u2014 that marker is what the push gate accepts for a behaviour-altering adoption; a plan alone no longer discharges it. Emit the **reviewed plan delta** in the PR body as the proof (replacing the proposed-unproven section). No supplied credential is stored, and no plan attribute VALUES are kept \u2014 only the aggregate totals and affected addresses the push gates read.
1832
1833
  - Same guardrails: one cluster per PR, never auto-merge, Terraform-only, never write-PR a read-only module repo.
1833
1834
 
@@ -1910,7 +1911,7 @@ function remediateMode(t) {
1910
1911
  **Bulk remediation (\`fix rule <rule-id>\` / \`fix all rule <rule-id>\`)**: a request to fix ONE scanner rule everywhere it fires (e.g. \`${COMMENT_COMMAND} fix rule CKV_AWS_23\` \u2014 "add a description to every security group", or \`fix rule terraform_required_version\`). Re-scan with \`group_by: "rule"\` so that rule becomes ONE group spanning every file, then act on the single group whose \`rule_ids\` include \`<rule-id>\` \u2014 apply the SAME minimal fix at every site in its \`files\` and open ONE coherent PR (not one per file). This is the sweep path; still honour \`max_prs\` and never batch a \`needs-human\` group. Cite each fixed site in the PR body.
1911
1912
 
1912
1913
  4. **for the chosen group**:
1913
- - **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.
1914
+ - **base branch**: this run's base branch is resolved deterministically \u2014 \`${t("create_pull_request")}\` targets the \`base_branch\` input if set, else the branch the run started on, 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.
1914
1915
  - **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.
1915
1916
  - **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.
1916
1917
  - **honest refusal (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.
@@ -2237,9 +2238,10 @@ This mode reviews the **Terraform** in a human PR and submits ONE review. It is
2237
2238
 
2238
2239
  4. **introduced findings \u2014 comment or dismiss each one**: for EVERY introduced finding the delta lists (the most severe first; past its limit it only counts the rest, which need nothing from you), read the cited code and decide:
2239
2240
  - **comment**: an inline comment on the changed line, with \`finding_id\` set to that finding's id and an \`audit\` filled from your verification pass. Explain the concrete consequence in this configuration, not the rule's generic text. The tool adds the scanner, rule and mapped controls itself \u2014 do not restate them.
2240
- - **dismiss**: list it in \`dismissed\` with the specific reason it needs no comment here \u2014 a named sibling resource or module default already satisfies the control, the resource is not instantiated (\`count = 0\`), or the rule does not fit this configuration for a stated reason. "Not important" is not a reason. A finding marked \`possibly_preexisting\` is usually a rename, or a move into another module, without a \`moved\` block: say that (and whether a \`moved\` block is needed) rather than treating the misconfiguration as new.
2241
+ - **dismiss**: list it in \`dismissed\` with the specific reason it needs no comment here \u2014 a named sibling resource or module default already satisfies the control, the resource is not instantiated (\`count = 0\`), or the rule does not fit this configuration for a stated reason. "Not important" is not a reason. A finding marked \`possibly_preexisting\` is usually a rename, or a move into another module, without a \`moved\` block (\`renamed_from\` names the resource it pairs with): the review's verdict already names each such resource once, so make ONE comment per renamed resource giving the \`moved\` block it needs, and dismiss its other findings as part of that rename rather than treating each misconfiguration as new.
2241
2242
  **One comment per root cause.** When several introduced findings are the same problem on the same lines (two scanners flagging one open ingress rule), comment once with the most specific \`finding_id\` and dismiss the others as duplicates of that comment.
2242
2243
  The review tool refuses once if any introduced finding is neither commented nor dismissed; on the retry it names the rest to the author as unaddressed.
2244
+ **A comment limit is not yours to apply.** The repo may set \`review_comment_limit\`; the tool enforces it after you submit, keeping the most severe comments inline and listing the rest in the review body. Comment on or dismiss every introduced finding exactly as you would without one \u2014 never leave a finding unaddressed, or fold it into prose, because of a limit.
2243
2245
 
2244
2246
  5. **suggestions**: offer a \`suggestion\` only on a scanner-backed comment whose fix fits inside that comment's own line range [start_line, line], preserving exact indentation. The tool applies it to a scratch copy, re-runs fmt, validate and every scanner, and makes it committable only if the finding is gone and nothing new appeared ("Proven by re-scan"); otherwise it is shown as plain code marked "Unproven". A fix that needs another resource, a new variable or lines outside the range is described in prose instead \u2014 it cannot be proven from a line range. A judgement's suggestion is always shown as unproven code.
2245
2247
 
@@ -2262,9 +2264,11 @@ This mode reviews the **Terraform** in a human PR and submits ONE review. It is
2262
2264
 
2263
2265
  When the finding under test is a **cloud-misconfig** (public exposure, missing encryption, over-broad IAM, open ingress), the verification dispatch MUST include the Terraform-security refute lens above in addition to the generic charge \u2014 these IaC false-positive patterns (a separate hardening resource, a disabled resource, a call-site override) are exactly what a generic code lens misses. A scanner-backed finding the refute lens knocks down is DISMISSED with that reason, not silently dropped.
2264
2266
 
2265
- Every \u{1F6A8}/\u26A0\uFE0F comment \u2014 and every scanner-backed comment \u2014 carries an \`audit\`; one without is withheld. Then ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\` (never \`report_progress\` for review output), with \`comments\` (anchored to NEW line numbers) and \`dismissed\`. Non-anchorable concerns go in the body \`### \` sections. The tool opens the body with the verdict (merge-blocking or not, and the author's next steps) and closes it with the scan delta \u2014 counts, gate, controls, dismissals and suggestion proofs \u2014 so do not restate those yourself.
2267
+ A \`betterleaks:\` finding is a secret the PR's commits add, in any file, Terraform or not: answer it like any scanner finding, and never quote, decode or guess its value \u2014 say which file and line holds it and that it must be removed from the history and rotated.
2266
2268
 
2267
- Same opening-callout ladder + \`approved\` lever as Review \u2014 \`[!CAUTION]\` (a state-destroying or security-breaking change) \u2192 \`[!IMPORTANT]\` (a real misconfig to fix before merge; at least this tier whenever an introduced high/critical finding stands after verification) \u2192 \`> \u2139\uFE0F ...\` (minor HCL/style nits) \u2192 \`> \u2705 No new issues found.\` (mergeable, \`approved: true\`, no inline comments). Pick the tier the author's actual next action justifies. note: the first \`create_pull_request_review\` submission may error with a one-time diff-coverage, audit or scan-delta nudge \u2014 fix what it names and retry.
2269
+ Every \u{1F6A8}/\u26A0\uFE0F comment \u2014 and every scanner-backed comment \u2014 carries an \`audit\`; one without is withheld. Then ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\` (never \`report_progress\` for review output), with \`comments\` (anchored to NEW line numbers) and \`dismissed\`. Non-anchorable concerns go in the body \`### \` sections. With more than about 10 comments and dismissals in all, stage them first with \`${t("stage_review")}\` in calls of up to 10 \u2014 one dismissal entry can carry several \`finding_ids\` that share a reason \u2014 and then submit \`${t("create_pull_request_review")}\` with the body alone: one oversized call can be cut off mid-way and cost the whole review. The tool opens the body with the verdict (merge-blocking or not, and the author's next steps) and closes it with the scan delta \u2014 counts, gate, controls, dismissals and suggestion proofs \u2014 so do not restate those yourself.
2270
+
2271
+ Same opening-callout ladder + \`approved\` lever as Review \u2014 \`[!CAUTION]\` (a state-destroying or security-breaking change) \u2192 \`[!IMPORTANT]\` (a real misconfig to fix before merge; at least this tier whenever an introduced high/critical finding stands after verification) \u2192 \`> \u2139\uFE0F ...\` (minor HCL/style nits) \u2192 \`> \u2705 No new issues found.\` (mergeable, \`approved: true\`, no inline comments). Pick the tier the author's actual next action justifies. note: the first \`create_pull_request_review\` submission may error with a one-time diff-coverage, audit or scan-delta nudge \u2014 fix what it names (stage what it asks for) and retry.
2268
2272
 
2269
2273
  9. **guardrails**: never modify \`*.tf\`/\`*.tfvars\`, never commit/push, never open a PR or issue. Only Terraform files are in scope. The review is the only deliverable.
2270
2274
 
@@ -2278,20 +2282,20 @@ ${PR_SUMMARY_FORMAT}`
2278
2282
  function updateDependenciesMode(t) {
2279
2283
  return {
2280
2284
  name: "UpdateDependencies",
2281
- description: "Upgrade outdated or unpinned Terraform provider and module versions to current, supported pins, then PROVE the bump is safe \u2014 config-preserving bumps via the equivalence check (same resource set + arguments), behaviour-affecting major bumps via a reviewed plan delta (or shipped `proposed \u2014 unproven`). One scoped PR per provider/module.",
2285
+ description: "Upgrade outdated or unpinned Terraform provider and module versions to current, supported pins, then PROVE the bump is safe \u2014 config-preserving bumps via the equivalence check (same resource set + arguments), behaviour-affecting major bumps via a reviewed plan delta (or shipped proposed \u2014 unproven). One scoped PR per provider/module.",
2282
2286
  prompt: `### Checklist
2283
2287
 
2284
2288
  This mode turns the version-currency ADVISORY into action: it bumps stale/unpinned provider + module versions and PROVES the bump is safe before a human sees it, reusing the Refactor proof machinery. A pin bump that needs no code change is behaviour-preserving (proven by \`${t("terraform_equivalence_check")}\`); a major bump that changes resource shape ALTERS behaviour and is proven by a reviewed plan delta (or shipped \`proposed \u2014 unproven\`) \u2014 never a false equivalence claim.
2285
2289
 
2286
2290
  1. **task list**: create your task list for this run as your first action.
2287
2291
 
2288
- 2. **detect**: call \`${t("terraform_version_currency")}\` \u2014 it returns provider/module pins that are outdated or unpinned, each with the current constraint, the latest version, and how many majors behind it is.
2292
+ 2. **detect**: call \`${t("terraform_version_currency")}\` \u2014 it returns provider/module pins that are outdated or unpinned, each with the current constraint, the latest version, how many majors behind it is, and \`latest_in_major\` \u2014 the newest release in the major it already sits on.
2289
2293
 
2290
- 3. **pick scope \u2014 ONE provider or module per PR**: take the most impactful safe upgrade first (a security-relevant provider bump, or pinning an unpinned pin to an exact constraint). **Targeted directive override:** if YOUR TASK names a specific provider/module, act only on it. Unless the task asks for more, open **at most one PR this run**; list deferred upgrades in the final report (never skip silently).
2294
+ 3. **pick scope \u2014 ONE provider or module per PR**: take the most impactful safe upgrade first (a security-relevant provider bump, or pinning an unpinned pin to an exact constraint). A row with \`latest_in_major\` has an upgrade that crosses no major \u2014 take that one when the major is not allowed (\`allow_major_upgrade\`), rather than reporting that nothing could be done. **Targeted directive override:** if YOUR TASK names a specific provider/module, act only on it. Unless the task asks for more, open **at most one PR this run**; list deferred upgrades in the final report (never skip silently).
2291
2295
 
2292
2296
  4. **classify the bump (this decides the proof)**:
2293
- - **config-preserving** \u2014 a version-constraint change only (a patch/minor bump, or pinning a floating version) with NO change to any resource or argument. This is behaviour-preserving: prove it with \`${t("terraform_equivalence_check")}\` exactly as Refactor does \u2014 it must return \`equivalent: true\` (same resource set, identical arguments, validate + fmt clean). The push hard-fails an unproven one.
2294
- - **behaviour-affecting** \u2014 a major bump that requires resource/argument changes (renamed args, new required blocks, changed defaults). Equivalence is FALSE and must never be claimed. Make the minimal code changes the new major requires \u2014 use \`${t("terraform_validate")}\`'s \`providers\` majors and \`${t("terraform_provider_schema")}\` to get the new argument shape \u2014 then prove via the reviewed-plan path in step 6.
2297
+ - **config-preserving** \u2014 a PROVIDER version-constraint change only (a patch/minor bump, or pinning a floating version) with NO change to any resource or argument. This is behaviour-preserving: prove it with \`${t("terraform_equivalence_check")}\` exactly as Refactor does \u2014 it must return \`equivalent: true\` (same resource set, identical arguments, validate + fmt clean). The push hard-fails an unproven one.
2298
+ - **behaviour-affecting** \u2014 a major bump that requires resource/argument changes (renamed args, new required blocks, changed defaults), AND every registry MODULE version change, patch included: the module's resources are its code, which the bump replaces outside this tree, so \`${t("terraform_equivalence_check")}\` cannot see what changed and will not claim it. Equivalence is FALSE and must never be claimed. Make the minimal code changes the new major requires \u2014 use \`${t("terraform_validate")}\`'s \`providers\` majors and \`${t("terraform_provider_schema")}\` to get the new argument shape \u2014 then prove via the reviewed-plan path in step 6.
2295
2299
 
2296
2300
  4b. **for a provider MAJOR bump, call \`${t("terraform_provider_upgrade")}\` BEFORE editing.** It returns the codified breaking changes for that exact major boundary, each with the file it is in. Three things in its output decide what you do next, and none of them can be inferred from the diff:
2297
2301
  - \`needs_human: true\` \u2014 the transform MOVES STATE (a removed resource type, or a rename the vendor marks manual), forces replacement, or needs the VALUE changed and not just the name. Never apply one autonomously: put it in its own PR, label it \`needs-human\` (\`${t("add_labels")}\`), and name the affected resources in the body. Read \`needs_human_reason\` \u2014 the three causes need different PR bodies.
@@ -2303,7 +2307,7 @@ This mode turns the version-currency ADVISORY into action: it bumps stale/unpinn
2303
2307
 
2304
2308
  6. **prove safety**:
2305
2309
  - config-preserving \u2192 \`${t("terraform_equivalence_check")}\` is the proof (\`equivalent: true\`).
2306
- - behaviour-affecting \u2192 call \`${t("terraform_plan")}\` when cloud credentials are present and attach the **reviewed plan delta** as the proof; treat \`has_destroy_or_replace\`/\`stateful_destructive\` exactly as Remediate (a stateful destroy/replace is hard-blocked at push unless \`allow_replace\` covers that address \u2014 abandon otherwise). With NO creds, do not guess: ship the PR labelled \`proposed \u2014 unproven\` (\`${t("add_labels")}\`) with the proposed-unproven banner, stating the major bump ALTERS behaviour and the plan must be reviewed.
2310
+ - behaviour-affecting \u2192 call \`${t("terraform_plan")}\` when cloud credentials are present and attach the **reviewed plan delta** as the proof; treat \`has_destroy_or_replace\`/\`stateful_destructive\` exactly as Remediate (a stateful destroy/replace is hard-blocked at push unless \`allow_replace\` covers that address \u2014 abandon otherwise). With NO creds, do not guess: ship the PR labelled \`proposed-unproven\` (exactly that name, \`${t("add_labels")}\`) with the proposed-unproven banner, stating the major bump ALTERS behaviour and the plan must be reviewed.
2307
2311
  - record either proof with \`${t("terraform_emit_evidence")}\`.
2308
2312
 
2309
2313
  7. **keep module tests consistent** (only if the bump changed a local module's interface): \`${t("terraform_module_tests")}\` \u2014 update the drifting \`examples/\`/tests, never weaken an assertion.
@@ -2349,6 +2353,84 @@ var NON_COMMITTING_MODES = new Set(
2349
2353
  modes.filter((m) => m.nonCommitting).map((m) => m.name)
2350
2354
  );
2351
2355
 
2356
+ // node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/buffer_utils.js
2357
+ var encoder = new TextEncoder();
2358
+ var decoder = new TextDecoder();
2359
+ var strictDecoder = new TextDecoder("utf-8", { fatal: true });
2360
+ var MAX_INT32 = 2 ** 32;
2361
+ function decodeBase64(encoded, url = false) {
2362
+ if (Uint8Array.fromBase64)
2363
+ return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" });
2364
+ if (url) {
2365
+ if (encoded.includes("+") || encoded.includes("/"))
2366
+ throw new TypeError("Invalid base64url");
2367
+ encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
2368
+ }
2369
+ const binary = atob(encoded), bytes = new Uint8Array(binary.length);
2370
+ for (let i = 0; i < binary.length; i++)
2371
+ bytes[i] = binary.charCodeAt(i);
2372
+ return bytes;
2373
+ }
2374
+
2375
+ // node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/util/errors.js
2376
+ var JOSEError = class extends Error {
2377
+ static code = "ERR_JOSE_GENERIC";
2378
+ code = "ERR_JOSE_GENERIC";
2379
+ constructor(message, options) {
2380
+ super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor);
2381
+ }
2382
+ };
2383
+ var JWTInvalid = class extends JOSEError {
2384
+ static code = "ERR_JWT_INVALID";
2385
+ code = "ERR_JWT_INVALID";
2386
+ };
2387
+
2388
+ // node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/util/base64url.js
2389
+ var invalid = "The input to be decoded is not correctly encoded.";
2390
+ function decode(input) {
2391
+ try {
2392
+ return decodeBase64(typeof input == "string" ? input : decoder.decode(input), true);
2393
+ } catch (cause) {
2394
+ throw new TypeError(invalid, { cause });
2395
+ }
2396
+ }
2397
+
2398
+ // node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/lib/validate.js
2399
+ function isObject(input) {
2400
+ if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]")
2401
+ return false;
2402
+ const prototype = Object.getPrototypeOf(input);
2403
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
2404
+ }
2405
+
2406
+ // node_modules/.pnpm/jose@6.2.12/node_modules/jose/dist/webapi/util/decode_jwt.js
2407
+ function decodeJwt(jwt) {
2408
+ if (typeof jwt != "string")
2409
+ throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string");
2410
+ const { 1: payload, length } = jwt.split(".");
2411
+ if (length === 5)
2412
+ throw new JWTInvalid("Only JWTs using Compact JWS serialization can be decoded");
2413
+ if (length !== 3)
2414
+ throw new JWTInvalid("Invalid JWT");
2415
+ if (!payload)
2416
+ throw new JWTInvalid("JWTs must contain a payload");
2417
+ let decoded;
2418
+ try {
2419
+ decoded = decode(payload);
2420
+ } catch {
2421
+ throw new JWTInvalid("Failed to base64url decode the payload");
2422
+ }
2423
+ let result;
2424
+ try {
2425
+ result = JSON.parse(strictDecoder.decode(decoded));
2426
+ } catch {
2427
+ throw new JWTInvalid("Failed to parse the decoded payload as JSON");
2428
+ }
2429
+ if (!isObject(result))
2430
+ throw new JWTInvalid("Invalid JWT Claims Set");
2431
+ return result;
2432
+ }
2433
+
2352
2434
  // src/utils/agent/codexOAuth.ts
2353
2435
  var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
2354
2436
  var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -2393,16 +2475,14 @@ async function refreshCodexAuthBody(body) {
2393
2475
  };
2394
2476
  }
2395
2477
  function decodeJwtExpMs(token) {
2396
- const parts = token.split(".");
2397
- if (parts.length !== 3) return null;
2398
- let payload;
2478
+ let exp;
2399
2479
  try {
2400
- payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
2480
+ exp = decodeJwt(token).exp;
2401
2481
  } catch {
2402
2482
  return null;
2403
2483
  }
2404
- if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
2405
- return payload.exp * 1e3;
2484
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
2485
+ return exp * 1e3;
2406
2486
  }
2407
2487
  function parseCodexAuthBody(raw) {
2408
2488
  let parsed;
@@ -0,0 +1,41 @@
1
+ import type { ToolState } from "../toolState.js";
2
+ import type { RunContextData } from "../utils/cloud/runContextData.js";
3
+ import type { ResolvedPayload } from "../utils/config/payload.js";
4
+ /**
5
+ * The branch a run's change is proposed into. Its own module because three
6
+ * places need the same answer and must never disagree: the PR tools (where the
7
+ * PR goes), `terraform_change_summary` (what the change is measured against)
8
+ * and the push guardrail that refuses a change an open PR already proposes.
9
+ */
10
+ /**
11
+ * The slice `resolveBaseBranch` reads: the declared base-branch input, the
12
+ * branch the run started on and the repository's default branch.
13
+ */
14
+ export interface BaseBranchContext {
15
+ payload: ResolvedPayload;
16
+ repo: RunContextData["repo"];
17
+ toolState: Pick<ToolState, "initialHead">;
18
+ }
19
+ /**
20
+ * Pure base-branch picker. Precedence: an explicit `base_branch` declaration
21
+ * always wins; then the branch the run started on, so a run dispatched on
22
+ * `develop` proposes into `develop`; then the repository's default branch
23
+ * (GitHub reports it — normally `main`, sometimes `master`); otherwise prefer
24
+ * `main`, then `master`; final fallback `main`. Split out from git/ctx so it's
25
+ * exhaustively testable.
26
+ */
27
+ export declare function pickBaseBranch(opts: {
28
+ declared?: string | undefined;
29
+ startBranch?: string | undefined;
30
+ defaultBranch?: string | undefined;
31
+ mainExists: boolean;
32
+ masterExists: boolean;
33
+ }): string;
34
+ /**
35
+ * Deterministically resolve the branch a PR targets, so the base is never left
36
+ * to the agent's guess: the `base_branch` input, else the branch the run
37
+ * started on, else the repository's default branch (`main`, or `master`). Git
38
+ * is only probed for main/master in the last-resort case where none of those is
39
+ * available (essentially never).
40
+ */
41
+ export declare function resolveBaseBranch(ctx: BaseBranchContext): string;
@@ -1,4 +1,4 @@
1
- import type { BaseBranchContext } from "./pr.js";
1
+ import type { BaseBranchContext } from "./baseBranch.js";
2
2
  /**
3
3
  * AI PR summaries — the deterministic Terraform-change anchor. A PR summary
4
4
  * written purely by the model drifts (miscounts resources, invents changes). This
@@ -89,6 +89,8 @@ export interface CitedCode {
89
89
  }
90
90
  /** Evaluate one audited finding. Pure. */
91
91
  export declare function evaluateFindingAudit(audit: FindingAudit, cited: CitedCode): AuditOutcome;
92
+ /** The severity marker a comment body opens with, if any. */
93
+ export declare function leadingMarker(body: string): string | null;
92
94
  /** Replace the body's leading severity marker with `marker`, or prefix it. */
93
95
  export declare function withMarker(body: string, marker: string): string;
94
96
  interface AuditableComment {
@@ -394,6 +394,17 @@ export declare function resolveMaxPrs(ctx: GuardrailContext): number;
394
394
  * PRs instead of fanning out.
395
395
  */
396
396
  export declare function assertUnderPrCap(ctx: GuardrailContext): void;
397
+ /**
398
+ * Refuse a guarded-mode PR whose head is not a commit this run pushed.
399
+ *
400
+ * Every content guardrail — the path allow-list, the secret scan, the rename
401
+ * and equivalence gates — runs in `push_branch`, so a PR is only as safe as the
402
+ * push behind it. A head this run never pushed went through none of them: a
403
+ * branch another run (or a person) left on the remote, adopted after this
404
+ * run's own push was rejected, opens a PR whose content nothing here checked,
405
+ * under a body describing work it does not contain.
406
+ */
407
+ export declare function assertHeadPushedThisRun(ctx: GuardrailContext, branch: string, headSha: string): void;
397
408
  /** record that a guarded-mode PR was opened (after create_pull_request succeeds). */
398
409
  export declare function recordRemediationPrOpened(ctx: GuardrailContext): void;
399
410
  /**
@@ -406,3 +417,30 @@ export declare function recordRemediationPrOpened(ctx: GuardrailContext): void;
406
417
  * disables that check. Throws with an agent-actionable message, before creation.
407
418
  */
408
419
  export declare function assertUnderOpenPrCaps(ctx: OpenPrCapContext): Promise<void>;
420
+ /** whether a mode names a fresh branch for every change, so that a remote
421
+ * branch of the same name which this run did not push is another change. */
422
+ export declare function isBranchPerChangeMode(mode: string | undefined): boolean;
423
+ /**
424
+ * Refuse to open a PR for a change to files an open PR of this engine already
425
+ * changes against the same base.
426
+ *
427
+ * Checked when the PR is opened, because that is where a duplicate comes into
428
+ * being and where the GitHub API is reachable — the push tools deliberately
429
+ * shell out without it. The pushed branch is left for the agent to delete.
430
+ *
431
+ * Every run of a branch-per-change mode names its own branch, so nothing else
432
+ * notices that a scheduled run is re-proposing what last week's run proposed
433
+ * and nobody has reviewed yet: each run opened another PR over the same code,
434
+ * duplicating the review and conflicting with the first on merge. Overlapping
435
+ * FILES is the test rather than an identical diff: two open PRs editing one
436
+ * file from one base conflict whatever they say, and a run that finds its
437
+ * files taken can still pick work elsewhere.
438
+ *
439
+ * Fails open on an API error — this guards review noise, not infrastructure,
440
+ * and a GitHub hiccup must not throw away a proven change.
441
+ */
442
+ export declare function assertNotAlreadyProposed(ctx: OpenPrCapContext, opts: {
443
+ branch: string;
444
+ base: string;
445
+ changed: readonly string[];
446
+ }): Promise<void>;
@@ -13,7 +13,7 @@ import type { ResolvedPayload } from "../utils/config/payload.js";
13
13
  * needs more (octokit, push, PR state) belongs on `ToolContext`, not here.
14
14
  */
15
15
  export interface LocalToolContext {
16
- payload: Pick<ResolvedPayload, "cwd" | "scanScope" | "iacLanguages" | "severityThreshold" | "grouping" | "autonomyThreshold" | "repeatedLiteralThreshold" | "costIncreaseBlockUsd" | "moduleCatalogue" | "allowReplace" | "allowMajorUpgrade" | "toolsEnabled" | "allowedPaths" | "toolsNetwork" | "gitleaks" | "terratest" | "docs" | "terraformMcp" | "moduleFetchToken" | "moduleFetchSshKey" | "registryToken" | "registryHost" | "opentofuMcp" | "fixMemory">;
16
+ payload: Pick<ResolvedPayload, "cwd" | "scanScope" | "iacLanguages" | "severityThreshold" | "grouping" | "autonomyThreshold" | "repeatedLiteralThreshold" | "varFiles" | "costIncreaseBlockUsd" | "moduleCatalogue" | "allowReplace" | "allowMajorUpgrade" | "toolsEnabled" | "allowedPaths" | "toolsNetwork" | "gitleaks" | "terratest" | "docs" | "terraformMcp" | "moduleFetchToken" | "moduleFetchSshKey" | "registryToken" | "registryHost" | "opentofuMcp" | "fixMemory">;
17
17
  toolState: ToolState;
18
18
  tmpdir: string;
19
19
  /** resolved upstream model specifier driving this run's agent (e.g.
@@ -61,7 +61,12 @@ export interface ExtractionCluster {
61
61
  export declare function clusterResources(file: string, resources: ParsedResource[]): ExtractionCluster[];
62
62
  export interface CandidateModule {
63
63
  name: string;
64
+ /** the module's identity: repo-relative (`./modules/s3`) for a house module. */
64
65
  source: string;
66
+ /** a house module only: the `source` the cluster's root writes to call it —
67
+ * relative to that root, so `../../modules/s3` from `env/dev`. `source` is
68
+ * not a valid module source anywhere but the repo root. */
69
+ call_source?: string;
65
70
  version: string | null;
66
71
  kind: ModuleSourceKind;
67
72
  /** how the match was made: a house module's actual resource types, or a