@yawlabs/ctxlint 0.24.1 → 0.25.1

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.24.1 --strict
7
+ entry: npx @yawlabs/ctxlint@0.25.1 --strict
8
8
  language: node
9
9
  always_run: true
10
10
  pass_filenames: false
@@ -15,7 +15,7 @@ This specification defines a standard set of lint rules for validating agent ses
15
15
 
16
16
  The specification includes:
17
17
  - A reference of session data locations across 8 AI coding agents
18
- - 12 lint rules in the `session` category with defined severities
18
+ - 13 lint rules in the `session` category with defined severities
19
19
  - A machine-readable rule catalog ([`agent-session-lint-rules.json`](./agent-session-lint-rules.json))
20
20
  - Sibling-repo detection for cross-project checks
21
21
 
@@ -51,6 +51,7 @@ This is the third pillar alongside context file linting (`CLAUDE.md`, `.cursorru
51
51
  - [2.10 session/unverified-gate-claimed-clean](#210-sessionunverified-gate-claimed-clean)
52
52
  - [2.11 session/default-branch-accumulation](#211-sessiondefault-branch-accumulation)
53
53
  - [2.12 session/unresolvable-sha](#212-sessionunresolvable-sha)
54
+ - [2.13 session/large-read](#213-sessionlarge-read)
54
55
  - [3. Rule Catalog (machine-readable)](#3-rule-catalog-machine-readable)
55
56
  - [4. Implementing This Specification](#4-implementing-this-specification)
56
57
  - [5. Contributing](#5-contributing)
@@ -106,7 +107,7 @@ Session data varies enormously in size and format. This specification targets on
106
107
 
107
108
  **What we explicitly DO NOT scan:**
108
109
 
109
- - **Full session transcripts** -- too large. Active projects can accumulate hundreds of megabytes of transcript data. Scanning these would be slow and yield low-signal results.
110
+ - **Full session transcripts** -- too large. Active projects can accumulate hundreds of megabytes of transcript data. Scanning these would be slow and yield low-signal results. Rules whose signal is what the agent *did* read a bounded, project-scoped slice instead -- see §3, "Data sources".
110
111
  - **SQLite databases** -- Goose's `sessions.db` requires a SQLite dependency. Out of scope for v1. Future versions may add opt-in SQLite support.
111
112
  - **File history or shell snapshots** -- some agents capture filesystem state or shell output. These are agent-internal data and not useful for cross-project linting.
112
113
 
@@ -132,7 +133,7 @@ Skip hidden directories (starting with `.`) and `node_modules`.
132
133
 
133
134
  ## 2. Lint Rules
134
135
 
135
- 12 rules in 1 category (`session`). All rules in this category perform cross-project checks using sibling detection or per-project history analysis.
136
+ 13 rules in 1 category (`session`). All rules in this category perform cross-project checks using sibling detection or per-project history analysis.
136
137
 
137
138
  Severity levels:
138
139
  - **error** -- the session data reveals a verifiably missing configuration. Should fail CI.
@@ -459,6 +460,36 @@ Detects a memory that cites a git SHA which no longer resolves in the repository
459
460
 
460
461
  ---
461
462
 
463
+ ### 2.13 session/large-read
464
+
465
+ Measures how much of a project's session context goes to **whole-file Reads of large files**. It is a baseline, not a defect report.
466
+
467
+ | Field | Value |
468
+ |---|---|
469
+ | **Rule ID** | `session/large-read` |
470
+ | **Severity** | info |
471
+ | **Trigger** | One or more whole-file `Read` calls (no `offset`, `limit` or `pages`) in the project transcript whose result is 4,000 tokens or more |
472
+ | **Message** | `<count> whole-file Read(s) of 4,000+ tokens (<tokens> tokens); est. <carry> tokens of cache-read carry on later turns` |
473
+ | **Source** | Claude Code transcript format; measured corpus (see Notes) |
474
+
475
+ **Detection algorithm:**
476
+
477
+ 1. Read the project transcript (see §3, "Data sources"). For each `Read` tool_use, record whether it was partial (`offset`, `limit` or `pages` set), and pair it with its tool_result to count the result's tokens.
478
+ 2. Count each session's turns as distinct assistant `message.id`s, falling back to `requestId`, then to one turn per record. Claude Code writes one API response as several records that share an id. Harness-written `<synthetic>` records and sidechain records are not turns. Record the turn count at each `compact_boundary`.
479
+ 3. Keep whole-file, non-error Reads whose result is at least 4,000 tokens. A Read that appears twice -- a continued session copies earlier records into its own file under a new session id -- is counted once, by its `tool_use` id.
480
+ 4. For each, carry = result tokens × the later turns of its session that re-sent it: from the turn after the Read to the session's last turn or its next compaction, whichever comes first.
481
+ 5. When anything qualified, emit ONE info finding per project with the count, total tokens, summed carry, and the top three files by tokens with their read counts. Emit nothing otherwise. When the transcript read was capped, say so in the finding.
482
+
483
+ **Notes:**
484
+ - Why it matters: a tool_result stays in the prompt of every later turn. With prompt caching each re-send bills as a cache read -- cheap per token, but paid per turn for the rest of the session. A large file read whole early in a long session is re-sent hundreds of times, when `grep -n` plus a ranged Read would have carried the few lines needed.
485
+ - It is a baseline for judging read-routing changes against, which is why it is one summary at `info` severity rather than a finding per Read. Reading a file whole is often correct.
486
+ - The threshold: across 739 whole-file Reads in a real corpus of 149 transcripts, Read output (line-number prefixes included) ran at a median 13.5 tokens per line, so 4,000 tokens is roughly 300 lines. An implementation counting with a chars/4 estimate instead measures 12.2 tokens per line on the same corpus, crossing the threshold at roughly 330 lines.
487
+ - Turn counting matters: counting records instead of message ids roughly doubles every carry (one measured transcript held 1,338 assistant records for 640 ids), and ignoring compaction overstates any session that compacted (25 of 150 transcripts in the same corpus did).
488
+ - The carry is an estimate. Tokens come from a proxy tokenizer -- cl100k where the implementation can load one, a chars/4 approximation otherwise, which is what the reference implementation's published bundle uses -- a result's first re-send is a cache write rather than a read, and a Read copied into a continued session is counted with the smaller of its two carries. Figures are not converted to cost, since token prices vary by model and change.
489
+ - Remediation: find the region with `grep -n` (or the Grep tool) and Read it with `offset`/`limit`, or delegate the whole-file question to a subagent, whose reads stay in its own context.
490
+
491
+ ---
492
+
462
493
  ## 3. Rule Catalog (machine-readable)
463
494
 
464
495
  A machine-readable JSON catalog of all rules is available at [`agent-session-lint-rules.json`](./agent-session-lint-rules.json). It conforms to the shared catalog schema ([`schemas/ctxlint-catalog.schema.json`](./schemas/ctxlint-catalog.schema.json)) used by all four pillars: each rule entry carries `id`, `category`, `severity`, `description`, `trigger`, `message`, `fixable`, and `stability`, plus rule-specific extras (e.g. `canonicalFiles` on `session/diverged-file`).
@@ -483,6 +514,7 @@ Catalog rule IDs use the pillar-stable `session/<slug>` form -- these are the cr
483
514
  | `session/unverified-gate-claimed-clean` | `session-unverified-gate-claimed-clean/unverified-gate-claimed-clean` |
484
515
  | `session/default-branch-accumulation` | `session-default-branch-accumulation/default-branch-accumulation` |
485
516
  | `session/unresolvable-sha` | `session-unresolvable-sha/unresolvable-sha` |
517
+ | `session/large-read` | `session-large-read/large-read` |
486
518
 
487
519
  ### Data sources: history vs. transcript
488
520
 
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
  [![GitHub stars](https://img.shields.io/github/stars/YawLabs/ctxlint)](https://github.com/YawLabs/ctxlint/stargazers)
6
6
  [![MCP Compliance](https://raw.githubusercontent.com/YawLabs/ctxlint/main/compliance-badge.svg)](https://github.com/YawLabs/mcp-compliance)
7
+ [![Follow @TokenLimitNews on X](https://img.shields.io/badge/follow-%40TokenLimitNews-000000?logo=x&logoColor=white)](https://x.com/TokenLimitNews)
7
8
 
8
9
  **Lint your AI agent context files, MCP server configs, and session data against your actual codebase.** Context linting + MCP config linting + session auditing. 16 AI tools, 8 MCP clients, cross-project consistency, auto-fix. Works as a CLI, CI step, pre-commit hook, or MCP server.
9
10
 
@@ -89,6 +90,11 @@ Useful if you want `ctxlint` available in every project without per-project setu
89
90
  | **Duplicate memory** | Near-duplicate memories across projects (>60% content overlap) |
90
91
  | **Loop detection** | Agent stuck in loops — repeated commands or cyclic patterns in session history |
91
92
  | **Memory overflow** | `MEMORY.md` past Claude Code's 200-line / 25KB session-load cap — entries beyond it are invisible to the agent |
93
+ | **Shared temp path** | A fixed temp path (e.g. `/tmp/pkg.bak`) the agent writes and later reads back — any concurrent session can overwrite it in between |
94
+ | **Unverified gate** | A lint/typecheck/test/build run that errored or printed nothing, followed by agent prose claiming it passed |
95
+ | **Default-branch edits** | 10+ files edited on `main`/`master` with no intervening commit or branch-away |
96
+ | **Unresolvable SHA** | A memory cites a commit SHA that does not resolve in this repository |
97
+ | **Large reads** | Whole-file Reads of 4,000+ tokens, with an estimate of the tokens they re-send as cached context on later turns (an info-level baseline) |
92
98
 
93
99
  ## Supported Context Files
94
100
 
@@ -201,6 +207,8 @@ Session checks are **opt-in** because they access files outside the project dire
201
207
  | Claude Code | `~/.claude/history.jsonl` | `~/.claude/projects/*/memory/*.md` |
202
208
  | Codex CLI | `~/.codex/history.jsonl` | — |
203
209
 
210
+ Checks whose signal is what the agent did (commands run, files written or read) also read the current project's Claude Code session transcripts, `~/.claude/projects/<encoded-project>/*.jsonl`, bounded to the 5 most recent.
211
+
204
212
  ### What session checks catch
205
213
 
206
214
  | Check | What it finds |
@@ -212,6 +220,11 @@ Session checks are **opt-in** because they access files outside the project dire
212
220
  | **Duplicate memory** | Near-duplicate memory entries across projects (>60% overlap) |
213
221
  | **Loop detection** | Agent stuck in a loop — 3+ consecutive identical commands, or cyclic A,B,A,B patterns |
214
222
  | **Memory index overflow** | `MEMORY.md` exceeds Claude Code's documented 200-line / 25KB session-load cap, so entries past the cap are invisible to the agent |
223
+ | **Shared temp path** | A fixed temp path (e.g. `/tmp/pkg.bak`) the agent writes and later reads back — any concurrent session can overwrite it in between |
224
+ | **Unverified gate** | A lint/typecheck/test/build run that errored or printed nothing, followed by agent prose claiming it passed |
225
+ | **Default-branch edits** | 10+ files edited on `main`/`master` with no intervening commit or branch-away |
226
+ | **Unresolvable SHA** | A memory cites a commit SHA that does not resolve in this repository |
227
+ | **Large reads** | Whole-file Reads of 4,000+ tokens, with an estimate of the tokens they re-send as cached context on later turns (an info-level baseline) |
215
228
 
216
229
  ### Session Linting Specification
217
230
 
@@ -221,7 +234,7 @@ Session checks are **opt-in** because they access files outside the project dire
221
234
  ## Example Output
222
235
 
223
236
  ```
224
- ctxlint v0.9.10
237
+ ctxlint v0.25.0
225
238
 
226
239
  Scanning /Users/you/my-app...
227
240
 
@@ -282,7 +295,7 @@ Commands:
282
295
  init Set up a git pre-commit hook
283
296
  ```
284
297
 
285
- **Available checks:** `paths`, `commands`, `staleness`, `tokens`, `tier-tokens`, `redundancy`, `contradictions`, `frontmatter`, `ci-coverage`, `ci-secrets`, `content-secrets`, `hook-coverage`, `mcp-schema`, `mcp-security`, `mcp-commands`, `mcp-deprecated`, `mcp-env`, `mcp-urls`, `mcp-consistency`, `mcp-redundancy`, `session-missing-secret`, `session-diverged-file`, `session-missing-workflow`, `session-stale-memory`, `session-duplicate-memory`, `session-loop-detection`, `session-memory-index-overflow`, `skill-frontmatter`, `skill-broken-ref`, `skill-trigger-collision`, `skill-orphaned`, `skill-dead-tool-restriction`
298
+ **Available checks:** `paths`, `commands`, `staleness`, `tokens`, `tier-tokens`, `redundancy`, `contradictions`, `frontmatter`, `ci-coverage`, `ci-secrets`, `content-secrets`, `hook-coverage`, `mcp-schema`, `mcp-security`, `mcp-commands`, `mcp-deprecated`, `mcp-env`, `mcp-urls`, `mcp-consistency`, `mcp-redundancy`, `session-missing-secret`, `session-diverged-file`, `session-missing-workflow`, `session-stale-memory`, `session-duplicate-memory`, `session-loop-detection`, `session-memory-index-overflow`, `session-shared-temp-path`, `session-unverified-gate-claimed-clean`, `session-default-branch-accumulation`, `session-unresolvable-sha`, `session-large-read`, `skill-frontmatter`, `skill-broken-ref`, `skill-trigger-collision`, `skill-orphaned`, `skill-dead-tool-restriction`
286
299
 
287
300
  Passing any `mcp-*` check name implies `--mcp`. Passing any `session-*` check name implies `--session`. Passing any `skill-*` check name implies `--skills`.
288
301
 
@@ -364,7 +377,7 @@ Add to your `.pre-commit-config.yaml`:
364
377
  ```yaml
365
378
  repos:
366
379
  - repo: https://github.com/yawlabs/ctxlint
367
- rev: v0.9.10
380
+ rev: v0.25.0
368
381
  hooks:
369
382
  - id: ctxlint
370
383
  ```
@@ -519,7 +532,7 @@ ctxlint is the reference implementation of four open specifications for linting
519
532
  | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
520
533
  | **[AI Context File Linting Spec](./CONTEXT_LINT_SPEC.md)** | 41 rules for validating context files (CLAUDE.md, .cursorrules, AGENTS.md, etc.) across 16 clients. Covers file formats, frontmatter schemas, path/command validation, staleness, token budgets, redundancy, and contradictions. |
521
534
  | **[MCP Config Linting Spec](./MCP_CONFIG_LINT_SPEC.md)** | 29 rules for validating MCP server configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, etc.) across 8 clients. Covers schema validation, hardcoded secrets, env var syntax, deprecated transports, and cross-file consistency. |
522
- | **[Agent Session Linting Spec](./AGENT_SESSION_LINT_SPEC.md)** | 12 rules for auditing agent session data (history, memory) across 8 agents. Covers cross-project secret consistency, config drift, stale memory, and loop detection. |
535
+ | **[Agent Session Linting Spec](./AGENT_SESSION_LINT_SPEC.md)** | 13 rules for auditing agent session data (history, memory) across 8 agents. Covers cross-project secret consistency, config drift, stale memory, and loop detection. |
523
536
  | **[Agent Skill Linting Spec](./AGENT_SKILL_LINT_SPEC.md)** | 5 rules for auditing Claude Code skill (`SKILL.md`) and agent (`.md`) definitions under `~/.claude`. Covers frontmatter presence, broken refs, trigger-phrase collisions, orphaned skills, and dead tool restrictions. (v1, experimental) |
524
537
 
525
538
  All specs include machine-readable rule catalogs for programmatic consumption:
@@ -144,6 +144,16 @@
144
144
  "message": "cited commit {sha} does not resolve in this repository",
145
145
  "fixable": false,
146
146
  "stability": "experimental"
147
+ },
148
+ {
149
+ "id": "session/large-read",
150
+ "category": "session",
151
+ "severity": "info",
152
+ "description": "Measures session context spent on whole-file Reads of large files. Each result is re-sent as prompt context (cache reads) on every later turn of its session; the finding is a baseline for judging read-routing changes, reported as one summary per project.",
153
+ "trigger": "One or more whole-file Read calls (no offset, limit or pages) in the current project's transcripts whose result is 4,000 tokens or more. Carry is each result's tokens times the later turns of its session that re-sent it, up to the session's end or its next /compact boundary; a turn is a distinct assistant message id.",
154
+ "message": "{count} whole-file Reads of 4,000+ tokens ({tokens} tokens); est. {carry} tokens of cache-read carry on later turns",
155
+ "fixable": false,
156
+ "stability": "experimental"
147
157
  }
148
158
  ],
149
159
  "dataSources": [
package/dist/index.js CHANGED
@@ -27551,6 +27551,20 @@ import { readdir as readdir3 } from "node:fs/promises";
27551
27551
  import { homedir as homedir6 } from "node:os";
27552
27552
  import { join as join9 } from "node:path";
27553
27553
  import { createInterface as createInterface2 } from "node:readline";
27554
+ function emptyRead() {
27555
+ return {
27556
+ events: [],
27557
+ filesRead: 0,
27558
+ truncated: false,
27559
+ sessionTurns: /* @__PURE__ */ new Map(),
27560
+ sessionCompactions: /* @__PURE__ */ new Map()
27561
+ };
27562
+ }
27563
+ function turnsCarried(read, ev) {
27564
+ const total = read.sessionTurns.get(ev.sessionId) ?? ev.turn;
27565
+ const boundary = read.sessionCompactions.get(ev.sessionId)?.find((b2) => b2 >= ev.turn);
27566
+ return Math.max(0, (boundary ?? total) - ev.turn);
27567
+ }
27554
27568
  function asString(v2) {
27555
27569
  return typeof v2 === "string" ? v2 : "";
27556
27570
  }
@@ -27571,14 +27585,17 @@ function readProjectTranscript(project, home2 = resolveHome()) {
27571
27585
  }
27572
27586
  return hit;
27573
27587
  }
27588
+ function clearTranscriptCache() {
27589
+ cache.clear();
27590
+ }
27574
27591
  function candidateDirs(project, home2) {
27575
27592
  const root = join9(home2, ".claude", "projects");
27576
27593
  return projectDirCandidates(project).map((n7) => join9(root, n7)).filter((d) => existsSync5(d));
27577
27594
  }
27578
27595
  async function readUncached(project, home2) {
27579
- if (!home2 || !project) return EMPTY;
27596
+ if (!home2 || !project) return emptyRead();
27580
27597
  const dirs = candidateDirs(project, home2);
27581
- if (dirs.length === 0) return EMPTY;
27598
+ if (dirs.length === 0) return emptyRead();
27582
27599
  const names = [];
27583
27600
  for (const dir of dirs) {
27584
27601
  for (const name of await readdir3(dir).catch(() => [])) {
@@ -27595,8 +27612,7 @@ async function readUncached(project, home2) {
27595
27612
  }).filter((f) => f !== null).sort((a, b2) => b2.mtime - a.mtime);
27596
27613
  let truncated = files.length > MAX_TRANSCRIPTS;
27597
27614
  const selected = files.slice(0, MAX_TRANSCRIPTS);
27598
- const events = [];
27599
- const pending = /* @__PURE__ */ new Map();
27615
+ const st2 = { events: [], pending: /* @__PURE__ */ new Map(), sessions: /* @__PURE__ */ new Map(), anonymous: 0 };
27600
27616
  let lines = 0;
27601
27617
  for (const { p: p2 } of selected) {
27602
27618
  if (lines >= MAX_LINES2) {
@@ -27621,21 +27637,75 @@ async function readUncached(project, home2) {
27621
27637
  } catch {
27622
27638
  continue;
27623
27639
  }
27624
- collect(rec, events, pending);
27640
+ collect(rec, st2);
27625
27641
  }
27626
27642
  } catch {
27627
27643
  } finally {
27628
27644
  rl.close();
27629
27645
  }
27630
27646
  }
27631
- return { events, filesRead: selected.length, truncated };
27647
+ const sessionTurns = /* @__PURE__ */ new Map();
27648
+ const sessionCompactions = /* @__PURE__ */ new Map();
27649
+ for (const [id, s] of st2.sessions) {
27650
+ sessionTurns.set(id, s.ordinals.size);
27651
+ if (s.compactions.length > 0) {
27652
+ sessionCompactions.set(
27653
+ id,
27654
+ [...s.compactions].sort((a, b2) => a - b2)
27655
+ );
27656
+ }
27657
+ }
27658
+ return {
27659
+ events: st2.events,
27660
+ filesRead: selected.length,
27661
+ truncated,
27662
+ sessionTurns,
27663
+ sessionCompactions
27664
+ };
27665
+ }
27666
+ function sessionState(st2, sessionId) {
27667
+ let s = st2.sessions.get(sessionId);
27668
+ if (!s) {
27669
+ s = { ordinals: /* @__PURE__ */ new Map(), compactions: [] };
27670
+ st2.sessions.set(sessionId, s);
27671
+ }
27672
+ return s;
27632
27673
  }
27633
- function collect(rec, events, pending) {
27674
+ function turnOf(rec, message, st2, sessionId) {
27675
+ const s = sessionState(st2, sessionId);
27676
+ if (message?.model === "<synthetic>" || rec.isSidechain === true) return s.ordinals.size;
27677
+ const key = asString(message?.id) || asString(rec.requestId) || `#anonymous:${++st2.anonymous}`;
27678
+ let ordinal = s.ordinals.get(key);
27679
+ if (ordinal === void 0) {
27680
+ ordinal = s.ordinals.size + 1;
27681
+ s.ordinals.set(key, ordinal);
27682
+ }
27683
+ return ordinal;
27684
+ }
27685
+ function isSet(v2) {
27686
+ return v2 !== void 0 && v2 !== null && v2 !== "";
27687
+ }
27688
+ function readResultLines(rec, path21) {
27689
+ const result = rec.toolUseResult;
27690
+ if (!result || typeof result !== "object") return void 0;
27691
+ const file2 = result.file;
27692
+ if (!file2 || typeof file2 !== "object") return void 0;
27693
+ const { filePath, numLines } = file2;
27694
+ if (filePath !== path21) return void 0;
27695
+ return typeof numLines === "number" ? numLines : void 0;
27696
+ }
27697
+ function collect(rec, st2) {
27698
+ const sessionId = asString(rec.sessionId) || asString(rec.session_id);
27699
+ if (rec.type === "system" && rec.subtype === "compact_boundary") {
27700
+ const s = sessionState(st2, sessionId);
27701
+ s.compactions.push(s.ordinals.size);
27702
+ return;
27703
+ }
27634
27704
  const message = rec.message;
27705
+ const turn = rec.type === "assistant" ? turnOf(rec, message, st2, sessionId) : st2.sessions.get(sessionId)?.ordinals.size ?? 0;
27635
27706
  const content = message?.content;
27636
27707
  if (!Array.isArray(content)) return;
27637
27708
  const timestamp = Date.parse(asString(rec.timestamp)) || 0;
27638
- const sessionId = asString(rec.sessionId) || asString(rec.session_id);
27639
27709
  const gitBranch = asString(rec.gitBranch) || void 0;
27640
27710
  for (const raw of content) {
27641
27711
  if (!raw || typeof raw !== "object") continue;
@@ -27644,7 +27714,15 @@ function collect(rec, events, pending) {
27644
27714
  if (type === "text" && rec.type === "assistant") {
27645
27715
  const text = asString(block.text);
27646
27716
  if (text) {
27647
- events.push({ kind: "assistant-text", text, tool: "", gitBranch, timestamp, sessionId });
27717
+ st2.events.push({
27718
+ kind: "assistant-text",
27719
+ text,
27720
+ tool: "",
27721
+ gitBranch,
27722
+ turn,
27723
+ timestamp,
27724
+ sessionId
27725
+ });
27648
27726
  }
27649
27727
  continue;
27650
27728
  }
@@ -27653,49 +27731,76 @@ function collect(rec, events, pending) {
27653
27731
  const input = block.input ?? {};
27654
27732
  const cmdField = COMMAND_TOOLS[tool];
27655
27733
  const writeField = WRITE_TOOLS[tool];
27734
+ const readField = READ_TOOLS[tool];
27656
27735
  let ev = null;
27657
27736
  if (cmdField) {
27658
27737
  const text = asString(input[cmdField]);
27659
- if (text) ev = { kind: "command", text, tool, gitBranch, timestamp, sessionId };
27738
+ if (text) ev = { kind: "command", text, tool, gitBranch, turn, timestamp, sessionId };
27660
27739
  } else if (writeField) {
27661
27740
  const text = asString(input[writeField]);
27662
- if (text) ev = { kind: "file-write", text, tool, gitBranch, timestamp, sessionId };
27741
+ if (text) ev = { kind: "file-write", text, tool, gitBranch, turn, timestamp, sessionId };
27742
+ } else if (readField) {
27743
+ const text = asString(input[readField]);
27744
+ if (text) {
27745
+ ev = {
27746
+ kind: "file-read",
27747
+ text,
27748
+ tool,
27749
+ partial: PARTIAL_READ_FIELDS.some((f) => isSet(input[f])),
27750
+ gitBranch,
27751
+ turn,
27752
+ timestamp,
27753
+ sessionId
27754
+ };
27755
+ }
27663
27756
  }
27664
27757
  if (ev) {
27665
- events.push(ev);
27666
27758
  const id = asString(block.id);
27667
- if (id) pending.set(id, ev);
27759
+ if (id) ev.toolUseId = id;
27760
+ st2.events.push(ev);
27761
+ if (id) st2.pending.set(id, ev);
27668
27762
  }
27669
27763
  continue;
27670
27764
  }
27671
27765
  if (type === "tool_result") {
27672
27766
  const id = asString(block.tool_use_id);
27673
- const ev = id ? pending.get(id) : void 0;
27767
+ const ev = id ? st2.pending.get(id) : void 0;
27674
27768
  if (!ev) continue;
27675
- pending.delete(id);
27769
+ st2.pending.delete(id);
27770
+ const out = resultText(block.content);
27676
27771
  ev.isError = block.is_error === true;
27677
- ev.emptyOutput = resultText(block.content).trim().length === 0;
27772
+ ev.emptyOutput = out.trim().length === 0;
27773
+ ev.outputChars = out.length;
27774
+ if (ev.kind === "file-read") {
27775
+ ev.outputTokens = countTokens(out);
27776
+ const lineCount = readResultLines(rec, ev.text);
27777
+ if (lineCount !== void 0) ev.outputLines = lineCount;
27778
+ }
27678
27779
  }
27679
27780
  }
27680
27781
  }
27681
- var WRITE_TOOLS, COMMAND_TOOLS, MAX_TRANSCRIPTS, MAX_LINES2, EMPTY, cache;
27782
+ var WRITE_TOOLS, READ_TOOLS, PARTIAL_READ_FIELDS, COMMAND_TOOLS, MAX_TRANSCRIPTS, MAX_LINES2, cache;
27682
27783
  var init_transcript = __esm({
27683
27784
  "src/core/transcript.ts"() {
27684
27785
  "use strict";
27685
27786
  init_define_WEB_FIRST_SEGMENTS();
27787
+ init_tokens();
27686
27788
  init_session_parser();
27687
27789
  WRITE_TOOLS = {
27688
27790
  Write: "file_path",
27689
27791
  Edit: "file_path",
27690
27792
  NotebookEdit: "notebook_path"
27691
27793
  };
27794
+ READ_TOOLS = {
27795
+ Read: "file_path"
27796
+ };
27797
+ PARTIAL_READ_FIELDS = ["offset", "limit", "pages"];
27692
27798
  COMMAND_TOOLS = {
27693
27799
  Bash: "command",
27694
27800
  PowerShell: "command"
27695
27801
  };
27696
27802
  MAX_TRANSCRIPTS = 5;
27697
27803
  MAX_LINES2 = 2e5;
27698
- EMPTY = { events: [], filesRead: 0, truncated: false };
27699
27804
  cache = /* @__PURE__ */ new Map();
27700
27805
  }
27701
27806
  });
@@ -27833,7 +27938,7 @@ function isUnverified(ev) {
27833
27938
  async function checkUnverifiedGateClaimedClean(ctx) {
27834
27939
  const { events } = await readProjectTranscript(ctx.currentProject);
27835
27940
  if (events.length === 0) return [];
27836
- const ordered = [...events].sort((a, b2) => a.timestamp - b2.timestamp);
27941
+ const ordered = events.filter((e) => e.kind !== "file-read").sort((a, b2) => a.timestamp - b2.timestamp);
27837
27942
  const issues = [];
27838
27943
  const reported = /* @__PURE__ */ new Set();
27839
27944
  for (let i2 = 0; i2 < ordered.length; i2++) {
@@ -27914,7 +28019,7 @@ function isBranchAway(cmd) {
27914
28019
  async function checkDefaultBranchAccumulation(ctx) {
27915
28020
  const { events } = await readProjectTranscript(ctx.currentProject);
27916
28021
  if (events.length === 0) return [];
27917
- const ordered = [...events].sort((a, b2) => a.timestamp - b2.timestamp);
28022
+ const ordered = events.filter((e) => e.kind !== "file-read").sort((a, b2) => a.timestamp - b2.timestamp);
27918
28023
  const pending = /* @__PURE__ */ new Set();
27919
28024
  let branch = "";
27920
28025
  let firstWrite = "";
@@ -28058,6 +28163,93 @@ var init_unresolvable_sha = __esm({
28058
28163
  }
28059
28164
  });
28060
28165
 
28166
+ // src/core/checks/session/large-read.ts
28167
+ import { isAbsolute as isAbsolute6, relative as relative6 } from "node:path";
28168
+ function qualifies(ev) {
28169
+ if (ev.kind !== "file-read" || ev.partial || ev.isError) return false;
28170
+ return (ev.outputTokens ?? 0) >= LARGE_READ_TOKENS;
28171
+ }
28172
+ function fmt(n7) {
28173
+ return n7.toLocaleString("en-US");
28174
+ }
28175
+ function displayPath(path21, project) {
28176
+ const rel = relative6(project, path21);
28177
+ if (rel && !rel.startsWith("..") && !isAbsolute6(rel)) return rel.replace(/\\/g, "/");
28178
+ return path21;
28179
+ }
28180
+ function fileKey(path21) {
28181
+ const slashed = path21.replace(/\\/g, "/");
28182
+ return process.platform === "win32" ? slashed.toLowerCase() : slashed;
28183
+ }
28184
+ async function checkLargeRead(ctx) {
28185
+ const read = await readProjectTranscript(ctx.currentProject);
28186
+ const byCall = /* @__PURE__ */ new Map();
28187
+ const unkeyed = [];
28188
+ for (const ev of read.events) {
28189
+ if (!qualifies(ev)) continue;
28190
+ const hit = { ev, tokens: ev.outputTokens ?? 0, carried: turnsCarried(read, ev) };
28191
+ if (!ev.toolUseId) {
28192
+ unkeyed.push(hit);
28193
+ continue;
28194
+ }
28195
+ const prev = byCall.get(ev.toolUseId);
28196
+ if (!prev || hit.carried < prev.carried) byCall.set(ev.toolUseId, hit);
28197
+ }
28198
+ const hits = [...byCall.values(), ...unkeyed];
28199
+ if (hits.length === 0) return [];
28200
+ let totalTokens = 0;
28201
+ let carry = 0;
28202
+ const files = /* @__PURE__ */ new Map();
28203
+ for (const { ev, tokens, carried } of hits) {
28204
+ totalTokens += tokens;
28205
+ carry += tokens * carried;
28206
+ const key = fileKey(ev.text);
28207
+ const file2 = files.get(key) ?? { path: ev.text, reads: 0, tokens: 0 };
28208
+ file2.reads += 1;
28209
+ file2.tokens += tokens;
28210
+ if (ev.outputLines !== void 0) file2.lines = Math.max(file2.lines ?? 0, ev.outputLines);
28211
+ files.set(key, file2);
28212
+ }
28213
+ const top = [...files.values()].sort((a, b2) => b2.tokens - a.tokens || a.path.localeCompare(b2.path)).slice(0, TOP_FILES);
28214
+ const topLines = top.map((f) => {
28215
+ const reads = `${f.reads} read${f.reads === 1 ? "" : "s"}`;
28216
+ const lines = f.lines !== void 0 ? `, ${fmt(f.lines)} line${f.lines === 1 ? "" : "s"}` : "";
28217
+ return ` ${displayPath(f.path, ctx.currentProject)} -- ${reads}, ${fmt(f.tokens)} tokens${lines}`;
28218
+ });
28219
+ const count = hits.length;
28220
+ const detail = [
28221
+ `Largest files by tokens read whole:`,
28222
+ ...topLines,
28223
+ `Carry = each Read's result tokens x the later turns of its session that re-sent it (to the session's end or its next /compact). An estimate: tokens are counted with a proxy tokenizer, and a result's first re-send is a cache write rather than a read.`
28224
+ ];
28225
+ if (read.truncated) {
28226
+ detail.push(
28227
+ `Transcript read was capped (${read.filesRead} most recent transcripts, bounded line count): these figures cover only what was read, so the real totals are higher.`
28228
+ );
28229
+ }
28230
+ return [
28231
+ {
28232
+ severity: "info",
28233
+ check: "session-large-read",
28234
+ ruleId: "session-large-read/large-read",
28235
+ line: 0,
28236
+ message: `${count} whole-file Read${count === 1 ? "" : "s"} of ${fmt(LARGE_READ_TOKENS)}+ tokens (${fmt(totalTokens)} tokens); est. ${fmt(carry)} tokens of cache-read carry on later turns`,
28237
+ detail: detail.join("\n"),
28238
+ suggestion: "Before reading a large file whole, find the part you need with `grep -n` (or the Grep tool) and Read just that range with `offset`/`limit`. For a question that needs the whole file, delegate it to a subagent: its reads stay in its own context and only the answer comes back."
28239
+ }
28240
+ ];
28241
+ }
28242
+ var LARGE_READ_TOKENS, TOP_FILES;
28243
+ var init_large_read = __esm({
28244
+ "src/core/checks/session/large-read.ts"() {
28245
+ "use strict";
28246
+ init_define_WEB_FIRST_SEGMENTS();
28247
+ init_transcript();
28248
+ LARGE_READ_TOKENS = 4e3;
28249
+ TOP_FILES = 3;
28250
+ }
28251
+ });
28252
+
28061
28253
  // src/core/checks/ci-coverage.ts
28062
28254
  import { readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
28063
28255
  import { join as join10 } from "node:path";
@@ -29108,7 +29300,7 @@ import { readFileSync as readFileSync8 } from "node:fs";
29108
29300
  import { resolve as resolve15, dirname as dirname7 } from "node:path";
29109
29301
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29110
29302
  function loadVersion() {
29111
- if (true) return "0.24.1";
29303
+ if (true) return "0.25.1";
29112
29304
  try {
29113
29305
  const __dir = dirname7(fileURLToPath2(import.meta.url));
29114
29306
  const pkgPath = resolve15(__dir, "../package.json");
@@ -29357,6 +29549,8 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
29357
29549
  sessionPromises.push(checkDefaultBranchAccumulation(sessionCtx));
29358
29550
  if (sessionChecksToRun.includes("session-unresolvable-sha"))
29359
29551
  sessionPromises.push(checkUnresolvableSha(sessionCtx));
29552
+ if (sessionChecksToRun.includes("session-large-read"))
29553
+ sessionPromises.push(checkLargeRead(sessionCtx));
29360
29554
  const sessionResults = await Promise.all(sessionPromises);
29361
29555
  const sessionIssues = sessionResults.flat();
29362
29556
  fileResults.push({
@@ -29562,6 +29756,7 @@ var init_audit = __esm({
29562
29756
  init_unverified_gate_claimed_clean();
29563
29757
  init_default_branch_accumulation();
29564
29758
  init_unresolvable_sha();
29759
+ init_large_read();
29565
29760
  init_ci_coverage();
29566
29761
  init_ci_secrets();
29567
29762
  init_content_secrets();
@@ -29607,7 +29802,8 @@ var init_audit = __esm({
29607
29802
  "session-shared-temp-path",
29608
29803
  "session-unverified-gate-claimed-clean",
29609
29804
  "session-default-branch-accumulation",
29610
- "session-unresolvable-sha"
29805
+ "session-unresolvable-sha",
29806
+ "session-large-read"
29611
29807
  ];
29612
29808
  ALL_SKILL_CHECKS = [
29613
29809
  "skill-frontmatter",
@@ -56870,49 +57066,49 @@ var require_fast_uri = __commonJS({
56870
57066
  schemelessOptions.skipEscape = true;
56871
57067
  return serialize(resolved, schemelessOptions);
56872
57068
  }
56873
- function resolveComponent(base, relative8, options, skipNormalization) {
57069
+ function resolveComponent(base, relative9, options, skipNormalization) {
56874
57070
  const target = {};
56875
57071
  if (!skipNormalization) {
56876
57072
  base = parse5(serialize(base, options), options);
56877
- relative8 = parse5(serialize(relative8, options), options);
57073
+ relative9 = parse5(serialize(relative9, options), options);
56878
57074
  }
56879
57075
  options = options || {};
56880
- if (!options.tolerant && relative8.scheme) {
56881
- target.scheme = relative8.scheme;
56882
- target.userinfo = relative8.userinfo;
56883
- target.host = relative8.host;
56884
- target.port = relative8.port;
56885
- target.path = removeDotSegments(relative8.path || "");
56886
- target.query = relative8.query;
57076
+ if (!options.tolerant && relative9.scheme) {
57077
+ target.scheme = relative9.scheme;
57078
+ target.userinfo = relative9.userinfo;
57079
+ target.host = relative9.host;
57080
+ target.port = relative9.port;
57081
+ target.path = removeDotSegments(relative9.path || "");
57082
+ target.query = relative9.query;
56887
57083
  } else {
56888
- if (relative8.userinfo !== void 0 || relative8.host !== void 0 || relative8.port !== void 0) {
56889
- target.userinfo = relative8.userinfo;
56890
- target.host = relative8.host;
56891
- target.port = relative8.port;
56892
- target.path = removeDotSegments(relative8.path || "");
56893
- target.query = relative8.query;
57084
+ if (relative9.userinfo !== void 0 || relative9.host !== void 0 || relative9.port !== void 0) {
57085
+ target.userinfo = relative9.userinfo;
57086
+ target.host = relative9.host;
57087
+ target.port = relative9.port;
57088
+ target.path = removeDotSegments(relative9.path || "");
57089
+ target.query = relative9.query;
56894
57090
  } else {
56895
- if (!relative8.path) {
57091
+ if (!relative9.path) {
56896
57092
  target.path = base.path;
56897
- if (relative8.query !== void 0) {
56898
- target.query = relative8.query;
57093
+ if (relative9.query !== void 0) {
57094
+ target.query = relative9.query;
56899
57095
  } else {
56900
57096
  target.query = base.query;
56901
57097
  }
56902
57098
  } else {
56903
- if (relative8.path[0] === "/") {
56904
- target.path = removeDotSegments(relative8.path);
57099
+ if (relative9.path[0] === "/") {
57100
+ target.path = removeDotSegments(relative9.path);
56905
57101
  } else {
56906
57102
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
56907
- target.path = "/" + relative8.path;
57103
+ target.path = "/" + relative9.path;
56908
57104
  } else if (!base.path) {
56909
- target.path = relative8.path;
57105
+ target.path = relative9.path;
56910
57106
  } else {
56911
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative8.path;
57107
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative9.path;
56912
57108
  }
56913
57109
  target.path = removeDotSegments(target.path);
56914
57110
  }
56915
- target.query = relative8.query;
57111
+ target.query = relative9.query;
56916
57112
  }
56917
57113
  target.userinfo = base.userinfo;
56918
57114
  target.host = base.host;
@@ -56920,7 +57116,7 @@ var require_fast_uri = __commonJS({
56920
57116
  }
56921
57117
  target.scheme = base.scheme;
56922
57118
  }
56923
- target.fragment = relative8.fragment;
57119
+ target.fragment = relative9.fragment;
56924
57120
  return target;
56925
57121
  }
56926
57122
  function equal(uriA, uriB, options) {
@@ -59407,11 +59603,11 @@ var require_format = __commonJS({
59407
59603
  }
59408
59604
  function getFormat(fmtDef) {
59409
59605
  const code = fmtDef instanceof RegExp ? (0, codegen_1.regexpCode)(fmtDef) : opts.code.formats ? (0, codegen_1._)`${opts.code.formats}${(0, codegen_1.getProperty)(schema)}` : void 0;
59410
- const fmt = gen.scopeValue("formats", { key: schema, ref: fmtDef, code });
59606
+ const fmt2 = gen.scopeValue("formats", { key: schema, ref: fmtDef, code });
59411
59607
  if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) {
59412
- return [fmtDef.type || "string", fmtDef.validate, (0, codegen_1._)`${fmt}.validate`];
59608
+ return [fmtDef.type || "string", fmtDef.validate, (0, codegen_1._)`${fmt2}.validate`];
59413
59609
  }
59414
- return ["string", fmtDef, fmt];
59610
+ return ["string", fmtDef, fmt2];
59415
59611
  }
59416
59612
  function validCondition() {
59417
59613
  if (typeof formatDef == "object" && !(formatDef instanceof RegExp) && formatDef.async) {
@@ -60081,8 +60277,8 @@ var require_limit = __commonJS({
60081
60277
  ref: self2.formats,
60082
60278
  code: opts.code.formats
60083
60279
  });
60084
- const fmt = gen.const("fmt", (0, codegen_1._)`${fmts}[${fCxt.schemaCode}]`);
60085
- cxt.fail$data((0, codegen_1.or)((0, codegen_1._)`typeof ${fmt} != "object"`, (0, codegen_1._)`${fmt} instanceof RegExp`, (0, codegen_1._)`typeof ${fmt}.compare != "function"`, compareCode(fmt)));
60280
+ const fmt2 = gen.const("fmt", (0, codegen_1._)`${fmts}[${fCxt.schemaCode}]`);
60281
+ cxt.fail$data((0, codegen_1.or)((0, codegen_1._)`typeof ${fmt2} != "object"`, (0, codegen_1._)`${fmt2} instanceof RegExp`, (0, codegen_1._)`typeof ${fmt2}.compare != "function"`, compareCode(fmt2)));
60086
60282
  }
60087
60283
  function validateFormat() {
60088
60284
  const format3 = fCxt.schema;
@@ -60092,15 +60288,15 @@ var require_limit = __commonJS({
60092
60288
  if (typeof fmtDef != "object" || fmtDef instanceof RegExp || typeof fmtDef.compare != "function") {
60093
60289
  throw new Error(`"${keyword}": format "${format3}" does not define "compare" function`);
60094
60290
  }
60095
- const fmt = gen.scopeValue("formats", {
60291
+ const fmt2 = gen.scopeValue("formats", {
60096
60292
  key: format3,
60097
60293
  ref: fmtDef,
60098
60294
  code: opts.code.formats ? (0, codegen_1._)`${opts.code.formats}${(0, codegen_1.getProperty)(format3)}` : void 0
60099
60295
  });
60100
- cxt.fail$data(compareCode(fmt));
60296
+ cxt.fail$data(compareCode(fmt2));
60101
60297
  }
60102
- function compareCode(fmt) {
60103
- return (0, codegen_1._)`${fmt}.compare(${data}, ${schemaCode}) ${KWDs[keyword].fail} 0`;
60298
+ function compareCode(fmt2) {
60299
+ return (0, codegen_1._)`${fmt2}.compare(${data}, ${schemaCode}) ${KWDs[keyword].fail} 0`;
60104
60300
  }
60105
60301
  },
60106
60302
  dependencies: ["format"]
@@ -62825,6 +63021,7 @@ var init_server4 = __esm({
62825
63021
  init_tokens();
62826
63022
  init_git();
62827
63023
  init_paths();
63024
+ init_transcript();
62828
63025
  init_version2();
62829
63026
  contextCheckEnum = external_exports.enum(ALL_CHECKS);
62830
63027
  mcpCheckEnum = external_exports.enum(ALL_MCP_CHECKS);
@@ -63077,7 +63274,7 @@ var init_server4 = __esm({
63077
63274
  );
63078
63275
  server.tool(
63079
63276
  "ctxlint_session_audit",
63080
- "Audit AI agent session data for cross-project consistency. Checks for missing GitHub secrets, diverged config files, missing workflows, stale memory entries, and duplicate memories across sibling repositories.",
63277
+ "Audit AI agent session data for cross-project consistency and session hazards. Checks for missing GitHub secrets, diverged config files and missing workflows across sibling repositories; stale, duplicate or overflowing memory entries; command loops; and, from Claude Code transcripts, shared temp paths, gates claimed clean after failing, edits piling up on the default branch, unresolvable commit SHAs in memory, and large whole-file Reads re-sent as context on later turns.",
63081
63278
  {
63082
63279
  projectPath: external_exports.string().optional().describe("Path to the project root. Defaults to current working directory."),
63083
63280
  checks: external_exports.array(sessionCheckEnum).optional().describe("Specific session checks to run (default: all session-* checks).")
@@ -63110,6 +63307,7 @@ var init_server4 = __esm({
63110
63307
  resetGit();
63111
63308
  resetPathsCache();
63112
63309
  resetPackageJsonCache();
63310
+ clearTranscriptCache();
63113
63311
  }
63114
63312
  }
63115
63313
  );
@@ -70128,6 +70326,13 @@ function buildRuleDescriptors() {
70128
70326
  },
70129
70327
  helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
70130
70328
  },
70329
+ {
70330
+ id: "ctxlint/session-large-read",
70331
+ shortDescription: {
70332
+ text: "Whole-file Reads of large files re-sent as context on every later turn"
70333
+ },
70334
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
70335
+ },
70131
70336
  {
70132
70337
  id: "ctxlint/skill-frontmatter",
70133
70338
  shortDescription: { text: "Skill/agent definition missing required frontmatter" },
@@ -70362,6 +70567,7 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
70362
70567
  resetGit();
70363
70568
  resetPathsCache();
70364
70569
  resetPackageJsonCache();
70570
+ clearTranscriptCache();
70365
70571
  }
70366
70572
  if (opts.watch) {
70367
70573
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
@@ -70458,6 +70664,7 @@ Fixed ${applied.totalFixes} issue${applied.totalFixes !== 1 ? "s" : ""} in ${app
70458
70664
  resetPathsCache();
70459
70665
  resetPackageJsonCache();
70460
70666
  clearFileCache();
70667
+ clearTranscriptCache();
70461
70668
  }
70462
70669
  console.log(chalk2.dim("\nWatching for changes... (Ctrl+C to stop)\n"));
70463
70670
  }, 300);
@@ -70651,6 +70858,7 @@ var init_cli = __esm({
70651
70858
  init_ora();
70652
70859
  init_paths();
70653
70860
  init_cache();
70861
+ init_transcript();
70654
70862
  init_reporter();
70655
70863
  init_fixer();
70656
70864
  init_tokens();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@yawlabs/ctxlint",
3
- "version": "0.24.1",
3
+ "version": "0.25.1",
4
4
  "mcpName": "io.github.YawLabs/ctxlint",
5
- "description": "Lint your AI agent context files, MCP server configs, and session data against your actual codebase",
5
+ "description": "Linter for AI agent context files and MCP configs: CLAUDE.md, AGENTS.md, .cursorrules, .mcp.json - catches broken paths, wrong commands, leaked secrets",
6
6
  "bin": {
7
7
  "ctxlint": "bin/ctxlint.mjs"
8
8
  },
@@ -27,36 +27,33 @@
27
27
  "mcp": "node dist/index.js --mcp-server"
28
28
  },
29
29
  "keywords": [
30
- "claude",
31
- "agents",
32
- "context",
33
- "lint",
34
- "linter",
35
30
  "claude-md",
36
31
  "agents-md",
32
+ "cursorrules",
33
+ "windsurfrules",
34
+ "copilot-instructions",
35
+ "context-lint",
36
+ "linter",
37
+ "lint",
38
+ "ai-agents",
37
39
  "ai-coding",
38
40
  "context-engineering",
39
41
  "mcp",
40
- "cursorrules",
41
- "copilot-instructions",
42
- "windsurfrules",
43
- "pre-commit",
44
- "agentic",
45
- "codex",
46
- "sarif",
47
42
  "mcp-server",
48
43
  "model-context-protocol",
49
- "context-lint",
44
+ "claude-code",
45
+ "cursor",
46
+ "windsurf",
47
+ "github-copilot",
50
48
  "gemini",
51
49
  "cline",
52
- "windsurf",
53
- "session",
54
- "cross-project",
55
- "agent-session",
56
50
  "aider",
57
- "vibe-cli",
58
51
  "amazon-q",
59
- "goose"
52
+ "goose",
53
+ "codex",
54
+ "sarif",
55
+ "pre-commit",
56
+ "agent-session"
60
57
  ],
61
58
  "files": [
62
59
  "dist/index.js",
@@ -83,7 +80,7 @@
83
80
  "bugs": {
84
81
  "url": "https://github.com/YawLabs/ctxlint/issues"
85
82
  },
86
- "homepage": "https://github.com/YawLabs/ctxlint",
83
+ "homepage": "https://yaw.sh/mcp-servers/ctxlint/",
87
84
  "author": "Yaw Labs <contact@yaw.sh>",
88
85
  "engines": {
89
86
  "node": ">=20"