@wix/pathgrade 0.35.0 → 0.37.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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Nadav Lachish
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ NOTICE: Pathgrade was originally forked from skillgrade by Minko Gechev,
24
+ which is MIT licensed.
package/README.md ADDED
@@ -0,0 +1,424 @@
1
+ # Pathgrade
2
+
3
+ **Evaluate AI coding agents with Vitest.** Write evals as normal `.eval.ts` files, run Claude Code, Codex, or Cursor in isolated sandboxes, and score the result with deterministic checks, rubric judges, and tool-usage assertions.
4
+
5
+ ## Why Pathgrade?
6
+
7
+ - Write evals in plain TypeScript with Vitest
8
+ - Run each trial in an isolated workspace and HOME directory
9
+ - Seed trials from fixtures, real skills, or mocked MCP servers
10
+ - Score both final artifacts and the workflow that produced them
11
+ - Debug long conversations with preserved workspaces and run snapshots
12
+ - Use the same evals locally and in CI
13
+
14
+ ## Quick Start
15
+
16
+ **Prerequisites**: Node.js 20.11+, Vitest 4+, and at least one of [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex CLI](https://github.com/openai/codex), or the `cursor-agent` CLI
17
+
18
+ ```bash
19
+ yarn add -D @wix/pathgrade
20
+ ```
21
+
22
+ ### Authentication
23
+
24
+ By default, Pathgrade tries to reuse the agent CLI's native auth before falling back to explicit environment variables.
25
+
26
+ - **Claude**
27
+ - macOS: reuses Claude Code OAuth from Keychain
28
+ - other platforms: forwards `ANTHROPIC_API_KEY` when present
29
+ - **Codex**
30
+ - reuses cached `~/.codex/auth.json` when available
31
+ - or forwards `OPENAI_API_KEY`
32
+ - or runs `codex login --with-api-key` inside the sandbox when an API key is present but no cached login exists
33
+ - **Cursor**
34
+ - forwards `CURSOR_API_KEY` when set
35
+ - macOS: reuses `cursor-agent login` OAuth tokens from the login Keychain
36
+ - surfaces a clear error when neither is available (run `cursor-agent login` or set `CURSOR_API_KEY`)
37
+
38
+ If you set `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, or `CURSOR_API_BASE_URL`, set the matching API key too.
39
+
40
+ Override credentials per test with `env`:
41
+
42
+ ```typescript
43
+ const agent = await createAgent({
44
+ agent: 'claude',
45
+ env: {
46
+ ANTHROPIC_API_KEY: process.env.MY_ANTHROPIC_KEY!,
47
+ },
48
+ });
49
+ ```
50
+
51
+ ### Transport (Codex only)
52
+
53
+ Codex supports two transports and Pathgrade defaults to `app-server`:
54
+
55
+ - `app-server` (default) — uses `codex app-server` and keeps native thread state. Required for `AskUserReaction` handshakes (`request_user_input` reaches the model). Requires `OPENAI_API_KEY`; cached `~/.codex/auth.json` is not supported under this transport.
56
+ - `exec` — uses `codex exec` and re-injects the transcript every turn. Kept for stateless CI matrices that don't need the handshake.
57
+
58
+ Precedence: `createAgent({ transport })` > `PATHGRADE_CODEX_TRANSPORT` env > default (`app-server`). An invalid env value throws at `createAgent` time.
59
+
60
+ ```typescript
61
+ const agent = await createAgent({
62
+ agent: 'codex',
63
+ transport: 'exec', // opt out of app-server
64
+ });
65
+ ```
66
+
67
+ If `transport: 'exec'` is resolved and any `AskUserReaction` is present in `ConverseOptions.reactions`, the conversation fails fast before turn 1 — the handshake cannot fire under `exec`. Set `allowUnreachableReactions: true` on `runConversation` to silence the guard.
68
+
69
+ Migrating from `exec` to `app-server`:
70
+
71
+ - Export `OPENAI_API_KEY`, or set `transport: 'exec'` / `PATHGRADE_CODEX_TRANSPORT=exec` to stay on the old transport.
72
+ - The `noninteractive-user-question` runtime policy no longer attaches under `app-server`. Snapshots that captured model output influenced by that policy text may need re-recording.
73
+ - `MAX_TURN_RETRIES` does not apply under `app-server` — a crashed turn ends the conversation with `completionReason: 'agent_crashed'`.
74
+
75
+ ### Plugin Setup
76
+
77
+ Create a `vitest.config.ts` with the Pathgrade plugin:
78
+
79
+ ```typescript
80
+ import { defineConfig } from 'vitest/config';
81
+ import { pathgrade } from '@wix/pathgrade/plugin';
82
+
83
+ export default defineConfig({
84
+ plugins: [pathgrade({ timeout: 120 })],
85
+ });
86
+ ```
87
+
88
+ The plugin registers the setup hooks Pathgrade needs, wires in the reporter, and automatically cleans up agent workspaces after each test.
89
+
90
+ ### First Eval
91
+
92
+ Write an eval file such as `hello.eval.ts`:
93
+
94
+ ```typescript
95
+ import * as fs from 'fs';
96
+ import * as path from 'path';
97
+ import { describe, it, expect } from 'vitest';
98
+ import { createAgent, check, evaluate } from '@wix/pathgrade';
99
+
100
+ describe('hello world', () => {
101
+ it('agent creates the requested file', async () => {
102
+ const agent = await createAgent({
103
+ agent: 'claude',
104
+ workspace: path.join(__dirname, 'fixtures'),
105
+ });
106
+
107
+ await agent.prompt('Create a file called hello.txt with the text "Hello, world!"');
108
+
109
+ const result = await evaluate(agent, [
110
+ check('hello.txt exists', ({ workspace }) =>
111
+ fs.existsSync(path.join(workspace, 'hello.txt'))),
112
+ ]);
113
+
114
+ expect(result.score).toBe(1);
115
+ });
116
+ });
117
+ ```
118
+
119
+ Run your evals:
120
+
121
+ ```bash
122
+ npx pathgrade run
123
+ ```
124
+
125
+ `pathgrade run` is the recommended wrapper: it loads `.env`, warns when no auth is configured, and adds Pathgrade-specific flags such as `--changed`, `--diagnostics`, and `--verbose`. Plain `npx vitest run` works too if you do not need those extras.
126
+
127
+ ## Core Concepts
128
+
129
+ - **Agent**: the coding agent under test, such as Claude, Codex, or Cursor
130
+ - **Workspace**: an isolated directory where the agent works, optionally seeded from fixtures
131
+ - **Scorer**: a function or judge that evaluates output or behavior
132
+ - **Evaluation**: the aggregated result of one or more scorers, returned as a score from `0.0` to `1.0`
133
+
134
+ ## Scorers
135
+
136
+ Scorers evaluate the agent's output and behavior. `evaluate()` runs all scorers and computes a weighted average between `0.0` and `1.0`.
137
+
138
+ Use `check()` for binary requirements, `score()` for partial credit, `judge()` for rubric-based evaluation, and `toolUsage()` when the workflow matters as much as the final output.
139
+
140
+ ### `check()` - Boolean gate
141
+
142
+ ```typescript
143
+ check('tests-pass', async ({ runCommand }) => {
144
+ const { exitCode } = await runCommand('npm test');
145
+ return exitCode === 0;
146
+ });
147
+ ```
148
+
149
+ ### `score()` - Partial credit
150
+
151
+ ```typescript
152
+ score('coverage', async ({ runCommand }) => {
153
+ const { stdout } = await runCommand('npx coverage-summary');
154
+ return parseFloat(stdout) / 100;
155
+ });
156
+ ```
157
+
158
+ ### `judge()` - Rubric evaluation
159
+
160
+ ```typescript
161
+ judge('workflow-quality', {
162
+ rubric: `Did the agent read the file before editing? (0-0.5)
163
+ Was the fix minimal and correct? (0-0.5)`,
164
+ });
165
+ ```
166
+
167
+ Judge scorers also support:
168
+
169
+ - `retry` for transient judge failures
170
+ - `includeToolEvents` when the rubric should see the tool trace
171
+ - `input` for extra context such as generated file contents or command output
172
+ - `tools` to let the judge LLM read workspace artifacts itself via a bounded tool-use loop (`readFile`, `listDir`, `grep`, `getToolEvents`)
173
+
174
+ Example with artifact-backed input:
175
+
176
+ ```typescript
177
+ judge('output-quality', {
178
+ rubric: 'Is the generated markdown correct and complete?',
179
+ includeToolEvents: true,
180
+ input: async ({ artifacts }) => ({
181
+ 'output.md': await artifacts.read('output.md'),
182
+ }),
183
+ });
184
+ ```
185
+
186
+ Example with tool-using judge (the judge reads the file itself — no pre-computed probe):
187
+
188
+ ```typescript
189
+ judge('spec-structure', {
190
+ rubric: `Read artifacts/spec.md. Score 1.0 if it contains Intent Hierarchy,
191
+ Functional Requirements, and API Surface sections. 0.33 per section.`,
192
+ tools: ['readFile'],
193
+ });
194
+ ```
195
+
196
+ Tool-using judges currently require the Anthropic HTTP provider (`ANTHROPIC_API_KEY`); other providers produce a clean `provider_not_supported` error. See the [User Guide](packages/pathgrade/docs/USER_GUIDE.md#tool-using-judges--judge-tools-) for the full tool list, failure codes, and the migration recipe from `input`-helper probes.
197
+
198
+ ### `toolUsage()` - Tool event matching
199
+
200
+ ```typescript
201
+ toolUsage('expected-workflow', [
202
+ { action: 'read_file', min: 1, weight: 0.3 },
203
+ { action: 'edit_file', min: 1, weight: 0.3 },
204
+ { action: 'run_shell', commandContains: 'test', min: 1, weight: 0.4 },
205
+ ]);
206
+ ```
207
+
208
+ ## Conversations
209
+
210
+ Pathgrade currently supports three agent backends: `claude`, `codex`, and `cursor`. Set the backend per test via `createAgent({ agent: 'claude' })` or globally via `PATHGRADE_AGENT`.
211
+
212
+ ### `agent.prompt()` - One shot
213
+
214
+ Send a single instruction and let the agent work to completion:
215
+
216
+ ```typescript
217
+ const agent = await createAgent({ agent: 'claude', workspace: 'fixtures' });
218
+ await agent.prompt('Create a file called hello.txt with the text "Hello, world!"');
219
+ ```
220
+
221
+ ### `startChat()` - Imperative
222
+
223
+ Drive the conversation yourself:
224
+
225
+ ```typescript
226
+ const chat = await agent.startChat('Set up a new TypeScript project.');
227
+ await chat.reply('Use strict mode and add eslint.');
228
+ if (await chat.hasFile('tsconfig.json')) {
229
+ await chat.reply('Now add a build script.');
230
+ }
231
+ chat.end();
232
+ ```
233
+
234
+ ### `runConversation()` - Scripted or persona-driven
235
+
236
+ Drive the loop with reactions:
237
+
238
+ ```typescript
239
+ const result = await agent.runConversation({
240
+ firstMessage: 'I want to create a new feature.',
241
+ maxTurns: 12,
242
+ until: async ({ hasFile }) => await hasFile('project-brief.md'),
243
+ reactions: [
244
+ { when: /goal/i, reply: 'Solve a user pain point' },
245
+ { when: /audience/i, reply: 'Self-Creator' },
246
+ ],
247
+ });
248
+ ```
249
+
250
+ Or let a persona answer on the user's behalf:
251
+
252
+ ```typescript
253
+ const result = await agent.runConversation({
254
+ firstMessage: 'I want to create a new feature.',
255
+ maxTurns: 12,
256
+ until: async ({ hasFile }) => await hasFile('project-brief.md'),
257
+ persona: {
258
+ description: 'A product manager who communicates concisely.',
259
+ facts: ['The feature is for online stores'],
260
+ },
261
+ });
262
+ ```
263
+
264
+ `runConversation()` also supports `stepScorers`, so long conversations can be graded at intermediate milestones instead of only at the end.
265
+
266
+ ## Advanced SDK Features
267
+
268
+ Pathgrade exposes a few useful features that are easy to miss from the basic examples:
269
+
270
+ - `createAgent({ skillDir, workspace })` stages a real skill and a fixture workspace into the sandbox, which is how Pathgrade's skill examples are evaluated.
271
+ - `createAgent({ debug: true })` preserves the final workspace under `pathgrade-debug/<test-name>/`; when you use `runConversation()`, it also writes `run-snapshot.json`.
272
+ - `evaluate.fromSnapshot(snapshotPath, scorers)` re-runs grading against a saved snapshot without re-running the agent.
273
+ - `previewReactions(messages, reactions)` lets you inspect which scripted reactions would fire offline.
274
+ - `conversationWindow` on agents and personas keeps long transcripts bounded with summarization instead of sending the full conversation every turn.
275
+ - `copyIgnore` and `DEFAULT_COPY_IGNORE` let you control what gets copied into the sandbox when seeding from large fixtures or skill directories.
276
+
277
+ See [sdk-showcase](packages/pathgrade/examples/sdk-showcase/) for a single example suite that demonstrates these APIs together.
278
+
279
+ ## MCP Mock Servers
280
+
281
+ Simulate MCP tools when testing Claude-driven evals:
282
+
283
+ ```typescript
284
+ import { mockMcpServer } from '@wix/pathgrade/mcp-mock';
285
+
286
+ const mock = mockMcpServer({
287
+ name: 'weather',
288
+ tools: [{
289
+ name: 'get_weather',
290
+ description: 'Get weather for a city',
291
+ when: 'weather',
292
+ response: { temp: 72, unit: 'F' },
293
+ }],
294
+ });
295
+
296
+ const agent = await createAgent({ agent: 'claude', mcpMock: mock });
297
+ ```
298
+
299
+ ## CLI
300
+
301
+ ```bash
302
+ pathgrade run [--changed] [--diagnostics] [--verbose] [-- vitest-args]
303
+ pathgrade init [--force]
304
+ pathgrade validate <file.eval.ts>
305
+ pathgrade validate --affected
306
+ pathgrade analyze [--skill=<name>] [--dir=<path>]
307
+ pathgrade affected [--since=<ref>] [--changed-files=<path>] [--explain] [--json]
308
+ pathgrade preview [browser] [--last=N] [--filter=text]
309
+ pathgrade preview-reactions --snapshot <run-snapshot.json> --reactions <file.ts>
310
+ pathgrade report [--results-path=<path>] [--no-comment] [--comment-id=<id>]
311
+ ```
312
+
313
+ Useful details:
314
+
315
+ - `pathgrade run --changed` computes affected evals first, writes selection metadata to `.pathgrade/selection.json`, and only then launches Vitest.
316
+ - `pathgrade preview browser` starts a local viewer on `http://localhost:3847`.
317
+ - `pathgrade report` posts or updates a PR comment in GitHub Actions; locally it prints the markdown report and then the numeric pass rate.
318
+ - `pathgrade validate --affected` is a strict mode for CI: every discovered eval must either live under a `SKILL.md` anchor or export valid `__pathgradeMeta`.
319
+
320
+ Run `pathgrade --help` for the full help text.
321
+
322
+ ## Plugin Options
323
+
324
+ ```typescript
325
+ import { pathgrade } from '@wix/pathgrade/plugin';
326
+
327
+ pathgrade({
328
+ include: ['**/*.eval.ts'], // default: ['**/*.eval.ts']
329
+ timeout: 300, // seconds, default: 300
330
+ reporter: 'cli', // 'cli' | 'browser' | 'json'
331
+ diagnostics: false, // print full diagnostics for passing evals too
332
+ verbose: false, // stream live per-turn events to stderr while evals run
333
+ ci: { threshold: 0.8 }, // fail when the mean test score drops below threshold
334
+ affected: {
335
+ global: ['package.json', 'yarn.lock'],
336
+ },
337
+ });
338
+ ```
339
+
340
+ Notes:
341
+
342
+ - `exclude` is also supported. If you set it, it replaces the default exclude list instead of merging with it.
343
+ - `reporter: 'browser'` writes results JSON and opens the viewer automatically after the run.
344
+ - `affected.global` is a repo-level "rerun everything" escape hatch for `pathgrade affected` and `pathgrade run --changed`.
345
+
346
+ ## Environment Variables
347
+
348
+ | Variable | Purpose |
349
+ |----------|---------|
350
+ | `ANTHROPIC_API_KEY` | Claude auth and the required key when using `ANTHROPIC_BASE_URL` |
351
+ | `OPENAI_API_KEY` | Codex auth and the required key when using `OPENAI_BASE_URL` |
352
+ | `CURSOR_API_KEY` | Cursor auth and the required key when using `CURSOR_API_BASE_URL` |
353
+ | `ANTHROPIC_BASE_URL` | Custom Anthropic-compatible endpoint |
354
+ | `OPENAI_BASE_URL` | Custom OpenAI-compatible endpoint |
355
+ | `CURSOR_API_BASE_URL` | Custom Cursor-compatible endpoint |
356
+ | `PATHGRADE_AGENT` | Fallback agent for all tests (`claude`, `codex`, or `cursor`). `createAgent({ agent })` wins over this. |
357
+ | `PATHGRADE_CODEX_TRANSPORT` | Fallback Codex transport (`exec` or `app-server`). `createAgent({ transport })` wins over this. |
358
+ | `PATHGRADE_VERBOSE` | `1` enables live per-turn streaming to stderr |
359
+ | `PATHGRADE_DIAGNOSTICS` | `1` prints full diagnostics for passing evals too |
360
+ | `NO_COLOR` | Disable ANSI colors |
361
+
362
+ `pathgrade run` loads `.env` from the working directory automatically.
363
+
364
+ ## CI / GitHub Actions
365
+
366
+ Run evals on every PR and post results as a PR comment:
367
+
368
+ ```yaml
369
+ jobs:
370
+ eval:
371
+ runs-on: ubuntu-latest
372
+ permissions:
373
+ pull-requests: write
374
+ contents: read
375
+ steps:
376
+ - uses: actions/checkout@v4
377
+ with:
378
+ fetch-depth: 0 # required for affected selection
379
+ - uses: actions/setup-node@v4
380
+ with:
381
+ node-version: '20'
382
+ - run: npm ci
383
+
384
+ - name: Run affected evals
385
+ env:
386
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
387
+ run: npx pathgrade run --changed
388
+
389
+ - name: Post PR report
390
+ if: always()
391
+ env:
392
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
393
+ run: npx pathgrade report
394
+
395
+ - uses: actions/upload-artifact@v4
396
+ if: always()
397
+ with:
398
+ name: pathgrade-reports
399
+ path: .pathgrade/
400
+ ```
401
+
402
+ - `fetch-depth: 0` is required for `--changed`; shallow clones break merge-base resolution.
403
+ - Evals under a `SKILL.md` are tracked automatically; use `__pathgradeMeta` for cross-skill or non-standard dependencies.
404
+ - Set `ci: { threshold: 0.8 }` in the plugin config to fail the run when the mean test score drops below your threshold.
405
+
406
+ See the [User Guide - CI Integration](packages/pathgrade/docs/USER_GUIDE.md#ci-integration) for the full reference.
407
+
408
+ ## Links
409
+
410
+ - [User Guide](packages/pathgrade/docs/USER_GUIDE.md) - full API reference and usage patterns
411
+ - Examples:
412
+ - [start-chat](packages/pathgrade/examples/start-chat/) - multi-turn conversation
413
+ - [sdk-showcase](packages/pathgrade/examples/sdk-showcase/) - advanced SDK features in one suite
414
+ - [tool-judge-demo](packages/pathgrade/examples/tool-judge-demo/) - `judge({ tools })` reading workspace artifacts
415
+
416
+ ## Note on AI provider dependencies
417
+
418
+ Pathgrade is released under the MIT license. Pathgrade can use third-party AI agent tools and SDKs to run evaluations, including `@anthropic-ai/claude-agent-sdk` for Claude, Codex CLI for Codex, and `cursor-agent` for Cursor. These tools, SDKs, hosted services, and related authentication methods are governed by their respective provider terms, which are separate from Pathgrade's MIT license.
419
+
420
+ In particular, `@anthropic-ai/claude-agent-sdk` is governed by Anthropic's Commercial Terms of Service, except where Anthropic specifies a different license for a specific component or dependency.
421
+
422
+ ## License
423
+
424
+ MIT
@@ -18,8 +18,11 @@
18
18
  */
19
19
  export interface AffectedConfig {
20
20
  global: string[];
21
+ include?: string[];
22
+ exclude?: string[];
21
23
  }
22
24
  export interface LoadOptions {
25
+ configPath?: string;
23
26
  onWarning?: (message: string) => void;
24
27
  }
25
28
  export declare function loadAffectedConfig(repoRoot: string, options?: LoadOptions): Promise<AffectedConfig>;
@@ -41,9 +41,11 @@ export async function loadAffectedConfig(repoRoot, options = {}) {
41
41
  return { global: [] };
42
42
  }
43
43
  }
44
- const configPath = VITEST_CONFIG_CANDIDATES
45
- .map(c => path.join(repoRoot, c))
46
- .find(p => fs.existsSync(p));
44
+ const configPath = options.configPath
45
+ ? path.resolve(repoRoot, options.configPath)
46
+ : VITEST_CONFIG_CANDIDATES
47
+ .map(c => path.join(repoRoot, c))
48
+ .find(p => fs.existsSync(p));
47
49
  if (!configPath)
48
50
  return { global: [] };
49
51
  let loaded;
@@ -52,7 +54,11 @@ export async function loadAffectedConfig(repoRoot, options = {}) {
52
54
  loaded = await jiti.import(configPath, { default: true });
53
55
  }
54
56
  catch (err) {
55
- warn(`pathgrade: failed to load ${path.relative(repoRoot, configPath)}: ${errMsg(err)}`);
57
+ const message = `pathgrade: failed to load ${path.relative(repoRoot, configPath)}: ${errMsg(err)}`;
58
+ if (options.configPath) {
59
+ throw new Error(message);
60
+ }
61
+ warn(message);
56
62
  return { global: [] };
57
63
  }
58
64
  const plugins = findPluginsList(loaded);
@@ -68,7 +74,13 @@ export async function loadAffectedConfig(repoRoot, options = {}) {
68
74
  }
69
75
  const opts = pathgradePlugin.__pathgradeOptions ?? {};
70
76
  const global = opts.affected?.global;
71
- return { global: Array.isArray(global) ? global : [] };
77
+ const include = opts.include;
78
+ const exclude = opts.exclude;
79
+ return {
80
+ global: Array.isArray(global) ? global : [],
81
+ ...(Array.isArray(include) ? { include } : {}),
82
+ ...(Array.isArray(exclude) ? { exclude } : {}),
83
+ };
72
84
  }
73
85
  /**
74
86
  * Given a loaded vitest config (either the raw export or a `defineConfig()`
@@ -13,6 +13,7 @@
13
13
  * only producer of the file list here.
14
14
  */
15
15
  import * as fs from 'fs';
16
+ import picomatch from 'picomatch';
16
17
  import { selectAffected } from '../affected/select.js';
17
18
  import { resolveBaseRef, computeChangedFiles } from '../affected/git.js';
18
19
  import { loadAffectedConfig } from '../affected/config.js';
@@ -52,13 +53,22 @@ export async function runChanged(opts) {
52
53
  baseRefLine = `pathgrade: base = ${baseRef} (merge-base with HEAD)`;
53
54
  }
54
55
  // 2. Selection
55
- const evalFiles = discoverEvalFiles(cwd);
56
- const config = await loadAffectedConfig(cwd, {
57
- onWarning: w => {
58
- if (!parsed.quiet)
59
- process.stderr.write(`${w}\n`);
60
- },
61
- });
56
+ const configPath = findVitestConfigArg(parsed.vitestArgs);
57
+ let config;
58
+ try {
59
+ config = await loadAffectedConfig(cwd, {
60
+ configPath,
61
+ onWarning: w => {
62
+ if (!parsed.quiet)
63
+ process.stderr.write(`${w}\n`);
64
+ },
65
+ });
66
+ }
67
+ catch (err) {
68
+ process.stderr.write(`${errMsg(err)}\n`);
69
+ return 1;
70
+ }
71
+ const evalFiles = filterEvalFilesForConfig(discoverEvalFiles(cwd), config);
62
72
  let result;
63
73
  try {
64
74
  result = selectAffected({
@@ -93,6 +103,11 @@ export async function runChanged(opts) {
93
103
  return 0;
94
104
  }
95
105
  const selectedFiles = result.selected.map(s => s.file);
106
+ if (hasPassWithNoTests(parsed.vitestArgs)) {
107
+ process.stderr.write('pathgrade run: --passWithNoTests cannot be used with pathgrade run --changed. ' +
108
+ 'The command already exits 0 when no evals are selected; if selected evals resolve to no Vitest files, CI must fail.\n');
109
+ return 1;
110
+ }
96
111
  const argv = ['run', ...selectedFiles, ...parsed.vitestArgs];
97
112
  if (!parsed.quiet) {
98
113
  process.stderr.write(`→ vitest run ${selectedFiles.join(' ')}\n`);
@@ -126,6 +141,37 @@ function readChangedFilesList(filePath) {
126
141
  function errMsg(err) {
127
142
  return err instanceof Error ? err.message : String(err);
128
143
  }
144
+ function hasPassWithNoTests(args) {
145
+ return args.some(arg => {
146
+ if (arg === '--passWithNoTests')
147
+ return true;
148
+ if (!arg.startsWith('--passWithNoTests='))
149
+ return false;
150
+ return arg.slice('--passWithNoTests='.length).toLowerCase() !== 'false';
151
+ });
152
+ }
153
+ function findVitestConfigArg(args) {
154
+ for (let i = 0; i < args.length; i++) {
155
+ const arg = args[i];
156
+ if (arg === '--config' || arg === '-c')
157
+ return args[i + 1];
158
+ if (arg.startsWith('--config='))
159
+ return arg.slice('--config='.length);
160
+ if (arg.startsWith('-c='))
161
+ return arg.slice('-c='.length);
162
+ }
163
+ return undefined;
164
+ }
165
+ function filterEvalFilesForConfig(evalFiles, config) {
166
+ if (!config.include && !config.exclude)
167
+ return evalFiles;
168
+ const includeMatchers = config.include?.map(g => picomatch(g, { dot: true }));
169
+ const excludeMatchers = config.exclude?.map(g => picomatch(g, { dot: true })) ?? [];
170
+ return evalFiles.filter(file => {
171
+ const included = includeMatchers ? includeMatchers.some(m => m(file)) : true;
172
+ return included && !excludeMatchers.some(m => m(file));
173
+ });
174
+ }
129
175
  async function defaultSpawnVitest(req) {
130
176
  const { spawn } = await import('child_process');
131
177
  return await new Promise(resolve => {
@@ -15,6 +15,7 @@ export declare class PathgradeReporter implements Reporter {
15
15
  */
16
16
  private getGroupKey;
17
17
  private toTestEntry;
18
+ private isReportableEntry;
18
19
  private printCliSummary;
19
20
  private writeJsonResults;
20
21
  private buildEvalReport;
@@ -45,6 +45,8 @@ export class PathgradeReporter {
45
45
  for (const testCase of mod.children.allTests()) {
46
46
  const groupKey = this.getGroupKey(testCase);
47
47
  const entry = this.toTestEntry(testCase);
48
+ if (!this.isReportableEntry(entry))
49
+ continue;
48
50
  if (!groupMap.has(groupKey)) {
49
51
  groupMap.set(groupKey, []);
50
52
  }
@@ -100,6 +102,9 @@ export class PathgradeReporter {
100
102
  diagnostics,
101
103
  };
102
104
  }
105
+ isReportableEntry(entry) {
106
+ return entry.state !== 'skipped' && entry.state !== 'pending';
107
+ }
103
108
  printCliSummary(groups) {
104
109
  console.log(`\n${fmt.bold('── pathgrade summary ')}${fmt.dim('─'.repeat(40))}\n`);
105
110
  const forceVerbose = this.opts.diagnostics === true || process.env.PATHGRADE_DIAGNOSTICS === '1';
package/package.json CHANGED
@@ -1,18 +1,11 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
+ "packageManager": "yarn@4.12.0",
4
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
5
- "main": "./dist/sdk/index.js",
6
- "types": "./dist/sdk/index.d.ts",
7
- "typesVersions": {
8
- "*": {
9
- "plugin": [
10
- "./dist/plugin/index.d.ts"
11
- ],
12
- "mcp-mock": [
13
- "./dist/core/mcp-mock.d.ts"
14
- ]
15
- }
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/wix-incubator/pathgrade.git"
16
9
  },
17
10
  "exports": {
18
11
  ".": {
@@ -29,7 +22,7 @@
29
22
  },
30
23
  "./package.json": "./package.json"
31
24
  },
32
- "bin": "./bin/pathgrade.js",
25
+ "bin": "bin/pathgrade.js",
33
26
  "files": [
34
27
  "dist/**/*.js",
35
28
  "dist/**/*.d.ts",
@@ -42,9 +35,10 @@
42
35
  "LICENSE"
43
36
  ],
44
37
  "scripts": {
45
- "test": "tsc && vitest run",
38
+ "test": "yarn build && vitest run",
39
+ "test:evals": "vitest run --config evals/vitest.config.mts",
46
40
  "test:coverage": "vitest run --coverage",
47
- "dev": "ts-node src/pathgrade.ts",
41
+ "dev": "tsx src/pathgrade.ts",
48
42
  "build": "tsc -p tsconfig.build.json && cp src/viewer.html dist/viewer.html"
49
43
  },
50
44
  "keywords": [
@@ -60,16 +54,6 @@
60
54
  "llm",
61
55
  "testing"
62
56
  ],
63
- "publishConfig": {
64
- "registry": "https://registry.npmjs.org/",
65
- "access": "public"
66
- },
67
- "wix": {
68
- "artifact": {
69
- "groupId": "com.wixpress",
70
- "artifactId": "pathgrade"
71
- }
72
- },
73
57
  "author": "Nadav Lachish",
74
58
  "license": "MIT",
75
59
  "type": "module",
@@ -82,20 +66,21 @@
82
66
  "devDependencies": {
83
67
  "@types/fs-extra": "^11.0.4",
84
68
  "@types/picomatch": "^4.0.2",
85
- "@vitest/coverage-v8": "^4.0.18",
86
- "ts-node": "^10.9.2",
87
- "tsx": "^4.21.0",
88
- "vitest": "^4.0.18"
69
+ "@vitest/coverage-v8": "4.1.7",
70
+ "tsx": "4.22.3",
71
+ "vitest": "4.1.7"
89
72
  },
90
73
  "dependencies": {
91
- "@anthropic-ai/claude-agent-sdk": "0.2.85",
74
+ "@anthropic-ai/claude-agent-sdk": "0.2.116",
92
75
  "@modelcontextprotocol/sdk": "1.29.0",
93
- "@types/node": "^25.3.1",
94
- "fs-extra": "^11.3.3",
95
- "jiti": "^2.6.1",
76
+ "@types/node": "25.6.0",
77
+ "fs-extra": "11.3.3",
78
+ "jiti": "2.6.1",
96
79
  "picomatch": "^4.0.4",
97
80
  "typescript": "^5.9.3",
98
81
  "zod": "4.3.6"
99
82
  },
100
- "falconPackageHash": "7d7c65385d2ddc29c1b518b76ca8c6c57fd24241c74c182d1fd6fb56"
101
- }
83
+ "resolutions": {
84
+ "fast-uri@npm:^3.0.1": "npm:3.1.2"
85
+ }
86
+ }