@wix/pathgrade 1.0.8 → 1.0.10
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/bin/pathgrade.js +6 -1
- package/dist/agents/{opencode-contract.d.ts → opencode/contract.d.ts} +2 -2
- package/dist/agents/opencode/process-groups.d.ts +3 -0
- package/dist/agents/opencode/process-groups.js +47 -0
- package/dist/agents/opencode.js +25 -13
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +42 -15
- package/dist/commands/validate.js +8 -2
- package/dist/evals/discovery.js +3 -11
- package/dist/evals/sdk-import.d.ts +3 -0
- package/dist/evals/sdk-import.js +76 -0
- package/dist/pathgrade.d.ts +5 -1
- package/dist/pathgrade.js +42 -30
- package/dist/sdk/agent.js +1 -1
- package/dist/sdk/judge-tool-session.js +18 -1
- package/dist/utils/llm-providers/anthropic.js +6 -1
- package/dist/utils/llm.js +1 -0
- package/package.json +6 -2
- /package/dist/agents/{opencode-contract.js → opencode/contract.js} +0 -0
package/bin/pathgrade.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MockMcpServerDescriptor } from '
|
|
2
|
-
import type { AgentName, AgentOptions } from '
|
|
1
|
+
import type { MockMcpServerDescriptor } from '../../core/mcp-mock.types.js';
|
|
2
|
+
import type { AgentName, AgentOptions } from '../../sdk/types.js';
|
|
3
3
|
export declare const OPENCODE_MODEL = "anthropic/claude-sonnet-5";
|
|
4
4
|
export declare const OPENCODE_VERSION = "1.18.18";
|
|
5
5
|
export interface OpenCodeRuntimeLockEntry {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Process-wide ownership of detached OpenCode child process groups. */
|
|
2
|
+
const activeProcessGroups = new Set();
|
|
3
|
+
let processCleanupInstalled = false;
|
|
4
|
+
export function killOpenCodeProcessGroup(pid, signal) {
|
|
5
|
+
try {
|
|
6
|
+
process.kill(-pid, signal);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// The process group may already have exited.
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function killAllProcessGroups() {
|
|
13
|
+
for (const pid of activeProcessGroups) {
|
|
14
|
+
killOpenCodeProcessGroup(pid, 'SIGKILL');
|
|
15
|
+
}
|
|
16
|
+
activeProcessGroups.clear();
|
|
17
|
+
}
|
|
18
|
+
function uninstallProcessCleanup() {
|
|
19
|
+
if (!processCleanupInstalled)
|
|
20
|
+
return;
|
|
21
|
+
processCleanupInstalled = false;
|
|
22
|
+
process.removeListener('exit', onProcessExit);
|
|
23
|
+
process.removeListener('SIGINT', onProcessSignal);
|
|
24
|
+
process.removeListener('SIGTERM', onProcessSignal);
|
|
25
|
+
}
|
|
26
|
+
function onProcessExit() {
|
|
27
|
+
killAllProcessGroups();
|
|
28
|
+
}
|
|
29
|
+
function onProcessSignal(signal) {
|
|
30
|
+
killAllProcessGroups();
|
|
31
|
+
uninstallProcessCleanup();
|
|
32
|
+
process.kill(process.pid, signal);
|
|
33
|
+
}
|
|
34
|
+
export function registerOpenCodeProcessGroup(pid) {
|
|
35
|
+
activeProcessGroups.add(pid);
|
|
36
|
+
if (processCleanupInstalled)
|
|
37
|
+
return;
|
|
38
|
+
processCleanupInstalled = true;
|
|
39
|
+
process.once('exit', onProcessExit);
|
|
40
|
+
process.once('SIGINT', onProcessSignal);
|
|
41
|
+
process.once('SIGTERM', onProcessSignal);
|
|
42
|
+
}
|
|
43
|
+
export function unregisterOpenCodeProcessGroup(pid) {
|
|
44
|
+
activeProcessGroups.delete(pid);
|
|
45
|
+
if (activeProcessGroups.size === 0)
|
|
46
|
+
uninstallProcessCleanup();
|
|
47
|
+
}
|
package/dist/agents/opencode.js
CHANGED
|
@@ -7,7 +7,8 @@ import fs from 'fs-extra';
|
|
|
7
7
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
|
|
8
8
|
import { buildSummary, enrichSkillEvents } from '../tool-events.js';
|
|
9
9
|
import { readStagedMcpServers } from '../providers/mcp-config.js';
|
|
10
|
-
import { currentOpenCodePlatformKey, OPENCODE_MODEL, OPENCODE_RUNTIME_LOCK, } from './opencode
|
|
10
|
+
import { currentOpenCodePlatformKey, OPENCODE_MODEL, OPENCODE_RUNTIME_LOCK, } from './opencode/contract.js';
|
|
11
|
+
import { killOpenCodeProcessGroup, registerOpenCodeProcessGroup, unregisterOpenCodeProcessGroup, } from './opencode/process-groups.js';
|
|
11
12
|
const OUTPUT_CAP_BYTES = 16 * 1024 * 1024;
|
|
12
13
|
const FIXED_OPENCODE_ENV = {
|
|
13
14
|
OPENCODE_CLIENT: 'pathgrade',
|
|
@@ -68,6 +69,7 @@ export function spawnOpenCode(executable, args, options) {
|
|
|
68
69
|
let aborted = false;
|
|
69
70
|
let killTimer;
|
|
70
71
|
let settled = false;
|
|
72
|
+
let terminating = false;
|
|
71
73
|
const child = spawn(executable, args, {
|
|
72
74
|
cwd: options.cwd,
|
|
73
75
|
env: options.env,
|
|
@@ -75,22 +77,22 @@ export function spawnOpenCode(executable, args, options) {
|
|
|
75
77
|
detached: true,
|
|
76
78
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
79
|
});
|
|
80
|
+
if (child.pid)
|
|
81
|
+
registerOpenCodeProcessGroup(child.pid);
|
|
78
82
|
const killGroup = (signal) => {
|
|
79
83
|
if (!child.pid)
|
|
80
84
|
return;
|
|
81
|
-
|
|
82
|
-
process.kill(-child.pid, signal);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// The process may have exited between the state check and kill.
|
|
86
|
-
}
|
|
85
|
+
killOpenCodeProcessGroup(child.pid, signal);
|
|
87
86
|
};
|
|
88
87
|
const terminate = () => {
|
|
88
|
+
terminating = true;
|
|
89
89
|
killGroup('SIGTERM');
|
|
90
90
|
killTimer ??= setTimeout(() => killGroup('SIGKILL'), 250);
|
|
91
91
|
killTimer.unref();
|
|
92
92
|
};
|
|
93
93
|
const onAbort = () => {
|
|
94
|
+
if (aborted)
|
|
95
|
+
return;
|
|
94
96
|
aborted = true;
|
|
95
97
|
terminate();
|
|
96
98
|
};
|
|
@@ -98,9 +100,6 @@ export function spawnOpenCode(executable, args, options) {
|
|
|
98
100
|
overflow = true;
|
|
99
101
|
terminate();
|
|
100
102
|
};
|
|
101
|
-
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
102
|
-
if (options.signal?.aborted)
|
|
103
|
-
onAbort();
|
|
104
103
|
child.stdout.on('data', (chunk) => {
|
|
105
104
|
stdoutBytes += chunk.length;
|
|
106
105
|
if (stdoutBytes <= cap)
|
|
@@ -114,16 +113,23 @@ export function spawnOpenCode(executable, args, options) {
|
|
|
114
113
|
onOverflow();
|
|
115
114
|
});
|
|
116
115
|
child.stdin.on('error', () => undefined);
|
|
116
|
+
child.once('error', (error) => finish(undefined, undefined, error));
|
|
117
|
+
child.once('close', (code, signal) => finish(code, signal));
|
|
118
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
119
|
+
if (options.signal?.aborted)
|
|
120
|
+
onAbort();
|
|
117
121
|
if (!aborted)
|
|
118
122
|
child.stdin.end(options.stdin);
|
|
119
123
|
else
|
|
120
124
|
child.stdin.destroy();
|
|
121
|
-
child.once('error', (error) => finish(undefined, undefined, error));
|
|
122
|
-
child.once('close', (code, signal) => finish(code, signal));
|
|
123
125
|
function finish(code, signal, error) {
|
|
124
126
|
if (settled)
|
|
125
127
|
return;
|
|
126
128
|
settled = true;
|
|
129
|
+
if (child.pid && terminating)
|
|
130
|
+
killOpenCodeProcessGroup(child.pid, 'SIGKILL');
|
|
131
|
+
if (child.pid)
|
|
132
|
+
unregisterOpenCodeProcessGroup(child.pid);
|
|
127
133
|
options.signal?.removeEventListener('abort', onAbort);
|
|
128
134
|
if (killTimer)
|
|
129
135
|
clearTimeout(killTimer);
|
|
@@ -377,6 +383,7 @@ class OpenCodeSession {
|
|
|
377
383
|
disposed = false;
|
|
378
384
|
sessionId;
|
|
379
385
|
inFlight;
|
|
386
|
+
disposeController = new AbortController();
|
|
380
387
|
constructor(runtime, options) {
|
|
381
388
|
this.workspacePath = getWorkspacePath(runtime);
|
|
382
389
|
this.runtimeEnv = getRuntimeEnv(runtime);
|
|
@@ -432,11 +439,15 @@ class OpenCodeSession {
|
|
|
432
439
|
'--model', OPENCODE_MODEL, '--agent', 'build',
|
|
433
440
|
...(this.sessionId ? ['--session', this.sessionId] : []),
|
|
434
441
|
];
|
|
442
|
+
const turnSignal = this.getAbortSignal();
|
|
443
|
+
const signal = turnSignal
|
|
444
|
+
? AbortSignal.any([turnSignal, this.disposeController.signal])
|
|
445
|
+
: this.disposeController.signal;
|
|
435
446
|
const processResult = await spawnOpenCode(this.resolvedExecutable, args, {
|
|
436
447
|
cwd: this.workspacePath,
|
|
437
448
|
env,
|
|
438
449
|
stdin: message,
|
|
439
|
-
signal
|
|
450
|
+
signal,
|
|
440
451
|
});
|
|
441
452
|
const parsed = parseOpenCodeOutput(processResult.stdout, processResult, this.mcpToolNames);
|
|
442
453
|
if (this.sessionId && parsed.sessionId !== this.sessionId) {
|
|
@@ -494,6 +505,7 @@ class OpenCodeSession {
|
|
|
494
505
|
if (this.disposed)
|
|
495
506
|
return;
|
|
496
507
|
this.disposed = true;
|
|
508
|
+
this.disposeController.abort();
|
|
497
509
|
await this.inFlight?.catch(() => undefined);
|
|
498
510
|
await this.cleanupState();
|
|
499
511
|
}
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -22,6 +22,11 @@ async function findExistingEvalFile(dir) {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
export async function runInit(dir, opts = {}) {
|
|
25
|
+
const commandName = opts.commandName?.trim() || 'pathgrade';
|
|
26
|
+
const skills = await detectSkills(dir);
|
|
27
|
+
const dirName = path.basename(dir);
|
|
28
|
+
const evalName = skills.length === 1 ? skills[0].name : dirName;
|
|
29
|
+
const evalPath = resolveEvalPath(dir, evalName);
|
|
25
30
|
const existing = await findExistingEvalFile(dir);
|
|
26
31
|
if (existing) {
|
|
27
32
|
if (opts.force) {
|
|
@@ -33,16 +38,11 @@ export async function runInit(dir, opts = {}) {
|
|
|
33
38
|
throw new Error(`${name} already exists`);
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
|
-
console.log(
|
|
37
|
-
// Detect skills
|
|
38
|
-
const skills = await detectSkills(dir);
|
|
39
|
-
// Derive eval filename: <skill-name>.eval.ts or <dirname>.eval.ts
|
|
40
|
-
const dirName = path.basename(dir);
|
|
41
|
+
console.log(`\n${commandName} init\n`);
|
|
41
42
|
if (skills.length === 0) {
|
|
42
43
|
console.log(' No SKILL.md found. Creating a generic template.');
|
|
43
44
|
console.log(' Place a SKILL.md in this directory for better scaffolding.\n');
|
|
44
|
-
|
|
45
|
-
await writeTemplate(evalPath, 'my-skill', 'Describe what the agent should do with this skill.');
|
|
45
|
+
await writeTemplate(evalPath, 'my-skill', 'Describe what the agent should do with this skill.', commandName);
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
console.log(` Found ${skills.length} skill(s): ${skills.map(s => s.name).join(', ')}\n`);
|
|
@@ -61,17 +61,15 @@ export async function runInit(dir, opts = {}) {
|
|
|
61
61
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
62
62
|
const hasApiKey = !!(anthropicKey || openaiKey);
|
|
63
63
|
const cliAvailable = await isClaudeCliAvailable();
|
|
64
|
-
const evalName = skills.length === 1 ? skills[0].name : dirName;
|
|
65
|
-
const evalPath = path.join(dir, `${evalName}.eval.ts`);
|
|
66
64
|
if (hasApiKey || cliAvailable) {
|
|
67
65
|
const { Spinner, fmt } = await import('../utils/cli.js');
|
|
68
66
|
const label = 'generating eval with available LLM backend';
|
|
69
67
|
const spinner = new Spinner('init', label);
|
|
70
68
|
try {
|
|
71
69
|
const config = await generateWithLLM(skills);
|
|
72
|
-
await
|
|
70
|
+
await writeEvalFile(evalPath, config);
|
|
73
71
|
spinner.stop(fmt.green(`created ${path.basename(evalPath)}`));
|
|
74
|
-
console.log(` Review and edit the file, then run:
|
|
72
|
+
console.log(` Review and edit the file, then run: ${commandName}\n`);
|
|
75
73
|
return;
|
|
76
74
|
}
|
|
77
75
|
catch (err) {
|
|
@@ -86,9 +84,9 @@ export async function runInit(dir, opts = {}) {
|
|
|
86
84
|
const skill = skills[0];
|
|
87
85
|
const taskName = `test-${skill.name}`;
|
|
88
86
|
const instruction = extractInstructionHint(skill.skillMd);
|
|
89
|
-
await writeTemplate(evalPath, taskName, instruction);
|
|
87
|
+
await writeTemplate(evalPath, taskName, instruction, commandName);
|
|
90
88
|
}
|
|
91
|
-
async function writeTemplate(evalPath, taskName, instruction) {
|
|
89
|
+
async function writeTemplate(evalPath, taskName, instruction, commandName) {
|
|
92
90
|
const templatePath = path.join(import.meta.dirname, '..', '..', 'templates', 'eval.ts.template');
|
|
93
91
|
let template;
|
|
94
92
|
if (await fs.pathExists(templatePath)) {
|
|
@@ -101,9 +99,38 @@ async function writeTemplate(evalPath, taskName, instruction) {
|
|
|
101
99
|
const result = template
|
|
102
100
|
.replace(/\{\{TASK_NAME\}\}/g, taskName)
|
|
103
101
|
.replace(/\{\{INSTRUCTION\}\}/g, instruction);
|
|
104
|
-
await
|
|
102
|
+
await writeEvalFile(evalPath, result);
|
|
105
103
|
console.log(` Created ${path.basename(evalPath)}.`);
|
|
106
|
-
console.log(` Edit the file to define your eval tasks, then run:
|
|
104
|
+
console.log(` Edit the file to define your eval tasks, then run: ${commandName}\n`);
|
|
105
|
+
}
|
|
106
|
+
function resolveEvalPath(dir, evalName) {
|
|
107
|
+
const baseDir = path.resolve(dir);
|
|
108
|
+
if (!evalName
|
|
109
|
+
|| evalName === '.'
|
|
110
|
+
|| evalName === '..'
|
|
111
|
+
|| path.basename(evalName) !== evalName
|
|
112
|
+
|| evalName.includes('/')
|
|
113
|
+
|| evalName.includes('\\')) {
|
|
114
|
+
throw new Error(`Invalid skill name for eval filename: ${JSON.stringify(evalName)}`);
|
|
115
|
+
}
|
|
116
|
+
const evalPath = path.resolve(baseDir, `${evalName}.eval.ts`);
|
|
117
|
+
if (path.dirname(evalPath) !== baseDir) {
|
|
118
|
+
throw new Error(`Eval filename escapes the target directory: ${JSON.stringify(evalName)}`);
|
|
119
|
+
}
|
|
120
|
+
return evalPath;
|
|
121
|
+
}
|
|
122
|
+
async function writeEvalFile(evalPath, content) {
|
|
123
|
+
try {
|
|
124
|
+
const stat = await fs.lstat(evalPath);
|
|
125
|
+
if (stat.isSymbolicLink()) {
|
|
126
|
+
throw new Error(`Refusing to write eval through symbolic link: ${path.basename(evalPath)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error.code !== 'ENOENT')
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
await fs.writeFile(evalPath, content, 'utf-8');
|
|
107
134
|
}
|
|
108
135
|
/**
|
|
109
136
|
* Extract a reasonable instruction hint from SKILL.md content.
|
|
@@ -15,6 +15,8 @@ import fs from 'fs-extra';
|
|
|
15
15
|
import { findSkillRoot } from '../affected/anchor.js';
|
|
16
16
|
import { parsePathgradeMeta } from '../affected/meta.js';
|
|
17
17
|
import { discoverEvalFiles } from './affected.js';
|
|
18
|
+
import * as ts from 'typescript';
|
|
19
|
+
import { hasEvalSdkImport } from '../evals/sdk-import.js';
|
|
18
20
|
/**
|
|
19
21
|
* Run the validate command. Returns exit code (0 = valid, 1 = errors).
|
|
20
22
|
*/
|
|
@@ -40,8 +42,12 @@ export async function runValidate(filePath, opts = {}) {
|
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
// Check 3: Imports
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
46
|
+
if (!hasEvalSdkImport(sourceFile)) {
|
|
47
|
+
errors.push({
|
|
48
|
+
check: 'imports-pathgrade',
|
|
49
|
+
message: 'No recognizable eval SDK import found.',
|
|
50
|
+
});
|
|
45
51
|
}
|
|
46
52
|
// Check 4: describe/it blocks
|
|
47
53
|
if (!/describe\s*\(/.test(content) || !/it\s*\(/.test(content)) {
|
package/dist/evals/discovery.js
CHANGED
|
@@ -2,8 +2,8 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import picomatch from 'picomatch';
|
|
4
4
|
import * as ts from 'typescript';
|
|
5
|
+
import { hasEvalSdkImport } from './sdk-import.js';
|
|
5
6
|
const EVAL_SUFFIX = '.eval.ts';
|
|
6
|
-
const PATHGRADE_PACKAGE = '@wix/pathgrade';
|
|
7
7
|
export function discoverPathgradeEvalFiles(opts) {
|
|
8
8
|
const { cwd, include, exclude } = opts;
|
|
9
9
|
const root = path.resolve(cwd);
|
|
@@ -48,16 +48,8 @@ export function isPathgradeEval(absPath) {
|
|
|
48
48
|
const source = fs.readFileSync(absPath, 'utf8');
|
|
49
49
|
const sourceFile = ts.createSourceFile(absPath, source, ts.ScriptTarget.Latest,
|
|
50
50
|
/* setParentNodes */ false, ts.ScriptKind.TS);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
function isPathgradeImport(stmt) {
|
|
58
|
-
if (!ts.isImportDeclaration(stmt))
|
|
59
|
-
return false;
|
|
60
|
-
return ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === PATHGRADE_PACKAGE;
|
|
51
|
+
return hasEvalSdkImport(sourceFile)
|
|
52
|
+
|| sourceFile.statements.some(isPathgradeMetaExport);
|
|
61
53
|
}
|
|
62
54
|
function isPathgradeMetaExport(stmt) {
|
|
63
55
|
if (!ts.isVariableStatement(stmt))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
const PATHGRADE_PACKAGE = '@wix/pathgrade';
|
|
3
|
+
const SUPPORTING_SDK_IMPORTS = new Set([
|
|
4
|
+
'Agent',
|
|
5
|
+
'check',
|
|
6
|
+
'judge',
|
|
7
|
+
'score',
|
|
8
|
+
'toolUsage',
|
|
9
|
+
]);
|
|
10
|
+
/** Detects the Pathgrade SDK shape without depending on a module specifier. */
|
|
11
|
+
export function hasEvalSdkImport(sourceFile) {
|
|
12
|
+
const sdkSignalsBySource = new Map();
|
|
13
|
+
const namespaceSources = new Map();
|
|
14
|
+
for (const stmt of sourceFile.statements) {
|
|
15
|
+
if (!ts.isImportDeclaration(stmt))
|
|
16
|
+
continue;
|
|
17
|
+
if (ts.isStringLiteral(stmt.moduleSpecifier)
|
|
18
|
+
&& stmt.moduleSpecifier.text === PATHGRADE_PACKAGE)
|
|
19
|
+
return true;
|
|
20
|
+
const bindings = stmt.importClause?.namedBindings;
|
|
21
|
+
const importSource = ts.isStringLiteral(stmt.moduleSpecifier)
|
|
22
|
+
? stmt.moduleSpecifier.text
|
|
23
|
+
: stmt.moduleSpecifier.getText(sourceFile);
|
|
24
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
25
|
+
namespaceSources.set(bindings.name.text, importSource);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
29
|
+
const importedNames = sdkSignalsBySource.get(importSource) ?? new Set();
|
|
30
|
+
for (const element of bindings.elements) {
|
|
31
|
+
importedNames.add(element.propertyName?.text ?? element.name.text);
|
|
32
|
+
}
|
|
33
|
+
sdkSignalsBySource.set(importSource, importedNames);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function addNamespaceMember(namespace, member) {
|
|
37
|
+
const importSource = namespaceSources.get(namespace);
|
|
38
|
+
if (!importSource)
|
|
39
|
+
return;
|
|
40
|
+
const members = sdkSignalsBySource.get(importSource) ?? new Set();
|
|
41
|
+
members.add(member);
|
|
42
|
+
sdkSignalsBySource.set(importSource, members);
|
|
43
|
+
}
|
|
44
|
+
function visit(node) {
|
|
45
|
+
if (ts.isPropertyAccessExpression(node)
|
|
46
|
+
&& ts.isIdentifier(node.expression)
|
|
47
|
+
&& namespaceSources.has(node.expression.text)) {
|
|
48
|
+
addNamespaceMember(node.expression.text, node.name.text);
|
|
49
|
+
}
|
|
50
|
+
if (ts.isQualifiedName(node)
|
|
51
|
+
&& ts.isIdentifier(node.left)
|
|
52
|
+
&& namespaceSources.has(node.left.text)) {
|
|
53
|
+
addNamespaceMember(node.left.text, node.right.text);
|
|
54
|
+
}
|
|
55
|
+
if (ts.isVariableDeclaration(node)
|
|
56
|
+
&& ts.isObjectBindingPattern(node.name)
|
|
57
|
+
&& node.initializer
|
|
58
|
+
&& ts.isIdentifier(node.initializer)
|
|
59
|
+
&& namespaceSources.has(node.initializer.text)) {
|
|
60
|
+
for (const element of node.name.elements) {
|
|
61
|
+
const importedName = element.propertyName ?? element.name;
|
|
62
|
+
if (ts.isIdentifier(importedName)) {
|
|
63
|
+
addNamespaceMember(node.initializer.text, importedName.text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
ts.forEachChild(node, visit);
|
|
68
|
+
}
|
|
69
|
+
visit(sourceFile);
|
|
70
|
+
return [...sdkSignalsBySource.values()].some(matchesSdkShape);
|
|
71
|
+
}
|
|
72
|
+
function matchesSdkShape(names) {
|
|
73
|
+
return names.has('evaluate')
|
|
74
|
+
&& (names.has('createAgent')
|
|
75
|
+
|| [...SUPPORTING_SDK_IMPORTS].some(name => names.has(name)));
|
|
76
|
+
}
|
package/dist/pathgrade.d.ts
CHANGED
|
@@ -7,4 +7,8 @@
|
|
|
7
7
|
* pathgrade init [--force] Generate eval scaffolding
|
|
8
8
|
* pathgrade preview [browser] View results (CLI default, or browser)
|
|
9
9
|
*/
|
|
10
|
-
export {
|
|
10
|
+
export interface PathgradeCliOptions {
|
|
11
|
+
name?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function runPathgradeCli(options?: PathgradeCliOptions): Promise<void>;
|
package/dist/pathgrade.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { runInit } from './commands/init.js';
|
|
13
14
|
import { runAnalyze } from './commands/analyze.js';
|
|
14
15
|
import { runValidate, runValidateAffected } from './commands/validate.js';
|
|
@@ -58,15 +59,20 @@ function validateApiKeys() {
|
|
|
58
59
|
` ${fmt.dim(' Claude CLI auth (keychain) and Codex exec cached login (~/.codex/auth.json) may still work if installed.')}\n`);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
async function
|
|
62
|
+
export async function runPathgradeCli(options = {}) {
|
|
63
|
+
const cliName = options.name?.trim() || 'pathgrade';
|
|
62
64
|
shutdown.install();
|
|
63
65
|
const args = process.argv.slice(2);
|
|
64
66
|
const command = args[0];
|
|
65
67
|
if (command === '--help' || command === '-h') {
|
|
66
|
-
printHelp();
|
|
68
|
+
printHelp(cliName);
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
69
71
|
if (command === '--version' || command === '-v') {
|
|
72
|
+
if (options.version) {
|
|
73
|
+
console.log(options.version);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
70
76
|
const pkg = JSON.parse(await import('fs').then(fs => fs.promises.readFile(new URL('../package.json', import.meta.url), 'utf-8')));
|
|
71
77
|
console.log(pkg.version);
|
|
72
78
|
return;
|
|
@@ -89,7 +95,7 @@ async function main() {
|
|
|
89
95
|
}
|
|
90
96
|
const filePath = validateArgs[0];
|
|
91
97
|
if (!filePath) {
|
|
92
|
-
console.error(
|
|
98
|
+
console.error(`Usage: ${cliName} validate <file.eval.ts> | ${cliName} validate --affected`);
|
|
93
99
|
process.exitCode = 1;
|
|
94
100
|
return;
|
|
95
101
|
}
|
|
@@ -99,13 +105,16 @@ async function main() {
|
|
|
99
105
|
}
|
|
100
106
|
if (command === 'init') {
|
|
101
107
|
const hasForce = args.includes('--force');
|
|
102
|
-
await runInit(process.cwd(), {
|
|
108
|
+
await runInit(process.cwd(), {
|
|
109
|
+
force: hasForce,
|
|
110
|
+
commandName: cliName,
|
|
111
|
+
});
|
|
103
112
|
return;
|
|
104
113
|
}
|
|
105
114
|
if (command === 'clean') {
|
|
106
115
|
const result = await runClean(process.cwd(), parseCleanArgs(args.slice(1)));
|
|
107
116
|
const action = result.dryRun ? 'would remove' : 'removed';
|
|
108
|
-
console.log(
|
|
117
|
+
console.log(`${cliName}: ${action} ${result.removed} debug run(s); ` +
|
|
109
118
|
`retained ${result.retained}; active ${result.active}`);
|
|
110
119
|
return;
|
|
111
120
|
}
|
|
@@ -159,7 +168,7 @@ async function main() {
|
|
|
159
168
|
validateApiKeys();
|
|
160
169
|
const parsed = parsePathgradeRunArgs(command === 'run' ? args.slice(1) : args);
|
|
161
170
|
for (const warning of parsed.warnings ?? []) {
|
|
162
|
-
console.error(
|
|
171
|
+
console.error(`${cliName}: ${warning}`);
|
|
163
172
|
}
|
|
164
173
|
if (parsed.changed) {
|
|
165
174
|
const exitCode = await runChanged({
|
|
@@ -194,15 +203,15 @@ async function main() {
|
|
|
194
203
|
return;
|
|
195
204
|
}
|
|
196
205
|
console.error(`Unknown command: ${command}`);
|
|
197
|
-
console.error(
|
|
206
|
+
console.error(`Run "${cliName} --help" for usage.`);
|
|
198
207
|
process.exitCode = 1;
|
|
199
208
|
}
|
|
200
|
-
function printHelp() {
|
|
209
|
+
function printHelp(cliName) {
|
|
201
210
|
console.log(`
|
|
202
|
-
|
|
211
|
+
${cliName} - Evaluate AI agent skills with a runner adapter
|
|
203
212
|
|
|
204
213
|
Usage:
|
|
205
|
-
|
|
214
|
+
${cliName} run [-- runner-args] Run evals (loads .env, delegates to the selected adapter)
|
|
206
215
|
[--changed] Run only evals affected by the current PR/change-set
|
|
207
216
|
[--since=<ref>] Override base ref (implies git mode)
|
|
208
217
|
[--changed-files=<path>] Use an explicit newline-delimited file list
|
|
@@ -210,22 +219,22 @@ function printHelp() {
|
|
|
210
219
|
[--diagnostics] Print full diagnostics for passing evals too
|
|
211
220
|
[--quiet] Suppress the run-start summary
|
|
212
221
|
[--verbose|-v] Stream live per-turn events to stderr during the run
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
${cliName} init [--force] Generate eval scaffolding
|
|
223
|
+
${cliName} analyze [--skill=X] Analyze skills and output JSON
|
|
224
|
+
${cliName} validate <file> Validate an .eval.ts file
|
|
225
|
+
${cliName} validate --affected Strict: every eval must be anchored or have valid __pathgradeMeta
|
|
226
|
+
${cliName} clean --debug Remove completed debug runs
|
|
218
227
|
[--keep=N] Keep the N newest completed debug runs
|
|
219
228
|
[--dry-run] Report removals without changing files
|
|
220
|
-
|
|
229
|
+
${cliName} preview [browser] View results (CLI default, or browser)
|
|
221
230
|
[--last=N] Show only the N most recent reports
|
|
222
231
|
[--filter=X] Filter reports by test name (substring)
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
${cliName} preview-reactions Preview reactions against a snapshot
|
|
233
|
+
${cliName} report Format .pathgrade/results.json as a markdown PR comment
|
|
225
234
|
[--results-path=<path>] Override results.json location
|
|
226
235
|
[--no-comment] Print markdown to stdout; do not post
|
|
227
236
|
[--comment-id=<id>] Override comment marker (default: $GITHUB_WORKFLOW:$GITHUB_JOB)
|
|
228
|
-
|
|
237
|
+
${cliName} affected Print eval files affected by a change-set (one per line)
|
|
229
238
|
[--since=<ref>] Diff <ref>...HEAD (overrides git auto-detection)
|
|
230
239
|
[--changed-files=<path>] Newline-delimited repo-relative file list
|
|
231
240
|
[--explain] Print human-readable per-eval decision to stderr
|
|
@@ -237,16 +246,19 @@ function printHelp() {
|
|
|
237
246
|
OPENAI_API_KEY API key for Codex and OpenAI-backed judges
|
|
238
247
|
|
|
239
248
|
Examples:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
${cliName} run # run all *.eval.ts files
|
|
250
|
+
${cliName} run --diagnostics # print full diagnostics for passing evals too
|
|
251
|
+
${cliName} run --verbose # stream per-turn events live to stderr while evals run
|
|
252
|
+
${cliName} run -- --grep superlint # filter by test name
|
|
253
|
+
${cliName} init # scaffold eval files
|
|
254
|
+
${cliName} preview browser # open web UI
|
|
255
|
+
${cliName} preview-reactions --snapshot ./pathgrade-debug/run-snapshot.json --reactions ./reactions.ts
|
|
247
256
|
`);
|
|
248
257
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
258
|
+
if (process.argv[1]
|
|
259
|
+
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
260
|
+
runPathgradeCli().catch(err => {
|
|
261
|
+
console.error(err);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
264
|
+
}
|
package/dist/sdk/agent.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
|
|
|
15
15
|
import fs from 'fs-extra';
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } from '../providers/debug-runs.js';
|
|
18
|
-
import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode
|
|
18
|
+
import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode/contract.js';
|
|
19
19
|
/**
|
|
20
20
|
* Test-only injection point: override the sink used by the next emitter
|
|
21
21
|
* built inside `createAgent`. Pass `null` to restore the default (stderr).
|
|
@@ -35,13 +35,14 @@ export async function runJudgeSession(input, options = {}) {
|
|
|
35
35
|
{ role: 'user', content: user },
|
|
36
36
|
];
|
|
37
37
|
let rounds = 0;
|
|
38
|
+
let scoreRepairAttempted = false;
|
|
38
39
|
while (rounds < maxRounds) {
|
|
39
40
|
rounds++;
|
|
40
41
|
let response;
|
|
41
42
|
try {
|
|
42
43
|
response = await llm.callWithTools(messages, {
|
|
43
44
|
system,
|
|
44
|
-
tools: toolSchemas,
|
|
45
|
+
tools: scoreRepairAttempted ? [] : toolSchemas,
|
|
45
46
|
model: scorer.model,
|
|
46
47
|
cacheControl: scorer.cacheControl,
|
|
47
48
|
});
|
|
@@ -63,6 +64,22 @@ export async function runJudgeSession(input, options = {}) {
|
|
|
63
64
|
}
|
|
64
65
|
const parsed = parseFinalScore(response.text);
|
|
65
66
|
if (!parsed.ok) {
|
|
67
|
+
if (parsed.message.startsWith('JSON parse failed:')
|
|
68
|
+
&& !scoreRepairAttempted
|
|
69
|
+
&& rounds < maxRounds) {
|
|
70
|
+
scoreRepairAttempted = true;
|
|
71
|
+
messages.push({ role: 'assistant', content: response.text });
|
|
72
|
+
messages.push({
|
|
73
|
+
role: 'user',
|
|
74
|
+
content: [
|
|
75
|
+
`Your final answer was not valid JSON: ${parsed.message}.`,
|
|
76
|
+
'Return ONLY a valid JSON object with double-quoted keys in this exact shape:',
|
|
77
|
+
'{"score": <number 0..1>, "details": "<brief explanation>"}',
|
|
78
|
+
'Do not call more tools or include Markdown fences.',
|
|
79
|
+
].join('\n'),
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
66
83
|
return makeOutcome(tokenUsage, toolCalls, logEntries, rounds, {
|
|
67
84
|
code: 'invalid_score',
|
|
68
85
|
details: parsed.message,
|
|
@@ -11,6 +11,11 @@ function resolveBaseUrl(env) {
|
|
|
11
11
|
|| process.env.APP_ANTHROPIC_BASE_URL
|
|
12
12
|
|| 'https://api.anthropic.com';
|
|
13
13
|
}
|
|
14
|
+
function resolveMessagesUrl(env) {
|
|
15
|
+
const baseUrl = resolveBaseUrl(env).replace(/\/+$/, '');
|
|
16
|
+
const apiRoot = baseUrl.endsWith('/v1') ? baseUrl : `${baseUrl}/v1`;
|
|
17
|
+
return `${apiRoot}/messages`;
|
|
18
|
+
}
|
|
14
19
|
function buildHeaders(apiKey, useCache) {
|
|
15
20
|
const headers = {
|
|
16
21
|
'Content-Type': 'application/json',
|
|
@@ -57,7 +62,7 @@ function resolveTemperature(model, config, temperature) {
|
|
|
57
62
|
return temperature ?? 0;
|
|
58
63
|
}
|
|
59
64
|
async function postAnthropic(apiKey, useCache, body, env) {
|
|
60
|
-
const response = await fetch(
|
|
65
|
+
const response = await fetch(resolveMessagesUrl(env), {
|
|
61
66
|
method: 'POST',
|
|
62
67
|
headers: buildHeaders(apiKey, useCache),
|
|
63
68
|
body: JSON.stringify(body),
|
package/dist/utils/llm.js
CHANGED
|
@@ -199,6 +199,7 @@ export function createAgentLLM(agentName, agentEnv) {
|
|
|
199
199
|
const adapters = agentEnv && Object.keys(agentEnv).length > 0
|
|
200
200
|
? baseAdapters.map((a) => ({
|
|
201
201
|
...a,
|
|
202
|
+
isAvailable: (env) => a.isAvailable({ ...agentEnv, ...env }),
|
|
202
203
|
call: (prompt, opts) => a.call(prompt, { ...opts, env: { ...agentEnv, ...opts.env } }),
|
|
203
204
|
...(a.callWithTools
|
|
204
205
|
? { callWithTools: (messages, opts) => a.callWithTools(messages, { ...opts, env: { ...agentEnv, ...opts.env } }) }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"packageManager": "yarn@4.12.0",
|
|
5
5
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
6
6
|
"exports": {
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./dist/core/mcp-mock.d.ts",
|
|
45
45
|
"default": "./dist/core/mcp-mock.js"
|
|
46
46
|
},
|
|
47
|
+
"./cli": {
|
|
48
|
+
"types": "./dist/pathgrade.d.ts",
|
|
49
|
+
"default": "./dist/pathgrade.js"
|
|
50
|
+
},
|
|
47
51
|
"./package.json": "./package.json"
|
|
48
52
|
},
|
|
49
53
|
"bin": "bin/pathgrade.js",
|
|
@@ -128,5 +132,5 @@
|
|
|
128
132
|
"typescript": "^5.9.3",
|
|
129
133
|
"zod": "4.3.6"
|
|
130
134
|
},
|
|
131
|
-
"falconPackageHash": "
|
|
135
|
+
"falconPackageHash": "be877aa61dfb2cd1b58c716b704cffee9592428675a660382e86e358"
|
|
132
136
|
}
|
|
File without changes
|