@wix/pathgrade 1.0.7 → 1.0.8
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/dist/commands/clean.d.ts +8 -0
- package/dist/commands/clean.js +94 -0
- 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/pathgrade.js +13 -5
- 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 +2 -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.
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/pathgrade.js
CHANGED
|
@@ -18,11 +18,13 @@ import { runPreviewReactions } from './commands/preview-reactions.js';
|
|
|
18
18
|
import { runReport } from './commands/report.js';
|
|
19
19
|
import { runAffected } from './commands/affected.js';
|
|
20
20
|
import { runChanged } from './commands/run-changed.js';
|
|
21
|
+
import { parseCleanArgs, runClean } from './commands/clean.js';
|
|
21
22
|
import { clearSidecar } from './affected/sidecar.js';
|
|
22
23
|
import { resolvePathgradeConfig } from './config/pathgrade.js';
|
|
23
24
|
import { loadRunnerInvocationAdapter } from './runners/adapter-loader.js';
|
|
24
25
|
import { fmt } from './utils/cli.js';
|
|
25
26
|
import { shutdown } from './utils/shutdown.js';
|
|
27
|
+
import { buildRunnerEnv } from './commands/runner-env.js';
|
|
26
28
|
function loadDotenv() {
|
|
27
29
|
const envPath = path.resolve(process.cwd(), '.env');
|
|
28
30
|
if (!fs.existsSync(envPath))
|
|
@@ -100,6 +102,13 @@ async function main() {
|
|
|
100
102
|
await runInit(process.cwd(), { force: hasForce });
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
105
|
+
if (command === 'clean') {
|
|
106
|
+
const result = await runClean(process.cwd(), parseCleanArgs(args.slice(1)));
|
|
107
|
+
const action = result.dryRun ? 'would remove' : 'removed';
|
|
108
|
+
console.log(`pathgrade: ${action} ${result.removed} debug run(s); ` +
|
|
109
|
+
`retained ${result.retained}; active ${result.active}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
103
112
|
if (command === 'preview') {
|
|
104
113
|
const previewArgs = args.slice(1);
|
|
105
114
|
const mode = previewArgs.includes('browser') ? 'browser' : 'cli';
|
|
@@ -164,11 +173,7 @@ async function main() {
|
|
|
164
173
|
// previous `--changed` run so it doesn't leak into this full-suite
|
|
165
174
|
// run (the reporter would otherwise merge old metadata).
|
|
166
175
|
await clearSidecar(process.cwd());
|
|
167
|
-
const env =
|
|
168
|
-
...process.env,
|
|
169
|
-
...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
|
|
170
|
-
...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
|
|
171
|
-
};
|
|
176
|
+
const env = buildRunnerEnv(parsed);
|
|
172
177
|
try {
|
|
173
178
|
const config = await resolvePathgradeConfig({ cwd: process.cwd() });
|
|
174
179
|
const runner = await loadRunnerInvocationAdapter({
|
|
@@ -209,6 +214,9 @@ function printHelp() {
|
|
|
209
214
|
pathgrade analyze [--skill=X] Analyze skills and output JSON
|
|
210
215
|
pathgrade validate <file> Validate an .eval.ts file
|
|
211
216
|
pathgrade validate --affected Strict: every eval must be anchored or have valid __pathgradeMeta
|
|
217
|
+
pathgrade clean --debug Remove completed debug runs
|
|
218
|
+
[--keep=N] Keep the N newest completed debug runs
|
|
219
|
+
[--dry-run] Report removals without changing files
|
|
212
220
|
pathgrade preview [browser] View results (CLI default, or browser)
|
|
213
221
|
[--last=N] Show only the N most recent reports
|
|
214
222
|
[--filter=X] Filter reports by test name (substring)
|
|
@@ -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.8",
|
|
4
4
|
"packageManager": "yarn@4.12.0",
|
|
5
5
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
6
6
|
"exports": {
|
|
@@ -128,5 +128,5 @@
|
|
|
128
128
|
"typescript": "^5.9.3",
|
|
129
129
|
"zod": "4.3.6"
|
|
130
130
|
},
|
|
131
|
-
"falconPackageHash": "
|
|
131
|
+
"falconPackageHash": "3a6990dfd4a93a86e7bd1cf9acc42873b8b0b580f5a9b0d7c9692ed5"
|
|
132
132
|
}
|