@yawlabs/ctxlint 0.18.6 → 0.19.0

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
@@ -13770,10 +13770,16 @@ function extractPathReferences(lines, sections) {
13770
13770
  if (inCodeBlock && codeBlockLang === "" && /^\s/.test(line) && /^[\w@.-]+\/$/.test(cleanValue)) {
13771
13771
  continue;
13772
13772
  }
13773
- const lowerProse = /^([a-z][\w-]*)\/[a-z][\w-]*$/.exec(cleanValue);
13774
- if (lowerProse && !PATH_FIRST_SEGMENTS.has(lowerProse[1])) {
13773
+ const twoSegmentProse = /^([A-Za-z][\w-]*)\/[A-Za-z][\w-]*$/.exec(cleanValue);
13774
+ if (twoSegmentProse && !PATH_FIRST_SEGMENTS.has(twoSegmentProse[1].toLowerCase())) {
13775
13775
  continue;
13776
13776
  }
13777
+ if (/^[a-z][\w-]*(?:\/[a-z][\w-]*){2,}$/.test(cleanValue)) continue;
13778
+ if (/([A-Z])\1{2,}/.test(cleanValue)) continue;
13779
+ if (cleanValue.includes("*")) {
13780
+ const anchored = /^\.{0,2}\//.test(cleanValue) || cleanValue.includes("**") || PATH_FIRST_SEGMENTS.has(cleanValue.split("/")[0]);
13781
+ if (!anchored) continue;
13782
+ }
13777
13783
  if (/(^|\/)\.git\//.test(cleanValue) || /\.app\//.test(cleanValue)) continue;
13778
13784
  const column = match.index + match[0].length - match[1].length + 1;
13779
13785
  paths.push({
@@ -19960,8 +19966,8 @@ async function findRenames(projectRoot, filePath) {
19960
19966
  async function findRenamesBatch(projectRoot, relPaths) {
19961
19967
  const result = /* @__PURE__ */ new Map();
19962
19968
  if (relPaths.length === 0) return result;
19963
- let cache = renameCache.get(projectRoot);
19964
- if (!cache) {
19969
+ let cache2 = renameCache.get(projectRoot);
19970
+ if (!cache2) {
19965
19971
  try {
19966
19972
  const git = getGit(projectRoot);
19967
19973
  const rawOutput = await git.raw([
@@ -19979,14 +19985,14 @@ async function findRenamesBatch(projectRoot, relPaths) {
19979
19985
  } catch {
19980
19986
  }
19981
19987
  const { allRenames: allRenames2, basenameBuckets: basenameBuckets2 } = parseRenameLogAll(rawOutput);
19982
- cache = { allRenames: allRenames2, basenameBuckets: basenameBuckets2, prefix: prefix2 };
19983
- renameCache.set(projectRoot, cache);
19988
+ cache2 = { allRenames: allRenames2, basenameBuckets: basenameBuckets2, prefix: prefix2 };
19989
+ renameCache.set(projectRoot, cache2);
19984
19990
  } catch {
19985
19991
  for (const p2 of relPaths) result.set(p2, null);
19986
19992
  return result;
19987
19993
  }
19988
19994
  }
19989
- const { allRenames, basenameBuckets, prefix } = cache;
19995
+ const { allRenames, basenameBuckets, prefix } = cache2;
19990
19996
  for (const relPath of relPaths) {
19991
19997
  const rel = relPath.replace(/\\/g, "/");
19992
19998
  const target = prefix + rel;
@@ -20505,6 +20511,7 @@ async function checkPaths(file2, projectRoot) {
20505
20511
  const projectFiles = getProjectFiles(projectRoot);
20506
20512
  const basenameIndex = getBasenameIndex(projectRoot, projectFiles);
20507
20513
  const contextDir = path4.dirname(file2.filePath);
20514
+ const inRootDotConfigDir = path4.dirname(contextDir) === projectRoot && path4.basename(contextDir).startsWith(".");
20508
20515
  const pending = [];
20509
20516
  for (const ref of file2.references.paths) {
20510
20517
  const rawNormalized = ref.value.replace(/\\/g, "/");
@@ -20515,7 +20522,7 @@ async function checkPaths(file2, projectRoot) {
20515
20522
  continue;
20516
20523
  }
20517
20524
  const explicitRel = /^\.\.?\//.test(normalizedRef);
20518
- const baseDirs = explicitRel || isImport ? [contextDir] : [projectRoot, contextDir];
20525
+ const baseDirs = isImport ? [contextDir] : explicitRel ? inRootDotConfigDir ? [contextDir, projectRoot] : [contextDir] : [projectRoot, contextDir];
20519
20526
  const primaryBase = baseDirs[0];
20520
20527
  if (normalizedRef.includes("*")) {
20521
20528
  let hasMatch = false;
@@ -20676,423 +20683,6 @@ var init_paths = __esm({
20676
20683
  }
20677
20684
  });
20678
20685
 
20679
- // src/core/checks/commands.ts
20680
- import * as fs4 from "node:fs";
20681
- import * as path5 from "node:path";
20682
- function isManagerBuiltin(manager, sub) {
20683
- return PM_MANAGER_BUILTINS[manager]?.has(sub) ?? false;
20684
- }
20685
- function scriptNameFromMatch(match) {
20686
- const [, manager, explicitRun, name] = match;
20687
- if (name.startsWith("-")) return null;
20688
- if (manager && !explicitRun && PM_BUILTIN_SUBCOMMANDS.has(name)) return null;
20689
- if (manager && !explicitRun && isManagerBuiltin(manager, name)) return null;
20690
- return name;
20691
- }
20692
- function extractNpxPackage(cmd) {
20693
- if (!/^npx\b/.test(cmd)) return null;
20694
- const tokens = cmd.split(/\s+/).slice(1);
20695
- for (let i2 = 0; i2 < tokens.length; i2++) {
20696
- const t2 = tokens[i2];
20697
- if (t2 === "-p" || t2 === "--package") {
20698
- const v2 = tokens[i2 + 1];
20699
- if (v2 && !v2.startsWith("-")) return v2;
20700
- continue;
20701
- }
20702
- if (t2.startsWith("-p=") || t2.startsWith("--package=")) {
20703
- return t2.slice(t2.indexOf("=") + 1) || null;
20704
- }
20705
- if (t2.startsWith("-")) continue;
20706
- return t2;
20707
- }
20708
- return null;
20709
- }
20710
- function wouldNeedPackageJson(cmd) {
20711
- const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
20712
- if (scriptMatch && scriptNameFromMatch(scriptMatch) !== null) return true;
20713
- const shorthandMatch = cmd.match(PKG_SHORTHAND_PATTERN);
20714
- if (shorthandMatch && !isManagerBuiltin(shorthandMatch[1], shorthandMatch[2])) return true;
20715
- return /^npx\b/.test(cmd) || PKG_DEPENDENT_TOOL_PATTERN.test(cmd);
20716
- }
20717
- async function checkCommands(file2, projectRoot) {
20718
- const issues = [];
20719
- const pkgJson = loadPackageJson(projectRoot);
20720
- const makefile = loadMakefile(projectRoot);
20721
- if (!pkgJson) {
20722
- const skipped = file2.references.commands.find((ref) => wouldNeedPackageJson(ref.value));
20723
- if (skipped) {
20724
- issues.push({
20725
- severity: "info",
20726
- check: "commands",
20727
- ruleId: "commands/package-json-missing",
20728
- line: skipped.line,
20729
- message: "package.json missing or unparseable \u2014 command checks skipped",
20730
- suggestion: "Add a parseable package.json at the project root so script, npx, and tool references can be validated."
20731
- });
20732
- }
20733
- }
20734
- for (const ref of file2.references.commands) {
20735
- const cmd = ref.value;
20736
- const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
20737
- if (scriptMatch && pkgJson) {
20738
- const scriptName = scriptNameFromMatch(scriptMatch);
20739
- if (scriptName && pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
20740
- const available = Object.keys(pkgJson.scripts).join(", ");
20741
- issues.push({
20742
- severity: "error",
20743
- check: "commands",
20744
- ruleId: "commands/script-not-found",
20745
- line: ref.line,
20746
- message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`,
20747
- suggestion: available ? `Available scripts: ${available}` : void 0
20748
- });
20749
- }
20750
- continue;
20751
- }
20752
- const shorthandMatch = cmd.match(PKG_SHORTHAND_PATTERN);
20753
- if (shorthandMatch && pkgJson) {
20754
- const manager = shorthandMatch[1];
20755
- const scriptName = shorthandMatch[2];
20756
- if (isManagerBuiltin(manager, scriptName)) continue;
20757
- if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
20758
- issues.push({
20759
- severity: "error",
20760
- check: "commands",
20761
- ruleId: "commands/script-not-found",
20762
- line: ref.line,
20763
- message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`
20764
- });
20765
- }
20766
- continue;
20767
- }
20768
- if (/^npx\b/.test(cmd) && pkgJson) {
20769
- const pkgName = extractNpxPackage(cmd);
20770
- if (!pkgName) continue;
20771
- const allDeps = {
20772
- ...pkgJson.dependencies,
20773
- ...pkgJson.devDependencies,
20774
- ...pkgJson.peerDependencies,
20775
- ...pkgJson.optionalDependencies
20776
- };
20777
- if (!(pkgName in allDeps)) {
20778
- const binPath = path5.join(projectRoot, "node_modules", ".bin", pkgName);
20779
- try {
20780
- fs4.accessSync(binPath);
20781
- } catch {
20782
- issues.push({
20783
- severity: "warning",
20784
- check: "commands",
20785
- ruleId: "commands/npx-not-in-deps",
20786
- line: ref.line,
20787
- message: `"${cmd}" \u2014 "${pkgName}" not found in dependencies`,
20788
- suggestion: "If this is a global tool, consider adding it to devDependencies for reproducibility"
20789
- });
20790
- }
20791
- }
20792
- continue;
20793
- }
20794
- if (MAKE_PATTERN.test(cmd)) {
20795
- if (!makefile) {
20796
- issues.push({
20797
- severity: "error",
20798
- check: "commands",
20799
- ruleId: "commands/no-makefile",
20800
- line: ref.line,
20801
- message: `"${cmd}" \u2014 no Makefile found in project`
20802
- });
20803
- } else {
20804
- const target = extractMakeTarget(cmd);
20805
- if (target && !hasMakeTarget(makefile, target)) {
20806
- issues.push({
20807
- severity: "error",
20808
- check: "commands",
20809
- ruleId: "commands/make-target-not-found",
20810
- line: ref.line,
20811
- message: `"${cmd}" \u2014 target "${target}" not found in Makefile`
20812
- });
20813
- }
20814
- }
20815
- continue;
20816
- }
20817
- const toolMatch = cmd.match(PKG_DEPENDENT_TOOL_PATTERN);
20818
- if (toolMatch && pkgJson) {
20819
- const tool = toolMatch[1];
20820
- const pkgName = BIN_TO_PACKAGE[tool] ?? tool;
20821
- const allDeps = {
20822
- ...pkgJson.dependencies,
20823
- ...pkgJson.devDependencies,
20824
- ...pkgJson.peerDependencies,
20825
- ...pkgJson.optionalDependencies
20826
- };
20827
- if (!(pkgName in allDeps)) {
20828
- const binPath = path5.join(projectRoot, "node_modules", ".bin", tool);
20829
- try {
20830
- fs4.accessSync(binPath);
20831
- } catch {
20832
- issues.push({
20833
- severity: "warning",
20834
- check: "commands",
20835
- ruleId: "commands/tool-not-found",
20836
- line: ref.line,
20837
- message: `"${cmd}" \u2014 "${tool}" not found in dependencies or node_modules/.bin`
20838
- });
20839
- }
20840
- }
20841
- }
20842
- }
20843
- return issues;
20844
- }
20845
- function loadMakefile(projectRoot) {
20846
- try {
20847
- return stripBom(fs4.readFileSync(path5.join(projectRoot, "Makefile"), "utf-8"));
20848
- } catch {
20849
- return null;
20850
- }
20851
- }
20852
- function extractMakeTarget(cmd) {
20853
- for (const token of cmd.split(/\s+/).slice(1)) {
20854
- if (token.startsWith("-")) return null;
20855
- if (token.includes("=")) continue;
20856
- return token;
20857
- }
20858
- return null;
20859
- }
20860
- function hasMakeTarget(makefile, target) {
20861
- const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20862
- const pattern = new RegExp(`^${escaped}\\s*:(?!:?=)`, "m");
20863
- return pattern.test(makefile);
20864
- }
20865
- var NPM_SCRIPT_PATTERN, MAKE_PATTERN, PM_BUILTIN_SUBCOMMANDS, PM_MANAGER_BUILTINS, PKG_DEPENDENT_TOOL_PATTERN, BIN_TO_PACKAGE, PKG_SHORTHAND_PATTERN;
20866
- var init_commands = __esm({
20867
- "src/core/checks/commands.ts"() {
20868
- "use strict";
20869
- init_define_WEB_FIRST_SEGMENTS();
20870
- init_fs();
20871
- NPM_SCRIPT_PATTERN = /^(?:npm\s+run|(pnpm|yarn|bun)(?:\s+(run))?)\s+(\S+)/;
20872
- MAKE_PATTERN = /^make\s+\S/;
20873
- PM_BUILTIN_SUBCOMMANDS = /* @__PURE__ */ new Set([
20874
- "add",
20875
- "approve-builds",
20876
- "audit",
20877
- "bin",
20878
- "cache",
20879
- "ci",
20880
- "config",
20881
- "create",
20882
- "dedupe",
20883
- "dlx",
20884
- "doctor",
20885
- "env",
20886
- "exec",
20887
- "fetch",
20888
- "global",
20889
- "help",
20890
- "i",
20891
- "import",
20892
- "info",
20893
- "init",
20894
- "install",
20895
- "licenses",
20896
- "link",
20897
- "list",
20898
- "login",
20899
- "logout",
20900
- "ls",
20901
- "node",
20902
- "npm",
20903
- "outdated",
20904
- "pack",
20905
- "patch",
20906
- "patch-commit",
20907
- "prune",
20908
- "publish",
20909
- "rebuild",
20910
- "remove",
20911
- "rm",
20912
- "root",
20913
- "run",
20914
- "self-update",
20915
- "set",
20916
- "setup",
20917
- "store",
20918
- "team",
20919
- "un",
20920
- "uninstall",
20921
- "unlink",
20922
- "up",
20923
- "update",
20924
- "upgrade",
20925
- "version",
20926
- "whoami",
20927
- "why",
20928
- "workspace",
20929
- "workspaces",
20930
- "x"
20931
- ]);
20932
- PM_MANAGER_BUILTINS = {
20933
- bun: /* @__PURE__ */ new Set(["test"])
20934
- };
20935
- PKG_DEPENDENT_TOOL_PATTERN = /^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/;
20936
- BIN_TO_PACKAGE = {
20937
- tsc: "typescript"
20938
- };
20939
- PKG_SHORTHAND_PATTERN = /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/;
20940
- }
20941
- });
20942
-
20943
- // src/core/checks/staleness.ts
20944
- import * as path6 from "node:path";
20945
- async function checkStaleness(file2, projectRoot) {
20946
- const issues = [];
20947
- if (!await isGitRepo(projectRoot)) {
20948
- return issues;
20949
- }
20950
- const relativePath = path6.relative(projectRoot, file2.filePath).replace(/\\/g, "/");
20951
- const lastModified = await getFileLastModified(projectRoot, relativePath);
20952
- if (!lastModified || isNaN(lastModified.getTime())) {
20953
- return issues;
20954
- }
20955
- const daysSinceUpdate = Math.floor((Date.now() - lastModified.getTime()) / (1e3 * 60 * 60 * 24));
20956
- if (daysSinceUpdate < INFO_DAYS) {
20957
- return issues;
20958
- }
20959
- const referencedPaths = /* @__PURE__ */ new Set();
20960
- const globByPrefix = /* @__PURE__ */ new Map();
20961
- for (const ref of file2.references.paths) {
20962
- const normalized = ref.value.replace(/\\/g, "/");
20963
- if (normalized.includes("*")) {
20964
- const staticSegments = [];
20965
- for (const segment of normalized.split("/")) {
20966
- if (segment === ".") continue;
20967
- if (segment.includes("*")) break;
20968
- staticSegments.push(segment);
20969
- }
20970
- const prefix = staticSegments.join("/");
20971
- if (prefix) {
20972
- const key = `${prefix}/`;
20973
- referencedPaths.add(key);
20974
- if (!globByPrefix.has(key)) globByPrefix.set(key, ref.value);
20975
- }
20976
- continue;
20977
- }
20978
- referencedPaths.add(ref.value);
20979
- }
20980
- if (referencedPaths.size === 0) {
20981
- return issues;
20982
- }
20983
- const counts = await getCommitsSinceBatch(projectRoot, [...referencedPaths], lastModified);
20984
- let totalCommits = 0;
20985
- let mostActiveRef = "";
20986
- let mostActiveCommits = 0;
20987
- for (const [refPath, commits] of counts) {
20988
- totalCommits += commits;
20989
- if (commits > mostActiveCommits) {
20990
- mostActiveCommits = commits;
20991
- mostActiveRef = refPath;
20992
- }
20993
- }
20994
- if (totalCommits === 0) {
20995
- return issues;
20996
- }
20997
- const isStale = daysSinceUpdate >= WARNING_DAYS;
20998
- const severity = isStale ? "warning" : "info";
20999
- const mostActiveLabel = globByPrefix.get(mostActiveRef) ?? mostActiveRef;
21000
- issues.push({
21001
- severity,
21002
- check: "staleness",
21003
- ruleId: isStale ? "staleness/stale" : "staleness/aging",
21004
- line: 1,
21005
- message: `Last updated ${daysSinceUpdate} days ago. ${mostActiveLabel} has ${mostActiveCommits} commits since.`,
21006
- suggestion: "Review and update this context file to reflect recent changes.",
21007
- detail: `${totalCommits} total commits to referenced paths since last update.`
21008
- });
21009
- return issues;
21010
- }
21011
- var WARNING_DAYS, INFO_DAYS;
21012
- var init_staleness = __esm({
21013
- "src/core/checks/staleness.ts"() {
21014
- "use strict";
21015
- init_define_WEB_FIRST_SEGMENTS();
21016
- init_git();
21017
- WARNING_DAYS = 30;
21018
- INFO_DAYS = 14;
21019
- }
21020
- });
21021
-
21022
- // src/core/checks/tokens.ts
21023
- function resolveTokenThresholds(overrides) {
21024
- if (!overrides) return DEFAULT_TOKEN_THRESHOLDS;
21025
- const merged = { ...DEFAULT_TOKEN_THRESHOLDS, ...overrides };
21026
- if (merged.info >= merged.warning || merged.warning >= merged.error) {
21027
- console.error(
21028
- `Warning: token thresholds should satisfy info < warning < error (got ${merged.info}, ${merged.warning}, ${merged.error}) \u2014 using defaults`
21029
- );
21030
- return DEFAULT_TOKEN_THRESHOLDS;
21031
- }
21032
- return merged;
21033
- }
21034
- async function checkTokens(file2, _projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
21035
- const issues = [];
21036
- const tokens = file2.totalTokens;
21037
- if (tokens >= thresholds.error) {
21038
- issues.push({
21039
- severity: "error",
21040
- check: "tokens",
21041
- ruleId: "tokens/excessive",
21042
- line: 1,
21043
- message: `${tokens.toLocaleString()} tokens \u2014 consumes significant context window space`,
21044
- suggestion: "Consider splitting into focused sections or removing redundant content."
21045
- });
21046
- } else if (tokens >= thresholds.warning) {
21047
- issues.push({
21048
- severity: "warning",
21049
- check: "tokens",
21050
- ruleId: "tokens/large",
21051
- line: 1,
21052
- message: `${tokens.toLocaleString()} tokens \u2014 large context file`,
21053
- suggestion: "Consider trimming \u2014 research shows diminishing returns past ~300 lines."
21054
- });
21055
- } else if (tokens >= thresholds.info) {
21056
- issues.push({
21057
- severity: "info",
21058
- check: "tokens",
21059
- ruleId: "tokens/info",
21060
- line: 1,
21061
- message: `Uses ~${tokens.toLocaleString()} tokens per session`
21062
- });
21063
- }
21064
- return issues;
21065
- }
21066
- function checkAggregateTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
21067
- const total = files.reduce((sum, f) => sum + f.tokens, 0);
21068
- if (total >= thresholds.aggregate && files.length > 1) {
21069
- return {
21070
- severity: "warning",
21071
- check: "tokens",
21072
- ruleId: "tokens/aggregate",
21073
- line: 0,
21074
- message: `${files.length} context files consume ${total.toLocaleString()} tokens combined`,
21075
- suggestion: "Consider consolidating or trimming to reduce per-session context cost."
21076
- };
21077
- }
21078
- return null;
21079
- }
21080
- var DEFAULT_TOKEN_THRESHOLDS;
21081
- var init_tokens2 = __esm({
21082
- "src/core/checks/tokens.ts"() {
21083
- "use strict";
21084
- init_define_WEB_FIRST_SEGMENTS();
21085
- DEFAULT_TOKEN_THRESHOLDS = {
21086
- info: 1e3,
21087
- warning: 3e3,
21088
- error: 8e3,
21089
- aggregate: 5e3,
21090
- tierBreakdown: 1e3,
21091
- tierAggregate: 4e3
21092
- };
21093
- }
21094
- });
21095
-
21096
20686
  // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
21097
20687
  function createScanner(text, ignoreTrivia = false) {
21098
20688
  const len = text.length;
@@ -22170,6 +21760,494 @@ var init_main3 = __esm({
22170
21760
  }
22171
21761
  });
22172
21762
 
21763
+ // src/core/checks/commands.ts
21764
+ import * as fs4 from "node:fs";
21765
+ import * as path5 from "node:path";
21766
+ function isManagerBuiltin(manager, sub) {
21767
+ return PM_MANAGER_BUILTINS[manager]?.has(sub) ?? false;
21768
+ }
21769
+ function scriptNameFromMatch(match) {
21770
+ const [, manager, explicitRun, name] = match;
21771
+ if (name.startsWith("-")) return null;
21772
+ if (manager && !explicitRun && PM_BUILTIN_SUBCOMMANDS.has(name)) return null;
21773
+ if (manager && !explicitRun && isManagerBuiltin(manager, name)) return null;
21774
+ return name;
21775
+ }
21776
+ function extractNpxPackage(cmd) {
21777
+ if (!/^npx\b/.test(cmd)) return null;
21778
+ const tokens = cmd.split(/\s+/).slice(1);
21779
+ for (let i2 = 0; i2 < tokens.length; i2++) {
21780
+ const t2 = tokens[i2];
21781
+ if (t2 === "-p" || t2 === "--package") {
21782
+ const v2 = tokens[i2 + 1];
21783
+ if (v2 && !v2.startsWith("-")) return v2;
21784
+ continue;
21785
+ }
21786
+ if (t2.startsWith("-p=") || t2.startsWith("--package=")) {
21787
+ return t2.slice(t2.indexOf("=") + 1) || null;
21788
+ }
21789
+ if (t2.startsWith("-")) continue;
21790
+ return t2;
21791
+ }
21792
+ return null;
21793
+ }
21794
+ function wouldNeedPackageJson(cmd) {
21795
+ const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
21796
+ if (scriptMatch && scriptNameFromMatch(scriptMatch) !== null) return true;
21797
+ const shorthandMatch = cmd.match(PKG_SHORTHAND_PATTERN);
21798
+ if (shorthandMatch && !isManagerBuiltin(shorthandMatch[1], shorthandMatch[2])) return true;
21799
+ return /^npx\b/.test(cmd) || PKG_DEPENDENT_TOOL_PATTERN.test(cmd);
21800
+ }
21801
+ function loadDeniedCommandPrefixes(projectRoot) {
21802
+ const prefixes = [];
21803
+ for (const rel of ["settings.json", "settings.local.json"]) {
21804
+ let content;
21805
+ try {
21806
+ content = stripBom(fs4.readFileSync(path5.join(projectRoot, ".claude", rel), "utf-8"));
21807
+ } catch {
21808
+ continue;
21809
+ }
21810
+ const data = parse2(content, [], { allowTrailingComma: true });
21811
+ const deny = data?.permissions?.deny;
21812
+ if (!Array.isArray(deny)) continue;
21813
+ for (const entry of deny) {
21814
+ if (typeof entry !== "string") continue;
21815
+ const inner = entry.replace(/^[A-Za-z]+\((.*)\)$/, "$1");
21816
+ const prefix = inner.replace(/:?\*+$/, "").trim();
21817
+ if (prefix) prefixes.push(prefix);
21818
+ }
21819
+ }
21820
+ return prefixes;
21821
+ }
21822
+ function isDeniedCommand(cmd, deniedPrefixes) {
21823
+ return deniedPrefixes.some((p2) => cmd === p2 || cmd.startsWith(`${p2} `));
21824
+ }
21825
+ async function checkCommands(file2, projectRoot) {
21826
+ const issues = [];
21827
+ const pkgJson = loadPackageJson(projectRoot);
21828
+ const makefile = loadMakefile(projectRoot);
21829
+ const deniedPrefixes = loadDeniedCommandPrefixes(projectRoot);
21830
+ if (!pkgJson) {
21831
+ const skipped = file2.references.commands.find((ref) => wouldNeedPackageJson(ref.value));
21832
+ if (skipped) {
21833
+ issues.push({
21834
+ severity: "info",
21835
+ check: "commands",
21836
+ ruleId: "commands/package-json-missing",
21837
+ line: skipped.line,
21838
+ message: "package.json missing or unparseable \u2014 command checks skipped",
21839
+ suggestion: "Add a parseable package.json at the project root so script, npx, and tool references can be validated."
21840
+ });
21841
+ }
21842
+ }
21843
+ for (const ref of file2.references.commands) {
21844
+ const cmd = ref.value;
21845
+ const scriptMatch = cmd.match(NPM_SCRIPT_PATTERN);
21846
+ if (scriptMatch && pkgJson) {
21847
+ const scriptName = scriptNameFromMatch(scriptMatch);
21848
+ if (scriptName && pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
21849
+ const available = Object.keys(pkgJson.scripts).join(", ");
21850
+ issues.push({
21851
+ severity: "error",
21852
+ check: "commands",
21853
+ ruleId: "commands/script-not-found",
21854
+ line: ref.line,
21855
+ message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`,
21856
+ suggestion: available ? `Available scripts: ${available}` : void 0
21857
+ });
21858
+ }
21859
+ continue;
21860
+ }
21861
+ const shorthandMatch = cmd.match(PKG_SHORTHAND_PATTERN);
21862
+ if (shorthandMatch && pkgJson) {
21863
+ const manager = shorthandMatch[1];
21864
+ const scriptName = shorthandMatch[2];
21865
+ if (isManagerBuiltin(manager, scriptName)) continue;
21866
+ if (pkgJson.scripts && !(scriptName in pkgJson.scripts)) {
21867
+ issues.push({
21868
+ severity: "error",
21869
+ check: "commands",
21870
+ ruleId: "commands/script-not-found",
21871
+ line: ref.line,
21872
+ message: `"${cmd}" \u2014 script "${scriptName}" not found in package.json`
21873
+ });
21874
+ }
21875
+ continue;
21876
+ }
21877
+ if (/^npx\b/.test(cmd) && pkgJson) {
21878
+ const pkgName = extractNpxPackage(cmd);
21879
+ if (!pkgName) continue;
21880
+ const allDeps = {
21881
+ ...pkgJson.dependencies,
21882
+ ...pkgJson.devDependencies,
21883
+ ...pkgJson.peerDependencies,
21884
+ ...pkgJson.optionalDependencies
21885
+ };
21886
+ if (!(pkgName in allDeps)) {
21887
+ if (isDeniedCommand(cmd, deniedPrefixes)) continue;
21888
+ const binPath = path5.join(projectRoot, "node_modules", ".bin", pkgName);
21889
+ try {
21890
+ fs4.accessSync(binPath);
21891
+ } catch {
21892
+ issues.push({
21893
+ severity: "warning",
21894
+ check: "commands",
21895
+ ruleId: "commands/npx-not-in-deps",
21896
+ line: ref.line,
21897
+ message: `"${cmd}" \u2014 "${pkgName}" not found in dependencies`,
21898
+ suggestion: "If this is a global tool, consider adding it to devDependencies for reproducibility"
21899
+ });
21900
+ }
21901
+ }
21902
+ continue;
21903
+ }
21904
+ if (MAKE_PATTERN.test(cmd)) {
21905
+ if (!makefile) {
21906
+ issues.push({
21907
+ severity: "error",
21908
+ check: "commands",
21909
+ ruleId: "commands/no-makefile",
21910
+ line: ref.line,
21911
+ message: `"${cmd}" \u2014 no Makefile found in project`
21912
+ });
21913
+ } else {
21914
+ const target = extractMakeTarget(cmd);
21915
+ if (target && !hasMakeTarget(makefile, target)) {
21916
+ issues.push({
21917
+ severity: "error",
21918
+ check: "commands",
21919
+ ruleId: "commands/make-target-not-found",
21920
+ line: ref.line,
21921
+ message: `"${cmd}" \u2014 target "${target}" not found in Makefile`
21922
+ });
21923
+ }
21924
+ }
21925
+ continue;
21926
+ }
21927
+ const toolMatch = cmd.match(PKG_DEPENDENT_TOOL_PATTERN);
21928
+ if (toolMatch && pkgJson) {
21929
+ const tool = toolMatch[1];
21930
+ const pkgName = BIN_TO_PACKAGE[tool] ?? tool;
21931
+ const allDeps = {
21932
+ ...pkgJson.dependencies,
21933
+ ...pkgJson.devDependencies,
21934
+ ...pkgJson.peerDependencies,
21935
+ ...pkgJson.optionalDependencies
21936
+ };
21937
+ if (!(pkgName in allDeps)) {
21938
+ const binPath = path5.join(projectRoot, "node_modules", ".bin", tool);
21939
+ try {
21940
+ fs4.accessSync(binPath);
21941
+ } catch {
21942
+ issues.push({
21943
+ severity: "warning",
21944
+ check: "commands",
21945
+ ruleId: "commands/tool-not-found",
21946
+ line: ref.line,
21947
+ message: `"${cmd}" \u2014 "${tool}" not found in dependencies or node_modules/.bin`
21948
+ });
21949
+ }
21950
+ }
21951
+ }
21952
+ }
21953
+ return issues;
21954
+ }
21955
+ function loadMakefile(projectRoot) {
21956
+ try {
21957
+ return stripBom(fs4.readFileSync(path5.join(projectRoot, "Makefile"), "utf-8"));
21958
+ } catch {
21959
+ return null;
21960
+ }
21961
+ }
21962
+ function extractMakeTarget(cmd) {
21963
+ for (const token of cmd.split(/\s+/).slice(1)) {
21964
+ if (token.startsWith("-")) return null;
21965
+ if (token.includes("=")) continue;
21966
+ return token;
21967
+ }
21968
+ return null;
21969
+ }
21970
+ function hasMakeTarget(makefile, target) {
21971
+ const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21972
+ const pattern = new RegExp(`^${escaped}\\s*:(?!:?=)`, "m");
21973
+ return pattern.test(makefile);
21974
+ }
21975
+ var NPM_SCRIPT_PATTERN, MAKE_PATTERN, PM_BUILTIN_SUBCOMMANDS, PM_MANAGER_BUILTINS, PKG_DEPENDENT_TOOL_PATTERN, BIN_TO_PACKAGE, PKG_SHORTHAND_PATTERN;
21976
+ var init_commands = __esm({
21977
+ "src/core/checks/commands.ts"() {
21978
+ "use strict";
21979
+ init_define_WEB_FIRST_SEGMENTS();
21980
+ init_main3();
21981
+ init_fs();
21982
+ NPM_SCRIPT_PATTERN = /^(?:npm\s+run|(pnpm|yarn|bun)(?:\s+(run))?)\s+(\S+)/;
21983
+ MAKE_PATTERN = /^make\s+\S/;
21984
+ PM_BUILTIN_SUBCOMMANDS = /* @__PURE__ */ new Set([
21985
+ "add",
21986
+ "approve-builds",
21987
+ "audit",
21988
+ "bin",
21989
+ "cache",
21990
+ "ci",
21991
+ "config",
21992
+ "create",
21993
+ "dedupe",
21994
+ "dlx",
21995
+ "doctor",
21996
+ "env",
21997
+ "exec",
21998
+ "fetch",
21999
+ "global",
22000
+ "help",
22001
+ "i",
22002
+ "import",
22003
+ "info",
22004
+ "init",
22005
+ "install",
22006
+ "licenses",
22007
+ "link",
22008
+ "list",
22009
+ "login",
22010
+ "logout",
22011
+ "ls",
22012
+ "node",
22013
+ "npm",
22014
+ "outdated",
22015
+ "pack",
22016
+ "patch",
22017
+ "patch-commit",
22018
+ "prune",
22019
+ "publish",
22020
+ "rebuild",
22021
+ "remove",
22022
+ "rm",
22023
+ "root",
22024
+ "run",
22025
+ "self-update",
22026
+ "set",
22027
+ "setup",
22028
+ "store",
22029
+ "team",
22030
+ "un",
22031
+ "uninstall",
22032
+ "unlink",
22033
+ "up",
22034
+ "update",
22035
+ "upgrade",
22036
+ "version",
22037
+ "whoami",
22038
+ "why",
22039
+ "workspace",
22040
+ "workspaces",
22041
+ "x"
22042
+ ]);
22043
+ PM_MANAGER_BUILTINS = {
22044
+ bun: /* @__PURE__ */ new Set(["test"])
22045
+ };
22046
+ PKG_DEPENDENT_TOOL_PATTERN = /^(vitest|jest|pytest|mocha|eslint|prettier|tsc)\b/;
22047
+ BIN_TO_PACKAGE = {
22048
+ tsc: "typescript"
22049
+ };
22050
+ PKG_SHORTHAND_PATTERN = /^(npm|pnpm|yarn|bun)\s+(test|start|build|dev|lint|format|check|typecheck|clean|serve|preview|e2e)\b/;
22051
+ }
22052
+ });
22053
+
22054
+ // src/core/checks/staleness.ts
22055
+ import * as path6 from "node:path";
22056
+ async function checkStaleness(file2, projectRoot) {
22057
+ const issues = [];
22058
+ if (!await isGitRepo(projectRoot)) {
22059
+ return issues;
22060
+ }
22061
+ const relativePath = path6.relative(projectRoot, file2.filePath).replace(/\\/g, "/");
22062
+ const lastModified = await getFileLastModified(projectRoot, relativePath);
22063
+ if (!lastModified || isNaN(lastModified.getTime())) {
22064
+ return issues;
22065
+ }
22066
+ const daysSinceUpdate = Math.floor((Date.now() - lastModified.getTime()) / (1e3 * 60 * 60 * 24));
22067
+ if (daysSinceUpdate < INFO_DAYS) {
22068
+ return issues;
22069
+ }
22070
+ const referencedPaths = /* @__PURE__ */ new Set();
22071
+ const globByPrefix = /* @__PURE__ */ new Map();
22072
+ for (const ref of file2.references.paths) {
22073
+ const normalized = ref.value.replace(/\\/g, "/");
22074
+ if (normalized.includes("*")) {
22075
+ const staticSegments = [];
22076
+ for (const segment of normalized.split("/")) {
22077
+ if (segment === ".") continue;
22078
+ if (segment.includes("*")) break;
22079
+ staticSegments.push(segment);
22080
+ }
22081
+ const prefix = staticSegments.join("/");
22082
+ if (prefix) {
22083
+ const key = `${prefix}/`;
22084
+ referencedPaths.add(key);
22085
+ if (!globByPrefix.has(key)) globByPrefix.set(key, ref.value);
22086
+ }
22087
+ continue;
22088
+ }
22089
+ referencedPaths.add(ref.value);
22090
+ }
22091
+ if (referencedPaths.size === 0) {
22092
+ return issues;
22093
+ }
22094
+ const counts = await getCommitsSinceBatch(projectRoot, [...referencedPaths], lastModified);
22095
+ let totalCommits = 0;
22096
+ let mostActiveRef = "";
22097
+ let mostActiveCommits = 0;
22098
+ for (const [refPath, commits] of counts) {
22099
+ totalCommits += commits;
22100
+ if (commits > mostActiveCommits) {
22101
+ mostActiveCommits = commits;
22102
+ mostActiveRef = refPath;
22103
+ }
22104
+ }
22105
+ if (totalCommits === 0) {
22106
+ return issues;
22107
+ }
22108
+ const isStale = daysSinceUpdate >= WARNING_DAYS;
22109
+ const severity = isStale ? "warning" : "info";
22110
+ const mostActiveLabel = globByPrefix.get(mostActiveRef) ?? mostActiveRef;
22111
+ issues.push({
22112
+ severity,
22113
+ check: "staleness",
22114
+ ruleId: isStale ? "staleness/stale" : "staleness/aging",
22115
+ line: 1,
22116
+ message: `Last updated ${daysSinceUpdate} days ago. ${mostActiveLabel} has ${mostActiveCommits} commits since.`,
22117
+ suggestion: "Review and update this context file to reflect recent changes.",
22118
+ detail: `${totalCommits} total commits to referenced paths since last update.`
22119
+ });
22120
+ return issues;
22121
+ }
22122
+ var WARNING_DAYS, INFO_DAYS;
22123
+ var init_staleness = __esm({
22124
+ "src/core/checks/staleness.ts"() {
22125
+ "use strict";
22126
+ init_define_WEB_FIRST_SEGMENTS();
22127
+ init_git();
22128
+ WARNING_DAYS = 30;
22129
+ INFO_DAYS = 14;
22130
+ }
22131
+ });
22132
+
22133
+ // src/core/suppressions.ts
22134
+ function collectSuppressions(content) {
22135
+ const suppressions = /* @__PURE__ */ new Map();
22136
+ const lines = content.split("\n");
22137
+ const add = (lineNumber, checks) => {
22138
+ const existing = suppressions.get(lineNumber);
22139
+ if (existing === null) return;
22140
+ if (checks.length === 0) {
22141
+ suppressions.set(lineNumber, null);
22142
+ return;
22143
+ }
22144
+ const merged = existing ?? /* @__PURE__ */ new Set();
22145
+ for (const check2 of checks) merged.add(check2);
22146
+ suppressions.set(lineNumber, merged);
22147
+ };
22148
+ for (let i2 = 0; i2 < lines.length; i2++) {
22149
+ const line = lines[i2];
22150
+ if (!line.includes("ctxlint-ignore-")) continue;
22151
+ DIRECTIVE_RE.lastIndex = 0;
22152
+ let match;
22153
+ while ((match = DIRECTIVE_RE.exec(line)) !== null) {
22154
+ const kind = match[1];
22155
+ const checks = (match[2] ?? "").trim().split(/\s+/).filter(Boolean);
22156
+ add(kind === "next-line" ? i2 + 2 : i2 + 1, checks);
22157
+ }
22158
+ }
22159
+ return suppressions;
22160
+ }
22161
+ function isSuppressed(suppressions, issue2) {
22162
+ if (suppressions.size === 0) return false;
22163
+ if (!suppressions.has(issue2.line)) return false;
22164
+ const checks = suppressions.get(issue2.line);
22165
+ if (checks === null) return true;
22166
+ return checks.has(issue2.check);
22167
+ }
22168
+ var DIRECTIVE_RE;
22169
+ var init_suppressions = __esm({
22170
+ "src/core/suppressions.ts"() {
22171
+ "use strict";
22172
+ init_define_WEB_FIRST_SEGMENTS();
22173
+ DIRECTIVE_RE = /<!--\s*ctxlint-ignore-(next-line|line)((?:\s+[\w-]+)*)\s*-->/g;
22174
+ }
22175
+ });
22176
+
22177
+ // src/core/checks/tokens.ts
22178
+ function resolveTokenThresholds(overrides) {
22179
+ if (!overrides) return DEFAULT_TOKEN_THRESHOLDS;
22180
+ const merged = { ...DEFAULT_TOKEN_THRESHOLDS, ...overrides };
22181
+ if (merged.info >= merged.warning || merged.warning >= merged.error) {
22182
+ console.error(
22183
+ `Warning: token thresholds should satisfy info < warning < error (got ${merged.info}, ${merged.warning}, ${merged.error}) \u2014 using defaults`
22184
+ );
22185
+ return DEFAULT_TOKEN_THRESHOLDS;
22186
+ }
22187
+ return merged;
22188
+ }
22189
+ async function checkTokens(file2, _projectRoot, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
22190
+ const issues = [];
22191
+ const tokens = file2.totalTokens;
22192
+ if (tokens >= thresholds.error) {
22193
+ issues.push({
22194
+ severity: "error",
22195
+ check: "tokens",
22196
+ ruleId: "tokens/excessive",
22197
+ line: 1,
22198
+ message: `${tokens.toLocaleString()} tokens \u2014 consumes significant context window space`,
22199
+ suggestion: "Consider splitting into focused sections or removing redundant content."
22200
+ });
22201
+ } else if (tokens >= thresholds.warning) {
22202
+ issues.push({
22203
+ severity: "warning",
22204
+ check: "tokens",
22205
+ ruleId: "tokens/large",
22206
+ line: 1,
22207
+ message: `${tokens.toLocaleString()} tokens \u2014 large context file`,
22208
+ suggestion: "Consider trimming \u2014 research shows diminishing returns past ~300 lines."
22209
+ });
22210
+ } else if (tokens >= thresholds.info) {
22211
+ issues.push({
22212
+ severity: "info",
22213
+ check: "tokens",
22214
+ ruleId: "tokens/info",
22215
+ line: 1,
22216
+ message: `Uses ~${tokens.toLocaleString()} tokens per session`
22217
+ });
22218
+ }
22219
+ return issues;
22220
+ }
22221
+ function checkAggregateTokens(files, thresholds = DEFAULT_TOKEN_THRESHOLDS) {
22222
+ const total = files.reduce((sum, f) => sum + f.tokens, 0);
22223
+ if (total >= thresholds.aggregate && files.length > 1) {
22224
+ return {
22225
+ severity: "warning",
22226
+ check: "tokens",
22227
+ ruleId: "tokens/aggregate",
22228
+ line: 0,
22229
+ message: `${files.length} context files consume ${total.toLocaleString()} tokens combined`,
22230
+ suggestion: "Consider consolidating or trimming to reduce per-session context cost."
22231
+ };
22232
+ }
22233
+ return null;
22234
+ }
22235
+ var DEFAULT_TOKEN_THRESHOLDS;
22236
+ var init_tokens2 = __esm({
22237
+ "src/core/checks/tokens.ts"() {
22238
+ "use strict";
22239
+ init_define_WEB_FIRST_SEGMENTS();
22240
+ DEFAULT_TOKEN_THRESHOLDS = {
22241
+ info: 1e3,
22242
+ warning: 3e3,
22243
+ error: 8e3,
22244
+ aggregate: 5e3,
22245
+ tierBreakdown: 1e3,
22246
+ tierAggregate: 4e3
22247
+ };
22248
+ }
22249
+ });
22250
+
22173
22251
  // src/core/checks/tier-tokens.ts
22174
22252
  import * as fs5 from "node:fs";
22175
22253
  import * as os2 from "node:os";
@@ -24047,6 +24125,11 @@ function loadWebFirstSegments() {
24047
24125
  function encodeProjectDir(fsPath) {
24048
24126
  return fsPath.replace(/[:\\/\.]/g, "-");
24049
24127
  }
24128
+ function projectDirCandidates(fsPath) {
24129
+ const encoded = encodeProjectDir(fsPath);
24130
+ const folded = encoded.replace(/_/g, "-");
24131
+ return folded === encoded ? [encoded] : [encoded, folded];
24132
+ }
24050
24133
  function projectDirMatchesPath(encodedDir, fsPath) {
24051
24134
  const normalized = fsPath.replace(/\\/g, "/");
24052
24135
  return encodedDir === encodeProjectDir(normalized);
@@ -24933,13 +25016,19 @@ import { join as join8 } from "node:path";
24933
25016
  async function checkMemoryIndexOverflow(ctx) {
24934
25017
  const home2 = process.env.HOME || process.env.USERPROFILE || homedir5();
24935
25018
  if (!home2) return [];
24936
- const encoded = encodeProjectDir(ctx.currentProject);
24937
- const memoryFile = join8(home2, ".claude", "projects", encoded, "memory", "MEMORY.md");
24938
- let content;
24939
- try {
24940
- content = stripBom(await readFile3(memoryFile, "utf-8"));
24941
- } catch {
24942
- return [];
25019
+ let content = "";
25020
+ let memoryFile = "";
25021
+ for (const encoded of projectDirCandidates(ctx.currentProject)) {
25022
+ const candidate = join8(home2, ".claude", "projects", encoded, "memory", "MEMORY.md");
25023
+ try {
25024
+ content = stripBom(await readFile3(candidate, "utf-8"));
25025
+ } catch {
25026
+ continue;
25027
+ }
25028
+ if (content) {
25029
+ memoryFile = candidate;
25030
+ break;
25031
+ }
24943
25032
  }
24944
25033
  if (!content) return [];
24945
25034
  const lines = content.split("\n");
@@ -24984,16 +25073,429 @@ var init_memory_index_overflow = __esm({
24984
25073
  }
24985
25074
  });
24986
25075
 
24987
- // src/core/checks/ci-coverage.ts
24988
- import { readdir as readdir3, readFile as readFile4 } from "node:fs/promises";
25076
+ // src/core/transcript.ts
25077
+ import { createReadStream as createReadStream2, existsSync as existsSync5, statSync as statSync4 } from "node:fs";
25078
+ import { readdir as readdir3 } from "node:fs/promises";
25079
+ import { homedir as homedir6 } from "node:os";
24989
25080
  import { join as join9 } from "node:path";
24990
- import { existsSync as existsSync5 } from "node:fs";
25081
+ import { createInterface as createInterface2 } from "node:readline";
25082
+ function asString(v2) {
25083
+ return typeof v2 === "string" ? v2 : "";
25084
+ }
25085
+ function resultText(content) {
25086
+ if (typeof content === "string") return content;
25087
+ if (!Array.isArray(content)) return "";
25088
+ return content.map((b2) => b2 && typeof b2 === "object" ? asString(b2.text) : "").join("");
25089
+ }
25090
+ function resolveHome() {
25091
+ return process.env.HOME || process.env.USERPROFILE || homedir6();
25092
+ }
25093
+ function readProjectTranscript(project, home2 = resolveHome()) {
25094
+ const key = `${home2} ${project}`;
25095
+ let hit = cache.get(key);
25096
+ if (!hit) {
25097
+ hit = readUncached(project, home2);
25098
+ cache.set(key, hit);
25099
+ }
25100
+ return hit;
25101
+ }
25102
+ function candidateDirs(project, home2) {
25103
+ const root = join9(home2, ".claude", "projects");
25104
+ return projectDirCandidates(project).map((n7) => join9(root, n7)).filter((d) => existsSync5(d));
25105
+ }
25106
+ async function readUncached(project, home2) {
25107
+ if (!home2 || !project) return EMPTY;
25108
+ const dirs = candidateDirs(project, home2);
25109
+ if (dirs.length === 0) return EMPTY;
25110
+ const names = [];
25111
+ for (const dir of dirs) {
25112
+ for (const name of await readdir3(dir).catch(() => [])) {
25113
+ names.push({ dir, name });
25114
+ }
25115
+ }
25116
+ const files = names.filter(({ name }) => name.endsWith(".jsonl")).map(({ dir, name }) => {
25117
+ const p2 = join9(dir, name);
25118
+ try {
25119
+ return { p: p2, mtime: statSync4(p2).mtimeMs };
25120
+ } catch {
25121
+ return null;
25122
+ }
25123
+ }).filter((f) => f !== null).sort((a, b2) => b2.mtime - a.mtime);
25124
+ let truncated = files.length > MAX_TRANSCRIPTS;
25125
+ const selected = files.slice(0, MAX_TRANSCRIPTS);
25126
+ const events = [];
25127
+ const pending = /* @__PURE__ */ new Map();
25128
+ let lines = 0;
25129
+ for (const { p: p2 } of selected) {
25130
+ if (lines >= MAX_LINES2) {
25131
+ truncated = true;
25132
+ break;
25133
+ }
25134
+ const rl = createInterface2({
25135
+ input: createReadStream2(p2, { encoding: "utf-8" }),
25136
+ crlfDelay: Infinity
25137
+ });
25138
+ try {
25139
+ for await (const raw of rl) {
25140
+ if (++lines >= MAX_LINES2) {
25141
+ truncated = true;
25142
+ break;
25143
+ }
25144
+ const line = raw.trim();
25145
+ if (!line) continue;
25146
+ let rec;
25147
+ try {
25148
+ rec = JSON.parse(line);
25149
+ } catch {
25150
+ continue;
25151
+ }
25152
+ collect(rec, events, pending);
25153
+ }
25154
+ } catch {
25155
+ } finally {
25156
+ rl.close();
25157
+ }
25158
+ }
25159
+ return { events, filesRead: selected.length, truncated };
25160
+ }
25161
+ function collect(rec, events, pending) {
25162
+ const message = rec.message;
25163
+ const content = message?.content;
25164
+ if (!Array.isArray(content)) return;
25165
+ const timestamp = Date.parse(asString(rec.timestamp)) || 0;
25166
+ const sessionId = asString(rec.sessionId) || asString(rec.session_id);
25167
+ const gitBranch = asString(rec.gitBranch) || void 0;
25168
+ for (const raw of content) {
25169
+ if (!raw || typeof raw !== "object") continue;
25170
+ const block = raw;
25171
+ const type = asString(block.type);
25172
+ if (type === "text" && rec.type === "assistant") {
25173
+ const text = asString(block.text);
25174
+ if (text) {
25175
+ events.push({ kind: "assistant-text", text, tool: "", gitBranch, timestamp, sessionId });
25176
+ }
25177
+ continue;
25178
+ }
25179
+ if (type === "tool_use") {
25180
+ const tool = asString(block.name);
25181
+ const input = block.input ?? {};
25182
+ const cmdField = COMMAND_TOOLS[tool];
25183
+ const writeField = WRITE_TOOLS[tool];
25184
+ let ev = null;
25185
+ if (cmdField) {
25186
+ const text = asString(input[cmdField]);
25187
+ if (text) ev = { kind: "command", text, tool, gitBranch, timestamp, sessionId };
25188
+ } else if (writeField) {
25189
+ const text = asString(input[writeField]);
25190
+ if (text) ev = { kind: "file-write", text, tool, gitBranch, timestamp, sessionId };
25191
+ }
25192
+ if (ev) {
25193
+ events.push(ev);
25194
+ const id = asString(block.id);
25195
+ if (id) pending.set(id, ev);
25196
+ }
25197
+ continue;
25198
+ }
25199
+ if (type === "tool_result") {
25200
+ const id = asString(block.tool_use_id);
25201
+ const ev = id ? pending.get(id) : void 0;
25202
+ if (!ev) continue;
25203
+ pending.delete(id);
25204
+ ev.isError = block.is_error === true;
25205
+ ev.emptyOutput = resultText(block.content).trim().length === 0;
25206
+ }
25207
+ }
25208
+ }
25209
+ var WRITE_TOOLS, COMMAND_TOOLS, MAX_TRANSCRIPTS, MAX_LINES2, EMPTY, cache;
25210
+ var init_transcript = __esm({
25211
+ "src/core/transcript.ts"() {
25212
+ "use strict";
25213
+ init_define_WEB_FIRST_SEGMENTS();
25214
+ init_session_parser();
25215
+ WRITE_TOOLS = {
25216
+ Write: "file_path",
25217
+ Edit: "file_path",
25218
+ NotebookEdit: "notebook_path"
25219
+ };
25220
+ COMMAND_TOOLS = {
25221
+ Bash: "command",
25222
+ PowerShell: "command"
25223
+ };
25224
+ MAX_TRANSCRIPTS = 5;
25225
+ MAX_LINES2 = 2e5;
25226
+ EMPTY = { events: [], filesRead: 0, truncated: false };
25227
+ cache = /* @__PURE__ */ new Map();
25228
+ }
25229
+ });
25230
+
25231
+ // src/core/checks/session/shared-temp-path.ts
25232
+ function unquote(raw) {
25233
+ let s = raw;
25234
+ let prev;
25235
+ do {
25236
+ prev = s;
25237
+ s = s.replace(/^['"`]|['"`]$/g, "");
25238
+ } while (s !== prev);
25239
+ return s;
25240
+ }
25241
+ function normalize3(raw) {
25242
+ return unquote(raw).replace(/\\/g, "/").toLowerCase();
25243
+ }
25244
+ function isFixedTempPath(candidate) {
25245
+ const p2 = normalize3(candidate);
25246
+ const root = TEMP_ROOTS.map((r2) => normalize3(r2)).find((r2) => p2.startsWith(r2));
25247
+ if (!root) return false;
25248
+ if (p2.length <= root.length) return false;
25249
+ return !SCOPED_MARKERS.some((m) => p2.includes(m.toLowerCase()));
25250
+ }
25251
+ function extract(line, patterns) {
25252
+ const found = [];
25253
+ for (const re2 of patterns) {
25254
+ for (const m of line.matchAll(new RegExp(re2, "g"))) {
25255
+ if (m[1]) found.push(m[1]);
25256
+ }
25257
+ }
25258
+ return found;
25259
+ }
25260
+ async function checkSharedTempPath(ctx) {
25261
+ const issues = [];
25262
+ const written = /* @__PURE__ */ new Map();
25263
+ const reported = /* @__PURE__ */ new Set();
25264
+ const transcript = await readProjectTranscript(ctx.currentProject);
25265
+ const scanned = [
25266
+ ...transcript.events.filter((e) => e.kind === "command").map((e) => ({ display: e.text, timestamp: e.timestamp })),
25267
+ ...ctx.history.map((h2) => ({ display: h2.display, timestamp: h2.timestamp }))
25268
+ ];
25269
+ const ordered = scanned.sort((a, b2) => a.timestamp - b2.timestamp);
25270
+ for (const entry of ordered) {
25271
+ const line = entry.display;
25272
+ const usesMktemp = /\bmktemp\b/.test(line);
25273
+ for (const candidate of extract(line, READ_PATTERNS)) {
25274
+ if (usesMktemp || !isFixedTempPath(candidate)) continue;
25275
+ const key = normalize3(candidate);
25276
+ const origin = written.get(key);
25277
+ if (!origin || reported.has(key)) continue;
25278
+ reported.add(key);
25279
+ issues.push({
25280
+ severity: "error",
25281
+ check: "session-shared-temp-path",
25282
+ ruleId: "session-shared-temp-path/shared-temp-path",
25283
+ line: 0,
25284
+ message: `Fixed temp path "${unquote(candidate)}" is written and later read back`,
25285
+ detail: `Written by: ${origin.display.trim().slice(0, 120)}
25286
+ Read by: ${line.trim().slice(0, 120)}`,
25287
+ suggestion: "Use a per-run path instead: `T=$(mktemp)` in shell, or a session-scoped scratch directory. A literal path under a shared temp root can be overwritten by any concurrent session between the write and the read, and a restore then puts the wrong bytes into your workspace."
25288
+ });
25289
+ }
25290
+ for (const candidate of extract(line, WRITE_PATTERNS)) {
25291
+ if (usesMktemp || !isFixedTempPath(candidate)) continue;
25292
+ written.set(normalize3(candidate), entry);
25293
+ }
25294
+ }
25295
+ return issues;
25296
+ }
25297
+ var TEMP_ROOTS, SCOPED_MARKERS, WRITE_PATTERNS, READ_PATTERNS;
25298
+ var init_shared_temp_path = __esm({
25299
+ "src/core/checks/session/shared-temp-path.ts"() {
25300
+ "use strict";
25301
+ init_define_WEB_FIRST_SEGMENTS();
25302
+ init_transcript();
25303
+ TEMP_ROOTS = [
25304
+ "/tmp/",
25305
+ "/var/tmp/",
25306
+ "$TMPDIR/",
25307
+ "${TMPDIR}/",
25308
+ "%TEMP%\\",
25309
+ "%TMP%\\"
25310
+ ];
25311
+ SCOPED_MARKERS = [
25312
+ "$$",
25313
+ "$pid",
25314
+ "${pid}",
25315
+ "mktemp",
25316
+ "$random",
25317
+ "${random}",
25318
+ "$session",
25319
+ "${session}",
25320
+ "$uuid",
25321
+ "${uuid}"
25322
+ ];
25323
+ WRITE_PATTERNS = [
25324
+ />\s*(\S+)/,
25325
+ // shell redirect
25326
+ /\bcp\s+\S+\s+(\S+)/,
25327
+ // cp src dest
25328
+ /\bmv\s+\S+\s+(\S+)/,
25329
+ /\bcopy\s+\S+\s+(\S+)/,
25330
+ /\bmove\s+\S+\s+(\S+)/,
25331
+ /\btee\s+(\S+)/,
25332
+ /writeFileSync\(\s*['"`]([^'"`]+)/,
25333
+ /\bcurl\b[^|]*-o\s+(\S+)/
25334
+ ];
25335
+ READ_PATTERNS = [
25336
+ /\bcp\s+(\S+)\s+\S+/,
25337
+ /\bcopy\s+(\S+)\s+\S+/,
25338
+ /\bcat\s+(\S+)/,
25339
+ /\btype\s+(\S+)/,
25340
+ /\bsource\s+(\S+)/,
25341
+ /\breadFileSync\(\s*['"`]([^'"`]+)/,
25342
+ // Input redirect. `(?!<)` excludes heredocs (`<<'EOF'`), and the leading
25343
+ // boundary plus the restricted character class keep it from matching a `<`
25344
+ // that is part of a comparison, an HTML fragment, or another operator --
25345
+ // a bare /<\s*(\S+)/ matched unrelated text in real transcripts.
25346
+ /(?:^|\s)<(?!<)\s*([^\s<>|&;]+)/
25347
+ ];
25348
+ }
25349
+ });
25350
+
25351
+ // src/core/checks/session/unverified-gate-claimed-clean.ts
25352
+ function gateOf(cmd) {
25353
+ for (const { re: re2, gate } of GATE_PATTERNS) {
25354
+ if (re2.test(cmd)) return gate;
25355
+ }
25356
+ return null;
25357
+ }
25358
+ function isUnverified(ev) {
25359
+ return ev.isError === true || ev.emptyOutput === true;
25360
+ }
25361
+ async function checkUnverifiedGateClaimedClean(ctx) {
25362
+ const { events } = await readProjectTranscript(ctx.currentProject);
25363
+ if (events.length === 0) return [];
25364
+ const ordered = [...events].sort((a, b2) => a.timestamp - b2.timestamp);
25365
+ const issues = [];
25366
+ const reported = /* @__PURE__ */ new Set();
25367
+ for (let i2 = 0; i2 < ordered.length; i2++) {
25368
+ const ev = ordered[i2];
25369
+ if (ev.kind !== "command") continue;
25370
+ const gate = gateOf(ev.text);
25371
+ if (!gate || !isUnverified(ev)) continue;
25372
+ for (let j3 = i2 + 1; j3 < ordered.length && j3 <= i2 + ADJACENCY; j3++) {
25373
+ const next = ordered[j3];
25374
+ if (next.kind === "command" && gateOf(next.text) === gate) break;
25375
+ if (next.kind !== "assistant-text") continue;
25376
+ if (HONEST_MARKERS.some((re2) => re2.test(next.text))) break;
25377
+ if (!CLEAN_CLAIMS.some((re2) => re2.test(next.text))) continue;
25378
+ if (reported.has(gate)) break;
25379
+ reported.add(gate);
25380
+ const why = ev.isError ? "the invocation reported an error" : "the invocation produced no output";
25381
+ issues.push({
25382
+ severity: "warning",
25383
+ check: "session-unverified-gate-claimed-clean",
25384
+ ruleId: "session-unverified-gate-claimed-clean/unverified-gate-claimed-clean",
25385
+ line: 0,
25386
+ message: `'${gate}' asserted as passing, but ${why}`,
25387
+ detail: `Gate command: ${ev.text.trim().slice(0, 120)}
25388
+ Claim: ${next.text.trim().slice(0, 120)}`,
25389
+ suggestion: 'An empty or failed gate carries no information about cleanliness -- a runner that dies before emitting diagnostics looks exactly like one that found none. Either re-run the gate until it produces a real result, or report the state honestly ("lint UNVERIFIED -- runner crashed").'
25390
+ });
25391
+ break;
25392
+ }
25393
+ }
25394
+ return issues;
25395
+ }
25396
+ var GATE_PATTERNS, CLEAN_CLAIMS, HONEST_MARKERS, ADJACENCY;
25397
+ var init_unverified_gate_claimed_clean = __esm({
25398
+ "src/core/checks/session/unverified-gate-claimed-clean.ts"() {
25399
+ "use strict";
25400
+ init_define_WEB_FIRST_SEGMENTS();
25401
+ init_transcript();
25402
+ GATE_PATTERNS = [
25403
+ { re: /\b(biome|eslint|ruff|clippy)\b/, gate: "lint" },
25404
+ { re: /\blint(:fix)?\b/, gate: "lint" },
25405
+ { re: /\btsc\b|\btypecheck\b|\btype-check\b/, gate: "typecheck" },
25406
+ { re: /\b(vitest|jest|pytest|mocha)\b/, gate: "test" },
25407
+ { re: /\btest\b/, gate: "test" },
25408
+ { re: /\bbuild\b/, gate: "build" }
25409
+ ];
25410
+ CLEAN_CLAIMS = [
25411
+ /\ball green\b/i,
25412
+ /\bno (lint )?(violations|errors|issues|diagnostics)\b/i,
25413
+ /\b0 errors\b/i,
25414
+ /\bzero (errors|violations|diagnostics)\b/i,
25415
+ /\b(lint|typecheck|type-check|tests?|build)\b[^.\n]{0,40}\b(clean|passe[sd]|passing|green|succeeded)\b/i,
25416
+ /\b(clean|passing|green)\b[^.\n]{0,25}\b(lint|typecheck|type-check|tests?|build)\b/i
25417
+ ];
25418
+ HONEST_MARKERS = [
25419
+ /\bunverified\b/i,
25420
+ /\bcould not verify\b/i,
25421
+ /\bcannot verify\b/i,
25422
+ /\bunable to verify\b/i,
25423
+ /\bcrashed?\b/i,
25424
+ /\bsegfault/i,
25425
+ /\bblocked\b/i,
25426
+ /\bnot trustworthy\b/i,
25427
+ /\bno signal\b/i,
25428
+ /\bdid not run\b/i,
25429
+ /\binconclusive\b/i
25430
+ ];
25431
+ ADJACENCY = 12;
25432
+ }
25433
+ });
25434
+
25435
+ // src/core/checks/session/default-branch-accumulation.ts
25436
+ function isCommit(cmd) {
25437
+ return /\bgit\s+commit\b/.test(cmd) && !/--dry-run\b/.test(cmd);
25438
+ }
25439
+ function isBranchAway(cmd) {
25440
+ return /\bgit\s+(checkout\s+-b|switch\s+-c|worktree\s+add)\b/.test(cmd);
25441
+ }
25442
+ async function checkDefaultBranchAccumulation(ctx) {
25443
+ const { events } = await readProjectTranscript(ctx.currentProject);
25444
+ if (events.length === 0) return [];
25445
+ const ordered = [...events].sort((a, b2) => a.timestamp - b2.timestamp);
25446
+ const pending = /* @__PURE__ */ new Set();
25447
+ let branch = "";
25448
+ let firstWrite = "";
25449
+ for (const ev of ordered) {
25450
+ if (ev.gitBranch) branch = ev.gitBranch;
25451
+ if (ev.kind === "command") {
25452
+ if (isCommit(ev.text) || isBranchAway(ev.text)) {
25453
+ pending.clear();
25454
+ firstWrite = "";
25455
+ }
25456
+ continue;
25457
+ }
25458
+ if (ev.kind !== "file-write") continue;
25459
+ if (!DEFAULT_BRANCHES.has(branch)) continue;
25460
+ if (pending.size === 0) firstWrite = ev.text;
25461
+ pending.add(ev.text);
25462
+ }
25463
+ if (pending.size < THRESHOLD) return [];
25464
+ const sample = [...pending].slice(0, 5);
25465
+ return [
25466
+ {
25467
+ severity: "warning",
25468
+ check: "session-default-branch-accumulation",
25469
+ ruleId: "session-default-branch-accumulation/default-branch-accumulation",
25470
+ line: 0,
25471
+ message: `${pending.size} files edited on '${branch}' with no intervening commit`,
25472
+ detail: `First uncommitted write: ${firstWrite}
25473
+ Sample: ${sample.join(", ")}${pending.size > sample.length ? ", ..." : ""}`,
25474
+ suggestion: "Commit as you go, or move the work to a topic branch (`git checkout -b`). A large uncommitted delta on a shared default branch is hard to review and, when other agent sessions share the checkout, one `git stash` or `git checkout --` away from being lost."
25475
+ }
25476
+ ];
25477
+ }
25478
+ var DEFAULT_BRANCHES, THRESHOLD;
25479
+ var init_default_branch_accumulation = __esm({
25480
+ "src/core/checks/session/default-branch-accumulation.ts"() {
25481
+ "use strict";
25482
+ init_define_WEB_FIRST_SEGMENTS();
25483
+ init_transcript();
25484
+ DEFAULT_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
25485
+ THRESHOLD = 10;
25486
+ }
25487
+ });
25488
+
25489
+ // src/core/checks/ci-coverage.ts
25490
+ import { readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
25491
+ import { join as join10 } from "node:path";
25492
+ import { existsSync as existsSync6 } from "node:fs";
24991
25493
  async function findReleaseWorkflows(projectRoot) {
24992
- const workflowDir = join9(projectRoot, ".github", "workflows");
24993
- if (!existsSync5(workflowDir)) return [];
25494
+ const workflowDir = join10(projectRoot, ".github", "workflows");
25495
+ if (!existsSync6(workflowDir)) return [];
24994
25496
  let files;
24995
25497
  try {
24996
- files = await readdir3(workflowDir);
25498
+ files = await readdir4(workflowDir);
24997
25499
  } catch {
24998
25500
  return [];
24999
25501
  }
@@ -25005,7 +25507,7 @@ async function findReleaseWorkflows(projectRoot) {
25005
25507
  continue;
25006
25508
  }
25007
25509
  try {
25008
- const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
25510
+ const content = stripBom(await readFile4(join10(workflowDir, f), "utf-8"));
25009
25511
  const nameMatch = content.match(/^\s*name:\s*(.+?)\s*$/m);
25010
25512
  if (nameMatch) {
25011
25513
  const name = nameMatch[1].replace(/^(['"])(.*)\1$/, "$2");
@@ -25064,15 +25566,15 @@ var init_ci_coverage = __esm({
25064
25566
  });
25065
25567
 
25066
25568
  // src/core/checks/ci-secrets.ts
25067
- import { readdir as readdir4, readFile as readFile5 } from "node:fs/promises";
25068
- import { join as join10 } from "node:path";
25069
- import { existsSync as existsSync6 } from "node:fs";
25569
+ import { readdir as readdir5, readFile as readFile5 } from "node:fs/promises";
25570
+ import { join as join11 } from "node:path";
25571
+ import { existsSync as existsSync7 } from "node:fs";
25070
25572
  async function findSecretUsages(projectRoot) {
25071
- const workflowDir = join10(projectRoot, ".github", "workflows");
25072
- if (!existsSync6(workflowDir)) return [];
25573
+ const workflowDir = join11(projectRoot, ".github", "workflows");
25574
+ if (!existsSync7(workflowDir)) return [];
25073
25575
  let files;
25074
25576
  try {
25075
- files = await readdir4(workflowDir);
25577
+ files = await readdir5(workflowDir);
25076
25578
  } catch {
25077
25579
  return [];
25078
25580
  }
@@ -25081,7 +25583,7 @@ async function findSecretUsages(projectRoot) {
25081
25583
  if (!(f.endsWith(".yml") || f.endsWith(".yaml"))) continue;
25082
25584
  let content;
25083
25585
  try {
25084
- content = stripBom(await readFile5(join10(workflowDir, f), "utf-8"));
25586
+ content = stripBom(await readFile5(join11(workflowDir, f), "utf-8"));
25085
25587
  } catch {
25086
25588
  continue;
25087
25589
  }
@@ -25470,9 +25972,24 @@ function isMsysFlag(token) {
25470
25972
  }
25471
25973
  function translateMsysDrivePath(s, platform) {
25472
25974
  if (platform !== "win32") return s;
25473
- const m = /^\/([A-Za-z])\/(.*)$/.exec(s);
25975
+ const m = /^\/{1,2}([A-Za-z])\/(.*)$/.exec(s);
25474
25976
  return m ? `${m[1]}:/${m[2]}` : s;
25475
25977
  }
25978
+ function findExistingVariant(raw, projectRoot, homeDir, platform) {
25979
+ const token = stripMatcherWildcard(raw);
25980
+ const variants = /* @__PURE__ */ new Set();
25981
+ if (/^\/{2}[A-Za-z]\//.test(token)) variants.add(token.slice(1));
25982
+ if (/^\/[A-Za-z]\//.test(token)) variants.add(`/${token}`);
25983
+ if (token.startsWith("/") && !/^\/{1,2}[A-Za-z]\//.test(token)) {
25984
+ variants.add(token.replace(/^\/+/, ""));
25985
+ }
25986
+ if (token.includes("\\")) variants.add(token.replace(/\\/g, "/"));
25987
+ for (const v2 of variants) {
25988
+ const resolved = expandPath(v2, projectRoot, homeDir, platform);
25989
+ if (resolved !== null && fileExists(resolved)) return resolved;
25990
+ }
25991
+ return null;
25992
+ }
25476
25993
  function extractCommandPaths(command, projectRoot, homeDir, platform) {
25477
25994
  const tokens = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
25478
25995
  const out = [];
@@ -25505,6 +26022,18 @@ function checkSource(source, projectRoot, homeDir, platform) {
25505
26022
  if (cand.resolved === null) return;
25506
26023
  if (fileExists(cand.resolved)) return;
25507
26024
  const where = source.isUserGlobal ? ` (in ${source.displayPath})` : "";
26025
+ const variant = findExistingVariant(cand.raw, projectRoot, homeDir, platform);
26026
+ if (variant !== null) {
26027
+ issues.push({
26028
+ severity: "warning",
26029
+ check: "hook-coverage",
26030
+ ruleId: "hook-coverage/dead-hook",
26031
+ line,
26032
+ message: `${origin} references "${cand.raw}" which does not resolve${where}, but "${variant}" exists \u2014 the path form is wrong, so the gate silently no-ops`,
26033
+ suggestion: `Rewrite the entry to use "${variant}". Do NOT delete it \u2014 the target exists; only the path form is unresolvable.`
26034
+ });
26035
+ return;
26036
+ }
25508
26037
  issues.push({
25509
26038
  severity: "warning",
25510
26039
  check: "hook-coverage",
@@ -27805,7 +28334,7 @@ import { readFileSync as readFileSync7 } from "node:fs";
27805
28334
  import { resolve as resolve14, dirname as dirname6 } from "node:path";
27806
28335
  import { fileURLToPath as fileURLToPath2 } from "node:url";
27807
28336
  function loadVersion() {
27808
- if (true) return "0.18.5";
28337
+ if (true) return "0.18.7";
27809
28338
  try {
27810
28339
  const __dir = dirname6(fileURLToPath2(import.meta.url));
27811
28340
  const pkgPath = resolve14(__dir, "../package.json");
@@ -27894,7 +28423,8 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
27894
28423
  if (activeChecks.includes("content-secrets"))
27895
28424
  checkPromises.push(checkContentSecrets(parseResult, projectRoot));
27896
28425
  const results = await Promise.all(checkPromises);
27897
- const singleFileIssues = results.flat();
28426
+ const suppressions = collectSuppressions(parseResult.content);
28427
+ const singleFileIssues = results.flat().filter((issue2) => !isSuppressed(suppressions, issue2));
27898
28428
  if (stat3 !== null) {
27899
28429
  setCacheEntry(absPath, {
27900
28430
  mtime: stat3.mtimeMs,
@@ -28043,6 +28573,12 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
28043
28573
  sessionPromises.push(checkLoopDetection(sessionCtx));
28044
28574
  if (sessionChecksToRun.includes("session-memory-index-overflow"))
28045
28575
  sessionPromises.push(checkMemoryIndexOverflow(sessionCtx));
28576
+ if (sessionChecksToRun.includes("session-shared-temp-path"))
28577
+ sessionPromises.push(checkSharedTempPath(sessionCtx));
28578
+ if (sessionChecksToRun.includes("session-unverified-gate-claimed-clean"))
28579
+ sessionPromises.push(checkUnverifiedGateClaimedClean(sessionCtx));
28580
+ if (sessionChecksToRun.includes("session-default-branch-accumulation"))
28581
+ sessionPromises.push(checkDefaultBranchAccumulation(sessionCtx));
28046
28582
  const sessionResults = await Promise.all(sessionPromises);
28047
28583
  const sessionIssues = sessionResults.flat();
28048
28584
  fileResults.push({
@@ -28221,6 +28757,7 @@ var init_audit = __esm({
28221
28757
  init_paths();
28222
28758
  init_commands();
28223
28759
  init_staleness();
28760
+ init_suppressions();
28224
28761
  init_tokens2();
28225
28762
  init_tier_tokens();
28226
28763
  init_redundancy();
@@ -28242,6 +28779,9 @@ var init_audit = __esm({
28242
28779
  init_duplicate_memory();
28243
28780
  init_loop_detection();
28244
28781
  init_memory_index_overflow();
28782
+ init_shared_temp_path();
28783
+ init_unverified_gate_claimed_clean();
28784
+ init_default_branch_accumulation();
28245
28785
  init_ci_coverage();
28246
28786
  init_ci_secrets();
28247
28787
  init_content_secrets();
@@ -28283,7 +28823,10 @@ var init_audit = __esm({
28283
28823
  "session-stale-memory",
28284
28824
  "session-duplicate-memory",
28285
28825
  "session-loop-detection",
28286
- "session-memory-index-overflow"
28826
+ "session-memory-index-overflow",
28827
+ "session-shared-temp-path",
28828
+ "session-unverified-gate-claimed-clean",
28829
+ "session-default-branch-accumulation"
28287
28830
  ];
28288
28831
  ALL_SKILL_CHECKS = [
28289
28832
  "skill-frontmatter",
@@ -54126,8 +54669,8 @@ var require_resolve = __commonJS({
54126
54669
  }
54127
54670
  return count;
54128
54671
  }
54129
- function getFullPath(resolver, id = "", normalize3) {
54130
- if (normalize3 !== false)
54672
+ function getFullPath(resolver, id = "", normalize4) {
54673
+ if (normalize4 !== false)
54131
54674
  id = normalizeId(id);
54132
54675
  const p2 = resolver.parse(id);
54133
54676
  return _getFullPath(resolver, p2);
@@ -55530,7 +56073,7 @@ var require_fast_uri = __commonJS({
55530
56073
  init_define_WEB_FIRST_SEGMENTS();
55531
56074
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils2();
55532
56075
  var { SCHEMES, getSchemeHandler } = require_schemes();
55533
- function normalize3(uri, options) {
56076
+ function normalize4(uri, options) {
55534
56077
  if (typeof uri === "string") {
55535
56078
  uri = /** @type {T} */
55536
56079
  normalizeString(uri, options);
@@ -55797,7 +56340,7 @@ var require_fast_uri = __commonJS({
55797
56340
  }
55798
56341
  var fastUri = {
55799
56342
  SCHEMES,
55800
- normalize: normalize3,
56343
+ normalize: normalize4,
55801
56344
  resolve: resolve18,
55802
56345
  resolveComponent,
55803
56346
  equal,
@@ -68350,6 +68893,8 @@ var init_ora = __esm({
68350
68893
  });
68351
68894
 
68352
68895
  // src/core/reporter.ts
68896
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "node:fs";
68897
+ import { join as join16 } from "node:path";
68353
68898
  function classifyFile(f) {
68354
68899
  if (f.path === "(project)") return "context";
68355
68900
  if (f.path === "(mcp)") return "mcp";
@@ -68371,10 +68916,32 @@ function classifyFile(f) {
68371
68916
  function isSyntheticBucket(p2) {
68372
68917
  return p2 === "(project)" || p2 === "(mcp)" || p2.includes(SESSION_AUDIT_PATH_MARKER) || p2.includes(SKILL_AUDIT_PATH_MARKER);
68373
68918
  }
68919
+ function detectVersionSkew(projectRoot, runningVersion) {
68920
+ try {
68921
+ const pkgPath = join16(projectRoot, "package.json");
68922
+ if (!existsSync8(pkgPath)) return null;
68923
+ const raw = readFileSync10(pkgPath, "utf8").replace(/^\uFEFF/, "");
68924
+ const pkg = JSON.parse(raw);
68925
+ const name = pkg.name ?? "";
68926
+ if (name !== "ctxlint" && name !== "@yawlabs/ctxlint") return null;
68927
+ if (!pkg.version || pkg.version === runningVersion) return null;
68928
+ return pkg.version;
68929
+ } catch {
68930
+ return null;
68931
+ }
68932
+ }
68374
68933
  function formatText(result, verbose = false) {
68375
68934
  const lines = [];
68376
68935
  lines.push("");
68377
68936
  lines.push(source_default.bold(`ctxlint v${result.version}`));
68937
+ const skew = detectVersionSkew(result.projectRoot, result.version);
68938
+ if (skew !== null) {
68939
+ lines.push(
68940
+ source_default.yellow(
68941
+ ` note: this checkout is v${skew} -- you are running v${result.version}. Rebuild or reinstall before trusting a result against it.`
68942
+ )
68943
+ );
68944
+ }
68378
68945
  lines.push("");
68379
68946
  lines.push(`Scanning ${result.projectRoot}...`);
68380
68947
  lines.push("");
@@ -68748,6 +69315,27 @@ function buildRuleDescriptors() {
68748
69315
  },
68749
69316
  helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
68750
69317
  },
69318
+ {
69319
+ id: "ctxlint/session-shared-temp-path",
69320
+ shortDescription: {
69321
+ text: "Fixed temp path written then read back -- clobberable by a concurrent session"
69322
+ },
69323
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
69324
+ },
69325
+ {
69326
+ id: "ctxlint/session-unverified-gate-claimed-clean",
69327
+ shortDescription: {
69328
+ text: "A quality gate asserted as passing while its invocation failed or emitted nothing"
69329
+ },
69330
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
69331
+ },
69332
+ {
69333
+ id: "ctxlint/session-default-branch-accumulation",
69334
+ shortDescription: {
69335
+ text: "Many files edited on the default branch with no intervening commit"
69336
+ },
69337
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
69338
+ },
68751
69339
  {
68752
69340
  id: "ctxlint/skill-frontmatter",
68753
69341
  shortDescription: { text: "Skill/agent definition missing required frontmatter" },
@@ -69144,8 +69732,8 @@ npx @yawlabs/ctxlint@${VERSION} --strict
69144
69732
  program2.parse();
69145
69733
  }
69146
69734
  async function promptYesNo(question) {
69147
- const { createInterface: createInterface2 } = await import("node:readline/promises");
69148
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
69735
+ const { createInterface: createInterface3 } = await import("node:readline/promises");
69736
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
69149
69737
  try {
69150
69738
  const answer = await rl.question(question);
69151
69739
  return /^\s*y(es)?\s*$/i.test(answer);
@@ -69276,4 +69864,3 @@ if (args.includes("--lsp")) {
69276
69864
  const { runCli: runCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
69277
69865
  await runCli2();
69278
69866
  }
69279
- //# sourceMappingURL=index.js.map