@yawlabs/ctxlint 0.13.1 → 0.13.3

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.
package/dist/index.js CHANGED
@@ -22404,7 +22404,7 @@ var init_protocol = __esm({
22404
22404
  return;
22405
22405
  }
22406
22406
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
22407
- await new Promise((resolve14) => setTimeout(resolve14, pollInterval));
22407
+ await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
22408
22408
  options?.signal?.throwIfAborted();
22409
22409
  }
22410
22410
  } catch (error49) {
@@ -22421,7 +22421,7 @@ var init_protocol = __esm({
22421
22421
  */
22422
22422
  request(request, resultSchema, options) {
22423
22423
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
22424
- return new Promise((resolve14, reject) => {
22424
+ return new Promise((resolve15, reject) => {
22425
22425
  const earlyReject = (error49) => {
22426
22426
  reject(error49);
22427
22427
  };
@@ -22499,7 +22499,7 @@ var init_protocol = __esm({
22499
22499
  if (!parseResult.success) {
22500
22500
  reject(parseResult.error);
22501
22501
  } else {
22502
- resolve14(parseResult.data);
22502
+ resolve15(parseResult.data);
22503
22503
  }
22504
22504
  } catch (error49) {
22505
22505
  reject(error49);
@@ -22760,12 +22760,12 @@ var init_protocol = __esm({
22760
22760
  }
22761
22761
  } catch {
22762
22762
  }
22763
- return new Promise((resolve14, reject) => {
22763
+ return new Promise((resolve15, reject) => {
22764
22764
  if (signal.aborted) {
22765
22765
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
22766
22766
  return;
22767
22767
  }
22768
- const timeoutId = setTimeout(resolve14, interval);
22768
+ const timeoutId = setTimeout(resolve15, interval);
22769
22769
  signal.addEventListener("abort", () => {
22770
22770
  clearTimeout(timeoutId);
22771
22771
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -25813,7 +25813,7 @@ var require_compile = __commonJS({
25813
25813
  const schOrFunc = root.refs[ref];
25814
25814
  if (schOrFunc)
25815
25815
  return schOrFunc;
25816
- let _sch = resolve14.call(this, root, ref);
25816
+ let _sch = resolve15.call(this, root, ref);
25817
25817
  if (_sch === void 0) {
25818
25818
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
25819
25819
  const { schemaId } = this.opts;
@@ -25840,7 +25840,7 @@ var require_compile = __commonJS({
25840
25840
  function sameSchemaEnv(s1, s2) {
25841
25841
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
25842
25842
  }
25843
- function resolve14(root, ref) {
25843
+ function resolve15(root, ref) {
25844
25844
  let sch;
25845
25845
  while (typeof (sch = this.refs[ref]) == "string")
25846
25846
  ref = sch;
@@ -26418,7 +26418,7 @@ var require_fast_uri = __commonJS({
26418
26418
  }
26419
26419
  return uri;
26420
26420
  }
26421
- function resolve14(baseURI, relativeURI, options) {
26421
+ function resolve15(baseURI, relativeURI, options) {
26422
26422
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
26423
26423
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
26424
26424
  schemelessOptions.skipEscape = true;
@@ -26645,7 +26645,7 @@ var require_fast_uri = __commonJS({
26645
26645
  var fastUri = {
26646
26646
  SCHEMES,
26647
26647
  normalize: normalize3,
26648
- resolve: resolve14,
26648
+ resolve: resolve15,
26649
26649
  resolveComponent,
26650
26650
  equal,
26651
26651
  serialize,
@@ -30827,7 +30827,7 @@ var init_mcp = __esm({
30827
30827
  let task = createTaskResult.task;
30828
30828
  const pollInterval = task.pollInterval ?? 5e3;
30829
30829
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
30830
- await new Promise((resolve14) => setTimeout(resolve14, pollInterval));
30830
+ await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
30831
30831
  const updatedTask = await extra.taskStore.getTask(taskId);
30832
30832
  if (!updatedTask) {
30833
30833
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -31423,12 +31423,12 @@ var init_stdio2 = __esm({
31423
31423
  this.onclose?.();
31424
31424
  }
31425
31425
  send(message) {
31426
- return new Promise((resolve14) => {
31426
+ return new Promise((resolve15) => {
31427
31427
  const json2 = serializeMessage(message);
31428
31428
  if (this._stdout.write(json2)) {
31429
- resolve14();
31429
+ resolve15();
31430
31430
  } else {
31431
- this._stdout.once("drain", resolve14);
31431
+ this._stdout.once("drain", resolve15);
31432
31432
  }
31433
31433
  });
31434
31434
  }
@@ -34433,19 +34433,28 @@ function stripBom(content) {
34433
34433
  return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
34434
34434
  }
34435
34435
  function loadPackageJson(projectRoot) {
34436
- if (pkgJsonCache?.root === projectRoot) return pkgJsonCache.data;
34436
+ if (pkgJsonCache.has(projectRoot)) {
34437
+ const cached2 = pkgJsonCache.get(projectRoot) ?? null;
34438
+ pkgJsonCache.delete(projectRoot);
34439
+ pkgJsonCache.set(projectRoot, cached2);
34440
+ return cached2;
34441
+ }
34442
+ let data;
34437
34443
  try {
34438
34444
  const content = stripBom(fs2.readFileSync(path.join(projectRoot, "package.json"), "utf-8"));
34439
- const data = JSON.parse(content);
34440
- pkgJsonCache = { root: projectRoot, data };
34441
- return data;
34445
+ data = JSON.parse(content);
34442
34446
  } catch {
34443
- pkgJsonCache = { root: projectRoot, data: null };
34444
- return null;
34447
+ data = null;
34445
34448
  }
34449
+ pkgJsonCache.set(projectRoot, data);
34450
+ if (pkgJsonCache.size > PKG_CACHE_MAX_ENTRIES) {
34451
+ const oldest = pkgJsonCache.keys().next().value;
34452
+ if (oldest !== void 0) pkgJsonCache.delete(oldest);
34453
+ }
34454
+ return data;
34446
34455
  }
34447
34456
  function resetPackageJsonCache() {
34448
- pkgJsonCache = null;
34457
+ pkgJsonCache.clear();
34449
34458
  }
34450
34459
  function fileExists(filePath) {
34451
34460
  try {
@@ -34511,12 +34520,13 @@ function getAllProjectFiles(projectRoot) {
34511
34520
  walk(projectRoot, 0);
34512
34521
  return files;
34513
34522
  }
34514
- var pkgJsonCache, IGNORED_DIRS;
34523
+ var PKG_CACHE_MAX_ENTRIES, pkgJsonCache, IGNORED_DIRS;
34515
34524
  var init_fs = __esm({
34516
34525
  "src/utils/fs.ts"() {
34517
34526
  "use strict";
34518
34527
  init_define_WEB_FIRST_SEGMENTS();
34519
- pkgJsonCache = null;
34528
+ PKG_CACHE_MAX_ENTRIES = 256;
34529
+ pkgJsonCache = /* @__PURE__ */ new Map();
34520
34530
  IGNORED_DIRS = /* @__PURE__ */ new Set([
34521
34531
  "node_modules",
34522
34532
  ".git",
@@ -40917,7 +40927,7 @@ function parseRenameLog(result, targetPath) {
40917
40927
  let basenameFallback = null;
40918
40928
  const lines = result.trim().split("\n");
40919
40929
  for (const line of lines) {
40920
- const headerMatch = line.match(/^([a-f0-9]{7,40})\s+(.+)$/);
40930
+ const headerMatch = line.match(/^([a-f0-9]{7,40})\s+(\d{4}-\d\d-\d\d\b.*)$/);
40921
40931
  if (headerMatch) {
40922
40932
  currentHash = headerMatch[1].substring(0, 7);
40923
40933
  currentDateStr = headerMatch[2];
@@ -42479,7 +42489,7 @@ async function checkPaths(file2, projectRoot) {
42479
42489
  const resolvedPath = path4.resolve(baseDir, ref.value);
42480
42490
  const normalizedRef = ref.value.replace(/\\/g, "/");
42481
42491
  if (normalizedRef.includes("*")) {
42482
- const matches = await Ze(normalizedRef, { cwd: baseDir, nodir: false });
42492
+ const matches = path4.isAbsolute(normalizedRef) ? await Ze(normalizedRef, { absolute: true, nodir: false }) : await Ze(normalizedRef, { cwd: baseDir, nodir: false });
42483
42493
  if (matches.length === 0) {
42484
42494
  issues.push({
42485
42495
  severity: "error",
@@ -42603,10 +42613,26 @@ function extractNpxPackage(cmd) {
42603
42613
  }
42604
42614
  return null;
42605
42615
  }
42616
+ function wouldNeedPackageJson(cmd) {
42617
+ return NPM_SCRIPT_PATTERN.test(cmd) || PKG_SHORTHAND_PATTERN.test(cmd) || /^npx\b/.test(cmd) || PKG_DEPENDENT_TOOL_PATTERN.test(cmd);
42618
+ }
42606
42619
  async function checkCommands(file2, projectRoot) {
42607
42620
  const issues = [];
42608
42621
  const pkgJson = loadPackageJson(projectRoot);
42609
42622
  const makefile = loadMakefile(projectRoot);
42623
+ if (!pkgJson) {
42624
+ const skipped = file2.references.commands.find((ref) => wouldNeedPackageJson(ref.value));
42625
+ if (skipped) {
42626
+ issues.push({
42627
+ severity: "info",
42628
+ check: "commands",
42629
+ ruleId: "commands/package-json-missing",
42630
+ line: skipped.line,
42631
+ message: "package.json missing or unparseable \u2014 command checks skipped",
42632
+ suggestion: "Add a parseable package.json at the project root so script, npx, and tool references can be validated."
42633
+ });
42634
+ }
42635
+ }
42610
42636
  for (const ref of file2.references.commands) {
42611
42637
  const cmd = ref.value;
42612
42638
  const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
@@ -42625,9 +42651,7 @@ async function checkCommands(file2, projectRoot) {
42625
42651
  }
42626
42652
  continue;
42627
42653
  }
42628
- const shorthandMatch = cmd.match(
42629
- /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
42630
- );
42654
+ const shorthandMatch = cmd.match(PKG_SHORTHAND_PATTERN);
42631
42655
  if (shorthandMatch && pkgJson) {
42632
42656
  const scriptName = shorthandMatch[2];
42633
42657
  if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
@@ -42689,7 +42713,7 @@ async function checkCommands(file2, projectRoot) {
42689
42713
  }
42690
42714
  continue;
42691
42715
  }
42692
- const toolMatch = cmd.match(/^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/);
42716
+ const toolMatch = cmd.match(PKG_DEPENDENT_TOOL_PATTERN);
42693
42717
  if (toolMatch && pkgJson) {
42694
42718
  const tool = toolMatch[1];
42695
42719
  const allDeps = {
@@ -42727,7 +42751,7 @@ function hasMakeTarget(makefile, target) {
42727
42751
  const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
42728
42752
  return pattern.test(makefile);
42729
42753
  }
42730
- var NPM_SCRIPT_PATTERN, MAKE_PATTERN;
42754
+ var NPM_SCRIPT_PATTERN, MAKE_PATTERN, PKG_DEPENDENT_TOOL_PATTERN, PKG_SHORTHAND_PATTERN;
42731
42755
  var init_commands = __esm({
42732
42756
  "src/core/checks/commands.ts"() {
42733
42757
  "use strict";
@@ -42735,6 +42759,8 @@ var init_commands = __esm({
42735
42759
  init_fs();
42736
42760
  NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
42737
42761
  MAKE_PATTERN = /^make\s+(\S+)/;
42762
+ PKG_DEPENDENT_TOOL_PATTERN = /^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/;
42763
+ PKG_SHORTHAND_PATTERN = /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/;
42738
42764
  }
42739
42765
  });
42740
42766
 
@@ -42845,7 +42871,7 @@ async function checkTokens(file2, _projectRoot, thresholds = DEFAULT_TOKEN_THRES
42845
42871
  }
42846
42872
  function checkAggregateTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
42847
42873
  const total = files.reduce((sum, f) => sum + f.tokens, 0);
42848
- if (total > thresholds.aggregate && files.length > 1) {
42874
+ if (total >= thresholds.aggregate && files.length > 1) {
42849
42875
  return {
42850
42876
  severity: "warning",
42851
42877
  check: "tokens",
@@ -42924,13 +42950,20 @@ function computeSectionCosts(file2) {
42924
42950
  return { title: s.title, line: s.startLine, tokens: countTokens(body) };
42925
42951
  }).sort((a, b2) => b2.tokens - a.tokens);
42926
42952
  }
42927
- function loadSettingsSources(projectRoot) {
42953
+ function loadSettingsSources(projectRoot, includeGlobal) {
42954
+ if (settingsCache?.root === projectRoot && settingsCache.includeGlobal === includeGlobal) {
42955
+ return settingsCache.data;
42956
+ }
42928
42957
  const sources = [];
42929
42958
  const candidates = [
42930
42959
  path7.join(projectRoot, ".claude", "settings.json"),
42931
- path7.join(projectRoot, ".claude", "settings.local.json"),
42932
- path7.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
42960
+ path7.join(projectRoot, ".claude", "settings.local.json")
42933
42961
  ];
42962
+ if (includeGlobal) {
42963
+ candidates.push(
42964
+ path7.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
42965
+ );
42966
+ }
42934
42967
  for (const p2 of candidates) {
42935
42968
  let content;
42936
42969
  try {
@@ -42944,6 +42977,7 @@ function loadSettingsSources(projectRoot) {
42944
42977
  console.warn(`ctxlint: could not parse ${p2}: ${err.message}`);
42945
42978
  }
42946
42979
  }
42980
+ settingsCache = { root: projectRoot, includeGlobal, data: sources };
42947
42981
  return sources;
42948
42982
  }
42949
42983
  function canonicalizeCommand(backticked) {
@@ -42995,7 +43029,7 @@ function checkHardEnforcement(file2, settings) {
42995
43029
  }
42996
43030
  return issues;
42997
43031
  }
42998
- async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
43032
+ async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS, includeGlobal = false) {
42999
43033
  if (!isAlwaysLoaded(file2)) return [];
43000
43034
  const issues = [];
43001
43035
  const threshold = thresholds.tierBreakdown;
@@ -43017,7 +43051,7 @@ async function checkTierTokens(file2, projectRoot, thresholds = DEFAULT_TOKEN_TH
43017
43051
  });
43018
43052
  }
43019
43053
  }
43020
- const settings = loadSettingsSources(projectRoot);
43054
+ const settings = loadSettingsSources(projectRoot, includeGlobal);
43021
43055
  issues.push(...checkHardEnforcement(file2, settings));
43022
43056
  return issues;
43023
43057
  }
@@ -43038,7 +43072,7 @@ function checkAggregateTierTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS)
43038
43072
  suggestion: "Consider moving the largest files or their heaviest sections to on-demand tiers (skills, subagents, memory)."
43039
43073
  };
43040
43074
  }
43041
- var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
43075
+ var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND, settingsCache;
43042
43076
  var init_tier_tokens = __esm({
43043
43077
  "src/core/checks/tier-tokens.ts"() {
43044
43078
  "use strict";
@@ -43068,6 +43102,7 @@ var init_tier_tokens = __esm({
43068
43102
  ];
43069
43103
  TOP_SECTIONS_TO_REPORT = 3;
43070
43104
  INVIOLABLE_WITH_COMMAND = /\b(NEVER|ALWAYS|DON'?T|DO NOT|MUST NOT)\b[^.!?`]{0,80}`([^`]+)`/i;
43105
+ settingsCache = null;
43071
43106
  }
43072
43107
  });
43073
43108
 
@@ -43164,7 +43199,8 @@ async function checkRedundancy(file2, projectRoot) {
43164
43199
  ruleId: "redundancy/tech-mention",
43165
43200
  line: i2 + 1,
43166
43201
  message: `"${mention}" is in package.json ${pkgJson.dependencies?.[pkg] ? "dependencies" : pkgJson.devDependencies?.[pkg] ? "devDependencies" : pkgJson.peerDependencies?.[pkg] ? "peerDependencies" : "optionalDependencies"} \u2014 agent can infer this`,
43167
- suggestion: `~${wastedTokens} tokens could be saved`
43202
+ suggestion: `~${wastedTokens} tokens could be saved`,
43203
+ wastedTokens
43168
43204
  });
43169
43205
  }
43170
43206
  }
@@ -43178,7 +43214,8 @@ async function checkRedundancy(file2, projectRoot) {
43178
43214
  );
43179
43215
  if (dirMatch) {
43180
43216
  const dir = dirMatch[1].replace(/[`"]/g, "");
43181
- const fullPath = path8.resolve(projectRoot, dir);
43217
+ const baseDir = dir.startsWith("./") || dir.startsWith("../") ? path8.dirname(file2.filePath) : projectRoot;
43218
+ const fullPath = path8.resolve(baseDir, dir);
43182
43219
  if (isDirectory(fullPath)) {
43183
43220
  issues.push({
43184
43221
  severity: "info",
@@ -43410,19 +43447,23 @@ var init_contradictions = __esm({
43410
43447
  options: [
43411
43448
  {
43412
43449
  label: "Jest",
43413
- patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest.*with\s+jest\b/i]
43450
+ // The third pattern bounds the gap between `test` and `with <fw>` to a
43451
+ // few intra-clause chars so prose like "run our test suite and then
43452
+ // deploy with jest" no longer matches the framework directive — only
43453
+ // tight phrasings like "test with jest" / "tests run with jest" do.
43454
+ patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest\w*\s+with\s+jest\b/i]
43414
43455
  },
43415
43456
  {
43416
43457
  label: "Vitest",
43417
- patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest.*with\s+vitest\b/i]
43458
+ patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest\w*\s+with\s+vitest\b/i]
43418
43459
  },
43419
43460
  {
43420
43461
  label: "Mocha",
43421
- patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest.*with\s+mocha\b/i]
43462
+ patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest\w*\s+with\s+mocha\b/i]
43422
43463
  },
43423
43464
  {
43424
43465
  label: "pytest",
43425
- patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest.*with\s+pytest\b/i]
43466
+ patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest\w*\s+with\s+pytest\b/i]
43426
43467
  },
43427
43468
  {
43428
43469
  label: "Playwright",
@@ -43477,19 +43518,11 @@ var init_contradictions = __esm({
43477
43518
  },
43478
43519
  {
43479
43520
  label: "2 spaces",
43480
- patterns: [
43481
- /\b2[\s-]?space\s+indent/i,
43482
- /\bindent\s+with\s+2\s+spaces/i,
43483
- /\b2[\s-]?space\s+tabs?\b/i
43484
- ]
43521
+ patterns: [/\b2[\s-]?space\s+indent/i, /\bindent\s+with\s+2\s+spaces/i]
43485
43522
  },
43486
43523
  {
43487
43524
  label: "4 spaces",
43488
- patterns: [
43489
- /\b4[\s-]?space\s+indent/i,
43490
- /\bindent\s+with\s+4\s+spaces/i,
43491
- /\b4[\s-]?space\s+tabs?\b/i
43492
- ]
43525
+ patterns: [/\b4[\s-]?space\s+indent/i, /\bindent\s+with\s+4\s+spaces/i]
43493
43526
  }
43494
43527
  ]
43495
43528
  },
@@ -43651,10 +43684,11 @@ function hasUnbalancedBracketsOrQuotes(val) {
43651
43684
  if (square < 0 || curly < 0) return true;
43652
43685
  }
43653
43686
  if (square !== 0 || curly !== 0) return true;
43654
- const doubleQuotes = (val.match(/"/g) || []).length;
43655
- const singleQuotes = (val.match(/'/g) || []).length;
43656
- if (doubleQuotes % 2 !== 0) return true;
43657
- if (singleQuotes % 2 !== 0) return true;
43687
+ const quoteChar = val[0];
43688
+ if (quoteChar === '"' || quoteChar === "'") {
43689
+ const count = (val.match(quoteChar === '"' ? /"/g : /'/g) || []).length;
43690
+ if (count % 2 !== 0) return true;
43691
+ }
43658
43692
  return false;
43659
43693
  }
43660
43694
  async function checkFrontmatter(file2, _projectRoot) {
@@ -44039,7 +44073,9 @@ async function checkMcpSecurity(config2, _projectRoot) {
44039
44073
  try {
44040
44074
  if (!isEnvVarRef(server2.url)) {
44041
44075
  const parsed = new URL(server2.url);
44042
- if (parsed.protocol === "http:" && parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1" && parsed.hostname !== "::1") {
44076
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
44077
+ const isLoopback = host === "localhost" || host === "::1" || host.startsWith("127.");
44078
+ if (parsed.protocol === "http:" && !isLoopback) {
44043
44079
  issues.push({
44044
44080
  severity: "warning",
44045
44081
  check: "mcp-security",
@@ -44495,6 +44531,7 @@ function checkMissingFromClient(configs) {
44495
44531
  function collectServerNameKeys(content, rootKey) {
44496
44532
  const names = [];
44497
44533
  let i2 = 0;
44534
+ let keyStart = -1;
44498
44535
  let depth = 0;
44499
44536
  let inString = false;
44500
44537
  let escape2 = false;
@@ -44520,6 +44557,7 @@ function collectServerNameKeys(content, rootKey) {
44520
44557
  inString = true;
44521
44558
  collectingKey = true;
44522
44559
  pendingKey = "";
44560
+ keyStart = i2;
44523
44561
  } else {
44524
44562
  inString = false;
44525
44563
  let j3 = i2 + 1;
@@ -44528,7 +44566,11 @@ function collectServerNameKeys(content, rootKey) {
44528
44566
  if (pendingKey === rootKey && rootKeyDepth === -1) {
44529
44567
  rootKeyDepth = depth;
44530
44568
  } else if (rootKeyDepth !== -1 && depth === rootKeyDepth + 1) {
44531
- names.push(pendingKey);
44569
+ let line = 1;
44570
+ for (let k3 = 0; k3 < keyStart; k3++) {
44571
+ if (content[k3] === "\n") line++;
44572
+ }
44573
+ names.push({ name: pendingKey, line });
44532
44574
  }
44533
44575
  }
44534
44576
  collectingKey = false;
@@ -44554,14 +44596,19 @@ function checkSingleFileIssues(configs) {
44554
44596
  if (!config2.actualRootKey) continue;
44555
44597
  const serverKeys = collectServerNameKeys(config2.content, config2.actualRootKey);
44556
44598
  const counts = /* @__PURE__ */ new Map();
44557
- for (const k3 of serverKeys) counts.set(k3, (counts.get(k3) ?? 0) + 1);
44599
+ const secondLines = /* @__PURE__ */ new Map();
44600
+ for (const { name, line } of serverKeys) {
44601
+ const next = (counts.get(name) ?? 0) + 1;
44602
+ counts.set(name, next);
44603
+ if (next === 2) secondLines.set(name, line);
44604
+ }
44558
44605
  for (const [name, count] of counts) {
44559
44606
  if (count > 1) {
44560
44607
  issues.push({
44561
44608
  severity: "warning",
44562
44609
  check: "mcp-consistency",
44563
44610
  ruleId: "mcp-consistency/duplicate-server-name",
44564
- line: 1,
44611
+ line: secondLines.get(name) ?? 1,
44565
44612
  message: `Duplicate server name "${name}" in ${config2.relativePath} \u2014 only the last definition is used`
44566
44613
  });
44567
44614
  }
@@ -44720,6 +44767,17 @@ async function checkMcphApibase(config2, _projectRoot) {
44720
44767
  });
44721
44768
  return issues;
44722
44769
  }
44770
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
44771
+ issues.push({
44772
+ severity: "error",
44773
+ check: "mcph-apibase",
44774
+ ruleId: "mcph-apibase/invalid-apibase",
44775
+ line: pos.line,
44776
+ message: `"apiBase" must be an http(s) URL, got ${parsed.protocol.replace(/:$/, "")}`,
44777
+ suggestion: `Use an absolute http(s) URL, e.g. "https://mcp.hosting".`
44778
+ });
44779
+ return issues;
44780
+ }
44723
44781
  if (parsed.protocol === "http:" && !isPrivateHost(parsed.hostname)) {
44724
44782
  issues.push({
44725
44783
  severity: "warning",
@@ -44864,13 +44922,13 @@ var init_gitignore = __esm({
44864
44922
  // src/core/session-parser.ts
44865
44923
  import { readFile } from "node:fs/promises";
44866
44924
  import { readFileSync as readFileSync4 } from "node:fs";
44867
- import { dirname as dirname2, resolve as resolve4 } from "node:path";
44925
+ import { dirname as dirname3, resolve as resolve4 } from "node:path";
44868
44926
  import { fileURLToPath } from "node:url";
44869
44927
  function loadWebFirstSegments() {
44870
44928
  if (typeof define_WEB_FIRST_SEGMENTS_default !== "undefined") {
44871
44929
  return new Set(define_WEB_FIRST_SEGMENTS_default);
44872
44930
  }
44873
- const __dir = dirname2(fileURLToPath(import.meta.url));
44931
+ const __dir = dirname3(fileURLToPath(import.meta.url));
44874
44932
  const fixturePath = resolve4(__dir, "../../tests/fixtures/web-first-segments.json");
44875
44933
  const raw = JSON.parse(readFileSync4(fixturePath, "utf-8"));
44876
44934
  const list = Array.isArray(raw) ? raw : raw.segments;
@@ -44913,6 +44971,12 @@ function extractPathsClassified(content) {
44913
44971
  candidates.push(p2);
44914
44972
  }
44915
44973
  }
44974
+ for (const match of content.matchAll(DRIVE_ABS_PATH)) {
44975
+ const p2 = match[1].replace(/[)}\]]+$/, "");
44976
+ if (p2.length > 2) {
44977
+ candidates.push(p2);
44978
+ }
44979
+ }
44916
44980
  const seen = /* @__PURE__ */ new Set();
44917
44981
  const out = [];
44918
44982
  for (const value of candidates) {
@@ -44965,7 +45029,7 @@ async function parseMemoryFile(filePath, projectDir) {
44965
45029
  referencedPaths
44966
45030
  };
44967
45031
  }
44968
- var WEB_FIRST_SEGMENTS, PATH_PATTERN2, BARE_FILE_PATH;
45032
+ var WEB_FIRST_SEGMENTS, PATH_PATTERN2, BARE_FILE_PATH, DRIVE_ABS_PATH;
44969
45033
  var init_session_parser = __esm({
44970
45034
  "src/core/session-parser.ts"() {
44971
45035
  "use strict";
@@ -44974,6 +45038,7 @@ var init_session_parser = __esm({
44974
45038
  WEB_FIRST_SEGMENTS = loadWebFirstSegments();
44975
45039
  PATH_PATTERN2 = /(?:^|\s|['"`(])([.~/][^\s'"`),;:!?]+)/g;
44976
45040
  BARE_FILE_PATH = /(?:^|[\s`"'(])([\w][\w-]*(?:\/[\w.-]+)+\.[a-zA-Z0-9]{1,8})\b/g;
45041
+ DRIVE_ABS_PATH = /(?:^|[\s`"'(])([A-Za-z]:[\\/][^\s'"`),;!?]+)/g;
44977
45042
  }
44978
45043
  });
44979
45044
 
@@ -44981,7 +45046,7 @@ var init_session_parser = __esm({
44981
45046
  import { readdir, stat } from "node:fs/promises";
44982
45047
  import { createReadStream } from "node:fs";
44983
45048
  import { createInterface } from "node:readline";
44984
- import { dirname as dirname3, join as join5, resolve as resolve5 } from "node:path";
45049
+ import { dirname as dirname4, join as join5, resolve as resolve5 } from "node:path";
44985
45050
  import { existsSync } from "node:fs";
44986
45051
  function detectProviders() {
44987
45052
  if (!home) return [];
@@ -45076,7 +45141,7 @@ function detectAiderInSiblings(siblings) {
45076
45141
  return null;
45077
45142
  }
45078
45143
  async function detectSiblings(projectRoot) {
45079
- const parentDir = dirname3(resolve5(projectRoot));
45144
+ const parentDir = dirname4(resolve5(projectRoot));
45080
45145
  let entries;
45081
45146
  try {
45082
45147
  entries = await readdir(parentDir);
@@ -45173,7 +45238,12 @@ var init_session_scanner = __esm({
45173
45238
  },
45174
45239
  {
45175
45240
  provider: "goose",
45176
- dir: process.platform === "win32" ? join5(process.env.APPDATA || "", "Block", "goose") : join5(home, ".config", "goose")
45241
+ // On win32 the goose dir lives under %APPDATA%. If APPDATA is unset,
45242
+ // `join('', 'Block', 'goose')` yields the cwd-relative `Block/goose`,
45243
+ // which the HOME guard in `detectProviders` does NOT catch (HOME/USERPROFILE
45244
+ // may still be set). Leave the dir empty so `existsSync('')` is always false
45245
+ // and we never match a cwd-relative `Block/goose`, mirroring the HOME-guard intent.
45246
+ dir: process.platform === "win32" ? process.env.APPDATA ? join5(process.env.APPDATA, "Block", "goose") : "" : join5(home, ".config", "goose")
45177
45247
  },
45178
45248
  { provider: "continue", dir: join5(home, ".continue") },
45179
45249
  {
@@ -45189,43 +45259,68 @@ import { resolve as resolve6, basename as basename3 } from "node:path";
45189
45259
  function normalizePath2(p2) {
45190
45260
  return resolve6(p2).replace(/\\/g, "/").toLowerCase();
45191
45261
  }
45262
+ function extractSecretName(display) {
45263
+ const prefixMatch = display.match(SECRET_SET_PREFIX);
45264
+ if (!prefixMatch) return void 0;
45265
+ const rest = display.slice(prefixMatch.index + prefixMatch[0].length);
45266
+ const tokens = rest.split(/\s+/).filter((t2) => t2.length > 0);
45267
+ for (let i2 = 0; i2 < tokens.length; i2++) {
45268
+ const tok = tokens[i2];
45269
+ if (!tok.startsWith("-")) return tok;
45270
+ if (VALUE_FLAGS.has(tok) && !tok.includes("=")) i2++;
45271
+ }
45272
+ return void 0;
45273
+ }
45274
+ function repoBasename(repoSpec) {
45275
+ return (repoSpec.split("/").pop() || repoSpec).toLowerCase();
45276
+ }
45277
+ function repoMatchesSibling(repoSpec, sib) {
45278
+ const slash = repoSpec.indexOf("/");
45279
+ const sibBase = basename3(normalizePath2(sib.path));
45280
+ if (slash === -1) {
45281
+ return repoBasename(repoSpec) === sibBase;
45282
+ }
45283
+ const owner = repoSpec.slice(0, slash).toLowerCase();
45284
+ const repoName = repoSpec.slice(slash + 1).toLowerCase();
45285
+ if (repoName !== sibBase) return false;
45286
+ if (sib.gitOrg) return sib.gitOrg.toLowerCase() === owner;
45287
+ if (sib.gitRemoteUrl) {
45288
+ return sib.gitRemoteUrl.toLowerCase().includes(`${owner}/${repoName}`);
45289
+ }
45290
+ return true;
45291
+ }
45192
45292
  async function checkMissingSecret(ctx) {
45193
45293
  const issues = [];
45194
45294
  const secrets = [];
45195
45295
  for (const entry of ctx.history) {
45196
- const match = entry.display.match(SECRET_SET_PATTERN) || entry.display.match(SECRET_SET_SIMPLE);
45197
- if (!match) continue;
45296
+ const name = extractSecretName(entry.display);
45297
+ if (!name) continue;
45298
+ const repoMatch = entry.display.match(REPO_FLAG_PATTERN);
45198
45299
  secrets.push({
45199
- name: match[1],
45200
- repo: match[2],
45300
+ name,
45301
+ repo: repoMatch ? repoMatch[1] : void 0,
45201
45302
  project: entry.project
45202
45303
  });
45203
45304
  }
45204
45305
  if (secrets.length === 0) return issues;
45205
45306
  const byName = /* @__PURE__ */ new Map();
45206
45307
  for (const s of secrets) {
45207
- if (!byName.has(s.name)) byName.set(s.name, /* @__PURE__ */ new Set());
45208
- byName.get(s.name).add(s.project);
45209
- if (s.repo) byName.get(s.name).add(s.repo);
45308
+ if (!byName.has(s.name)) byName.set(s.name, { projects: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set() });
45309
+ const bucket = byName.get(s.name);
45310
+ bucket.projects.add(s.project);
45311
+ if (s.repo) bucket.repos.add(s.repo);
45210
45312
  }
45211
45313
  const currentNorm = normalizePath2(ctx.currentProject);
45212
45314
  const currentBase = basename3(currentNorm);
45213
- for (const [secretName, projects] of byName) {
45315
+ for (const [secretName, { projects, repos }] of byName) {
45214
45316
  const normalizedProjects = [...projects].map(normalizePath2);
45215
- const currentHas = normalizedProjects.some((p2) => {
45216
- if (p2 === currentNorm) return true;
45217
- const lastSegment = p2.split("/").pop() || "";
45218
- return lastSegment === currentBase;
45219
- });
45317
+ const repoSpecs = [...repos];
45318
+ const currentHas = normalizedProjects.some((p2) => p2 === currentNorm) || repoSpecs.some((r2) => repoBasename(r2) === currentBase);
45220
45319
  if (currentHas) continue;
45221
45320
  const siblingMatches = ctx.siblings.filter((sib) => {
45222
45321
  const sibNorm = normalizePath2(sib.path);
45223
- const sibBase = basename3(sibNorm);
45224
- return normalizedProjects.some((p2) => {
45225
- if (p2 === sibNorm) return true;
45226
- const lastSegment = p2.split("/").pop() || "";
45227
- return lastSegment === sibBase;
45228
- });
45322
+ if (normalizedProjects.some((p2) => p2 === sibNorm)) return true;
45323
+ return repoSpecs.some((r2) => repoMatchesSibling(r2, sib));
45229
45324
  });
45230
45325
  if (siblingMatches.length >= 2) {
45231
45326
  const sibNames = siblingMatches.map((s) => s.name).join(", ");
@@ -45242,19 +45337,20 @@ async function checkMissingSecret(ctx) {
45242
45337
  }
45243
45338
  return issues;
45244
45339
  }
45245
- var SECRET_SET_PATTERN, SECRET_SET_SIMPLE;
45340
+ var SECRET_SET_PREFIX, REPO_FLAG_PATTERN, VALUE_FLAGS;
45246
45341
  var init_missing_secret = __esm({
45247
45342
  "src/core/checks/session/missing-secret.ts"() {
45248
45343
  "use strict";
45249
45344
  init_define_WEB_FIRST_SEGMENTS();
45250
- SECRET_SET_PATTERN = /gh\s+secret\s+set\s+(\S+)\s+--repo\s+(\S+)/;
45251
- SECRET_SET_SIMPLE = /gh\s+secret\s+set\s+(\S+)/;
45345
+ SECRET_SET_PREFIX = /gh\s+secret\s+set\s+/;
45346
+ REPO_FLAG_PATTERN = /--repo\s+(\S+)/;
45347
+ VALUE_FLAGS = /* @__PURE__ */ new Set(["--repo", "-R", "-b", "--body", "--app", "--env"]);
45252
45348
  }
45253
45349
  });
45254
45350
 
45255
45351
  // src/core/checks/session/diverged-file.ts
45256
45352
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
45257
- import { join as join6 } from "node:path";
45353
+ import { join as join6, resolve as resolve7 } from "node:path";
45258
45354
  import { existsSync as existsSync2 } from "node:fs";
45259
45355
  async function loadLineSet(absPath) {
45260
45356
  let mtimeMs;
@@ -45303,11 +45399,16 @@ async function checkDivergedFile(ctx) {
45303
45399
  bothEmptyIsIdentical: true
45304
45400
  });
45305
45401
  if (overlap >= 0.2 && overlap < 0.9) {
45306
- diverged.push({ sibling: sib.name, overlap: Math.round(overlap * 100) });
45402
+ diverged.push({
45403
+ sibling: sib.name,
45404
+ overlap: Math.round(overlap * 100),
45405
+ path: resolve7(sibPath)
45406
+ });
45307
45407
  }
45308
45408
  }
45309
45409
  if (diverged.length > 0) {
45310
45410
  const details = diverged.map((d) => `${d.sibling} (${d.overlap}% overlap)`).join(", ");
45411
+ const sibPaths = diverged.map((d) => ` ${d.sibling}: ${d.path}`).join("\n");
45311
45412
  issues.push({
45312
45413
  severity: "warning",
45313
45414
  check: "session-diverged-file",
@@ -45315,7 +45416,9 @@ async function checkDivergedFile(ctx) {
45315
45416
  line: 0,
45316
45417
  message: `${fileName} has diverged from sibling repos: ${details}`,
45317
45418
  suggestion: `Compare with sibling versions to identify unintentional drift`,
45318
- detail: `Files with the same name across sibling repos should be kept in sync when they serve the same purpose`
45419
+ detail: `File: ${resolve7(currentPath)}
45420
+ Diverged siblings:
45421
+ ${sibPaths}`
45319
45422
  });
45320
45423
  }
45321
45424
  }
@@ -45402,15 +45505,15 @@ var init_missing_workflow = __esm({
45402
45505
 
45403
45506
  // src/core/checks/session/stale-memory.ts
45404
45507
  import { existsSync as existsSync4 } from "node:fs";
45405
- import { resolve as resolve7, isAbsolute as isAbsolute2 } from "node:path";
45508
+ import { resolve as resolve8, isAbsolute as isAbsolute3 } from "node:path";
45406
45509
  function resolveRef2(ref, projectRoot) {
45407
45510
  if (ref.startsWith("~/") || ref === "~") {
45408
45511
  const home2 = process.env.HOME || process.env.USERPROFILE;
45409
45512
  if (!home2) return null;
45410
- return ref === "~" ? home2 : resolve7(home2, ref.slice(2));
45513
+ return ref === "~" ? home2 : resolve8(home2, ref.slice(2));
45411
45514
  }
45412
- if (isAbsolute2(ref)) return ref;
45413
- return resolve7(projectRoot, ref);
45515
+ if (isAbsolute3(ref)) return ref;
45516
+ return resolve8(projectRoot, ref);
45414
45517
  }
45415
45518
  async function checkStaleMemory(ctx) {
45416
45519
  const issues = [];
@@ -45434,6 +45537,7 @@ async function checkStaleMemory(ctx) {
45434
45537
  ruleId: "session-stale-memory/stale-memory",
45435
45538
  line: 0,
45436
45539
  message: `Memory "${name}" references ${brokenPaths.length} path(s) that no longer exist: ${brokenPaths.join(", ")}`,
45540
+ affectedPaths: brokenPaths,
45437
45541
  suggestion: `Update or remove the memory file: ${mem.filePath}`,
45438
45542
  detail: `Memory files with broken path references may cause the AI agent to follow stale instructions`
45439
45543
  });
@@ -45453,7 +45557,15 @@ var init_stale_memory = __esm({
45453
45557
  async function checkDuplicateMemory(ctx) {
45454
45558
  const issues = [];
45455
45559
  const reported = /* @__PURE__ */ new Set();
45456
- const lineSets = ctx.memories.map((m) => toLineSet(m.content, MIN_TOKEN_LEN2));
45560
+ const lineSets = new Array(ctx.memories.length);
45561
+ const getLineSet = (idx) => {
45562
+ let ls2 = lineSets[idx];
45563
+ if (ls2 === void 0) {
45564
+ ls2 = toLineSet(ctx.memories[idx].content, MIN_TOKEN_LEN2);
45565
+ lineSets[idx] = ls2;
45566
+ }
45567
+ return ls2;
45568
+ };
45457
45569
  for (let i2 = 0; i2 < ctx.memories.length; i2++) {
45458
45570
  for (let j3 = i2 + 1; j3 < ctx.memories.length; j3++) {
45459
45571
  const a = ctx.memories[i2];
@@ -45463,7 +45575,7 @@ async function checkDuplicateMemory(ctx) {
45463
45575
  const bIsCurrent = projectDirMatchesPath(b2.projectDir, ctx.currentProject);
45464
45576
  if (!aIsCurrent && !bIsCurrent) continue;
45465
45577
  if (a.content.length < 50 || b2.content.length < 50) continue;
45466
- const overlap = jaccardSimilarityFromSets(lineSets[i2], lineSets[j3]);
45578
+ const overlap = jaccardSimilarityFromSets(getLineSet(i2), getLineSet(j3));
45467
45579
  if (overlap < 0.6) continue;
45468
45580
  const pairKey = [a.filePath, b2.filePath].sort().join("::");
45469
45581
  if (reported.has(pairKey)) continue;
@@ -45497,9 +45609,9 @@ var init_duplicate_memory = __esm({
45497
45609
  });
45498
45610
 
45499
45611
  // src/core/checks/session/loop-detection.ts
45500
- import { resolve as resolve8 } from "node:path";
45612
+ import { resolve as resolve9 } from "node:path";
45501
45613
  function normalizeProject(p2) {
45502
- return resolve8(p2).replace(/\\/g, "/");
45614
+ return resolve9(p2).replace(/\\/g, "/");
45503
45615
  }
45504
45616
  function findConsecutiveRepeats(displays) {
45505
45617
  const results = [];
@@ -45509,13 +45621,13 @@ function findConsecutiveRepeats(displays) {
45509
45621
  while (j3 < displays.length && displays[j3] === displays[i2]) j3++;
45510
45622
  const count = j3 - i2;
45511
45623
  if (count >= CONSECUTIVE_THRESHOLD) {
45512
- results.push({ command: displays[i2], count });
45624
+ results.push({ command: displays[i2], count, startIdx: i2, endIdx: j3 });
45513
45625
  }
45514
45626
  i2 = j3;
45515
45627
  }
45516
45628
  return results;
45517
45629
  }
45518
- function findCyclicPatterns(displays) {
45630
+ function findCyclicPatterns(displays, consecutiveSpans) {
45519
45631
  const results = [];
45520
45632
  for (let cycleLen = 2; cycleLen <= MAX_CYCLE_LENGTH; cycleLen++) {
45521
45633
  for (let start = 0; start <= displays.length - cycleLen * CYCLE_REPEAT_THRESHOLD; start++) {
@@ -45533,8 +45645,13 @@ function findCyclicPatterns(displays) {
45533
45645
  }
45534
45646
  }
45535
45647
  if (repeats >= CYCLE_REPEAT_THRESHOLD) {
45648
+ const cycleEnd = start + cycleLen * repeats;
45649
+ const overlapsConsecutive = consecutiveSpans.some(
45650
+ (s) => s.startIdx < cycleEnd && start < s.endIdx
45651
+ );
45652
+ if (overlapsConsecutive) continue;
45536
45653
  const alreadyCovered = results.some(
45537
- (r2) => r2.startIdx <= start && r2.startIdx + r2.cycle.length * r2.repeats >= start + cycleLen * repeats
45654
+ (r2) => r2.startIdx <= start && r2.startIdx + r2.cycle.length * r2.repeats >= cycleEnd
45538
45655
  );
45539
45656
  if (!alreadyCovered) {
45540
45657
  results.push({ cycle, repeats, startIdx: start });
@@ -45563,7 +45680,8 @@ async function checkLoopDetection(ctx) {
45563
45680
  suggestion: "An agent may be looping on this command. Check history.jsonl for context on what went wrong"
45564
45681
  });
45565
45682
  }
45566
- const cycles = findCyclicPatterns(displays);
45683
+ const consecutiveSpans = repeats.map((r2) => ({ startIdx: r2.startIdx, endIdx: r2.endIdx }));
45684
+ const cycles = findCyclicPatterns(displays, consecutiveSpans);
45567
45685
  for (const { cycle, repeats: reps } of cycles) {
45568
45686
  const cycleStr = cycle.map((c3) => c3.length > 40 ? c3.slice(0, 37) + "..." : c3).join(" -> ");
45569
45687
  issues.push({
@@ -45613,7 +45731,7 @@ async function checkMemoryIndexOverflow(ctx) {
45613
45731
  issues.push({
45614
45732
  severity: "warning",
45615
45733
  check: "session-memory-index-overflow",
45616
- ruleId: "session-memory-index-overflow/memory-index-overflow",
45734
+ ruleId: "session-memory-index-overflow/line-overflow",
45617
45735
  line: MAX_LINES + 1,
45618
45736
  message: `MEMORY.md has ${lineCount.toLocaleString()} lines \u2014 only the first ${MAX_LINES} are loaded. ${excess.toLocaleString()} line(s) are effectively invisible to the agent.`,
45619
45737
  detail: `File: ${memoryFile}`,
@@ -45625,7 +45743,7 @@ async function checkMemoryIndexOverflow(ctx) {
45625
45743
  issues.push({
45626
45744
  severity: "warning",
45627
45745
  check: "session-memory-index-overflow",
45628
- ruleId: "session-memory-index-overflow/memory-index-overflow",
45746
+ ruleId: "session-memory-index-overflow/byte-overflow",
45629
45747
  line: 0,
45630
45748
  message: `MEMORY.md is ${byteSize.toLocaleString()} bytes \u2014 only the first ${MAX_BYTES.toLocaleString()} bytes are loaded. ~${excess.toLocaleString()} bytes are effectively invisible.`,
45631
45749
  detail: `File: ${memoryFile}`,
@@ -45668,9 +45786,12 @@ async function findReleaseWorkflows(projectRoot) {
45668
45786
  }
45669
45787
  try {
45670
45788
  const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
45671
- const nameMatch = content.match(/^name:\s*(.+)$/m);
45672
- if (nameMatch && RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(nameMatch[1]))) {
45673
- releaseWorkflows.push(f);
45789
+ const nameMatch = content.match(/^\s*name:\s*(.+?)\s*$/m);
45790
+ if (nameMatch) {
45791
+ const name = nameMatch[1].replace(/^(['"])(.*)\1$/, "$2");
45792
+ if (RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(name))) {
45793
+ releaseWorkflows.push(f);
45794
+ }
45674
45795
  }
45675
45796
  } catch {
45676
45797
  }
@@ -46524,6 +46645,7 @@ function extractPathsFromMessage(msg) {
46524
46645
  function applyIgnoreRules(issues, rules) {
46525
46646
  const compiled = compileRules(rules);
46526
46647
  const kept = [];
46648
+ const keepMask = [];
46527
46649
  let dropped = 0;
46528
46650
  for (const issue2 of issues) {
46529
46651
  let suppress = false;
@@ -46531,7 +46653,7 @@ function applyIgnoreRules(issues, rules) {
46531
46653
  if (rule.check !== issue2.check) continue;
46532
46654
  if (rule.pathPattern) {
46533
46655
  if (issue2.check !== "session-stale-memory") continue;
46534
- const paths = extractPathsFromMessage(issue2.message);
46656
+ const paths = issue2.affectedPaths ?? extractPathsFromMessage(issue2.message);
46535
46657
  if (paths.length === 0) continue;
46536
46658
  const allMatch = paths.every((p2) => rule.pathPattern.test(p2));
46537
46659
  if (!allMatch) continue;
@@ -46543,12 +46665,15 @@ function applyIgnoreRules(issues, rules) {
46543
46665
  }
46544
46666
  if (suppress) {
46545
46667
  dropped++;
46668
+ keepMask.push(false);
46546
46669
  } else {
46547
46670
  kept.push(issue2);
46671
+ keepMask.push(true);
46548
46672
  }
46549
46673
  }
46550
46674
  return {
46551
46675
  kept,
46676
+ keepMask,
46552
46677
  dropped,
46553
46678
  unusedRules: compiled.filter((r2) => !r2.fired).map(stripCompiled),
46554
46679
  rulesMissingReason: rules.filter((r2) => !r2.reason)
@@ -46583,12 +46708,12 @@ var init_types3 = __esm({
46583
46708
 
46584
46709
  // src/version.ts
46585
46710
  import { readFileSync as readFileSync7 } from "node:fs";
46586
- import { resolve as resolve11, dirname as dirname5 } from "node:path";
46711
+ import { resolve as resolve12, dirname as dirname6 } from "node:path";
46587
46712
  import { fileURLToPath as fileURLToPath2 } from "node:url";
46588
46713
  function loadVersion() {
46589
- if (true) return "0.13.0";
46590
- const __dir = dirname5(fileURLToPath2(import.meta.url));
46591
- const pkgPath = resolve11(__dir, "../package.json");
46714
+ if (true) return "0.13.2";
46715
+ const __dir = dirname6(fileURLToPath2(import.meta.url));
46716
+ const pkgPath = resolve12(__dir, "../package.json");
46592
46717
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
46593
46718
  return pkg.version;
46594
46719
  }
@@ -46641,7 +46766,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
46641
46766
  if (activeChecks.includes("tokens"))
46642
46767
  checkPromises.push(checkTokens(file2, projectRoot, thresholds));
46643
46768
  if (activeChecks.includes("tier-tokens"))
46644
- checkPromises.push(checkTierTokens(file2, projectRoot, thresholds));
46769
+ checkPromises.push(
46770
+ checkTierTokens(file2, projectRoot, thresholds, Boolean(options.hooksGlobal))
46771
+ );
46645
46772
  if (activeChecks.includes("redundancy"))
46646
46773
  checkPromises.push(checkRedundancy(file2, projectRoot));
46647
46774
  if (activeChecks.includes("frontmatter"))
@@ -46896,10 +47023,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
46896
47023
  }
46897
47024
  });
46898
47025
  const applied = applyIgnoreRules(flat, options.ignoreRules);
46899
- const keptSet = new Set(applied.kept);
46900
47026
  const rebuilt = fileResults.map(() => []);
46901
47027
  flat.forEach((issue2, i2) => {
46902
- if (keptSet.has(issue2)) rebuilt[owners[i2]].push(issue2);
47028
+ if (applied.keepMask[i2]) rebuilt[owners[i2]].push(issue2);
46903
47029
  });
46904
47030
  fileResults.forEach((fr, idx) => {
46905
47031
  fr.issues = rebuilt[idx];
@@ -46913,9 +47039,13 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
46913
47039
  let estimatedWaste = 0;
46914
47040
  for (const fr of fileResults) {
46915
47041
  for (const issue2 of fr.issues) {
46916
- if (issue2.check === "redundancy" && issue2.suggestion) {
46917
- const tokenMatch = issue2.suggestion.match(/~(\d+)\s+tokens/);
46918
- if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
47042
+ if (issue2.check !== "redundancy") continue;
47043
+ if (typeof issue2.wastedTokens === "number") {
47044
+ estimatedWaste += issue2.wastedTokens;
47045
+ } else if (issue2.suggestion) {
47046
+ for (const m of issue2.suggestion.matchAll(/~(\d+)\s+tokens/g)) {
47047
+ estimatedWaste += parseInt(m[1], 10);
47048
+ }
46919
47049
  }
46920
47050
  }
46921
47051
  }
@@ -47593,12 +47723,6 @@ function applyFixes(result, options = {}) {
47593
47723
  } : console.log.bind(console);
47594
47724
  const dryRun = options.dryRun ?? false;
47595
47725
  const skipSymlinks = options.skipSymlinks ?? true;
47596
- const symlinkFiles = /* @__PURE__ */ new Set();
47597
- if (skipSymlinks) {
47598
- for (const f of result.files) {
47599
- if (f.isSymlink) symlinkFiles.add(f.path);
47600
- }
47601
- }
47602
47726
  const fixesByFile = /* @__PURE__ */ new Map();
47603
47727
  const dedupeKeys = /* @__PURE__ */ new Map();
47604
47728
  const skippedSymlinks = /* @__PURE__ */ new Set();
@@ -48154,7 +48278,7 @@ var init_server3 = __esm({
48154
48278
  checks: external_exports3.array(mcphCheckEnum).optional().describe("Specific mcph checks to run (default: all mcph-* checks)."),
48155
48279
  includeGlobal: external_exports3.boolean().optional().describe("Also scan ~/.mcph.json (user-global config)."),
48156
48280
  strictEnvToken: external_exports3.boolean().optional().describe(
48157
- "Upgrade mcph-config/prefer-env-token from warning to error (env-var-only posture)."
48281
+ "Upgrade mcph-token-security/prefer-env-token from warning to error (env-var-only posture)."
48158
48282
  )
48159
48283
  },
48160
48284
  {
@@ -54801,6 +54925,11 @@ function formatText(result, verbose = false) {
54801
54925
  lines.push("Session audit scanned");
54802
54926
  renderedAnySummary = true;
54803
54927
  }
54928
+ if (groups.skill.length > 0) {
54929
+ if (renderedAnySummary) lines.push("");
54930
+ lines.push("Skill audit scanned");
54931
+ renderedAnySummary = true;
54932
+ }
54804
54933
  if (!renderedAnySummary) {
54805
54934
  lines.push(`Found ${result.files.length} file${result.files.length !== 1 ? "s" : ""}`);
54806
54935
  }
@@ -54896,12 +55025,13 @@ function formatTokenReport(result) {
54896
55025
  source_default.dim(" (counts use GPT-4 cl100k_base tokenizer \u2014 Claude counts may vary slightly)")
54897
55026
  );
54898
55027
  lines.push("");
54899
- const maxPathLen = Math.max(...result.files.map((f) => f.path.length), 4);
55028
+ const realFiles = result.files.filter((f) => !isSyntheticBucket(f.path));
55029
+ const maxPathLen = Math.max(...realFiles.map((f) => f.path.length), 4);
54900
55030
  lines.push(
54901
55031
  ` ${source_default.dim("File".padEnd(maxPathLen))} ${source_default.dim("Tokens".padStart(8))} ${source_default.dim("Lines".padStart(6))}`
54902
55032
  );
54903
55033
  lines.push(` ${"-".repeat(maxPathLen)} ${"-".repeat(8)} ${"-".repeat(6)}`);
54904
- for (const file2 of result.files) {
55034
+ for (const file2 of realFiles) {
54905
55035
  const tokenStr = file2.tokens.toLocaleString().padStart(8);
54906
55036
  const lineStr = file2.lines.toString().padStart(6);
54907
55037
  lines.push(` ${file2.path.padEnd(maxPathLen)} ${tokenStr} ${lineStr}`);
@@ -54922,7 +55052,7 @@ function formatTokenReport(result) {
54922
55052
  return lines.join("\n");
54923
55053
  }
54924
55054
  function isSyntheticPath(p2) {
54925
- return p2.startsWith("(") || p2.startsWith("~");
55055
+ return isSyntheticBucket(p2) || p2.startsWith("~");
54926
55056
  }
54927
55057
  function formatSarif(result) {
54928
55058
  const severityToLevel = {
@@ -55244,7 +55374,7 @@ async function runCli() {
55244
55374
  new Option("--format <format>", "Output format: text, json, or sarif").choices(["text", "json", "sarif"]).default("text")
55245
55375
  ).option("--tokens", "Show token breakdown per file", false).option("--verbose", "Show passing checks too", false).option("--fix", "Auto-fix broken paths using git history and fuzzy matching", false).option("--fix-dry-run", "Preview --fix changes without writing", false).option("--yes", "Skip interactive confirmation prompts (required for --fix in TTY)", false).option("--follow-symlinks", "Allow --fix to write through symlinks (default: skip)", false).option("--ignore <checks>", "Comma-separated list of checks to ignore", "").option("--quiet", "Suppress all output except errors (exit code only)", false).option("--config <path>", "Path to config file (default: .ctxlintrc in project root)").option("--depth <n>", "Max subdirectory depth to scan (default: 2)", "2").option("--mcp", "Enable MCP config linting alongside context file checks", false).option("--mcp-only", "Run only MCP config checks, skip context file checks", false).option("--mcp-global", "Also scan user/global MCP config files (implies --mcp)", false).option("--mcp-server", "Start the MCP server (alias: `ctxlint serve`)").option("--mcph", "Enable .mcph.json (mcp.hosting CLI config) linting", false).option("--mcph-only", "Run only mcph config checks", false).option("--mcph-global", "Also scan ~/.mcph.json (implies --mcph)", false).option(
55246
55376
  "--mcph-strict-env-token",
55247
- "Upgrade mcph-config/prefer-env-token from warning to error (strict env-var-only posture)",
55377
+ "Upgrade mcph-token-security/prefer-env-token from warning to error (strict env-var-only posture)",
55248
55378
  false
55249
55379
  ).option("--session", "Run session audit checks (cross-project consistency)", false).option("--session-only", "Run only session checks, skip context and MCP checks", false).option("--skills", "Run agent-skill checks (~/.claude/skills + ~/.claude/agents)", false).option("--skills-only", "Run only agent-skill checks, skip everything else", false).option(
55250
55380
  "--hooks-global",
@@ -55278,7 +55408,7 @@ async function runCli() {
55278
55408
  if (result.files.length === 0) {
55279
55409
  if (!options.quiet) {
55280
55410
  if (options.format === "json") {
55281
- console.log(JSON.stringify(result));
55411
+ console.log(formatJson(result));
55282
55412
  } else if (options.format === "sarif") {
55283
55413
  console.log(formatSarif(result));
55284
55414
  } else {
@@ -55418,7 +55548,7 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
55418
55548
  let liveOptions = options;
55419
55549
  let liveActiveChecks = activeChecks;
55420
55550
  try {
55421
- const resolved = resolveSession(resolvedPath, opts);
55551
+ const resolved = resolveSession(resolvedPath, opts, true);
55422
55552
  liveConfig = resolved.config;
55423
55553
  liveOptions = resolved.options;
55424
55554
  liveActiveChecks = resolved.activeChecks;
@@ -55540,9 +55670,9 @@ async function promptYesNo(question) {
55540
55670
  rl.close();
55541
55671
  }
55542
55672
  }
55543
- function resolveSession(resolvedPath, opts) {
55673
+ function resolveSession(resolvedPath, opts, throwOnConfigError = false) {
55544
55674
  const configPath = opts.config ? path16.resolve(opts.config) : void 0;
55545
- const config2 = configPath ? loadConfigFromPath(configPath) : loadConfig(resolvedPath);
55675
+ const config2 = configPath ? loadConfigFromPath(configPath, throwOnConfigError) : loadConfig(resolvedPath);
55546
55676
  const mcpGlobal = opts.mcpGlobal || config2?.mcpGlobal || false;
55547
55677
  const mcpOnly = opts.mcpOnly || config2?.mcpOnly || false;
55548
55678
  const mcpFlag = opts.mcp || mcpGlobal || mcpOnly || config2?.mcp || false;
@@ -55619,10 +55749,11 @@ function resolveSession(resolvedPath, opts) {
55619
55749
  const activeChecks = options.checks.filter((c3) => !options.ignore.includes(c3));
55620
55750
  return { config: config2, options, activeChecks };
55621
55751
  }
55622
- function loadConfigFromPath(configPath) {
55752
+ function loadConfigFromPath(configPath, throwOnError = false) {
55623
55753
  try {
55624
55754
  return loadConfigFromExplicitPath(configPath);
55625
55755
  } catch (err) {
55756
+ if (throwOnError) throw err;
55626
55757
  const detail = err instanceof Error ? err.message : String(err);
55627
55758
  console.error(`Error: ${detail}`);
55628
55759
  process.exit(2);