@yawlabs/ctxlint 0.24.1 → 0.25.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/.pre-commit-hooks.yaml +1 -1
- package/AGENT_SESSION_LINT_SPEC.md +35 -3
- package/README.md +17 -4
- package/agent-session-lint-rules.json +10 -0
- package/dist/index.js +265 -57
- package/package.json +1 -1
package/.pre-commit-hooks.yaml
CHANGED
|
@@ -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.
|
|
7
|
+
entry: npx @yawlabs/ctxlint@0.25.0 --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
|
-
-
|
|
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
|
-
|
|
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
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://github.com/YawLabs/ctxlint/stargazers)
|
|
6
6
|
[](https://github.com/YawLabs/mcp-compliance)
|
|
7
|
+
[](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.
|
|
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.
|
|
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)** |
|
|
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
|
|
27596
|
+
if (!home2 || !project) return emptyRead();
|
|
27580
27597
|
const dirs = candidateDirs(project, home2);
|
|
27581
|
-
if (dirs.length === 0) return
|
|
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
|
|
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,
|
|
27640
|
+
collect(rec, st2);
|
|
27625
27641
|
}
|
|
27626
27642
|
} catch {
|
|
27627
27643
|
} finally {
|
|
27628
27644
|
rl.close();
|
|
27629
27645
|
}
|
|
27630
27646
|
}
|
|
27631
|
-
|
|
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
|
|
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({
|
|
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)
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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.
|
|
29303
|
+
if (true) return "0.25.0";
|
|
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,
|
|
57069
|
+
function resolveComponent(base, relative9, options, skipNormalization) {
|
|
56874
57070
|
const target = {};
|
|
56875
57071
|
if (!skipNormalization) {
|
|
56876
57072
|
base = parse5(serialize(base, options), options);
|
|
56877
|
-
|
|
57073
|
+
relative9 = parse5(serialize(relative9, options), options);
|
|
56878
57074
|
}
|
|
56879
57075
|
options = options || {};
|
|
56880
|
-
if (!options.tolerant &&
|
|
56881
|
-
target.scheme =
|
|
56882
|
-
target.userinfo =
|
|
56883
|
-
target.host =
|
|
56884
|
-
target.port =
|
|
56885
|
-
target.path = removeDotSegments(
|
|
56886
|
-
target.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 (
|
|
56889
|
-
target.userinfo =
|
|
56890
|
-
target.host =
|
|
56891
|
-
target.port =
|
|
56892
|
-
target.path = removeDotSegments(
|
|
56893
|
-
target.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 (!
|
|
57091
|
+
if (!relative9.path) {
|
|
56896
57092
|
target.path = base.path;
|
|
56897
|
-
if (
|
|
56898
|
-
target.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 (
|
|
56904
|
-
target.path = removeDotSegments(
|
|
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 = "/" +
|
|
57103
|
+
target.path = "/" + relative9.path;
|
|
56908
57104
|
} else if (!base.path) {
|
|
56909
|
-
target.path =
|
|
57105
|
+
target.path = relative9.path;
|
|
56910
57106
|
} else {
|
|
56911
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
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 =
|
|
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 =
|
|
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
|
|
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._)`${
|
|
59608
|
+
return [fmtDef.type || "string", fmtDef.validate, (0, codegen_1._)`${fmt2}.validate`];
|
|
59413
59609
|
}
|
|
59414
|
-
return ["string", fmtDef,
|
|
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
|
|
60085
|
-
cxt.fail$data((0, codegen_1.or)((0, codegen_1._)`typeof ${
|
|
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
|
|
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(
|
|
60296
|
+
cxt.fail$data(compareCode(fmt2));
|
|
60101
60297
|
}
|
|
60102
|
-
function compareCode(
|
|
60103
|
-
return (0, codegen_1._)`${
|
|
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
|
|
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