@yawlabs/ctxlint 0.13.0 → 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.0 --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",
@@ -40905,6 +40915,45 @@ async function getCommitsSinceBatch(projectRoot, paths, since) {
40905
40915
  }
40906
40916
  return out;
40907
40917
  }
40918
+ function normalizeRenamePath(p2) {
40919
+ return p2.replace(/\\/g, "/").replace(/^\.\//, "");
40920
+ }
40921
+ function parseRenameLog(result, targetPath) {
40922
+ if (!result.trim()) return null;
40923
+ const want = targetPath ? normalizeRenamePath(targetPath) : void 0;
40924
+ const wantBase = want ? want.split("/").pop() ?? want : void 0;
40925
+ let currentHash = "unknown";
40926
+ let currentDateStr;
40927
+ let basenameFallback = null;
40928
+ const lines = result.trim().split("\n");
40929
+ for (const line of lines) {
40930
+ const headerMatch = line.match(/^([a-f0-9]{7,40})\s+(\d{4}-\d\d-\d\d\b.*)$/);
40931
+ if (headerMatch) {
40932
+ currentHash = headerMatch[1].substring(0, 7);
40933
+ currentDateStr = headerMatch[2];
40934
+ continue;
40935
+ }
40936
+ if (line.startsWith("R")) {
40937
+ const parts = line.split(" ");
40938
+ if (parts.length >= 3) {
40939
+ const daysAgo = currentDateStr ? Math.floor((Date.now() - new Date(currentDateStr).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
40940
+ const info2 = {
40941
+ oldPath: parts[1],
40942
+ newPath: parts[2],
40943
+ commitHash: currentHash,
40944
+ daysAgo
40945
+ };
40946
+ if (!want) return info2;
40947
+ const oldNorm = normalizeRenamePath(info2.oldPath);
40948
+ if (oldNorm === want) return info2;
40949
+ if (!basenameFallback && wantBase && (oldNorm.split("/").pop() ?? oldNorm) === wantBase) {
40950
+ basenameFallback = info2;
40951
+ }
40952
+ }
40953
+ }
40954
+ }
40955
+ return basenameFallback;
40956
+ }
40908
40957
  async function findRenames(projectRoot, filePath) {
40909
40958
  try {
40910
40959
  const git = getGit(projectRoot);
@@ -40914,35 +40963,9 @@ async function findRenames(projectRoot, filePath) {
40914
40963
  "--find-renames",
40915
40964
  "--name-status",
40916
40965
  "--format=%H %ai",
40917
- "-10",
40918
- "--",
40919
- filePath
40966
+ "-50"
40920
40967
  ]);
40921
- if (!result.trim()) return null;
40922
- let currentHash = "unknown";
40923
- let currentDateStr;
40924
- const lines = result.trim().split("\n");
40925
- for (const line of lines) {
40926
- const headerMatch = line.match(/^([a-f0-9]{7,40})\s+(.+)$/);
40927
- if (headerMatch) {
40928
- currentHash = headerMatch[1].substring(0, 7);
40929
- currentDateStr = headerMatch[2];
40930
- continue;
40931
- }
40932
- if (line.startsWith("R")) {
40933
- const parts = line.split(" ");
40934
- if (parts.length >= 3) {
40935
- const daysAgo = currentDateStr ? Math.floor((Date.now() - new Date(currentDateStr).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
40936
- return {
40937
- oldPath: parts[1],
40938
- newPath: parts[2],
40939
- commitHash: currentHash,
40940
- daysAgo
40941
- };
40942
- }
40943
- }
40944
- }
40945
- return null;
40968
+ return parseRenameLog(result, filePath);
40946
40969
  } catch {
40947
40970
  return null;
40948
40971
  }
@@ -42590,10 +42613,26 @@ function extractNpxPackage(cmd) {
42590
42613
  }
42591
42614
  return null;
42592
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
+ }
42593
42619
  async function checkCommands(file2, projectRoot) {
42594
42620
  const issues = [];
42595
42621
  const pkgJson = loadPackageJson(projectRoot);
42596
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
+ }
42597
42636
  for (const ref of file2.references.commands) {
42598
42637
  const cmd = ref.value;
42599
42638
  const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
@@ -42612,9 +42651,7 @@ async function checkCommands(file2, projectRoot) {
42612
42651
  }
42613
42652
  continue;
42614
42653
  }
42615
- const shorthandMatch = cmd.match(
42616
- /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/
42617
- );
42654
+ const shorthandMatch = cmd.match(PKG_SHORTHAND_PATTERN);
42618
42655
  if (shorthandMatch && pkgJson) {
42619
42656
  const scriptName = shorthandMatch[2];
42620
42657
  if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
@@ -42676,7 +42713,7 @@ async function checkCommands(file2, projectRoot) {
42676
42713
  }
42677
42714
  continue;
42678
42715
  }
42679
- const toolMatch = cmd.match(/^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/);
42716
+ const toolMatch = cmd.match(PKG_DEPENDENT_TOOL_PATTERN);
42680
42717
  if (toolMatch && pkgJson) {
42681
42718
  const tool = toolMatch[1];
42682
42719
  const allDeps = {
@@ -42714,7 +42751,7 @@ function hasMakeTarget(makefile, target) {
42714
42751
  const pattern = new RegExp(`^${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*:`, "m");
42715
42752
  return pattern.test(makefile);
42716
42753
  }
42717
- var NPM_SCRIPT_PATTERN, MAKE_PATTERN;
42754
+ var NPM_SCRIPT_PATTERN, MAKE_PATTERN, PKG_DEPENDENT_TOOL_PATTERN, PKG_SHORTHAND_PATTERN;
42718
42755
  var init_commands = __esm({
42719
42756
  "src/core/checks/commands.ts"() {
42720
42757
  "use strict";
@@ -42722,6 +42759,8 @@ var init_commands = __esm({
42722
42759
  init_fs();
42723
42760
  NPM_SCRIPT_PATTERN = /^(?:npm\s+run|pnpm(?:\s+run)?|yarn(?:\s+run)?|bun(?:\s+run)?)\s+(\S+)/;
42724
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/;
42725
42764
  }
42726
42765
  });
42727
42766
 
@@ -42912,6 +42951,7 @@ function computeSectionCosts(file2) {
42912
42951
  }).sort((a, b2) => b2.tokens - a.tokens);
42913
42952
  }
42914
42953
  function loadSettingsSources(projectRoot) {
42954
+ if (settingsCache?.root === projectRoot) return settingsCache.data;
42915
42955
  const sources = [];
42916
42956
  const candidates = [
42917
42957
  path7.join(projectRoot, ".claude", "settings.json"),
@@ -42931,6 +42971,7 @@ function loadSettingsSources(projectRoot) {
42931
42971
  console.warn(`ctxlint: could not parse ${p2}: ${err.message}`);
42932
42972
  }
42933
42973
  }
42974
+ settingsCache = { root: projectRoot, data: sources };
42934
42975
  return sources;
42935
42976
  }
42936
42977
  function canonicalizeCommand(backticked) {
@@ -43025,7 +43066,7 @@ function checkAggregateTierTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS)
43025
43066
  suggestion: "Consider moving the largest files or their heaviest sections to on-demand tiers (skills, subagents, memory)."
43026
43067
  };
43027
43068
  }
43028
- var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND;
43069
+ var ALWAYS_LOADED_NAMES, TOP_SECTIONS_TO_REPORT, INVIOLABLE_WITH_COMMAND, settingsCache;
43029
43070
  var init_tier_tokens = __esm({
43030
43071
  "src/core/checks/tier-tokens.ts"() {
43031
43072
  "use strict";
@@ -43055,6 +43096,7 @@ var init_tier_tokens = __esm({
43055
43096
  ];
43056
43097
  TOP_SECTIONS_TO_REPORT = 3;
43057
43098
  INVIOLABLE_WITH_COMMAND = /\b(NEVER|ALWAYS|DON'?T|DO NOT|MUST NOT)\b[^.!?`]{0,80}`([^`]+)`/i;
43099
+ settingsCache = null;
43058
43100
  }
43059
43101
  });
43060
43102
 
@@ -43151,7 +43193,8 @@ async function checkRedundancy(file2, projectRoot) {
43151
43193
  ruleId: "redundancy/tech-mention",
43152
43194
  line: i2 + 1,
43153
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`,
43154
- suggestion: `~${wastedTokens} tokens could be saved`
43196
+ suggestion: `~${wastedTokens} tokens could be saved`,
43197
+ wastedTokens
43155
43198
  });
43156
43199
  }
43157
43200
  }
@@ -43165,7 +43208,8 @@ async function checkRedundancy(file2, projectRoot) {
43165
43208
  );
43166
43209
  if (dirMatch) {
43167
43210
  const dir = dirMatch[1].replace(/[`"]/g, "");
43168
- 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);
43169
43213
  if (isDirectory(fullPath)) {
43170
43214
  issues.push({
43171
43215
  severity: "info",
@@ -43397,19 +43441,23 @@ var init_contradictions = __esm({
43397
43441
  options: [
43398
43442
  {
43399
43443
  label: "Jest",
43400
- 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]
43401
43449
  },
43402
43450
  {
43403
43451
  label: "Vitest",
43404
- 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]
43405
43453
  },
43406
43454
  {
43407
43455
  label: "Mocha",
43408
- 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]
43409
43457
  },
43410
43458
  {
43411
43459
  label: "pytest",
43412
- 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]
43413
43461
  },
43414
43462
  {
43415
43463
  label: "Playwright",
@@ -43464,19 +43512,11 @@ var init_contradictions = __esm({
43464
43512
  },
43465
43513
  {
43466
43514
  label: "2 spaces",
43467
- patterns: [
43468
- /\b2[\s-]?space\s+indent/i,
43469
- /\bindent\s+with\s+2\s+spaces/i,
43470
- /\b2[\s-]?space\s+tabs?\b/i
43471
- ]
43515
+ patterns: [/\b2[\s-]?space\s+indent/i, /\bindent\s+with\s+2\s+spaces/i]
43472
43516
  },
43473
43517
  {
43474
43518
  label: "4 spaces",
43475
- patterns: [
43476
- /\b4[\s-]?space\s+indent/i,
43477
- /\bindent\s+with\s+4\s+spaces/i,
43478
- /\b4[\s-]?space\s+tabs?\b/i
43479
- ]
43519
+ patterns: [/\b4[\s-]?space\s+indent/i, /\bindent\s+with\s+4\s+spaces/i]
43480
43520
  }
43481
43521
  ]
43482
43522
  },
@@ -43638,10 +43678,11 @@ function hasUnbalancedBracketsOrQuotes(val) {
43638
43678
  if (square < 0 || curly < 0) return true;
43639
43679
  }
43640
43680
  if (square !== 0 || curly !== 0) return true;
43641
- const doubleQuotes = (val.match(/"/g) || []).length;
43642
- const singleQuotes = (val.match(/'/g) || []).length;
43643
- if (doubleQuotes % 2 !== 0) return true;
43644
- 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
+ }
43645
43686
  return false;
43646
43687
  }
43647
43688
  async function checkFrontmatter(file2, _projectRoot) {
@@ -44851,13 +44892,13 @@ var init_gitignore = __esm({
44851
44892
  // src/core/session-parser.ts
44852
44893
  import { readFile } from "node:fs/promises";
44853
44894
  import { readFileSync as readFileSync4 } from "node:fs";
44854
- import { dirname as dirname2, resolve as resolve4 } from "node:path";
44895
+ import { dirname as dirname3, resolve as resolve4 } from "node:path";
44855
44896
  import { fileURLToPath } from "node:url";
44856
44897
  function loadWebFirstSegments() {
44857
44898
  if (typeof define_WEB_FIRST_SEGMENTS_default !== "undefined") {
44858
44899
  return new Set(define_WEB_FIRST_SEGMENTS_default);
44859
44900
  }
44860
- const __dir = dirname2(fileURLToPath(import.meta.url));
44901
+ const __dir = dirname3(fileURLToPath(import.meta.url));
44861
44902
  const fixturePath = resolve4(__dir, "../../tests/fixtures/web-first-segments.json");
44862
44903
  const raw = JSON.parse(readFileSync4(fixturePath, "utf-8"));
44863
44904
  const list = Array.isArray(raw) ? raw : raw.segments;
@@ -44968,7 +45009,7 @@ var init_session_parser = __esm({
44968
45009
  import { readdir, stat } from "node:fs/promises";
44969
45010
  import { createReadStream } from "node:fs";
44970
45011
  import { createInterface } from "node:readline";
44971
- 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";
44972
45013
  import { existsSync } from "node:fs";
44973
45014
  function detectProviders() {
44974
45015
  if (!home) return [];
@@ -45063,7 +45104,7 @@ function detectAiderInSiblings(siblings) {
45063
45104
  return null;
45064
45105
  }
45065
45106
  async function detectSiblings(projectRoot) {
45066
- const parentDir = dirname3(resolve5(projectRoot));
45107
+ const parentDir = dirname4(resolve5(projectRoot));
45067
45108
  let entries;
45068
45109
  try {
45069
45110
  entries = await readdir(parentDir);
@@ -45180,11 +45221,12 @@ async function checkMissingSecret(ctx) {
45180
45221
  const issues = [];
45181
45222
  const secrets = [];
45182
45223
  for (const entry of ctx.history) {
45183
- const match = entry.display.match(SECRET_SET_PATTERN) || entry.display.match(SECRET_SET_SIMPLE);
45224
+ const match = entry.display.match(SECRET_SET_SIMPLE);
45184
45225
  if (!match) continue;
45226
+ const repoMatch = entry.display.match(REPO_FLAG_PATTERN);
45185
45227
  secrets.push({
45186
45228
  name: match[1],
45187
- repo: match[2],
45229
+ repo: repoMatch ? repoMatch[1] : void 0,
45188
45230
  project: entry.project
45189
45231
  });
45190
45232
  }
@@ -45229,19 +45271,19 @@ async function checkMissingSecret(ctx) {
45229
45271
  }
45230
45272
  return issues;
45231
45273
  }
45232
- var SECRET_SET_PATTERN, SECRET_SET_SIMPLE;
45274
+ var SECRET_SET_SIMPLE, REPO_FLAG_PATTERN;
45233
45275
  var init_missing_secret = __esm({
45234
45276
  "src/core/checks/session/missing-secret.ts"() {
45235
45277
  "use strict";
45236
45278
  init_define_WEB_FIRST_SEGMENTS();
45237
- SECRET_SET_PATTERN = /gh\s+secret\s+set\s+(\S+)\s+--repo\s+(\S+)/;
45238
45279
  SECRET_SET_SIMPLE = /gh\s+secret\s+set\s+(\S+)/;
45280
+ REPO_FLAG_PATTERN = /--repo\s+(\S+)/;
45239
45281
  }
45240
45282
  });
45241
45283
 
45242
45284
  // src/core/checks/session/diverged-file.ts
45243
45285
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
45244
- import { join as join6 } from "node:path";
45286
+ import { join as join6, resolve as resolve7 } from "node:path";
45245
45287
  import { existsSync as existsSync2 } from "node:fs";
45246
45288
  async function loadLineSet(absPath) {
45247
45289
  let mtimeMs;
@@ -45290,11 +45332,16 @@ async function checkDivergedFile(ctx) {
45290
45332
  bothEmptyIsIdentical: true
45291
45333
  });
45292
45334
  if (overlap >= 0.2 && overlap < 0.9) {
45293
- 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
+ });
45294
45340
  }
45295
45341
  }
45296
45342
  if (diverged.length > 0) {
45297
45343
  const details = diverged.map((d) => `${d.sibling} (${d.overlap}% overlap)`).join(", ");
45344
+ const sibPaths = diverged.map((d) => ` ${d.sibling}: ${d.path}`).join("\n");
45298
45345
  issues.push({
45299
45346
  severity: "warning",
45300
45347
  check: "session-diverged-file",
@@ -45302,7 +45349,9 @@ async function checkDivergedFile(ctx) {
45302
45349
  line: 0,
45303
45350
  message: `${fileName} has diverged from sibling repos: ${details}`,
45304
45351
  suggestion: `Compare with sibling versions to identify unintentional drift`,
45305
- 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}`
45306
45355
  });
45307
45356
  }
45308
45357
  }
@@ -45389,15 +45438,15 @@ var init_missing_workflow = __esm({
45389
45438
 
45390
45439
  // src/core/checks/session/stale-memory.ts
45391
45440
  import { existsSync as existsSync4 } from "node:fs";
45392
- import { resolve as resolve7, isAbsolute as isAbsolute2 } from "node:path";
45441
+ import { resolve as resolve8, isAbsolute as isAbsolute2 } from "node:path";
45393
45442
  function resolveRef2(ref, projectRoot) {
45394
45443
  if (ref.startsWith("~/") || ref === "~") {
45395
45444
  const home2 = process.env.HOME || process.env.USERPROFILE;
45396
45445
  if (!home2) return null;
45397
- return ref === "~" ? home2 : resolve7(home2, ref.slice(2));
45446
+ return ref === "~" ? home2 : resolve8(home2, ref.slice(2));
45398
45447
  }
45399
45448
  if (isAbsolute2(ref)) return ref;
45400
- return resolve7(projectRoot, ref);
45449
+ return resolve8(projectRoot, ref);
45401
45450
  }
45402
45451
  async function checkStaleMemory(ctx) {
45403
45452
  const issues = [];
@@ -45440,7 +45489,15 @@ var init_stale_memory = __esm({
45440
45489
  async function checkDuplicateMemory(ctx) {
45441
45490
  const issues = [];
45442
45491
  const reported = /* @__PURE__ */ new Set();
45443
- 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
+ };
45444
45501
  for (let i2 = 0; i2 < ctx.memories.length; i2++) {
45445
45502
  for (let j3 = i2 + 1; j3 < ctx.memories.length; j3++) {
45446
45503
  const a = ctx.memories[i2];
@@ -45450,7 +45507,7 @@ async function checkDuplicateMemory(ctx) {
45450
45507
  const bIsCurrent = projectDirMatchesPath(b2.projectDir, ctx.currentProject);
45451
45508
  if (!aIsCurrent && !bIsCurrent) continue;
45452
45509
  if (a.content.length < 50 || b2.content.length < 50) continue;
45453
- const overlap = jaccardSimilarityFromSets(lineSets[i2], lineSets[j3]);
45510
+ const overlap = jaccardSimilarityFromSets(getLineSet(i2), getLineSet(j3));
45454
45511
  if (overlap < 0.6) continue;
45455
45512
  const pairKey = [a.filePath, b2.filePath].sort().join("::");
45456
45513
  if (reported.has(pairKey)) continue;
@@ -45484,9 +45541,9 @@ var init_duplicate_memory = __esm({
45484
45541
  });
45485
45542
 
45486
45543
  // src/core/checks/session/loop-detection.ts
45487
- import { resolve as resolve8 } from "node:path";
45544
+ import { resolve as resolve9 } from "node:path";
45488
45545
  function normalizeProject(p2) {
45489
- return resolve8(p2).replace(/\\/g, "/");
45546
+ return resolve9(p2).replace(/\\/g, "/");
45490
45547
  }
45491
45548
  function findConsecutiveRepeats(displays) {
45492
45549
  const results = [];
@@ -45496,13 +45553,13 @@ function findConsecutiveRepeats(displays) {
45496
45553
  while (j3 < displays.length && displays[j3] === displays[i2]) j3++;
45497
45554
  const count = j3 - i2;
45498
45555
  if (count >= CONSECUTIVE_THRESHOLD) {
45499
- results.push({ command: displays[i2], count });
45556
+ results.push({ command: displays[i2], count, startIdx: i2, endIdx: j3 });
45500
45557
  }
45501
45558
  i2 = j3;
45502
45559
  }
45503
45560
  return results;
45504
45561
  }
45505
- function findCyclicPatterns(displays) {
45562
+ function findCyclicPatterns(displays, consecutiveSpans) {
45506
45563
  const results = [];
45507
45564
  for (let cycleLen = 2; cycleLen <= MAX_CYCLE_LENGTH; cycleLen++) {
45508
45565
  for (let start = 0; start <= displays.length - cycleLen * CYCLE_REPEAT_THRESHOLD; start++) {
@@ -45520,8 +45577,13 @@ function findCyclicPatterns(displays) {
45520
45577
  }
45521
45578
  }
45522
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;
45523
45585
  const alreadyCovered = results.some(
45524
- (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
45525
45587
  );
45526
45588
  if (!alreadyCovered) {
45527
45589
  results.push({ cycle, repeats, startIdx: start });
@@ -45550,7 +45612,8 @@ async function checkLoopDetection(ctx) {
45550
45612
  suggestion: "An agent may be looping on this command. Check history.jsonl for context on what went wrong"
45551
45613
  });
45552
45614
  }
45553
- const cycles = findCyclicPatterns(displays);
45615
+ const consecutiveSpans = repeats.map((r2) => ({ startIdx: r2.startIdx, endIdx: r2.endIdx }));
45616
+ const cycles = findCyclicPatterns(displays, consecutiveSpans);
45554
45617
  for (const { cycle, repeats: reps } of cycles) {
45555
45618
  const cycleStr = cycle.map((c3) => c3.length > 40 ? c3.slice(0, 37) + "..." : c3).join(" -> ");
45556
45619
  issues.push({
@@ -45655,9 +45718,12 @@ async function findReleaseWorkflows(projectRoot) {
45655
45718
  }
45656
45719
  try {
45657
45720
  const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
45658
- const nameMatch = content.match(/^name:\s*(.+)$/m);
45659
- if (nameMatch && RELEASE_FILENAME_PATTERNS.some((p2) => p2.test(nameMatch[1]))) {
45660
- 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
+ }
45661
45727
  }
45662
45728
  } catch {
45663
45729
  }
@@ -46511,6 +46577,7 @@ function extractPathsFromMessage(msg) {
46511
46577
  function applyIgnoreRules(issues, rules) {
46512
46578
  const compiled = compileRules(rules);
46513
46579
  const kept = [];
46580
+ const keepMask = [];
46514
46581
  let dropped = 0;
46515
46582
  for (const issue2 of issues) {
46516
46583
  let suppress = false;
@@ -46530,12 +46597,15 @@ function applyIgnoreRules(issues, rules) {
46530
46597
  }
46531
46598
  if (suppress) {
46532
46599
  dropped++;
46600
+ keepMask.push(false);
46533
46601
  } else {
46534
46602
  kept.push(issue2);
46603
+ keepMask.push(true);
46535
46604
  }
46536
46605
  }
46537
46606
  return {
46538
46607
  kept,
46608
+ keepMask,
46539
46609
  dropped,
46540
46610
  unusedRules: compiled.filter((r2) => !r2.fired).map(stripCompiled),
46541
46611
  rulesMissingReason: rules.filter((r2) => !r2.reason)
@@ -46570,12 +46640,12 @@ var init_types3 = __esm({
46570
46640
 
46571
46641
  // src/version.ts
46572
46642
  import { readFileSync as readFileSync7 } from "node:fs";
46573
- import { resolve as resolve11, dirname as dirname5 } from "node:path";
46643
+ import { resolve as resolve12, dirname as dirname6 } from "node:path";
46574
46644
  import { fileURLToPath as fileURLToPath2 } from "node:url";
46575
46645
  function loadVersion() {
46576
- if (true) return "0.12.3";
46577
- const __dir = dirname5(fileURLToPath2(import.meta.url));
46578
- 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");
46579
46649
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
46580
46650
  return pkg.version;
46581
46651
  }
@@ -46883,10 +46953,9 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
46883
46953
  }
46884
46954
  });
46885
46955
  const applied = applyIgnoreRules(flat, options.ignoreRules);
46886
- const keptSet = new Set(applied.kept);
46887
46956
  const rebuilt = fileResults.map(() => []);
46888
46957
  flat.forEach((issue2, i2) => {
46889
- if (keptSet.has(issue2)) rebuilt[owners[i2]].push(issue2);
46958
+ if (applied.keepMask[i2]) rebuilt[owners[i2]].push(issue2);
46890
46959
  });
46891
46960
  fileResults.forEach((fr, idx) => {
46892
46961
  fr.issues = rebuilt[idx];
@@ -46900,7 +46969,10 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
46900
46969
  let estimatedWaste = 0;
46901
46970
  for (const fr of fileResults) {
46902
46971
  for (const issue2 of fr.issues) {
46903
- 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) {
46904
46976
  const tokenMatch = issue2.suggestion.match(/~(\d+)\s+tokens/);
46905
46977
  if (tokenMatch) estimatedWaste += parseInt(tokenMatch[1], 10);
46906
46978
  }
@@ -54788,6 +54860,11 @@ function formatText(result, verbose = false) {
54788
54860
  lines.push("Session audit scanned");
54789
54861
  renderedAnySummary = true;
54790
54862
  }
54863
+ if (groups.skill.length > 0) {
54864
+ if (renderedAnySummary) lines.push("");
54865
+ lines.push("Skill audit scanned");
54866
+ renderedAnySummary = true;
54867
+ }
54791
54868
  if (!renderedAnySummary) {
54792
54869
  lines.push(`Found ${result.files.length} file${result.files.length !== 1 ? "s" : ""}`);
54793
54870
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/ctxlint",
3
- "version": "0.13.0",
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": {