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
@@ -5,20 +5,20 @@ export function updateDependenciesMode(t: ToolRef): Mode {
5
5
  return {
6
6
  name: "UpdateDependencies",
7
7
  description:
8
- "Upgrade outdated or unpinned Terraform provider and module versions to current, supported pins, then PROVE the bump is safe — config-preserving bumps via the equivalence check (same resource set + arguments), behaviour-affecting major bumps via a reviewed plan delta (or shipped `proposed — unproven`). One scoped PR per provider/module.",
8
+ "Upgrade outdated or unpinned Terraform provider and module versions to current, supported pins, then PROVE the bump is safe — config-preserving bumps via the equivalence check (same resource set + arguments), behaviour-affecting major bumps via a reviewed plan delta (or shipped proposed — unproven). One scoped PR per provider/module.",
9
9
  prompt: `### Checklist
10
10
 
11
11
  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 — unproven\`) — never a false equivalence claim.
12
12
 
13
13
  1. **task list**: create your task list for this run as your first action.
14
14
 
15
- 2. **detect**: call \`${t("terraform_version_currency")}\` — 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.
15
+ 2. **detect**: call \`${t("terraform_version_currency")}\` — 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\` — the newest release in the major it already sits on.
16
16
 
17
- 3. **pick scope — 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).
17
+ 3. **pick scope — 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 — 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).
18
18
 
19
19
  4. **classify the bump (this decides the proof)**:
20
- - **config-preserving** — 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 — it must return \`equivalent: true\` (same resource set, identical arguments, validate + fmt clean). The push hard-fails an unproven one.
21
- - **behaviour-affecting** — 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 — use \`${t("terraform_validate")}\`'s \`providers\` majors and \`${t("terraform_provider_schema")}\` to get the new argument shape — then prove via the reviewed-plan path in step 6.
20
+ - **config-preserving** — 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 — it must return \`equivalent: true\` (same resource set, identical arguments, validate + fmt clean). The push hard-fails an unproven one.
21
+ - **behaviour-affecting** — 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 — use \`${t("terraform_validate")}\`'s \`providers\` majors and \`${t("terraform_provider_schema")}\` to get the new argument shape — then prove via the reviewed-plan path in step 6.
22
22
 
23
23
  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:
24
24
  - \`needs_human: true\` — 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\` — the three causes need different PR bodies.
@@ -30,7 +30,7 @@ This mode turns the version-currency ADVISORY into action: it bumps stale/unpinn
30
30
 
31
31
  6. **prove safety**:
32
32
  - config-preserving → \`${t("terraform_equivalence_check")}\` is the proof (\`equivalent: true\`).
33
- - behaviour-affecting → 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 — abandon otherwise). With NO creds, do not guess: ship the PR labelled \`proposed — unproven\` (\`${t("add_labels")}\`) with the proposed-unproven banner, stating the major bump ALTERS behaviour and the plan must be reviewed.
33
+ - behaviour-affecting → 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 — 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.
34
34
  - record either proof with \`${t("terraform_emit_evidence")}\`.
35
35
 
36
36
  7. **keep module tests consistent** (only if the bump changed a local module's interface): \`${t("terraform_module_tests")}\` — update the drifting \`examples/\`/tests, never weaken an assertion.
@@ -7,10 +7,11 @@
7
7
  * - its marker says a re-scan PROVED the fix — never an unproven or a
8
8
  * judgement suggestion, which the review showed as plain code on purpose,
9
9
  * - it was proved against the PR's current head, so the lines it replaces are
10
- * exactly the lines the proof saw — a push since then makes it stale,
10
+ * exactly the lines the proof saw — a push since then makes it stale, until
11
+ * the next review re-proves it there and replies with the lines it used,
11
12
  * - its thread is still open: a resolved thread's finding is already handled.
12
13
  */
13
- import { isOwnReviewComment, parseCommentMarker } from "#app/mcp/reviewMarkers";
14
+ import { isOwnReviewComment, parseCommentMarker, suggestionText } from "#app/mcp/reviewMarkers";
14
15
 
15
16
  export interface ReviewCommentLike {
16
17
  id: number;
@@ -39,14 +40,6 @@ export interface SkippedSuggestion {
39
40
  reason: string;
40
41
  }
41
42
 
42
- const SUGGESTION = /```suggestion\r?\n([\s\S]*?)```/;
43
-
44
- /** The text of a comment's `suggestion` block, without its closing newline. */
45
- export function suggestionText(body: string): string | undefined {
46
- const m = SUGGESTION.exec(body);
47
- return m ? (m[1] ?? "").replace(/\r?\n$/, "") : undefined;
48
- }
49
-
50
43
  export function planApply(input: {
51
44
  comments: readonly ReviewCommentLike[];
52
45
  /** root comment id → whether its thread is still open */
@@ -66,12 +59,21 @@ export function planApply(input: {
66
59
  skip("the comment carries no suggestion block");
67
60
  continue;
68
61
  }
62
+ // proved on this head, at the comment's own lines — or proved on an earlier
63
+ // one and re-proved here by the engine, at the lines the re-proof names
64
+ let line = c.original_line;
65
+ let startLine = c.original_start_line ?? line;
69
66
  if (c.original_commit_id !== input.headSha) {
70
- skip("it was proved against an earlier commit; the next review re-proves it");
71
- continue;
67
+ const reproof = reproofAt(input.comments, c.id, input.headSha);
68
+ if (!reproof) {
69
+ skip(
70
+ "it was proved against an earlier commit and does not hold on this one — the review re-proves it on each push while it still fits",
71
+ );
72
+ continue;
73
+ }
74
+ line = reproof.line;
75
+ startLine = reproof.startLine;
72
76
  }
73
- const line = c.original_line;
74
- const startLine = c.original_start_line ?? line;
75
77
  if (c.side === "LEFT" || line === undefined || startLine === undefined || startLine > line) {
76
78
  skip("it does not anchor to a line range of the new code");
77
79
  continue;
@@ -88,6 +90,23 @@ export function planApply(input: {
88
90
  return { edits, skipped };
89
91
  }
90
92
 
93
+ /** The engine's latest re-proof of root `rootId`'s suggestion on `head`, if any. */
94
+ function reproofAt(
95
+ comments: readonly ReviewCommentLike[],
96
+ rootId: number,
97
+ head: string,
98
+ ): { startLine: number; line: number } | null {
99
+ let found: { startLine: number; line: number } | null = null;
100
+ for (const r of comments) {
101
+ if (r.in_reply_to_id !== rootId || !isOwnReviewComment(r.user, r.body)) continue;
102
+ const marker = parseCommentMarker(r.body);
103
+ if (marker?.kind === "reproof" && marker.head === head) {
104
+ found = { startLine: marker.startLine, line: marker.line };
105
+ }
106
+ }
107
+ return found;
108
+ }
109
+
91
110
  /**
92
111
  * Replace each edit's line range, bottom-up so earlier ranges keep their line
93
112
  * numbers. The file's line ending and final newline are kept.
@@ -19,6 +19,7 @@ import {
19
19
  listOpenInfraweaverThreads,
20
20
  RESOLVE_REVIEW_THREAD_MUTATION,
21
21
  } from "#app/mcp/reviewComments";
22
+ import { count } from "#app/mcp/terraform/prose";
22
23
  import {
23
24
  type ApplyEdit,
24
25
  applyEdits,
@@ -156,14 +157,13 @@ async function apply(repo: Repo, comment: PrCommentRunContext): Promise<ApplyOut
156
157
  base_tree: head.tree.sha,
157
158
  tree,
158
159
  });
159
- const count = batch.length;
160
160
  const { data: commit } = await octokit.rest.git.createCommit({
161
161
  owner,
162
162
  repo: name,
163
163
  tree: newTree.sha,
164
164
  parents: [headSha],
165
165
  message:
166
- `Apply ${count} proven review suggestion${count === 1 ? "" : "s"}\n\n` +
166
+ `Apply ${count(batch.length, "proven review suggestion")}\n\n` +
167
167
  `Each was proved by ${PRODUCT_NAME}'s re-scan of ${headSha.slice(0, 7)}; ` +
168
168
  `requested by @${comment.author} in #${comment.number}.`,
169
169
  });
@@ -15,6 +15,7 @@ import { buildRunQuotas, hasGraceReserve } from "#app/agents/quotaReserve";
15
15
  import type { Agent, AgentResult } from "#app/agents/shared";
16
16
  import { reportProgress } from "#app/mcp/comment";
17
17
  import type { ToolContext } from "#app/mcp/server";
18
+ import { count } from "#app/mcp/terraform/prose";
18
19
  import type { ToolState } from "#app/toolState";
19
20
  import { raceAgentRun } from "#app/utils/agent/agentRace";
20
21
  import { INFRAWEAVER_DATA_DIR } from "#app/utils/agent/codexHome";
@@ -236,7 +237,7 @@ export async function runAgentWithWatchdogs(params: RunAgentParams): Promise<Age
236
237
  if (!wasTracked) return;
237
238
  const trackedRanges = toolState.diffCoverage?.coveredRanges ?? [];
238
239
  log.debug(
239
- `» diff coverage tracked from tool ${event.toolName} (${trackedRanges.length} merged range${trackedRanges.length === 1 ? "" : "s"})`,
240
+ `» diff coverage tracked from tool ${event.toolName} (${count(trackedRanges.length, "merged range")})`,
240
241
  );
241
242
  },
242
243
  });
package/src/toolState.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  } from "@infraweaver-io/ci/progressComment";
7
7
  import type { AgentUsage } from "@infraweaver-io/models/usage";
8
8
  import { DispatchRecorder } from "#app/agents/dispatchMetrics";
9
+ import type { StagedReview } from "#app/mcp/review";
9
10
  import { createScanSession, type ScanSession } from "#app/mcp/terraform/scanSession";
10
11
  import type { PrepResult } from "#app/prep/types";
11
12
  import type { AgentDiagnostic } from "#app/utils/agent/agentHangReport";
@@ -140,6 +141,10 @@ export interface ToolState {
140
141
  // number of remediation PRs opened this run. set only in Remediate mode by
141
142
  // create_pull_request; read by the max_prs guardrail (mcp/guardrails.ts).
142
143
  remediationPrsOpened?: number;
144
+ // remote branch → the commit push_branch pushed there this run. read by the
145
+ // pushed-head guardrail: a write mode's PR may only present a commit this
146
+ // run pushed, because the push is where every content guardrail runs.
147
+ pushedHeads?: Map<string, string>;
143
148
  // the terraform tool-to-tool session: the baseline/scan/plan/cost/refactor
144
149
  // signals the terraform MCP tools hand to one another and to end-of-run
145
150
  // reporting. Extracted from this god-bag into its own typed, documented
@@ -170,6 +175,8 @@ export interface ToolState {
170
175
  nodeId: string;
171
176
  reviewedSha: string | undefined;
172
177
  };
178
+ // comments and dismissals stage_review holds for the next review submission
179
+ stagedReview?: StagedReview | undefined;
173
180
  // dedupe key: parent review comment_id → most-recent reply written this
174
181
  // session by reply_to_review_comment. used by duplicateReplyDecision to
175
182
  // skip identical-body re-emissions of the same call (PR #610 root cause).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pure-stdlib (fetch + Buffer) Codex OAuth refresh + JWT exp decoding.
2
+ * Codex OAuth refresh (fetch) + JWT exp decoding (jose, decode-only).
3
3
  *
4
4
  * Deliberately free of node:child_process / spawn / mkdtemp: the Cloud's
5
5
  * Next.js server imports `refreshCodexAuthBody` through the
@@ -8,6 +8,8 @@
8
8
  * bundle. `src/utils/agent/codexHome.ts` is the in-runner consumer.
9
9
  */
10
10
 
11
+ import { decodeJwt } from "jose";
12
+
11
13
  export interface CodexAuthBody {
12
14
  auth_mode: "chatgpt";
13
15
  tokens: {
@@ -98,16 +100,14 @@ export async function refreshCodexAuthBody(body: CodexAuthBody): Promise<CodexAu
98
100
  * would just delay the inevitable 401 from OpenAI. No security boundary
99
101
  * at this decode step. */
100
102
  export function decodeJwtExpMs(token: string): number | null {
101
- const parts = token.split(".");
102
- if (parts.length !== 3) return null;
103
- let payload: { exp?: unknown };
103
+ let exp: unknown;
104
104
  try {
105
- payload = JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf8"));
105
+ exp = decodeJwt(token).exp;
106
106
  } catch {
107
107
  return null;
108
108
  }
109
- if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
110
- return payload.exp * 1000;
109
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
110
+ return exp * 1000;
111
111
  }
112
112
 
113
113
  /** parse + validate a Codex auth.json body from its JSON-string form.
@@ -29,6 +29,7 @@
29
29
  // (`selectFallbackModelIfNeeded` + `autoSelectModel`) share one shell-out.
30
30
 
31
31
  import { spawnSync } from "node:child_process";
32
+ import { stripVTControlCharacters } from "node:util";
32
33
  import { getModelEnvVars } from "@infraweaver-io/models/catalog";
33
34
  import { log } from "#app/utils/cli";
34
35
 
@@ -62,11 +63,6 @@ let baseline: Set<string> | undefined;
62
63
  let authorized: Set<string> | undefined;
63
64
  let failure: string | undefined;
64
65
 
65
- // the CLI paints its errors; these get re-rendered into a PR comment, where raw
66
- // SGR escapes read as literal `[91m` noise.
67
- // biome-ignore lint/suspicious/noControlCharactersInRegex: matching SGR escapes is the point
68
- const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
69
-
70
66
  function readModels(cliPath: string): Set<string> {
71
67
  // spawnSync, not execFileSync: we want opencode's stderr as a VALUE rather
72
68
  // than a throw, so `getModelsFailure` can hand the real reason to
@@ -84,7 +80,9 @@ function readModels(cliPath: string): Set<string> {
84
80
  // `result.error` carries the only reason there is. without folding it in,
85
81
  // `failure` stays undefined and the empty set reaches `validateAgentApiKey`
86
82
  // as a bare "you have no key" verdict for what is really a runner problem.
87
- const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
83
+ // the CLI paints its errors; this gets re-rendered into a PR comment, where
84
+ // raw escapes read as literal `[91m` noise.
85
+ const stderr = stripVTControlCharacters(result.stderr ?? "").trim();
88
86
  failure = stderr || result.error?.message;
89
87
  log.debug(`» \`opencode models\` failed (${result.status}): ${failure}`);
90
88
  return new Set();
package/src/utils/cli.ts CHANGED
@@ -2,35 +2,5 @@
2
2
  * CLI utilities
3
3
  */
4
4
 
5
- import { spawnSync } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
-
8
5
  // re-export logging utilities for backward compatibility
9
- export {
10
- formatIndentedField,
11
- formatJsonValue,
12
- formatUsageSummary,
13
- log,
14
- writeSummary,
15
- } from "#app/utils/log";
16
-
17
- /**
18
- * Finds a CLI executable path by checking if it's installed globally
19
- * @param name The name of the CLI executable to find
20
- * @returns The path to the CLI executable, or null if not found
21
- */
22
- export function findCliPath(name: string): string | null {
23
- // `which` is POSIX-only. The action always runs on Linux, but this repo is
24
- // developed on Windows, where `which` does not exist and the lookup fails
25
- // silently as "not installed" — use `where` there. `where` can print several
26
- // matches, one per line, so take the first.
27
- const isWindows = process.platform === "win32";
28
- const result = spawnSync(isWindows ? "where" : "which", [name], { encoding: "utf-8" });
29
- if (result.status === 0 && result.stdout) {
30
- const cliPath = result.stdout.split(/\r?\n/)[0]?.trim();
31
- if (cliPath && existsSync(cliPath)) {
32
- return cliPath;
33
- }
34
- }
35
- return null;
36
- }
6
+ export { formatJsonValue, formatUsageSummary, log, writeSummary } from "#app/utils/log";
@@ -210,10 +210,7 @@ export async function fetchRunContext(params: {
210
210
  repoContext: RepoContext;
211
211
  oidcToken?: string | undefined;
212
212
  }): Promise<RunContext> {
213
- const timeoutMs = 30000;
214
- const controller = new AbortController();
215
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
216
-
213
+ // no `signal`: apiFetch applies the cloud transport's default timeout
217
214
  try {
218
215
  const headers: Record<string, string> = {
219
216
  Authorization: `Bearer ${params.token}`,
@@ -225,11 +222,8 @@ export async function fetchRunContext(params: {
225
222
  const response = await apiFetch({
226
223
  path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
227
224
  headers,
228
- signal: controller.signal,
229
225
  });
230
226
 
231
- clearTimeout(timeoutId);
232
-
233
227
  if (!response.ok) {
234
228
  if ((response.status === 401 || response.status === 403) && isBackendConfigured()) {
235
229
  log.warning(cloudAuthHint(response.status, params.oidcToken));
@@ -285,7 +279,6 @@ export async function fetchRunContext(params: {
285
279
  ...(moduleCredentials ? { moduleCredentials } : {}),
286
280
  };
287
281
  } catch (err) {
288
- clearTimeout(timeoutId);
289
282
  return fellBackToDefaults(errorMessageOr(err, "request failed"), true);
290
283
  }
291
284
  }
@@ -80,6 +80,12 @@ export const GATE_RELEVANT_CONFIG_KEYS = [
80
80
  // silence it from the PR that set it, which is the shape this module exists
81
81
  // to refuse. Read from the base ref; a PR cannot retune it for its own run.
82
82
  "repeated_literal_threshold",
83
+ // which variable files the scanners load: a PR that unmaps its own var file
84
+ // would hide what that file sets.
85
+ "var_files",
86
+ // how many review comments post inline: lowering it moves findings out of
87
+ // the diff view and into the body, which a PR should not do for itself.
88
+ "review_comment_limit",
83
89
  ] as const satisfies readonly InfraweaverConfigKey[];
84
90
 
85
91
  /**
@@ -58,6 +58,8 @@ export const INFRAWEAVER_CONFIG_KEYS = [
58
58
  "module_catalogue",
59
59
  "repeated_literal_threshold",
60
60
  "required_controls",
61
+ "var_files",
62
+ "review_comment_limit",
61
63
  ] as const;
62
64
 
63
65
  export type InfraweaverConfigKey = (typeof INFRAWEAVER_CONFIG_KEYS)[number];
@@ -6,6 +6,7 @@ import { type } from "arktype";
6
6
  import { PRODUCT_SLUG } from "#app/brand";
7
7
  import type { AuthorPermission, PayloadEvent } from "#app/external";
8
8
  import { parseToolsNetwork } from "#app/mcp/terraform/scannerEgress";
9
+ import { parseVarFileMappings, type VarFileMapping } from "#app/mcp/terraform/treeLayout";
9
10
  import { log } from "#app/utils/cli";
10
11
  import type { RepoSettings } from "#app/utils/cloud/runContext";
11
12
  import { applyBaseRefConfigPolicy } from "#app/utils/config/baseRefConfig";
@@ -115,6 +116,8 @@ export const Inputs = type({
115
116
  "protected_paths?": type.string.or("undefined"),
116
117
  "autonomy_threshold?": type.string.or("undefined"),
117
118
  "repeated_literal_threshold?": type.string.or("undefined"),
119
+ "var_files?": type.string.or("undefined"),
120
+ "review_comment_limit?": type.string.or("undefined"),
118
121
  "required_controls?": type.string.or("undefined"),
119
122
  "gitleaks?": type.string.or("undefined"),
120
123
  "cost_increase_block_usd?": type.string.or("undefined"),
@@ -216,6 +219,8 @@ function resolveNonPromptInputs() {
216
219
  protected_paths: core.getInput("protected_paths") || undefined,
217
220
  autonomy_threshold: core.getInput("autonomy_threshold") || undefined,
218
221
  repeated_literal_threshold: core.getInput("repeated_literal_threshold") || undefined,
222
+ var_files: core.getInput("var_files") || undefined,
223
+ review_comment_limit: core.getInput("review_comment_limit") || undefined,
219
224
  required_controls: core.getInput("required_controls") || undefined,
220
225
  gitleaks: core.getInput("gitleaks") || undefined,
221
226
  cost_increase_block_usd: core.getInput("cost_increase_block_usd") || undefined,
@@ -275,6 +280,35 @@ function parseRepeatThreshold(raw: string | undefined): number | undefined {
275
280
  return n;
276
281
  }
277
282
 
283
+ /** Inline comments one review posts when `review_comment_limit` is unset. */
284
+ const DEFAULT_REVIEW_COMMENT_LIMIT = 25;
285
+
286
+ /**
287
+ * The `review_comment_limit` setting: a whole number of inline comments, 0 for
288
+ * no limit. Unset or unusable falls back to the default rather than failing the
289
+ * run — a presentation knob set wrong is not worth losing a review over — and an
290
+ * unusable value says so, since silently ignoring it would look like a bug.
291
+ */
292
+ function parseReviewCommentLimit(raw: string | undefined): number {
293
+ const text = raw?.trim();
294
+ if (!text) return DEFAULT_REVIEW_COMMENT_LIMIT;
295
+ const n = Number(text);
296
+ if (Number.isInteger(n) && n >= 0) return n;
297
+ log.warning(
298
+ `» review_comment_limit: "${text}" is not a whole number — using ${DEFAULT_REVIEW_COMMENT_LIMIT}`,
299
+ );
300
+ return DEFAULT_REVIEW_COMMENT_LIMIT;
301
+ }
302
+
303
+ /** The `var_files` setting as mappings; a malformed entry is reported and left out. */
304
+ function parseVarFiles(raw: string | undefined): VarFileMapping[] | undefined {
305
+ const { mappings, invalid } = parseVarFileMappings(raw);
306
+ for (const entry of invalid) {
307
+ log.warning(`» var_files: "${entry}" is not root=file — ignored`);
308
+ }
309
+ return mappings.length > 0 ? mappings : undefined;
310
+ }
311
+
278
312
  function parseSeverityThreshold(raw: string | undefined): string | undefined {
279
313
  if (!raw) return undefined;
280
314
  const v = raw.trim().toLowerCase();
@@ -569,6 +603,13 @@ export function resolvePayload(
569
603
  repeatedLiteralThreshold: parseRepeatThreshold(
570
604
  inputs.repeated_literal_threshold ?? fileConfig.repeated_literal_threshold,
571
605
  ),
606
+ // `-var-file` files and the root each belongs to, for the files no
607
+ // matching can place (`treeLayout.ts`).
608
+ varFiles: parseVarFiles(inputs.var_files ?? fileConfig.var_files),
609
+ // inline comments one review posts; the rest are listed in its body.
610
+ reviewCommentLimit: parseReviewCommentLimit(
611
+ inputs.review_comment_limit ?? fileConfig.review_comment_limit,
612
+ ),
572
613
  // compliance controls that must have NO open finding — a finding mapping to
573
614
  // one fails the gate regardless of severity (the "required controls" policy:
574
615
  // binds the crosswalk to the gate). Matched by framework / control /
@@ -83,11 +83,6 @@ export async function downloadAssetsInMarkdown(
83
83
  * {@link MAX_ASSET_BYTES}). */
84
84
  const ASSET_DOWNLOAD_CONCURRENCY = 5;
85
85
 
86
- /** Escape a literal for embedding in a RegExp. */
87
- function escapeRegExp(literal: string): string {
88
- return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
89
- }
90
-
91
86
  /**
92
87
  * Substitute every key of `replacements` in `text` in ONE pass, via a single
93
88
  * alternation.
@@ -103,7 +98,7 @@ function replaceAll(text: string, replacements: Map<string, string>): string {
103
98
  if (replacements.size === 0) return text;
104
99
  const pattern = [...replacements.keys()]
105
100
  .sort((a, b) => b.length - a.length)
106
- .map(escapeRegExp)
101
+ .map((key) => RegExp.escape(key))
107
102
  .join("|");
108
103
  return text.replace(new RegExp(pattern, "g"), (match) => replacements.get(match) ?? match);
109
104
  }