@yawlabs/ctxlint 0.13.1 → 0.13.2

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.
@@ -4,7 +4,7 @@
4
4
  # Version-pinned so a checkout at `rev: vX.Y.Z` runs exactly that release
5
5
  # of ctxlint — matches the pinning done by `ctxlint init`. release.sh keeps
6
6
  # this in sync with package.json on each bump.
7
- entry: npx @yawlabs/ctxlint@0.13.1 --strict
7
+ entry: npx @yawlabs/ctxlint@0.13.2 --strict
8
8
  language: node
9
9
  always_run: true
10
10
  pass_filenames: false
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;
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);
34445
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];
@@ -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
 
@@ -42925,6 +42951,7 @@ function computeSectionCosts(file2) {
42925
42951
  }).sort((a, b2) => b2.tokens - a.tokens);
42926
42952
  }
42927
42953
  function loadSettingsSources(projectRoot) {
42954
+ if (settingsCache?.root === projectRoot) return settingsCache.data;
42928
42955
  const sources = [];
42929
42956
  const candidates = [
42930
42957
  path7.join(projectRoot, ".claude", "settings.json"),
@@ -42944,6 +42971,7 @@ function loadSettingsSources(projectRoot) {
42944
42971
  console.warn(`ctxlint: could not parse ${p2}: ${err.message}`);
42945
42972
  }
42946
42973
  }
42974
+ settingsCache = { root: projectRoot, data: sources };
42947
42975
  return sources;
42948
42976
  }
42949
42977
  function canonicalizeCommand(backticked) {
@@ -43038,7 +43066,7 @@ function checkAggregateTierTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS)
43038
43066
  suggestion: "Consider moving the largest files or their heaviest sections to on-demand tiers (skills, subagents, memory)."
43039
43067
  };
43040
43068
  }
43041
- var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
43069
+ var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND, settingsCache;
43042
43070
  var init_tier_tokens = __esm({
43043
43071
  "src/core/checks/tier-tokens.ts"() {
43044
43072
  "use strict";
@@ -43068,6 +43096,7 @@ var init_tier_tokens = __esm({
43068
43096
  ];
43069
43097
  TOP_SECTIONS_TO_REPORT = 3;
43070
43098
  INVIOLABLE_WITH_COMMAND = /\b(NEVER|ALWAYS|DON'?T|DO NOT|MUST NOT)\b[^.!?`]{0,80}`([^`]+)`/i;
43099
+ settingsCache = null;
43071
43100
  }
43072
43101
  });
43073
43102
 
@@ -43164,7 +43193,8 @@ async function checkRedundancy(file2, projectRoot) {
43164
43193
  ruleId: "redundancy/tech-mention",
43165
43194
  line: i2 + 1,
43166
43195
  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`
43196
+ suggestion: `~${wastedTokens} tokens could be saved`,
43197
+ wastedTokens
43168
43198
  });
43169
43199
  }
43170
43200
  }
@@ -43178,7 +43208,8 @@ async function checkRedundancy(file2, projectRoot) {
43178
43208
  );
43179
43209
  if (dirMatch) {
43180
43210
  const dir = dirMatch[1].replace(/[`"]/g, "");
43181
- const fullPath = path8.resolve(projectRoot, dir);
43211
+ const baseDir = dir.startsWith("./") || dir.startsWith("../") ? path8.dirname(file2.filePath) : projectRoot;
43212
+ const fullPath = path8.resolve(baseDir, dir);
43182
43213
  if (isDirectory(fullPath)) {
43183
43214
  issues.push({
43184
43215
  severity: "info",
@@ -43410,19 +43441,23 @@ var init_contradictions = __esm({
43410
43441
  options: [
43411
43442
  {
43412
43443
  label: "Jest",
43413
- patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest.*with\s+jest\b/i]
43444
+ // The third pattern bounds the gap between `test` and `with <fw>` to a
43445
+ // few intra-clause chars so prose like "run our test suite and then
43446
+ // deploy with jest" no longer matches the framework directive — only
43447
+ // tight phrasings like "test with jest" / "tests run with jest" do.
43448
+ patterns: [/\buse\s+jest\b/i, /\bjest\s+for\s+test/i, /\btest\w*\s+with\s+jest\b/i]
43414
43449
  },
43415
43450
  {
43416
43451
  label: "Vitest",
43417
- patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest.*with\s+vitest\b/i]
43452
+ patterns: [/\buse\s+vitest\b/i, /\bvitest\s+for\s+test/i, /\btest\w*\s+with\s+vitest\b/i]
43418
43453
  },
43419
43454
  {
43420
43455
  label: "Mocha",
43421
- patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest.*with\s+mocha\b/i]
43456
+ patterns: [/\buse\s+mocha\b/i, /\bmocha\s+for\s+test/i, /\btest\w*\s+with\s+mocha\b/i]
43422
43457
  },
43423
43458
  {
43424
43459
  label: "pytest",
43425
- patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest.*with\s+pytest\b/i]
43460
+ patterns: [/\buse\s+pytest\b/i, /\bpytest\s+for\s+test/i, /\btest\w*\s+with\s+pytest\b/i]
43426
43461
  },
43427
43462
  {
43428
43463
  label: "Playwright",
@@ -43477,19 +43512,11 @@ var init_contradictions = __esm({
43477
43512
  },
43478
43513
  {
43479
43514
  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
- ]
43515
+ patterns: [/\b2[\s-]?space\s+indent/i, /\bindent\s+with\s+2\s+spaces/i]
43485
43516
  },
43486
43517
  {
43487
43518
  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
- ]
43519
+ patterns: [/\b4[\s-]?space\s+indent/i, /\bindent\s+with\s+4\s+spaces/i]
43493
43520
  }
43494
43521
  ]
43495
43522
  },
@@ -43651,10 +43678,11 @@ function hasUnbalancedBracketsOrQuotes(val) {
43651
43678
  if (square < 0 || curly < 0) return true;
43652
43679
  }
43653
43680
  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;
43681
+ const quoteChar = val[0];
43682
+ if (quoteChar === '"' || quoteChar === "'") {
43683
+ const count = (val.match(quoteChar === '"' ? /"/g : /'/g) || []).length;
43684
+ if (count % 2 !== 0) return true;
43685
+ }
43658
43686
  return false;
43659
43687
  }
43660
43688
  async function checkFrontmatter(file2, _projectRoot) {
@@ -44864,13 +44892,13 @@ var init_gitignore = __esm({
44864
44892
  // src/core/session-parser.ts
44865
44893
  import { readFile } from "node:fs/promises";
44866
44894
  import { readFileSync as readFileSync4 } from "node:fs";
44867
- import { dirname as dirname2, resolve as resolve4 } from "node:path";
44895
+ import { dirname as dirname3, resolve as resolve4 } from "node:path";
44868
44896
  import { fileURLToPath } from "node:url";
44869
44897
  function loadWebFirstSegments() {
44870
44898
  if (typeof define_WEB_FIRST_SEGMENTS_default !== "undefined") {
44871
44899
  return new Set(define_WEB_FIRST_SEGMENTS_default);
44872
44900
  }
44873
- const __dir = dirname2(fileURLToPath(import.meta.url));
44901
+ const __dir = dirname3(fileURLToPath(import.meta.url));
44874
44902
  const fixturePath = resolve4(__dir, "../../tests/fixtures/web-first-segments.json");
44875
44903
  const raw = JSON.parse(readFileSync4(fixturePath, "utf-8"));
44876
44904
  const list = Array.isArray(raw) ? raw : raw.segments;
@@ -44981,7 +45009,7 @@ var init_session_parser = __esm({
44981
45009
  import { readdir, stat } from "node:fs/promises";
44982
45010
  import { createReadStream } from "node:fs";
44983
45011
  import { createInterface } from "node:readline";
44984
- import { dirname as dirname3, join as join5, resolve as resolve5 } from "node:path";
45012
+ import { dirname as dirname4, join as join5, resolve as resolve5 } from "node:path";
44985
45013
  import { existsSync } from "node:fs";
44986
45014
  function detectProviders() {
44987
45015
  if (!home) return [];
@@ -45076,7 +45104,7 @@ function detectAiderInSiblings(siblings) {
45076
45104
  return null;
45077
45105
  }
45078
45106
  async function detectSiblings(projectRoot) {
45079
- const parentDir = dirname3(resolve5(projectRoot));
45107
+ const parentDir = dirname4(resolve5(projectRoot));
45080
45108
  let entries;
45081
45109
  try {
45082
45110
  entries = await readdir(parentDir);
@@ -45193,11 +45221,12 @@ async function checkMissingSecret(ctx) {
45193
45221
  const issues = [];
45194
45222
  const secrets = [];
45195
45223
  for (const entry of ctx.history) {
45196
- const match = entry.display.match(SECRET_SET_PATTERN) || entry.display.match(SECRET_SET_SIMPLE);
45224
+ const match = entry.display.match(SECRET_SET_SIMPLE);
45197
45225
  if (!match) continue;
45226
+ const repoMatch = entry.display.match(REPO_FLAG_PATTERN);
45198
45227
  secrets.push({
45199
45228
  name: match[1],
45200
- repo: match[2],
45229
+ repo: repoMatch ? repoMatch[1] : void 0,
45201
45230
  project: entry.project
45202
45231
  });
45203
45232
  }
@@ -45242,19 +45271,19 @@ async function checkMissingSecret(ctx) {
45242
45271
  }
45243
45272
  return issues;
45244
45273
  }
45245
- var SECRET_SET_PATTERN, SECRET_SET_SIMPLE;
45274
+ var SECRET_SET_SIMPLE, REPO_FLAG_PATTERN;
45246
45275
  var init_missing_secret = __esm({
45247
45276
  "src/core/checks/session/missing-secret.ts"() {
45248
45277
  "use strict";
45249
45278
  init_define_WEB_FIRST_SEGMENTS();
45250
- SECRET_SET_PATTERN = /gh\s+secret\s+set\s+(\S+)\s+--repo\s+(\S+)/;
45251
45279
  SECRET_SET_SIMPLE = /gh\s+secret\s+set\s+(\S+)/;
45280
+ REPO_FLAG_PATTERN = /--repo\s+(\S+)/;
45252
45281
  }
45253
45282
  });
45254
45283
 
45255
45284
  // src/core/checks/session/diverged-file.ts
45256
45285
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
45257
- import { join as join6 } from "node:path";
45286
+ import { join as join6, resolve as resolve7 } from "node:path";
45258
45287
  import { existsSync as existsSync2 } from "node:fs";
45259
45288
  async function loadLineSet(absPath) {
45260
45289
  let mtimeMs;
@@ -45303,11 +45332,16 @@ async function checkDivergedFile(ctx) {
45303
45332
  bothEmptyIsIdentical: true
45304
45333
  });
45305
45334
  if (overlap >= 0.2 && overlap < 0.9) {
45306
- diverged.push({ sibling: sib.name, overlap: Math.round(overlap * 100) });
45335
+ diverged.push({
45336
+ sibling: sib.name,
45337
+ overlap: Math.round(overlap * 100),
45338
+ path: resolve7(sibPath)
45339
+ });
45307
45340
  }
45308
45341
  }
45309
45342
  if (diverged.length > 0) {
45310
45343
  const details = diverged.map((d) => `${d.sibling} (${d.overlap}% overlap)`).join(", ");
45344
+ const sibPaths = diverged.map((d) => ` ${d.sibling}: ${d.path}`).join("\n");
45311
45345
  issues.push({
45312
45346
  severity: "warning",
45313
45347
  check: "session-diverged-file",
@@ -45315,7 +45349,9 @@ async function checkDivergedFile(ctx) {
45315
45349
  line: 0,
45316
45350
  message: `${fileName} has diverged from sibling repos: ${details}`,
45317
45351
  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`
45352
+ detail: `File: ${resolve7(currentPath)}
45353
+ Diverged siblings:
45354
+ ${sibPaths}`
45319
45355
  });
45320
45356
  }
45321
45357
  }
@@ -45402,15 +45438,15 @@ var init_missing_workflow = __esm({
45402
45438
 
45403
45439
  // src/core/checks/session/stale-memory.ts
45404
45440
  import { existsSync as existsSync4 } from "node:fs";
45405
- import { resolve as resolve7, isAbsolute as isAbsolute2 } from "node:path";
45441
+ import { resolve as resolve8, isAbsolute as isAbsolute2 } from "node:path";
45406
45442
  function resolveRef2(ref, projectRoot) {
45407
45443
  if (ref.startsWith("~/") || ref === "~") {
45408
45444
  const home2 = process.env.HOME || process.env.USERPROFILE;
45409
45445
  if (!home2) return null;
45410
- return ref === "~" ? home2 : resolve7(home2, ref.slice(2));
45446
+ return ref === "~" ? home2 : resolve8(home2, ref.slice(2));
45411
45447
  }
45412
45448
  if (isAbsolute2(ref)) return ref;
45413
- return resolve7(projectRoot, ref);
45449
+ return resolve8(projectRoot, ref);
45414
45450
  }
45415
45451
  async function checkStaleMemory(ctx) {
45416
45452
  const issues = [];
@@ -45453,7 +45489,15 @@ var init_stale_memory = __esm({
45453
45489
  async function checkDuplicateMemory(ctx) {
45454
45490
  const issues = [];
45455
45491
  const reported = /* @__PURE__ */ new Set();
45456
- const lineSets = ctx.memories.map((m) => toLineSet(m.content, MIN_TOKEN_LEN2));
45492
+ const lineSets = new Array(ctx.memories.length);
45493
+ const getLineSet = (idx) => {
45494
+ let ls2 = lineSets[idx];
45495
+ if (ls2 === void 0) {
45496
+ ls2 = toLineSet(ctx.memories[idx].content, MIN_TOKEN_LEN2);
45497
+ lineSets[idx] = ls2;
45498
+ }
45499
+ return ls2;
45500
+ };
45457
45501
  for (let i2 = 0; i2 < ctx.memories.length; i2++) {
45458
45502
  for (let j3 = i2 + 1; j3 < ctx.memories.length; j3++) {
45459
45503
  const a = ctx.memories[i2];
@@ -45463,7 +45507,7 @@ async function checkDuplicateMemory(ctx) {
45463
45507
  const bIsCurrent = projectDirMatchesPath(b2.projectDir, ctx.currentProject);
45464
45508
  if (!aIsCurrent && !bIsCurrent) continue;
45465
45509
  if (a.content.length < 50 || b2.content.length < 50) continue;
45466
- const overlap = jaccardSimilarityFromSets(lineSets[i2], lineSets[j3]);
45510
+ const overlap = jaccardSimilarityFromSets(getLineSet(i2), getLineSet(j3));
45467
45511
  if (overlap < 0.6) continue;
45468
45512
  const pairKey = [a.filePath, b2.filePath].sort().join("::");
45469
45513
  if (reported.has(pairKey)) continue;
@@ -45497,9 +45541,9 @@ var init_duplicate_memory = __esm({
45497
45541
  });
45498
45542
 
45499
45543
  // src/core/checks/session/loop-detection.ts
45500
- import { resolve as resolve8 } from "node:path";
45544
+ import { resolve as resolve9 } from "node:path";
45501
45545
  function normalizeProject(p2) {
45502
- return resolve8(p2).replace(/\\/g, "/");
45546
+ return resolve9(p2).replace(/\\/g, "/");
45503
45547
  }
45504
45548
  function findConsecutiveRepeats(displays) {
45505
45549
  const results = [];
@@ -45509,13 +45553,13 @@ function findConsecutiveRepeats(displays) {
45509
45553
  while (j3 < displays.length && displays[j3] === displays[i2]) j3++;
45510
45554
  const count = j3 - i2;
45511
45555
  if (count >= CONSECUTIVE_THRESHOLD) {
45512
- results.push({ command: displays[i2], count });
45556
+ results.push({ command: displays[i2], count, startIdx: i2, endIdx: j3 });
45513
45557
  }
45514
45558
  i2 = j3;
45515
45559
  }
45516
45560
  return results;
45517
45561
  }
45518
- function findCyclicPatterns(displays) {
45562
+ function findCyclicPatterns(displays, consecutiveSpans) {
45519
45563
  const results = [];
45520
45564
  for (let cycleLen = 2; cycleLen <= MAX_CYCLE_LENGTH; cycleLen++) {
45521
45565
  for (let start = 0; start <= displays.length - cycleLen * CYCLE_REPEAT_THRESHOLD; start++) {
@@ -45533,8 +45577,13 @@ function findCyclicPatterns(displays) {
45533
45577
  }
45534
45578
  }
45535
45579
  if (repeats >= CYCLE_REPEAT_THRESHOLD) {
45580
+ const cycleEnd = start + cycleLen * repeats;
45581
+ const overlapsConsecutive = consecutiveSpans.some(
45582
+ (s) => s.startIdx < cycleEnd && start < s.endIdx
45583
+ );
45584
+ if (overlapsConsecutive) continue;
45536
45585
  const alreadyCovered = results.some(
45537
- (r2) => r2.startIdx <= start && r2.startIdx + r2.cycle.length * r2.repeats >= start + cycleLen * repeats
45586
+ (r2) => r2.startIdx <= start && r2.startIdx + r2.cycle.length * r2.repeats >= cycleEnd
45538
45587
  );
45539
45588
  if (!alreadyCovered) {
45540
45589
  results.push({ cycle, repeats, startIdx: start });
@@ -45563,7 +45612,8 @@ async function checkLoopDetection(ctx) {
45563
45612
  suggestion: "An agent may be looping on this command. Check history.jsonl for context on what went wrong"
45564
45613
  });
45565
45614
  }
45566
- const cycles = findCyclicPatterns(displays);
45615
+ const consecutiveSpans = repeats.map((r2) => ({ startIdx: r2.startIdx, endIdx: r2.endIdx }));
45616
+ const cycles = findCyclicPatterns(displays, consecutiveSpans);
45567
45617
  for (const { cycle, repeats: reps } of cycles) {
45568
45618
  const cycleStr = cycle.map((c3) => c3.length > 40 ? c3.slice(0, 37) + "..." : c3).join(" -> ");
45569
45619
  issues.push({
@@ -45668,9 +45718,12 @@ async function findReleaseWorkflows(projectRoot) {
45668
45718
  }
45669
45719
  try {
45670
45720
  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);
45721
+ const nameMatch = content.match(/^\s*name:\s*(.+?)\s*$/m);
45722
+ if (nameMatch) {
45723
+ const name = nameMatch[1].replace(/^(['"])(.*)\1$/, "$2");
45724
+ if (RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(name))) {
45725
+ releaseWorkflows.push(f);
45726
+ }
45674
45727
  }
45675
45728
  } catch {
45676
45729
  }
@@ -46524,6 +46577,7 @@ function extractPathsFromMessage(msg) {
46524
46577
  function applyIgnoreRules(issues, rules) {
46525
46578
  const compiled = compileRules(rules);
46526
46579
  const kept = [];
46580
+ const keepMask = [];
46527
46581
  let dropped = 0;
46528
46582
  for (const issue2 of issues) {
46529
46583
  let suppress = false;
@@ -46543,12 +46597,15 @@ function applyIgnoreRules(issues, rules) {
46543
46597
  }
46544
46598
  if (suppress) {
46545
46599
  dropped++;
46600
+ keepMask.push(false);
46546
46601
  } else {
46547
46602
  kept.push(issue2);
46603
+ keepMask.push(true);
46548
46604
  }
46549
46605
  }
46550
46606
  return {
46551
46607
  kept,
46608
+ keepMask,
46552
46609
  dropped,
46553
46610
  unusedRules: compiled.filter((r2) => !r2.fired).map(stripCompiled),
46554
46611
  rulesMissingReason: rules.filter((r2) => !r2.reason)
@@ -46583,12 +46640,12 @@ var init_types3 = __esm({
46583
46640
 
46584
46641
  // src/version.ts
46585
46642
  import { readFileSync as readFileSync7 } from "node:fs";
46586
- import { resolve as resolve11, dirname as dirname5 } from "node:path";
46643
+ import { resolve as resolve12, dirname as dirname6 } from "node:path";
46587
46644
  import { fileURLToPath as fileURLToPath2 } from "node:url";
46588
46645
  function loadVersion() {
46589
- if (true) return "0.13.0";
46590
- const __dir = dirname5(fileURLToPath2(import.meta.url));
46591
- const pkgPath = resolve11(__dir, "../package.json");
46646
+ if (true) return "0.13.1";
46647
+ const __dir = dirname6(fileURLToPath2(import.meta.url));
46648
+ const pkgPath = resolve12(__dir, "../package.json");
46592
46649
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
46593
46650
  return pkg.version;
46594
46651
  }
@@ -46896,10 +46953,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
46896
46953
  }
46897
46954
  });
46898
46955
  const applied = applyIgnoreRules(flat, options.ignoreRules);
46899
- const keptSet = new Set(applied.kept);
46900
46956
  const rebuilt = fileResults.map(() => []);
46901
46957
  flat.forEach((issue2, i2) => {
46902
- if (keptSet.has(issue2)) rebuilt[owners[i2]].push(issue2);
46958
+ if (applied.keepMask[i2]) rebuilt[owners[i2]].push(issue2);
46903
46959
  });
46904
46960
  fileResults.forEach((fr, idx) => {
46905
46961
  fr.issues = rebuilt[idx];
@@ -46913,7 +46969,10 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
46913
46969
  let estimatedWaste = 0;
46914
46970
  for (const fr of fileResults) {
46915
46971
  for (const issue2 of fr.issues) {
46916
- if (issue2.check === "redundancy" && issue2.suggestion) {
46972
+ if (issue2.check !== "redundancy") continue;
46973
+ if (typeof issue2.wastedTokens === "number") {
46974
+ estimatedWaste += issue2.wastedTokens;
46975
+ } else if (issue2.suggestion) {
46917
46976
  const tokenMatch = issue2.suggestion.match(/~(\d+)\s+tokens/);
46918
46977
  if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
46919
46978
  }
@@ -54801,6 +54860,11 @@ function formatText(result, verbose = false) {
54801
54860
  lines.push("Session audit scanned");
54802
54861
  renderedAnySummary = true;
54803
54862
  }
54863
+ if (groups.skill.length > 0) {
54864
+ if (renderedAnySummary) lines.push("");
54865
+ lines.push("Skill audit scanned");
54866
+ renderedAnySummary = true;
54867
+ }
54804
54868
  if (!renderedAnySummary) {
54805
54869
  lines.push(`Found ${result.files.length} file${result.files.length !== 1 ? "s" : ""}`);
54806
54870
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/ctxlint",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "mcpName": "io.github.YawLabs/ctxlint",
5
5
  "description": "Lint your AI agent context files, MCP server configs, and session data against your actual codebase",
6
6
  "bin": {