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
@@ -19,7 +19,7 @@ import {
19
19
  import { analysePlanPairs, type PairAnalysis } from "#app/mcp/terraform/planPairs";
20
20
  import { readWorkingTree, workingTreeFingerprint } from "#app/mcp/terraform/refactor/workspace";
21
21
  import { scanSessionOf } from "#app/mcp/terraform/scanSession";
22
- import { runTf } from "#app/mcp/terraform/subprocess";
22
+ import { runTfAsync } from "#app/mcp/terraform/subprocess";
23
23
  import { skipResult } from "#app/mcp/terraform/types";
24
24
  import { log } from "#app/utils/cli";
25
25
  import { resolveModuleCredentialEnv } from "#app/utils/setup/registryAuth";
@@ -66,13 +66,13 @@ interface RootPlanOutcome {
66
66
  * throws on that deliberately) yields no pairs, and the proof degrades to the
67
67
  * keys-free floor it always had.
68
68
  */
69
- function capturePlanPairs(
69
+ async function capturePlanPairs(
70
70
  absDir: string,
71
71
  planFile: string,
72
72
  creds: Record<string, string>,
73
- ): PairAnalysis | undefined {
73
+ ): Promise<PairAnalysis | undefined> {
74
74
  try {
75
- const shown = runTf(["show", "-json", planFile], absDir, creds);
75
+ const shown = await runTfAsync(["show", "-json", planFile], absDir, creds);
76
76
  if (shown.status !== 0 || !shown.stdout.trim()) return undefined;
77
77
  return analysePlanPairs(parseTerraformPlanFile(shown.stdout).resourceChanges);
78
78
  } catch {
@@ -85,14 +85,14 @@ function capturePlanPairs(
85
85
 
86
86
  /** init + plan (+ a stability re-plan + a human-readable plan) in a SINGLE root.
87
87
  * Used by terraform_plan once per discovered root. */
88
- function planOneRoot(
88
+ async function planOneRoot(
89
89
  absDir: string,
90
90
  creds: Record<string, string>,
91
91
  tmpdir?: string,
92
- ): RootPlanOutcome {
92
+ ): Promise<RootPlanOutcome> {
93
93
  // `terraform` when installed, else `tofu` (identical plan CLI, resolved by
94
- // runTf) — an OpenTofu-only runner keeps the plan gate instead of skipping it.
95
- const init = runTf(["init", "-input=false", "-no-color"], absDir, creds);
94
+ // runTfAsync) — an OpenTofu-only runner keeps the plan gate instead of skipping it.
95
+ const init = await runTfAsync(["init", "-input=false", "-no-color"], absDir, creds);
96
96
  if (init.missing) return { ran: false, skipReason: "terraform (or tofu) not installed" };
97
97
  if (init.status !== 0) {
98
98
  return {
@@ -106,7 +106,7 @@ function planOneRoot(
106
106
  // and a stray one left by a crash would land in the customer's diff. Absent a
107
107
  // tmpdir (a caller that did not supply one) the pairing is simply skipped.
108
108
  const planFile = tmpdir ? join(tmpdir, `plan-${process.pid}-${planFileSeq++}.tfplan`) : undefined;
109
- const plan = runTf(
109
+ const plan = await runTfAsync(
110
110
  planFile ? [...PLAN_JSON_ARGS, `-out=${planFile}`] : PLAN_JSON_ARGS,
111
111
  absDir,
112
112
  creds,
@@ -118,7 +118,7 @@ function planOneRoot(
118
118
  skipReason: `terraform plan failed: ${plan.stderr.trim().slice(0, 200) || "unknown error"}`,
119
119
  };
120
120
  }
121
- const pairs = planFile ? capturePlanPairs(absDir, planFile, creds) : undefined;
121
+ const pairs = planFile ? await capturePlanPairs(absDir, planFile, creds) : undefined;
122
122
  const summary = parseTerraformPlanJson(plan.stdout);
123
123
  const hasChanges =
124
124
  summary.add + summary.change + summary.destroy > 0 || summary.changed.length > 0;
@@ -127,7 +127,7 @@ function planOneRoot(
127
127
  let stable = true;
128
128
  let stabilityReason: string | undefined;
129
129
  if (hasChanges) {
130
- const plan2 = runTf(PLAN_JSON_ARGS, absDir, creds);
130
+ const plan2 = await runTfAsync(PLAN_JSON_ARGS, absDir, creds);
131
131
  if (plan2.status === 0) {
132
132
  const s = comparePlanStability(summary, parseTerraformPlanJson(plan2.stdout));
133
133
  stable = s.stable;
@@ -137,7 +137,11 @@ function planOneRoot(
137
137
  // human-readable plan for the PR <details> block (separate non-json run).
138
138
  let planText: string | undefined;
139
139
  if (hasChanges) {
140
- const readable = runTf(["plan", "-input=false", "-no-color", "-lock=false"], absDir, creds);
140
+ const readable = await runTfAsync(
141
+ ["plan", "-input=false", "-no-color", "-lock=false"],
142
+ absDir,
143
+ creds,
144
+ );
141
145
  if (readable.status === 0 && readable.stdout.trim())
142
146
  planText = readable.stdout.trim().slice(0, 12_000);
143
147
  }
@@ -220,7 +224,11 @@ const TerraformPlanParams = type({});
220
224
  export interface PlanRunners {
221
225
  /** `tmpdir` is where the saved plan file goes — outside the repository,
222
226
  * because it holds attribute values. */
223
- planOneRoot: (absDir: string, creds: Record<string, string>, tmpdir?: string) => RootPlanOutcome;
227
+ planOneRoot: (
228
+ absDir: string,
229
+ creds: Record<string, string>,
230
+ tmpdir?: string,
231
+ ) => Promise<RootPlanOutcome>;
224
232
  hasCloudCredentials: () => boolean;
225
233
  }
226
234
 
@@ -250,10 +258,15 @@ export async function runPlan(ctx: LocalToolContext, runners: PlanRunners = real
250
258
  // aggregate. resolveRoots falls back to [cwd] for a single-root repo, so
251
259
  // behaviour there is identical to before.
252
260
  const roots = resolveRoots(cwd);
253
- const perRoot = roots.map((r) => ({
254
- dir: r.relDir || ".",
255
- outcome: runners.planOneRoot(r.absDir, creds, ctx.tmpdir),
256
- }));
261
+ // one root at a time: each init writes its root's `.terraform`, and the
262
+ // providers they download share the same plugin directories.
263
+ const perRoot: { dir: string; outcome: RootPlanOutcome }[] = [];
264
+ for (const r of roots) {
265
+ perRoot.push({
266
+ dir: r.relDir || ".",
267
+ outcome: await runners.planOneRoot(r.absDir, creds, ctx.tmpdir),
268
+ });
269
+ }
257
270
  const ran = perRoot.filter((p) => p.outcome.ran);
258
271
  if (ran.length === 0) {
259
272
  // every root skipped — surface the first reason (e.g. not installed / init failed).
@@ -69,6 +69,9 @@ function describe(f: DeltaFinding) {
69
69
  ? {
70
70
  possibly_preexisting:
71
71
  "this resource appeared as another with the same finding vanished — likely a rename without a `moved` block",
72
+ ...(f.renamedFrom
73
+ ? { renamed_from: { resource: f.renamedFrom.address, file: f.renamedFrom.file } }
74
+ : {}),
72
75
  }
73
76
  : {}),
74
77
  evidence: c.evidence,
@@ -19,7 +19,7 @@ import { EMPTY_NOISE_PROFILE, explainNoiseAdjustment } from "#app/mcp/terraform/
19
19
  import { scoreConcern, topRisks } from "#app/mcp/terraform/risk";
20
20
  import { runScannersCached } from "#app/mcp/terraform/scannerCache";
21
21
  import { changedTerraformFiles } from "#app/mcp/terraform/scanners";
22
- import { scanSessionOf } from "#app/mcp/terraform/scanSession";
22
+ import { recordScanConcerns, scanSessionOf } from "#app/mcp/terraform/scanSession";
23
23
  import { auditNativeSkips } from "#app/mcp/terraform/suppressions";
24
24
  import { type Concern, SEVERITY_RANK, type Severity } from "#app/mcp/terraform/types";
25
25
  import { UNTRUSTED_DATA_NOTICE } from "#app/mcp/untrustedContent";
@@ -38,7 +38,7 @@ const TerraformScanParams = type({
38
38
  "'file' (default) makes one group per file (smaller blast radius per PR). 'rule' groups a single rule's concerns across ALL files into one group — use for sweeping, low-risk rules (e.g. 'add tags everywhere') so they become one PR instead of many.",
39
39
  ),
40
40
  "download_external_modules?": type("boolean").describe(
41
- "default false. When true, checkov resolves and scans INSIDE registry/git modules — REQUIRED for the refactor b.4 path (adopting a third-party module): scanning with it OFF would stop looking inside the adopted module and silently drop its findings (a false ✗→✓ / 'finding-laundering'). Leave off for normal Terraform-only scans (faster, offline).",
41
+ "default false. When true, checkov resolves and scans INSIDE registry/git modules — REQUIRED for the refactor b.4 path (adopting a third-party module): scanning with it OFF would stop looking inside the adopted module and silently drop its findings (a false ✗→✓ / 'finding-laundering'). A finding inside a module is reported on the line of the `module` block that calls it, naming the module resource. Leave off for normal Terraform-only scans (faster, offline).",
42
42
  ),
43
43
  });
44
44
 
@@ -99,6 +99,8 @@ export async function runScan(
99
99
  // the SAME threshold everywhere a scan happens. A verifier running on a
100
100
  // different one would report a finding resolved because the knob moved.
101
101
  repeatedLiteralThreshold: ctx.payload.repeatedLiteralThreshold,
102
+ // and the same variable files, which decide what trivy and checkov see
103
+ varFiles: ctx.payload.varFiles,
102
104
  });
103
105
  if (ctx.payload.toolsNetwork?.unknown.length) {
104
106
  log.warning(
@@ -152,7 +154,7 @@ export async function runScan(
152
154
  const reintroduced = findReintroduced(active, resolved);
153
155
  // the reported (post-filter) set — read at end-of-run by finalizeSuccessRun
154
156
  // to emit the SARIF artifact + findings outputs.
155
- scanSessionOf(ctx.toolState).lastScanConcerns = result.annotated;
157
+ recordScanConcerns(scanSessionOf(ctx.toolState), result.annotated);
156
158
  // G7: the org's fleet-wide dismissal record, seeded at run start. Affects the
157
159
  // ORDER of `top_risks` / `risk_score` only — every concern above is already
158
160
  // final by this point, and nothing here can remove one.
@@ -55,7 +55,7 @@ export function TerraformStandardsReportTool(ctx: LocalToolContext) {
55
55
  note: "no `.tf` files found in the workspace — nothing to report on",
56
56
  });
57
57
  }
58
- const report = standardsReport(cwd);
58
+ const report = await standardsReport(cwd);
59
59
  scanSessionOf(ctx.toolState).standardsReport = report;
60
60
  log.info(
61
61
  `» terraform_standards_report: ${report.summary.violations} violation(s), ` +
@@ -1,10 +1,12 @@
1
1
  import { type } from "arktype";
2
2
  import type { LocalToolContext } from "#app/mcp/localContext";
3
3
  import { execute, tool, toolOk } from "#app/mcp/shared";
4
+ import { initArtefacts, removeNewInitArtefacts } from "#app/mcp/terraform/initArtefacts";
4
5
  import { resolveRoots } from "#app/mcp/terraform/paths";
5
6
  import {
6
7
  checkArgumentsAgainstSchema,
7
8
  collectProviderRequirements,
9
+ initFailures,
8
10
  scanFmt,
9
11
  scanTflint,
10
12
  scanValidate,
@@ -29,6 +31,7 @@ export interface ValidateScanners {
29
31
  scanTflint: typeof scanTflint;
30
32
  collectProviderRequirements: typeof collectProviderRequirements;
31
33
  checkArgumentsAgainstSchema: typeof checkArgumentsAgainstSchema;
34
+ initFailures: typeof initFailures;
32
35
  }
33
36
 
34
37
  const realScanners: ValidateScanners = {
@@ -37,6 +40,7 @@ const realScanners: ValidateScanners = {
37
40
  scanTflint,
38
41
  collectProviderRequirements,
39
42
  checkArgumentsAgainstSchema,
43
+ initFailures,
40
44
  };
41
45
 
42
46
  export async function runValidate(
@@ -53,14 +57,15 @@ export async function runValidate(
53
57
  });
54
58
  // `terraform validate` runs per-root (multi-root aware); fmt + tflint are
55
59
  // recursive over the whole tree.
56
- const fmtOutcome = scanners.scanFmt(cwd);
57
- const validateOutcome = scanners.scanValidate(cwd, terraformEnv);
60
+ const artefactsBefore = initArtefacts(cwd);
61
+ const fmtOutcome = await scanners.scanFmt(cwd);
62
+ const validateOutcome = await scanners.scanValidate(cwd, terraformEnv);
58
63
  // the SAME egress directive as terraform_scan: without it, a validate call
59
64
  // would spawn tflint (and its network-fetching `--init`, memoised per dir)
60
65
  // with open egress under `tools_network: deny` — and whichever call inits
61
66
  // first wins for the rest of the process.
62
67
  const tflintOutcome = selection.enabled("tflint")
63
- ? scanners.scanTflint(cwd, ctx.payload.toolsNetwork)
68
+ ? await scanners.scanTflint(cwd, ctx.payload.toolsNetwork)
64
69
  : skipped("tflint", selection.offReason("tflint") ?? "disabled by tools_enabled");
65
70
 
66
71
  // PRE-EXISTING `terraform fmt` drift (captured by the pre-fix scan, before
@@ -89,6 +94,10 @@ export async function runValidate(
89
94
  // those roots are valid, so `passed` must fail closed below rather than
90
95
  // silently treating an un-validated root as passing.
91
96
  const unvalidatedRoots = validateOutcome.unvalidated ?? 0;
97
+ // roots whose init failed validated without their modules and providers, so
98
+ // a clean list there checked almost nothing — a module bump the provider pin
99
+ // cannot satisfy reads exactly like a pass unless this is said
100
+ const initFailed = scanners.initFailures(cwd);
92
101
  // surface the pinned provider majors so the fix targets the right
93
102
  // argument schema (deterministic, read straight from required_providers).
94
103
  const providers = scanners.collectProviderRequirements(cwd);
@@ -100,6 +109,8 @@ export async function runValidate(
100
109
  // Deterministic and degrades green (omitted) when the schema isn't
101
110
  // available (terraform not installed / dir not init-ed).
102
111
  const schemaCheck = scanners.checkArgumentsAgainstSchema(cwd);
112
+ // the schema was read from what init installed; left behind, it blocks the push
113
+ removeNewInitArtefacts(cwd, artefactsBefore, "terraform_validate");
103
114
  return toolOk({
104
115
  // `passed` stays gated on fmt + validate + tflint only — those are
105
116
  // authoritative (fmt counts only NEW drift; see preexisting_fmt_drift).
@@ -107,13 +118,17 @@ export async function runValidate(
107
118
  // parse), surfaced separately so a parser edge case can never wrongly
108
119
  // block a valid fix. Fails closed when a root ran but couldn't be
109
120
  // validated: an un-validated root is "unknown", not "clean".
110
- passed: remaining.length === 0 && unvalidatedRoots === 0,
121
+ passed: remaining.length === 0 && unvalidatedRoots === 0 && initFailed.length === 0,
111
122
  checks_ran: ran,
112
123
  // true when ≥1 Terraform root could not be validated (terraform ran but
113
124
  // its `-json` output was unparseable). When set, `passed` is false even
114
125
  // with no remaining_issues — re-run after fixing the root's init/state,
115
126
  // or inspect it by hand; do NOT treat the clean issue list as a pass.
116
- validate_incomplete: unvalidatedRoots > 0,
127
+ validate_incomplete: unvalidatedRoots > 0 || initFailed.length > 0,
128
+ // the roots whose `terraform init` failed, with init's error — fix what it
129
+ // names (often a version constraint the change made unsatisfiable) before
130
+ // anything else; their validate result means nothing until then.
131
+ init_failed: initFailed,
117
132
  remaining_issues: remaining,
118
133
  // ADVISORY (does not gate `passed`): files that already failed
119
134
  // `terraform fmt` at the pre-fix baseline. Reformatting them would pull
@@ -55,6 +55,8 @@ export async function runVerifyRemediation(
55
55
  // the SAME threshold everywhere a scan happens. A verifier running on a
56
56
  // different one would report a finding resolved because the knob moved.
57
57
  repeatedLiteralThreshold: ctx.payload.repeatedLiteralThreshold,
58
+ // and the same variable files, which decide what trivy and checkov see
59
+ varFiles: ctx.payload.varFiles,
58
60
  });
59
61
  const currentConcerns = dedupe(outcomes.flatMap((o) => o.concerns));
60
62
  const currentIds = currentConcerns.map((c) => c.id);
@@ -63,12 +65,16 @@ export async function runVerifyRemediation(
63
65
  // concern look resolved nor a pre-existing one look like a regression.
64
66
  const currentKeys = new Set(currentConcerns.map((c) => concernKeyOf(c)));
65
67
 
66
- // Map each requested id → its key via the original scan's concerns
67
- // (lastScanConcerns). Ids we can't key-map (verify called without a prior
68
- // scan this run — e.g. a bare findings.json) fall back to exact-id matching.
69
- const keyById = new Map(
70
- (session.lastScanConcerns ?? []).map((c) => [c.id, concernKeyOf(c)] as const),
71
- );
68
+ // Map each requested id → its key via EVERY scan this run, not only the
69
+ // latest: an agent that re-scans after its edit still verifies the ids it
70
+ // took from the first scan, and those are line-pinned. Ids no scan this run
71
+ // reported (verify called without one — e.g. a bare findings.json) fall back
72
+ // to exact-id matching.
73
+ const reported = new Map([
74
+ ...(session.concernsById ?? []),
75
+ ...(session.lastScanConcerns ?? []).map((c) => [c.id, c] as const),
76
+ ]);
77
+ const keyById = new Map([...reported].map(([id, c]) => [id, concernKeyOf(c)] as const));
72
78
  const keyed: { id: string; key: string }[] = [];
73
79
  const unkeyed: string[] = [];
74
80
  for (const id of concern_ids) {
@@ -116,7 +122,8 @@ export async function runVerifyRemediation(
116
122
  resolved.length > 0
117
123
  ) {
118
124
  const resolvedSet = new Set(resolved);
119
- const captures = (session.lastScanConcerns ?? [])
125
+ // from every scan: after a re-scan the resolved ids are the FIRST scan's
126
+ const captures = [...reported.values()]
120
127
  .filter((c) => resolvedSet.has(c.id))
121
128
  .map(captureProvenFix);
122
129
  if (captures.length > 0) {
@@ -15,7 +15,9 @@ export function TerraformVersionCurrencyTool(ctx: LocalToolContext) {
15
15
  "Check the workspace's pinned providers and registry modules against the Terraform Registry's " +
16
16
  "published versions — the upgrade intelligence no scanner provides (tflint checks pins EXIST, not " +
17
17
  "that they're current). Reports per provider/module the written constraint, the `latest` stable " +
18
- "version, the newest version the constraint admits, and `outdated`/`majors_behind`; registry " +
18
+ "version, the newest version the constraint admits, `latest_in_major` (the newest release in the " +
19
+ "major the constraint already sits on — the bump that crosses no major, offered even when `latest` " +
20
+ "is majors ahead), and `outdated`/`majors_behind`; registry " +
19
21
  "modules with no version pin are flagged `unpinned` (pin them to `latest`). Remediation contract: " +
20
22
  "one `chore(deps)` PR per upgrade group; minor/patch bumps may proceed autonomously; a MAJOR " +
21
23
  "bump means the module/provider interface may have changed — apply it only with a `needs-human` " +