@wix/pathgrade 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/affected/config.d.ts +3 -0
- package/dist/affected/config.js +17 -5
- package/dist/agents/codex-app-server/agent.js +51 -5
- package/dist/agents/codex-app-server/transport.js +3 -1
- package/dist/agents/codex.js +1 -1
- package/dist/commands/run-changed.js +53 -7
- package/dist/plugin/reporter.d.ts +1 -0
- package/dist/plugin/reporter.js +5 -0
- package/package.json +2 -2
|
@@ -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>;
|
package/dist/affected/config.js
CHANGED
|
@@ -41,9 +41,11 @@ export async function loadAffectedConfig(repoRoot, options = {}) {
|
|
|
41
41
|
return { global: [] };
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
const configPath =
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()`
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../../types.js';
|
|
2
2
|
import { mountMcpForCodexAppServer } from '../../providers/mcp-runtime-mounting.js';
|
|
3
3
|
import { assertMcpSecretReferencesReady } from '../../providers/mcp-config.js';
|
|
4
|
+
import { buildSummary, enrichSkillEvents, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
|
|
4
5
|
import { requireAskBusForLiveBatches, } from '../../sdk/ask-bus/bus.js';
|
|
5
6
|
import { toAskUserToolEvent } from '../../sdk/ask-bus/projection.js';
|
|
6
7
|
import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
7
8
|
import { spawnAppServerTransport, } from './transport.js';
|
|
8
9
|
import { normalizeUpstreamQuestion, toWireAnswerMap, } from './wire-translators.js';
|
|
9
|
-
const DEFAULT_MODEL = 'gpt-5.
|
|
10
|
+
const DEFAULT_MODEL = 'gpt-5.4';
|
|
10
11
|
const TURN_COMPLETED_METHOD = 'turn/completed';
|
|
11
12
|
function recordFromUnknown(value) {
|
|
12
13
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -40,6 +41,27 @@ function extractMcpToolApprovalRequest(params) {
|
|
|
40
41
|
function parseToolNameFromApprovalMessage(message) {
|
|
41
42
|
return message.match(/tool\s+"([^"]+)"/i)?.[1];
|
|
42
43
|
}
|
|
44
|
+
function extractCommandActionSkillName(action) {
|
|
45
|
+
if (typeof action.path === 'string') {
|
|
46
|
+
const direct = extractSkillNameFromPath(action.path);
|
|
47
|
+
if (direct)
|
|
48
|
+
return direct;
|
|
49
|
+
const embedded = extractSkillNameFromText(action.path);
|
|
50
|
+
if (embedded)
|
|
51
|
+
return embedded;
|
|
52
|
+
}
|
|
53
|
+
return typeof action.command === 'string' ? extractSkillNameFromText(action.command) : undefined;
|
|
54
|
+
}
|
|
55
|
+
function extractSkillNameFromText(value) {
|
|
56
|
+
if (!value)
|
|
57
|
+
return undefined;
|
|
58
|
+
return value.match(/(?:^|[/\s"'])\.(?:agents|claude)\/skills\/([^/\s"']+)\/SKILL\.md(?:$|[\s"'])/)?.[1];
|
|
59
|
+
}
|
|
60
|
+
function extractSkillPathFromText(value) {
|
|
61
|
+
if (!value)
|
|
62
|
+
return undefined;
|
|
63
|
+
return value.match(/(?:^|[\s"'])(?<path>(?:\/|\.{1,2}\/)?[^\s"']*(?:\.agents|\.claude)\/skills\/[^/\s"']+\/SKILL\.md)(?:$|[\s"'])/)?.groups?.path;
|
|
64
|
+
}
|
|
43
65
|
function projectItemIntoTurn(item, turn) {
|
|
44
66
|
if (item.type === 'agentMessage') {
|
|
45
67
|
const msg = item;
|
|
@@ -50,16 +72,40 @@ function projectItemIntoTurn(item, turn) {
|
|
|
50
72
|
}
|
|
51
73
|
if (item.type === 'commandExecution') {
|
|
52
74
|
const cmd = item;
|
|
75
|
+
const action = inferCodexExecAction(cmd.command);
|
|
76
|
+
const skillPath = extractSkillPathFromText(cmd.command);
|
|
77
|
+
const args = {
|
|
78
|
+
command: cmd.command,
|
|
79
|
+
...(skillPath ? { path: skillPath } : {}),
|
|
80
|
+
};
|
|
53
81
|
turn.nonAskToolEvents.push({
|
|
54
|
-
action
|
|
82
|
+
action,
|
|
55
83
|
provider: 'codex',
|
|
56
84
|
providerToolName: 'commandExecution',
|
|
57
85
|
turnNumber: turn.turnNumber,
|
|
58
|
-
arguments:
|
|
59
|
-
summary:
|
|
86
|
+
arguments: args,
|
|
87
|
+
summary: buildSummary(action, 'commandExecution', args),
|
|
60
88
|
confidence: 'high',
|
|
61
89
|
rawSnippet: JSON.stringify(cmd),
|
|
62
90
|
});
|
|
91
|
+
const recordedSkills = new Set();
|
|
92
|
+
for (const action of cmd.commandActions ?? []) {
|
|
93
|
+
const skillName = extractCommandActionSkillName(action) ?? extractSkillNameFromText(cmd.command);
|
|
94
|
+
if (!skillName || recordedSkills.has(skillName))
|
|
95
|
+
continue;
|
|
96
|
+
recordedSkills.add(skillName);
|
|
97
|
+
turn.nonAskToolEvents.push({
|
|
98
|
+
action: 'use_skill',
|
|
99
|
+
provider: 'codex',
|
|
100
|
+
providerToolName: `commandExecution.commandActions.${action.type ?? 'unknown'}`,
|
|
101
|
+
turnNumber: turn.turnNumber,
|
|
102
|
+
arguments: { path: action.path, name: action.name },
|
|
103
|
+
summary: `use_skill ${skillName}`,
|
|
104
|
+
confidence: 'high',
|
|
105
|
+
rawSnippet: JSON.stringify(action),
|
|
106
|
+
skillName,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
63
109
|
return;
|
|
64
110
|
}
|
|
65
111
|
if (item.type === 'fileChange') {
|
|
@@ -515,7 +561,7 @@ function assembleTurnResult(args) {
|
|
|
515
561
|
.snapshot()
|
|
516
562
|
.filter((s) => askBatchIds.has(s.batchId))
|
|
517
563
|
.map((s) => toAskUserToolEvent(s));
|
|
518
|
-
const toolEvents = [...askEvents, ...activeTurn.nonAskToolEvents];
|
|
564
|
+
const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents]);
|
|
519
565
|
const rawOutput = exitCode === 0
|
|
520
566
|
? message
|
|
521
567
|
: [
|
|
@@ -142,9 +142,11 @@ function quoteCodexConfigString(value) {
|
|
|
142
142
|
}
|
|
143
143
|
export function buildAppServerSpawnArgs(args = [], env = process.env) {
|
|
144
144
|
const baseUrl = env.OPENAI_BASE_URL?.trim();
|
|
145
|
+
const requestUserInputConfig = ['-c', 'features.default_mode_request_user_input=true'];
|
|
145
146
|
if (!baseUrl)
|
|
146
|
-
return [...args, 'app-server'];
|
|
147
|
+
return [...requestUserInputConfig, ...args, 'app-server'];
|
|
147
148
|
return [
|
|
149
|
+
...requestUserInputConfig,
|
|
148
150
|
'-c', `model_provider=${quoteCodexConfigString(CODEX_PROXY_PROVIDER_ID)}`,
|
|
149
151
|
'-c', `model_providers.${CODEX_PROXY_PROVIDER_ID}.name=${quoteCodexConfigString('PathGrade OpenAI Proxy')}`,
|
|
150
152
|
'-c', `model_providers.${CODEX_PROXY_PROVIDER_ID}.base_url=${quoteCodexConfigString(baseUrl)}`,
|
package/dist/agents/codex.js
CHANGED
|
@@ -26,7 +26,7 @@ export class CodexAgent extends TranscriptAgent {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
const DEFAULT_CODEX_MODEL = 'gpt-5.
|
|
29
|
+
const DEFAULT_CODEX_MODEL = 'gpt-5.4';
|
|
30
30
|
const CODEX_PROXY_PROVIDER_ID = 'pathgrade_openai_proxy';
|
|
31
31
|
function buildCodexExecCommand(promptPath, model = DEFAULT_CODEX_MODEL) {
|
|
32
32
|
const quotedPromptPath = JSON.stringify(promptPath);
|
|
@@ -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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 => {
|
package/dist/plugin/reporter.js
CHANGED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
5
5
|
"main": "./dist/sdk/index.js",
|
|
6
6
|
"types": "./dist/sdk/index.d.ts",
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"typescript": "^5.9.3",
|
|
98
98
|
"zod": "4.3.6"
|
|
99
99
|
},
|
|
100
|
-
"falconPackageHash": "
|
|
100
|
+
"falconPackageHash": "85d2875d7e92c76ffad2391c7756cbca44f843842a3d1b700b3e11f5"
|
|
101
101
|
}
|