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/index.js CHANGED
@@ -107552,23 +107552,23 @@ var require_helpers = __commonJS({
107552
107552
  "node_modules/.pnpm/ip-address@10.7.3/node_modules/ip-address/dist/v6/helpers.js"(exports2) {
107553
107553
  "use strict";
107554
107554
  Object.defineProperty(exports2, "__esModule", { value: true });
107555
- exports2.escapeHtml = escapeHtml;
107555
+ exports2.escapeHtml = escapeHtml2;
107556
107556
  exports2.spanAllZeroes = spanAllZeroes;
107557
107557
  exports2.spanAll = spanAll;
107558
107558
  exports2.spanLeadingZeroes = spanLeadingZeroes;
107559
107559
  exports2.simpleGroup = simpleGroup;
107560
- function escapeHtml(s) {
107560
+ function escapeHtml2(s) {
107561
107561
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
107562
107562
  }
107563
107563
  function spanAllZeroes(s) {
107564
- return escapeHtml(s).replace(/(0+)/g, '<span class="zero">$1</span>');
107564
+ return escapeHtml2(s).replace(/(0+)/g, '<span class="zero">$1</span>');
107565
107565
  }
107566
107566
  function spanAll(s, offset = 0) {
107567
107567
  const letters = s.split("");
107568
- return letters.map((n, i) => `<span class="digit value-${escapeHtml(n)} position-${i + offset}">${spanAllZeroes(n)}</span>`).join("");
107568
+ return letters.map((n, i) => `<span class="digit value-${escapeHtml2(n)} position-${i + offset}">${spanAllZeroes(n)}</span>`).join("");
107569
107569
  }
107570
107570
  function spanLeadingZeroesSimple(group2) {
107571
- return escapeHtml(group2).replace(/^(0+)/, '<span class="zero">$1</span>');
107571
+ return escapeHtml2(group2).replace(/^(0+)/, '<span class="zero">$1</span>');
107572
107572
  }
107573
107573
  function spanLeadingZeroes(address) {
107574
107574
  const groups2 = address.split(":");
@@ -160384,7 +160384,7 @@ var init_cacheTwirpClient = __esm({
160384
160384
  });
160385
160385
 
160386
160386
  // node_modules/.pnpm/@actions+cache@6.3.0_supports-color@7.2.0/node_modules/@actions/cache/lib/internal/tar.js
160387
- import { existsSync as existsSync17, writeFileSync as writeFileSync21 } from "fs";
160387
+ import { existsSync as existsSync16, writeFileSync as writeFileSync21 } from "fs";
160388
160388
  import * as path13 from "path";
160389
160389
  function getTarPath() {
160390
160390
  return __awaiter18(this, void 0, void 0, function* () {
@@ -160394,7 +160394,7 @@ function getTarPath() {
160394
160394
  const systemTar = SystemTarPathOnWindows;
160395
160395
  if (gnuTar) {
160396
160396
  return { path: gnuTar, type: ArchiveToolType.GNU };
160397
- } else if (existsSync17(systemTar)) {
160397
+ } else if (existsSync16(systemTar)) {
160398
160398
  return { path: systemTar, type: ArchiveToolType.BSD };
160399
160399
  }
160400
160400
  break;
@@ -250325,7 +250325,7 @@ var FastMCP = class extends FastMCPEventEmitter {
250325
250325
  }
250326
250326
  };
250327
250327
 
250328
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/catalog.js
250328
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/catalog.js
250329
250329
  function provider(config3) {
250330
250330
  return config3;
250331
250331
  }
@@ -258848,7 +258848,7 @@ function isGeminiRouted(ctx) {
258848
258848
  init_core3();
258849
258849
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
258850
258850
 
258851
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/usage.js
258851
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/usage.js
258852
258852
  function formatCostUsd(costUsd) {
258853
258853
  return costUsd.toFixed(4);
258854
258854
  }
@@ -259203,7 +259203,7 @@ import { randomBytes as randomBytes2 } from "node:crypto";
259203
259203
  // package.json
259204
259204
  var package_default = {
259205
259205
  name: "infraweaver",
259206
- version: "0.3.12",
259206
+ version: "0.3.13",
259207
259207
  description: "GitHub Action that remediates Terraform to best practices and opens one scoped pull request per concern.",
259208
259208
  keywords: [
259209
259209
  "ai-coding-agent",
@@ -259306,7 +259306,7 @@ var package_default = {
259306
259306
  "@infraweaver-io/ci": "^0.1.8",
259307
259307
  "@infraweaver-io/core": "^0.1.5",
259308
259308
  "@infraweaver-io/crosswalk": "^1.2.0",
259309
- "@infraweaver-io/models": "^0.1.6",
259309
+ "@infraweaver-io/models": "^0.1.7",
259310
259310
  "@infraweaver-io/policy-spec": "^1.2.0",
259311
259311
  "@infraweaver-io/report-contract": "^0.1.5",
259312
259312
  "@infraweaver-io/sandbox": "^0.1.8",
@@ -266110,7 +266110,18 @@ var SECRET_WORDS = /* @__PURE__ */ new Set([
266110
266110
  "credential",
266111
266111
  "credentials"
266112
266112
  ]);
266113
- var SECRET_KEY_PREFIXES = /* @__PURE__ */ new Set(["api", "secret", "private", "signing"]);
266113
+ var SECRET_KEY_PREFIXES = /* @__PURE__ */ new Set([
266114
+ "api",
266115
+ "secret",
266116
+ "private",
266117
+ "signing",
266118
+ "access",
266119
+ "account",
266120
+ "client",
266121
+ "shared",
266122
+ "preshared",
266123
+ "master"
266124
+ ]);
266114
266125
  var NOT_SECRET_TOKENS = /* @__PURE__ */ new Set(["creation", "idempotency", "client", "request", "continuation"]);
266115
266126
  function words(name) {
266116
266127
  return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_-]+/).filter(Boolean);
@@ -266122,24 +266133,70 @@ function isSecretName(name) {
266122
266133
  if (last === void 0) return false;
266123
266134
  if (last === "key") return prev !== void 0 && SECRET_KEY_PREFIXES.has(prev);
266124
266135
  if (last === "token" && prev !== void 0 && NOT_SECRET_TOKENS.has(prev)) return false;
266136
+ if (last === "string") return prev === "connection";
266137
+ if (last === "secret" && w.length === 1) return false;
266125
266138
  return SECRET_WORDS.has(last);
266126
266139
  }
266127
266140
  function isPlaceholder(inner) {
266128
- return inner.trim() === "" || /^[*xX.]+$/.test(inner) || /^<[^>]*>$/.test(inner) || // the dummy credentials offline plans and LocalStack are given (`secret_key = "test"`)
266129
- /^(?:test|mock|fake|dummy|fixture|example|placeholder|sample)(?:[_-].*)?$/i.test(inner) || // a token's lifetime unit: `token_validity_units { access_token = "hours" }`
266141
+ 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 }"`
266142
+ inner.includes("%{") || // the dummy credentials offline plans and LocalStack are given (`secret_key = "test"`,
266143
+ // `"example-secret"`) — but only when that is the whole value: a lowercase tail at
266144
+ // most, so `"Test-Pr0dDbPassw0rd!"` is a password that happens to start with a word
266145
+ isDummy(inner) || // a token's lifetime unit: `token_validity_units { access_token = "hours" }`
266130
266146
  /^(?:seconds|minutes|hours|days)$/.test(inner);
266131
266147
  }
266132
- var HEADER = /^(resource|data|provider|module|locals|variable|output|terraform)\b([^{]*)\{\s*$/;
266148
+ function isDummy(inner) {
266149
+ const m = /^(?:test|mock|fake|dummy|fixture|example|placeholder|sample)([_-].*)?$/i.exec(inner);
266150
+ return m !== null && (m[1] === void 0 || /^[_-][a-z0-9_-]*$/.test(m[1]));
266151
+ }
266152
+ function isSecretReference(inner) {
266153
+ return inner.startsWith("arn:") || /^projects\/[^/]+\/secrets\//.test(inner) || /^https:\/\/[\w-]+\.vault\.azure\.net\/secrets\//i.test(inner) || /^\/[\w./-]+$/.test(inner);
266154
+ }
266155
+ var HEADER = /^(resource|data|provider|module|locals|variable|output|terraform)\b([^{]*)\{(.*)$/;
266133
266156
  var ASSIGNMENT = /^\s*(?:"([A-Za-z_][\w-]*)"|([A-Za-z_][\w-]*))\s*=(?!=)\s*(.*)$/;
266134
266157
  var HEREDOC = /<<-?([A-Za-z_]\w*)\s*$/;
266135
266158
  var INLINE_PAIR = /(?:"([A-Za-z_][\w-]*)"|\b([A-Za-z_][\w-]*))\s*=(?!=)\s*("(?:[^"\\]|\\.)*")/g;
266136
- function literalValue(raw2) {
266159
+ function literalValue(name, raw2) {
266137
266160
  const value2 = stripTrailingComment(raw2).replace(/,$/, "").trim();
266138
- return isStringLiteral(value2) && !isPlaceholder(value2.slice(1, -1));
266161
+ if (!isStringLiteral(value2)) return false;
266162
+ const inner = value2.slice(1, -1);
266163
+ if (isPlaceholder(inner) || isSecretReference(inner)) return false;
266164
+ return !(words(name).at(-1) === "auth" && /^[A-Z]+(?:_[A-Z]+)*$/.test(inner));
266165
+ }
266166
+ function variableName(holder) {
266167
+ return /^variable\s+"?([\w-]+)"?/.exec(holder)?.[1] ?? "";
266168
+ }
266169
+ function opensBlockComment(line) {
266170
+ for (let i = 0; i < line.length; i++) {
266171
+ const span = skipNonCode(line, i);
266172
+ if (!span) continue;
266173
+ if (line.startsWith("/*", i) && !line.includes("*/", i + 2)) return true;
266174
+ i = span.end;
266175
+ }
266176
+ return false;
266177
+ }
266178
+ function closesOnLine(rest) {
266179
+ let depth = 1;
266180
+ for (let i = 0; i < rest.length; i++) {
266181
+ const span = skipNonCode(rest, i);
266182
+ if (span) {
266183
+ i = span.end;
266184
+ continue;
266185
+ }
266186
+ if (rest[i] === "{") depth++;
266187
+ else if (rest[i] === "}" && --depth === 0) return true;
266188
+ }
266189
+ return false;
266139
266190
  }
266140
266191
  function findHardcodedSecrets(hcl2) {
266192
+ return findInLines(hcl2, null);
266193
+ }
266194
+ function findTfvarsSecrets(hcl2, path15) {
266195
+ return findInLines(hcl2, `tfvars input ${path15}`);
266196
+ }
266197
+ function findInLines(hcl2, fileHolder) {
266141
266198
  const out = [];
266142
- let holder = null;
266199
+ let holder = fileHolder;
266143
266200
  let heredoc = null;
266144
266201
  let inComment = false;
266145
266202
  const lines = hcl2.split("\n");
@@ -266159,12 +266216,28 @@ function findHardcodedSecrets(hcl2) {
266159
266216
  continue;
266160
266217
  }
266161
266218
  if (trimmed.startsWith("#") || trimmed.startsWith("//")) continue;
266162
- const header = HEADER.exec(line);
266219
+ if (opensBlockComment(line)) inComment = true;
266220
+ const header = fileHolder === null ? HEADER.exec(line) : null;
266163
266221
  if (header) {
266164
266222
  holder = `${header[1]}${header[2].trimEnd() ? ` ${header[2].trim()}` : ""}`;
266223
+ const rest = header[3];
266224
+ const opens2 = HEREDOC.exec(rest);
266225
+ if (opens2) heredoc = opens2[1];
266226
+ if (!holder.startsWith("output ") && !holder.startsWith("terraform")) {
266227
+ for (const pair of rest.matchAll(INLINE_PAIR)) {
266228
+ const key = pair[1] ?? pair[2];
266229
+ const isVariable = holder.startsWith("variable ");
266230
+ if (isVariable && key !== "default") continue;
266231
+ const name2 = isVariable ? variableName(holder) : key;
266232
+ if (isSecretName(name2) && literalValue(name2, pair[3])) {
266233
+ out.push({ name: name2, holder, line: i + 1 });
266234
+ }
266235
+ }
266236
+ }
266237
+ if (closesOnLine(rest)) holder = null;
266165
266238
  continue;
266166
266239
  }
266167
- if (line.startsWith("}")) {
266240
+ if (fileHolder === null && line.startsWith("}")) {
266168
266241
  holder = null;
266169
266242
  continue;
266170
266243
  }
@@ -266176,30 +266249,34 @@ function findHardcodedSecrets(hcl2) {
266176
266249
  const name = m[1] ?? m[2];
266177
266250
  const value2 = m[3];
266178
266251
  if (holder.startsWith("variable ")) {
266179
- const variable = /^variable\s+"?([\w-]+)"?/.exec(holder)?.[1] ?? "";
266180
- if (name === "default" && isSecretName(variable) && literalValue(value2)) {
266252
+ const variable = variableName(holder);
266253
+ if (name === "default" && isSecretName(variable) && literalValue(variable, value2)) {
266181
266254
  out.push({ name: variable, holder, line: i + 1 });
266182
266255
  }
266183
266256
  continue;
266184
266257
  }
266185
266258
  if (holder.startsWith("output ") || holder.startsWith("terraform")) continue;
266186
- if (isSecretName(name) && literalValue(value2)) {
266259
+ if (isSecretName(name) && literalValue(name, value2)) {
266187
266260
  out.push({ name, holder, line: i + 1 });
266188
266261
  continue;
266189
266262
  }
266190
266263
  for (const pair of value2.matchAll(INLINE_PAIR)) {
266191
266264
  const key = pair[1] ?? pair[2];
266192
- if (isSecretName(key) && literalValue(pair[3])) {
266265
+ if (isSecretName(key) && literalValue(key, pair[3])) {
266193
266266
  out.push({ name: key, holder, line: i + 1 });
266194
266267
  }
266195
266268
  }
266196
266269
  }
266197
266270
  return out;
266198
266271
  }
266199
- function scanHardcodedSecrets(files) {
266272
+ function scanHardcodedSecrets(files, tfvarsFiles = []) {
266200
266273
  const concerns = [];
266201
- for (const { path: path15, hcl: hcl2 } of files) {
266202
- for (const hit of findHardcodedSecrets(hcl2)) {
266274
+ const hits = [
266275
+ ...files.map((f) => ({ path: f.path, found: findHardcodedSecrets(f.hcl) })),
266276
+ ...tfvarsFiles.map((f) => ({ path: f.path, found: findTfvarsSecrets(f.hcl, f.path) }))
266277
+ ];
266278
+ for (const { path: path15, found } of hits) {
266279
+ for (const hit of found) {
266203
266280
  concerns.push(
266204
266281
  buildConcern({
266205
266282
  source: NATIVE_SOURCE,
@@ -268202,7 +268279,8 @@ var NATIVE_RULE_SCANNERS = [
268202
268279
  name: "hardcoded-secrets",
268203
268280
  rules: [HARDCODED_SECRET_ARGUMENT],
268204
268281
  // Per file: the argument's name and its literal are both on the line itself.
268205
- scan: (ctx) => ({ concerns: scanHardcodedSecrets(ctx.files) })
268282
+ // The only rule that reads `.tfvars`: an input file holds values, not config.
268283
+ scan: (ctx) => ({ concerns: scanHardcodedSecrets(ctx.files, ctx.tfvarsFiles) })
268206
268284
  },
268207
268285
  {
268208
268286
  name: "write-only",
@@ -268287,6 +268365,7 @@ function createRuleContext(input2) {
268287
268365
  cwd: input2.cwd,
268288
268366
  files: input2.files,
268289
268367
  tofuFiles: input2.tofuFiles ?? [],
268368
+ tfvarsFiles: input2.tfvarsFiles ?? [],
268290
268369
  truncated: input2.truncated ?? null,
268291
268370
  repeatedLiteralThreshold: input2.repeatedLiteralThreshold
268292
268371
  };
@@ -268338,6 +268417,9 @@ function readTfTree(cwd, limits) {
268338
268417
  function readTofuTree(cwd, limits) {
268339
268418
  return readTree(cwd, ".tofu", limits);
268340
268419
  }
268420
+ function readTfvarsTree(cwd, limits) {
268421
+ return readTree(cwd, ".tfvars", limits);
268422
+ }
268341
268423
  var OVERSIZE_NAMED = 5;
268342
268424
  function describeOversize(paths) {
268343
268425
  const named = paths.slice(0, OVERSIZE_NAMED).join(", ");
@@ -268366,6 +268448,9 @@ function scanNative(cwd, opts = {}) {
268366
268448
  // walk over a suffix most repos do not use at all, and threading it through
268367
268449
  // the context keeps the scanners uniform.
268368
268450
  tofuFiles: readTofuTree(cwd).files,
268451
+ // likewise only for the hardcoded-secret rule: a value kept out of the `.tf`
268452
+ // source is most often committed here instead
268453
+ tfvarsFiles: readTfvarsTree(cwd).files,
268369
268454
  truncated: tree.truncated,
268370
268455
  repeatedLiteralThreshold: opts.repeatedLiteralThreshold ?? DEFAULT_REPEAT_THRESHOLD
268371
268456
  });
@@ -272923,7 +273008,7 @@ function TerraformChangeSummaryTool(ctx) {
272923
273008
  }
272924
273009
 
272925
273010
  // src/mcp/checkout.ts
272926
- import { createHash as createHash9 } from "node:crypto";
273011
+ import { createHash as createHash10 } from "node:crypto";
272927
273012
  import { statSync as statSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
272928
273013
  import { join as join36 } from "node:path";
272929
273014
 
@@ -273331,6 +273416,9 @@ import { spawnSync as spawnSync5 } from "node:child_process";
273331
273416
  import { readFileSync as readFileSync12, rmSync as rmSync2 } from "node:fs";
273332
273417
  import { join as join22 } from "node:path";
273333
273418
 
273419
+ // src/mcp/staleFix.ts
273420
+ import { createHash as createHash6 } from "node:crypto";
273421
+
273334
273422
  // node_modules/.pnpm/@infraweaver-io+ci@0.1.8/node_modules/@infraweaver-io/ci/dist/progressComment.js
273335
273423
  function parseProgressComment(raw2) {
273336
273424
  if (!raw2?.id)
@@ -274283,7 +274371,31 @@ function groupIdFromBranch(branch) {
274283
274371
  }
274284
274372
  function remediationBranchSuffix(base, defaultBranch) {
274285
274373
  if (!base || !defaultBranch || base === defaultBranch) return "";
274286
- return `--${base.replace(/[^A-Za-z0-9._-]+/g, "-")}`;
274374
+ const slug2 = base.replace(/[^A-Za-z0-9._-]+/g, "-");
274375
+ if (slug2 === base) return `--${slug2}`;
274376
+ return `--${slug2}-${createHash6("sha1").update(base).digest("hex").slice(0, 8)}`;
274377
+ }
274378
+ function remediationBranchFor(id, base, defaultBranch, open3) {
274379
+ const computed = `remediate/${id}${remediationBranchSuffix(base, defaultBranch)}`;
274380
+ 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));
274381
+ return heads.includes(computed) ? computed : heads[0] ?? computed;
274382
+ }
274383
+ async function listOpenRemediationPrs(ctx) {
274384
+ const { octokit, repo } = ctx;
274385
+ if (ctx.toolState?.selectedMode !== "Remediate") return [];
274386
+ if (!octokit || !repo?.owner || !repo.name) return [];
274387
+ try {
274388
+ const open3 = await octokit.paginate(octokit.rest.pulls.list, {
274389
+ owner: repo.owner,
274390
+ repo: repo.name,
274391
+ state: "open",
274392
+ per_page: 100
274393
+ });
274394
+ 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 }));
274395
+ } catch (e) {
274396
+ log2.warning(`\xBB could not list open remediation PRs: ${errorMessage(e)}`);
274397
+ return [];
274398
+ }
274287
274399
  }
274288
274400
  function matchesBotIdentity(identity) {
274289
274401
  const normalized = identity.replace(/\[bot\]$/i, "").toLowerCase();
@@ -274539,13 +274651,13 @@ function ClosePullRequestTool(ctx) {
274539
274651
  }
274540
274652
 
274541
274653
  // src/mcp/terraform/emittedArtefacts.ts
274542
- import { createHash as createHash6 } from "node:crypto";
274654
+ import { createHash as createHash7 } from "node:crypto";
274543
274655
  import { writeFileSync as writeFileSync4 } from "node:fs";
274544
274656
  import { resolve as resolve6 } from "node:path";
274545
274657
  var ARTEFACT_DIR = "compliance/";
274546
274658
  function gitBlobId(bytes) {
274547
274659
  const content = Buffer.from(bytes, "utf8");
274548
- return createHash6("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
274660
+ return createHash7("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
274549
274661
  }
274550
274662
  function writeEmittedArtefact(toolState, path15, bytes) {
274551
274663
  writeFileSync4(path15, bytes);
@@ -274895,7 +275007,7 @@ function parseResourcesWithArgs(hcl2) {
274895
275007
  }
274896
275008
 
274897
275009
  // src/mcp/terraform/refactor/workspace.ts
274898
- import { createHash as createHash7 } from "node:crypto";
275010
+ import { createHash as createHash8 } from "node:crypto";
274899
275011
  import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
274900
275012
  import { join as join21, posix as posix6 } from "node:path";
274901
275013
  function readTreeAtRef(cwd, ref) {
@@ -274925,7 +275037,7 @@ function readWorkingTree(cwd) {
274925
275037
  return files;
274926
275038
  }
274927
275039
  function workingTreeFingerprint(files) {
274928
- const h = createHash7("sha256");
275040
+ const h = createHash8("sha256");
274929
275041
  for (const f of [...files].sort((a, b) => a.path.localeCompare(b.path, "en"))) {
274930
275042
  h.update(f.path);
274931
275043
  h.update("\0");
@@ -276636,7 +276748,7 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR \u2014 the equivalen
276636
276748
  - \`${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.
276637
276749
  - \`${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.
276638
276750
 
276639
- **A result marked \`report_only\` is an observation, not work.** The repository is a module other configurations call (\`report_only_reason\` says why), and restructuring it is churn for every caller: name those candidates in your \`${t("report_progress")}\` summary and open no PR for them. \`examples/\` roots are left out of every candidate list unless the operator set \`refactor_examples\`.
276751
+ **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\`.
276640
276752
 
276641
276753
  **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.
276642
276754
 
@@ -276769,7 +276881,7 @@ function remediateMode(t) {
276769
276881
 
276770
276882
  4. **for the chosen group**:
276771
276883
  - **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.
276772
- - **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch) \u2014 use it exactly, never a name of your own. Before doing anything, check whether that branch or an open PR for it already exists (\`${t("git")}\` / \`${t("get_pull_request")}\`). If one exists, update it rather than opening a duplicate.
276884
+ - **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.
276773
276885
  - **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.
276774
276886
  - **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.
276775
276887
  - **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.
@@ -276830,7 +276942,7 @@ This mode composes the **Remediate** and **Refactor** verbs into a single run. I
276830
276942
  - **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.
276831
276943
 
276832
276944
  3. **REFACTOR (phase 2) \u2014 modularise on top of the committed fix**: follow the **Refactor** mode's flow:
276833
- - \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` \u2192 pick one behaviour-preserving refactor; a result marked \`report_only\` (the repository is a module others call) is named in the summary, never proposed. Respect a pinned \`refactor_source\` if one was supplied (it constrains which module source you may use; the behaviour-altering b.4 path is only available when the operator pinned \`third-party\`).
276945
+ - \`${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\`).
276834
276946
  - 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")}\`).
276835
276947
  - \`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.
276836
276948
  - **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.
@@ -278383,15 +278495,52 @@ var Git = type({
278383
278495
  args: type.string.array().describe("Additional arguments for the git command").optional()
278384
278496
  });
278385
278497
  function splitWords(line) {
278386
- return [...line.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
278498
+ const words2 = [];
278499
+ let word = "";
278500
+ let inWord = false;
278501
+ let quote;
278502
+ for (let i = 0; i < line.length; i++) {
278503
+ const ch = line.charAt(i);
278504
+ if (quote === "'") {
278505
+ if (ch === "'") quote = void 0;
278506
+ else word += ch;
278507
+ } else if (quote === '"') {
278508
+ if (ch === '"') quote = void 0;
278509
+ else if (ch === "\\" && i + 1 < line.length && '"\\$`\n'.includes(line.charAt(i + 1))) {
278510
+ i += 1;
278511
+ if (line.charAt(i) !== "\n") word += line.charAt(i);
278512
+ } else word += ch;
278513
+ } else if (ch === " " || ch === " ") {
278514
+ if (inWord) words2.push(word);
278515
+ word = "";
278516
+ inWord = false;
278517
+ } else if (ch === "\n") {
278518
+ throw new Error(
278519
+ "command holds an unquoted newline \u2014 pass one git command per call, and quote a multi-line message"
278520
+ );
278521
+ } else if (ch === "\\") {
278522
+ if (i + 1 >= line.length) throw new Error("command ends in a backslash that escapes nothing");
278523
+ i += 1;
278524
+ if (line.charAt(i) === "\n") continue;
278525
+ word += line.charAt(i);
278526
+ inWord = true;
278527
+ } else if (ch === "'" || ch === '"') {
278528
+ quote = ch;
278529
+ inWord = true;
278530
+ } else {
278531
+ word += ch;
278532
+ inWord = true;
278533
+ }
278534
+ }
278535
+ if (quote) throw new Error(`command has an unterminated ${quote} quote`);
278536
+ if (inWord) words2.push(word);
278537
+ return words2;
278387
278538
  }
278388
278539
  function normalizeGitCall(params) {
278389
- let words2;
278390
- if (params.command === void 0) words2 = [...params.args ?? []];
278391
- else if (/^git\s/.test(params.command))
278392
- words2 = [...splitWords(params.command).slice(1), ...params.args ?? []];
278393
- else return { command: params.command, args: params.args ?? [] };
278394
- const [command, ...args2] = words2[0] === "git" ? words2.slice(1) : words2;
278540
+ const words2 = params.command === void 0 ? [] : /^git\s/.test(params.command) ? splitWords(params.command) : [params.command];
278541
+ words2.push(...params.args ?? []);
278542
+ while (words2[0] === "git") words2.shift();
278543
+ const [command, ...args2] = words2;
278395
278544
  if (command === void 0 || !/^[a-z][a-z0-9-]*$/.test(command)) {
278396
278545
  throw new Error(
278397
278546
  `command must be a Git subcommand such as 'status' or 'checkout' (was "${command ?? ""}")`
@@ -278827,7 +278976,7 @@ function claimsBlockingSeverity(body2) {
278827
278976
  init_core3();
278828
278977
  import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
278829
278978
 
278830
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/monotonicQuota.js
278979
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/monotonicQuota.js
278831
278980
  function createMonotonicQuota(params) {
278832
278981
  const { limit, unlimited, normalize: normalize5 } = params;
278833
278982
  let used = 0;
@@ -278852,7 +279001,7 @@ function createMonotonicQuota(params) {
278852
279001
  };
278853
279002
  }
278854
279003
 
278855
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/costQuota.js
279004
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/costQuota.js
278856
279005
  var CostQuotaExceededError = class extends Error {
278857
279006
  code = "COST_QUOTA_EXCEEDED";
278858
279007
  constructor(message2) {
@@ -278899,7 +279048,7 @@ function resolveRunCostQuota(parsed2) {
278899
279048
  return parsed2 === void 0 ? DEFAULT_RUN_COST_QUOTA_USD : parsed2;
278900
279049
  }
278901
279050
 
278902
- // node_modules/.pnpm/@infraweaver-io+models@0.1.6/node_modules/@infraweaver-io/models/dist/tokenQuota.js
279051
+ // node_modules/.pnpm/@infraweaver-io+models@0.1.7/node_modules/@infraweaver-io/models/dist/tokenQuota.js
278903
279052
  var TokenQuotaExceededError = class extends Error {
278904
279053
  code = "TOKEN_QUOTA_EXCEEDED";
278905
279054
  constructor(message2) {
@@ -278909,19 +279058,16 @@ var TokenQuotaExceededError = class extends Error {
278909
279058
  };
278910
279059
  var DEFAULT_RUN_TOKEN_QUOTA = 1e7;
278911
279060
  var RUN_TOKEN_QUOTA_INPUT = "max_run_tokens";
278912
- function billableTokens(t) {
278913
- let total = 0;
278914
- for (const v of [t.input, t.output, t.cacheRead, t.cacheWrite]) {
278915
- if (typeof v === "number" && Number.isFinite(v) && v > 0)
278916
- total += v;
278917
- }
278918
- return Math.floor(total);
279061
+ var CACHE_READ_WEIGHT = 0.1;
279062
+ function quotaTokens(t) {
279063
+ const part = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
279064
+ return Math.floor(part(t.input) + part(t.output) + part(t.cacheWrite) + part(t.cacheRead) * CACHE_READ_WEIGHT);
278919
279065
  }
278920
279066
  function normalizeTotal(total) {
278921
- return typeof total === "number" ? Math.max(0, Math.floor(total)) : billableTokens(total);
279067
+ return typeof total === "number" ? Math.max(0, Math.floor(total)) : quotaTokens(total);
278922
279068
  }
278923
279069
  function tokenGap(used, ceiling) {
278924
- 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.`;
279070
+ 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.`;
278925
279071
  }
278926
279072
  function unlimitedTokenQuota() {
278927
279073
  return createMonotonicQuota({
@@ -280064,6 +280210,12 @@ function parseCommentMarker(body2) {
280064
280210
  return null;
280065
280211
  }
280066
280212
  }
280213
+ function findingKeysIn(body2) {
280214
+ return [...body2.matchAll(new RegExp(MARKER.source, "g"))].flatMap((m) => {
280215
+ const marker2 = parseCommentMarker(m[0]);
280216
+ return marker2?.kind === "finding" ? [marker2.key] : [];
280217
+ });
280218
+ }
280067
280219
  var SUGGESTION = /```suggestion\r?\n([\s\S]*?)```/;
280068
280220
  function suggestionText(body2) {
280069
280221
  const m = SUGGESTION.exec(body2);
@@ -280220,23 +280372,54 @@ function computeScanDelta(input2) {
280220
280372
  };
280221
280373
  }
280222
280374
  function markPossibleRenames(introduced, resolved, headAddresses) {
280223
- const vanished = resolved.filter((r) => {
280224
- if (r.address === null) return false;
280225
- return !headAddresses(dirOf3(r.concern.location.file)).has(r.address);
280226
- });
280375
+ const byResource = (findings) => {
280376
+ const out = /* @__PURE__ */ new Map();
280377
+ for (const f of [...findings].sort((a, b) => byLocation(a.concern, b.concern))) {
280378
+ if (f.address === null) continue;
280379
+ const dir = dirOf3(f.concern.location.file);
280380
+ const key = `${dir}|${f.address}`;
280381
+ const entry4 = out.get(key);
280382
+ if (entry4) entry4.findings.push(f);
280383
+ else out.set(key, { dir, address: f.address, findings: [f] });
280384
+ }
280385
+ return [...out.values()];
280386
+ };
280387
+ const vanished = byResource(resolved).filter((r) => !headAddresses(r.dir).has(r.address));
280388
+ const appeared = byResource(introduced);
280389
+ const shared = (from, to) => {
280390
+ const left = /* @__PURE__ */ new Map();
280391
+ for (const f of from.findings)
280392
+ left.set(ruleKey(f.concern), (left.get(ruleKey(f.concern)) ?? 0) + 1);
280393
+ return to.findings.filter((f) => {
280394
+ const n = left.get(ruleKey(f.concern)) ?? 0;
280395
+ if (n === 0) return false;
280396
+ left.set(ruleKey(f.concern), n - 1);
280397
+ return true;
280398
+ });
280399
+ };
280400
+ const order2 = (a, b) => a.dir.localeCompare(b.dir) || a.address.localeCompare(b.address);
280401
+ const candidates = vanished.flatMap(
280402
+ (from) => appeared.filter(
280403
+ (to) => (
280404
+ // a `moved` block cannot change a resource's type
280405
+ typeOf(from.address) === typeOf(to.address) && // renamed in place, or moved elsewhere under its own name
280406
+ from.dir === to.dir !== (from.address === to.address)
280407
+ )
280408
+ ).map((to) => ({ from, to, findings: shared(from, to) })).filter((p) => p.findings.length > 0)
280409
+ );
280410
+ candidates.sort(
280411
+ (a, b) => b.findings.length - a.findings.length || order2(a.from, b.from) || order2(a.to, b.to)
280412
+ );
280227
280413
  const used = /* @__PURE__ */ new Set();
280228
- for (const finding of introduced) {
280229
- if (finding.address === null) continue;
280230
- const dir = dirOf3(finding.concern.location.file);
280231
- const match5 = vanished.find(
280232
- (r) => !used.has(r) && ruleKey(r.concern) === ruleKey(finding.concern) && // a `moved` block cannot change a resource's type
280233
- typeOf(r.address) === typeOf(finding.address) && // renamed in place, or moved elsewhere under its own name
280234
- dirOf3(r.concern.location.file) === dir !== (r.address === finding.address)
280235
- );
280236
- if (!match5) continue;
280237
- used.add(match5);
280238
- finding.possiblyPreexisting = true;
280239
- finding.renamedFrom = { address: match5.address, file: match5.concern.location.file };
280414
+ for (const { from, to, findings } of candidates) {
280415
+ if (used.has(from) || used.has(to)) continue;
280416
+ used.add(from);
280417
+ used.add(to);
280418
+ const file2 = from.findings[0]?.concern.location.file;
280419
+ for (const finding of findings) {
280420
+ finding.possiblyPreexisting = true;
280421
+ finding.renamedFrom = { address: from.address, file: file2 };
280422
+ }
280240
280423
  }
280241
280424
  }
280242
280425
  function typeOf(address) {
@@ -280505,39 +280688,115 @@ function formatDuplicateCommentsNote(duplicates) {
280505
280688
  var MAX_DUPLICATE_COMMENT_LINES = 50;
280506
280689
 
280507
280690
  // src/mcp/reviewCommentLimit.ts
280508
- var MAX_OVERFLOW_LINES = 50;
280691
+ var MAX_NOTE_CHARS = 3e4;
280692
+ var TRAILER_RESERVE = 400;
280509
280693
  var PROVEN_MARK = "\u2705 **Proven by re-scan**";
280510
- function rank(c) {
280694
+ var SEVERITY_LEVEL = {
280695
+ critical: 0,
280696
+ high: 1,
280697
+ medium: 2,
280698
+ low: 3,
280699
+ info: 4
280700
+ };
280701
+ var MARKER_LEVEL = { "\u{1F6A8}": 1, "\u26A0\uFE0F": 2, "\u26A0": 2, "\u2139\uFE0F": 3, \u2139: 3 };
280702
+ var UNMARKED_LEVEL = 2;
280703
+ function rank(c, severity) {
280511
280704
  const body2 = c.body ?? "";
280512
- const marker2 = leadingMarker(body2);
280513
- const severity = marker2 === "\u{1F6A8}" ? 0 : marker2 === "\u26A0\uFE0F" || marker2 === "\u26A0" ? 2 : 4;
280514
- return severity + (body2.includes(PROVEN_MARK) ? 0 : 1);
280705
+ const level = severity ? SEVERITY_LEVEL[severity] : MARKER_LEVEL[leadingMarker(body2) ?? ""] ?? UNMARKED_LEVEL;
280706
+ return level * 2 + (body2.includes(PROVEN_MARK) ? 0 : 1);
280515
280707
  }
280516
- function limitInlineComments(comments, limit) {
280708
+ function limitInlineComments(comments, limit, severityOf) {
280517
280709
  if (!limit || comments.length <= limit) return { kept: [...comments], overflow: [] };
280518
280710
  const keep = new Set(
280519
- 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)
280711
+ 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)
280520
280712
  );
280521
280713
  return {
280522
280714
  kept: comments.filter((_, i) => keep.has(i)),
280523
280715
  overflow: comments.filter((_, i) => !keep.has(i))
280524
280716
  };
280525
280717
  }
280526
- function formatOverflowNote(overflow, limit) {
280718
+ var escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
280719
+ var whereOf = (c) => c.line ? `${c.path}:${c.line}` : c.path;
280720
+ function closesItself(body2) {
280721
+ const occurrences = (re) => body2.match(re)?.length ?? 0;
280722
+ const fences = body2.split("\n").filter((l) => /^\s*(```|~~~)/.test(l)).length;
280723
+ return fences % 2 === 0 && occurrences(/<details\b/gi) === occurrences(/<\/details\s*>/gi) && occurrences(/<!--/g) === occurrences(/-->/g);
280724
+ }
280725
+ function asText(body2) {
280726
+ const longest = Math.max(0, ...(body2.match(/`+/g) ?? []).map((run2) => run2.length));
280727
+ const fence = "`".repeat(Math.max(3, longest + 1));
280728
+ return `${fence}text
280729
+ ${body2}
280730
+ ${fence}`;
280731
+ }
280732
+ function renderWhole(c, linkOf) {
280733
+ const where4 = escapeHtml(whereOf(c));
280734
+ const gist = gistOf(c.body);
280735
+ const href = linkOf?.(c);
280736
+ const body2 = c.body ?? "";
280737
+ return [
280738
+ `<details><summary><code>${where4}</code>${gist ? ` \u2014 ${gist}` : ""}</summary>`,
280739
+ ...href ? [`<a href="${escapeHtml(href)}">${where4}</a>`] : [],
280740
+ ...body2 ? [closesItself(body2) ? body2 : asText(body2)] : [],
280741
+ "</details>"
280742
+ ].join("\n\n");
280743
+ }
280744
+ function renderBrief(c) {
280745
+ const gist = gistOf(c.body);
280746
+ const markers = findingKeysIn(c.body ?? "").map(
280747
+ (key) => renderCommentMarker({ kind: "finding", key, proven: false })
280748
+ );
280749
+ return `- \`${whereOf(c).replace(/`/g, "'")}\`${gist ? ` \u2014 ${gist}` : ""}${markers.map((m) => ` ${m}`).join("")}`;
280750
+ }
280751
+ function formatOverflowNote(overflow, limit, linkOf) {
280527
280752
  if (overflow.length === 0) return "";
280528
- const shown = overflow.slice(0, MAX_OVERFLOW_LINES).map((c) => {
280529
- const where4 = c.line ? `${c.path}:${c.line}` : c.path;
280530
- const gist = gistOf(c.body);
280531
- return `- \`${where4.replace(/`/g, "'")}\`${gist ? ` \u2014 ${gist}` : ""}`;
280532
- });
280533
- const rest = overflow.length - shown.length;
280534
- if (rest > 0) shown.push(`- \u2026and ${rest} more not shown`);
280535
- return `
280753
+ const header = `
280536
280754
 
280537
280755
  ---
280538
280756
 
280539
- **Note:** this review posts at most ${limit} inline comments (\`review_comment_limit\`), so ${overflow.length} more ${overflow.length === 1 ? "is" : "are"} listed here instead:
280540
- ` + shown.join("\n");
280757
+ **Note:** this review posts at most ${limit} inline comments (\`review_comment_limit\`), so ${overflow.length} more ${overflow.length === 1 ? "is" : "are"} listed here instead:`;
280758
+ const SEP = 2;
280759
+ let room = MAX_NOTE_CHARS - TRAILER_RESERVE - header.length;
280760
+ const items = [];
280761
+ for (const c of overflow) {
280762
+ const brief2 = renderBrief(c);
280763
+ if (brief2.length + SEP > room) break;
280764
+ items.push(brief2);
280765
+ room -= brief2.length + SEP;
280766
+ }
280767
+ let brief = items.length;
280768
+ for (const [i, c] of overflow.slice(0, items.length).entries()) {
280769
+ const whole = renderWhole(c, linkOf);
280770
+ const extra = whole.length - items[i].length;
280771
+ if (extra > room) continue;
280772
+ items[i] = whole;
280773
+ room -= extra;
280774
+ brief--;
280775
+ }
280776
+ const omitted = overflow.length - items.length;
280777
+ if (omitted > 0) items.push(`- \u2026and ${omitted} more not shown`);
280778
+ const cut = [
280779
+ brief > 0 ? `${brief} of them ${brief === 1 ? "is" : "are"} shown by place and first line only` : "",
280780
+ omitted > 0 ? `${omitted} ${omitted === 1 ? "is" : "are"} not shown at all` : ""
280781
+ ].filter(Boolean);
280782
+ if (cut.length > 0) {
280783
+ items.push(`${cut.join(" and ")}, to keep this review within GitHub's size limit.`);
280784
+ }
280785
+ return `${header}
280786
+
280787
+ ${items.join("\n\n")}`;
280788
+ }
280789
+ function foldedFindingsStillIntroduced(input2) {
280790
+ const folded = new Set(
280791
+ input2.reviews.flatMap(
280792
+ (r) => r.body && isOwnReviewComment(r.user, r.body) ? findingKeysIn(r.body) : []
280793
+ )
280794
+ );
280795
+ if (folded.size === 0) return [];
280796
+ const dismissed = new Set(input2.dismissed.map((d) => d.finding_id));
280797
+ return input2.delta.introduced.filter(
280798
+ (f) => folded.has(findingKey(f)) && !dismissed.has(f.concern.id)
280799
+ );
280541
280800
  }
280542
280801
 
280543
280802
  // src/mcp/reviewComments.ts
@@ -281368,7 +281627,8 @@ function deltaVerdict(delta, requiredControls) {
281368
281627
  return {
281369
281628
  pass: gate.pass && required3.length === 0,
281370
281629
  gating: gate.gating.length,
281371
- required: required3.length
281630
+ required: required3.length,
281631
+ blocking: (/* @__PURE__ */ new Set([...gate.gating, ...required3])).size
281372
281632
  };
281373
281633
  }
281374
281634
  function failsGate(finding, requiredControls) {
@@ -281495,7 +281755,7 @@ function where2(f) {
281495
281755
  function renderReviewVerdict(input2) {
281496
281756
  const { delta, verdict: verdict2 } = input2;
281497
281757
  const introduced = delta.introduced.length;
281498
- const blocking = verdict2.gating + verdict2.required;
281758
+ const { blocking } = verdict2;
281499
281759
  const renamedBlocking = delta.introduced.filter(
281500
281760
  (f) => f.possiblyPreexisting && failsGate(f, [...input2.requiredControls ?? []])
281501
281761
  ).length;
@@ -281604,7 +281864,7 @@ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, rmSync as
281604
281864
  import { join as join28, posix as posix8 } from "node:path";
281605
281865
 
281606
281866
  // src/mcp/terraform/lineage.ts
281607
- import { createHash as createHash8 } from "node:crypto";
281867
+ import { createHash as createHash9 } from "node:crypto";
281608
281868
  import { readFileSync as readFileSync15 } from "node:fs";
281609
281869
  import { join as join27 } from "node:path";
281610
281870
  function repoFileReader(cwd) {
@@ -281617,7 +281877,7 @@ function repoFileReader(cwd) {
281617
281877
  };
281618
281878
  }
281619
281879
  function lineageKeyOf(c, address) {
281620
- return createHash8("sha1").update(`${c.source}|${bareRuleOf(c.source, c.rule_id)}|${c.location.file}|${address ?? ""}`).digest("hex").slice(0, 16);
281880
+ return createHash9("sha1").update(`${c.source}|${bareRuleOf(c.source, c.rule_id)}|${c.location.file}|${address ?? ""}`).digest("hex").slice(0, 16);
281621
281881
  }
281622
281882
  function lineageResolver(read3) {
281623
281883
  const files = /* @__PURE__ */ new Map();
@@ -282249,15 +282509,17 @@ function siteOf(c, read3) {
282249
282509
  const text = read3(c.location.file);
282250
282510
  return text === null ? null : resourceAddressAt(text, c.location.line);
282251
282511
  }
282252
- function recordConcernSites(session, concerns, cwd) {
282512
+ function recordConcernSites(session, concerns, cwd, linesMatchTree) {
282253
282513
  const read3 = fileReader(cwd);
282254
282514
  session.siteById ??= /* @__PURE__ */ new Map();
282255
282515
  session.firstSitesByKey ??= /* @__PURE__ */ new Map();
282256
282516
  const first = session.firstSitesByKey;
282257
282517
  const fresh = /* @__PURE__ */ new Map();
282258
282518
  for (const c of concerns) {
282259
- const site = siteOf(c, read3);
282260
- session.siteById.set(c.id, site);
282519
+ const site = linesMatchTree ? siteOf(c, read3) : null;
282520
+ const prior = session.siteById.get(c.id);
282521
+ session.siteById.set(c.id, prior === void 0 || prior === site ? site : null);
282522
+ if (!linesMatchTree) continue;
282261
282523
  const key = concernKeyOf(c);
282262
282524
  if (first.has(key)) continue;
282263
282525
  const sites = fresh.get(key) ?? /* @__PURE__ */ new Set();
@@ -282313,7 +282575,7 @@ function buildConcernResult(ctx, rawConcerns, opts) {
282313
282575
  fullBaseline.map((c) => concernKeyOf(c))
282314
282576
  );
282315
282577
  recordConcernKeys(session, fullBaseline);
282316
- recordConcernSites(session, fullBaseline, opts.cwd);
282578
+ recordConcernSites(session, fullBaseline, opts.cwd, opts.linesMatchTree ?? false);
282317
282579
  session.baselineFmtDriftFiles = union3(
282318
282580
  session.baselineFmtDriftFiles,
282319
282581
  fullBaseline.filter((c) => c.source === "terraform-fmt").map((c) => c.location.file)
@@ -282328,13 +282590,13 @@ function buildConcernResult(ctx, rawConcerns, opts) {
282328
282590
  active,
282329
282591
  opts.autonomyThreshold
282330
282592
  );
282331
- const suffix2 = remediationBranchSuffix(
282332
- remediationBaseOf(ctx),
282333
- ctx.repo?.data.default_branch?.trim() || void 0
282334
- );
282335
- const groups2 = (opts.priority ? prioritizeGroups(annotatedGroups, active, opts.priority) : annotatedGroups).map((g) => ({ ...g, branch: `remediate/${g.id}${suffix2}` }));
282593
+ const base = remediationBaseOf(ctx);
282594
+ const defaultBranch = ctx.repo?.data.default_branch?.trim() || void 0;
282595
+ const branchFor = (id) => remediationBranchFor(id, base, defaultBranch, opts.openRemediationPrs);
282596
+ const groups2 = (opts.priority ? prioritizeGroups(annotatedGroups, active, opts.priority) : annotatedGroups).map((g) => ({ ...g, branch: branchFor(g.id) }));
282336
282597
  const planned = planBatches(groups2);
282337
- const batchPlan = planned.batch_branch ? { ...planned, batch_branch: `${planned.batch_branch}${suffix2}` } : planned;
282598
+ const batchId = planned.batch_branch ? groupIdFromBranch(planned.batch_branch) : null;
282599
+ const batchPlan = batchId ? { ...planned, batch_branch: branchFor(batchId) } : planned;
282338
282600
  const bySeverity = {};
282339
282601
  for (const c of active) bySeverity[c.severity] = (bySeverity[c.severity] ?? 0) + 1;
282340
282602
  return {
@@ -282630,7 +282892,9 @@ async function runScan(ctx, {
282630
282892
  grouping,
282631
282893
  autonomyThreshold,
282632
282894
  inScope,
282633
- priority
282895
+ priority,
282896
+ openRemediationPrs: await listOpenRemediationPrs(ctx),
282897
+ linesMatchTree: true
282634
282898
  }
282635
282899
  );
282636
282900
  const { active, suppressed, groups: groups2, batchPlan, bySeverity: by_severity } = result;
@@ -283271,6 +283535,22 @@ function CreatePullRequestReviewTool(ctx) {
283271
283535
  `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.`
283272
283536
  );
283273
283537
  }
283538
+ if (delta) {
283539
+ const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
283540
+ owner: ctx.repo.owner,
283541
+ repo: ctx.repo.name,
283542
+ pull_number,
283543
+ per_page: 100
283544
+ });
283545
+ const folded = foldedFindingsStillIntroduced({ reviews, delta, dismissed });
283546
+ if (folded.length > 0) {
283547
+ throw new Error(
283548
+ `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(
283549
+ (f) => `${f.concern.id} (${f.concern.rule_id} at ${f.concern.location.file}:${f.concern.location.line ?? "?"})`
283550
+ ).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.`
283551
+ );
283552
+ }
283553
+ }
283274
283554
  }
283275
283555
  const skip = reviewSkipDecision({
283276
283556
  approved: approved ?? false,
@@ -283377,6 +283657,7 @@ ${formatReanchoredNote(reanchored.moved)}`
283377
283657
  params.event = event;
283378
283658
  }
283379
283659
  const proofs = delta ? await proveReviewSuggestions(ctx, auditedComments) : /* @__PURE__ */ new Map();
283660
+ const findingSeverity = /* @__PURE__ */ new Map();
283380
283661
  const reviewComments = auditedComments.map((comment, index) => {
283381
283662
  let commentBody = fixDoubleEscapedString(comment.body || "");
283382
283663
  if (comment.suggestion !== void 0) {
@@ -283418,6 +283699,9 @@ ${marker2}`;
283418
283699
  reviewComment.start_line = comment.start_line;
283419
283700
  reviewComment.start_side = side;
283420
283701
  }
283702
+ if (comment.provenance?.kind === "scanner") {
283703
+ findingSeverity.set(reviewComment, comment.provenance.finding.concern.severity);
283704
+ }
283421
283705
  return reviewComment;
283422
283706
  });
283423
283707
  let droppedComments = [];
@@ -283453,7 +283737,11 @@ ${marker2}`;
283453
283737
  `withholding ${duplicateComments.length}/${reviewComments.length} inline comment(s) that duplicate open review threads`
283454
283738
  );
283455
283739
  }
283456
- const limited = limitInlineComments(deduped.kept, ctx.payload.reviewCommentLimit);
283740
+ const limited = limitInlineComments(
283741
+ deduped.kept,
283742
+ ctx.payload.reviewCommentLimit,
283743
+ (c) => findingSeverity.get(c)
283744
+ );
283457
283745
  overflowComments = limited.overflow;
283458
283746
  if (overflowComments.length > 0) {
283459
283747
  log2.info(
@@ -283485,7 +283773,13 @@ ${marker2}`;
283485
283773
  body2 = body2 ? body2 + note : note.replace(/^\n\n/, "");
283486
283774
  }
283487
283775
  if (overflowComments.length > 0) {
283488
- const note = formatOverflowNote(overflowComments, ctx.payload.reviewCommentLimit);
283776
+ const server = process.env.GITHUB_SERVER_URL || "https://github.com";
283777
+ const commit = params.commit_id;
283778
+ const note = formatOverflowNote(
283779
+ overflowComments,
283780
+ ctx.payload.reviewCommentLimit,
283781
+ 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
283782
+ );
283489
283783
  body2 = body2 ? body2 + note : note.replace(/^\n\n/, "");
283490
283784
  }
283491
283785
  let reproved = 0;
@@ -284497,7 +284791,7 @@ function formatFilesWithLineNumbers(files) {
284497
284791
  }
284498
284792
  const tocLines = [`## Files (${files.length})`];
284499
284793
  for (const entry4 of tocEntries) {
284500
- const anchor2 = createHash9("sha256").update(entry4.filename).digest("hex");
284794
+ const anchor2 = createHash10("sha256").update(entry4.filename).digest("hex");
284501
284795
  tocLines.push(
284502
284796
  `- ${entry4.filename} \u2192 lines ${entry4.startLine}-${entry4.endLine} \xB7 diff-${anchor2}`
284503
284797
  );
@@ -286526,12 +286820,47 @@ function findPartialInterfaces(cwd) {
286526
286820
  }
286527
286821
 
286528
286822
  // src/mcp/terraform/refactorScope.ts
286529
- import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync23, statSync as statSync6 } from "node:fs";
286823
+ import { readdirSync as readdirSync7, readFileSync as readFileSync23, statSync as statSync6 } from "node:fs";
286530
286824
  import { join as join42 } from "node:path";
286531
286825
  function isExamplePath(path15) {
286532
- return /(?:^|\/)examples?\//.test(path15.replace(/\\/g, "/"));
286826
+ return /(?:^|\/)examples\/|^example\//i.test(path15.replace(/\\/g, "/"));
286827
+ }
286828
+ var PROVIDER_OR_BACKEND = new RegExp(
286829
+ String.raw`(?:^|\n)\s*(?:provider|backend)\s+${BLOCK_LABEL}\s*\{`
286830
+ );
286831
+ var CLOUD_BLOCK = /(?:^|\n)\s*cloud\s*\{/;
286832
+ function configuresDeployment(name, text) {
286833
+ if (name.endsWith(".tf.json")) {
286834
+ let doc;
286835
+ try {
286836
+ doc = JSON.parse(text);
286837
+ } catch {
286838
+ return false;
286839
+ }
286840
+ if (typeof doc !== "object" || doc === null) return false;
286841
+ const { provider: provider2, terraform } = doc;
286842
+ if (provider2 !== void 0) return true;
286843
+ return [terraform].flat().some((t) => typeof t === "object" && t !== null && ("backend" in t || "cloud" in t));
286844
+ }
286845
+ if (PROVIDER_OR_BACKEND.test(text)) return true;
286846
+ return parseBlocks(text, ["terraform"]).some((b) => CLOUD_BLOCK.test(b.body));
286847
+ }
286848
+ var isTerraformFile2 = (name) => name.endsWith(".tf") || name.endsWith(".tf.json");
286849
+ function deploysFrom(dir) {
286850
+ let names;
286851
+ try {
286852
+ names = readdirSync7(dir).filter(isTerraformFile2);
286853
+ } catch {
286854
+ return false;
286855
+ }
286856
+ return names.some((name) => {
286857
+ try {
286858
+ return configuresDeployment(name, readFileSync23(join42(dir, name), "utf8"));
286859
+ } catch {
286860
+ return false;
286861
+ }
286862
+ });
286533
286863
  }
286534
- var PROVIDER_OR_BACKEND = /^\s*(?:provider\s+"|backend\s+")/m;
286535
286864
  function holdsTerraform(dir, depth) {
286536
286865
  let entries;
286537
286866
  try {
@@ -286549,26 +286878,35 @@ function holdsTerraform(dir, depth) {
286549
286878
  return false;
286550
286879
  }
286551
286880
  function libraryRepoReason(cwd) {
286552
- let rootFiles;
286881
+ let rootEntries;
286553
286882
  try {
286554
- rootFiles = readdirSync7(cwd).filter((f) => f.endsWith(".tf"));
286883
+ rootEntries = readdirSync7(cwd);
286555
286884
  } catch {
286556
286885
  return null;
286557
286886
  }
286558
- if (rootFiles.length === 0) return null;
286559
- for (const f of rootFiles) {
286560
- let text;
286561
- try {
286562
- text = readFileSync23(join42(cwd, f), "utf8");
286563
- } catch {
286564
- continue;
286565
- }
286566
- if (PROVIDER_OR_BACKEND.test(text)) return null;
286567
- }
286568
- const examples = ["examples", "example"].find((d) => existsSync10(join42(cwd, d)));
286887
+ if (!rootEntries.some(isTerraformFile2) || deploysFrom(cwd)) return null;
286888
+ const examples = rootEntries.find((d) => /^examples?$/i.test(d));
286569
286889
  if (examples === void 0 || !holdsTerraform(join42(cwd, examples), 2)) return null;
286570
286890
  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`;
286571
286891
  }
286892
+ function libraryScope(cwd) {
286893
+ const reason = libraryRepoReason(cwd);
286894
+ if (reason === null) return () => null;
286895
+ const deploys = /* @__PURE__ */ new Map();
286896
+ return (file2) => {
286897
+ const dirs = file2.replace(/\\/g, "/").split("/").slice(0, -1);
286898
+ for (let i = dirs.length; i > 0; i--) {
286899
+ const dir = dirs.slice(0, i).join("/");
286900
+ let d = deploys.get(dir);
286901
+ if (d === void 0) {
286902
+ d = deploysFrom(join42(cwd, dir));
286903
+ deploys.set(dir, d);
286904
+ }
286905
+ if (d) return null;
286906
+ }
286907
+ return reason;
286908
+ };
286909
+ }
286572
286910
  function reportOnly(reason) {
286573
286911
  return reason === null ? {} : { report_only: true, report_only_reason: reason };
286574
286912
  }
@@ -286926,12 +287264,12 @@ function ModuleExtractionCandidatesTool(ctx) {
286926
287264
  parameters: ModuleExtractionCandidatesParams,
286927
287265
  execute: execute(async () => {
286928
287266
  const cwd = ctx.payload.cwd ?? process.cwd();
287267
+ const library = libraryScope(cwd);
286929
287268
  const candidates = findExtractionCandidates(
286930
287269
  cwd,
286931
287270
  ctx.payload.moduleCatalogue,
286932
287271
  ctx.toolState.orgModuleHcl
286933
- ).filter((c) => ctx.payload.refactorExamples || !isExamplePath(c.cluster.file));
286934
- const library = libraryRepoReason(cwd);
287272
+ ).filter((c) => ctx.payload.refactorExamples || !isExamplePath(c.cluster.file)).map((c) => ({ ...reportOnly(library(c.cluster.file)), ...c }));
286935
287273
  const matched = candidates.filter((c) => c.candidates.length > 0);
286936
287274
  const reasons = {};
286937
287275
  for (const c of candidates)
@@ -286945,7 +287283,6 @@ function ModuleExtractionCandidatesTool(ctx) {
286945
287283
  `\xBB module_extraction_candidates: ${candidates.length} cluster(s), ${matched.length} with a module match` + (partialInterfaces.length > 0 ? `, ${partialInterfaces.length} partial-interface block(s)` : "")
286946
287284
  );
286947
287285
  return toolOk({
286948
- ...reportOnly(library),
286949
287286
  cluster_count: candidates.length,
286950
287287
  matched_count: matched.length,
286951
287288
  candidates,
@@ -286969,7 +287306,7 @@ function ModuleExtractionCandidatesTool(ctx) {
286969
287306
  }
286970
287307
 
286971
287308
  // src/mcp/moduleTests.ts
286972
- import { existsSync as existsSync11, readdirSync as readdirSync8, readFileSync as readFileSync25, statSync as statSync7 } from "node:fs";
287309
+ import { existsSync as existsSync10, readdirSync as readdirSync8, readFileSync as readFileSync25, statSync as statSync7 } from "node:fs";
286973
287310
  import { join as join44 } from "node:path";
286974
287311
  var MODULE_META_ARGS = /* @__PURE__ */ new Set([
286975
287312
  "source",
@@ -287090,7 +287427,7 @@ function resolveSourceDir(fromDir, source) {
287090
287427
  }
287091
287428
  function isDir(p) {
287092
287429
  try {
287093
- return existsSync11(p) && statSync7(p).isDirectory();
287430
+ return existsSync10(p) && statSync7(p).isDirectory();
287094
287431
  } catch {
287095
287432
  return false;
287096
287433
  }
@@ -287284,7 +287621,7 @@ function SetOutputTool(ctx, outputSchema) {
287284
287621
 
287285
287622
  // src/mcp/policy.ts
287286
287623
  import { spawnSync as spawnSync8 } from "node:child_process";
287287
- import { existsSync as existsSync12, readFileSync as readFileSync26 } from "node:fs";
287624
+ import { existsSync as existsSync11, readFileSync as readFileSync26 } from "node:fs";
287288
287625
  import { join as join45 } from "node:path";
287289
287626
  function parseConftestOutput(stdout) {
287290
287627
  let parsed2;
@@ -287336,7 +287673,7 @@ function namespaceArgs(conftestCwd) {
287336
287673
  function findPlanJson(cwd) {
287337
287674
  for (const candidate of PLAN_JSON_NAMES) {
287338
287675
  const abs = join45(cwd, candidate);
287339
- if (existsSync12(abs)) return abs;
287676
+ if (existsSync11(abs)) return abs;
287340
287677
  }
287341
287678
  return null;
287342
287679
  }
@@ -287377,11 +287714,11 @@ function resolvePolicyDir(cwd, explicit) {
287377
287714
  } catch {
287378
287715
  return null;
287379
287716
  }
287380
- return existsSync12(abs) ? abs : null;
287717
+ return existsSync11(abs) ? abs : null;
287381
287718
  }
287382
287719
  for (const d of DEFAULT_POLICY_DIRS) {
287383
287720
  const abs = join45(cwd, d);
287384
- if (existsSync12(abs)) return abs;
287721
+ if (existsSync11(abs)) return abs;
287385
287722
  }
287386
287723
  return null;
287387
287724
  }
@@ -287415,7 +287752,7 @@ function PolicyCheckTool(ctx) {
287415
287752
  } catch {
287416
287753
  abs = null;
287417
287754
  }
287418
- targetFile = abs && existsSync12(abs) ? abs : null;
287755
+ targetFile = abs && existsSync11(abs) ? abs : null;
287419
287756
  if (!targetFile) {
287420
287757
  return toolSkip(
287421
287758
  "target_not_found",
@@ -287861,7 +288198,7 @@ function SelectModeTool(ctx) {
287861
288198
  }
287862
288199
 
287863
288200
  // src/mcp/terraform/tools/advise.ts
287864
- import { existsSync as existsSync13, readFileSync as readFileSync28 } from "node:fs";
288201
+ import { existsSync as existsSync12, readFileSync as readFileSync28 } from "node:fs";
287865
288202
  import { join as join48 } from "node:path";
287866
288203
 
287867
288204
  // src/mcp/terraform/fixMemory.ts
@@ -288020,7 +288357,7 @@ function inlineWaivers(content, file2) {
288020
288357
  }
288021
288358
  function readLearnings(tmpdir5) {
288022
288359
  const path15 = join48(tmpdir5, LEARNINGS_FILE);
288023
- if (!existsSync13(path15)) return [];
288360
+ if (!existsSync12(path15)) return [];
288024
288361
  try {
288025
288362
  return readFileSync28(path15, "utf-8").split("\n");
288026
288363
  } catch {
@@ -289885,7 +290222,8 @@ function DetectPlanDriftTool(ctx) {
289885
290222
  autonomyThreshold,
289886
290223
  // drift is a narrow concern source — join any prior scan baseline so a
289887
290224
  // later ✗→✓ verify doesn't read the whole workspace as regressions.
289888
- baselineMode: "merge"
290225
+ baselineMode: "merge",
290226
+ openRemediationPrs: await listOpenRemediationPrs(ctx)
289889
290227
  });
289890
290228
  log2.info(
289891
290229
  `\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)` : "")
@@ -290377,7 +290715,7 @@ function buildSuppressionSummary(waivers) {
290377
290715
  }
290378
290716
 
290379
290717
  // node_modules/.pnpm/@infraweaver-io+attest@0.1.7_supports-color@7.2.0/node_modules/@infraweaver-io/attest/dist/signing.js
290380
- import { createHash as createHash10, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify } from "node:crypto";
290718
+ import { createHash as createHash11, createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify } from "node:crypto";
290381
290719
  var SIGNATURE_ALGORITHM = "Ed25519";
290382
290720
  var DEFAULT_KEY_LABEL = "the signing key";
290383
290721
  function loadSigningKey(raw2, options) {
@@ -290404,7 +290742,7 @@ function loadSigningKey(raw2, options) {
290404
290742
  return {
290405
290743
  privateKey,
290406
290744
  publicKeyPem,
290407
- fingerprint: createHash10("sha256").update(spkiDer).digest("hex")
290745
+ fingerprint: createHash11("sha256").update(spkiDer).digest("hex")
290408
290746
  };
290409
290747
  }
290410
290748
  function signBytes(bytes, key) {
@@ -290639,7 +290977,7 @@ function buildEvidenceBundle(args2) {
290639
290977
  // node_modules/.pnpm/@infraweaver-io+attest@0.1.7_supports-color@7.2.0/node_modules/@infraweaver-io/attest/dist/keyless.js
290640
290978
  var import_bundle = __toESM(require_dist6(), 1);
290641
290979
  var import_sign = __toESM(require_dist9(), 1);
290642
- import { createHash as createHash11 } from "node:crypto";
290980
+ import { createHash as createHash12 } from "node:crypto";
290643
290981
 
290644
290982
  // node_modules/.pnpm/@infraweaver-io+attest@0.1.7_supports-color@7.2.0/node_modules/@infraweaver-io/attest/dist/keylessOidc.js
290645
290983
  var SIGSTORE_OIDC_AUDIENCE = "sigstore";
@@ -290668,7 +291006,7 @@ function keylessSigningPossible(env2, envName) {
290668
291006
  }
290669
291007
  function buildInTotoStatement(args2) {
290670
291008
  const data = typeof args2.artifactBytes === "string" ? Buffer.from(args2.artifactBytes, "utf8") : args2.artifactBytes;
290671
- const sha2562 = createHash11("sha256").update(data).digest("hex");
291009
+ const sha2562 = createHash12("sha256").update(data).digest("hex");
290672
291010
  return {
290673
291011
  _type: "https://in-toto.io/Statement/v1",
290674
291012
  subject: [{ name: args2.subjectName, digest: { sha256: sha2562 } }],
@@ -290878,12 +291216,12 @@ import { mkdirSync as mkdirSync5 } from "node:fs";
290878
291216
  import { dirname as dirname6 } from "node:path";
290879
291217
 
290880
291218
  // src/mcp/terraform/oscal.ts
290881
- import { createHash as createHash12 } from "node:crypto";
291219
+ import { createHash as createHash13 } from "node:crypto";
290882
291220
  var OSCAL_VERSION = "1.1.2";
290883
291221
  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.";
290884
291222
  var NS = "https://infraweaver.io/ns/oscal";
290885
291223
  function deterministicUuid(seed) {
290886
- const hex4 = createHash12("sha256").update(seed).digest("hex").slice(0, 32).split("");
291224
+ const hex4 = createHash13("sha256").update(seed).digest("hex").slice(0, 32).split("");
290887
291225
  hex4[12] = "5";
290888
291226
  hex4[16] = (Number.parseInt(hex4[16] ?? "0", 16) & 3 | 8).toString(16);
290889
291227
  const u = hex4.join("");
@@ -291999,16 +292337,32 @@ function uncoveredInstanceKeys(p) {
291999
292337
  if (expected === null) return null;
292000
292338
  const kind = level[0].kind;
292001
292339
  const written = (k) => kind === "count" ? k : `"${k}"`;
292340
+ const fromAt = level[0].at;
292341
+ const toAt = p.after.multiplicity[0]?.at;
292342
+ if (fromAt === void 0 || toAt === void 0) return null;
292002
292343
  const declared2 = /* @__PURE__ */ new Set();
292003
292344
  for (const blk of p.blocks) {
292345
+ if (stripIndices(normaliseAddress(blk.from)) !== p.before.address || stripIndices(normaliseAddress(blk.to)) !== p.after.address)
292346
+ return null;
292004
292347
  const from = instanceKeysOf(blk.from);
292005
292348
  const to = instanceKeysOf(blk.to);
292006
292349
  if (from.length > 1 || to.length > 1) return null;
292007
292350
  if (from.length === 0 && to.length === 0) return null;
292008
- if (from.length === 1 && to.length === 1 && from[0] === to[0]) declared2.add(from[0]);
292351
+ if (from.length !== 1 || to.length !== 1 || from[0] !== to[0]) continue;
292352
+ const fromPlaced = keyAtLevel(blk.from, fromAt);
292353
+ const toPlaced = keyAtLevel(blk.to, toAt);
292354
+ if (fromPlaced === false || toPlaced === false) continue;
292355
+ if (fromPlaced === null || toPlaced === null) return null;
292356
+ declared2.add(from[0]);
292009
292357
  }
292010
292358
  return expected.filter((k) => !declared2.has(written(k)));
292011
292359
  }
292360
+ function keyAtLevel(address, at) {
292361
+ const onResource = instanceKeysOf(stripModuleHops(normaliseAddress(address))).length > 0;
292362
+ if (at === "resource") return onResource;
292363
+ if (onResource) return false;
292364
+ return moduleHops(address).length === 1 ? true : null;
292365
+ }
292012
292366
  function computeEquivalence(before, after, movedBlocks, ambiguous = [], planPairs, opts = {}) {
292013
292367
  const reasons = [];
292014
292368
  const corr = correspond(before, after, movedBlocks);
@@ -293935,7 +294289,8 @@ function IngestExternalFindingsTool(ctx) {
293935
294289
  // an external feed is a narrow concern source — join any prior scan
293936
294290
  // baseline so a later ✗→✓ verify doesn't read the workspace as regressions.
293937
294291
  baselineMode: "merge",
293938
- seedCaps: seedCapsFrom(input2)
294292
+ seedCaps: seedCapsFrom(input2),
294293
+ openRemediationPrs: await listOpenRemediationPrs(ctx)
293939
294294
  });
293940
294295
  const cappedBlock = cappedSummary(capped);
293941
294296
  log2.info(
@@ -294405,15 +294760,13 @@ function NormalizationCandidatesTool(ctx) {
294405
294760
  parameters: NormalizationCandidatesParams,
294406
294761
  execute: execute(async () => {
294407
294762
  const cwd = ctx.payload.cwd ?? process.cwd();
294408
- const files = findNormalizationCandidates(cwd).filter(
294409
- (f) => ctx.payload.refactorExamples || !isExamplePath(f.file)
294410
- );
294763
+ const library = libraryScope(cwd);
294764
+ const files = findNormalizationCandidates(cwd).filter((f) => ctx.payload.refactorExamples || !isExamplePath(f.file)).map((f) => ({ ...reportOnly(library(f.file)), ...f }));
294411
294765
  const total = files.reduce((n, f) => n + f.count, 0);
294412
294766
  log2.info(
294413
294767
  `\xBB terraform_normalization_candidates: ${total} site(s) across ${files.length} file(s)`
294414
294768
  );
294415
294769
  return toolOk({
294416
- ...reportOnly(libraryRepoReason(cwd)),
294417
294770
  file_count: files.length,
294418
294771
  total,
294419
294772
  files,
@@ -294948,7 +295301,8 @@ function ReadFindingsTool(ctx) {
294948
295301
  minRank,
294949
295302
  grouping,
294950
295303
  autonomyThreshold,
294951
- seedCaps: seedCapsFrom(input2)
295304
+ seedCaps: seedCapsFrom(input2),
295305
+ openRemediationPrs: await listOpenRemediationPrs(ctx)
294952
295306
  });
294953
295307
  const cappedBlock = cappedSummary(capped);
294954
295308
  log2.info(
@@ -298855,7 +299209,7 @@ async function startMcpHttpServer(ctx, options) {
298855
299209
 
298856
299210
  // src/mcp/terraform/policyGate.ts
298857
299211
  import {
298858
- existsSync as existsSync14,
299212
+ existsSync as existsSync13,
298859
299213
  mkdirSync as mkdirSync7,
298860
299214
  mkdtempSync as mkdtempSync7,
298861
299215
  readdirSync as readdirSync9,
@@ -299238,7 +299592,7 @@ function readOrEmpty(path15) {
299238
299592
  function regoDirIn(root, skip) {
299239
299593
  for (const dir of DEFAULT_POLICY_DIRS) {
299240
299594
  const abs = join58(root, dir);
299241
- if (existsSync14(abs) && walkFilesBySuffix(abs, ".rego").some(
299595
+ if (existsSync13(abs) && walkFilesBySuffix(abs, ".rego").some(
299242
299596
  (rel) => !skip(join58(dir, rel), readOrEmpty(join58(abs, rel)))
299243
299597
  )) {
299244
299598
  return dir;
@@ -299307,7 +299661,7 @@ function resolveRepoPolicy(cwd, baseRef, tmpdir5, notes, skip) {
299307
299661
  return null;
299308
299662
  }
299309
299663
  function findWorkflowPlanJson(cwd, notes) {
299310
- const present = PLAN_JSON_NAMES.filter((name) => existsSync14(join58(cwd, name)));
299664
+ const present = PLAN_JSON_NAMES.filter((name) => existsSync13(join58(cwd, name)));
299311
299665
  if (present.length === 0) return null;
299312
299666
  const listing = run("git", ["ls-files", "-z"], cwd);
299313
299667
  if (listing.status !== 0) {
@@ -300666,9 +301020,9 @@ function shouldRunGraceTurn(params) {
300666
301020
  }
300667
301021
 
300668
301022
  // src/agents/quotaTokens.ts
300669
- var CACHE_READ_WEIGHT = 0.1;
300670
- function quotaTokens(t) {
300671
- return t.input + t.output + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT;
301023
+ var CACHE_READ_WEIGHT2 = 0.1;
301024
+ function quotaTokens2(t) {
301025
+ return t.input + t.output + (t.reasoning ?? 0) + t.cacheWrite + t.cacheRead * CACHE_READ_WEIGHT2;
300672
301026
  }
300673
301027
 
300674
301028
  // src/agents/sessionLabeler.ts
@@ -301161,7 +301515,7 @@ function createProcessOutputActivityTimeout(ctx) {
301161
301515
  }
301162
301516
 
301163
301517
  // src/utils/prompt/skills.ts
301164
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync36, writeFileSync as writeFileSync19 } from "node:fs";
301518
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync36, writeFileSync as writeFileSync19 } from "node:fs";
301165
301519
  import { dirname as dirname9, join as join61 } from "node:path";
301166
301520
  import { fileURLToPath } from "node:url";
301167
301521
  var BUNDLED_SKILL_NAMES = ["terraform-best-practices"];
@@ -301172,7 +301526,7 @@ function resolveSkillPath(name) {
301172
301526
  join61(here, "skills", name, "SKILL.md")
301173
301527
  ];
301174
301528
  for (const candidate of candidates) {
301175
- if (existsSync15(candidate)) return candidate;
301529
+ if (existsSync14(candidate)) return candidate;
301176
301530
  }
301177
301531
  throw new Error(`bundled skill not found: ${name} (looked in ${candidates.join(", ")})`);
301178
301532
  }
@@ -301191,8 +301545,8 @@ function installBundledSkills(params) {
301191
301545
 
301192
301546
  // src/utils/setup/install.ts
301193
301547
  import { spawnSync as spawnSync9 } from "node:child_process";
301194
- import { createHash as createHash14 } from "node:crypto";
301195
- import { chmodSync as chmodSync2, createWriteStream as createWriteStream2, existsSync as existsSync18, readFileSync as readFileSync37 } from "node:fs";
301548
+ import { createHash as createHash15 } from "node:crypto";
301549
+ import { chmodSync as chmodSync2, createWriteStream as createWriteStream2, existsSync as existsSync17, readFileSync as readFileSync37 } from "node:fs";
301196
301550
  import { join as join66 } from "node:path";
301197
301551
  import { pipeline as pipeline2 } from "node:stream/promises";
301198
301552
 
@@ -301258,7 +301612,7 @@ function verifyTarballIntegrity(tarballPath, dist, descriptor) {
301258
301612
  const expected = candidate.slice(separator2 + 1);
301259
301613
  let actual;
301260
301614
  try {
301261
- actual = createHash14(algorithm).update(bytes).digest("base64");
301615
+ actual = createHash15(algorithm).update(bytes).digest("base64");
301262
301616
  } catch {
301263
301617
  continue;
301264
301618
  }
@@ -301268,7 +301622,7 @@ function verifyTarballIntegrity(tarballPath, dist, descriptor) {
301268
301622
  `Integrity check FAILED for ${descriptor}: the downloaded tarball does not match the registry's published digest (${integrity2}). Refusing to execute it.`
301269
301623
  );
301270
301624
  }
301271
- const actualShasum = createHash14("sha1").update(bytes).digest("hex");
301625
+ const actualShasum = createHash15("sha1").update(bytes).digest("hex");
301272
301626
  if (actualShasum !== shasum) {
301273
301627
  throw new Error(
301274
301628
  `Integrity check FAILED for ${descriptor}: the downloaded tarball does not match the registry's published shasum (expected ${shasum}, got ${actualShasum}). Refusing to execute it.`
@@ -301279,7 +301633,7 @@ async function installFromNpmTarball(params) {
301279
301633
  const tempDir = getTempDir();
301280
301634
  const extractedDir = join66(tempDir, "package");
301281
301635
  const cliPath = join66(extractedDir, params.executablePath);
301282
- if (existsSync18(cliPath)) {
301636
+ if (existsSync17(cliPath)) {
301283
301637
  log2.debug(`\xBB using cached binary at ${cliPath}`);
301284
301638
  return cliPath;
301285
301639
  }
@@ -301370,7 +301724,7 @@ async function installFromNpmTarball(params) {
301370
301724
  `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
301371
301725
  );
301372
301726
  }
301373
- if (!existsSync18(cliPath)) {
301727
+ if (!existsSync17(cliPath)) {
301374
301728
  throw new Error(`Executable not found in extracted package at ${cliPath}`);
301375
301729
  }
301376
301730
  if (params.installDependencies) {
@@ -301610,7 +301964,7 @@ function timerFor(state5, label) {
301610
301964
  function chargeTokenQuota(state5) {
301611
301965
  const quota = state5.params.tokenQuota;
301612
301966
  if (!quota || state5.stopRun.signal.aborted) return;
301613
- if (!quota.observe(state5.quotaSource, quotaTokens(state5.accumulatedTokens))) return;
301967
+ if (!quota.observe(state5.quotaSource, quotaTokens2(state5.accumulatedTokens))) return;
301614
301968
  log2.info(`\xBB ${state5.params.label}: ${quota.gap()}`);
301615
301969
  state5.stopRun.abort(new TokenQuotaExceededError(quota.gap()));
301616
301970
  }
@@ -308342,7 +308696,7 @@ function newTurn() {
308342
308696
  turnCounter += 1;
308343
308697
  return {
308344
308698
  finalText: "",
308345
- tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
308699
+ tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 },
308346
308700
  costUsd: 0,
308347
308701
  sessionError: null,
308348
308702
  lastToolError: null,
@@ -308352,7 +308706,7 @@ function newTurn() {
308352
308706
  function chargeTokenQuota2(ctx) {
308353
308707
  const quota = ctx.tokenQuota;
308354
308708
  if (!quota || !ctx.currentTurn || ctx.tokenQuotaExceeded) return;
308355
- if (!quota.observe(ctx.currentTurn.quotaSource, quotaTokens(ctx.currentTurn.tokens))) return;
308709
+ if (!quota.observe(ctx.currentTurn.quotaSource, quotaTokens2(ctx.currentTurn.tokens))) return;
308356
308710
  ctx.tokenQuotaExceeded = true;
308357
308711
  log2.info(`\xBB ${ctx.label}: ${quota.gap()}`);
308358
308712
  ctx.abortRun?.(new TokenQuotaExceededError(quota.gap()));
@@ -308461,6 +308815,7 @@ async function onPartUpdated(ctx, part) {
308461
308815
  if (t) {
308462
308816
  ctx.currentTurn.tokens.input += t.input || 0;
308463
308817
  ctx.currentTurn.tokens.output += t.output || 0;
308818
+ ctx.currentTurn.tokens.reasoning += t.reasoning || 0;
308464
308819
  ctx.currentTurn.tokens.cacheRead += t.cache?.read || 0;
308465
308820
  ctx.currentTurn.tokens.cacheWrite += t.cache?.write || 0;
308466
308821
  }
@@ -309065,10 +309420,10 @@ var DispatchRecorder = class {
309065
309420
  * orchestrator counts as subagent — including a session the labeler could not
309066
309421
  * name, because an unnamed session is by definition not the orchestrator's.
309067
309422
  */
309068
- recordTokens(label, billableTokens2) {
309069
- if (!Number.isFinite(billableTokens2) || billableTokens2 <= 0) return;
309070
- if (label === ORCHESTRATOR_LABEL) this.#orchestratorTokens += billableTokens2;
309071
- else this.#subagentTokens += billableTokens2;
309423
+ recordTokens(label, billableTokens) {
309424
+ if (!Number.isFinite(billableTokens) || billableTokens <= 0) return;
309425
+ if (label === ORCHESTRATOR_LABEL) this.#orchestratorTokens += billableTokens;
309426
+ else this.#subagentTokens += billableTokens;
309072
309427
  }
309073
309428
  tally() {
309074
309429
  return {
@@ -309957,8 +310312,8 @@ function addQueryParameters(url5, parameters) {
309957
310312
  }).join("&");
309958
310313
  }
309959
310314
  var urlVariableRegex = /\{[^{}}]+\}/g;
309960
- function removeNonChars(variableName) {
309961
- return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
310315
+ function removeNonChars(variableName2) {
310316
+ return variableName2.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
309962
310317
  }
309963
310318
  function extractUrlVariableNames(url5) {
309964
310319
  const matches2 = url5.match(urlVariableRegex);
@@ -317637,7 +317992,7 @@ async function runAgentWithWatchdogs(params) {
317637
317992
  const tokenQuota = reserved ? quotas.token : createTokenQuota(quotas.tokenLimit.full);
317638
317993
  const costQuota = reserved ? quotas.cost : createCostQuota(quotas.costLimit.full);
317639
317994
  log2.info(
317640
- 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)`
317995
+ 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)`
317641
317996
  );
317642
317997
  log2.info(
317643
317998
  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)`
@@ -317735,7 +318090,7 @@ async function runAgentWithWatchdogs(params) {
317735
318090
  }
317736
318091
 
317737
318092
  // src/phases/startToolPlane.ts
317738
- import { existsSync as existsSync19, readdirSync as readdirSync10 } from "node:fs";
318093
+ import { existsSync as existsSync18, readdirSync as readdirSync10 } from "node:fs";
317739
318094
  import { readFile as readFile5 } from "node:fs/promises";
317740
318095
  import { join as join74 } from "node:path";
317741
318096
 
@@ -318566,7 +318921,7 @@ ${instructions.user}` : null,
318566
318921
  });
318567
318922
  if (toolContext.agentId === "opencode") {
318568
318923
  const pluginDir = join74(process.cwd(), ".opencode", "plugin");
318569
- const hasPlugins = existsSync19(pluginDir) && readdirSync10(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
318924
+ const hasPlugins = existsSync18(pluginDir) && readdirSync10(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
318570
318925
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
318571
318926
  log2.info(
318572
318927
  "\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"