infraweaver 0.3.12 → 0.3.13

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 (52) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/opencode.d.ts +6 -0
  3. package/dist/agents/quotaTokens.d.ts +6 -0
  4. package/dist/cli.mjs +554 -199
  5. package/dist/index.js +531 -176
  6. package/dist/internal.js +4 -4
  7. package/dist/mcp/git.d.ts +6 -4
  8. package/dist/mcp/localContext.d.ts +7 -0
  9. package/dist/mcp/reviewCommentLimit.d.ts +55 -5
  10. package/dist/mcp/reviewMarkers.d.ts +5 -0
  11. package/dist/mcp/staleFix.d.ts +36 -1
  12. package/dist/mcp/terraform/concernResult.d.ts +9 -0
  13. package/dist/mcp/terraform/deltaSummary.d.ts +3 -0
  14. package/dist/mcp/terraform/hardcodedSecrets.d.ts +24 -10
  15. package/dist/mcp/terraform/refactorScope.d.ts +14 -2
  16. package/dist/mcp/terraform/ruleContext.d.ts +3 -0
  17. package/dist/mcp/terraform/tree.d.ts +5 -0
  18. package/dist/mcp/terraform/types.d.ts +2 -1
  19. package/dist/mcp/terraform/verifySites.d.ts +11 -3
  20. package/dist/vendor/@infraweaver-io/models/tokenQuota.d.ts +7 -6
  21. package/package.json +2 -2
  22. package/src/agents/opencode.ts +8 -1
  23. package/src/agents/quotaTokens.ts +7 -1
  24. package/src/mcp/git.ts +64 -12
  25. package/src/mcp/localContext.ts +6 -1
  26. package/src/mcp/moduleExtraction.ts +5 -4
  27. package/src/mcp/review.ts +52 -3
  28. package/src/mcp/reviewCommentLimit.ts +169 -26
  29. package/src/mcp/reviewMarkers.ts +11 -0
  30. package/src/mcp/reviewProvenance.ts +1 -1
  31. package/src/mcp/staleFix.ts +73 -2
  32. package/src/mcp/terraform/concernResult.ts +17 -10
  33. package/src/mcp/terraform/deltaSummary.ts +4 -0
  34. package/src/mcp/terraform/hardcodedSecrets.ts +148 -27
  35. package/src/mcp/terraform/nativeRuleScanners.ts +2 -1
  36. package/src/mcp/terraform/nativeScan.ts +4 -0
  37. package/src/mcp/terraform/refactor/equivalence.ts +42 -9
  38. package/src/mcp/terraform/refactorScope.ts +90 -21
  39. package/src/mcp/terraform/ruleContext.ts +4 -0
  40. package/src/mcp/terraform/scanDelta.ts +67 -26
  41. package/src/mcp/terraform/tools/detectPlanDrift.ts +2 -0
  42. package/src/mcp/terraform/tools/ingestExternalFindings.ts +2 -0
  43. package/src/mcp/terraform/tools/normalizationCandidates.ts +5 -5
  44. package/src/mcp/terraform/tools/readFindings.ts +2 -0
  45. package/src/mcp/terraform/tools/scan.ts +3 -0
  46. package/src/mcp/terraform/tree.ts +8 -0
  47. package/src/mcp/terraform/types.ts +2 -1
  48. package/src/mcp/terraform/verifySites.ts +23 -5
  49. package/src/modes/refactor.ts +1 -1
  50. package/src/modes/remediate-and-refactor.ts +1 -1
  51. package/src/modes/remediate.ts +1 -1
  52. package/src/phases/runAgentWithWatchdogs.ts +3 -2
package/dist/cli.mjs CHANGED
@@ -90400,23 +90400,23 @@ var require_helpers = __commonJS({
90400
90400
  "node_modules/.pnpm/ip-address@10.7.3/node_modules/ip-address/dist/v6/helpers.js"(exports2) {
90401
90401
  "use strict";
90402
90402
  Object.defineProperty(exports2, "__esModule", { value: true });
90403
- exports2.escapeHtml = escapeHtml;
90403
+ exports2.escapeHtml = escapeHtml2;
90404
90404
  exports2.spanAllZeroes = spanAllZeroes;
90405
90405
  exports2.spanAll = spanAll;
90406
90406
  exports2.spanLeadingZeroes = spanLeadingZeroes;
90407
90407
  exports2.simpleGroup = simpleGroup;
90408
- function escapeHtml(s) {
90408
+ function escapeHtml2(s) {
90409
90409
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
90410
90410
  }
90411
90411
  function spanAllZeroes(s) {
90412
- return escapeHtml(s).replace(/(0+)/g, '<span class="zero">$1</span>');
90412
+ return escapeHtml2(s).replace(/(0+)/g, '<span class="zero">$1</span>');
90413
90413
  }
90414
90414
  function spanAll(s, offset = 0) {
90415
90415
  const letters = s.split("");
90416
- return letters.map((n, i) => `<span class="digit value-${escapeHtml(n)} position-${i + offset}">${spanAllZeroes(n)}</span>`).join("");
90416
+ return letters.map((n, i) => `<span class="digit value-${escapeHtml2(n)} position-${i + offset}">${spanAllZeroes(n)}</span>`).join("");
90417
90417
  }
90418
90418
  function spanLeadingZeroesSimple(group2) {
90419
- return escapeHtml(group2).replace(/^(0+)/, '<span class="zero">$1</span>');
90419
+ return escapeHtml2(group2).replace(/^(0+)/, '<span class="zero">$1</span>');
90420
90420
  }
90421
90421
  function spanLeadingZeroes(address) {
90422
90422
  const groups2 = address.split(":");
@@ -160536,7 +160536,7 @@ var init_cacheTwirpClient = __esm({
160536
160536
  });
160537
160537
 
160538
160538
  // node_modules/.pnpm/@actions+cache@6.3.0_supports-color@7.2.0/node_modules/@actions/cache/lib/internal/tar.js
160539
- import { existsSync as existsSync19, writeFileSync as writeFileSync21 } from "fs";
160539
+ import { existsSync as existsSync18, writeFileSync as writeFileSync21 } from "fs";
160540
160540
  import * as path13 from "path";
160541
160541
  function getTarPath() {
160542
160542
  return __awaiter18(this, void 0, void 0, function* () {
@@ -160546,7 +160546,7 @@ function getTarPath() {
160546
160546
  const systemTar = SystemTarPathOnWindows;
160547
160547
  if (gnuTar) {
160548
160548
  return { path: gnuTar, type: ArchiveToolType.GNU };
160549
- } else if (existsSync19(systemTar)) {
160549
+ } else if (existsSync18(systemTar)) {
160550
160550
  return { path: systemTar, type: ArchiveToolType.BSD };
160551
160551
  }
160552
160552
  break;
@@ -191031,7 +191031,7 @@ var BOT_EMAIL = `${BOT_LOGIN}@users.noreply.github.com`;
191031
191031
  var OIDC_AUDIENCE = `${PRODUCT_SLUG}-api`;
191032
191032
 
191033
191033
  // src/commands/assess.ts
191034
- import { existsSync as existsSync10, statSync as statSync8 } from "node:fs";
191034
+ import { existsSync as existsSync9, statSync as statSync8 } from "node:fs";
191035
191035
  import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
191036
191036
  var import_arg = __toESM(require_arg(), 1);
191037
191037
 
@@ -199939,7 +199939,7 @@ var log = {
199939
199939
  debug: (...args2) => current.debug(...args2)
199940
199940
  };
199941
199941
 
199942
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/usage.js
199942
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/usage.js
199943
199943
  function formatCostUsd(costUsd) {
199944
199944
  return costUsd.toFixed(4);
199945
199945
  }
@@ -200490,7 +200490,7 @@ import { randomBytes } from "node:crypto";
200490
200490
  // package.json
200491
200491
  var package_default = {
200492
200492
  name: "infraweaver",
200493
- version: "0.3.12",
200493
+ version: "0.3.13",
200494
200494
  description: "GitHub Action that remediates Terraform to best practices and opens one scoped pull request per concern.",
200495
200495
  keywords: [
200496
200496
  "ai-coding-agent",
@@ -200593,7 +200593,7 @@ var package_default = {
200593
200593
  "@infraweaver-io/ci": "^0.1.8",
200594
200594
  "@infraweaver-io/core": "^0.1.5",
200595
200595
  "@infraweaver-io/crosswalk": "^1.2.0",
200596
- "@infraweaver-io/models": "^0.1.6",
200596
+ "@infraweaver-io/models": "^0.1.7",
200597
200597
  "@infraweaver-io/policy-spec": "^1.2.0",
200598
200598
  "@infraweaver-io/report-contract": "^0.1.5",
200599
200599
  "@infraweaver-io/sandbox": "^0.1.8",
@@ -207495,7 +207495,18 @@ var SECRET_WORDS = /* @__PURE__ */ new Set([
207495
207495
  "credential",
207496
207496
  "credentials"
207497
207497
  ]);
207498
- var SECRET_KEY_PREFIXES = /* @__PURE__ */ new Set(["api", "secret", "private", "signing"]);
207498
+ var SECRET_KEY_PREFIXES = /* @__PURE__ */ new Set([
207499
+ "api",
207500
+ "secret",
207501
+ "private",
207502
+ "signing",
207503
+ "access",
207504
+ "account",
207505
+ "client",
207506
+ "shared",
207507
+ "preshared",
207508
+ "master"
207509
+ ]);
207499
207510
  var NOT_SECRET_TOKENS = /* @__PURE__ */ new Set(["creation", "idempotency", "client", "request", "continuation"]);
207500
207511
  function words(name) {
207501
207512
  return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_-]+/).filter(Boolean);
@@ -207507,24 +207518,70 @@ function isSecretName(name) {
207507
207518
  if (last === void 0) return false;
207508
207519
  if (last === "key") return prev !== void 0 && SECRET_KEY_PREFIXES.has(prev);
207509
207520
  if (last === "token" && prev !== void 0 && NOT_SECRET_TOKENS.has(prev)) return false;
207521
+ if (last === "string") return prev === "connection";
207522
+ if (last === "secret" && w.length === 1) return false;
207510
207523
  return SECRET_WORDS.has(last);
207511
207524
  }
207512
207525
  function isPlaceholder(inner) {
207513
- return inner.trim() === "" || /^[*xX.]+$/.test(inner) || /^<[^>]*>$/.test(inner) || // the dummy credentials offline plans and LocalStack are given (`secret_key = "test"`)
207514
- /^(?:test|mock|fake|dummy|fixture|example|placeholder|sample)(?:[_-].*)?$/i.test(inner) || // a token's lifetime unit: `token_validity_units { access_token = "hours" }`
207526
+ return inner.trim() === "" || /^[*xX.]+$/.test(inner) || /^<[^>]*>$/.test(inner) || /^(?:true|false|null)$/.test(inner) || /^-?\d+(?:\.\d+)?$/.test(inner) || // a template directive is computed, like an interpolation: `"%{ if … }…%{ endif }"`
207527
+ inner.includes("%{") || // the dummy credentials offline plans and LocalStack are given (`secret_key = "test"`,
207528
+ // `"example-secret"`) — but only when that is the whole value: a lowercase tail at
207529
+ // most, so `"Test-Pr0dDbPassw0rd!"` is a password that happens to start with a word
207530
+ isDummy(inner) || // a token's lifetime unit: `token_validity_units { access_token = "hours" }`
207515
207531
  /^(?:seconds|minutes|hours|days)$/.test(inner);
207516
207532
  }
207517
- var HEADER = /^(resource|data|provider|module|locals|variable|output|terraform)\b([^{]*)\{\s*$/;
207533
+ function isDummy(inner) {
207534
+ const m = /^(?:test|mock|fake|dummy|fixture|example|placeholder|sample)([_-].*)?$/i.exec(inner);
207535
+ return m !== null && (m[1] === void 0 || /^[_-][a-z0-9_-]*$/.test(m[1]));
207536
+ }
207537
+ function isSecretReference(inner) {
207538
+ return inner.startsWith("arn:") || /^projects\/[^/]+\/secrets\//.test(inner) || /^https:\/\/[\w-]+\.vault\.azure\.net\/secrets\//i.test(inner) || /^\/[\w./-]+$/.test(inner);
207539
+ }
207540
+ var HEADER = /^(resource|data|provider|module|locals|variable|output|terraform)\b([^{]*)\{(.*)$/;
207518
207541
  var ASSIGNMENT = /^\s*(?:"([A-Za-z_][\w-]*)"|([A-Za-z_][\w-]*))\s*=(?!=)\s*(.*)$/;
207519
207542
  var HEREDOC = /<<-?([A-Za-z_]\w*)\s*$/;
207520
207543
  var INLINE_PAIR = /(?:"([A-Za-z_][\w-]*)"|\b([A-Za-z_][\w-]*))\s*=(?!=)\s*("(?:[^"\\]|\\.)*")/g;
207521
- function literalValue(raw2) {
207544
+ function literalValue(name, raw2) {
207522
207545
  const value2 = stripTrailingComment(raw2).replace(/,$/, "").trim();
207523
- return isStringLiteral(value2) && !isPlaceholder(value2.slice(1, -1));
207546
+ if (!isStringLiteral(value2)) return false;
207547
+ const inner = value2.slice(1, -1);
207548
+ if (isPlaceholder(inner) || isSecretReference(inner)) return false;
207549
+ return !(words(name).at(-1) === "auth" && /^[A-Z]+(?:_[A-Z]+)*$/.test(inner));
207550
+ }
207551
+ function variableName(holder) {
207552
+ return /^variable\s+"?([\w-]+)"?/.exec(holder)?.[1] ?? "";
207553
+ }
207554
+ function opensBlockComment(line) {
207555
+ for (let i = 0; i < line.length; i++) {
207556
+ const span = skipNonCode(line, i);
207557
+ if (!span) continue;
207558
+ if (line.startsWith("/*", i) && !line.includes("*/", i + 2)) return true;
207559
+ i = span.end;
207560
+ }
207561
+ return false;
207562
+ }
207563
+ function closesOnLine(rest) {
207564
+ let depth = 1;
207565
+ for (let i = 0; i < rest.length; i++) {
207566
+ const span = skipNonCode(rest, i);
207567
+ if (span) {
207568
+ i = span.end;
207569
+ continue;
207570
+ }
207571
+ if (rest[i] === "{") depth++;
207572
+ else if (rest[i] === "}" && --depth === 0) return true;
207573
+ }
207574
+ return false;
207524
207575
  }
207525
207576
  function findHardcodedSecrets(hcl2) {
207577
+ return findInLines(hcl2, null);
207578
+ }
207579
+ function findTfvarsSecrets(hcl2, path15) {
207580
+ return findInLines(hcl2, `tfvars input ${path15}`);
207581
+ }
207582
+ function findInLines(hcl2, fileHolder) {
207526
207583
  const out = [];
207527
- let holder = null;
207584
+ let holder = fileHolder;
207528
207585
  let heredoc = null;
207529
207586
  let inComment = false;
207530
207587
  const lines = hcl2.split("\n");
@@ -207544,12 +207601,28 @@ function findHardcodedSecrets(hcl2) {
207544
207601
  continue;
207545
207602
  }
207546
207603
  if (trimmed.startsWith("#") || trimmed.startsWith("//")) continue;
207547
- const header = HEADER.exec(line);
207604
+ if (opensBlockComment(line)) inComment = true;
207605
+ const header = fileHolder === null ? HEADER.exec(line) : null;
207548
207606
  if (header) {
207549
207607
  holder = `${header[1]}${header[2].trimEnd() ? ` ${header[2].trim()}` : ""}`;
207608
+ const rest = header[3];
207609
+ const opens2 = HEREDOC.exec(rest);
207610
+ if (opens2) heredoc = opens2[1];
207611
+ if (!holder.startsWith("output ") && !holder.startsWith("terraform")) {
207612
+ for (const pair of rest.matchAll(INLINE_PAIR)) {
207613
+ const key = pair[1] ?? pair[2];
207614
+ const isVariable = holder.startsWith("variable ");
207615
+ if (isVariable && key !== "default") continue;
207616
+ const name2 = isVariable ? variableName(holder) : key;
207617
+ if (isSecretName(name2) && literalValue(name2, pair[3])) {
207618
+ out.push({ name: name2, holder, line: i + 1 });
207619
+ }
207620
+ }
207621
+ }
207622
+ if (closesOnLine(rest)) holder = null;
207550
207623
  continue;
207551
207624
  }
207552
- if (line.startsWith("}")) {
207625
+ if (fileHolder === null && line.startsWith("}")) {
207553
207626
  holder = null;
207554
207627
  continue;
207555
207628
  }
@@ -207561,30 +207634,34 @@ function findHardcodedSecrets(hcl2) {
207561
207634
  const name = m[1] ?? m[2];
207562
207635
  const value2 = m[3];
207563
207636
  if (holder.startsWith("variable ")) {
207564
- const variable = /^variable\s+"?([\w-]+)"?/.exec(holder)?.[1] ?? "";
207565
- if (name === "default" && isSecretName(variable) && literalValue(value2)) {
207637
+ const variable = variableName(holder);
207638
+ if (name === "default" && isSecretName(variable) && literalValue(variable, value2)) {
207566
207639
  out.push({ name: variable, holder, line: i + 1 });
207567
207640
  }
207568
207641
  continue;
207569
207642
  }
207570
207643
  if (holder.startsWith("output ") || holder.startsWith("terraform")) continue;
207571
- if (isSecretName(name) && literalValue(value2)) {
207644
+ if (isSecretName(name) && literalValue(name, value2)) {
207572
207645
  out.push({ name, holder, line: i + 1 });
207573
207646
  continue;
207574
207647
  }
207575
207648
  for (const pair of value2.matchAll(INLINE_PAIR)) {
207576
207649
  const key = pair[1] ?? pair[2];
207577
- if (isSecretName(key) && literalValue(pair[3])) {
207650
+ if (isSecretName(key) && literalValue(key, pair[3])) {
207578
207651
  out.push({ name: key, holder, line: i + 1 });
207579
207652
  }
207580
207653
  }
207581
207654
  }
207582
207655
  return out;
207583
207656
  }
207584
- function scanHardcodedSecrets(files) {
207657
+ function scanHardcodedSecrets(files, tfvarsFiles = []) {
207585
207658
  const concerns = [];
207586
- for (const { path: path15, hcl: hcl2 } of files) {
207587
- for (const hit of findHardcodedSecrets(hcl2)) {
207659
+ const hits = [
207660
+ ...files.map((f) => ({ path: f.path, found: findHardcodedSecrets(f.hcl) })),
207661
+ ...tfvarsFiles.map((f) => ({ path: f.path, found: findTfvarsSecrets(f.hcl, f.path) }))
207662
+ ];
207663
+ for (const { path: path15, found } of hits) {
207664
+ for (const hit of found) {
207588
207665
  concerns.push(
207589
207666
  buildConcern({
207590
207667
  source: NATIVE_SOURCE,
@@ -209587,7 +209664,8 @@ var NATIVE_RULE_SCANNERS = [
209587
209664
  name: "hardcoded-secrets",
209588
209665
  rules: [HARDCODED_SECRET_ARGUMENT],
209589
209666
  // Per file: the argument's name and its literal are both on the line itself.
209590
- scan: (ctx) => ({ concerns: scanHardcodedSecrets(ctx.files) })
209667
+ // The only rule that reads `.tfvars`: an input file holds values, not config.
209668
+ scan: (ctx) => ({ concerns: scanHardcodedSecrets(ctx.files, ctx.tfvarsFiles) })
209591
209669
  },
209592
209670
  {
209593
209671
  name: "write-only",
@@ -209672,6 +209750,7 @@ function createRuleContext(input2) {
209672
209750
  cwd: input2.cwd,
209673
209751
  files: input2.files,
209674
209752
  tofuFiles: input2.tofuFiles ?? [],
209753
+ tfvarsFiles: input2.tfvarsFiles ?? [],
209675
209754
  truncated: input2.truncated ?? null,
209676
209755
  repeatedLiteralThreshold: input2.repeatedLiteralThreshold
209677
209756
  };
@@ -209723,6 +209802,9 @@ function readTfTree(cwd, limits) {
209723
209802
  function readTofuTree(cwd, limits) {
209724
209803
  return readTree(cwd, ".tofu", limits);
209725
209804
  }
209805
+ function readTfvarsTree(cwd, limits) {
209806
+ return readTree(cwd, ".tfvars", limits);
209807
+ }
209726
209808
  var OVERSIZE_NAMED = 5;
209727
209809
  function describeOversize(paths) {
209728
209810
  const named = paths.slice(0, OVERSIZE_NAMED).join(", ");
@@ -209751,6 +209833,9 @@ function scanNative(cwd, opts = {}) {
209751
209833
  // walk over a suffix most repos do not use at all, and threading it through
209752
209834
  // the context keeps the scanners uniform.
209753
209835
  tofuFiles: readTofuTree(cwd).files,
209836
+ // likewise only for the hardcoded-secret rule: a value kept out of the `.tf`
209837
+ // source is most often committed here instead
209838
+ tfvarsFiles: readTfvarsTree(cwd).files,
209754
209839
  truncated: tree.truncated,
209755
209840
  repeatedLiteralThreshold: opts.repeatedLiteralThreshold ?? DEFAULT_REPEAT_THRESHOLD
209756
209841
  });
@@ -271571,7 +271656,7 @@ var FastMCP = class extends FastMCPEventEmitter {
271571
271656
  }
271572
271657
  };
271573
271658
 
271574
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/catalog.js
271659
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/catalog.js
271575
271660
  function provider(config3) {
271576
271661
  return config3;
271577
271662
  }
@@ -272932,12 +273017,47 @@ function readTfDir(cwd, dir) {
272932
273017
  }
272933
273018
 
272934
273019
  // src/mcp/terraform/refactorScope.ts
272935
- import { existsSync as existsSync6, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync5 } from "node:fs";
273020
+ import { readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync5 } from "node:fs";
272936
273021
  import { join as join22 } from "node:path";
272937
273022
  function isExamplePath(path15) {
272938
- return /(?:^|\/)examples?\//.test(path15.replace(/\\/g, "/"));
273023
+ return /(?:^|\/)examples\/|^example\//i.test(path15.replace(/\\/g, "/"));
273024
+ }
273025
+ var PROVIDER_OR_BACKEND = new RegExp(
273026
+ String.raw`(?:^|\n)\s*(?:provider|backend)\s+${BLOCK_LABEL}\s*\{`
273027
+ );
273028
+ var CLOUD_BLOCK = /(?:^|\n)\s*cloud\s*\{/;
273029
+ function configuresDeployment(name, text) {
273030
+ if (name.endsWith(".tf.json")) {
273031
+ let doc;
273032
+ try {
273033
+ doc = JSON.parse(text);
273034
+ } catch {
273035
+ return false;
273036
+ }
273037
+ if (typeof doc !== "object" || doc === null) return false;
273038
+ const { provider: provider2, terraform } = doc;
273039
+ if (provider2 !== void 0) return true;
273040
+ return [terraform].flat().some((t) => typeof t === "object" && t !== null && ("backend" in t || "cloud" in t));
273041
+ }
273042
+ if (PROVIDER_OR_BACKEND.test(text)) return true;
273043
+ return parseBlocks(text, ["terraform"]).some((b) => CLOUD_BLOCK.test(b.body));
273044
+ }
273045
+ var isTerraformFile2 = (name) => name.endsWith(".tf") || name.endsWith(".tf.json");
273046
+ function deploysFrom(dir) {
273047
+ let names;
273048
+ try {
273049
+ names = readdirSync6(dir).filter(isTerraformFile2);
273050
+ } catch {
273051
+ return false;
273052
+ }
273053
+ return names.some((name) => {
273054
+ try {
273055
+ return configuresDeployment(name, readFileSync12(join22(dir, name), "utf8"));
273056
+ } catch {
273057
+ return false;
273058
+ }
273059
+ });
272939
273060
  }
272940
- var PROVIDER_OR_BACKEND = /^\s*(?:provider\s+"|backend\s+")/m;
272941
273061
  function holdsTerraform(dir, depth) {
272942
273062
  let entries;
272943
273063
  try {
@@ -272955,26 +273075,35 @@ function holdsTerraform(dir, depth) {
272955
273075
  return false;
272956
273076
  }
272957
273077
  function libraryRepoReason(cwd) {
272958
- let rootFiles;
273078
+ let rootEntries;
272959
273079
  try {
272960
- rootFiles = readdirSync6(cwd).filter((f) => f.endsWith(".tf"));
273080
+ rootEntries = readdirSync6(cwd);
272961
273081
  } catch {
272962
273082
  return null;
272963
273083
  }
272964
- if (rootFiles.length === 0) return null;
272965
- for (const f of rootFiles) {
272966
- let text;
272967
- try {
272968
- text = readFileSync12(join22(cwd, f), "utf8");
272969
- } catch {
272970
- continue;
272971
- }
272972
- if (PROVIDER_OR_BACKEND.test(text)) return null;
272973
- }
272974
- const examples = ["examples", "example"].find((d) => existsSync6(join22(cwd, d)));
273084
+ if (!rootEntries.some(isTerraformFile2) || deploysFrom(cwd)) return null;
273085
+ const examples = rootEntries.find((d) => /^examples?$/i.test(d));
272975
273086
  if (examples === void 0 || !holdsTerraform(join22(cwd, examples), 2)) return null;
272976
273087
  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`;
272977
273088
  }
273089
+ function libraryScope(cwd) {
273090
+ const reason = libraryRepoReason(cwd);
273091
+ if (reason === null) return () => null;
273092
+ const deploys = /* @__PURE__ */ new Map();
273093
+ return (file2) => {
273094
+ const dirs = file2.replace(/\\/g, "/").split("/").slice(0, -1);
273095
+ for (let i = dirs.length; i > 0; i--) {
273096
+ const dir = dirs.slice(0, i).join("/");
273097
+ let d = deploys.get(dir);
273098
+ if (d === void 0) {
273099
+ d = deploysFrom(join22(cwd, dir));
273100
+ deploys.set(dir, d);
273101
+ }
273102
+ if (d) return null;
273103
+ }
273104
+ return reason;
273105
+ };
273106
+ }
272978
273107
  function reportOnly(reason) {
272979
273108
  return reason === null ? {} : { report_only: true, report_only_reason: reason };
272980
273109
  }
@@ -273332,12 +273461,12 @@ function ModuleExtractionCandidatesTool(ctx) {
273332
273461
  parameters: ModuleExtractionCandidatesParams,
273333
273462
  execute: execute(async () => {
273334
273463
  const cwd = ctx.payload.cwd ?? process.cwd();
273464
+ const library = libraryScope(cwd);
273335
273465
  const candidates = findExtractionCandidates(
273336
273466
  cwd,
273337
273467
  ctx.payload.moduleCatalogue,
273338
273468
  ctx.toolState.orgModuleHcl
273339
- ).filter((c) => ctx.payload.refactorExamples || !isExamplePath(c.cluster.file));
273340
- const library = libraryRepoReason(cwd);
273469
+ ).filter((c) => ctx.payload.refactorExamples || !isExamplePath(c.cluster.file)).map((c) => ({ ...reportOnly(library(c.cluster.file)), ...c }));
273341
273470
  const matched = candidates.filter((c) => c.candidates.length > 0);
273342
273471
  const reasons = {};
273343
273472
  for (const c of candidates)
@@ -273351,7 +273480,6 @@ function ModuleExtractionCandidatesTool(ctx) {
273351
273480
  `\xBB module_extraction_candidates: ${candidates.length} cluster(s), ${matched.length} with a module match` + (partialInterfaces.length > 0 ? `, ${partialInterfaces.length} partial-interface block(s)` : "")
273352
273481
  );
273353
273482
  return toolOk({
273354
- ...reportOnly(library),
273355
273483
  cluster_count: candidates.length,
273356
273484
  matched_count: matched.length,
273357
273485
  candidates,
@@ -273375,7 +273503,7 @@ function ModuleExtractionCandidatesTool(ctx) {
273375
273503
  }
273376
273504
 
273377
273505
  // src/mcp/moduleTests.ts
273378
- import { existsSync as existsSync7, readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
273506
+ import { existsSync as existsSync6, readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync6 } from "node:fs";
273379
273507
  import { join as join24 } from "node:path";
273380
273508
  var MODULE_META_ARGS = /* @__PURE__ */ new Set([
273381
273509
  "source",
@@ -273496,7 +273624,7 @@ function resolveSourceDir(fromDir, source) {
273496
273624
  }
273497
273625
  function isDir(p) {
273498
273626
  try {
273499
- return existsSync7(p) && statSync6(p).isDirectory();
273627
+ return existsSync6(p) && statSync6(p).isDirectory();
273500
273628
  } catch {
273501
273629
  return false;
273502
273630
  }
@@ -273630,7 +273758,7 @@ function TerraformModuleTestsTool(ctx) {
273630
273758
  }
273631
273759
 
273632
273760
  // src/mcp/terraform/tools/advise.ts
273633
- import { existsSync as existsSync8, readFileSync as readFileSync16 } from "node:fs";
273761
+ import { existsSync as existsSync7, readFileSync as readFileSync16 } from "node:fs";
273634
273762
  import { join as join26 } from "node:path";
273635
273763
 
273636
273764
  // src/mcp/terraform/fixMemory.ts
@@ -273789,7 +273917,7 @@ function inlineWaivers(content, file2) {
273789
273917
  }
273790
273918
  function readLearnings(tmpdir6) {
273791
273919
  const path15 = join26(tmpdir6, LEARNINGS_FILE);
273792
- if (!existsSync8(path15)) return [];
273920
+ if (!existsSync7(path15)) return [];
273793
273921
  try {
273794
273922
  return readFileSync16(path15, "utf-8").split("\n");
273795
273923
  } catch {
@@ -275151,6 +275279,9 @@ function ConsolidationCandidatesTool(ctx) {
275151
275279
  import { readFileSync as readFileSync18 } from "node:fs";
275152
275280
  import { join as join28 } from "node:path";
275153
275281
 
275282
+ // src/mcp/staleFix.ts
275283
+ import { createHash as createHash6 } from "node:crypto";
275284
+
275154
275285
  // node_modules/.pnpm/@infraweaver-io+ci@0.1.8/node_modules/@infraweaver-io/ci/dist/progressComment.js
275155
275286
  function parseProgressComment(raw2) {
275156
275287
  if (!raw2?.id)
@@ -276206,7 +276337,31 @@ function groupIdFromBranch(branch) {
276206
276337
  }
276207
276338
  function remediationBranchSuffix(base, defaultBranch) {
276208
276339
  if (!base || !defaultBranch || base === defaultBranch) return "";
276209
- return `--${base.replace(/[^A-Za-z0-9._-]+/g, "-")}`;
276340
+ const slug2 = base.replace(/[^A-Za-z0-9._-]+/g, "-");
276341
+ if (slug2 === base) return `--${slug2}`;
276342
+ return `--${slug2}-${createHash6("sha1").update(base).digest("hex").slice(0, 8)}`;
276343
+ }
276344
+ function remediationBranchFor(id, base, defaultBranch, open3) {
276345
+ const computed = `remediate/${id}${remediationBranchSuffix(base, defaultBranch)}`;
276346
+ const heads = (open3 ?? []).filter((pr) => pr.base === base && groupIdFromBranch(pr.head) === id).map((pr) => pr.head).filter((head) => !(open3 ?? []).some((pr) => pr.head === head && pr.base !== base));
276347
+ return heads.includes(computed) ? computed : heads[0] ?? computed;
276348
+ }
276349
+ async function listOpenRemediationPrs(ctx) {
276350
+ const { octokit, repo } = ctx;
276351
+ if (ctx.toolState?.selectedMode !== "Remediate") return [];
276352
+ if (!octokit || !repo?.owner || !repo.name) return [];
276353
+ try {
276354
+ const open3 = await octokit.paginate(octokit.rest.pulls.list, {
276355
+ owner: repo.owner,
276356
+ repo: repo.name,
276357
+ state: "open",
276358
+ per_page: 100
276359
+ });
276360
+ return open3.filter((pr) => pr.head.repo?.id === pr.base.repo.id && groupIdFromBranch(pr.head.ref)).map((pr) => ({ head: pr.head.ref, base: pr.base.ref }));
276361
+ } catch (e) {
276362
+ log2.warning(`\xBB could not list open remediation PRs: ${errorMessage(e)}`);
276363
+ return [];
276364
+ }
276210
276365
  }
276211
276366
  function matchesBotIdentity(identity) {
276212
276367
  const normalized = identity.replace(/\[bot\]$/i, "").toLowerCase();
@@ -276919,15 +277074,17 @@ function siteOf(c, read3) {
276919
277074
  const text = read3(c.location.file);
276920
277075
  return text === null ? null : resourceAddressAt(text, c.location.line);
276921
277076
  }
276922
- function recordConcernSites(session, concerns, cwd) {
277077
+ function recordConcernSites(session, concerns, cwd, linesMatchTree) {
276923
277078
  const read3 = fileReader(cwd);
276924
277079
  session.siteById ??= /* @__PURE__ */ new Map();
276925
277080
  session.firstSitesByKey ??= /* @__PURE__ */ new Map();
276926
277081
  const first = session.firstSitesByKey;
276927
277082
  const fresh = /* @__PURE__ */ new Map();
276928
277083
  for (const c of concerns) {
276929
- const site = siteOf(c, read3);
276930
- session.siteById.set(c.id, site);
277084
+ const site = linesMatchTree ? siteOf(c, read3) : null;
277085
+ const prior = session.siteById.get(c.id);
277086
+ session.siteById.set(c.id, prior === void 0 || prior === site ? site : null);
277087
+ if (!linesMatchTree) continue;
276931
277088
  const key = concernKeyOf(c);
276932
277089
  if (first.has(key)) continue;
276933
277090
  const sites = fresh.get(key) ?? /* @__PURE__ */ new Set();
@@ -276983,7 +277140,7 @@ function buildConcernResult(ctx, rawConcerns, opts) {
276983
277140
  fullBaseline.map((c) => concernKeyOf(c))
276984
277141
  );
276985
277142
  recordConcernKeys(session, fullBaseline);
276986
- recordConcernSites(session, fullBaseline, opts.cwd);
277143
+ recordConcernSites(session, fullBaseline, opts.cwd, opts.linesMatchTree ?? false);
276987
277144
  session.baselineFmtDriftFiles = union3(
276988
277145
  session.baselineFmtDriftFiles,
276989
277146
  fullBaseline.filter((c) => c.source === "terraform-fmt").map((c) => c.location.file)
@@ -276998,13 +277155,13 @@ function buildConcernResult(ctx, rawConcerns, opts) {
276998
277155
  active,
276999
277156
  opts.autonomyThreshold
277000
277157
  );
277001
- const suffix2 = remediationBranchSuffix(
277002
- remediationBaseOf(ctx),
277003
- ctx.repo?.data.default_branch?.trim() || void 0
277004
- );
277005
- const groups2 = (opts.priority ? prioritizeGroups(annotatedGroups, active, opts.priority) : annotatedGroups).map((g) => ({ ...g, branch: `remediate/${g.id}${suffix2}` }));
277158
+ const base = remediationBaseOf(ctx);
277159
+ const defaultBranch = ctx.repo?.data.default_branch?.trim() || void 0;
277160
+ const branchFor = (id) => remediationBranchFor(id, base, defaultBranch, opts.openRemediationPrs);
277161
+ const groups2 = (opts.priority ? prioritizeGroups(annotatedGroups, active, opts.priority) : annotatedGroups).map((g) => ({ ...g, branch: branchFor(g.id) }));
277006
277162
  const planned = planBatches(groups2);
277007
- const batchPlan = planned.batch_branch ? { ...planned, batch_branch: `${planned.batch_branch}${suffix2}` } : planned;
277163
+ const batchId = planned.batch_branch ? groupIdFromBranch(planned.batch_branch) : null;
277164
+ const batchPlan = batchId ? { ...planned, batch_branch: branchFor(batchId) } : planned;
277008
277165
  const bySeverity = {};
277009
277166
  for (const c of active) bySeverity[c.severity] = (bySeverity[c.severity] ?? 0) + 1;
277010
277167
  return {
@@ -277521,7 +277678,8 @@ function DetectPlanDriftTool(ctx) {
277521
277678
  autonomyThreshold,
277522
277679
  // drift is a narrow concern source — join any prior scan baseline so a
277523
277680
  // later ✗→✓ verify doesn't read the whole workspace as regressions.
277524
- baselineMode: "merge"
277681
+ baselineMode: "merge",
277682
+ openRemediationPrs: await listOpenRemediationPrs(ctx)
277525
277683
  });
277526
277684
  log2.info(
277527
277685
  `\xBB detect_plan_drift: ${active.length} drifted resource(s) \u2265 ${threshold} from ${planPath}` + (noiseFiltered.length ? `, ${noiseFiltered.length} noise-filtered` : "") + (unlocated.length ? `, ${unlocated.length} unlocated` : "") + (statefulForgotten.length ? `, ${statefulForgotten.length} STATEFUL forget(s)` : "") + (suppressed.length ? ` (${suppressed.length} suppressed)` : "")
@@ -277592,13 +277750,13 @@ import { readFileSync as readFileSync19, rmSync as rmSync2 } from "node:fs";
277592
277750
  import { join as join29 } from "node:path";
277593
277751
 
277594
277752
  // src/mcp/terraform/emittedArtefacts.ts
277595
- import { createHash as createHash6 } from "node:crypto";
277753
+ import { createHash as createHash7 } from "node:crypto";
277596
277754
  import { writeFileSync as writeFileSync3 } from "node:fs";
277597
277755
  import { resolve as resolve5 } from "node:path";
277598
277756
  var ARTEFACT_DIR = "compliance/";
277599
277757
  function gitBlobId(bytes) {
277600
277758
  const content = Buffer.from(bytes, "utf8");
277601
- return createHash6("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
277759
+ return createHash7("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
277602
277760
  }
277603
277761
  function writeEmittedArtefact(toolState, path15, bytes) {
277604
277762
  writeFileSync3(path15, bytes);
@@ -278795,7 +278953,7 @@ function buildSuppressionSummary(waivers) {
278795
278953
  }
278796
278954
 
278797
278955
  // node_modules/.pnpm/@infraweaver-io+attest@0.1.7_supports-color@7.2.0/node_modules/@infraweaver-io/attest/dist/signing.js
278798
- import { createHash as createHash7, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify } from "node:crypto";
278956
+ import { createHash as createHash8, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify } from "node:crypto";
278799
278957
  var SIGNATURE_ALGORITHM = "Ed25519";
278800
278958
  var DEFAULT_KEY_LABEL = "the signing key";
278801
278959
  function loadSigningKey(raw2, options) {
@@ -278822,7 +278980,7 @@ function loadSigningKey(raw2, options) {
278822
278980
  return {
278823
278981
  privateKey,
278824
278982
  publicKeyPem,
278825
- fingerprint: createHash7("sha256").update(spkiDer).digest("hex")
278983
+ fingerprint: createHash8("sha256").update(spkiDer).digest("hex")
278826
278984
  };
278827
278985
  }
278828
278986
  function signBytes(bytes, key) {
@@ -278843,7 +279001,7 @@ function publicKeyFingerprint(publicKeyPem) {
278843
279001
  type: "spki",
278844
279002
  format: "der"
278845
279003
  });
278846
- return createHash7("sha256").update(der).digest("hex");
279004
+ return createHash8("sha256").update(der).digest("hex");
278847
279005
  }
278848
279006
  function signatureMetadata(key, sigFile) {
278849
279007
  return {
@@ -279073,7 +279231,7 @@ function buildEvidenceBundle(args2) {
279073
279231
  // node_modules/.pnpm/@infraweaver-io+attest@0.1.7_supports-color@7.2.0/node_modules/@infraweaver-io/attest/dist/keyless.js
279074
279232
  var import_bundle = __toESM(require_dist6(), 1);
279075
279233
  var import_sign = __toESM(require_dist9(), 1);
279076
- import { createHash as createHash8 } from "node:crypto";
279234
+ import { createHash as createHash9 } from "node:crypto";
279077
279235
 
279078
279236
  // node_modules/.pnpm/@infraweaver-io+attest@0.1.7_supports-color@7.2.0/node_modules/@infraweaver-io/attest/dist/keylessOidc.js
279079
279237
  var SIGSTORE_OIDC_AUDIENCE = "sigstore";
@@ -279102,7 +279260,7 @@ function keylessSigningPossible(env2, envName) {
279102
279260
  }
279103
279261
  function buildInTotoStatement(args2) {
279104
279262
  const data = typeof args2.artifactBytes === "string" ? Buffer.from(args2.artifactBytes, "utf8") : args2.artifactBytes;
279105
- const sha2562 = createHash8("sha256").update(data).digest("hex");
279263
+ const sha2562 = createHash9("sha256").update(data).digest("hex");
279106
279264
  return {
279107
279265
  _type: "https://in-toto.io/Statement/v1",
279108
279266
  subject: [{ name: args2.subjectName, digest: { sha256: sha2562 } }],
@@ -279312,12 +279470,12 @@ import { mkdirSync as mkdirSync4 } from "node:fs";
279312
279470
  import { dirname as dirname6 } from "node:path";
279313
279471
 
279314
279472
  // src/mcp/terraform/oscal.ts
279315
- import { createHash as createHash9 } from "node:crypto";
279473
+ import { createHash as createHash10 } from "node:crypto";
279316
279474
  var OSCAL_VERSION = "1.1.2";
279317
279475
  var INDICATIVE_REMARK = "Indicative, crosswalk-derived assessment from a deterministic starter rule-pack \u2014 NOT a formal OSCAL assessment against an authored assessment plan. Statuses are code-verified only; absence of a finding is not proof of compliance.";
279318
279476
  var NS = "https://infraweaver.io/ns/oscal";
279319
279477
  function deterministicUuid(seed) {
279320
- const hex4 = createHash9("sha256").update(seed).digest("hex").slice(0, 32).split("");
279478
+ const hex4 = createHash10("sha256").update(seed).digest("hex").slice(0, 32).split("");
279321
279479
  hex4[12] = "5";
279322
279480
  hex4[16] = (Number.parseInt(hex4[16] ?? "0", 16) & 3 | 8).toString(16);
279323
279481
  const u = hex4.join("");
@@ -279456,7 +279614,7 @@ function TerraformEmitOscalTool(ctx) {
279456
279614
  import { writeFileSync as writeFileSync5 } from "node:fs";
279457
279615
 
279458
279616
  // src/mcp/terraform/lineage.ts
279459
- import { createHash as createHash10 } from "node:crypto";
279617
+ import { createHash as createHash11 } from "node:crypto";
279460
279618
  import { readFileSync as readFileSync21 } from "node:fs";
279461
279619
  import { join as join31 } from "node:path";
279462
279620
  function repoFileReader(cwd) {
@@ -279469,7 +279627,7 @@ function repoFileReader(cwd) {
279469
279627
  };
279470
279628
  }
279471
279629
  function lineageKeyOf(c, address) {
279472
- return createHash10("sha1").update(`${c.source}|${bareRuleOf(c.source, c.rule_id)}|${c.location.file}|${address ?? ""}`).digest("hex").slice(0, 16);
279630
+ return createHash11("sha1").update(`${c.source}|${bareRuleOf(c.source, c.rule_id)}|${c.location.file}|${address ?? ""}`).digest("hex").slice(0, 16);
279473
279631
  }
279474
279632
  function lineageResolver(read3) {
279475
279633
  const files = /* @__PURE__ */ new Map();
@@ -280511,16 +280669,32 @@ function uncoveredInstanceKeys(p) {
280511
280669
  if (expected === null) return null;
280512
280670
  const kind = level[0].kind;
280513
280671
  const written = (k) => kind === "count" ? k : `"${k}"`;
280672
+ const fromAt = level[0].at;
280673
+ const toAt = p.after.multiplicity[0]?.at;
280674
+ if (fromAt === void 0 || toAt === void 0) return null;
280514
280675
  const declared2 = /* @__PURE__ */ new Set();
280515
280676
  for (const blk of p.blocks) {
280677
+ if (stripIndices(normaliseAddress(blk.from)) !== p.before.address || stripIndices(normaliseAddress(blk.to)) !== p.after.address)
280678
+ return null;
280516
280679
  const from = instanceKeysOf(blk.from);
280517
280680
  const to = instanceKeysOf(blk.to);
280518
280681
  if (from.length > 1 || to.length > 1) return null;
280519
280682
  if (from.length === 0 && to.length === 0) return null;
280520
- if (from.length === 1 && to.length === 1 && from[0] === to[0]) declared2.add(from[0]);
280683
+ if (from.length !== 1 || to.length !== 1 || from[0] !== to[0]) continue;
280684
+ const fromPlaced = keyAtLevel(blk.from, fromAt);
280685
+ const toPlaced = keyAtLevel(blk.to, toAt);
280686
+ if (fromPlaced === false || toPlaced === false) continue;
280687
+ if (fromPlaced === null || toPlaced === null) return null;
280688
+ declared2.add(from[0]);
280521
280689
  }
280522
280690
  return expected.filter((k) => !declared2.has(written(k)));
280523
280691
  }
280692
+ function keyAtLevel(address, at) {
280693
+ const onResource = instanceKeysOf(stripModuleHops(normaliseAddress(address))).length > 0;
280694
+ if (at === "resource") return onResource;
280695
+ if (onResource) return false;
280696
+ return moduleHops(address).length === 1 ? true : null;
280697
+ }
280524
280698
  function computeEquivalence(before, after, movedBlocks, ambiguous = [], planPairs, opts = {}) {
280525
280699
  const reasons = [];
280526
280700
  const corr = correspond(before, after, movedBlocks);
@@ -282447,7 +282621,8 @@ function IngestExternalFindingsTool(ctx) {
282447
282621
  // an external feed is a narrow concern source — join any prior scan
282448
282622
  // baseline so a later ✗→✓ verify doesn't read the workspace as regressions.
282449
282623
  baselineMode: "merge",
282450
- seedCaps: seedCapsFrom(input2)
282624
+ seedCaps: seedCapsFrom(input2),
282625
+ openRemediationPrs: await listOpenRemediationPrs(ctx)
282451
282626
  });
282452
282627
  const cappedBlock = cappedSummary(capped);
282453
282628
  log2.info(
@@ -282917,15 +283092,13 @@ function NormalizationCandidatesTool(ctx) {
282917
283092
  parameters: NormalizationCandidatesParams,
282918
283093
  execute: execute(async () => {
282919
283094
  const cwd = ctx.payload.cwd ?? process.cwd();
282920
- const files = findNormalizationCandidates(cwd).filter(
282921
- (f) => ctx.payload.refactorExamples || !isExamplePath(f.file)
282922
- );
283095
+ const library = libraryScope(cwd);
283096
+ const files = findNormalizationCandidates(cwd).filter((f) => ctx.payload.refactorExamples || !isExamplePath(f.file)).map((f) => ({ ...reportOnly(library(f.file)), ...f }));
282923
283097
  const total = files.reduce((n, f) => n + f.count, 0);
282924
283098
  log2.info(
282925
283099
  `\xBB terraform_normalization_candidates: ${total} site(s) across ${files.length} file(s)`
282926
283100
  );
282927
283101
  return toolOk({
282928
- ...reportOnly(libraryRepoReason(cwd)),
282929
283102
  file_count: files.length,
282930
283103
  total,
282931
283104
  files,
@@ -283460,7 +283633,8 @@ function ReadFindingsTool(ctx) {
283460
283633
  minRank,
283461
283634
  grouping,
283462
283635
  autonomyThreshold,
283463
- seedCaps: seedCapsFrom(input2)
283636
+ seedCaps: seedCapsFrom(input2),
283637
+ openRemediationPrs: await listOpenRemediationPrs(ctx)
283464
283638
  });
283465
283639
  const cappedBlock = cappedSummary(capped);
283466
283640
  log2.info(
@@ -283639,23 +283813,54 @@ function computeScanDelta(input2) {
283639
283813
  };
283640
283814
  }
283641
283815
  function markPossibleRenames(introduced, resolved, headAddresses) {
283642
- const vanished = resolved.filter((r) => {
283643
- if (r.address === null) return false;
283644
- return !headAddresses(dirOf3(r.concern.location.file)).has(r.address);
283645
- });
283816
+ const byResource = (findings) => {
283817
+ const out = /* @__PURE__ */ new Map();
283818
+ for (const f of [...findings].sort((a, b) => byLocation(a.concern, b.concern))) {
283819
+ if (f.address === null) continue;
283820
+ const dir = dirOf3(f.concern.location.file);
283821
+ const key = `${dir}|${f.address}`;
283822
+ const entry4 = out.get(key);
283823
+ if (entry4) entry4.findings.push(f);
283824
+ else out.set(key, { dir, address: f.address, findings: [f] });
283825
+ }
283826
+ return [...out.values()];
283827
+ };
283828
+ const vanished = byResource(resolved).filter((r) => !headAddresses(r.dir).has(r.address));
283829
+ const appeared = byResource(introduced);
283830
+ const shared = (from, to) => {
283831
+ const left = /* @__PURE__ */ new Map();
283832
+ for (const f of from.findings)
283833
+ left.set(ruleKey(f.concern), (left.get(ruleKey(f.concern)) ?? 0) + 1);
283834
+ return to.findings.filter((f) => {
283835
+ const n = left.get(ruleKey(f.concern)) ?? 0;
283836
+ if (n === 0) return false;
283837
+ left.set(ruleKey(f.concern), n - 1);
283838
+ return true;
283839
+ });
283840
+ };
283841
+ const order2 = (a, b) => a.dir.localeCompare(b.dir) || a.address.localeCompare(b.address);
283842
+ const candidates = vanished.flatMap(
283843
+ (from) => appeared.filter(
283844
+ (to) => (
283845
+ // a `moved` block cannot change a resource's type
283846
+ typeOf(from.address) === typeOf(to.address) && // renamed in place, or moved elsewhere under its own name
283847
+ from.dir === to.dir !== (from.address === to.address)
283848
+ )
283849
+ ).map((to) => ({ from, to, findings: shared(from, to) })).filter((p) => p.findings.length > 0)
283850
+ );
283851
+ candidates.sort(
283852
+ (a, b) => b.findings.length - a.findings.length || order2(a.from, b.from) || order2(a.to, b.to)
283853
+ );
283646
283854
  const used = /* @__PURE__ */ new Set();
283647
- for (const finding of introduced) {
283648
- if (finding.address === null) continue;
283649
- const dir = dirOf3(finding.concern.location.file);
283650
- const match5 = vanished.find(
283651
- (r) => !used.has(r) && ruleKey(r.concern) === ruleKey(finding.concern) && // a `moved` block cannot change a resource's type
283652
- typeOf(r.address) === typeOf(finding.address) && // renamed in place, or moved elsewhere under its own name
283653
- dirOf3(r.concern.location.file) === dir !== (r.address === finding.address)
283654
- );
283655
- if (!match5) continue;
283656
- used.add(match5);
283657
- finding.possiblyPreexisting = true;
283658
- finding.renamedFrom = { address: match5.address, file: match5.concern.location.file };
283855
+ for (const { from, to, findings } of candidates) {
283856
+ if (used.has(from) || used.has(to)) continue;
283857
+ used.add(from);
283858
+ used.add(to);
283859
+ const file2 = from.findings[0]?.concern.location.file;
283860
+ for (const finding of findings) {
283861
+ finding.possiblyPreexisting = true;
283862
+ finding.renamedFrom = { address: from.address, file: file2 };
283863
+ }
283659
283864
  }
283660
283865
  }
283661
283866
  function typeOf(address) {
@@ -283698,7 +283903,8 @@ function deltaVerdict(delta, requiredControls) {
283698
283903
  return {
283699
283904
  pass: gate.pass && required3.length === 0,
283700
283905
  gating: gate.gating.length,
283701
- required: required3.length
283906
+ required: required3.length,
283907
+ blocking: (/* @__PURE__ */ new Set([...gate.gating, ...required3])).size
283702
283908
  };
283703
283909
  }
283704
283910
  function failsGate(finding, requiredControls) {
@@ -283991,7 +284197,9 @@ async function runScan(ctx, {
283991
284197
  grouping,
283992
284198
  autonomyThreshold,
283993
284199
  inScope,
283994
- priority
284200
+ priority,
284201
+ openRemediationPrs: await listOpenRemediationPrs(ctx),
284202
+ linesMatchTree: true
283995
284203
  }
283996
284204
  );
283997
284205
  const { active, suppressed, groups: groups2, batchPlan, bySeverity: by_severity } = result;
@@ -284520,8 +284728,8 @@ function TerraformStandardsReportTool(ctx) {
284520
284728
  }
284521
284729
 
284522
284730
  // src/mcp/terraform/tools/tag.ts
284523
- import { createHash as createHash11 } from "node:crypto";
284524
- import { existsSync as existsSync9, readdirSync as readdirSync8, readFileSync as readFileSync26, realpathSync as realpathSync2, rmSync as rmSync7 } from "node:fs";
284731
+ import { createHash as createHash12 } from "node:crypto";
284732
+ import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync26, realpathSync as realpathSync2, rmSync as rmSync7 } from "node:fs";
284525
284733
  import { isAbsolute as isAbsolute3, join as join40, relative as relative5, resolve as resolve6, sep as sep2 } from "node:path";
284526
284734
  var TerraformTagParams = type({
284527
284735
  tags: type({ "[string]": "string" }).describe(
@@ -284543,7 +284751,7 @@ var TerraformTagParams = type({
284543
284751
  var realRunners3 = { run };
284544
284752
  function sha(path15) {
284545
284753
  try {
284546
- return createHash11("sha256").update(readFileSync26(path15)).digest("hex");
284754
+ return createHash12("sha256").update(readFileSync26(path15)).digest("hex");
284547
284755
  } catch {
284548
284756
  return null;
284549
284757
  }
@@ -284557,7 +284765,7 @@ function toPosix(p) {
284557
284765
  function terratagFiles(dir) {
284558
284766
  const dirs = [dir];
284559
284767
  const manifest = join40(dir, ".terraform", "modules", "modules.json");
284560
- if (existsSync9(manifest)) {
284768
+ if (existsSync8(manifest)) {
284561
284769
  let modules;
284562
284770
  try {
284563
284771
  modules = JSON.parse(readFileSync26(manifest, "utf8")).Modules;
@@ -284600,12 +284808,12 @@ function runTag(ctx, params, runners = realRunners3) {
284600
284808
  return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
284601
284809
  };
284602
284810
  const requested = resolve6(cwd, params.dir ?? ".");
284603
- const target = existsSync9(requested) ? realpathSync2(requested) : requested;
284811
+ const target = existsSync8(requested) ? realpathSync2(requested) : requested;
284604
284812
  if (!inside(target)) {
284605
284813
  throw new Error(`terraform_tag: \`dir\` must stay inside the workspace (got ${params.dir})`);
284606
284814
  }
284607
284815
  const relDir = toPosix(relative5(cwd, target)) || ".";
284608
- if (!existsSync9(target)) {
284816
+ if (!existsSync8(target)) {
284609
284817
  return toolSkip("no_terraform_files", `no .tf files under ${relDir}`);
284610
284818
  }
284611
284819
  const absFiles = terratagFiles(target);
@@ -284643,7 +284851,7 @@ function runTag(ctx, params, runners = realRunners3) {
284643
284851
  if (params.filter) args2.push(`-filter=${params.filter}`);
284644
284852
  if (params.skip) args2.push(`-skip=${params.skip}`);
284645
284853
  const before = snapshot2(cwd, files);
284646
- const priorBackups = new Set(files.filter((f) => existsSync9(join40(cwd, `${f}.bak`))));
284854
+ const priorBackups = new Set(files.filter((f) => existsSync8(join40(cwd, `${f}.bak`))));
284647
284855
  const r = runners.run("terratag", args2, cwd);
284648
284856
  if (r.missing) {
284649
284857
  return toolSkip(
@@ -285066,10 +285274,10 @@ var DispatchRecorder = class {
285066
285274
  * orchestrator counts as subagent — including a session the labeler could not
285067
285275
  * name, because an unnamed session is by definition not the orchestrator's.
285068
285276
  */
285069
- recordTokens(label, billableTokens2) {
285070
- if (!Number.isFinite(billableTokens2) || billableTokens2 <= 0) return;
285071
- if (label === ORCHESTRATOR_LABEL) this.#orchestratorTokens += billableTokens2;
285072
- else this.#subagentTokens += billableTokens2;
285277
+ recordTokens(label, billableTokens) {
285278
+ if (!Number.isFinite(billableTokens) || billableTokens <= 0) return;
285279
+ if (label === ORCHESTRATOR_LABEL) this.#orchestratorTokens += billableTokens;
285280
+ else this.#subagentTokens += billableTokens;
285073
285281
  }
285074
285282
  tally() {
285075
285283
  return {
@@ -285239,7 +285447,7 @@ function buildLocalTools(ctx) {
285239
285447
  ];
285240
285448
  }
285241
285449
  function cliVersion() {
285242
- const raw2 = "0.3.12";
285450
+ const raw2 = "0.3.13";
285243
285451
  if (raw2 !== void 0 && /^\d+\.\d+\.\d+$/.test(raw2)) {
285244
285452
  return raw2;
285245
285453
  }
@@ -285378,7 +285586,7 @@ async function runAssessCli(params) {
285378
285586
  }
285379
285587
  let cwd = process.cwd();
285380
285588
  if (rawCwd) cwd = isAbsolute4(rawCwd) ? rawCwd : resolve7(process.cwd(), rawCwd);
285381
- if (!existsSync10(cwd) || !statSync8(cwd).isDirectory()) {
285589
+ if (!existsSync9(cwd) || !statSync8(cwd).isDirectory()) {
285382
285590
  console.error(`assess: not a directory: ${cwd}`);
285383
285591
  process.exit(ASSESS_EXIT.USAGE);
285384
285592
  }
@@ -285652,7 +285860,7 @@ var import_arg3 = __toESM(require_arg(), 1);
285652
285860
 
285653
285861
  // node_modules/.pnpm/@infraweaver-io+ci@0.1.8/node_modules/@infraweaver-io/ci/dist/gitAuth.js
285654
285862
  import { execSync } from "node:child_process";
285655
- import { createHash as createHash12 } from "node:crypto";
285863
+ import { createHash as createHash13 } from "node:crypto";
285656
285864
  import { readFileSync as readFileSync28, realpathSync as realpathSync3, unlinkSync } from "node:fs";
285657
285865
 
285658
285866
  // node_modules/.pnpm/@infraweaver-io+sandbox@0.1.8/node_modules/@infraweaver-io/sandbox/dist/subprocess.js
@@ -285979,7 +286187,7 @@ ${errMsg}` : errMsg;
285979
286187
  // node_modules/.pnpm/@infraweaver-io+ci@0.1.8/node_modules/@infraweaver-io/ci/dist/gitAuth.js
285980
286188
  var gitBinary;
285981
286189
  function hashFile(path15) {
285982
- return createHash12("sha256").update(readFileSync28(path15)).digest("hex");
286190
+ return createHash13("sha256").update(readFileSync28(path15)).digest("hex");
285983
286191
  }
285984
286192
  function resolveGit() {
285985
286193
  const lookup3 = process.platform === "win32" ? "where git" : "which git";
@@ -286352,7 +286560,7 @@ function TerraformChangeSummaryTool(ctx) {
286352
286560
  }
286353
286561
 
286354
286562
  // src/mcp/checkout.ts
286355
- import { createHash as createHash13 } from "node:crypto";
286563
+ import { createHash as createHash14 } from "node:crypto";
286356
286564
  import { statSync as statSync9, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "node:fs";
286357
286565
  import { join as join53 } from "node:path";
286358
286566
 
@@ -287546,7 +287754,7 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
287546
287754
  - \`${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.
287547
287755
  - \`${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.
287548
287756
 
287549
- **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\`.
287757
+ **A candidate marked \`report_only\` is an observation, not work.** It sits in the code of a module other configurations call (\`report_only_reason\` says why), and restructuring that 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\`.
287550
287758
 
287551
287759
  **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.
287552
287760
 
@@ -287679,7 +287887,7 @@ function remediateMode(t) {
287679
287887
 
287680
287888
  4. **for the chosen group**:
287681
287889
  - **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.
287682
- - **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.
287890
+ - **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch, or the branch of the PR already open for this group) \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.
287683
287891
  - **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.
287684
287892
  - **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.
287685
287893
  - **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.
@@ -287740,7 +287948,7 @@ This mode composes the **Remediate** and **Refactor** verbs into a single run. I
287740
287948
  - **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.
287741
287949
 
287742
287950
  3. **REFACTOR (phase 2) \u2014 modularise on top of the committed fix**: follow the **Refactor** mode's flow:
287743
- - \`${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\`).
287951
+ - \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor; a candidate marked \`report_only\` (it sits in the code of 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\`).
287744
287952
  - 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")}\`).
287745
287953
  - \`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.
287746
287954
  - **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.
@@ -288158,7 +288366,7 @@ import { setTimeout as sleep3 } from "node:timers/promises";
288158
288366
 
288159
288367
  // node_modules/.pnpm/@infraweaver-io+core@0.1.5/node_modules/@infraweaver-io/core/dist/packageManager.js
288160
288368
  var import_semver = __toESM(require_semver2(), 1);
288161
- import { existsSync as existsSync11 } from "node:fs";
288369
+ import { existsSync as existsSync10 } from "node:fs";
288162
288370
  import { mkdir as mkdir2, readFile } from "node:fs/promises";
288163
288371
  import { delimiter as delimiter2, join as join44 } from "node:path";
288164
288372
  var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
@@ -288201,7 +288409,7 @@ function parseDevEnginesField(field) {
288201
288409
  }
288202
288410
  async function resolvePackageManagerSpec(cwd) {
288203
288411
  const pkgPath = join44(cwd, "package.json");
288204
- if (!existsSync11(pkgPath))
288412
+ if (!existsSync10(pkgPath))
288205
288413
  return null;
288206
288414
  let pkg;
288207
288415
  try {
@@ -289293,15 +289501,52 @@ var Git = type({
289293
289501
  args: type.string.array().describe("Additional arguments for the git command").optional()
289294
289502
  });
289295
289503
  function splitWords(line) {
289296
- return [...line.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
289504
+ const words2 = [];
289505
+ let word = "";
289506
+ let inWord = false;
289507
+ let quote;
289508
+ for (let i = 0; i < line.length; i++) {
289509
+ const ch = line.charAt(i);
289510
+ if (quote === "'") {
289511
+ if (ch === "'") quote = void 0;
289512
+ else word += ch;
289513
+ } else if (quote === '"') {
289514
+ if (ch === '"') quote = void 0;
289515
+ else if (ch === "\\" && i + 1 < line.length && '"\\$`\n'.includes(line.charAt(i + 1))) {
289516
+ i += 1;
289517
+ if (line.charAt(i) !== "\n") word += line.charAt(i);
289518
+ } else word += ch;
289519
+ } else if (ch === " " || ch === " ") {
289520
+ if (inWord) words2.push(word);
289521
+ word = "";
289522
+ inWord = false;
289523
+ } else if (ch === "\n") {
289524
+ throw new Error(
289525
+ "command holds an unquoted newline \u2014 pass one git command per call, and quote a multi-line message"
289526
+ );
289527
+ } else if (ch === "\\") {
289528
+ if (i + 1 >= line.length) throw new Error("command ends in a backslash that escapes nothing");
289529
+ i += 1;
289530
+ if (line.charAt(i) === "\n") continue;
289531
+ word += line.charAt(i);
289532
+ inWord = true;
289533
+ } else if (ch === "'" || ch === '"') {
289534
+ quote = ch;
289535
+ inWord = true;
289536
+ } else {
289537
+ word += ch;
289538
+ inWord = true;
289539
+ }
289540
+ }
289541
+ if (quote) throw new Error(`command has an unterminated ${quote} quote`);
289542
+ if (inWord) words2.push(word);
289543
+ return words2;
289297
289544
  }
289298
289545
  function normalizeGitCall(params) {
289299
- let words2;
289300
- if (params.command === void 0) words2 = [...params.args ?? []];
289301
- else if (/^git\s/.test(params.command))
289302
- words2 = [...splitWords(params.command).slice(1), ...params.args ?? []];
289303
- else return { command: params.command, args: params.args ?? [] };
289304
- const [command, ...args2] = words2[0] === "git" ? words2.slice(1) : words2;
289546
+ const words2 = params.command === void 0 ? [] : /^git\s/.test(params.command) ? splitWords(params.command) : [params.command];
289547
+ words2.push(...params.args ?? []);
289548
+ while (words2[0] === "git") words2.shift();
289549
+ const [command, ...args2] = words2;
289305
289550
  if (command === void 0 || !/^[a-z][a-z0-9-]*$/.test(command)) {
289306
289551
  throw new Error(
289307
289552
  `command must be a Git subcommand such as 'status' or 'checkout' (was "${command ?? ""}")`
@@ -289737,7 +289982,7 @@ function claimsBlockingSeverity(body2) {
289737
289982
  init_core();
289738
289983
  import { isAbsolute as isAbsolute6, resolve as resolve10 } from "node:path";
289739
289984
 
289740
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/monotonicQuota.js
289985
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/monotonicQuota.js
289741
289986
  function createMonotonicQuota(params) {
289742
289987
  const { limit, unlimited, normalize: normalize5 } = params;
289743
289988
  let used = 0;
@@ -289762,7 +290007,7 @@ function createMonotonicQuota(params) {
289762
290007
  };
289763
290008
  }
289764
290009
 
289765
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/costQuota.js
290010
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/costQuota.js
289766
290011
  var CostQuotaExceededError = class extends Error {
289767
290012
  code = "COST_QUOTA_EXCEEDED";
289768
290013
  constructor(message2) {
@@ -289809,7 +290054,7 @@ function resolveRunCostQuota(parsed2) {
289809
290054
  return parsed2 === void 0 ? DEFAULT_RUN_COST_QUOTA_USD : parsed2;
289810
290055
  }
289811
290056
 
289812
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/tokenQuota.js
290057
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/tokenQuota.js
289813
290058
  var TokenQuotaExceededError = class extends Error {
289814
290059
  code = "TOKEN_QUOTA_EXCEEDED";
289815
290060
  constructor(message2) {
@@ -289819,19 +290064,16 @@ var TokenQuotaExceededError = class extends Error {
289819
290064
  };
289820
290065
  var DEFAULT_RUN_TOKEN_QUOTA = 1e7;
289821
290066
  var RUN_TOKEN_QUOTA_INPUT = "max_run_tokens";
289822
- function billableTokens(t) {
289823
- let total = 0;
289824
- for (const v of [t.input, t.output, t.cacheRead, t.cacheWrite]) {
289825
- if (typeof v === "number" && Number.isFinite(v) && v > 0)
289826
- total += v;
289827
- }
289828
- return Math.floor(total);
290067
+ var CACHE_READ_WEIGHT = 0.1;
290068
+ function quotaTokens(t) {
290069
+ const part = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
290070
+ return Math.floor(part(t.input) + part(t.output) + part(t.cacheWrite) + part(t.cacheRead) * CACHE_READ_WEIGHT);
289829
290071
  }
289830
290072
  function normalizeTotal(total) {
289831
- return typeof total === "number" ? Math.max(0, Math.floor(total)) : billableTokens(total);
290073
+ return typeof total === "number" ? Math.max(0, Math.floor(total)) : quotaTokens(total);
289832
290074
  }
289833
290075
  function tokenGap(used, ceiling) {
289834
- return `run token quota exhausted: ${used.toLocaleString("en-GB")} of ${ceiling.toLocaleString("en-GB")} billable tokens consumed, so the run was stopped before it finished. Its results are INCOMPLETE and must not be read as a clean assessment \u2014 a finding that would have been raised after this point was never looked for. Raise \`${RUN_TOKEN_QUOTA_INPUT}\` if the run legitimately needs more budget, or set \`${RUN_TOKEN_QUOTA_INPUT}: 0\` to remove the bound entirely. A run that reaches this limit without finishing is more often a remediation loop that cannot converge (a fix that re-breaks its own check) than a genuinely large repository.`;
290076
+ return `run token quota exhausted: ${used.toLocaleString("en-GB")} of ${ceiling.toLocaleString("en-GB")} tokens counted against the quota (a token read from the prompt cache counts as a tenth), so the run was stopped before it finished. Its results are INCOMPLETE and must not be read as a clean assessment \u2014 a finding that would have been raised after this point was never looked for. Raise \`${RUN_TOKEN_QUOTA_INPUT}\` if the run legitimately needs more budget, or set \`${RUN_TOKEN_QUOTA_INPUT}: 0\` to remove the bound entirely. A run that reaches this limit without finishing is more often a remediation loop that cannot converge (a fix that re-breaks its own check) than a genuinely large repository.`;
289835
290077
  }
289836
290078
  function unlimitedTokenQuota() {
289837
290079
  return createMonotonicQuota({
@@ -290974,6 +291216,12 @@ function parseCommentMarker(body2) {
290974
291216
  return null;
290975
291217
  }
290976
291218
  }
291219
+ function findingKeysIn(body2) {
291220
+ return [...body2.matchAll(new RegExp(MARKER.source, "g"))].flatMap((m) => {
291221
+ const marker2 = parseCommentMarker(m[0]);
291222
+ return marker2?.kind === "finding" ? [marker2.key] : [];
291223
+ });
291224
+ }
290977
291225
  var SUGGESTION = /```suggestion\r?\n([\s\S]*?)```/;
290978
291226
  function suggestionText(body2) {
290979
291227
  const m = SUGGESTION.exec(body2);
@@ -291223,39 +291471,115 @@ function formatDuplicateCommentsNote(duplicates) {
291223
291471
  var MAX_DUPLICATE_COMMENT_LINES = 50;
291224
291472
 
291225
291473
  // src/mcp/reviewCommentLimit.ts
291226
- var MAX_OVERFLOW_LINES = 50;
291474
+ var MAX_NOTE_CHARS = 3e4;
291475
+ var TRAILER_RESERVE = 400;
291227
291476
  var PROVEN_MARK = "\u2705 **Proven by re-scan**";
291228
- function rank(c) {
291477
+ var SEVERITY_LEVEL = {
291478
+ critical: 0,
291479
+ high: 1,
291480
+ medium: 2,
291481
+ low: 3,
291482
+ info: 4
291483
+ };
291484
+ var MARKER_LEVEL = { "\u{1F6A8}": 1, "\u26A0\uFE0F": 2, "\u26A0": 2, "\u2139\uFE0F": 3, \u2139: 3 };
291485
+ var UNMARKED_LEVEL = 2;
291486
+ function rank(c, severity) {
291229
291487
  const body2 = c.body ?? "";
291230
- const marker2 = leadingMarker(body2);
291231
- const severity = marker2 === "\u{1F6A8}" ? 0 : marker2 === "\u26A0\uFE0F" || marker2 === "\u26A0" ? 2 : 4;
291232
- return severity + (body2.includes(PROVEN_MARK) ? 0 : 1);
291488
+ const level = severity ? SEVERITY_LEVEL[severity] : MARKER_LEVEL[leadingMarker(body2) ?? ""] ?? UNMARKED_LEVEL;
291489
+ return level * 2 + (body2.includes(PROVEN_MARK) ? 0 : 1);
291233
291490
  }
291234
- function limitInlineComments(comments, limit) {
291491
+ function limitInlineComments(comments, limit, severityOf) {
291235
291492
  if (!limit || comments.length <= limit) return { kept: [...comments], overflow: [] };
291236
291493
  const keep = new Set(
291237
- comments.map((c, i) => ({ i, r: rank(c) })).sort((a, b) => a.r - b.r || a.i - b.i).slice(0, limit).map(({ i }) => i)
291494
+ comments.map((c, i) => ({ i, r: rank(c, severityOf?.(c)) })).sort((a, b) => a.r - b.r || a.i - b.i).slice(0, limit).map(({ i }) => i)
291238
291495
  );
291239
291496
  return {
291240
291497
  kept: comments.filter((_, i) => keep.has(i)),
291241
291498
  overflow: comments.filter((_, i) => !keep.has(i))
291242
291499
  };
291243
291500
  }
291244
- function formatOverflowNote(overflow, limit) {
291501
+ var escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
291502
+ var whereOf = (c) => c.line ? `${c.path}:${c.line}` : c.path;
291503
+ function closesItself(body2) {
291504
+ const occurrences = (re) => body2.match(re)?.length ?? 0;
291505
+ const fences = body2.split("\n").filter((l) => /^\s*(```|~~~)/.test(l)).length;
291506
+ return fences % 2 === 0 && occurrences(/<details\b/gi) === occurrences(/<\/details\s*>/gi) && occurrences(/<!--/g) === occurrences(/-->/g);
291507
+ }
291508
+ function asText(body2) {
291509
+ const longest = Math.max(0, ...(body2.match(/`+/g) ?? []).map((run4) => run4.length));
291510
+ const fence = "`".repeat(Math.max(3, longest + 1));
291511
+ return `${fence}text
291512
+ ${body2}
291513
+ ${fence}`;
291514
+ }
291515
+ function renderWhole(c, linkOf) {
291516
+ const where4 = escapeHtml(whereOf(c));
291517
+ const gist = gistOf(c.body);
291518
+ const href = linkOf?.(c);
291519
+ const body2 = c.body ?? "";
291520
+ return [
291521
+ `<details><summary><code>${where4}</code>${gist ? ` \u2014 ${gist}` : ""}</summary>`,
291522
+ ...href ? [`<a href="${escapeHtml(href)}">${where4}</a>`] : [],
291523
+ ...body2 ? [closesItself(body2) ? body2 : asText(body2)] : [],
291524
+ "</details>"
291525
+ ].join("\n\n");
291526
+ }
291527
+ function renderBrief(c) {
291528
+ const gist = gistOf(c.body);
291529
+ const markers = findingKeysIn(c.body ?? "").map(
291530
+ (key) => renderCommentMarker({ kind: "finding", key, proven: false })
291531
+ );
291532
+ return `- \`${whereOf(c).replace(/`/g, "'")}\`${gist ? ` \u2014 ${gist}` : ""}${markers.map((m) => ` ${m}`).join("")}`;
291533
+ }
291534
+ function formatOverflowNote(overflow, limit, linkOf) {
291245
291535
  if (overflow.length === 0) return "";
291246
- const shown = overflow.slice(0, MAX_OVERFLOW_LINES).map((c) => {
291247
- const where4 = c.line ? `${c.path}:${c.line}` : c.path;
291248
- const gist = gistOf(c.body);
291249
- return `- \`${where4.replace(/`/g, "'")}\`${gist ? ` \u2014 ${gist}` : ""}`;
291250
- });
291251
- const rest = overflow.length - shown.length;
291252
- if (rest > 0) shown.push(`- \u2026and ${rest} more not shown`);
291253
- return `
291536
+ const header = `
291254
291537
 
291255
291538
  ---
291256
291539
 
291257
- **Note:** this review posts at most ${limit} inline comments (\`review_comment_limit\`), so ${overflow.length} more ${overflow.length === 1 ? "is" : "are"} listed here instead:
291258
- ` + shown.join("\n");
291540
+ **Note:** this review posts at most ${limit} inline comments (\`review_comment_limit\`), so ${overflow.length} more ${overflow.length === 1 ? "is" : "are"} listed here instead:`;
291541
+ const SEP = 2;
291542
+ let room = MAX_NOTE_CHARS - TRAILER_RESERVE - header.length;
291543
+ const items = [];
291544
+ for (const c of overflow) {
291545
+ const brief2 = renderBrief(c);
291546
+ if (brief2.length + SEP > room) break;
291547
+ items.push(brief2);
291548
+ room -= brief2.length + SEP;
291549
+ }
291550
+ let brief = items.length;
291551
+ for (const [i, c] of overflow.slice(0, items.length).entries()) {
291552
+ const whole = renderWhole(c, linkOf);
291553
+ const extra = whole.length - items[i].length;
291554
+ if (extra > room) continue;
291555
+ items[i] = whole;
291556
+ room -= extra;
291557
+ brief--;
291558
+ }
291559
+ const omitted = overflow.length - items.length;
291560
+ if (omitted > 0) items.push(`- \u2026and ${omitted} more not shown`);
291561
+ const cut = [
291562
+ brief > 0 ? `${brief} of them ${brief === 1 ? "is" : "are"} shown by place and first line only` : "",
291563
+ omitted > 0 ? `${omitted} ${omitted === 1 ? "is" : "are"} not shown at all` : ""
291564
+ ].filter(Boolean);
291565
+ if (cut.length > 0) {
291566
+ items.push(`${cut.join(" and ")}, to keep this review within GitHub's size limit.`);
291567
+ }
291568
+ return `${header}
291569
+
291570
+ ${items.join("\n\n")}`;
291571
+ }
291572
+ function foldedFindingsStillIntroduced(input2) {
291573
+ const folded = new Set(
291574
+ input2.reviews.flatMap(
291575
+ (r) => r.body && isOwnReviewComment(r.user, r.body) ? findingKeysIn(r.body) : []
291576
+ )
291577
+ );
291578
+ if (folded.size === 0) return [];
291579
+ const dismissed = new Set(input2.dismissed.map((d) => d.finding_id));
291580
+ return input2.delta.introduced.filter(
291581
+ (f) => folded.has(findingKey(f)) && !dismissed.has(f.concern.id)
291582
+ );
291259
291583
  }
291260
291584
 
291261
291585
  // src/mcp/reviewComments.ts
@@ -292135,7 +292459,7 @@ function where2(f) {
292135
292459
  function renderReviewVerdict(input2) {
292136
292460
  const { delta, verdict: verdict2 } = input2;
292137
292461
  const introduced = delta.introduced.length;
292138
- const blocking = verdict2.gating + verdict2.required;
292462
+ const { blocking } = verdict2;
292139
292463
  const renamedBlocking = delta.introduced.filter(
292140
292464
  (f) => f.possiblyPreexisting && failsGate(f, [...input2.requiredControls ?? []])
292141
292465
  ).length;
@@ -292381,7 +292705,7 @@ function apply(worktree, path15, cs) {
292381
292705
  }
292382
292706
 
292383
292707
  // src/mcp/terraform/gateScan.ts
292384
- import { existsSync as existsSync12, mkdtempSync as mkdtempSync6, rmSync as rmSync10 } from "node:fs";
292708
+ import { existsSync as existsSync11, mkdtempSync as mkdtempSync6, rmSync as rmSync10 } from "node:fs";
292385
292709
  import { join as join50, posix as posix15 } from "node:path";
292386
292710
 
292387
292711
  // src/mcp/terraform/secretScan.ts
@@ -292538,7 +292862,7 @@ async function reviewScans(ctx, pr, scanners) {
292538
292862
  const changed = diffScope(ctx, cwd, scanners);
292539
292863
  const scope2 = base ? reviewScope(ctx, cwd, base) : void 0;
292540
292864
  const started = Date.now();
292541
- const scanBase = async (b) => existsSync12(b.cwd) ? scanTree(ctx, b.cwd, changed, scanners, reviewedConcern, scope2) : [];
292865
+ const scanBase = async (b) => existsSync11(b.cwd) ? scanTree(ctx, b.cwd, changed, scanners, reviewedConcern, scope2) : [];
292542
292866
  const [head, baseScan, secrets] = await Promise.allSettled([
292543
292867
  scanTree(ctx, cwd, changed, scanners, reviewedConcern, scope2),
292544
292868
  base ? scanBase(base) : Promise.resolve([]),
@@ -292970,6 +293294,22 @@ function CreatePullRequestReviewTool(ctx) {
292970
293294
  `cannot approve: ${outstanding} unresolved ${PRODUCT_NAME} review thread(s) still open on this PR. approval requires every prior ${PRODUCT_NAME} finding to be resolved, not just the latest commits to be clean. inspect them with \`${listRef}\`; for each thread the current code genuinely addresses, reply then call \`${resolveRef2}\`, and retry this approval. if any thread is a real outstanding issue, do NOT approve \u2014 submit a non-approving review (omit \`approved\`, or set \`request_changes\` for a blocking one) that covers it instead.`
292971
293295
  );
292972
293296
  }
293297
+ if (delta) {
293298
+ const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
293299
+ owner: ctx.repo.owner,
293300
+ repo: ctx.repo.name,
293301
+ pull_number,
293302
+ per_page: 100
293303
+ });
293304
+ const folded = foldedFindingsStillIntroduced({ reviews, delta, dismissed });
293305
+ if (folded.length > 0) {
293306
+ throw new Error(
293307
+ `cannot approve: ${folded.length} introduced finding(s) an earlier ${PRODUCT_NAME} review listed in its body (past \`review_comment_limit\`, so with no thread to resolve) are still introduced by this head: ` + folded.slice(0, 20).map(
293308
+ (f) => `${f.concern.id} (${f.concern.rule_id} at ${f.concern.location.file}:${f.concern.location.line ?? "?"})`
293309
+ ).join(", ") + `. approval requires every prior ${PRODUCT_NAME} finding to be addressed. if one genuinely needs no change, list it in \`dismissed\` with the reason and retry; otherwise do NOT approve \u2014 submit a non-approving review that covers it.`
293310
+ );
293311
+ }
293312
+ }
292973
293313
  }
292974
293314
  const skip = reviewSkipDecision({
292975
293315
  approved: approved ?? false,
@@ -293076,6 +293416,7 @@ ${formatReanchoredNote(reanchored.moved)}`
293076
293416
  params.event = event;
293077
293417
  }
293078
293418
  const proofs = delta ? await proveReviewSuggestions(ctx, auditedComments) : /* @__PURE__ */ new Map();
293419
+ const findingSeverity = /* @__PURE__ */ new Map();
293079
293420
  const reviewComments = auditedComments.map((comment, index) => {
293080
293421
  let commentBody = fixDoubleEscapedString(comment.body || "");
293081
293422
  if (comment.suggestion !== void 0) {
@@ -293117,6 +293458,9 @@ ${marker2}`;
293117
293458
  reviewComment.start_line = comment.start_line;
293118
293459
  reviewComment.start_side = side;
293119
293460
  }
293461
+ if (comment.provenance?.kind === "scanner") {
293462
+ findingSeverity.set(reviewComment, comment.provenance.finding.concern.severity);
293463
+ }
293120
293464
  return reviewComment;
293121
293465
  });
293122
293466
  let droppedComments = [];
@@ -293152,7 +293496,11 @@ ${marker2}`;
293152
293496
  `withholding ${duplicateComments.length}/${reviewComments.length} inline comment(s) that duplicate open review threads`
293153
293497
  );
293154
293498
  }
293155
- const limited = limitInlineComments(deduped.kept, ctx.payload.reviewCommentLimit);
293499
+ const limited = limitInlineComments(
293500
+ deduped.kept,
293501
+ ctx.payload.reviewCommentLimit,
293502
+ (c) => findingSeverity.get(c)
293503
+ );
293156
293504
  overflowComments = limited.overflow;
293157
293505
  if (overflowComments.length > 0) {
293158
293506
  log2.info(
@@ -293184,7 +293532,13 @@ ${marker2}`;
293184
293532
  body2 = body2 ? body2 + note : note.replace(/^\n\n/, "");
293185
293533
  }
293186
293534
  if (overflowComments.length > 0) {
293187
- const note = formatOverflowNote(overflowComments, ctx.payload.reviewCommentLimit);
293535
+ const server = process.env.GITHUB_SERVER_URL || "https://github.com";
293536
+ const commit = params.commit_id;
293537
+ const note = formatOverflowNote(
293538
+ overflowComments,
293539
+ ctx.payload.reviewCommentLimit,
293540
+ commit ? (c) => `${server}/${ctx.repo.owner}/${ctx.repo.name}/blob/${commit}/${c.path.split("/").map(encodeURIComponent).join("/")}${c.line && c.side !== "LEFT" ? `#L${c.line}` : ""}` : void 0
293541
+ );
293188
293542
  body2 = body2 ? body2 + note : note.replace(/^\n\n/, "");
293189
293543
  }
293190
293544
  let reproved = 0;
@@ -294196,7 +294550,7 @@ function formatFilesWithLineNumbers(files) {
294196
294550
  }
294197
294551
  const tocLines = [`## Files (${files.length})`];
294198
294552
  for (const entry4 of tocEntries) {
294199
- const anchor2 = createHash13("sha256").update(entry4.filename).digest("hex");
294553
+ const anchor2 = createHash14("sha256").update(entry4.filename).digest("hex");
294200
294554
  tocLines.push(
294201
294555
  `- ${entry4.filename} \u2192 lines ${entry4.startLine}-${entry4.endLine} \xB7 diff-${anchor2}`
294202
294556
  );
@@ -295042,7 +295396,7 @@ function UpsertRemediationDashboardTool(ctx) {
295042
295396
  import { performance as performance3 } from "node:perf_hooks";
295043
295397
 
295044
295398
  // src/prep/installNodeDependencies.ts
295045
- import { existsSync as existsSync13 } from "node:fs";
295399
+ import { existsSync as existsSync12 } from "node:fs";
295046
295400
  import { join as join56 } from "node:path";
295047
295401
 
295048
295402
  // node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
@@ -295439,7 +295793,7 @@ var installNodeDependencies = {
295439
295793
  name: "installNodeDependencies",
295440
295794
  shouldRun: () => {
295441
295795
  const packageJsonPath = join56(process.cwd(), "package.json");
295442
- return existsSync13(packageJsonPath);
295796
+ return existsSync12(packageJsonPath);
295443
295797
  },
295444
295798
  run: async (options) => {
295445
295799
  const declared2 = await resolvePackageManagerSpec(process.cwd());
@@ -295547,7 +295901,7 @@ ${errorMessage2}`]
295547
295901
  };
295548
295902
 
295549
295903
  // src/prep/installPythonDependencies.ts
295550
- import { existsSync as existsSync14, readFileSync as readFileSync34 } from "node:fs";
295904
+ import { existsSync as existsSync13, readFileSync as readFileSync34 } from "node:fs";
295551
295905
  import { join as join57 } from "node:path";
295552
295906
  var PYTHON_CONFIGS = [
295553
295907
  {
@@ -295633,13 +295987,13 @@ var installPythonDependencies = {
295633
295987
  }
295634
295988
  const cwd = process.cwd();
295635
295989
  return PYTHON_CONFIGS.some(
295636
- (config3) => existsSync14(join57(cwd, config3.file)) && configApplies(config3, cwd)
295990
+ (config3) => existsSync13(join57(cwd, config3.file)) && configApplies(config3, cwd)
295637
295991
  );
295638
295992
  },
295639
295993
  run: async (options) => {
295640
295994
  const cwd = process.cwd();
295641
295995
  const config3 = PYTHON_CONFIGS.find(
295642
- (c) => existsSync14(join57(cwd, c.file)) && configApplies(c, cwd)
295996
+ (c) => existsSync13(join57(cwd, c.file)) && configApplies(c, cwd)
295643
295997
  );
295644
295998
  if (!config3) {
295645
295999
  return {
@@ -296220,7 +296574,7 @@ function SetOutputTool(ctx, outputSchema) {
296220
296574
 
296221
296575
  // src/mcp/policy.ts
296222
296576
  import { spawnSync as spawnSync8 } from "node:child_process";
296223
- import { existsSync as existsSync15, readFileSync as readFileSync35 } from "node:fs";
296577
+ import { existsSync as existsSync14, readFileSync as readFileSync35 } from "node:fs";
296224
296578
  import { join as join58 } from "node:path";
296225
296579
  function parseConftestOutput(stdout) {
296226
296580
  let parsed2;
@@ -296272,7 +296626,7 @@ function namespaceArgs(conftestCwd) {
296272
296626
  function findPlanJson(cwd) {
296273
296627
  for (const candidate of PLAN_JSON_NAMES) {
296274
296628
  const abs = join58(cwd, candidate);
296275
- if (existsSync15(abs)) return abs;
296629
+ if (existsSync14(abs)) return abs;
296276
296630
  }
296277
296631
  return null;
296278
296632
  }
@@ -296313,11 +296667,11 @@ function resolvePolicyDir(cwd, explicit) {
296313
296667
  } catch {
296314
296668
  return null;
296315
296669
  }
296316
- return existsSync15(abs) ? abs : null;
296670
+ return existsSync14(abs) ? abs : null;
296317
296671
  }
296318
296672
  for (const d of DEFAULT_POLICY_DIRS) {
296319
296673
  const abs = join58(cwd, d);
296320
- if (existsSync15(abs)) return abs;
296674
+ if (existsSync14(abs)) return abs;
296321
296675
  }
296322
296676
  return null;
296323
296677
  }
@@ -296351,7 +296705,7 @@ function PolicyCheckTool(ctx) {
296351
296705
  } catch {
296352
296706
  abs = null;
296353
296707
  }
296354
- targetFile = abs && existsSync15(abs) ? abs : null;
296708
+ targetFile = abs && existsSync14(abs) ? abs : null;
296355
296709
  if (!targetFile) {
296356
296710
  return toolSkip(
296357
296711
  "target_not_found",
@@ -299942,7 +300296,7 @@ async function startMcpHttpServer(ctx, options) {
299942
300296
 
299943
300297
  // src/mcp/terraform/policyGate.ts
299944
300298
  import {
299945
- existsSync as existsSync16,
300299
+ existsSync as existsSync15,
299946
300300
  mkdirSync as mkdirSync7,
299947
300301
  mkdtempSync as mkdtempSync8,
299948
300302
  readdirSync as readdirSync10,
@@ -300325,7 +300679,7 @@ function readOrEmpty(path15) {
300325
300679
  function regoDirIn(root, skip) {
300326
300680
  for (const dir of DEFAULT_POLICY_DIRS) {
300327
300681
  const abs = join61(root, dir);
300328
- if (existsSync16(abs) && walkFilesBySuffix(abs, ".rego").some(
300682
+ if (existsSync15(abs) && walkFilesBySuffix(abs, ".rego").some(
300329
300683
  (rel) => !skip(join61(dir, rel), readOrEmpty(join61(abs, rel)))
300330
300684
  )) {
300331
300685
  return dir;
@@ -300394,7 +300748,7 @@ function resolveRepoPolicy(cwd, baseRef, tmpdir6, notes, skip) {
300394
300748
  return null;
300395
300749
  }
300396
300750
  function findWorkflowPlanJson(cwd, notes) {
300397
- const present = PLAN_JSON_NAMES.filter((name) => existsSync16(join61(cwd, name)));
300751
+ const present = PLAN_JSON_NAMES.filter((name) => existsSync15(join61(cwd, name)));
300398
300752
  if (present.length === 0) return null;
300399
300753
  const listing = run("git", ["ls-files", "-z"], cwd);
300400
300754
  if (listing.status !== 0) {
@@ -301753,9 +302107,9 @@ function shouldRunGraceTurn(params) {
301753
302107
  }
301754
302108
 
301755
302109
  // src/agents/quotaTokens.ts
301756
- var CACHE_READ_WEIGHT = 0.1;
301757
- function quotaTokens(t) {
301758
- return t.input + t.output + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT;
302110
+ var CACHE_READ_WEIGHT2 = 0.1;
302111
+ function quotaTokens2(t) {
302112
+ return t.input + t.output + (t.reasoning ?? 0) + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT2;
301759
302113
  }
301760
302114
 
301761
302115
  // src/agents/toolPlaneWatch.ts
@@ -302153,7 +302507,7 @@ function createProcessOutputActivityTimeout(ctx) {
302153
302507
  }
302154
302508
 
302155
302509
  // src/utils/prompt/skills.ts
302156
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync38, writeFileSync as writeFileSync19 } from "node:fs";
302510
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync38, writeFileSync as writeFileSync19 } from "node:fs";
302157
302511
  import { dirname as dirname9, join as join64 } from "node:path";
302158
302512
  import { fileURLToPath } from "node:url";
302159
302513
  var BUNDLED_SKILL_NAMES = ["terraform-best-practices"];
@@ -302164,7 +302518,7 @@ function resolveSkillPath(name) {
302164
302518
  join64(here, "skills", name, "SKILL.md")
302165
302519
  ];
302166
302520
  for (const candidate of candidates) {
302167
- if (existsSync17(candidate)) return candidate;
302521
+ if (existsSync16(candidate)) return candidate;
302168
302522
  }
302169
302523
  throw new Error(`bundled skill not found: ${name} (looked in ${candidates.join(", ")})`);
302170
302524
  }
@@ -302183,8 +302537,8 @@ function installBundledSkills(params) {
302183
302537
 
302184
302538
  // src/utils/setup/install.ts
302185
302539
  import { spawnSync as spawnSync9 } from "node:child_process";
302186
- import { createHash as createHash15 } from "node:crypto";
302187
- import { chmodSync as chmodSync2, createWriteStream as createWriteStream2, existsSync as existsSync20, readFileSync as readFileSync39 } from "node:fs";
302540
+ import { createHash as createHash16 } from "node:crypto";
302541
+ import { chmodSync as chmodSync2, createWriteStream as createWriteStream2, existsSync as existsSync19, readFileSync as readFileSync39 } from "node:fs";
302188
302542
  import { join as join69 } from "node:path";
302189
302543
  import { pipeline as pipeline2 } from "node:stream/promises";
302190
302544
 
@@ -302250,7 +302604,7 @@ function verifyTarballIntegrity(tarballPath, dist, descriptor) {
302250
302604
  const expected = candidate.slice(separator2 + 1);
302251
302605
  let actual;
302252
302606
  try {
302253
- actual = createHash15(algorithm).update(bytes).digest("base64");
302607
+ actual = createHash16(algorithm).update(bytes).digest("base64");
302254
302608
  } catch {
302255
302609
  continue;
302256
302610
  }
@@ -302260,7 +302614,7 @@ function verifyTarballIntegrity(tarballPath, dist, descriptor) {
302260
302614
  `Integrity check FAILED for ${descriptor}: the downloaded tarball does not match the registry's published digest (${integrity2}). Refusing to execute it.`
302261
302615
  );
302262
302616
  }
302263
- const actualShasum = createHash15("sha1").update(bytes).digest("hex");
302617
+ const actualShasum = createHash16("sha1").update(bytes).digest("hex");
302264
302618
  if (actualShasum !== shasum) {
302265
302619
  throw new Error(
302266
302620
  `Integrity check FAILED for ${descriptor}: the downloaded tarball does not match the registry's published shasum (expected ${shasum}, got ${actualShasum}). Refusing to execute it.`
@@ -302271,7 +302625,7 @@ async function installFromNpmTarball(params) {
302271
302625
  const tempDir = getTempDir();
302272
302626
  const extractedDir = join69(tempDir, "package");
302273
302627
  const cliPath = join69(extractedDir, params.executablePath);
302274
- if (existsSync20(cliPath)) {
302628
+ if (existsSync19(cliPath)) {
302275
302629
  log2.debug(`\xBB using cached binary at ${cliPath}`);
302276
302630
  return cliPath;
302277
302631
  }
@@ -302362,7 +302716,7 @@ async function installFromNpmTarball(params) {
302362
302716
  `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
302363
302717
  );
302364
302718
  }
302365
- if (!existsSync20(cliPath)) {
302719
+ if (!existsSync19(cliPath)) {
302366
302720
  throw new Error(`Executable not found in extracted package at ${cliPath}`);
302367
302721
  }
302368
302722
  if (params.installDependencies) {
@@ -302602,7 +302956,7 @@ function timerFor(state5, label) {
302602
302956
  function chargeTokenQuota(state5) {
302603
302957
  const quota = state5.params.tokenQuota;
302604
302958
  if (!quota || state5.stopRun.signal.aborted) return;
302605
- if (!quota.observe(state5.quotaSource, quotaTokens(state5.accumulatedTokens))) return;
302959
+ if (!quota.observe(state5.quotaSource, quotaTokens2(state5.accumulatedTokens))) return;
302606
302960
  log2.info(`\xBB ${state5.params.label}: ${quota.gap()}`);
302607
302961
  state5.stopRun.abort(new TokenQuotaExceededError(quota.gap()));
302608
302962
  }
@@ -309334,7 +309688,7 @@ function newTurn() {
309334
309688
  turnCounter += 1;
309335
309689
  return {
309336
309690
  finalText: "",
309337
- tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
309691
+ tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 },
309338
309692
  costUsd: 0,
309339
309693
  sessionError: null,
309340
309694
  lastToolError: null,
@@ -309344,7 +309698,7 @@ function newTurn() {
309344
309698
  function chargeTokenQuota2(ctx) {
309345
309699
  const quota = ctx.tokenQuota;
309346
309700
  if (!quota || !ctx.currentTurn || ctx.tokenQuotaExceeded) return;
309347
- if (!quota.observe(ctx.currentTurn.quotaSource, quotaTokens(ctx.currentTurn.tokens))) return;
309701
+ if (!quota.observe(ctx.currentTurn.quotaSource, quotaTokens2(ctx.currentTurn.tokens))) return;
309348
309702
  ctx.tokenQuotaExceeded = true;
309349
309703
  log2.info(`\xBB ${ctx.label}: ${quota.gap()}`);
309350
309704
  ctx.abortRun?.(new TokenQuotaExceededError(quota.gap()));
@@ -309453,6 +309807,7 @@ async function onPartUpdated(ctx, part) {
309453
309807
  if (t) {
309454
309808
  ctx.currentTurn.tokens.input += t.input || 0;
309455
309809
  ctx.currentTurn.tokens.output += t.output || 0;
309810
+ ctx.currentTurn.tokens.reasoning += t.reasoning || 0;
309456
309811
  ctx.currentTurn.tokens.cacheRead += t.cache?.read || 0;
309457
309812
  ctx.currentTurn.tokens.cacheWrite += t.cache?.write || 0;
309458
309813
  }
@@ -310849,8 +311204,8 @@ function addQueryParameters(url5, parameters) {
310849
311204
  }).join("&");
310850
311205
  }
310851
311206
  var urlVariableRegex = /\{[^{}}]+\}/g;
310852
- function removeNonChars(variableName) {
310853
- return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
311207
+ function removeNonChars(variableName2) {
311208
+ return variableName2.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
310854
311209
  }
310855
311210
  function extractUrlVariableNames(url5) {
310856
311211
  const matches2 = url5.match(urlVariableRegex);
@@ -318529,7 +318884,7 @@ async function runAgentWithWatchdogs(params) {
318529
318884
  const tokenQuota = reserved ? quotas.token : createTokenQuota(quotas.tokenLimit.full);
318530
318885
  const costQuota = reserved ? quotas.cost : createCostQuota(quotas.costLimit.full);
318531
318886
  log2.info(
318532
- quotas.tokenLimit.full > 0 ? `\xBB run token quota: ${quotas.tokenLimit.full.toLocaleString("en-GB")} billable tokens` + (reserved ? ` (${tokenQuota.limit.toLocaleString("en-GB")} for the run, the rest reserved to deliver findings if it runs out)` : "") : `\xBB run token quota: disabled (${RUN_TOKEN_QUOTA_INPUT}: 0)`
318887
+ quotas.tokenLimit.full > 0 ? `\xBB run token quota: ${quotas.tokenLimit.full.toLocaleString("en-GB")} tokens` + (reserved ? ` (${tokenQuota.limit.toLocaleString("en-GB")} for the run, the rest reserved to deliver findings if it runs out)` : "") + "; a token read from the prompt cache counts as a tenth" : `\xBB run token quota: disabled (${RUN_TOKEN_QUOTA_INPUT}: 0)`
318533
318888
  );
318534
318889
  log2.info(
318535
318890
  quotas.costLimit.full > 0 ? `\xBB run cost quota: $${quotas.costLimit.full.toFixed(2)}` + (reserved ? ` ($${costQuota.limit.toFixed(2)} for the run, the rest reserved to deliver findings if it runs out)` : "") : `\xBB run cost quota: disabled (${RUN_COST_QUOTA_INPUT}: 0)`
@@ -318627,7 +318982,7 @@ async function runAgentWithWatchdogs(params) {
318627
318982
  }
318628
318983
 
318629
318984
  // src/phases/startToolPlane.ts
318630
- import { existsSync as existsSync21, readdirSync as readdirSync11 } from "node:fs";
318985
+ import { existsSync as existsSync20, readdirSync as readdirSync11 } from "node:fs";
318631
318986
  import { readFile as readFile5 } from "node:fs/promises";
318632
318987
  import { join as join77 } from "node:path";
318633
318988
 
@@ -319458,7 +319813,7 @@ ${instructions.user}` : null,
319458
319813
  });
319459
319814
  if (toolContext.agentId === "opencode") {
319460
319815
  const pluginDir = join77(process.cwd(), ".opencode", "plugin");
319461
- const hasPlugins = existsSync21(pluginDir) && readdirSync11(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
319816
+ const hasPlugins = existsSync20(pluginDir) && readdirSync11(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
319462
319817
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
319463
319818
  log2.info(
319464
319819
  "\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
@@ -319944,7 +320299,7 @@ async function run2(args2) {
319944
320299
  }
319945
320300
 
319946
320301
  // src/commands/mcp.ts
319947
- import { existsSync as existsSync22, statSync as statSync12 } from "node:fs";
320302
+ import { existsSync as existsSync21, statSync as statSync12 } from "node:fs";
319948
320303
  import { isAbsolute as isAbsolute9, resolve as resolve14 } from "node:path";
319949
320304
  var import_arg4 = __toESM(require_arg(), 1);
319950
320305
  var SEVERITIES5 = ["critical", "high", "medium", "low", "info"];
@@ -320017,7 +320372,7 @@ async function runMcpCli(params) {
320017
320372
  if (rawCwd) {
320018
320373
  cwd = isAbsolute9(rawCwd) ? rawCwd : resolve14(process.cwd(), rawCwd);
320019
320374
  }
320020
- if (!existsSync22(cwd) || !statSync12(cwd).isDirectory()) {
320375
+ if (!existsSync21(cwd) || !statSync12(cwd).isDirectory()) {
320021
320376
  console.error(`--cwd is not a directory: ${cwd}`);
320022
320377
  process.exit(1);
320023
320378
  }
@@ -320116,7 +320471,7 @@ async function runVerifyEvidenceCli2(params) {
320116
320471
  }
320117
320472
 
320118
320473
  // src/cli.ts
320119
- var VERSION14 = "0.3.12";
320474
+ var VERSION14 = "0.3.13";
320120
320475
  var bin = basename9(process.argv[1] || "");
320121
320476
  var PROG = bin === "pf" || bin === PRODUCT_SLUG ? bin : PRODUCT_SLUG;
320122
320477
  var rawArgs = process.argv.slice(2);