@wix/pathgrade 1.0.7 → 1.0.9
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/README.md +2 -1
- package/bin/pathgrade.js +6 -1
- package/dist/commands/clean.d.ts +8 -0
- package/dist/commands/clean.js +94 -0
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +42 -15
- package/dist/commands/run-changed.js +3 -5
- package/dist/commands/runner-env.d.ts +2 -0
- package/dist/commands/runner-env.js +10 -0
- 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 +53 -33
- package/dist/providers/debug-runs.d.ts +23 -0
- package/dist/providers/debug-runs.js +208 -0
- package/dist/sdk/agent.js +45 -18
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/types.d.ts +8 -2
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -312,7 +312,8 @@ const result = await agent.runConversation({
|
|
|
312
312
|
Pathgrade exposes a few useful features that are easy to miss from the basic examples:
|
|
313
313
|
|
|
314
314
|
- `createAgent({ skillDir, workspace })` stages a real skill and a fixture workspace into the sandbox, which is how Pathgrade's skill examples are evaluated.
|
|
315
|
-
- `createAgent({ debug: true })` preserves the final workspace under `pathgrade-debug/<test-name
|
|
315
|
+
- `createAgent({ debug: true })` preserves the final workspace under the backward-compatible `pathgrade-debug/<test-name>/` path; when you use `runConversation()`, it also writes `run-snapshot.json`.
|
|
316
|
+
- `createAgent({ debug: { retainRuns: 5 } })` opts into managed run retention under `pathgrade-debug/runs/<run-id>/<test-name>/`. Use `pathgrade clean --debug`, `--keep=N`, and `--dry-run` to clean marked, inactive debug runs safely.
|
|
316
317
|
- `evaluate.fromSnapshot(snapshotPath, scorers)` re-runs grading against a saved snapshot without re-running the agent.
|
|
317
318
|
- `previewReactions(messages, reactions)` lets you inspect which scripted reactions would fire offline.
|
|
318
319
|
- `conversationWindow` on agents and personas keeps long transcripts bounded with summarization instead of sending the full conversation every turn.
|
package/bin/pathgrade.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CleanDebugRunsResult } from '../providers/debug-runs.js';
|
|
2
|
+
export interface CleanCommandOptions {
|
|
3
|
+
debug: boolean;
|
|
4
|
+
keep?: number;
|
|
5
|
+
dryRun?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseCleanArgs(args: string[]): CleanCommandOptions;
|
|
8
|
+
export declare function runClean(cwd: string, options: CleanCommandOptions): Promise<CleanDebugRunsResult>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cleanDebugRuns, DEBUG_ROOT_MARKER, } from '../providers/debug-runs.js';
|
|
4
|
+
function parseKeep(value) {
|
|
5
|
+
if (value === undefined || !/^\d+$/.test(value)) {
|
|
6
|
+
throw new Error('pathgrade clean: --keep must be a non-negative integer');
|
|
7
|
+
}
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
10
|
+
throw new Error('pathgrade clean: --keep must be a non-negative integer');
|
|
11
|
+
}
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
export function parseCleanArgs(args) {
|
|
15
|
+
let debug = false;
|
|
16
|
+
let dryRun = false;
|
|
17
|
+
let keep;
|
|
18
|
+
for (let index = 0; index < args.length; index++) {
|
|
19
|
+
const arg = args[index];
|
|
20
|
+
if (arg === '--debug') {
|
|
21
|
+
debug = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (arg === '--dry-run') {
|
|
25
|
+
dryRun = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg === '--keep') {
|
|
29
|
+
keep = parseKeep(args[++index]);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg.startsWith('--keep=')) {
|
|
33
|
+
keep = parseKeep(arg.slice('--keep='.length));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`pathgrade clean: unknown option ${arg}`);
|
|
37
|
+
}
|
|
38
|
+
return { debug, ...(keep === undefined ? {} : { keep }), dryRun };
|
|
39
|
+
}
|
|
40
|
+
const SKIPPED_DIRECTORY_NAMES = new Set(['.git', '.worktrees', 'node_modules']);
|
|
41
|
+
async function findDebugRoots(cwd) {
|
|
42
|
+
const roots = [];
|
|
43
|
+
async function visit(dir) {
|
|
44
|
+
let stat;
|
|
45
|
+
try {
|
|
46
|
+
stat = await fs.lstat(dir);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
52
|
+
return;
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (entries.some(entry => entry.isFile() && !entry.isSymbolicLink() && entry.name === DEBUG_ROOT_MARKER)) {
|
|
61
|
+
roots.push(dir);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await Promise.all(entries.map(async (entry) => {
|
|
65
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || SKIPPED_DIRECTORY_NAMES.has(entry.name))
|
|
66
|
+
return;
|
|
67
|
+
await visit(path.join(dir, entry.name));
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
await visit(path.resolve(cwd));
|
|
71
|
+
return roots;
|
|
72
|
+
}
|
|
73
|
+
export async function runClean(cwd, options) {
|
|
74
|
+
if (!options.debug) {
|
|
75
|
+
throw new Error('pathgrade clean requires --debug');
|
|
76
|
+
}
|
|
77
|
+
const roots = await findDebugRoots(cwd);
|
|
78
|
+
const results = await Promise.all(roots.map(rootDir => cleanDebugRuns({
|
|
79
|
+
rootDir,
|
|
80
|
+
keep: options.keep,
|
|
81
|
+
dryRun: options.dryRun,
|
|
82
|
+
})));
|
|
83
|
+
return results.reduce((total, result) => ({
|
|
84
|
+
removed: total.removed + result.removed,
|
|
85
|
+
retained: total.retained + result.retained,
|
|
86
|
+
active: total.active + result.active,
|
|
87
|
+
dryRun: total.dryRun,
|
|
88
|
+
}), {
|
|
89
|
+
removed: 0,
|
|
90
|
+
retained: 0,
|
|
91
|
+
active: 0,
|
|
92
|
+
dryRun: options.dryRun ?? false,
|
|
93
|
+
});
|
|
94
|
+
}
|
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.
|
|
@@ -20,15 +20,13 @@ import { writeSidecar } from '../affected/sidecar.js';
|
|
|
20
20
|
import { discoverPathgradeEvalFiles } from '../evals/discovery.js';
|
|
21
21
|
import { resolvePathgradeConfig } from '../config/pathgrade.js';
|
|
22
22
|
import { loadRunnerInvocationAdapter } from '../runners/adapter-loader.js';
|
|
23
|
+
import { buildRunnerEnv } from './runner-env.js';
|
|
23
24
|
export async function runChanged(opts) {
|
|
24
25
|
const { cwd, parsed } = opts;
|
|
25
26
|
const selectionInvocationId = randomUUID();
|
|
26
|
-
const runnerEnv = {
|
|
27
|
-
...process.env,
|
|
28
|
-
...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
|
|
29
|
-
...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
|
|
27
|
+
const runnerEnv = buildRunnerEnv(parsed, {
|
|
30
28
|
PATHGRADE_SELECTION_INVOCATION_ID: selectionInvocationId,
|
|
31
|
-
};
|
|
29
|
+
});
|
|
32
30
|
const configPath = findVitestConfigArg(parsed.runnerArgs);
|
|
33
31
|
let config;
|
|
34
32
|
let runnerInvocation;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveDebugRunId } from '../providers/debug-runs.js';
|
|
2
|
+
export function buildRunnerEnv(parsed, additions = {}) {
|
|
3
|
+
return {
|
|
4
|
+
...process.env,
|
|
5
|
+
PATHGRADE_DEBUG_RUN_ID: resolveDebugRunId(),
|
|
6
|
+
...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
|
|
7
|
+
...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
|
|
8
|
+
...additions,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -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';
|
|
@@ -18,11 +19,13 @@ import { runPreviewReactions } from './commands/preview-reactions.js';
|
|
|
18
19
|
import { runReport } from './commands/report.js';
|
|
19
20
|
import { runAffected } from './commands/affected.js';
|
|
20
21
|
import { runChanged } from './commands/run-changed.js';
|
|
22
|
+
import { parseCleanArgs, runClean } from './commands/clean.js';
|
|
21
23
|
import { clearSidecar } from './affected/sidecar.js';
|
|
22
24
|
import { resolvePathgradeConfig } from './config/pathgrade.js';
|
|
23
25
|
import { loadRunnerInvocationAdapter } from './runners/adapter-loader.js';
|
|
24
26
|
import { fmt } from './utils/cli.js';
|
|
25
27
|
import { shutdown } from './utils/shutdown.js';
|
|
28
|
+
import { buildRunnerEnv } from './commands/runner-env.js';
|
|
26
29
|
function loadDotenv() {
|
|
27
30
|
const envPath = path.resolve(process.cwd(), '.env');
|
|
28
31
|
if (!fs.existsSync(envPath))
|
|
@@ -56,15 +59,20 @@ function validateApiKeys() {
|
|
|
56
59
|
` ${fmt.dim(' Claude CLI auth (keychain) and Codex exec cached login (~/.codex/auth.json) may still work if installed.')}\n`);
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
|
-
async function
|
|
62
|
+
export async function runPathgradeCli(options = {}) {
|
|
63
|
+
const cliName = options.name?.trim() || 'pathgrade';
|
|
60
64
|
shutdown.install();
|
|
61
65
|
const args = process.argv.slice(2);
|
|
62
66
|
const command = args[0];
|
|
63
67
|
if (command === '--help' || command === '-h') {
|
|
64
|
-
printHelp();
|
|
68
|
+
printHelp(cliName);
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
if (command === '--version' || command === '-v') {
|
|
72
|
+
if (options.version) {
|
|
73
|
+
console.log(options.version);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
68
76
|
const pkg = JSON.parse(await import('fs').then(fs => fs.promises.readFile(new URL('../package.json', import.meta.url), 'utf-8')));
|
|
69
77
|
console.log(pkg.version);
|
|
70
78
|
return;
|
|
@@ -87,7 +95,7 @@ async function main() {
|
|
|
87
95
|
}
|
|
88
96
|
const filePath = validateArgs[0];
|
|
89
97
|
if (!filePath) {
|
|
90
|
-
console.error(
|
|
98
|
+
console.error(`Usage: ${cliName} validate <file.eval.ts> | ${cliName} validate --affected`);
|
|
91
99
|
process.exitCode = 1;
|
|
92
100
|
return;
|
|
93
101
|
}
|
|
@@ -97,7 +105,17 @@ async function main() {
|
|
|
97
105
|
}
|
|
98
106
|
if (command === 'init') {
|
|
99
107
|
const hasForce = args.includes('--force');
|
|
100
|
-
await runInit(process.cwd(), {
|
|
108
|
+
await runInit(process.cwd(), {
|
|
109
|
+
force: hasForce,
|
|
110
|
+
commandName: cliName,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (command === 'clean') {
|
|
115
|
+
const result = await runClean(process.cwd(), parseCleanArgs(args.slice(1)));
|
|
116
|
+
const action = result.dryRun ? 'would remove' : 'removed';
|
|
117
|
+
console.log(`${cliName}: ${action} ${result.removed} debug run(s); ` +
|
|
118
|
+
`retained ${result.retained}; active ${result.active}`);
|
|
101
119
|
return;
|
|
102
120
|
}
|
|
103
121
|
if (command === 'preview') {
|
|
@@ -150,7 +168,7 @@ async function main() {
|
|
|
150
168
|
validateApiKeys();
|
|
151
169
|
const parsed = parsePathgradeRunArgs(command === 'run' ? args.slice(1) : args);
|
|
152
170
|
for (const warning of parsed.warnings ?? []) {
|
|
153
|
-
console.error(
|
|
171
|
+
console.error(`${cliName}: ${warning}`);
|
|
154
172
|
}
|
|
155
173
|
if (parsed.changed) {
|
|
156
174
|
const exitCode = await runChanged({
|
|
@@ -164,11 +182,7 @@ async function main() {
|
|
|
164
182
|
// previous `--changed` run so it doesn't leak into this full-suite
|
|
165
183
|
// run (the reporter would otherwise merge old metadata).
|
|
166
184
|
await clearSidecar(process.cwd());
|
|
167
|
-
const env =
|
|
168
|
-
...process.env,
|
|
169
|
-
...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
|
|
170
|
-
...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
|
|
171
|
-
};
|
|
185
|
+
const env = buildRunnerEnv(parsed);
|
|
172
186
|
try {
|
|
173
187
|
const config = await resolvePathgradeConfig({ cwd: process.cwd() });
|
|
174
188
|
const runner = await loadRunnerInvocationAdapter({
|
|
@@ -189,15 +203,15 @@ async function main() {
|
|
|
189
203
|
return;
|
|
190
204
|
}
|
|
191
205
|
console.error(`Unknown command: ${command}`);
|
|
192
|
-
console.error(
|
|
206
|
+
console.error(`Run "${cliName} --help" for usage.`);
|
|
193
207
|
process.exitCode = 1;
|
|
194
208
|
}
|
|
195
|
-
function printHelp() {
|
|
209
|
+
function printHelp(cliName) {
|
|
196
210
|
console.log(`
|
|
197
|
-
|
|
211
|
+
${cliName} - Evaluate AI agent skills with a runner adapter
|
|
198
212
|
|
|
199
213
|
Usage:
|
|
200
|
-
|
|
214
|
+
${cliName} run [-- runner-args] Run evals (loads .env, delegates to the selected adapter)
|
|
201
215
|
[--changed] Run only evals affected by the current PR/change-set
|
|
202
216
|
[--since=<ref>] Override base ref (implies git mode)
|
|
203
217
|
[--changed-files=<path>] Use an explicit newline-delimited file list
|
|
@@ -205,19 +219,22 @@ function printHelp() {
|
|
|
205
219
|
[--diagnostics] Print full diagnostics for passing evals too
|
|
206
220
|
[--quiet] Suppress the run-start summary
|
|
207
221
|
[--verbose|-v] Stream live per-turn events to stderr during the run
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
227
|
+
[--keep=N] Keep the N newest completed debug runs
|
|
228
|
+
[--dry-run] Report removals without changing files
|
|
229
|
+
${cliName} preview [browser] View results (CLI default, or browser)
|
|
213
230
|
[--last=N] Show only the N most recent reports
|
|
214
231
|
[--filter=X] Filter reports by test name (substring)
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
${cliName} preview-reactions Preview reactions against a snapshot
|
|
233
|
+
${cliName} report Format .pathgrade/results.json as a markdown PR comment
|
|
217
234
|
[--results-path=<path>] Override results.json location
|
|
218
235
|
[--no-comment] Print markdown to stdout; do not post
|
|
219
236
|
[--comment-id=<id>] Override comment marker (default: $GITHUB_WORKFLOW:$GITHUB_JOB)
|
|
220
|
-
|
|
237
|
+
${cliName} affected Print eval files affected by a change-set (one per line)
|
|
221
238
|
[--since=<ref>] Diff <ref>...HEAD (overrides git auto-detection)
|
|
222
239
|
[--changed-files=<path>] Newline-delimited repo-relative file list
|
|
223
240
|
[--explain] Print human-readable per-eval decision to stderr
|
|
@@ -229,16 +246,19 @@ function printHelp() {
|
|
|
229
246
|
OPENAI_API_KEY API key for Codex and OpenAI-backed judges
|
|
230
247
|
|
|
231
248
|
Examples:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
239
256
|
`);
|
|
240
257
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const DEBUG_ROOT_MARKER = ".pathgrade-debug-root.json";
|
|
2
|
+
export declare const DEBUG_RUN_MARKER = ".pathgrade-debug-run.json";
|
|
3
|
+
export declare const DEFAULT_DEBUG_RETAIN_RUNS = 3;
|
|
4
|
+
export declare function resolveDebugRunId(): string;
|
|
5
|
+
export interface CleanDebugRunsResult {
|
|
6
|
+
removed: number;
|
|
7
|
+
retained: number;
|
|
8
|
+
active: number;
|
|
9
|
+
dryRun: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function prepareManagedDebugRun(input: {
|
|
12
|
+
rootDir: string;
|
|
13
|
+
debugName: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
destination: string;
|
|
16
|
+
rootDir: string;
|
|
17
|
+
runId: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function cleanDebugRuns(input: {
|
|
20
|
+
rootDir: string;
|
|
21
|
+
keep?: number;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
}): Promise<CleanDebugRunsResult>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const DEBUG_ROOT_MARKER = '.pathgrade-debug-root.json';
|
|
5
|
+
export const DEBUG_RUN_MARKER = '.pathgrade-debug-run.json';
|
|
6
|
+
export const DEFAULT_DEBUG_RETAIN_RUNS = 3;
|
|
7
|
+
let generatedRunId;
|
|
8
|
+
function createRunId() {
|
|
9
|
+
generatedRunId ??= `${new Date().toISOString().replace(/[:.]/g, '-')}-${Math.random().toString(36).slice(2, 8)}`;
|
|
10
|
+
return generatedRunId;
|
|
11
|
+
}
|
|
12
|
+
export function resolveDebugRunId() {
|
|
13
|
+
const configured = process.env.PATHGRADE_DEBUG_RUN_ID;
|
|
14
|
+
if (configured && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(configured))
|
|
15
|
+
return configured;
|
|
16
|
+
return createRunId();
|
|
17
|
+
}
|
|
18
|
+
async function readJsonFile(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
const stat = await fs.lstat(filePath);
|
|
21
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
22
|
+
return undefined;
|
|
23
|
+
return await fs.readJson(filePath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function isOwnedDebugRoot(rootDir) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = await fs.lstat(rootDir);
|
|
32
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const marker = await readJsonFile(path.join(rootDir, DEBUG_ROOT_MARKER));
|
|
39
|
+
return marker?.version === 1;
|
|
40
|
+
}
|
|
41
|
+
async function ensureOwnedDebugRoot(rootDir) {
|
|
42
|
+
await fs.ensureDir(rootDir);
|
|
43
|
+
const rootStat = await fs.lstat(rootDir);
|
|
44
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
45
|
+
throw new Error(`Pathgrade debug root must be a real directory: ${rootDir}`);
|
|
46
|
+
}
|
|
47
|
+
const markerPath = path.join(rootDir, DEBUG_ROOT_MARKER);
|
|
48
|
+
let markerExists = false;
|
|
49
|
+
try {
|
|
50
|
+
const markerStat = await fs.lstat(markerPath);
|
|
51
|
+
markerExists = true;
|
|
52
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink()) {
|
|
53
|
+
throw new Error(`Pathgrade debug root has an unsafe ownership marker: ${rootDir}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code !== 'ENOENT')
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (!markerExists) {
|
|
61
|
+
await fs.writeJson(markerPath, { version: 1 }, { spaces: 2 });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const existing = await readJsonFile(markerPath);
|
|
65
|
+
if (existing?.version !== 1) {
|
|
66
|
+
throw new Error(`Pathgrade debug root has an unsupported ownership marker: ${rootDir}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function isRealDirectory(directory) {
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.lstat(directory);
|
|
72
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function ensureRealChildDirectory(directory) {
|
|
79
|
+
try {
|
|
80
|
+
await fs.mkdir(directory);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== 'EEXIST')
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (!await isRealDirectory(directory)) {
|
|
87
|
+
throw new Error(`Pathgrade managed debug path must be a real directory: ${directory}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function ensureRunMarker(runDir, runId) {
|
|
91
|
+
const markerPath = path.join(runDir, DEBUG_RUN_MARKER);
|
|
92
|
+
const temporaryMarker = `${markerPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
93
|
+
await fs.writeJson(temporaryMarker, {
|
|
94
|
+
version: 1,
|
|
95
|
+
runId,
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
}, { spaces: 2 });
|
|
98
|
+
try {
|
|
99
|
+
await fs.link(temporaryMarker, markerPath);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code !== 'EEXIST')
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await fs.remove(temporaryMarker);
|
|
107
|
+
}
|
|
108
|
+
const marker = await readJsonFile(markerPath);
|
|
109
|
+
if (marker?.version !== 1 ||
|
|
110
|
+
marker.runId !== runId ||
|
|
111
|
+
typeof marker.createdAt !== 'string' ||
|
|
112
|
+
!Number.isFinite(Date.parse(marker.createdAt))) {
|
|
113
|
+
throw new Error(`Pathgrade debug run has an unsafe ownership marker: ${runDir}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function prepareManagedDebugRun(input) {
|
|
117
|
+
const rootDir = path.resolve(input.rootDir);
|
|
118
|
+
await ensureOwnedDebugRoot(rootDir);
|
|
119
|
+
const runId = resolveDebugRunId();
|
|
120
|
+
const runsDir = path.join(rootDir, 'runs');
|
|
121
|
+
const runDir = path.join(runsDir, runId);
|
|
122
|
+
const activeDir = path.join(runDir, '.pathgrade-active');
|
|
123
|
+
await ensureRealChildDirectory(runsDir);
|
|
124
|
+
await ensureRealChildDirectory(runDir);
|
|
125
|
+
await ensureRealChildDirectory(activeDir);
|
|
126
|
+
await fs.writeFile(path.join(activeDir, String(process.pid)), '');
|
|
127
|
+
await ensureRunMarker(runDir, runId);
|
|
128
|
+
return {
|
|
129
|
+
destination: input.debugName ? path.join(runDir, input.debugName) : runDir,
|
|
130
|
+
rootDir,
|
|
131
|
+
runId,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function readOwnedRun(runDir) {
|
|
135
|
+
const marker = await readJsonFile(path.join(runDir, DEBUG_RUN_MARKER));
|
|
136
|
+
if (marker?.version !== 1 ||
|
|
137
|
+
typeof marker.runId !== 'string' ||
|
|
138
|
+
typeof marker.createdAt !== 'string' ||
|
|
139
|
+
!Number.isFinite(Date.parse(marker.createdAt))) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
if (path.basename(runDir) !== marker.runId)
|
|
143
|
+
return undefined;
|
|
144
|
+
return marker;
|
|
145
|
+
}
|
|
146
|
+
function isProcessAlive(pid) {
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pid, 0);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return error.code !== 'ESRCH';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function isRunActive(runDir) {
|
|
156
|
+
const activeDir = path.join(runDir, '.pathgrade-active');
|
|
157
|
+
const entries = await fs.readdir(activeDir, { withFileTypes: true }).catch(() => []);
|
|
158
|
+
return entries.some(entry => {
|
|
159
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !/^\d+$/.test(entry.name))
|
|
160
|
+
return false;
|
|
161
|
+
const pid = Number(entry.name);
|
|
162
|
+
return Number.isSafeInteger(pid) && pid > 0 && isProcessAlive(pid);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export async function cleanDebugRuns(input) {
|
|
166
|
+
const dryRun = input.dryRun ?? false;
|
|
167
|
+
if (!await isOwnedDebugRoot(input.rootDir)) {
|
|
168
|
+
return { removed: 0, retained: 0, active: 0, dryRun };
|
|
169
|
+
}
|
|
170
|
+
const runsDir = path.join(input.rootDir, 'runs');
|
|
171
|
+
if (!await isRealDirectory(runsDir)) {
|
|
172
|
+
return { removed: 0, retained: 0, active: 0, dryRun };
|
|
173
|
+
}
|
|
174
|
+
const entries = await fs.readdir(runsDir, { withFileTypes: true }).catch(() => []);
|
|
175
|
+
const ownedRuns = [];
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
178
|
+
continue;
|
|
179
|
+
const dir = path.join(runsDir, entry.name);
|
|
180
|
+
const marker = await readOwnedRun(dir);
|
|
181
|
+
if (marker)
|
|
182
|
+
ownedRuns.push({ dir, marker, active: await isRunActive(dir) });
|
|
183
|
+
}
|
|
184
|
+
const activeRuns = ownedRuns.filter(run => run.active);
|
|
185
|
+
const completedRuns = ownedRuns.filter(run => !run.active);
|
|
186
|
+
completedRuns.sort((a, b) => b.marker.createdAt.localeCompare(a.marker.createdAt));
|
|
187
|
+
const keep = input.keep ?? 0;
|
|
188
|
+
const retained = completedRuns.slice(0, keep);
|
|
189
|
+
const removable = completedRuns.slice(keep);
|
|
190
|
+
let removed = removable.length;
|
|
191
|
+
let newlyActive = 0;
|
|
192
|
+
if (!dryRun) {
|
|
193
|
+
const removalResults = await Promise.all(removable.map(async (run) => {
|
|
194
|
+
if (await isRunActive(run.dir))
|
|
195
|
+
return false;
|
|
196
|
+
await fs.remove(run.dir);
|
|
197
|
+
return true;
|
|
198
|
+
}));
|
|
199
|
+
removed = removalResults.filter(Boolean).length;
|
|
200
|
+
newlyActive = removalResults.length - removed;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
removed,
|
|
204
|
+
retained: retained.length,
|
|
205
|
+
active: activeRuns.length + newlyActive,
|
|
206
|
+
dryRun,
|
|
207
|
+
};
|
|
208
|
+
}
|
package/dist/sdk/agent.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getCurrentCaseContext } from './case-context.js';
|
|
|
14
14
|
import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
|
|
15
15
|
import fs from 'fs-extra';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } from '../providers/debug-runs.js';
|
|
17
18
|
import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode-contract.js';
|
|
18
19
|
/**
|
|
19
20
|
* Test-only injection point: override the sink used by the next emitter
|
|
@@ -319,26 +320,46 @@ class AgentImpl {
|
|
|
319
320
|
// Runner-owned agents stay tracked until flush consumes metadata.
|
|
320
321
|
// Manual agents have no runner flush, so dispose releases them.
|
|
321
322
|
lifecycleCore.releaseAgent(this);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
? this.debugOpt
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
await fs.
|
|
323
|
+
try {
|
|
324
|
+
await this.activeChatSession?.dispose?.();
|
|
325
|
+
this.activeChatSession = undefined;
|
|
326
|
+
if (this.debugOpt) {
|
|
327
|
+
const managedOptions = typeof this.debugOpt === 'object' ? this.debugOpt : undefined;
|
|
328
|
+
const managed = managedOptions
|
|
329
|
+
? await prepareManagedDebugRun({
|
|
330
|
+
rootDir: managedOptions.directory
|
|
331
|
+
? path.resolve(this.debugBaseDir, managedOptions.directory)
|
|
332
|
+
: path.join(this.debugBaseDir, 'pathgrade-debug'),
|
|
333
|
+
debugName: this.debugName,
|
|
334
|
+
})
|
|
335
|
+
: undefined;
|
|
336
|
+
const dest = typeof this.debugOpt === 'string'
|
|
337
|
+
? this.debugOpt
|
|
338
|
+
: managed?.destination ?? path.join(this.debugBaseDir, 'pathgrade-debug', this.debugName);
|
|
339
|
+
await fs.remove(dest);
|
|
340
|
+
await fs.copy(this.ws.path, dest);
|
|
341
|
+
if (this.interactionMode === 'runConversation' && this.lastConversationResult) {
|
|
342
|
+
const snapshot = buildRunSnapshot({
|
|
343
|
+
agent: this.agentName,
|
|
344
|
+
messages: this._messages,
|
|
345
|
+
log: this._log,
|
|
346
|
+
conversationResult: this.lastConversationResult,
|
|
347
|
+
workspace: dest,
|
|
348
|
+
});
|
|
349
|
+
await fs.writeJSON(path.join(dest, 'run-snapshot.json'), snapshot, { spaces: 2 });
|
|
350
|
+
}
|
|
351
|
+
if (managed) {
|
|
352
|
+
const retainRuns = managedOptions.retainRuns ?? DEFAULT_DEBUG_RETAIN_RUNS;
|
|
353
|
+
await cleanDebugRuns({
|
|
354
|
+
rootDir: managed.rootDir,
|
|
355
|
+
keep: Math.max(0, retainRuns - 1),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
339
358
|
}
|
|
340
359
|
}
|
|
341
|
-
|
|
360
|
+
finally {
|
|
361
|
+
await this.ws.dispose();
|
|
362
|
+
}
|
|
342
363
|
}
|
|
343
364
|
}
|
|
344
365
|
/**
|
|
@@ -364,6 +385,12 @@ function resolveCaseDebugContext() {
|
|
|
364
385
|
};
|
|
365
386
|
}
|
|
366
387
|
export async function createAgent(opts) {
|
|
388
|
+
if (typeof opts.debug === 'object') {
|
|
389
|
+
const retainRuns = opts.debug.retainRuns ?? DEFAULT_DEBUG_RETAIN_RUNS;
|
|
390
|
+
if (!Number.isSafeInteger(retainRuns) || retainRuns < 1) {
|
|
391
|
+
throw new Error('Pathgrade debug retainRuns must be a positive integer');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
367
394
|
const agentName = resolveAgentName(opts, process.env);
|
|
368
395
|
validateOpenCodeDeclaration(agentName, opts);
|
|
369
396
|
const transport = agentName === 'codex'
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { emitEvalResult, resetAllResultObserversForTests, resetUserResultObserve
|
|
|
21
21
|
export { getAgentCapabilities } from './types.js';
|
|
22
22
|
export type { AgentTransport, AgentCapabilities, AgentName, McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './types.js';
|
|
23
23
|
export type { AskBus, AskBatch, AskQuestion, AskOption, AskAnswer, AskResolution, AskBatchSnapshot, AskAnswerSnapshot, AskResolutionSnapshot, AskHandle, AskHandler, AskSource, AskLifecycle, AskAnswerSource, Unsubscribe as AskBusUnsubscribe, } from './ask-bus/types.js';
|
|
24
|
-
export type { Agent, AgentOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, EvaluationResultKind, AgentExecutionMetadata, AgentExecutionTransport, AgentInteractionMode, } from './types.js';
|
|
24
|
+
export type { Agent, AgentOptions, DebugOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, EvaluationResultKind, AgentExecutionMetadata, AgentExecutionTransport, AgentInteractionMode, } from './types.js';
|
|
25
25
|
export type { ConversationWindow, ConversationWindowOptions } from './conversation-window.js';
|
|
26
26
|
export type { JudgePipelineOptions } from './judge-pipeline.js';
|
|
27
27
|
export type { RunScorerOptions } from './run-scorer.js';
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ export interface AgentOptions {
|
|
|
31
31
|
mcpConfigFile?: string;
|
|
32
32
|
/** Configure the conversation window for transcript-based agents. Set false to disable. */
|
|
33
33
|
conversationWindow?: ConversationWindowConfig | false;
|
|
34
|
-
/**
|
|
35
|
-
debug?: boolean | string;
|
|
34
|
+
/** Preserve the workspace. true uses the legacy path; strings are exact paths; objects enable managed retention. */
|
|
35
|
+
debug?: boolean | string | DebugOptions;
|
|
36
36
|
/**
|
|
37
37
|
* Glob patterns to ignore when copying workspace and skill directories.
|
|
38
38
|
* Replaces the default ignore list entirely. Pass `[]` to disable filtering.
|
|
@@ -53,6 +53,12 @@ export interface AgentOptions {
|
|
|
53
53
|
*/
|
|
54
54
|
mcpSafety?: McpSafetyOptions;
|
|
55
55
|
}
|
|
56
|
+
export interface DebugOptions {
|
|
57
|
+
/** Managed debug root. Relative paths resolve next to the eval file. */
|
|
58
|
+
directory?: string;
|
|
59
|
+
/** Maximum managed runs to retain, including the current run. Default: 3. */
|
|
60
|
+
retainRuns?: number;
|
|
61
|
+
}
|
|
56
62
|
export type { McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './mcp-safety.js';
|
|
57
63
|
export interface ConversationWindowConfig {
|
|
58
64
|
/** Number of recent messages to keep verbatim. Default: 4 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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": "130e5449f0131fe2bba40bf0237a7620315714dfe492f7e5e9d7cc14"
|
|
132
136
|
}
|