infraweaver 0.3.10 → 0.3.11

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/quotaTokens.d.ts +8 -0
  3. package/dist/cli.mjs +1843 -1601
  4. package/dist/index.js +421 -182
  5. package/dist/internal.js +6 -4
  6. package/dist/mcp/crosswalk-digest.d.ts +1 -1
  7. package/dist/mcp/git.d.ts +19 -3
  8. package/dist/mcp/localContext.d.ts +9 -1
  9. package/dist/mcp/shell.d.ts +3 -3
  10. package/dist/mcp/staleFix.d.ts +13 -1
  11. package/dist/mcp/terraform/hardcodedSecrets.d.ts +49 -0
  12. package/dist/mcp/terraform/nativeRules.d.ts +1 -0
  13. package/dist/mcp/terraform/refactorScope.d.ts +12 -0
  14. package/dist/mcp/terraform/tools/scan.d.ts +1 -0
  15. package/dist/mcp/terraform/types.d.ts +3 -0
  16. package/dist/utils/config/payload.d.ts +2 -0
  17. package/package.json +1 -1
  18. package/src/agents/claude.ts +2 -1
  19. package/src/agents/opencode.ts +2 -1
  20. package/src/agents/quotaTokens.ts +25 -0
  21. package/src/mcp/crosswalk-digest.ts +1 -1
  22. package/src/mcp/crosswalk.ts +2 -0
  23. package/src/mcp/git.ts +44 -3
  24. package/src/mcp/localContext.ts +8 -0
  25. package/src/mcp/localServer.ts +3 -0
  26. package/src/mcp/moduleExtraction.ts +4 -1
  27. package/src/mcp/shell.ts +2 -2
  28. package/src/mcp/staleFix.ts +21 -2
  29. package/src/mcp/terraform/concernResult.ts +24 -4
  30. package/src/mcp/terraform/hardcodedSecrets.ts +202 -0
  31. package/src/mcp/terraform/nativeRuleScanners.ts +8 -0
  32. package/src/mcp/terraform/nativeRules.ts +14 -0
  33. package/src/mcp/terraform/refactorScope.ts +86 -0
  34. package/src/mcp/terraform/standardsReport.ts +2 -0
  35. package/src/mcp/terraform/tools/consolidationCandidates.ts +5 -1
  36. package/src/mcp/terraform/tools/normalizationCandidates.ts +5 -1
  37. package/src/mcp/terraform/types.ts +3 -0
  38. package/src/modes/refactor.ts +2 -0
  39. package/src/modes/refresh-remediation.ts +1 -1
  40. package/src/modes/remediate-and-refactor.ts +1 -1
  41. package/src/modes/remediate.ts +2 -2
  42. package/src/utils/config/payload.ts +4 -0
package/dist/index.js CHANGED
@@ -84161,14 +84161,14 @@ var require_turndown_cjs = __commonJS({
84161
84161
  } else if (node3.nodeType === 1) {
84162
84162
  replacement = replacementForNode.call(self2, node3);
84163
84163
  }
84164
- return join73(output2, replacement);
84164
+ return join74(output2, replacement);
84165
84165
  }, "");
84166
84166
  }
84167
84167
  function postProcess(output2) {
84168
84168
  var self2 = this;
84169
84169
  this.rules.forEach(function(rule) {
84170
84170
  if (typeof rule.append === "function") {
84171
- output2 = join73(output2, rule.append(self2.options));
84171
+ output2 = join74(output2, rule.append(self2.options));
84172
84172
  }
84173
84173
  });
84174
84174
  return output2.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -84180,7 +84180,7 @@ var require_turndown_cjs = __commonJS({
84180
84180
  if (whitespace.leading || whitespace.trailing) content = content.trim();
84181
84181
  return whitespace.leading + rule.replacement(content, node3, this.options) + whitespace.trailing;
84182
84182
  }
84183
- function join73(output2, replacement) {
84183
+ function join74(output2, replacement) {
84184
84184
  var s1 = trimTrailingNewlines(output2);
84185
84185
  var s2 = trimLeadingNewlines(replacement);
84186
84186
  var nls = Math.max(output2.length - s1.length, replacement.length - s2.length);
@@ -97300,7 +97300,7 @@ var require_polyfill = __commonJS({
97300
97300
  var {
97301
97301
  dirname: dirname15,
97302
97302
  isAbsolute: isAbsolute7,
97303
- join: join73,
97303
+ join: join74,
97304
97304
  parse: parse8,
97305
97305
  resolve: resolve11,
97306
97306
  sep: sep11,
@@ -97557,8 +97557,8 @@ var require_polyfill = __commonJS({
97557
97557
  const dir = await readdir2(src);
97558
97558
  for (let i = 0; i < dir.length; i++) {
97559
97559
  const item = dir[i];
97560
- const srcItem = join73(src, item);
97561
- const destItem = join73(dest, item);
97560
+ const srcItem = join74(src, item);
97561
+ const destItem = join74(dest, item);
97562
97562
  const { destStat } = await checkPaths2(srcItem, destItem, opts);
97563
97563
  await startCopy(destStat, srcItem, destItem, opts);
97564
97564
  }
@@ -97631,7 +97631,7 @@ var require_cp = __commonJS({
97631
97631
  // node_modules/.pnpm/@npmcli+fs@6.0.0/node_modules/@npmcli/fs/lib/with-temp-dir.js
97632
97632
  var require_with_temp_dir = __commonJS({
97633
97633
  "node_modules/.pnpm/@npmcli+fs@6.0.0/node_modules/@npmcli/fs/lib/with-temp-dir.js"(exports2, module) {
97634
- var { join: join73, sep: sep11 } = __require("path");
97634
+ var { join: join74, sep: sep11 } = __require("path");
97635
97635
  var getOptions2 = require_get_options();
97636
97636
  var { mkdir: mkdir5, mkdtemp, rm: rm2 } = __require("fs/promises");
97637
97637
  var withTempDir = async (root, fn2, opts) => {
@@ -97639,7 +97639,7 @@ var require_with_temp_dir = __commonJS({
97639
97639
  copy: ["tmpPrefix"]
97640
97640
  });
97641
97641
  await mkdir5(root, { recursive: true });
97642
- const target = await mkdtemp(join73(`${root}${sep11}`, options.tmpPrefix || ""));
97642
+ const target = await mkdtemp(join74(`${root}${sep11}`, options.tmpPrefix || ""));
97643
97643
  let err;
97644
97644
  let result;
97645
97645
  try {
@@ -97664,13 +97664,13 @@ var require_with_temp_dir = __commonJS({
97664
97664
  var require_readdir_scoped = __commonJS({
97665
97665
  "node_modules/.pnpm/@npmcli+fs@6.0.0/node_modules/@npmcli/fs/lib/readdir-scoped.js"(exports2, module) {
97666
97666
  var { readdir: readdir2 } = __require("fs/promises");
97667
- var { join: join73 } = __require("path");
97667
+ var { join: join74 } = __require("path");
97668
97668
  var readdirScoped = async (dir) => {
97669
97669
  const results = [];
97670
97670
  for (const item of await readdir2(dir)) {
97671
97671
  if (item.startsWith("@")) {
97672
- for (const scopedItem of await readdir2(join73(dir, item))) {
97673
- results.push(join73(item, scopedItem));
97672
+ for (const scopedItem of await readdir2(join74(dir, item))) {
97673
+ results.push(join74(item, scopedItem));
97674
97674
  }
97675
97675
  } else {
97676
97676
  results.push(item);
@@ -97685,7 +97685,7 @@ var require_readdir_scoped = __commonJS({
97685
97685
  // node_modules/.pnpm/@npmcli+fs@6.0.0/node_modules/@npmcli/fs/lib/move-file.js
97686
97686
  var require_move_file = __commonJS({
97687
97687
  "node_modules/.pnpm/@npmcli+fs@6.0.0/node_modules/@npmcli/fs/lib/move-file.js"(exports2, module) {
97688
- var { dirname: dirname15, join: join73, resolve: resolve11, relative: relative8, isAbsolute: isAbsolute7 } = __require("path");
97688
+ var { dirname: dirname15, join: join74, resolve: resolve11, relative: relative8, isAbsolute: isAbsolute7 } = __require("path");
97689
97689
  var fs12 = __require("fs/promises");
97690
97690
  var pathExists2 = async (path15) => {
97691
97691
  try {
@@ -97715,7 +97715,7 @@ var require_move_file = __commonJS({
97715
97715
  if (sourceStat.isDirectory()) {
97716
97716
  const files = await fs12.readdir(source);
97717
97717
  await Promise.all(files.map(
97718
- (file2) => moveFile(join73(source, file2), join73(destination, file2), options, false, symlinks)
97718
+ (file2) => moveFile(join74(source, file2), join74(destination, file2), options, false, symlinks)
97719
97719
  ));
97720
97720
  } else if (sourceStat.isSymbolicLink()) {
97721
97721
  symlinks.push({ source, destination });
@@ -160384,7 +160384,7 @@ var init_cacheTwirpClient = __esm({
160384
160384
  });
160385
160385
 
160386
160386
  // node_modules/.pnpm/@actions+cache@6.3.0_supports-color@7.2.0/node_modules/@actions/cache/lib/internal/tar.js
160387
- import { existsSync as existsSync16, writeFileSync as writeFileSync21 } from "fs";
160387
+ import { existsSync as existsSync17, writeFileSync as writeFileSync21 } from "fs";
160388
160388
  import * as path13 from "path";
160389
160389
  function getTarPath() {
160390
160390
  return __awaiter18(this, void 0, void 0, function* () {
@@ -160394,7 +160394,7 @@ function getTarPath() {
160394
160394
  const systemTar = SystemTarPathOnWindows;
160395
160395
  if (gnuTar) {
160396
160396
  return { path: gnuTar, type: ArchiveToolType.GNU };
160397
- } else if (existsSync16(systemTar)) {
160397
+ } else if (existsSync17(systemTar)) {
160398
160398
  return { path: systemTar, type: ArchiveToolType.BSD };
160399
160399
  }
160400
160400
  break;
@@ -258516,7 +258516,7 @@ var declare = ark.declare;
258516
258516
  import { createHash as createHash4 } from "node:crypto";
258517
258517
 
258518
258518
  // src/mcp/crosswalk-digest.ts
258519
- var EXPECTED_CROSSWALK_DIGEST = "95afd7e402f0f1875fa0aac33aeee8b6b54f869ec65499fb727f515ee2101ee1";
258519
+ var EXPECTED_CROSSWALK_DIGEST = "f583001997c4c69f84f189a14e69e4f6eb2c52bddc04ca7f404286b5280d1ff4";
258520
258520
 
258521
258521
  // node_modules/.pnpm/@infraweaver-io+crosswalk@1.2.0/node_modules/@infraweaver-io/crosswalk/dist/frameworks.js
258522
258522
  var FRAMEWORKS = [
@@ -259020,9 +259020,9 @@ function boxString(text, options) {
259020
259020
  if (line.length <= maxWidth - padding * 2) {
259021
259021
  wrappedLines.push(line);
259022
259022
  } else {
259023
- const words = line.split(" ");
259023
+ const words2 = line.split(" ");
259024
259024
  let currentLine = "";
259025
- for (const word of words) {
259025
+ for (const word of words2) {
259026
259026
  const testLine = currentLine ? `${currentLine} ${word}` : word;
259027
259027
  if (testLine.length <= maxWidth - padding * 2) {
259028
259028
  currentLine = testLine;
@@ -259203,7 +259203,7 @@ import { randomBytes as randomBytes2 } from "node:crypto";
259203
259203
  // package.json
259204
259204
  var package_default = {
259205
259205
  name: "infraweaver",
259206
- version: "0.3.10",
259206
+ version: "0.3.11",
259207
259207
  description: "GitHub Action that remediates Terraform to best practices and opens one scoped pull request per concern.",
259208
259208
  keywords: [
259209
259209
  "ai-coding-agent",
@@ -260156,6 +260156,7 @@ import { createHash as createHash3 } from "node:crypto";
260156
260156
  var NATIVE_SOURCE = TOOL_NAME;
260157
260157
  var SECRET_PERSISTED_TO_STATE = "secret-persisted-to-state";
260158
260158
  var COMMITTED_STATE_FILE = "committed-state-file";
260159
+ var HARDCODED_SECRET_ARGUMENT = "hardcoded-secret-argument";
260159
260160
  var IMPERATIVE_ACTION_TRIGGER = "imperative-action-trigger";
260160
260161
  var DYNAMIC_DESTROY_PROTECTION = "dynamic-destroy-protection";
260161
260162
  var DEPRECATED_STATE_LOCKING = "deprecated-state-locking";
@@ -260191,6 +260192,14 @@ var NATIVE_RULES = [
260191
260192
  asserts: "git tracks a `.tfstate` / `.tfstate.backup` file at this path, so the plaintext record of every attribute Terraform read back from the provider is in the history of every clone and fork",
260192
260193
  doesNotAssert: "that the state contains a live credential, or that anyone has read it \u2014 the file is never opened; the finding is that state is under version control, and what it recorded is for the owner to say"
260193
260194
  },
260195
+ {
260196
+ id: HARDCODED_SECRET_ARGUMENT,
260197
+ title: "Credential written into the configuration as a literal",
260198
+ band: "verified",
260199
+ category: "security",
260200
+ asserts: "an argument, map key or variable whose name says it holds a credential (`password`, `token`, `client_secret`, `api_key`, \u2026) is assigned a string literal, so the value is in the repository and in its history",
260201
+ doesNotAssert: "that the value is a live credential \u2014 it may be a test value; the finding is that a credential slot is filled from the code rather than at run time"
260202
+ },
260194
260203
  {
260195
260204
  id: IMPERATIVE_ACTION_TRIGGER,
260196
260205
  title: "Imperative side-effect invoked from declarative IaC (action / action_trigger)",
@@ -261515,7 +261524,9 @@ var THEME_LOCKED = /* @__PURE__ */ new Map([
261515
261524
  // credentials", and state in git is the canonical failure of it. It deliberately
261516
261525
  // does NOT also cite `encryption-at-rest`: whether the repository is encrypted is
261517
261526
  // not what the finding establishes.
261518
- ["committed-state-file", "secrets-management"]
261527
+ ["committed-state-file", "secrets-management"],
261528
+ // a credential in the code is the same CIS 16.4 failure one step earlier than state in git
261529
+ ["hardcoded-secret-argument", "secrets-management"]
261519
261530
  ]);
261520
261531
  function bareRuleId(ruleId) {
261521
261532
  const colon = ruleId.indexOf(":");
@@ -266086,6 +266097,128 @@ function scanDialect(files) {
266086
266097
  return concerns;
266087
266098
  }
266088
266099
 
266100
+ // src/mcp/terraform/hardcodedSecrets.ts
266101
+ var SECRET_WORDS = /* @__PURE__ */ new Set([
266102
+ "password",
266103
+ "passwd",
266104
+ "pwd",
266105
+ "passphrase",
266106
+ "secret",
266107
+ "token",
266108
+ "auth",
266109
+ "apikey",
266110
+ "credential",
266111
+ "credentials"
266112
+ ]);
266113
+ var SECRET_KEY_PREFIXES = /* @__PURE__ */ new Set(["api", "secret", "private", "signing"]);
266114
+ var NOT_SECRET_TOKENS = /* @__PURE__ */ new Set(["creation", "idempotency", "client", "request", "continuation"]);
266115
+ function words(name) {
266116
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_-]+/).filter(Boolean);
266117
+ }
266118
+ function isSecretName(name) {
266119
+ const w = words(name);
266120
+ const last = w.at(-1);
266121
+ const prev = w.at(-2);
266122
+ if (last === void 0) return false;
266123
+ if (last === "key") return prev !== void 0 && SECRET_KEY_PREFIXES.has(prev);
266124
+ if (last === "token" && prev !== void 0 && NOT_SECRET_TOKENS.has(prev)) return false;
266125
+ return SECRET_WORDS.has(last);
266126
+ }
266127
+ function isPlaceholder(inner) {
266128
+ return inner.trim() === "" || /^[*xX.]+$/.test(inner) || /^<[^>]*>$/.test(inner) || // the dummy credentials offline plans and LocalStack are given (`secret_key = "test"`)
266129
+ /^(?:test|mock|fake|dummy|fixture|example|placeholder|sample)(?:[_-].*)?$/i.test(inner) || // a token's lifetime unit: `token_validity_units { access_token = "hours" }`
266130
+ /^(?:seconds|minutes|hours|days)$/.test(inner);
266131
+ }
266132
+ var HEADER = /^(resource|data|provider|module|locals|variable|output|terraform)\b([^{]*)\{\s*$/;
266133
+ var ASSIGNMENT = /^\s*(?:"([A-Za-z_][\w-]*)"|([A-Za-z_][\w-]*))\s*=(?!=)\s*(.*)$/;
266134
+ var HEREDOC = /<<-?([A-Za-z_]\w*)\s*$/;
266135
+ var INLINE_PAIR = /(?:"([A-Za-z_][\w-]*)"|\b([A-Za-z_][\w-]*))\s*=(?!=)\s*("(?:[^"\\]|\\.)*")/g;
266136
+ function literalValue(raw2) {
266137
+ const value2 = stripTrailingComment(raw2).replace(/,$/, "").trim();
266138
+ return isStringLiteral(value2) && !isPlaceholder(value2.slice(1, -1));
266139
+ }
266140
+ function findHardcodedSecrets(hcl2) {
266141
+ const out = [];
266142
+ let holder = null;
266143
+ let heredoc = null;
266144
+ let inComment = false;
266145
+ const lines = hcl2.split("\n");
266146
+ for (let i = 0; i < lines.length; i++) {
266147
+ const line = lines[i].replace(/\r$/, "");
266148
+ if (heredoc !== null) {
266149
+ if (line.trim() === heredoc) heredoc = null;
266150
+ continue;
266151
+ }
266152
+ if (inComment) {
266153
+ if (line.includes("*/")) inComment = false;
266154
+ continue;
266155
+ }
266156
+ const trimmed = line.trimStart();
266157
+ if (trimmed.startsWith("/*")) {
266158
+ inComment = !trimmed.includes("*/");
266159
+ continue;
266160
+ }
266161
+ if (trimmed.startsWith("#") || trimmed.startsWith("//")) continue;
266162
+ const header = HEADER.exec(line);
266163
+ if (header) {
266164
+ holder = `${header[1]}${header[2].trimEnd() ? ` ${header[2].trim()}` : ""}`;
266165
+ continue;
266166
+ }
266167
+ if (line.startsWith("}")) {
266168
+ holder = null;
266169
+ continue;
266170
+ }
266171
+ const opens = HEREDOC.exec(line);
266172
+ if (opens) heredoc = opens[1];
266173
+ if (holder === null || opens) continue;
266174
+ const m = ASSIGNMENT.exec(line);
266175
+ if (!m) continue;
266176
+ const name = m[1] ?? m[2];
266177
+ const value2 = m[3];
266178
+ if (holder.startsWith("variable ")) {
266179
+ const variable = /^variable\s+"?([\w-]+)"?/.exec(holder)?.[1] ?? "";
266180
+ if (name === "default" && isSecretName(variable) && literalValue(value2)) {
266181
+ out.push({ name: variable, holder, line: i + 1 });
266182
+ }
266183
+ continue;
266184
+ }
266185
+ if (holder.startsWith("output ") || holder.startsWith("terraform")) continue;
266186
+ if (isSecretName(name) && literalValue(value2)) {
266187
+ out.push({ name, holder, line: i + 1 });
266188
+ continue;
266189
+ }
266190
+ for (const pair of value2.matchAll(INLINE_PAIR)) {
266191
+ const key = pair[1] ?? pair[2];
266192
+ if (isSecretName(key) && literalValue(pair[3])) {
266193
+ out.push({ name: key, holder, line: i + 1 });
266194
+ }
266195
+ }
266196
+ }
266197
+ return out;
266198
+ }
266199
+ function scanHardcodedSecrets(files) {
266200
+ const concerns = [];
266201
+ for (const { path: path15, hcl: hcl2 } of files) {
266202
+ for (const hit of findHardcodedSecrets(hcl2)) {
266203
+ concerns.push(
266204
+ buildConcern({
266205
+ source: NATIVE_SOURCE,
266206
+ rule: HARDCODED_SECRET_ARGUMENT,
266207
+ file: path15,
266208
+ line: hit.line,
266209
+ // a credential in the repository is readable by everyone who can read
266210
+ // it, and stays in the history after it is edited out
266211
+ severity: "high",
266212
+ category: "security",
266213
+ evidence: `\`${hit.name}\` in \`${hit.holder}\` is a string literal, so the credential it holds is committed to the repository (value not quoted here)`,
266214
+ remediation_hint: "Rotate the credential first \u2014 it stays in the branch history after the line is changed. Then pass it in at run time: a `sensitive` variable with no default, or a read from a secret store (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager)."
266215
+ })
266216
+ );
266217
+ }
266218
+ }
266219
+ return concerns;
266220
+ }
266221
+
266089
266222
  // src/mcp/roots.ts
266090
266223
  import { readFileSync as readFileSync5 } from "node:fs";
266091
266224
  import { join as join10 } from "node:path";
@@ -268065,6 +268198,12 @@ var NATIVE_RULE_SCANNERS = [
268065
268198
  // one. See [[stateFiles]].
268066
268199
  scan: (ctx) => scanCommittedStateFiles(ctx.cwd)
268067
268200
  },
268201
+ {
268202
+ name: "hardcoded-secrets",
268203
+ rules: [HARDCODED_SECRET_ARGUMENT],
268204
+ // Per file: the argument's name and its literal are both on the line itself.
268205
+ scan: (ctx) => ({ concerns: scanHardcodedSecrets(ctx.files) })
268206
+ },
268068
268207
  {
268069
268208
  name: "write-only",
268070
268209
  rules: [SECRET_PERSISTED_TO_STATE],
@@ -268655,8 +268794,8 @@ function matchRoot(file2, keys, roots) {
268655
268794
  const dir = dirOf2(file2);
268656
268795
  const inside = candidates.filter((r) => r.dir !== "" && isWithin(dir, r.dir)).sort((a, b) => b.dir.length - a.dir.length);
268657
268796
  if (inside[0]) return { root: inside[0].dir };
268658
- const words = new Set(tokens(file2));
268659
- const scored = candidates.map((r) => ({ dir: r.dir, score: tokens(r.dir).filter((t) => words.has(t)).length })).sort((a, b) => b.score - a.score);
268797
+ const words2 = new Set(tokens(file2));
268798
+ const scored = candidates.map((r) => ({ dir: r.dir, score: tokens(r.dir).filter((t) => words2.has(t)).length })).sort((a, b) => b.score - a.score);
268660
268799
  if (scored[0] && scored[0].score > 0 && scored[0].score !== scored[1]?.score) {
268661
268800
  return { root: scored[0].dir };
268662
268801
  }
@@ -274140,7 +274279,11 @@ function isRemediationBranch(branch) {
274140
274279
  return REMEDIATION_BRANCH.test(branch);
274141
274280
  }
274142
274281
  function groupIdFromBranch(branch) {
274143
- return branch.match(/^remediate\/(.+)$/)?.[1] ?? null;
274282
+ return branch.match(/^remediate\/(.+?)(?:--.+)?$/)?.[1] ?? null;
274283
+ }
274284
+ function remediationBranchSuffix(base, defaultBranch) {
274285
+ if (!base || !defaultBranch || base === defaultBranch) return "";
274286
+ return `--${base.replace(/[^A-Za-z0-9._-]+/g, "-")}`;
274144
274287
  }
274145
274288
  function matchesBotIdentity(identity) {
274146
274289
  const normalized = identity.replace(/\[bot\]$/i, "").toLowerCase();
@@ -276493,6 +276636,8 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
276493
276636
  - \`${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.
276494
276637
  - \`${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.
276495
276638
 
276639
+ **A result marked \`report_only\` is an observation, not work.** The repository is a module other configurations call (\`report_only_reason\` says why), and restructuring it is churn for every caller: name those candidates in your \`${t("report_progress")}\` summary and open no PR for them. \`examples/\` roots are left out of every candidate list unless the operator set \`refactor_examples\`.
276640
+
276496
276641
  **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.
276497
276642
 
276498
276643
  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.
@@ -276579,7 +276724,7 @@ This mode keeps already-open ${PRODUCT_NAME} remediation PRs healthy. A remediat
276579
276724
  - **resolved-on-base? \u2192 close it**: look for a group in step 3's scan whose \`id\` equals the PR's \`group_id\` (for a by-rule/batch PR, match on the concern ids it covered). If **no** current group/concern corresponds, the concern was already fixed on the base (a human fix, or a base change removed the file) \u2014 the PR is redundant. Call \`${t("close_pull_request")}\` with a one-line \`comment\` explaining it's resolved on the base. Do not push anything for this PR.
276580
276725
  - **still present \u2192 re-derive the fix on the current base**:
276581
276726
  - **read WHY the CI failed, before changing anything** (\`checks_failing\` PRs only): when step 2 gave the PR a \`failing_check_suite_id\`, call \`${t("get_check_suite_logs")}\` with that id FIRST. Re-deriving without reading the failure is exactly how attempt 2 and attempt 3 become identical repeats of attempt 1 \u2014 same fix, same red check, three times, until the retry cap escalates a PR nobody learned anything about. Use the returned \`failed_steps\` and log excerpt to decide what to do DIFFERENTLY this attempt. If the log shows the failure is not ours \u2014 a broken base, a missing credential, an unrelated flaky job \u2014 do not spend an attempt re-deriving: add \`needs-human\` (\`${t("add_labels")}\`), post ONE comment naming the failing step, and move on. Record the cause in the PR body/comment you write below, so the next attempt and any human start from what you found. When the id is absent, or the tool reports no failed runs, say so in the comment and continue with the plain re-derive rather than guessing.
276582
- - **branch**: recreate the remediation branch at the current base HEAD via \`${t("git")}\` (\`git checkout -B remediate/<id>\`) \u2014 \`-B\` force-resets it to the just-scanned base so the diff is only your fix.
276727
+ - **branch**: recreate the remediation branch at the current base HEAD via \`${t("git")}\` (\`git({ command: "checkout", args: ["-B", <the PR's branch>] })\`, the \`branch\` step 2 returned) \u2014 \`-B\` force-resets it to the just-scanned base so the diff is only your fix.
276583
276728
  - **fix \u2192 validate \u2192 plan \u2192 keep tests consistent \u2192 prove it**: apply the minimal fix for that group exactly as in **Remediate** step 4 (same \`${t("terraform_validate")}\`, \`${t("terraform_plan")}\`, \`${t("terraform_module_tests")}\`, and \`${t("terraform_verify_remediation")}\` gates, and the same guardrails \u2014 never open/keep a PR whose validate didn't pass, abandon a group that would destroy a stateful resource, etc.).
276584
276729
  - **force-update the PR branch**: \`${t("push_branch")}\` with \`force: true\` (the PR already exists; force-updating its branch refreshes it in place \u2014 do NOT open a second PR). The Terraform-only / secret / destroy guardrails still run at push time.
276585
276730
  - **refresh the body**: \`${t("update_pull_request_body")}\` rebuilt from the fresh \`${t("terraform_verify_remediation")}\` result (the Remediation PR format below), and add a one-line note that it was rebased onto the current base (\`<short-sha>\`) on this run.
@@ -276624,8 +276769,8 @@ function remediateMode(t) {
276624
276769
 
276625
276770
  4. **for the chosen group**:
276626
276771
  - **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.
276627
- - **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.
276628
- - **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.
276772
+ - **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch) \u2014 use it exactly, never a name of your own. 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.
276773
+ - **branch**: create the group's \`branch\` from the **current HEAD** (the checkout that was just scanned) via \`${t("git")}\` (\`git({ command: "checkout", args: ["-b", <branch>] })\`). Do NOT switch to a different base first \u2014 branching from the scanned checkout keeps the PR diff to exactly your fix.
276629
276774
  - **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.
276630
276775
  - **propose, then let me steer (when there's no single right fix)**: distinct from honest refusal (which refuses a fix a human must *decide*), this is for a finding with **2\u20133 genuinely distinct, defensible fixes** that differ in trade-offs, not correctness (e.g. encrypt with an AWS-managed key **vs** a customer-managed KMS key; a narrow security-group rule **vs** a prefix list **vs** a VPC endpoint). When such a fork exists **and the triggering comment did not already select a strategy**, do **not** silently pick for the reviewer: via \`${t("create_issue_comment")}\` post one short comment listing the options as **A / B / C** \u2014 each a single line (what it does + its trade-off) \u2014 and ask the reviewer to reply \`${COMMENT_COMMAND} fix #<concern-id> with strategy <A|B|C>\`. Then **skip the PR for this group** this run and note it in your final report (it resumes when the reviewer replies). When the comment **did** select one (\`fix #<id> with strategy B\`, or a bare \`strategy B\` reply on the proposal thread), apply **exactly** that strategy \u2014 don't second-guess it. Reserve this for real forks in the road; a fix with one obvious correct answer just gets made.
276631
276776
  - **reuse a proven fix (optional, do this BEFORE editing)**: call \`${t("terraform_fix_memory")}\`. When proven-fix memory is enabled it returns any patterns whose \`finding_type\` matches this group's concerns \u2014 each a transformation that already passed a \`\u2717\u2192\u2713\` proof on this repo or elsewhere in the fleet, with its \`remediation_hint\` + a \`before_example\`. Use a matching pattern as your STARTING POINT (it saves you rediscovering the approach), but ADAPT it to this repo's own resources \u2014 it is a prior, never a patch to paste. You MUST still run \`${t("terraform_verify_remediation")}\` to prove \`\u2717\u2192\u2713\` here; a pattern is never blind-applied and never substitutes for the per-target proof. Returns an empty list (never an error) when memory is off or nothing matches \u2014 then fix from first principles as usual.
@@ -276685,7 +276830,7 @@ This mode composes the **Remediate** and **Refactor** verbs into a single run. I
276685
276830
  - **commit the fix** (\`git add\` only the changed \`*.tf\`/\`*.tfvars\`, a \`fix(tf): \u2026\` message). Do NOT push yet. This committed state is the refactor's equivalence BASELINE.
276686
276831
 
276687
276832
  3. **REFACTOR (phase 2) \u2014 modularise on top of the committed fix**: follow the **Refactor** mode's flow:
276688
- - \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor. Respect a pinned \`refactor_source\` if one was supplied (it constrains which module source you may use; the behaviour-altering b.4 path is only available when the operator pinned \`third-party\`).
276833
+ - \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor; a result marked \`report_only\` (the repository is a module others call) is named in the summary, never proposed. Respect a pinned \`refactor_source\` if one was supplied (it constrains which module source you may use; the behaviour-altering b.4 path is only available when the operator pinned \`third-party\`).
276689
276834
  - resolve the module source, wire the \`module\` call against its real interface (\`${t("terraform_module_interface")}\` for a local module dir, \`${t("terraform_module_lookup")}\` for a registry-sourced one), and emit a \`moved {}\` block for EVERY relocated address (\`${t("terraform_generate_moved")}\`).
276690
276835
  - \`terraform fmt\` + \`${t("terraform_validate")}\`, then \u2014 **with the refactor edits still UNCOMMITTED** \u2014 call \`${t("terraform_equivalence_check")}\`. In this mode it diffs the working tree against the COMMITTED fix (not the run-start commit), so it measures only the refactor. It must return \`equivalent: true\` (zero uncovered moves, resource set + arguments preserved, validate + fmt clean) before you proceed; \`${t("push_branch")}\` hard-blocks an unproven one \u2014 and, once this run has added a \`moved {}\` block, it also hard-blocks a push that never ran the check at all, so abandoning the refactor means REVERTING its edits rather than leaving them in with the fix. If it can't be proven equivalent, abandon the refactor and ship the fix alone.
276691
276836
  - **document the module (only when the \`docs\` input is enabled)**: after the equivalence check passes, call \`${t("terraform_module_docs")}\` for the module you created/adopted (pass the relocated addresses as \`moves\`). Do not hand-write docs \u2014 the tool builds from the interface, moved blocks, and equivalence verdict.
@@ -277251,7 +277396,7 @@ async function ensurePackageManager(params) {
277251
277396
  // src/mcp/shell.ts
277252
277397
  var ShellParams = type({
277253
277398
  command: "string",
277254
- description: "string",
277399
+ "description?": "string",
277255
277400
  "timeout?": type.number.describe(
277256
277401
  "Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately."
277257
277402
  ),
@@ -277486,7 +277631,7 @@ function ShellTool(ctx) {
277486
277631
  return tool({
277487
277632
  name: "shell",
277488
277633
  timeoutMs: 12e4,
277489
- description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
277634
+ description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets. NOT for git: any command running \`git\` is refused here \u2014 use the git tool (and push_branch / git_fetch).
277490
277635
 
277491
277636
  Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
277492
277637
 
@@ -278232,18 +278377,35 @@ function spillGitOutput(params) {
278232
278377
  };
278233
278378
  }
278234
278379
  var subcommandPattern = regex("^[a-z][a-z0-9-]*$");
278380
+ var commandLinePattern = regex("^git\\s+[a-z][a-z0-9-]*(?:\\s.*)?$");
278235
278381
  var Git = type({
278236
- command: type(subcommandPattern).describe("Git command (e.g., 'status', 'log', 'diff')"),
278382
+ command: type(subcommandPattern).or(type(commandLinePattern)).describe("Git command (e.g., 'status', 'log', 'diff')").optional(),
278237
278383
  args: type.string.array().describe("Additional arguments for the git command").optional()
278238
278384
  });
278385
+ function splitWords(line) {
278386
+ return [...line.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
278387
+ }
278388
+ function normalizeGitCall(params) {
278389
+ let words2;
278390
+ if (params.command === void 0) words2 = [...params.args ?? []];
278391
+ else if (/^git\s/.test(params.command))
278392
+ words2 = [...splitWords(params.command).slice(1), ...params.args ?? []];
278393
+ else return { command: params.command, args: params.args ?? [] };
278394
+ const [command, ...args2] = words2[0] === "git" ? words2.slice(1) : words2;
278395
+ if (command === void 0 || !/^[a-z][a-z0-9-]*$/.test(command)) {
278396
+ throw new Error(
278397
+ `command must be a Git subcommand such as 'status' or 'checkout' (was "${command ?? ""}")`
278398
+ );
278399
+ }
278400
+ return { command, args: args2 };
278401
+ }
278239
278402
  function GitTool(ctx) {
278240
278403
  return tool({
278241
278404
  name: "git",
278242
278405
  description: 'Run a git subcommand. `command` is the subcommand ONLY \u2014 never repeat it inside `args`. `args` is optional; omit it entirely for no-flag invocations like plain `git status`. Example: `git({ command: "status" })` for plain `git status`. Example: `git({ command: "log", args: ["--oneline", "-n", "20"] })`. Example: `git({ command: "diff", args: ["--merge-base", "origin/main"] })` \u2014 merge-base diff including uncommitted edits (single MCP call). Example: `git({ command: "diff", args: ["origin/main...HEAD"] })` \u2014 three-dot, committed-only changes vs merge-base. For PR-scope diffs ALWAYS use `--merge-base <base>` or three-dot `<base>...HEAD`. Bare `<base>` and two-dot `<base>..HEAD` are symmetric (working-tree-or-HEAD vs ref): when your branch is behind `<base>` they include the inverse of every commit on `<base>` you lack \u2014 pure noise, and this tool will reject those forms when the divergence is detected. Output >50K chars is spilled to a tmp file; the tool returns a head preview + path you can `read` selectively. For push/fetch, use the dedicated MCP tools (push_branch, git_fetch). git pull is not available \u2014 use git_fetch then this tool with command \'merge\'.',
278243
278406
  parameters: Git,
278244
278407
  execute: execute(async (params) => {
278245
- const command = params.command;
278246
- const args2 = params.args ?? [];
278408
+ const { command, args: args2 } = normalizeGitCall(params);
278247
278409
  if (args2[0]?.toLowerCase() === command.toLowerCase()) {
278248
278410
  throw new Error(
278249
278411
  `git ${command}: '${args2[0]}' duplicates the subcommand \u2014 drop args[0] (the subcommand only belongs in 'command'). git would otherwise parse it as a pathspec and silently return empty/clean output when nothing matches. if you really meant a pathspec named '${args2[0]}', use args: ["--", "${args2[0]}"].`
@@ -279431,6 +279593,7 @@ var Inputs = type({
279431
279593
  // org-repo | author | third-party). Structured scope, not prompt text — see
279432
279594
  // parseRefactorSource / withRefactorSource. Gates the behaviour-altering b.4.
279433
279595
  "refactor_source?": type.string.or("undefined"),
279596
+ "refactor_examples?": type.string.or("undefined"),
279434
279597
  "review_instructions?": type.string.or("undefined"),
279435
279598
  "fp_filtering_instructions?": type.string.or("undefined"),
279436
279599
  "remediation_instructions?": type.string.or("undefined"),
@@ -279508,6 +279671,7 @@ function resolveNonPromptInputs() {
279508
279671
  registry_token: getInput("registry_token") || void 0,
279509
279672
  registry_host: getInput("registry_host") || void 0,
279510
279673
  refactor_source: getInput("refactor_source") || void 0,
279674
+ refactor_examples: getInput("refactor_examples") || void 0,
279511
279675
  review_instructions: getInput("review_instructions") || void 0,
279512
279676
  fp_filtering_instructions: getInput("fp_filtering_instructions") || void 0,
279513
279677
  remediation_instructions: getInput("remediation_instructions") || void 0,
@@ -279809,6 +279973,8 @@ function resolvePayload(resolvedPromptInput, repoSettings, orgRequiredControls =
279809
279973
  // Refactor mode for the b.4 consent gate. undefined → auto. Action-input/
279810
279974
  // trusted-dispatch only (not the untrusted repo file).
279811
279975
  refactorSource,
279976
+ // opt in to refactoring `examples/` roots, which Refactor leaves alone by default
279977
+ refactorExamples: parseBooleanInput(inputs.refactor_examples),
279812
279978
  // a `@infraweaver fix …` command parsed from the triggering comment
279813
279979
  // body (the raw prompt), scoping the run to a specific concern/severity/file.
279814
279980
  // null when the prompt isn't a recognised command.
@@ -282069,6 +282235,10 @@ function annotateConcerns(rawConcerns, opts) {
282069
282235
  opts.cwd
282070
282236
  );
282071
282237
  }
282238
+ function remediationBaseOf(ctx) {
282239
+ const head = ctx.toolState.initialHead;
282240
+ return ctx.payload.baseBranch?.trim() || (head?.kind === "branch" ? head.name : void 0) || ctx.repo?.data.default_branch?.trim() || void 0;
282241
+ }
282072
282242
  function buildConcernResult(ctx, rawConcerns, opts) {
282073
282243
  const fullBaseline = dedupe(rawConcerns);
282074
282244
  const session = scanSessionOf(ctx.toolState);
@@ -282097,8 +282267,13 @@ function buildConcernResult(ctx, rawConcerns, opts) {
282097
282267
  active,
282098
282268
  opts.autonomyThreshold
282099
282269
  );
282100
- const groups2 = opts.priority ? prioritizeGroups(annotatedGroups, active, opts.priority) : annotatedGroups;
282101
- const batchPlan = planBatches(groups2);
282270
+ const suffix2 = remediationBranchSuffix(
282271
+ remediationBaseOf(ctx),
282272
+ ctx.repo?.data.default_branch?.trim() || void 0
282273
+ );
282274
+ const groups2 = (opts.priority ? prioritizeGroups(annotatedGroups, active, opts.priority) : annotatedGroups).map((g) => ({ ...g, branch: `remediate/${g.id}${suffix2}` }));
282275
+ const planned = planBatches(groups2);
282276
+ const batchPlan = planned.batch_branch ? { ...planned, batch_branch: `${planned.batch_branch}${suffix2}` } : planned;
282102
282277
  const bySeverity = {};
282103
282278
  for (const c of active) bySeverity[c.severity] = (bySeverity[c.severity] ?? 0) + 1;
282104
282279
  return {
@@ -286225,8 +286400,8 @@ function AddLabelsTool(ctx) {
286225
286400
  }
286226
286401
 
286227
286402
  // src/mcp/moduleExtraction.ts
286228
- import { readFileSync as readFileSync22 } from "node:fs";
286229
- import { join as join41, posix as posix11 } from "node:path";
286403
+ import { readFileSync as readFileSync23 } from "node:fs";
286404
+ import { join as join42, posix as posix11 } from "node:path";
286230
286405
 
286231
286406
  // src/mcp/terraform/partialInterface.ts
286232
286407
  import { readdirSync as readdirSync6, readFileSync as readFileSync21 } from "node:fs";
@@ -286289,6 +286464,54 @@ function findPartialInterfaces(cwd) {
286289
286464
  return out;
286290
286465
  }
286291
286466
 
286467
+ // src/mcp/terraform/refactorScope.ts
286468
+ import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync22, statSync as statSync6 } from "node:fs";
286469
+ import { join as join41 } from "node:path";
286470
+ function isExamplePath(path15) {
286471
+ return /(?:^|\/)examples?\//.test(path15.replace(/\\/g, "/"));
286472
+ }
286473
+ var PROVIDER_OR_BACKEND = /^\s*(?:provider\s+"|backend\s+")/m;
286474
+ function holdsTerraform(dir, depth) {
286475
+ let entries;
286476
+ try {
286477
+ entries = readdirSync7(dir);
286478
+ } catch {
286479
+ return false;
286480
+ }
286481
+ for (const name of entries) {
286482
+ const path15 = join41(dir, name);
286483
+ if (name.endsWith(".tf")) return true;
286484
+ if (depth > 0 && !name.startsWith(".") && statSync6(path15, { throwIfNoEntry: false })?.isDirectory()) {
286485
+ if (holdsTerraform(path15, depth - 1)) return true;
286486
+ }
286487
+ }
286488
+ return false;
286489
+ }
286490
+ function libraryRepoReason(cwd) {
286491
+ let rootFiles;
286492
+ try {
286493
+ rootFiles = readdirSync7(cwd).filter((f) => f.endsWith(".tf"));
286494
+ } catch {
286495
+ return null;
286496
+ }
286497
+ if (rootFiles.length === 0) return null;
286498
+ for (const f of rootFiles) {
286499
+ let text;
286500
+ try {
286501
+ text = readFileSync22(join41(cwd, f), "utf8");
286502
+ } catch {
286503
+ continue;
286504
+ }
286505
+ if (PROVIDER_OR_BACKEND.test(text)) return null;
286506
+ }
286507
+ const examples = ["examples", "example"].find((d) => existsSync10(join41(cwd, d)));
286508
+ if (examples === void 0 || !holdsTerraform(join41(cwd, examples), 2)) return null;
286509
+ return `the repository root configures no provider and no backend, and ships \`${examples}/\`: it is a module other configurations call, so restructuring it is churn for every caller`;
286510
+ }
286511
+ function reportOnly(reason) {
286512
+ return reason === null ? {} : { report_only: true, report_only_reason: reason };
286513
+ }
286514
+
286292
286515
  // src/mcp/moduleExtraction.ts
286293
286516
  function parseResourceBlocks(hcl2) {
286294
286517
  const out = [];
@@ -286577,7 +286800,7 @@ function findExtractionCandidates(cwd, rawCatalogue, orgModuleHcl) {
286577
286800
  if (isInHouseModule(file2, graph) !== null) continue;
286578
286801
  let text;
286579
286802
  try {
286580
- text = readFileSync22(join41(cwd, file2), "utf8");
286803
+ text = readFileSync23(join42(cwd, file2), "utf8");
286581
286804
  } catch {
286582
286805
  continue;
286583
286806
  }
@@ -286646,7 +286869,8 @@ function ModuleExtractionCandidatesTool(ctx) {
286646
286869
  cwd,
286647
286870
  ctx.payload.moduleCatalogue,
286648
286871
  ctx.toolState.orgModuleHcl
286649
- );
286872
+ ).filter((c) => ctx.payload.refactorExamples || !isExamplePath(c.cluster.file));
286873
+ const library = libraryRepoReason(cwd);
286650
286874
  const matched = candidates.filter((c) => c.candidates.length > 0);
286651
286875
  const reasons = {};
286652
286876
  for (const c of candidates)
@@ -286660,6 +286884,7 @@ function ModuleExtractionCandidatesTool(ctx) {
286660
286884
  `\xBB module_extraction_candidates: ${candidates.length} cluster(s), ${matched.length} with a module match` + (partialInterfaces.length > 0 ? `, ${partialInterfaces.length} partial-interface block(s)` : "")
286661
286885
  );
286662
286886
  return toolOk({
286887
+ ...reportOnly(library),
286663
286888
  cluster_count: candidates.length,
286664
286889
  matched_count: matched.length,
286665
286890
  candidates,
@@ -286683,8 +286908,8 @@ function ModuleExtractionCandidatesTool(ctx) {
286683
286908
  }
286684
286909
 
286685
286910
  // src/mcp/moduleTests.ts
286686
- import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync23, statSync as statSync6 } from "node:fs";
286687
- import { join as join42 } from "node:path";
286911
+ import { existsSync as existsSync11, readdirSync as readdirSync8, readFileSync as readFileSync24, statSync as statSync7 } from "node:fs";
286912
+ import { join as join43 } from "node:path";
286688
286913
  var MODULE_META_ARGS = /* @__PURE__ */ new Set([
286689
286914
  "source",
286690
286915
  "version",
@@ -286804,30 +287029,30 @@ function resolveSourceDir(fromDir, source) {
286804
287029
  }
286805
287030
  function isDir(p) {
286806
287031
  try {
286807
- return existsSync10(p) && statSync6(p).isDirectory();
287032
+ return existsSync11(p) && statSync7(p).isDirectory();
286808
287033
  } catch {
286809
287034
  return false;
286810
287035
  }
286811
287036
  }
286812
287037
  function listFiles(absDir, suffix2) {
286813
287038
  try {
286814
- return readdirSync7(absDir).filter((f) => f.endsWith(suffix2)).sort();
287039
+ return readdirSync8(absDir).filter((f) => f.endsWith(suffix2)).sort();
286815
287040
  } catch {
286816
287041
  return [];
286817
287042
  }
286818
287043
  }
286819
287044
  function listSubdirs(absDir) {
286820
287045
  try {
286821
- return readdirSync7(absDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
287046
+ return readdirSync8(absDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
286822
287047
  } catch {
286823
287048
  return [];
286824
287049
  }
286825
287050
  }
286826
287051
  function readAllTf(cwd, relDir) {
286827
287052
  let text = "";
286828
- for (const f of listFiles(join42(cwd, relDir), ".tf")) {
287053
+ for (const f of listFiles(join43(cwd, relDir), ".tf")) {
286829
287054
  try {
286830
- text += `${readFileSync23(join42(cwd, relDir, f), "utf8")}
287055
+ text += `${readFileSync24(join43(cwd, relDir, f), "utf8")}
286831
287056
  `;
286832
287057
  } catch {
286833
287058
  }
@@ -286839,8 +287064,8 @@ function discoverModuleTestAssets(cwd, moduleDir) {
286839
287064
  const assets = [];
286840
287065
  const matchesModule = (source, fromDir) => resolveSourceDir(fromDir, source) === md;
286841
287066
  for (const examplesRoot of [`${md}/examples`, "examples"]) {
286842
- if (!isDir(join42(cwd, examplesRoot))) continue;
286843
- for (const sub of listSubdirs(join42(cwd, examplesRoot))) {
287067
+ if (!isDir(join43(cwd, examplesRoot))) continue;
287068
+ for (const sub of listSubdirs(join43(cwd, examplesRoot))) {
286844
287069
  const exDir = `${examplesRoot}/${sub}`;
286845
287070
  const hcl2 = readAllTf(cwd, exDir);
286846
287071
  if (!hcl2) continue;
@@ -286856,12 +287081,12 @@ function discoverModuleTestAssets(cwd, moduleDir) {
286856
287081
  }
286857
287082
  }
286858
287083
  for (const testDir of [`${md}/tests`, md]) {
286859
- if (!isDir(join42(cwd, testDir))) continue;
286860
- for (const f of listFiles(join42(cwd, testDir), ".tftest.hcl")) {
287084
+ if (!isDir(join43(cwd, testDir))) continue;
287085
+ for (const f of listFiles(join43(cwd, testDir), ".tftest.hcl")) {
286861
287086
  const rel = `${testDir}/${f}`;
286862
287087
  let hcl2;
286863
287088
  try {
286864
- hcl2 = readFileSync23(join42(cwd, rel), "utf8");
287089
+ hcl2 = readFileSync24(join43(cwd, rel), "utf8");
286865
287090
  } catch {
286866
287091
  continue;
286867
287092
  }
@@ -286869,12 +287094,12 @@ function discoverModuleTestAssets(cwd, moduleDir) {
286869
287094
  }
286870
287095
  }
286871
287096
  for (const goDir of [`${md}/test`, `${md}/tests`]) {
286872
- if (!isDir(join42(cwd, goDir))) continue;
286873
- for (const f of listFiles(join42(cwd, goDir), "_test.go")) {
287097
+ if (!isDir(join43(cwd, goDir))) continue;
287098
+ for (const f of listFiles(join43(cwd, goDir), "_test.go")) {
286874
287099
  const rel = `${goDir}/${f}`;
286875
287100
  let go;
286876
287101
  try {
286877
- go = readFileSync23(join42(cwd, rel), "utf8");
287102
+ go = readFileSync24(join43(cwd, rel), "utf8");
286878
287103
  } catch {
286879
287104
  continue;
286880
287105
  }
@@ -286998,8 +287223,8 @@ function SetOutputTool(ctx, outputSchema) {
286998
287223
 
286999
287224
  // src/mcp/policy.ts
287000
287225
  import { spawnSync as spawnSync8 } from "node:child_process";
287001
- import { existsSync as existsSync11, readFileSync as readFileSync24 } from "node:fs";
287002
- import { join as join43 } from "node:path";
287226
+ import { existsSync as existsSync12, readFileSync as readFileSync25 } from "node:fs";
287227
+ import { join as join44 } from "node:path";
287003
287228
  function parseConftestOutput(stdout) {
287004
287229
  let parsed2;
287005
287230
  try {
@@ -287033,7 +287258,7 @@ var CONFTEST_CONFIG = "conftest.toml";
287033
287258
  function conftestSelectsNamespaces(conftestCwd) {
287034
287259
  let raw2;
287035
287260
  try {
287036
- raw2 = readFileSync24(join43(conftestCwd, CONFTEST_CONFIG), "utf-8");
287261
+ raw2 = readFileSync25(join44(conftestCwd, CONFTEST_CONFIG), "utf-8");
287037
287262
  } catch {
287038
287263
  return false;
287039
287264
  }
@@ -287049,8 +287274,8 @@ function namespaceArgs(conftestCwd) {
287049
287274
  }
287050
287275
  function findPlanJson(cwd) {
287051
287276
  for (const candidate of PLAN_JSON_NAMES) {
287052
- const abs = join43(cwd, candidate);
287053
- if (existsSync11(abs)) return abs;
287277
+ const abs = join44(cwd, candidate);
287278
+ if (existsSync12(abs)) return abs;
287054
287279
  }
287055
287280
  return null;
287056
287281
  }
@@ -287091,11 +287316,11 @@ function resolvePolicyDir(cwd, explicit) {
287091
287316
  } catch {
287092
287317
  return null;
287093
287318
  }
287094
- return existsSync11(abs) ? abs : null;
287319
+ return existsSync12(abs) ? abs : null;
287095
287320
  }
287096
287321
  for (const d of DEFAULT_POLICY_DIRS) {
287097
- const abs = join43(cwd, d);
287098
- if (existsSync11(abs)) return abs;
287322
+ const abs = join44(cwd, d);
287323
+ if (existsSync12(abs)) return abs;
287099
287324
  }
287100
287325
  return null;
287101
287326
  }
@@ -287129,7 +287354,7 @@ function PolicyCheckTool(ctx) {
287129
287354
  } catch {
287130
287355
  abs = null;
287131
287356
  }
287132
- targetFile = abs && existsSync11(abs) ? abs : null;
287357
+ targetFile = abs && existsSync12(abs) ? abs : null;
287133
287358
  if (!targetFile) {
287134
287359
  return toolSkip(
287135
287360
  "target_not_found",
@@ -287402,9 +287627,9 @@ function PullRequestInfoTool(ctx) {
287402
287627
 
287403
287628
  // node_modules/.pnpm/@infraweaver-io+sandbox@0.1.8/node_modules/@infraweaver-io/sandbox/dist/writePolicy.js
287404
287629
  import { renameSync, writeFileSync as writeFileSync13 } from "node:fs";
287405
- import { join as join44 } from "node:path";
287630
+ import { join as join45 } from "node:path";
287406
287631
  function writePolicyPath(tmpdir5, filename) {
287407
- return join44(tmpdir5, filename);
287632
+ return join45(tmpdir5, filename);
287408
287633
  }
287409
287634
  function publishWritePolicy(params) {
287410
287635
  try {
@@ -287575,12 +287800,12 @@ function SelectModeTool(ctx) {
287575
287800
  }
287576
287801
 
287577
287802
  // src/mcp/terraform/tools/advise.ts
287578
- import { existsSync as existsSync12, readFileSync as readFileSync26 } from "node:fs";
287579
- import { join as join46 } from "node:path";
287803
+ import { existsSync as existsSync13, readFileSync as readFileSync27 } from "node:fs";
287804
+ import { join as join47 } from "node:path";
287580
287805
 
287581
287806
  // src/mcp/terraform/fixMemory.ts
287582
- import { readFileSync as readFileSync25 } from "node:fs";
287583
- import { join as join45 } from "node:path";
287807
+ import { readFileSync as readFileSync26 } from "node:fs";
287808
+ import { join as join46 } from "node:path";
287584
287809
  var FIX_MEMORY_FILENAME = FIX_MEMORY_FILE;
287585
287810
  var MAX_DESCRIPTION = 400;
287586
287811
  var MAX_BEFORE_EXAMPLE = 1200;
@@ -287667,7 +287892,7 @@ function loadFixMemory(cwd) {
287667
287892
  const root = cwd ?? process.cwd();
287668
287893
  for (const filename of [FIX_MEMORY_FILENAME, LEGACY_FIX_MEMORY_FILENAME]) {
287669
287894
  try {
287670
- return parseFixMemory(readFileSync25(join45(root, filename), "utf-8")) ?? [];
287895
+ return parseFixMemory(readFileSync26(join46(root, filename), "utf-8")) ?? [];
287671
287896
  } catch {
287672
287897
  }
287673
287898
  }
@@ -287733,10 +287958,10 @@ function inlineWaivers(content, file2) {
287733
287958
  ];
287734
287959
  }
287735
287960
  function readLearnings(tmpdir5) {
287736
- const path15 = join46(tmpdir5, LEARNINGS_FILE);
287737
- if (!existsSync12(path15)) return [];
287961
+ const path15 = join47(tmpdir5, LEARNINGS_FILE);
287962
+ if (!existsSync13(path15)) return [];
287738
287963
  try {
287739
- return readFileSync26(path15, "utf-8").split("\n");
287964
+ return readFileSync27(path15, "utf-8").split("\n");
287740
287965
  } catch {
287741
287966
  return [];
287742
287967
  }
@@ -287755,7 +287980,7 @@ function runAdvise(ctx, input2) {
287755
287980
  const learnings = readLearnings(ctx.tmpdir);
287756
287981
  const openConcerns = session.lastScanConcerns;
287757
287982
  const dossiers = files.map((file2) => {
287758
- const content = readTextCapped(join46(cwd, file2));
287983
+ const content = readTextCapped(join47(cwd, file2));
287759
287984
  const inline = content === null ? [] : inlineWaivers(content, file2);
287760
287985
  const reviewedInline = reviewedCopy(
287761
287986
  cwd,
@@ -289073,7 +289298,10 @@ function ConsolidationCandidatesTool(ctx) {
289073
289298
  parameters: ConsolidationCandidatesParams,
289074
289299
  execute: execute(async () => {
289075
289300
  const cwd = ctx.payload.cwd ?? process.cwd();
289076
- const result = findConsolidationGroups(readWorkingTree(cwd));
289301
+ const tree = readWorkingTree(cwd);
289302
+ const result = findConsolidationGroups(
289303
+ ctx.payload.refactorExamples ? tree : tree.filter((f) => !isExamplePath(f.path))
289304
+ );
289077
289305
  log2.info(
289078
289306
  `\xBB terraform_consolidation_candidates: ${result.groups.length} group(s), ${result.near_groups.length} near-group(s), ${result.declined_groups.length} declined`
289079
289307
  );
@@ -289090,8 +289318,8 @@ function ConsolidationCandidatesTool(ctx) {
289090
289318
  }
289091
289319
 
289092
289320
  // src/mcp/terraform/tools/detectPlanDrift.ts
289093
- import { readFileSync as readFileSync27 } from "node:fs";
289094
- import { join as join47 } from "node:path";
289321
+ import { readFileSync as readFileSync28 } from "node:fs";
289322
+ import { join as join48 } from "node:path";
289095
289323
 
289096
289324
  // src/mcp/terraform/cspm.ts
289097
289325
  var ExternalResource = type({
@@ -289509,7 +289737,7 @@ function readTfFiles(cwd) {
289509
289737
  const out = [];
289510
289738
  for (const file2 of walkTfFiles(cwd)) {
289511
289739
  try {
289512
- out.push({ file: file2, hcl: readFileSync27(join47(cwd, file2), "utf8") });
289740
+ out.push({ file: file2, hcl: readFileSync28(join48(cwd, file2), "utf8") });
289513
289741
  } catch {
289514
289742
  }
289515
289743
  }
@@ -289526,7 +289754,7 @@ function DetectPlanDriftTool(ctx) {
289526
289754
  const planPath = path15 || !envPlanPath ? resolveWithinCwd(cwd, path15 ?? "plan.json") : envPlanPath;
289527
289755
  let raw2;
289528
289756
  try {
289529
- raw2 = readFileSync27(planPath, "utf8");
289757
+ raw2 = readFileSync28(planPath, "utf8");
289530
289758
  } catch {
289531
289759
  return skipResult(
289532
289760
  "plan_artifact_not_found",
@@ -289658,8 +289886,8 @@ function DetectPlanDriftTool(ctx) {
289658
289886
  }
289659
289887
 
289660
289888
  // src/mcp/terraform/tools/emitCost.ts
289661
- import { readFileSync as readFileSync28, writeFileSync as writeFileSync14 } from "node:fs";
289662
- import { join as join48 } from "node:path";
289889
+ import { readFileSync as readFileSync29, writeFileSync as writeFileSync14 } from "node:fs";
289890
+ import { join as join49 } from "node:path";
289663
289891
 
289664
289892
  // src/mcp/terraform/awsPrices.ts
289665
289893
  var AWS_PRICE_REGION = "eu-west-2";
@@ -289983,7 +290211,7 @@ function readTfFiles2(cwd) {
289983
290211
  const out = [];
289984
290212
  for (const file2 of walkTfFiles(cwd)) {
289985
290213
  try {
289986
- out.push({ file: file2, hcl: readFileSync28(join48(cwd, file2), "utf8") });
290214
+ out.push({ file: file2, hcl: readFileSync29(join49(cwd, file2), "utf8") });
289987
290215
  } catch {
289988
290216
  }
289989
290217
  }
@@ -291247,7 +291475,7 @@ function TerraformEmitVexTool(ctx) {
291247
291475
  // src/mcp/terraform/tools/equivalenceCheck.ts
291248
291476
  import { mkdtempSync as mkdtempSync5, rmSync as rmSync7 } from "node:fs";
291249
291477
  import { tmpdir as tmpdir3 } from "node:os";
291250
- import { join as join49 } from "node:path";
291478
+ import { join as join50 } from "node:path";
291251
291479
 
291252
291480
  // src/mcp/terraform/refactor/attribution.ts
291253
291481
  import { posix as posix13 } from "node:path";
@@ -293039,12 +293267,12 @@ async function baselineValidateErrors(cwd, base, env2, session) {
293039
293267
  }
293040
293268
  const prefix2 = run("git", ["rev-parse", "--show-prefix"], cwd);
293041
293269
  if (prefix2.status !== 0) return null;
293042
- const tree = mkdtempSync5(join49(tmpdir3(), "iw-baseline-"));
293270
+ const tree = mkdtempSync5(join50(tmpdir3(), "iw-baseline-"));
293043
293271
  try {
293044
293272
  if (run("git", ["worktree", "add", "--detach", "--quiet", tree, base], cwd).status !== 0) {
293045
293273
  return null;
293046
293274
  }
293047
- const outcome2 = await scanValidate(join49(tree, prefix2.stdout.trim()), env2);
293275
+ const outcome2 = await scanValidate(join50(tree, prefix2.stdout.trim()), env2);
293048
293276
  if (!outcome2.ran || outcome2.unvalidated) return null;
293049
293277
  const errors = outcome2.concerns.map(validateErrorKey);
293050
293278
  session.baselineValidateErrors = { base, errors };
@@ -293145,7 +293373,7 @@ function TerraformGenerateMovedTool(ctx) {
293145
293373
 
293146
293374
  // src/mcp/terraform/cost.ts
293147
293375
  import { mkdtempSync as mkdtempSync6, rmSync as rmSync8 } from "node:fs";
293148
- import { join as join50 } from "node:path";
293376
+ import { join as join51 } from "node:path";
293149
293377
  function parseInfracostBreakdown(stdout) {
293150
293378
  const parsed2 = parseScannerJson(InfracostBreakdownOutput, stdout, "infracost");
293151
293379
  const raw2 = parsed2.totalMonthlyCost;
@@ -293227,15 +293455,15 @@ async function infracostBaseline(cwd, key, tmpdir5) {
293227
293455
  if (!baseRef) return null;
293228
293456
  const prefixResult = run("git", ["rev-parse", "--show-prefix"], cwd);
293229
293457
  const prefix2 = prefixResult.status === 0 ? prefixResult.stdout.trim() : "";
293230
- const baseDir = mkdtempSync6(join50(tmpdir5, "infracost-base-"));
293231
- const worktree = join50(baseDir, "wt");
293458
+ const baseDir = mkdtempSync6(join51(tmpdir5, "infracost-base-"));
293459
+ const worktree = join51(baseDir, "wt");
293232
293460
  const add = run("git", ["worktree", "add", "--detach", worktree, baseRef], cwd);
293233
293461
  if (add.status !== 0) {
293234
293462
  rmSync8(baseDir, { recursive: true, force: true });
293235
293463
  return null;
293236
293464
  }
293237
293465
  try {
293238
- const scanCwd = prefix2 ? join50(worktree, prefix2) : worktree;
293466
+ const scanCwd = prefix2 ? join51(worktree, prefix2) : worktree;
293239
293467
  const r = await runInfracostBreakdown(scanCwd, key);
293240
293468
  if (r.missing || r.status !== 0) return null;
293241
293469
  return parseInfracostBreakdown(r.stdout);
@@ -293305,8 +293533,8 @@ function InfracostDiffTool(ctx) {
293305
293533
  }
293306
293534
 
293307
293535
  // src/mcp/terraform/tools/ingestExternalFindings.ts
293308
- import { statSync as statSync7 } from "node:fs";
293309
- import { join as join51 } from "node:path";
293536
+ import { statSync as statSync8 } from "node:fs";
293537
+ import { join as join52 } from "node:path";
293310
293538
 
293311
293539
  // src/mcp/terraform/prowlerOcsf.ts
293312
293540
  var DETECTION_FINDING_CREATE = 200401;
@@ -293532,7 +293760,7 @@ var MAX_FEED_BYTES = 100 * 1024 * 1024;
293532
293760
  function readTfFiles3(cwd) {
293533
293761
  const out = [];
293534
293762
  for (const file2 of walkTfFiles(cwd)) {
293535
- const hcl2 = readTextCapped(join51(cwd, file2));
293763
+ const hcl2 = readTextCapped(join52(cwd, file2));
293536
293764
  if (hcl2 !== null) out.push({ file: file2, hcl: hcl2 });
293537
293765
  }
293538
293766
  return out;
@@ -293560,7 +293788,7 @@ function IngestExternalFindingsTool(ctx) {
293560
293788
  return unusableFeed("external_findings_outside_workspace", errorMessage(error63));
293561
293789
  }
293562
293790
  try {
293563
- const size = statSync7(feedPath).size;
293791
+ const size = statSync8(feedPath).size;
293564
293792
  if (size > MAX_FEED_BYTES) {
293565
293793
  return unusableFeed(
293566
293794
  "external_findings_too_large",
@@ -293689,8 +293917,8 @@ function IngestExternalFindingsTool(ctx) {
293689
293917
  }
293690
293918
 
293691
293919
  // src/mcp/terraform/tools/moduleDocs.ts
293692
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync16 } from "node:fs";
293693
- import { join as join52 } from "node:path";
293920
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync16 } from "node:fs";
293921
+ import { join as join53 } from "node:path";
293694
293922
 
293695
293923
  // src/mcp/terraform/moduleDocs.ts
293696
293924
  var VERDICT_CLAIM = {
@@ -293804,7 +294032,7 @@ function TerraformModuleDocsTool(ctx) {
293804
294032
  );
293805
294033
  }
293806
294034
  const absDir = resolveWithinCwd(cwd, module_dir);
293807
- const target = join52(absDir, "README.md");
294035
+ const target = join53(absDir, "README.md");
293808
294036
  let gitHead = null;
293809
294037
  try {
293810
294038
  gitHead = $("git", ["rev-parse", "HEAD"], { cwd, log: false });
@@ -293812,7 +294040,7 @@ function TerraformModuleDocsTool(ctx) {
293812
294040
  }
293813
294041
  let existing = null;
293814
294042
  try {
293815
- existing = readFileSync29(target, "utf8");
294043
+ existing = readFileSync30(target, "utf8");
293816
294044
  } catch {
293817
294045
  }
293818
294046
  const priorHead = readFooterGitHead(existing);
@@ -294116,12 +294344,15 @@ function NormalizationCandidatesTool(ctx) {
294116
294344
  parameters: NormalizationCandidatesParams,
294117
294345
  execute: execute(async () => {
294118
294346
  const cwd = ctx.payload.cwd ?? process.cwd();
294119
- const files = findNormalizationCandidates(cwd);
294347
+ const files = findNormalizationCandidates(cwd).filter(
294348
+ (f) => ctx.payload.refactorExamples || !isExamplePath(f.file)
294349
+ );
294120
294350
  const total = files.reduce((n, f) => n + f.count, 0);
294121
294351
  log2.info(
294122
294352
  `\xBB terraform_normalization_candidates: ${total} site(s) across ${files.length} file(s)`
294123
294353
  );
294124
294354
  return toolOk({
294355
+ ...reportOnly(libraryRepoReason(cwd)),
294125
294356
  file_count: files.length,
294126
294357
  total,
294127
294358
  files,
@@ -294133,7 +294364,7 @@ function NormalizationCandidatesTool(ctx) {
294133
294364
 
294134
294365
  // src/mcp/terraform/tools/plan.ts
294135
294366
  import { rmSync as rmSync9 } from "node:fs";
294136
- import { join as join53 } from "node:path";
294367
+ import { join as join54 } from "node:path";
294137
294368
  var PLAN_JSON_ARGS = ["plan", "-input=false", "-no-color", "-lock=false", "-json"];
294138
294369
  var planFileSeq = 0;
294139
294370
  async function capturePlanPairs(absDir, planFile, creds) {
@@ -294156,7 +294387,7 @@ async function planOneRoot(absDir, creds, tmpdir5) {
294156
294387
  skipReason: `terraform init failed: ${init.stderr.trim().slice(0, 200) || "unknown error"}`
294157
294388
  };
294158
294389
  }
294159
- const planFile = tmpdir5 ? join53(tmpdir5, `plan-${process.pid}-${planFileSeq++}.tfplan`) : void 0;
294390
+ const planFile = tmpdir5 ? join54(tmpdir5, `plan-${process.pid}-${planFileSeq++}.tfplan`) : void 0;
294160
294391
  const plan = await runTfAsync(
294161
294392
  planFile ? [...PLAN_JSON_ARGS, `-out=${planFile}`] : PLAN_JSON_ARGS,
294162
294393
  absDir,
@@ -294526,8 +294757,8 @@ function TerraformProviderUpgradeTool(ctx) {
294526
294757
  }
294527
294758
 
294528
294759
  // src/mcp/terraform/tfquery.ts
294529
- import { readFileSync as readFileSync30 } from "node:fs";
294530
- import { join as join54 } from "node:path";
294760
+ import { readFileSync as readFileSync31 } from "node:fs";
294761
+ import { join as join55 } from "node:path";
294531
294762
  var TFQUERY_SUFFIX = ".tfquery.hcl";
294532
294763
  function parseQueryFile(hcl2) {
294533
294764
  return parseBlocks(hcl2, ["list"]).map((block) => {
@@ -294553,7 +294784,7 @@ function collectQueryInventory(cwd) {
294553
294784
  for (const rel of walkFilesBySuffix(cwd, TFQUERY_SUFFIX)) {
294554
294785
  let text;
294555
294786
  try {
294556
- text = readFileSync30(join54(cwd, rel), "utf8");
294787
+ text = readFileSync31(join55(cwd, rel), "utf8");
294557
294788
  } catch {
294558
294789
  continue;
294559
294790
  }
@@ -294588,7 +294819,7 @@ function TerraformQueryInventoryTool(ctx) {
294588
294819
  }
294589
294820
 
294590
294821
  // src/mcp/terraform/tools/readFindings.ts
294591
- import { readFileSync as readFileSync31 } from "node:fs";
294822
+ import { readFileSync as readFileSync32 } from "node:fs";
294592
294823
  var ReadFindingsParams = type({
294593
294824
  "path?": type.string.describe(
294594
294825
  "path to the Assessor's findings.json. Defaults to $INFRAWEAVER_FINDINGS_PATH, then ./findings.json in the workspace."
@@ -294613,7 +294844,7 @@ function ReadFindingsTool(ctx) {
294613
294844
  const findingsPath = path15 || !envFindingsPath ? resolveWithinCwd(cwd, path15 ?? "findings.json") : envFindingsPath;
294614
294845
  let raw2;
294615
294846
  try {
294616
- raw2 = readFileSync31(findingsPath, "utf8");
294847
+ raw2 = readFileSync32(findingsPath, "utf8");
294617
294848
  } catch {
294618
294849
  return skipResult(
294619
294850
  "findings_not_found",
@@ -294872,6 +295103,8 @@ var SUBJECT = {
294872
295103
  // rule's silence a real statement. Whether git could answer is handled one
294873
295104
  // level up by the blind set: a run that could not ask is never conforming.
294874
295105
  "committed-state-file": ANY_BLOCK,
295106
+ // any block may hold a credential argument, a map of them, or a variable default
295107
+ "hardcoded-secret-argument": ANY_BLOCK,
294875
295108
  "deprecated-s3-bucket-argument": /(?:^|\n)\s*resource\s+"?aws_s3_bucket(?:"|\s)/,
294876
295109
  "superseded-resource-type": /(?:^|\n)\s*(?:resource\s+"?(?:aws_launch_configuration|aws_elb)(?:"|\s)|data\s+"?template_file(?:"|\s))/,
294877
295110
  "missing-required-version": /(?:^|\n)\s*(?:resource|module|provider)\s+"/,
@@ -298543,15 +298776,15 @@ async function startMcpHttpServer(ctx, options) {
298543
298776
 
298544
298777
  // src/mcp/terraform/policyGate.ts
298545
298778
  import {
298546
- existsSync as existsSync13,
298779
+ existsSync as existsSync14,
298547
298780
  mkdirSync as mkdirSync7,
298548
298781
  mkdtempSync as mkdtempSync7,
298549
- readdirSync as readdirSync8,
298550
- readFileSync as readFileSync33,
298782
+ readdirSync as readdirSync9,
298783
+ readFileSync as readFileSync34,
298551
298784
  writeFileSync as writeFileSync17
298552
298785
  } from "node:fs";
298553
298786
  import { tmpdir as osTmpdir } from "node:os";
298554
- import { dirname as dirname8, isAbsolute as isAbsolute6, join as join56, relative as relative6, resolve as resolve8 } from "node:path";
298787
+ import { dirname as dirname8, isAbsolute as isAbsolute6, join as join57, relative as relative6, resolve as resolve8 } from "node:path";
298555
298788
  function coercePolicyRules(raw2) {
298556
298789
  const rules = [];
298557
298790
  const rejected = [];
@@ -298579,7 +298812,7 @@ var UNKNOWN2 = /* @__PURE__ */ Symbol("unknown");
298579
298812
  function isScalar(v) {
298580
298813
  return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
298581
298814
  }
298582
- function literalValue(raw2) {
298815
+ function literalValue2(raw2) {
298583
298816
  const v = raw2.includes("\n") ? raw2.trim() : stripTrailingComment(raw2);
298584
298817
  if (v === "true") return true;
298585
298818
  if (v === "false") return false;
@@ -298612,7 +298845,7 @@ function objectLiteral(v) {
298612
298845
  for (const line of lines) {
298613
298846
  const m = OBJECT_LINE.exec(line);
298614
298847
  if (!m) return UNKNOWN2;
298615
- const value2 = literalValue(m[3] ?? "");
298848
+ const value2 = literalValue2(m[3] ?? "");
298616
298849
  if (value2 !== void 0) out[m[1] ?? m[2]] = value2;
298617
298850
  }
298618
298851
  return out;
@@ -298640,7 +298873,7 @@ function staticValues(body2) {
298640
298873
  continue;
298641
298874
  }
298642
298875
  if (e.kind === "attr") {
298643
- const value2 = literalValue(e.value ?? "");
298876
+ const value2 = literalValue2(e.value ?? "");
298644
298877
  if (value2 !== void 0) out[e.name] = value2;
298645
298878
  continue;
298646
298879
  }
@@ -298803,7 +299036,7 @@ function evaluateRulesStatically(cwd, rules, scope2 = { kind: "all", reason: ""
298803
299036
  const files = [];
298804
299037
  for (const rel of walk.files) {
298805
299038
  try {
298806
- files.push({ rel, text: readFileSync33(join56(cwd, rel), "utf-8") });
299039
+ files.push({ rel, text: readFileSync34(join57(cwd, rel), "utf-8") });
298807
299040
  } catch {
298808
299041
  }
298809
299042
  }
@@ -298918,16 +299151,16 @@ function cloudEmittedRego(rules) {
298918
299151
  }
298919
299152
  function readOrEmpty(path15) {
298920
299153
  try {
298921
- return readFileSync33(path15, "utf-8");
299154
+ return readFileSync34(path15, "utf-8");
298922
299155
  } catch {
298923
299156
  return "";
298924
299157
  }
298925
299158
  }
298926
299159
  function regoDirIn(root, skip) {
298927
299160
  for (const dir of DEFAULT_POLICY_DIRS) {
298928
- const abs = join56(root, dir);
298929
- if (existsSync13(abs) && walkFilesBySuffix(abs, ".rego").some(
298930
- (rel) => !skip(join56(dir, rel), readOrEmpty(join56(abs, rel)))
299161
+ const abs = join57(root, dir);
299162
+ if (existsSync14(abs) && walkFilesBySuffix(abs, ".rego").some(
299163
+ (rel) => !skip(join57(dir, rel), readOrEmpty(join57(abs, rel)))
298931
299164
  )) {
298932
299165
  return dir;
298933
299166
  }
@@ -298935,14 +299168,14 @@ function regoDirIn(root, skip) {
298935
299168
  return null;
298936
299169
  }
298937
299170
  function withoutSkipped(root, dir, skip, tmpdir5) {
298938
- const abs = join56(root, dir);
298939
- const files = readdirSync8(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile()).map((e) => relative6(abs, join56(e.parentPath, e.name)));
298940
- const kept = files.filter((rel) => !skip(join56(dir, rel), readOrEmpty(join56(abs, rel))));
299171
+ const abs = join57(root, dir);
299172
+ const files = readdirSync9(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile()).map((e) => relative6(abs, join57(e.parentPath, e.name)));
299173
+ const kept = files.filter((rel) => !skip(join57(dir, rel), readOrEmpty(join57(abs, rel))));
298941
299174
  if (kept.length === files.length) return abs;
298942
- const copy = join56(mkdtempSync7(join56(tmpdir5 ?? osTmpdir(), "policy-own-")), dir);
299175
+ const copy = join57(mkdtempSync7(join57(tmpdir5 ?? osTmpdir(), "policy-own-")), dir);
298943
299176
  for (const rel of kept) {
298944
- mkdirSync7(dirname8(join56(copy, rel)), { recursive: true });
298945
- writeFileSync17(join56(copy, rel), readFileSync33(join56(abs, rel)));
299177
+ mkdirSync7(dirname8(join57(copy, rel)), { recursive: true });
299178
+ writeFileSync17(join57(copy, rel), readFileSync34(join57(abs, rel)));
298946
299179
  }
298947
299180
  return copy;
298948
299181
  }
@@ -298964,18 +299197,18 @@ function resolveRepoPolicy(cwd, baseRef, tmpdir5, notes, skip) {
298964
299197
  }
298965
299198
  const own2 = tree.files.filter((f) => !skip(f.path, f.raw));
298966
299199
  if (!own2.some((f) => f.path.endsWith(".rego"))) continue;
298967
- const root = mkdtempSync7(join56(tmpdir5 ?? osTmpdir(), "policy-base-"));
299200
+ const root = mkdtempSync7(join57(tmpdir5 ?? osTmpdir(), "policy-base-"));
298968
299201
  for (const f of own2) {
298969
- mkdirSync7(dirname8(join56(root, f.path)), { recursive: true });
298970
- writeFileSync17(join56(root, f.path), f.raw);
299202
+ mkdirSync7(dirname8(join57(root, f.path)), { recursive: true });
299203
+ writeFileSync17(join57(root, f.path), f.raw);
298971
299204
  }
298972
299205
  const toml = readFileAtBaseRef(cwd, baseRef, CONFTEST_CONFIG);
298973
- if (toml.kind === "file") writeFileSync17(join56(root, CONFTEST_CONFIG), toml.raw);
298974
- const same = head === dir && readdirSync8(join56(cwd, dir), { recursive: true, withFileTypes: true }).filter(
299206
+ if (toml.kind === "file") writeFileSync17(join57(root, CONFTEST_CONFIG), toml.raw);
299207
+ const same = head === dir && readdirSync9(join57(cwd, dir), { recursive: true, withFileTypes: true }).filter(
298975
299208
  (e) => e.isFile()
298976
299209
  ).length === tree.files.length && tree.files.every((f) => {
298977
299210
  try {
298978
- return readFileSync33(join56(cwd, f.path), "utf-8") === f.raw;
299211
+ return readFileSync34(join57(cwd, f.path), "utf-8") === f.raw;
298979
299212
  } catch {
298980
299213
  return false;
298981
299214
  }
@@ -298985,7 +299218,7 @@ function resolveRepoPolicy(cwd, baseRef, tmpdir5, notes, skip) {
298985
299218
  `the Rego policy differs from the base ref (${tree.ref}) \u2014 applying the BASE ${dir}/ so this change cannot relax the policy reviewing it. It takes effect once merged.`
298986
299219
  );
298987
299220
  }
298988
- return { policyDir: join56(root, dir), conftestCwd: root };
299221
+ return { policyDir: join57(root, dir), conftestCwd: root };
298989
299222
  }
298990
299223
  if (head) {
298991
299224
  notes.push(
@@ -298995,7 +299228,7 @@ function resolveRepoPolicy(cwd, baseRef, tmpdir5, notes, skip) {
298995
299228
  return null;
298996
299229
  }
298997
299230
  function findWorkflowPlanJson(cwd, notes) {
298998
- const present = PLAN_JSON_NAMES.filter((name) => existsSync13(join56(cwd, name)));
299231
+ const present = PLAN_JSON_NAMES.filter((name) => existsSync14(join57(cwd, name)));
298999
299232
  if (present.length === 0) return null;
299000
299233
  const listing = run("git", ["ls-files", "-z"], cwd);
299001
299234
  if (listing.status !== 0) {
@@ -299012,7 +299245,7 @@ function findWorkflowPlanJson(cwd, notes) {
299012
299245
  );
299013
299246
  continue;
299014
299247
  }
299015
- return join56(cwd, name);
299248
+ return join57(cwd, name);
299016
299249
  }
299017
299250
  return null;
299018
299251
  }
@@ -299042,7 +299275,7 @@ function evaluatePolicyGate(cwd, rawRules, opts = {}) {
299042
299275
  let plan;
299043
299276
  if (planPath) {
299044
299277
  try {
299045
- plan = asPlan(JSON.parse(readFileSync33(planPath, "utf-8")));
299278
+ plan = asPlan(JSON.parse(readFileSync34(planPath, "utf-8")));
299046
299279
  } catch {
299047
299280
  }
299048
299281
  if (!plan) notes.push(`${planPath} is not a Terraform plan JSON \u2014 ignored`);
@@ -299145,7 +299378,7 @@ function capturePolicyGate(ctx) {
299145
299378
  if (!result) return;
299146
299379
  const workspace = process.env.GITHUB_WORKSPACE ?? cwd;
299147
299380
  for (const o of [...result.violations, ...result.inconclusive]) {
299148
- if (o.file) o.file = posix16(relative6(workspace, join56(cwd, o.file)));
299381
+ if (o.file) o.file = posix16(relative6(workspace, join57(cwd, o.file)));
299149
299382
  }
299150
299383
  session.policyGate = result;
299151
299384
  for (const n of result.notes) log2.warning(`\xBB policy gate: ${n}`);
@@ -299480,7 +299713,7 @@ var ThinkingTimer = class {
299480
299713
  // src/agents/claude.ts
299481
299714
  import { execFileSync as execFileSync3 } from "node:child_process";
299482
299715
  import { chmodSync as chmodSync3, mkdirSync as mkdirSync10, writeFileSync as writeFileSync22 } from "node:fs";
299483
- import { join as join65 } from "node:path";
299716
+ import { join as join66 } from "node:path";
299484
299717
  import { performance as performance6 } from "node:perf_hooks";
299485
299718
  import { setTimeout as sleep6 } from "node:timers/promises";
299486
299719
 
@@ -299840,7 +300073,7 @@ import { readFile as readFile2 } from "node:fs/promises";
299840
300073
 
299841
300074
  // src/agents/shared.ts
299842
300075
  import { execFileSync as execFileSync2 } from "node:child_process";
299843
- import { join as join57 } from "node:path";
300076
+ import { join as join58 } from "node:path";
299844
300077
  var MAX_STDERR_LINES = 20;
299845
300078
  var MAX_POST_RUN_RETRIES = 3;
299846
300079
  function getGitStatus() {
@@ -299877,7 +300110,7 @@ var agent = (input2) => {
299877
300110
  };
299878
300111
  };
299879
300112
  function agentHomeEnv(tmpdir5) {
299880
- return { HOME: tmpdir5, XDG_CONFIG_HOME: join57(tmpdir5, ".config") };
300113
+ return { HOME: tmpdir5, XDG_CONFIG_HOME: join58(tmpdir5, ".config") };
299881
300114
  }
299882
300115
  function baseAgentEnv(homeEnv, repoDir) {
299883
300116
  return { ...process.env, ...homeEnv, PWD: repoDir };
@@ -300353,6 +300586,12 @@ function shouldRunGraceTurn(params) {
300353
300586
  return params.stopReason === "quota" && params.hasReserve && params.canResume && params.unsubmitted !== null;
300354
300587
  }
300355
300588
 
300589
+ // src/agents/quotaTokens.ts
300590
+ var CACHE_READ_WEIGHT = 0.1;
300591
+ function quotaTokens(t) {
300592
+ return t.input + t.output + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT;
300593
+ }
300594
+
300356
300595
  // src/agents/sessionLabeler.ts
300357
300596
  var import_picocolors2 = __toESM(require_picocolors(), 1);
300358
300597
  var ORCHESTRATOR_LABEL = "orchestrator";
@@ -300674,7 +300913,7 @@ function matchesRouteSentinel(envVar, model) {
300674
300913
  import { randomUUID as randomUUID9 } from "node:crypto";
300675
300914
  import { mkdirSync as mkdirSync8, rmSync as rmSync10, writeFileSync as writeFileSync18 } from "node:fs";
300676
300915
  import { homedir } from "node:os";
300677
- import { join as join58 } from "node:path";
300916
+ import { join as join59 } from "node:path";
300678
300917
  var VERTEX_SERVICE_ACCOUNT_JSON_ENV = "VERTEX_SERVICE_ACCOUNT_JSON";
300679
300918
  var GOOGLE_APPLICATION_CREDENTIALS_ENV = "GOOGLE_APPLICATION_CREDENTIALS";
300680
300919
  var GOOGLE_CLOUD_PROJECT_ENV = "GOOGLE_CLOUD_PROJECT";
@@ -300698,7 +300937,7 @@ function readProjectIdFromVertexServiceAccountJson() {
300698
300937
  }
300699
300938
  function createSecretDir() {
300700
300939
  const base = process.env.INFRAWEAVER_SECRET_HOME || process.env.HOME || homedir();
300701
- const secretDir = join58(base, `.${PRODUCT_SLUG}`, "secrets", randomUUID9());
300940
+ const secretDir = join59(base, `.${PRODUCT_SLUG}`, "secrets", randomUUID9());
300702
300941
  mkdirSync8(secretDir, { recursive: true, mode: 448 });
300703
300942
  return secretDir;
300704
300943
  }
@@ -300707,7 +300946,7 @@ function materializeVertexCredentials(params) {
300707
300946
  const blob = process.env[VERTEX_SERVICE_ACCOUNT_JSON_ENV];
300708
300947
  if (!blob) return void 0;
300709
300948
  const secretDir = createSecretDir();
300710
- const credentialsPath = join58(secretDir, "vertex-sa.json");
300949
+ const credentialsPath = join59(secretDir, "vertex-sa.json");
300711
300950
  writeFileSync18(credentialsPath, blob, { mode: 384 });
300712
300951
  process.env[GOOGLE_APPLICATION_CREDENTIALS_ENV] = credentialsPath;
300713
300952
  const projectId = readProjectIdFromVertexServiceAccountJson();
@@ -300843,29 +301082,29 @@ function createProcessOutputActivityTimeout(ctx) {
300843
301082
  }
300844
301083
 
300845
301084
  // src/utils/prompt/skills.ts
300846
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync34, writeFileSync as writeFileSync19 } from "node:fs";
300847
- import { dirname as dirname9, join as join59 } from "node:path";
301085
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync35, writeFileSync as writeFileSync19 } from "node:fs";
301086
+ import { dirname as dirname9, join as join60 } from "node:path";
300848
301087
  import { fileURLToPath } from "node:url";
300849
301088
  var BUNDLED_SKILL_NAMES = ["terraform-best-practices"];
300850
301089
  function resolveSkillPath(name) {
300851
301090
  const here = dirname9(fileURLToPath(import.meta.url));
300852
301091
  const candidates = [
300853
- join59(here, "..", "..", "skills", name, "SKILL.md"),
300854
- join59(here, "skills", name, "SKILL.md")
301092
+ join60(here, "..", "..", "skills", name, "SKILL.md"),
301093
+ join60(here, "skills", name, "SKILL.md")
300855
301094
  ];
300856
301095
  for (const candidate of candidates) {
300857
- if (existsSync14(candidate)) return candidate;
301096
+ if (existsSync15(candidate)) return candidate;
300858
301097
  }
300859
301098
  throw new Error(`bundled skill not found: ${name} (looked in ${candidates.join(", ")})`);
300860
301099
  }
300861
301100
  var SKILL_TARGET_DIRS = [".opencode/skills", ".claude/skills", ".agents/skills"];
300862
301101
  function installBundledSkills(params) {
300863
301102
  for (const name of BUNDLED_SKILL_NAMES) {
300864
- const content = readFileSync34(resolveSkillPath(name), "utf8");
301103
+ const content = readFileSync35(resolveSkillPath(name), "utf8");
300865
301104
  for (const targetDir of SKILL_TARGET_DIRS) {
300866
- const skillDir = join59(params.home, targetDir, name);
301105
+ const skillDir = join60(params.home, targetDir, name);
300867
301106
  mkdirSync9(skillDir, { recursive: true });
300868
- writeFileSync19(join59(skillDir, "SKILL.md"), content);
301107
+ writeFileSync19(join60(skillDir, "SKILL.md"), content);
300869
301108
  }
300870
301109
  }
300871
301110
  log2.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
@@ -300874,8 +301113,8 @@ function installBundledSkills(params) {
300874
301113
  // src/utils/setup/install.ts
300875
301114
  import { spawnSync as spawnSync9 } from "node:child_process";
300876
301115
  import { createHash as createHash14 } from "node:crypto";
300877
- import { chmodSync as chmodSync2, createWriteStream as createWriteStream2, existsSync as existsSync17, readFileSync as readFileSync35 } from "node:fs";
300878
- import { join as join64 } from "node:path";
301116
+ import { chmodSync as chmodSync2, createWriteStream as createWriteStream2, existsSync as existsSync18, readFileSync as readFileSync36 } from "node:fs";
301117
+ import { join as join65 } from "node:path";
300879
301118
  import { pipeline as pipeline2 } from "node:stream/promises";
300880
301119
 
300881
301120
  // src/utils/setup/tarballCache.ts
@@ -300930,7 +301169,7 @@ function verifyTarballIntegrity(tarballPath, dist, descriptor) {
300930
301169
  `Refusing to install ${descriptor}: the registry published no integrity digest (neither dist.integrity nor dist.shasum), so the download cannot be verified.`
300931
301170
  );
300932
301171
  }
300933
- const bytes = readFileSync35(tarballPath);
301172
+ const bytes = readFileSync36(tarballPath);
300934
301173
  if (integrity2) {
300935
301174
  const candidates = integrity2.trim().split(/\s+/).filter(Boolean);
300936
301175
  for (const candidate of candidates) {
@@ -300959,9 +301198,9 @@ function verifyTarballIntegrity(tarballPath, dist, descriptor) {
300959
301198
  }
300960
301199
  async function installFromNpmTarball(params) {
300961
301200
  const tempDir = getTempDir();
300962
- const extractedDir = join64(tempDir, "package");
300963
- const cliPath = join64(extractedDir, params.executablePath);
300964
- if (existsSync17(cliPath)) {
301201
+ const extractedDir = join65(tempDir, "package");
301202
+ const cliPath = join65(extractedDir, params.executablePath);
301203
+ if (existsSync18(cliPath)) {
300965
301204
  log2.debug(`\xBB using cached binary at ${cliPath}`);
300966
301205
  return cliPath;
300967
301206
  }
@@ -301012,7 +301251,7 @@ async function installFromNpmTarball(params) {
301012
301251
  dist = (await manifestResponse.json()).dist;
301013
301252
  }
301014
301253
  log2.debug(`\xBB installing ${params.packageName}@${resolvedVersion}...`);
301015
- const tarballPath = join64(tempDir, "package.tgz");
301254
+ const tarballPath = join65(tempDir, "package.tgz");
301016
301255
  const tarballFileName = params.packageName.startsWith("@") ? params.packageName.split("/")[1] : params.packageName;
301017
301256
  const tarballUrl = `${npmRegistry}/${encodedName}/-/${tarballFileName}-${resolvedVersion}.tgz`;
301018
301257
  const restored = await restoreTarball({
@@ -301052,7 +301291,7 @@ async function installFromNpmTarball(params) {
301052
301291
  `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
301053
301292
  );
301054
301293
  }
301055
- if (!existsSync17(cliPath)) {
301294
+ if (!existsSync18(cliPath)) {
301056
301295
  throw new Error(`Executable not found in extracted package at ${cliPath}`);
301057
301296
  }
301058
301297
  if (params.installDependencies) {
@@ -301163,9 +301402,9 @@ var CLAUDE_DISALLOWED_TOOLS = CLAUDE_EXEC_TOOL_DENY_RULES.join(",");
301163
301402
  var INFRAWEAVER_MCP_AUTH_HEADER = `Bearer \${${MCP_SERVER_TOKEN_ENV}}`;
301164
301403
  var CLAUDE_MAX_CONCURRENT_SUBAGENTS_DEFAULT = "4";
301165
301404
  function writeMcpConfig(ctx) {
301166
- const configDir = join65(ctx.tmpdir, ".claude");
301405
+ const configDir = join66(ctx.tmpdir, ".claude");
301167
301406
  mkdirSync10(configDir, { recursive: true });
301168
- const configPath = join65(configDir, "mcp.json");
301407
+ const configPath = join66(configDir, "mcp.json");
301169
301408
  const terraformMcp = resolveTerraformMcp(ctx.payload);
301170
301409
  if (terraformMcp.kind === "docker_missing") log2.info(`\xBB ${terraformMcp.note}`);
301171
301410
  const opentofuMcp = resolveOpenTofuMcp(ctx.payload);
@@ -301197,13 +301436,13 @@ function writeMcpConfig(ctx) {
301197
301436
  return configPath;
301198
301437
  }
301199
301438
  function writePretoolGateAssets(ctx, stopHookPath) {
301200
- const scriptPath = join65(ctx.tmpdir, CLAUDE_PRETOOL_GATE_FILENAME);
301439
+ const scriptPath = join66(ctx.tmpdir, CLAUDE_PRETOOL_GATE_FILENAME);
301201
301440
  writeFileSync22(
301202
301441
  scriptPath,
301203
301442
  buildClaudePretoolGateSource(ctx.subagentDeniedTools, writePolicyPath2(ctx.tmpdir))
301204
301443
  );
301205
301444
  chmodSync3(scriptPath, 493);
301206
- const settingsPath = join65(ctx.tmpdir, "infraweaver-claude-settings.json");
301445
+ const settingsPath = join66(ctx.tmpdir, "infraweaver-claude-settings.json");
301207
301446
  const settings = buildClaudePretoolGateSettings(scriptPath, CLAUDE_EXEC_TOOL_DENY_RULES);
301208
301447
  settings.permissions.deny.push(...harnessTmpdirWriteDenyClaude(ctx.tmpdir));
301209
301448
  if (stopHookPath) {
@@ -301292,7 +301531,7 @@ function timerFor(state5, label) {
301292
301531
  function chargeTokenQuota(state5) {
301293
301532
  const quota = state5.params.tokenQuota;
301294
301533
  if (!quota || state5.stopRun.signal.aborted) return;
301295
- if (!quota.observe(state5.quotaSource, state5.accumulatedTokens)) return;
301534
+ if (!quota.observe(state5.quotaSource, quotaTokens(state5.accumulatedTokens))) return;
301296
301535
  log2.info(`\xBB ${state5.params.label}: ${quota.gap()}`);
301297
301536
  state5.stopRun.abort(new TokenQuotaExceededError(quota.gap()));
301298
301537
  }
@@ -301828,11 +302067,11 @@ var claude = agent({
301828
302067
  const onVertex = matchesRouteSentinel(VERTEX_MODEL_ID_ENV, specifier) && specifier !== void 0 && isVertexAnthropicId(specifier);
301829
302068
  const model = !specifier ? void 0 : onBedrock ? specifier : onVertex ? void 0 : stripProviderPrefix(specifier);
301830
302069
  const homeEnv = agentHomeEnv(ctx.tmpdir);
301831
- mkdirSync10(join65(homeEnv.XDG_CONFIG_HOME, "claude"), { recursive: true });
302070
+ mkdirSync10(join66(homeEnv.XDG_CONFIG_HOME, "claude"), { recursive: true });
301832
302071
  installBundledSkills({ home: homeEnv.HOME });
301833
302072
  const mcpConfigPath = writeMcpConfig(ctx);
301834
302073
  const effort = resolveEffort(model);
301835
- const stopHookPath = join65(ctx.tmpdir, "infraweaver-stop-hook.sh");
302074
+ const stopHookPath = join66(ctx.tmpdir, "infraweaver-stop-hook.sh");
301836
302075
  writeFileSync22(stopHookPath, buildStopHookScript(), { mode: 493 });
301837
302076
  const pretoolGate = writePretoolGateAssets(ctx, stopHookPath);
301838
302077
  installManagedSettings({
@@ -301998,7 +302237,7 @@ var claude = agent({
301998
302237
  init_core3();
301999
302238
  import { spawn as nodeSpawn2 } from "node:child_process";
302000
302239
  import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync24 } from "node:fs";
302001
- import { join as join67 } from "node:path";
302240
+ import { join as join68 } from "node:path";
302002
302241
  import { performance as performance7 } from "node:perf_hooks";
302003
302242
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
302004
302243
  import { setTimeout as sleep7 } from "node:timers/promises";
@@ -307681,7 +307920,7 @@ function autoSelectModel() {
307681
307920
  import { execFileSync as execFileSync4 } from "node:child_process";
307682
307921
  import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync23 } from "node:fs";
307683
307922
  import { homedir as homedir3, userInfo as userInfo2 } from "node:os";
307684
- import { join as join66 } from "node:path";
307923
+ import { join as join67 } from "node:path";
307685
307924
 
307686
307925
  // src/utils/agent/codexOAuth.ts
307687
307926
  init_webapi();
@@ -307735,8 +307974,8 @@ function installCodexAuth() {
307735
307974
  }
307736
307975
  const expiresMs = decodeJwtExpMs(body2.tokens.access_token) ?? 0;
307737
307976
  const xdgDataHome = resolveDataHome();
307738
- const opencodeDir = join66(xdgDataHome, "opencode");
307739
- const authPath = join66(opencodeDir, "auth.json");
307977
+ const opencodeDir = join67(xdgDataHome, "opencode");
307978
+ const authPath = join67(opencodeDir, "auth.json");
307740
307979
  const opencodeAuth = {
307741
307980
  openai: {
307742
307981
  type: "oauth",
@@ -307760,7 +307999,7 @@ function installCodexAuth() {
307760
307999
  };
307761
308000
  }
307762
308001
  function resolveDataHome() {
307763
- if (process.env.CI !== "true") return join66(homedir3(), ".local", "share");
308002
+ if (process.env.CI !== "true") return join67(homedir3(), ".local", "share");
307764
308003
  bootstrapInfraweaverDataDir();
307765
308004
  return INFRAWEAVER_DATA_DIR;
307766
308005
  }
@@ -308034,7 +308273,7 @@ function newTurn() {
308034
308273
  function chargeTokenQuota2(ctx) {
308035
308274
  const quota = ctx.tokenQuota;
308036
308275
  if (!quota || !ctx.currentTurn || ctx.tokenQuotaExceeded) return;
308037
- if (!quota.observe(ctx.currentTurn.quotaSource, ctx.currentTurn.tokens)) return;
308276
+ if (!quota.observe(ctx.currentTurn.quotaSource, quotaTokens(ctx.currentTurn.tokens))) return;
308038
308277
  ctx.tokenQuotaExceeded = true;
308039
308278
  log2.info(`\xBB ${ctx.label}: ${quota.gap()}`);
308040
308279
  ctx.abortRun?.(new TokenQuotaExceededError(quota.gap()));
@@ -308501,10 +308740,10 @@ var opencode = agent({
308501
308740
  const openAiCompatModel = resolveOpenAiCompatibleOpenCodeModel(rawModel);
308502
308741
  const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : vertexModel ?? openAiCompatModel ?? rawModel;
308503
308742
  const homeEnv = agentHomeEnv(ctx.tmpdir);
308504
- const opencodePluginDir = join67(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
308743
+ const opencodePluginDir = join68(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
308505
308744
  mkdirSync12(opencodePluginDir, { recursive: true });
308506
308745
  writeFileSync24(
308507
- join67(opencodePluginDir, INFRAWEAVER_OPENCODE_GATE_PLUGIN_FILENAME),
308746
+ join68(opencodePluginDir, INFRAWEAVER_OPENCODE_GATE_PLUGIN_FILENAME),
308508
308747
  buildOpencodeSubagentGateSource(ctx.subagentDeniedTools, writePolicyPath2(ctx.tmpdir))
308509
308748
  );
308510
308749
  installBundledSkills({ home: homeEnv.HOME });
@@ -309540,7 +309779,7 @@ async function fetchRunContext(params) {
309540
309779
  // src/utils/github/github.ts
309541
309780
  init_core3();
309542
309781
  import { rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
309543
- import { dirname as dirname12, join as join68 } from "node:path";
309782
+ import { dirname as dirname12, join as join69 } from "node:path";
309544
309783
 
309545
309784
  // node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
309546
309785
  function getUserAgent2() {
@@ -315356,7 +315595,7 @@ function getGitHubUsageSummary() {
315356
315595
  }
315357
315596
  async function writeGitHubUsageSummaryToFile(path15) {
315358
315597
  const summary2 = getGitHubUsageSummary();
315359
- const tmpPath = join68(dirname12(path15), `.usage-summary-${process.pid}.tmp`);
315598
+ const tmpPath = join69(dirname12(path15), `.usage-summary-${process.pid}.tmp`);
315360
315599
  await writeFile2(tmpPath, JSON.stringify(summary2));
315361
315600
  await rename2(tmpPath, path15);
315362
315601
  }
@@ -315901,7 +316140,7 @@ async function resolveModelAndAuth(params) {
315901
316140
  // src/utils/run/runLifecycle.ts
315902
316141
  init_core3();
315903
316142
  import { writeFile as writeFile5 } from "node:fs/promises";
315904
- import { join as join71, resolve as resolve10 } from "node:path";
316143
+ import { join as join72, resolve as resolve10 } from "node:path";
315905
316144
 
315906
316145
  // src/utils/cloud/cloudReport.ts
315907
316146
  init_core3();
@@ -316295,7 +316534,7 @@ async function reportToCloud(input2) {
316295
316534
 
316296
316535
  // src/utils/cloud/learnings.ts
316297
316536
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
316298
- import { dirname as dirname13, join as join69 } from "node:path";
316537
+ import { dirname as dirname13, join as join70 } from "node:path";
316299
316538
 
316300
316539
  // src/utils/cloud/learningsTruncate.ts
316301
316540
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -316312,7 +316551,7 @@ function truncateAtLineBoundary(body2, cap2) {
316312
316551
  // src/utils/cloud/learnings.ts
316313
316552
  var LEARNINGS_FILE_NAME = LEARNINGS_FILE;
316314
316553
  function learningsFilePath(tmpdir5) {
316315
- return join69(tmpdir5, LEARNINGS_FILE_NAME);
316554
+ return join70(tmpdir5, LEARNINGS_FILE_NAME);
316316
316555
  }
316317
316556
  async function seedLearningsFile(params) {
316318
316557
  const path15 = learningsFilePath(params.tmpdir);
@@ -316443,7 +316682,7 @@ ${ctx.error}` : ctx.error;
316443
316682
 
316444
316683
  // src/utils/github/prSummary.ts
316445
316684
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
316446
- import { dirname as dirname14, join as join70 } from "node:path";
316685
+ import { dirname as dirname14, join as join71 } from "node:path";
316447
316686
  var SUMMARY_FILE_NAME = SUMMARY_FILE;
316448
316687
  var SUMMARY_SCAFFOLD = `# PR summary
316449
316688
 
@@ -316453,7 +316692,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
316453
316692
  var MIN_SNAPSHOT_LENGTH = 60;
316454
316693
  var MAX_SNAPSHOT_LENGTH = 32768;
316455
316694
  function summaryFilePath(tmpdir5) {
316456
- return join70(tmpdir5, SUMMARY_FILE_NAME);
316695
+ return join71(tmpdir5, SUMMARY_FILE_NAME);
316457
316696
  }
316458
316697
  async function seedSummaryFile(params) {
316459
316698
  const path15 = summaryFilePath(params.tmpdir);
@@ -316987,7 +317226,7 @@ async function emitFindingsOutputs(toolState, requiredControls) {
316987
317226
  if (a.level === "error") error62(a.message, props);
316988
317227
  else warning(a.message, props);
316989
317228
  }
316990
- const sarifPath = join71(workspace, SARIF_FILE);
317229
+ const sarifPath = join72(workspace, SARIF_FILE);
316991
317230
  if (session.emittedSarifPath && resolve10(session.emittedSarifPath) === resolve10(sarifPath)) {
316992
317231
  log2.info(`\xBB findings output: replacing the agent-emitted ${SARIF_FILE} with the gate's`);
316993
317232
  }
@@ -317417,9 +317656,9 @@ async function runAgentWithWatchdogs(params) {
317417
317656
  }
317418
317657
 
317419
317658
  // src/phases/startToolPlane.ts
317420
- import { existsSync as existsSync18, readdirSync as readdirSync9 } from "node:fs";
317659
+ import { existsSync as existsSync19, readdirSync as readdirSync10 } from "node:fs";
317421
317660
  import { readFile as readFile5 } from "node:fs/promises";
317422
- import { join as join72 } from "node:path";
317661
+ import { join as join73 } from "node:path";
317423
317662
 
317424
317663
  // src/utils/cloud/runTerminalStatus.ts
317425
317664
  var TERMINAL_STATE_KEY = "run_terminal";
@@ -318247,8 +318486,8 @@ ${instructions.user}` : null,
318247
318486
  log2.info(instructions.full);
318248
318487
  });
318249
318488
  if (toolContext.agentId === "opencode") {
318250
- const pluginDir = join72(process.cwd(), ".opencode", "plugin");
318251
- const hasPlugins = existsSync18(pluginDir) && readdirSync9(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
318489
+ const pluginDir = join73(process.cwd(), ".opencode", "plugin");
318490
+ const hasPlugins = existsSync19(pluginDir) && readdirSync10(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
318252
318491
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
318253
318492
  log2.info(
318254
318493
  "\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"