@wix/pathgrade 1.0.27 → 1.0.29
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 +12 -19
- package/dist/adapters/jest/invocation-adapter.js +5 -1
- package/dist/adapters/jest/reporter.js +4 -1
- package/dist/adapters/jest/results.js +8 -0
- package/dist/adapters/node-test/index.d.ts +5 -0
- package/dist/adapters/node-test/index.js +35 -7
- package/dist/adapters/node-test/invocation-adapter.js +3 -1
- package/dist/adapters/node-test/runner-adapter.js +22 -15
- package/dist/adapters/vitest/reporter.js +4 -1
- package/dist/agents/claude.js +5 -4
- package/dist/agents/codex-app-server/agent.js +1 -51
- package/dist/agents/codex-app-server/turn-notifications.d.ts +6 -0
- package/dist/agents/codex-app-server/turn-notifications.js +51 -0
- package/dist/agents/codex-app-server/turn-state.d.ts +19 -0
- package/dist/agents/codex-app-server/turn-state.js +1 -0
- package/dist/agents/opencode/protocol.d.ts +7 -0
- package/dist/agents/opencode/protocol.js +47 -0
- package/dist/agents/opencode.js +1 -47
- package/dist/analytics/engine.js +5 -2
- package/dist/commands/report.js +16 -2
- package/dist/commands/run-args.d.ts +1 -0
- package/dist/commands/run-args.js +13 -0
- package/dist/commands/run-changed.js +5 -15
- package/dist/config/pathgrade.d.ts +3 -0
- package/dist/config/pathgrade.js +33 -1
- package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +2 -1
- package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +35 -17
- package/dist/internal/direct-mcp-v2/claude-profile.d.ts +33 -7
- package/dist/internal/direct-mcp-v2/claude-profile.js +104 -72
- package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +13 -16
- package/dist/pathgrade.js +10 -2
- package/dist/reporters/cli.js +13 -6
- package/dist/reporters/github-comment.js +12 -9
- package/dist/reporters/loader.d.ts +1 -0
- package/dist/reporters/loader.js +27 -2
- package/dist/reporters/report-summary.js +13 -5
- package/dist/reporting/core.d.ts +1 -0
- package/dist/reporting/core.js +183 -105
- package/dist/reporting/types.d.ts +19 -3
- package/dist/runners/adapter-loader.js +17 -12
- package/dist/runners/direct-reporter-attempts.d.ts +1 -0
- package/dist/runners/direct-reporter-attempts.js +7 -0
- package/dist/runners/invocation.d.ts +2 -0
- package/dist/runners/model-builders.js +1 -0
- package/dist/runners/model-validation.js +31 -0
- package/dist/runners/model.d.ts +4 -0
- package/dist/runners/orchestrator.d.ts +2 -0
- package/dist/runners/orchestrator.js +11 -1
- package/dist/runners/repeated-attempts.d.ts +7 -0
- package/dist/runners/repeated-attempts.js +149 -0
- package/dist/runners/repeated-invocation.d.ts +7 -0
- package/dist/runners/repeated-invocation.js +129 -0
- package/dist/runners/report-projection.js +16 -6
- package/dist/runners/vitest-adapter.js +10 -0
- package/dist/runners/vitest-invocation.js +2 -0
- package/dist/sdk/agent-runtime-options.d.ts +12 -0
- package/dist/sdk/agent-runtime-options.js +67 -0
- package/dist/sdk/agent.js +4 -57
- package/dist/sdk/case-context.js +7 -2
- package/dist/sdk/lifecycle.js +8 -3
- package/dist/sdk/result-capture.js +4 -1
- package/dist/types.d.ts +34 -5
- package/dist/viewer.html +19 -19
- package/package.json +2 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { validateNormalizedRunSnapshot } from './model-validation.js';
|
|
3
|
+
export class RepeatAggregationError extends Error {
|
|
4
|
+
reason;
|
|
5
|
+
constructor(reason, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
this.name = 'RepeatAggregationError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function mergeRepeatedRunSnapshots(snapshots) {
|
|
12
|
+
if (snapshots.length === 0) {
|
|
13
|
+
throw new RepeatAggregationError('incomplete_attempts', 'No repeated-run snapshots were provided.');
|
|
14
|
+
}
|
|
15
|
+
const indexed = snapshots.map((snapshot, index) => validateAndIndex(snapshot, index));
|
|
16
|
+
const expectedKeys = [...indexed[0].keys()];
|
|
17
|
+
const expectedSet = new Set(expectedKeys);
|
|
18
|
+
for (let index = 1; index < indexed.length; index++) {
|
|
19
|
+
const actualKeys = [...indexed[index].keys()];
|
|
20
|
+
if (actualKeys.length !== expectedKeys.length || actualKeys.some(key => !expectedSet.has(key))) {
|
|
21
|
+
throw new RepeatAggregationError('case_identity_mismatch', `Attempt ${index + 1} case identities do not exactly match attempt 1.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const runId = `pathgrade:repeated:${randomUUID()}`;
|
|
25
|
+
const first = snapshots[0];
|
|
26
|
+
const unitIds = new Map(first.model.units.map((unit, index) => [unit.id, `${runId}:unit-${index + 1}`]));
|
|
27
|
+
const units = first.model.units.map(unit => ({
|
|
28
|
+
...unit,
|
|
29
|
+
id: unitIds.get(unit.id),
|
|
30
|
+
runId,
|
|
31
|
+
}));
|
|
32
|
+
const cases = expectedKeys.map((repeatKey, caseIndex) => {
|
|
33
|
+
const sourceCases = indexed.map(casesByKey => casesByKey.get(repeatKey));
|
|
34
|
+
const firstCase = sourceCases[0];
|
|
35
|
+
const caseId = `${runId}:case-${caseIndex + 1}-${shortHash(repeatKey)}`;
|
|
36
|
+
const attempts = sourceCases.map((sourceCase, attemptIndex) => (rewriteAttempt(sourceCase, caseId, attemptIndex + 1)));
|
|
37
|
+
const scoringPolicy = sourceCases.every(sourceCase => sourceCase.scoringPolicy.kind === 'non-scoring')
|
|
38
|
+
? firstCase.scoringPolicy
|
|
39
|
+
: { kind: 'from-evaluations' };
|
|
40
|
+
return {
|
|
41
|
+
...firstCase,
|
|
42
|
+
id: caseId,
|
|
43
|
+
repeatKey,
|
|
44
|
+
runId,
|
|
45
|
+
...(firstCase.unitId ? { unitId: unitIds.get(firstCase.unitId) } : {}),
|
|
46
|
+
state: aggregateCaseState(sourceCases.map(runCase => runCase.state)),
|
|
47
|
+
scoringPolicy,
|
|
48
|
+
attempts,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
completeness: 'final',
|
|
54
|
+
model: {
|
|
55
|
+
run: {
|
|
56
|
+
...first.model.run,
|
|
57
|
+
id: runId,
|
|
58
|
+
status: aggregateRunStatus(snapshots.map(snapshot => snapshot.model.run.status)),
|
|
59
|
+
attemptsRequested: snapshots.length,
|
|
60
|
+
attemptsCompleted: snapshots.length,
|
|
61
|
+
},
|
|
62
|
+
units,
|
|
63
|
+
cases,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function validateAndIndex(snapshot, attemptIndex) {
|
|
68
|
+
const validation = validateNormalizedRunSnapshot(snapshot, { completeness: 'final' });
|
|
69
|
+
if (!validation.ok) {
|
|
70
|
+
throw new RepeatAggregationError('incomplete_attempts', `Attempt ${attemptIndex + 1} snapshot is invalid: ${validation.errors.map(error => `${error.path}: ${error.message}`).join('; ')}`);
|
|
71
|
+
}
|
|
72
|
+
const casesByKey = new Map();
|
|
73
|
+
for (const runCase of snapshot.model.cases) {
|
|
74
|
+
if (!runCase.repeatKey?.trim()) {
|
|
75
|
+
throw new RepeatAggregationError('case_identity_mismatch', `Attempt ${attemptIndex + 1} case "${runCase.name}" has no semantic repeat key.`);
|
|
76
|
+
}
|
|
77
|
+
if (casesByKey.has(runCase.repeatKey)) {
|
|
78
|
+
throw new RepeatAggregationError('case_identity_mismatch', `Attempt ${attemptIndex + 1} contains duplicate semantic repeat key "${runCase.repeatKey}".`);
|
|
79
|
+
}
|
|
80
|
+
if (runCase.attempts.length !== 1) {
|
|
81
|
+
throw new RepeatAggregationError('incomplete_attempts', `Attempt ${attemptIndex + 1} case "${runCase.name}" must contain exactly one runner attempt.`);
|
|
82
|
+
}
|
|
83
|
+
casesByKey.set(runCase.repeatKey, runCase);
|
|
84
|
+
}
|
|
85
|
+
return casesByKey;
|
|
86
|
+
}
|
|
87
|
+
function rewriteAttempt(sourceCase, caseId, attemptIndex) {
|
|
88
|
+
const source = sourceCase.attempts[0];
|
|
89
|
+
const attemptId = `${caseId}:attempt-${attemptIndex}`;
|
|
90
|
+
const evaluations = (source.evaluations ?? []).map((evaluation, index) => ({
|
|
91
|
+
...evaluation,
|
|
92
|
+
id: `${attemptId}:evaluation-${index + 1}`,
|
|
93
|
+
attemptId,
|
|
94
|
+
}));
|
|
95
|
+
if (sourceCase.scoringPolicy.kind === 'score') {
|
|
96
|
+
if (evaluations.length === 0) {
|
|
97
|
+
evaluations.push({
|
|
98
|
+
id: `${attemptId}:evaluation-1`,
|
|
99
|
+
attemptId,
|
|
100
|
+
score: sourceCase.scoringPolicy.score,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const terminal = evaluations[evaluations.length - 1];
|
|
105
|
+
const canonical = {
|
|
106
|
+
...terminal,
|
|
107
|
+
score: sourceCase.scoringPolicy.score,
|
|
108
|
+
...(terminal.trial ? {
|
|
109
|
+
trial: { ...terminal.trial, reward: sourceCase.scoringPolicy.score },
|
|
110
|
+
} : {}),
|
|
111
|
+
};
|
|
112
|
+
if (canonical.resultKind === 'synthetic_no_evaluation')
|
|
113
|
+
delete canonical.resultKind;
|
|
114
|
+
evaluations[evaluations.length - 1] = canonical;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
...source,
|
|
119
|
+
id: attemptId,
|
|
120
|
+
caseId,
|
|
121
|
+
...(evaluations.length > 0 ? { evaluations } : {}),
|
|
122
|
+
...(source.assertions ? {
|
|
123
|
+
assertions: source.assertions.map((assertion, index) => ({
|
|
124
|
+
...assertion,
|
|
125
|
+
id: `${attemptId}:assertion-${index + 1}`,
|
|
126
|
+
attemptId,
|
|
127
|
+
})),
|
|
128
|
+
} : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function aggregateCaseState(states) {
|
|
132
|
+
if (states.some(state => state === 'failed'))
|
|
133
|
+
return 'failed';
|
|
134
|
+
if (states.some(state => state === 'passed'))
|
|
135
|
+
return 'passed';
|
|
136
|
+
if (states.some(state => state === 'pending'))
|
|
137
|
+
return 'pending';
|
|
138
|
+
return 'skipped';
|
|
139
|
+
}
|
|
140
|
+
function aggregateRunStatus(statuses) {
|
|
141
|
+
for (const status of ['cancelled', 'timed_out', 'parked', 'failed']) {
|
|
142
|
+
if (statuses.includes(status))
|
|
143
|
+
return status;
|
|
144
|
+
}
|
|
145
|
+
return 'completed';
|
|
146
|
+
}
|
|
147
|
+
function shortHash(value) {
|
|
148
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
149
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResolvedPathgradeConfig } from '../config/pathgrade.js';
|
|
2
|
+
import type { RunnerInvocationAdapter } from './invocation.js';
|
|
3
|
+
export declare function withRepeatedAttempts(input: {
|
|
4
|
+
adapter: RunnerInvocationAdapter;
|
|
5
|
+
config: ResolvedPathgradeConfig;
|
|
6
|
+
openBrowser?: () => void | Promise<void>;
|
|
7
|
+
}): RunnerInvocationAdapter;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { readSidecarForInvocation } from '../affected/sidecar.js';
|
|
6
|
+
import { getPathgradeDir } from '../reporters/results-path.js';
|
|
7
|
+
import { printReportSummary } from '../reporters/report-summary.js';
|
|
8
|
+
import { buildPathgradeReport } from '../reporting/core.js';
|
|
9
|
+
import { writePathgradeArtifacts } from '../reporting/artifacts.js';
|
|
10
|
+
import { projectNormalizedRunSnapshotToReportInput } from './report-projection.js';
|
|
11
|
+
import { mergeRepeatedRunSnapshots } from './repeated-attempts.js';
|
|
12
|
+
export function withRepeatedAttempts(input) {
|
|
13
|
+
if (input.config.attempts === 1)
|
|
14
|
+
return input.adapter;
|
|
15
|
+
if (!input.adapter.supportsRepeatedAttempts) {
|
|
16
|
+
throw new Error(`Pathgrade runner adapter "${input.adapter.name}" does not support runner-owned repeated attempts.`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
name: input.adapter.name,
|
|
20
|
+
supportsRepeatedAttempts: true,
|
|
21
|
+
writeEmptyReport: input.adapter.writeEmptyReport,
|
|
22
|
+
async run(runInput) {
|
|
23
|
+
const finalArtifactRoot = getPathgradeDir(runInput.cwd);
|
|
24
|
+
const attemptRunRoot = path.join(finalArtifactRoot, 'attempts', randomUUID());
|
|
25
|
+
const snapshots = [];
|
|
26
|
+
let firstNonzeroExit = 0;
|
|
27
|
+
let resultCode = 1;
|
|
28
|
+
try {
|
|
29
|
+
await Promise.all([
|
|
30
|
+
fs.remove(path.join(finalArtifactRoot, 'results.json')),
|
|
31
|
+
fs.remove(path.join(finalArtifactRoot, 'traces')),
|
|
32
|
+
]);
|
|
33
|
+
for (let index = 1; index <= input.config.attempts; index++) {
|
|
34
|
+
const artifactRoot = path.join(attemptRunRoot, `attempt-${index}`);
|
|
35
|
+
const snapshotPath = path.join(artifactRoot, 'normalized-run.json');
|
|
36
|
+
const exitCode = await input.adapter.run({
|
|
37
|
+
...runInput,
|
|
38
|
+
env: {
|
|
39
|
+
...runInput.env,
|
|
40
|
+
PATHGRADE_ATTEMPT_INDEX: String(index),
|
|
41
|
+
PATHGRADE_ATTEMPT_COUNT: String(input.config.attempts),
|
|
42
|
+
PATHGRADE_ARTIFACT_ROOT: artifactRoot,
|
|
43
|
+
PATHGRADE_NORMALIZED_SNAPSHOT_PATH: snapshotPath,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (exitCode !== 0 && firstNonzeroExit === 0)
|
|
47
|
+
firstNonzeroExit = exitCode;
|
|
48
|
+
if (!await fs.pathExists(snapshotPath)) {
|
|
49
|
+
throw new Error(`Attempt ${index} did not produce a normalized snapshot at ${snapshotPath}.`);
|
|
50
|
+
}
|
|
51
|
+
const snapshot = await fs.readJson(snapshotPath);
|
|
52
|
+
snapshots.push(snapshot);
|
|
53
|
+
if (snapshot.model.run.status === 'cancelled')
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (snapshots.length !== input.config.attempts) {
|
|
57
|
+
throw new Error(`Only ${snapshots.length} of ${input.config.attempts} scheduled attempts completed.`);
|
|
58
|
+
}
|
|
59
|
+
const merged = mergeRepeatedRunSnapshots(snapshots);
|
|
60
|
+
const selection = await readSidecarForInvocation(runInput.cwd, runInput.env.PATHGRADE_SELECTION_INVOCATION_ID, message => process.stderr.write(`[pathgrade] ${message}\n`)) ?? undefined;
|
|
61
|
+
const built = buildPathgradeReport({
|
|
62
|
+
threshold: input.config.ci.threshold,
|
|
63
|
+
...projectNormalizedRunSnapshotToReportInput(merged, { selection }),
|
|
64
|
+
});
|
|
65
|
+
if (built.report.groups.length === 0 && input.adapter.writeEmptyReport === false) {
|
|
66
|
+
resultCode = firstNonzeroExit;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (const warning of built.warnings) {
|
|
70
|
+
process.stderr.write(` [pathgrade] warning: ${warning}\n`);
|
|
71
|
+
}
|
|
72
|
+
if ((input.config.reporter ?? 'cli') !== 'json') {
|
|
73
|
+
printReportSummary(built.summaries, {
|
|
74
|
+
forceVerbose: input.config.diagnostics
|
|
75
|
+
|| runInput.env.PATHGRADE_DIAGNOSTICS === '1',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
await writePathgradeArtifacts(finalArtifactRoot, built);
|
|
79
|
+
process.stdout.write(`\n Results written to ${finalArtifactRoot}\n\n`);
|
|
80
|
+
if (input.config.reporter === 'browser') {
|
|
81
|
+
await (input.openBrowser ?? openBrowserViewer)();
|
|
82
|
+
}
|
|
83
|
+
if (input.config.ci.threshold != null && built.report.status === 'fail') {
|
|
84
|
+
process.stdout.write(`\n CI THRESHOLD FAILED avg score ${built.report.overall_pass_rate.toFixed(3)} < threshold ${input.config.ci.threshold}\n\n`);
|
|
85
|
+
resultCode = firstNonzeroExit || 1;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
resultCode = firstNonzeroExit;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
process.stderr.write(`pathgrade: repeated attempts failed: ${errorMessage(error)}\n`);
|
|
94
|
+
resultCode = 1;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (runInput.env.PATHGRADE_KEEP_ATTEMPT_ARTIFACTS !== '1') {
|
|
98
|
+
try {
|
|
99
|
+
await fs.remove(attemptRunRoot);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
process.stderr.write(`pathgrade: could not clean repeated attempt artifacts: ${errorMessage(error)}\n`);
|
|
103
|
+
if (resultCode === 0)
|
|
104
|
+
resultCode = 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return resultCode;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function openBrowserViewer() {
|
|
113
|
+
const viewerPath = path.resolve(import.meta.dirname, '..', 'viewer.html');
|
|
114
|
+
try {
|
|
115
|
+
if (process.platform === 'win32') {
|
|
116
|
+
execFileSync('cmd', ['/c', 'start', '', viewerPath], { stdio: 'ignore' });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
execFileSync(process.platform === 'darwin' ? 'open' : 'xdg-open', [viewerPath], { stdio: 'ignore' });
|
|
120
|
+
}
|
|
121
|
+
process.stdout.write(' Opened viewer in browser\n\n');
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
process.stdout.write(` Open manually: ${viewerPath}\n\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function errorMessage(error) {
|
|
128
|
+
return error instanceof Error ? error.message : String(error);
|
|
129
|
+
}
|
|
@@ -6,6 +6,7 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
|
|
|
6
6
|
}
|
|
7
7
|
const unitsById = new Map(snapshot.model.units.map(unit => [unit.id, unit]));
|
|
8
8
|
const groups = new Map();
|
|
9
|
+
const repeated = (snapshot.model.run.attemptsRequested ?? 1) > 1;
|
|
9
10
|
for (const runCase of snapshot.model.cases) {
|
|
10
11
|
if (runCase.scoringPolicy.kind === 'non-scoring' && runCase.state !== 'skipped' && runCase.state !== 'pending') {
|
|
11
12
|
continue;
|
|
@@ -20,23 +21,31 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
|
|
|
20
21
|
caseId: runCase.id,
|
|
21
22
|
name: runCase.name,
|
|
22
23
|
state: runCase.state,
|
|
23
|
-
...(runCase.state === 'skipped' || runCase.state === 'pending' ? { reportable: runCase.scoringPolicy.kind !== 'non-scoring' } : {}),
|
|
24
|
+
...(runCase.state === 'skipped' || runCase.state === 'pending' ? { reportable: repeated || runCase.scoringPolicy.kind !== 'non-scoring' } : {}),
|
|
24
25
|
runnerDurationMs: totalDurationMs(runCase),
|
|
25
|
-
|
|
26
|
+
attempts: runCase.attempts.map((attempt, attemptIndex) => ({
|
|
27
|
+
attemptId: attempt.id,
|
|
28
|
+
attemptIndex: attemptIndex + 1,
|
|
29
|
+
outcome: attempt.outcome,
|
|
30
|
+
runnerDurationMs: attempt.durationMs ?? 0,
|
|
31
|
+
evaluations: projectedEvaluations(runCase, attempt.evaluations),
|
|
32
|
+
})),
|
|
26
33
|
});
|
|
27
34
|
groups.set(key, group);
|
|
28
35
|
}
|
|
29
36
|
return {
|
|
30
37
|
...(options.selection ? { selection: options.selection } : {}),
|
|
38
|
+
attemptsRequested: snapshot.model.run.attemptsRequested ?? 1,
|
|
39
|
+
attemptsCompleted: snapshot.model.run.attemptsCompleted ?? 1,
|
|
31
40
|
groups: [...groups.values()],
|
|
32
41
|
};
|
|
33
42
|
}
|
|
34
43
|
function totalDurationMs(runCase) {
|
|
35
44
|
return runCase.attempts.reduce((sum, attempt) => sum + (attempt.durationMs ?? 0), 0);
|
|
36
45
|
}
|
|
37
|
-
function projectedEvaluations(runCase) {
|
|
46
|
+
function projectedEvaluations(runCase, attemptEvaluations) {
|
|
38
47
|
if (runCase.scoringPolicy.kind === 'non-scoring') {
|
|
39
|
-
return
|
|
48
|
+
return (attemptEvaluations ?? []).map(evaluation => ({
|
|
40
49
|
...(evaluation.score !== undefined ? { score: evaluation.score } : {}),
|
|
41
50
|
...(evaluation.trial ? { trial: evaluation.trial } : {}),
|
|
42
51
|
...(evaluation.diagnostics ? { diagnostics: evaluation.diagnostics } : {}),
|
|
@@ -44,12 +53,13 @@ function projectedEvaluations(runCase) {
|
|
|
44
53
|
...(evaluation.scoringDurationMs !== undefined ? { scoringDurationMs: evaluation.scoringDurationMs } : {}),
|
|
45
54
|
...(evaluation.recordedAt ? { recordedAt: evaluation.recordedAt } : {}),
|
|
46
55
|
...(evaluation.agent ? { agent: evaluation.agent } : {}),
|
|
47
|
-
}))
|
|
56
|
+
}));
|
|
48
57
|
}
|
|
49
58
|
if (runCase.scoringPolicy.kind === 'score') {
|
|
50
59
|
return [{ score: runCase.scoringPolicy.score }];
|
|
51
60
|
}
|
|
52
|
-
const evaluation =
|
|
61
|
+
const evaluation = attemptEvaluations?.findLast(candidate => candidate.resultKind !== 'synthetic_no_evaluation')
|
|
62
|
+
?? attemptEvaluations?.at(-1);
|
|
53
63
|
if (!evaluation)
|
|
54
64
|
return [];
|
|
55
65
|
return [{
|
|
@@ -79,6 +79,7 @@ function toReportCaseInput(testCase, groupName) {
|
|
|
79
79
|
const runnerCaseId = typeof testCase.id === 'string' ? testCase.id : undefined;
|
|
80
80
|
return {
|
|
81
81
|
...(runnerCaseId ? { caseId: runnerCaseId, runnerCaseId } : {}),
|
|
82
|
+
repeatKey: semanticRepeatKey(filePath, testCase, runnerCaseId),
|
|
82
83
|
name: testCase.name,
|
|
83
84
|
state: normalized.state,
|
|
84
85
|
runnerDurationMs: diagnostics?.duration ?? 0,
|
|
@@ -97,6 +98,15 @@ function toReportCaseInput(testCase, groupName) {
|
|
|
97
98
|
diagnostics: normalized.diagnostics,
|
|
98
99
|
};
|
|
99
100
|
}
|
|
101
|
+
function semanticRepeatKey(filePath, testCase, runnerCaseId) {
|
|
102
|
+
const parentName = testCase.parent.type === 'suite'
|
|
103
|
+
? testCase.parent.fullName
|
|
104
|
+
: '';
|
|
105
|
+
return JSON.stringify([
|
|
106
|
+
'vitest', filePath.replaceAll('\\', '/'), parentName, testCase.name,
|
|
107
|
+
runnerCaseId ?? null,
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
100
110
|
function normalizeState(state) {
|
|
101
111
|
if (state === 'passed' || state === 'failed' || state === 'skipped' || state === 'pending') {
|
|
102
112
|
return { state };
|
|
@@ -5,6 +5,8 @@ export function createVitestInvocationAdapter(input = {}) {
|
|
|
5
5
|
const spawnVitest = input.spawnVitest ?? defaultSpawnVitest;
|
|
6
6
|
return {
|
|
7
7
|
name: 'vitest',
|
|
8
|
+
supportsRepeatedAttempts: true,
|
|
9
|
+
writeEmptyReport: false,
|
|
8
10
|
async run(runInput) {
|
|
9
11
|
if (runInput.selectedFiles && hasPassWithNoTests(runInput.runnerArgs)) {
|
|
10
12
|
process.stderr.write('pathgrade run: --passWithNoTests cannot be used with pathgrade run --changed. ' +
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CompiledMcpMockSession } from './mcp-mock-approvals.js';
|
|
2
|
+
import type { AgentName, AgentOptions, AgentTransport } from './types.js';
|
|
3
|
+
import type { ScenarioArtifact } from '../internal/direct-mcp-v2/types.js';
|
|
4
|
+
import type { PublicScenarioRuntimeFactory } from '../internal/direct-mcp-v2/public-scenario-runtime.js';
|
|
5
|
+
export interface ResolvedAgentRuntimeOptions {
|
|
6
|
+
agentName: AgentName;
|
|
7
|
+
transport?: AgentTransport;
|
|
8
|
+
scenarioArtifact?: ScenarioArtifact;
|
|
9
|
+
scenarioRuntimeFactory?: PublicScenarioRuntimeFactory;
|
|
10
|
+
scriptedMcp?: CompiledMcpMockSession;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveAgentRuntimeOptions(opts: AgentOptions, env: NodeJS.ProcessEnv): ResolvedAgentRuntimeOptions;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { resolveAgentName, resolveCodexTransport } from './agent-resolution.js';
|
|
2
|
+
import { compileMcpMockApprovalSession } from './mcp-mock-approvals.js';
|
|
3
|
+
import { compileScenario } from './scenario-machine-v2.js';
|
|
4
|
+
import { startPublicCodexScenarioRuntime } from '../agents/codex-app-server/scenario-mount.js';
|
|
5
|
+
import { validateOpenCodeDeclaration } from '../agents/opencode/contract.js';
|
|
6
|
+
import { createPublicOpenCodeScenarioRuntimeFactory } from '../agents/opencode/scenario.js';
|
|
7
|
+
import { createPublicCursorScenarioRuntimeFactory } from '../agents/cursor-scenario.js';
|
|
8
|
+
export function resolveAgentRuntimeOptions(opts, env) {
|
|
9
|
+
const agentName = resolveAgentName(opts, env);
|
|
10
|
+
let scenarioArtifact;
|
|
11
|
+
if (opts.mcpScenario !== undefined) {
|
|
12
|
+
if (opts.agent === undefined)
|
|
13
|
+
throw new Error('mcpScenario requires an explicit agent');
|
|
14
|
+
if (opts.mcpMock !== undefined || opts.mcpMockApprovalRules !== undefined || opts.mcpConfigFile !== undefined || opts.mcpSafety !== undefined) {
|
|
15
|
+
throw new Error('mcpScenario cannot be combined with legacy or live MCP options');
|
|
16
|
+
}
|
|
17
|
+
const compiled = compileScenario(opts.mcpScenario);
|
|
18
|
+
if (!compiled.ok)
|
|
19
|
+
throw new Error(`Scenario compilation failed: ${JSON.stringify(compiled.diagnostics)}`);
|
|
20
|
+
scenarioArtifact = compiled.artifact;
|
|
21
|
+
}
|
|
22
|
+
validateOpenCodeDeclaration(agentName, opts);
|
|
23
|
+
const transport = agentName === 'codex'
|
|
24
|
+
? resolveCodexTransport(opts, env)
|
|
25
|
+
: undefined;
|
|
26
|
+
if (scenarioArtifact && agentName === 'codex' && transport !== 'app-server') {
|
|
27
|
+
throw new Error('mcpScenario requires Codex transport app-server; Codex exec is unsupported');
|
|
28
|
+
}
|
|
29
|
+
const scenarioRuntimeFactory = resolveScenarioRuntimeFactory(opts, agentName, transport, scenarioArtifact);
|
|
30
|
+
const scriptedMcp = resolveScriptedMcp(opts, agentName, transport);
|
|
31
|
+
return { agentName, transport, scenarioArtifact, scenarioRuntimeFactory, scriptedMcp };
|
|
32
|
+
}
|
|
33
|
+
function resolveScenarioRuntimeFactory(opts, agentName, transport, scenarioArtifact) {
|
|
34
|
+
if (!scenarioArtifact)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (agentName === 'codex')
|
|
37
|
+
return transport === 'app-server' ? startPublicCodexScenarioRuntime : undefined;
|
|
38
|
+
if (agentName === 'opencode') {
|
|
39
|
+
return createPublicOpenCodeScenarioRuntimeFactory({
|
|
40
|
+
opencodeExecutable: opts.opencodeExecutable,
|
|
41
|
+
requestedModel: opts.model,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (agentName === 'cursor') {
|
|
45
|
+
return createPublicCursorScenarioRuntimeFactory({ requestedModel: opts.model });
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function resolveScriptedMcp(opts, agentName, transport) {
|
|
50
|
+
if (opts.mcpMockApprovalRules === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (opts.mcpMock === undefined)
|
|
53
|
+
throw new Error('mcpMockApprovalRules requires mcpMock');
|
|
54
|
+
if (opts.mcpConfigFile !== undefined)
|
|
55
|
+
throw new Error('mcpMockApprovalRules cannot be combined with mcpConfigFile');
|
|
56
|
+
if (opts.mcpSafety?.runMode !== undefined && opts.mcpSafety.runMode !== 'mock') {
|
|
57
|
+
throw new Error('mcpMockApprovalRules requires mcpSafety.runMode to be absent or mock');
|
|
58
|
+
}
|
|
59
|
+
if (agentName !== 'claude' && !(agentName === 'codex' && transport === 'app-server')) {
|
|
60
|
+
throw new Error('mcpMockApprovalRules supports Claude or Codex with transport app-server only');
|
|
61
|
+
}
|
|
62
|
+
return compileMcpMockApprovalSession({
|
|
63
|
+
mcpMock: opts.mcpMock,
|
|
64
|
+
rules: opts.mcpMockApprovalRules,
|
|
65
|
+
provider: agentName,
|
|
66
|
+
});
|
|
67
|
+
}
|
package/dist/sdk/agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { prepareWorkspace } from '../providers/workspace.js';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveExecutionTransport, } from './agent-resolution.js';
|
|
3
3
|
import { lifecycleCore } from './lifecycle.js';
|
|
4
4
|
import { ChatSessionImpl } from './chat.js';
|
|
5
5
|
import { runConversation } from './converse.js';
|
|
@@ -16,14 +16,10 @@ import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
|
|
|
16
16
|
import fs from 'fs-extra';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } from '../providers/debug-runs.js';
|
|
19
|
-
import { collectOpenCodeMcpToolNames
|
|
19
|
+
import { collectOpenCodeMcpToolNames } from '../agents/opencode/contract.js';
|
|
20
20
|
import { collectSensitiveEnvValues } from '../tool-event-results.js';
|
|
21
|
-
import { compileMcpMockApprovalSession, } from './mcp-mock-approvals.js';
|
|
22
|
-
import { compileScenario } from './scenario-machine-v2.js';
|
|
23
|
-
import { startPublicCodexScenarioRuntime } from '../agents/codex-app-server/scenario-mount.js';
|
|
24
|
-
import { createPublicOpenCodeScenarioRuntimeFactory } from '../agents/opencode/scenario.js';
|
|
25
|
-
import { createPublicCursorScenarioRuntimeFactory } from '../agents/cursor-scenario.js';
|
|
26
21
|
import { createAskUserHandler } from './ask-bus/handler.js';
|
|
22
|
+
import { resolveAgentRuntimeOptions } from './agent-runtime-options.js';
|
|
27
23
|
/**
|
|
28
24
|
* Test-only injection point: override the sink used by the next emitter
|
|
29
25
|
* built inside `createAgent`. Pass `null` to restore the default (stderr).
|
|
@@ -431,56 +427,7 @@ export async function createAgent(opts) {
|
|
|
431
427
|
throw new Error('Pathgrade debug retainRuns must be a positive integer');
|
|
432
428
|
}
|
|
433
429
|
}
|
|
434
|
-
const agentName =
|
|
435
|
-
let scenarioArtifact;
|
|
436
|
-
if (opts.mcpScenario !== undefined) {
|
|
437
|
-
if (opts.agent === undefined)
|
|
438
|
-
throw new Error('mcpScenario requires an explicit agent');
|
|
439
|
-
if (opts.mcpMock !== undefined || opts.mcpMockApprovalRules !== undefined || opts.mcpConfigFile !== undefined || opts.mcpSafety !== undefined) {
|
|
440
|
-
throw new Error('mcpScenario cannot be combined with legacy or live MCP options');
|
|
441
|
-
}
|
|
442
|
-
const compiled = compileScenario(opts.mcpScenario);
|
|
443
|
-
if (!compiled.ok)
|
|
444
|
-
throw new Error(`Scenario compilation failed: ${JSON.stringify(compiled.diagnostics)}`);
|
|
445
|
-
scenarioArtifact = compiled.artifact;
|
|
446
|
-
}
|
|
447
|
-
validateOpenCodeDeclaration(agentName, opts);
|
|
448
|
-
const transport = agentName === 'codex'
|
|
449
|
-
? resolveCodexTransport(opts, process.env)
|
|
450
|
-
: undefined;
|
|
451
|
-
if (scenarioArtifact && agentName === 'codex' && transport !== 'app-server') {
|
|
452
|
-
throw new Error('mcpScenario requires Codex transport app-server; Codex exec is unsupported');
|
|
453
|
-
}
|
|
454
|
-
const scenarioRuntimeFactory = !scenarioArtifact
|
|
455
|
-
? undefined
|
|
456
|
-
: agentName === 'codex'
|
|
457
|
-
? startPublicCodexScenarioRuntime
|
|
458
|
-
: agentName === 'opencode'
|
|
459
|
-
? createPublicOpenCodeScenarioRuntimeFactory({
|
|
460
|
-
opencodeExecutable: opts.opencodeExecutable,
|
|
461
|
-
requestedModel: opts.model,
|
|
462
|
-
})
|
|
463
|
-
: agentName === 'cursor'
|
|
464
|
-
? createPublicCursorScenarioRuntimeFactory({ requestedModel: opts.model })
|
|
465
|
-
: undefined;
|
|
466
|
-
let scriptedMcp;
|
|
467
|
-
if (opts.mcpMockApprovalRules !== undefined) {
|
|
468
|
-
if (opts.mcpMock === undefined)
|
|
469
|
-
throw new Error('mcpMockApprovalRules requires mcpMock');
|
|
470
|
-
if (opts.mcpConfigFile !== undefined)
|
|
471
|
-
throw new Error('mcpMockApprovalRules cannot be combined with mcpConfigFile');
|
|
472
|
-
if (opts.mcpSafety?.runMode !== undefined && opts.mcpSafety.runMode !== 'mock') {
|
|
473
|
-
throw new Error('mcpMockApprovalRules requires mcpSafety.runMode to be absent or mock');
|
|
474
|
-
}
|
|
475
|
-
if (agentName !== 'claude' && !(agentName === 'codex' && transport === 'app-server')) {
|
|
476
|
-
throw new Error('mcpMockApprovalRules supports Claude or Codex with transport app-server only');
|
|
477
|
-
}
|
|
478
|
-
scriptedMcp = compileMcpMockApprovalSession({
|
|
479
|
-
mcpMock: opts.mcpMock,
|
|
480
|
-
rules: opts.mcpMockApprovalRules,
|
|
481
|
-
provider: agentName,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
430
|
+
const { agentName, transport, scenarioArtifact, scenarioRuntimeFactory, scriptedMcp } = resolveAgentRuntimeOptions(opts, process.env);
|
|
484
431
|
const timeoutSetting = opts.timeout ?? 300;
|
|
485
432
|
// Capture runner context now; adapters own installation and restoration.
|
|
486
433
|
const testCtx = opts.debug ? resolveCaseDebugContext() : { name: '', dir: '' };
|
package/dist/sdk/case-context.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const storageKey = Symbol.for('@wix/pathgrade/case-context-storage');
|
|
3
|
+
const providersKey = Symbol.for('@wix/pathgrade/case-context-providers');
|
|
4
|
+
const sharedContext = globalThis;
|
|
5
|
+
const caseContextStorage = sharedContext[storageKey]
|
|
6
|
+
?? (sharedContext[storageKey] = new AsyncLocalStorage());
|
|
7
|
+
const providers = sharedContext[providersKey]
|
|
8
|
+
?? (sharedContext[providersKey] = []);
|
|
4
9
|
export function getCurrentCaseContext() {
|
|
5
10
|
const context = caseContextStorage.getStore();
|
|
6
11
|
if (context)
|
package/dist/sdk/lifecycle.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { getCurrentCaseContext } from './case-context.js';
|
|
2
2
|
import { buildDiagnosticsReport } from './diagnostics.js';
|
|
3
3
|
import { countShellCommandsFromLog } from '../tool-events.js';
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
4
|
+
const lifecycleStateKey = Symbol.for('@wix/pathgrade/lifecycle-state');
|
|
5
|
+
const sharedLifecycle = globalThis;
|
|
6
|
+
const lifecycleState = sharedLifecycle[lifecycleStateKey] ?? (sharedLifecycle[lifecycleStateKey] = {
|
|
7
|
+
pendingAgents: new Set(),
|
|
8
|
+
agentOwners: new WeakMap(),
|
|
9
|
+
agentResults: new WeakMap(),
|
|
10
|
+
});
|
|
11
|
+
const { pendingAgents, agentOwners, agentResults } = lifecycleState;
|
|
7
12
|
function currentAgentOwner() {
|
|
8
13
|
const current = getCurrentCaseContext();
|
|
9
14
|
if (current.status !== 'active')
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getCurrentCaseContext } from './case-context.js';
|
|
2
|
-
const
|
|
2
|
+
const observerRegistryKey = Symbol.for('@wix/pathgrade/eval-result-observers');
|
|
3
|
+
const globalRegistry = globalThis;
|
|
4
|
+
const observers = globalRegistry[observerRegistryKey]
|
|
5
|
+
?? (globalRegistry[observerRegistryKey] = new Set());
|
|
3
6
|
export function subscribeToEvalResults(observer, options = {}) {
|
|
4
7
|
if (options.owner === 'adapter' && options.key) {
|
|
5
8
|
removeObserverByOwnerAndKey(options.owner, options.key);
|