@wix/pathgrade 1.0.26 → 1.0.28
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 +14 -21
- 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/sdk-message-projector.js +5 -0
- package/dist/agents/codex-app-server/agent.js +7 -57
- 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/codex.js +1 -0
- package/dist/agents/cursor.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 +5 -49
- package/dist/analytics/engine.js +5 -2
- package/dist/commands/report.d.ts +10 -2
- package/dist/commands/report.js +41 -6
- 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/pathgrade.js +32 -3
- package/dist/reporters/cli.js +13 -6
- package/dist/reporters/github-comment.d.ts +12 -3
- package/dist/reporters/github-comment.js +92 -18
- 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/artifacts.js +5 -2
- 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 +11 -59
- package/dist/sdk/case-context.js +7 -2
- package/dist/sdk/evaluate.d.ts +2 -0
- package/dist/sdk/evaluate.js +14 -9
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/lifecycle.js +16 -6
- package/dist/sdk/result-capture.js +4 -1
- package/dist/sdk/types.d.ts +2 -0
- package/dist/tool-event-results.d.ts +1 -1
- package/dist/tool-event-results.js +2 -1
- package/dist/tool-events.d.ts +5 -0
- package/dist/tool-events.js +5 -0
- package/dist/types.d.ts +34 -5
- package/dist/viewer.html +19 -19
- package/package.json +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const NATIVE_TOOL_ACTIONS = {
|
|
2
|
+
bash: 'run_shell',
|
|
3
|
+
read: 'read_file',
|
|
4
|
+
write: 'write_file',
|
|
5
|
+
edit: 'edit_file',
|
|
6
|
+
grep: 'search_code',
|
|
7
|
+
glob: 'list_files',
|
|
8
|
+
list: 'list_files',
|
|
9
|
+
skill: 'use_skill',
|
|
10
|
+
todowrite: 'update_todos',
|
|
11
|
+
lsp: 'search_code',
|
|
12
|
+
};
|
|
13
|
+
export const ALLOWED_EVENT_TYPES = new Set([
|
|
14
|
+
'tool_use',
|
|
15
|
+
'step_start',
|
|
16
|
+
'step_finish',
|
|
17
|
+
'text',
|
|
18
|
+
'reasoning',
|
|
19
|
+
'error',
|
|
20
|
+
]);
|
|
21
|
+
export function protocolRecord(value, label) {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
23
|
+
throw new Error(`OpenCode protocol error: ${label} must be an object`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export function protocolFiniteNumber(value, label) {
|
|
28
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
29
|
+
throw new Error(`OpenCode protocol error: ${label} must be a nonnegative finite number`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
export function protocolRequiredString(value, label) {
|
|
34
|
+
if (typeof value !== 'string' || !value) {
|
|
35
|
+
throw new Error(`OpenCode protocol error: ${label} must be a nonempty string`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
export function sanitizedProviderError(event) {
|
|
40
|
+
const error = protocolRecord(event.error, 'error');
|
|
41
|
+
const data = error.data && typeof error.data === 'object' && !Array.isArray(error.data)
|
|
42
|
+
? error.data
|
|
43
|
+
: {};
|
|
44
|
+
const status = typeof data.statusCode === 'number' ? ` status=${data.statusCode}` : '';
|
|
45
|
+
const retryable = typeof data.isRetryable === 'boolean' ? ` retryable=${data.isRetryable}` : '';
|
|
46
|
+
return new Error(`OpenCode provider error${status}${retryable}`);
|
|
47
|
+
}
|
package/dist/agents/opencode.js
CHANGED
|
@@ -6,7 +6,7 @@ import { buildSummary, enrichSkillEvents } from '../tool-events.js';
|
|
|
6
6
|
import { collectSensitiveEnvValues, sanitizePersistenceValue, sanitizeToolEventResult, } from '../tool-event-results.js';
|
|
7
7
|
import { readStagedMcpServers } from '../providers/mcp-config.js';
|
|
8
8
|
import { removeSandboxRoot } from '../providers/sandbox-lifecycle.js';
|
|
9
|
-
import { attachTurnResultSensitiveValues } from '../sdk/turn-result-secrets.js';
|
|
9
|
+
import { attachTurnResultSensitiveValues, cloneTurnResultWithSensitiveValues } from '../sdk/turn-result-secrets.js';
|
|
10
10
|
import { attachOriginalMcpInput } from '../sdk/mcp-event-input.js';
|
|
11
11
|
import { attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
|
|
12
12
|
import { currentOpenCodePlatformKey, OPENCODE_RUNTIME_LOCK, } from './opencode/contract.js';
|
|
@@ -14,28 +14,9 @@ import { OpenCodeRuntimePolicy, OPENCODE_PERMISSION } from './opencode/runtime-p
|
|
|
14
14
|
import { killOpenCodeProcessGroup, registerOpenCodeProcessGroup, unregisterOpenCodeProcessGroup, } from './opencode/process-groups.js';
|
|
15
15
|
import { assertCleanManagedOpenCodeHost, sha256File } from './opencode/host-safety.js';
|
|
16
16
|
import { getOpenCodeScenarioSession } from './opencode/scenario.js';
|
|
17
|
+
import { ALLOWED_EVENT_TYPES, NATIVE_TOOL_ACTIONS, protocolFiniteNumber as finiteNumber, protocolRecord as record, protocolRequiredString as requiredString, sanitizedProviderError, } from './opencode/protocol.js';
|
|
17
18
|
export { assertCleanManagedOpenCodeHost, managedOpenCodeConfigPaths } from './opencode/host-safety.js';
|
|
18
19
|
const OUTPUT_CAP_BYTES = 16 * 1024 * 1024;
|
|
19
|
-
const NATIVE_TOOL_ACTIONS = {
|
|
20
|
-
bash: 'run_shell',
|
|
21
|
-
read: 'read_file',
|
|
22
|
-
write: 'write_file',
|
|
23
|
-
edit: 'edit_file',
|
|
24
|
-
grep: 'search_code',
|
|
25
|
-
glob: 'list_files',
|
|
26
|
-
list: 'list_files',
|
|
27
|
-
skill: 'use_skill',
|
|
28
|
-
todowrite: 'update_todos',
|
|
29
|
-
lsp: 'search_code',
|
|
30
|
-
};
|
|
31
|
-
const ALLOWED_EVENT_TYPES = new Set([
|
|
32
|
-
'tool_use',
|
|
33
|
-
'step_start',
|
|
34
|
-
'step_finish',
|
|
35
|
-
'text',
|
|
36
|
-
'reasoning',
|
|
37
|
-
'error',
|
|
38
|
-
]);
|
|
39
20
|
export function spawnOpenCode(executable, args, options) {
|
|
40
21
|
return new Promise((resolve, reject) => {
|
|
41
22
|
const stdout = [];
|
|
@@ -124,33 +105,6 @@ export function spawnOpenCode(executable, args, options) {
|
|
|
124
105
|
}
|
|
125
106
|
});
|
|
126
107
|
}
|
|
127
|
-
function record(value, label) {
|
|
128
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
129
|
-
throw new Error(`OpenCode protocol error: ${label} must be an object`);
|
|
130
|
-
}
|
|
131
|
-
return value;
|
|
132
|
-
}
|
|
133
|
-
function finiteNumber(value, label) {
|
|
134
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
135
|
-
throw new Error(`OpenCode protocol error: ${label} must be a nonnegative finite number`);
|
|
136
|
-
}
|
|
137
|
-
return value;
|
|
138
|
-
}
|
|
139
|
-
function requiredString(value, label) {
|
|
140
|
-
if (typeof value !== 'string' || !value) {
|
|
141
|
-
throw new Error(`OpenCode protocol error: ${label} must be a nonempty string`);
|
|
142
|
-
}
|
|
143
|
-
return value;
|
|
144
|
-
}
|
|
145
|
-
function sanitizedProviderError(event) {
|
|
146
|
-
const error = record(event.error, 'error');
|
|
147
|
-
const data = error.data && typeof error.data === 'object' && !Array.isArray(error.data)
|
|
148
|
-
? error.data
|
|
149
|
-
: {};
|
|
150
|
-
const status = typeof data.statusCode === 'number' ? ` status=${data.statusCode}` : '';
|
|
151
|
-
const retryable = typeof data.isRetryable === 'boolean' ? ` retryable=${data.isRetryable}` : '';
|
|
152
|
-
return new Error(`OpenCode provider error${status}${retryable}`);
|
|
153
|
-
}
|
|
154
108
|
export function parseOpenCodeOutput(stdout, processResult, mcpToolNames, sensitiveValues = []) {
|
|
155
109
|
if (processResult.overflow)
|
|
156
110
|
throw new Error('OpenCode output exceeded the 16 MiB limit');
|
|
@@ -451,7 +405,9 @@ class OpenCodeSession {
|
|
|
451
405
|
throw new Error('OpenCode protocol error: resumed session ID changed');
|
|
452
406
|
}
|
|
453
407
|
this.sessionId = parsed.sessionId;
|
|
454
|
-
return parsed.result
|
|
408
|
+
return cloneTurnResultWithSensitiveValues(parsed.result, {
|
|
409
|
+
resolvedModel: this.runtimePolicy.model,
|
|
410
|
+
});
|
|
455
411
|
}
|
|
456
412
|
catch (error) {
|
|
457
413
|
this.failed = true;
|
package/dist/analytics/engine.js
CHANGED
|
@@ -40,10 +40,10 @@ export class AnalyticsEngine {
|
|
|
40
40
|
for (const [task, data] of Object.entries(taskGroups)) {
|
|
41
41
|
const allReports = [...data.withSkill, ...data.withoutSkill];
|
|
42
42
|
const avgWith = data.withSkill.length > 0
|
|
43
|
-
? data.withSkill.reduce((a, b) => a + b
|
|
43
|
+
? data.withSkill.reduce((a, b) => a + reportMeanReward(b), 0) / data.withSkill.length
|
|
44
44
|
: 0;
|
|
45
45
|
const avgWithout = data.withoutSkill.length > 0
|
|
46
|
-
? data.withoutSkill.reduce((a, b) => a + b
|
|
46
|
+
? data.withoutSkill.reduce((a, b) => a + reportMeanReward(b), 0) / data.withoutSkill.length
|
|
47
47
|
: 0;
|
|
48
48
|
const allTrials = allReports.flatMap(r => r.trials);
|
|
49
49
|
const avgDurationMs = allTrials.length > 0
|
|
@@ -64,3 +64,6 @@ export class AnalyticsEngine {
|
|
|
64
64
|
return stats;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
function reportMeanReward(report) {
|
|
68
|
+
return report.mean_reward ?? report.pass_rate ?? 0;
|
|
69
|
+
}
|
|
@@ -24,6 +24,14 @@ export interface ReportOptions {
|
|
|
24
24
|
noComment?: boolean;
|
|
25
25
|
/** Override the comment-id used in the dedup marker. See `resolveCommentId`. */
|
|
26
26
|
commentId?: string;
|
|
27
|
+
/** Provider-neutral URL for a richer external report. */
|
|
28
|
+
detailsUrl?: string;
|
|
29
|
+
/** Provider-neutral status notice included in the report body. */
|
|
30
|
+
notice?: string;
|
|
31
|
+
/** Surface missing context, malformed results, and GitHub API failures. */
|
|
32
|
+
strict?: boolean;
|
|
33
|
+
/** Skip a comment update when the pull request has advanced. */
|
|
34
|
+
expectedHeadSha?: string;
|
|
27
35
|
}
|
|
28
36
|
/**
|
|
29
37
|
* Resolve the comment-id used for the dedup marker.
|
|
@@ -33,7 +41,7 @@ export interface ReportOptions {
|
|
|
33
41
|
*/
|
|
34
42
|
export declare function resolveCommentId(explicit?: string): string;
|
|
35
43
|
/**
|
|
36
|
-
* Entry point for `pathgrade report`.
|
|
37
|
-
*
|
|
44
|
+
* Entry point for `pathgrade report`. The default display-only mode never
|
|
45
|
+
* fails CI. Strict mode is intended for orchestrators that own publication.
|
|
38
46
|
*/
|
|
39
47
|
export declare function runReport(cwd: string, opts?: ReportOptions): Promise<void>;
|
package/dist/commands/report.js
CHANGED
|
@@ -43,10 +43,24 @@ function isPathgradeReport(value) {
|
|
|
43
43
|
if (!value || typeof value !== 'object')
|
|
44
44
|
return false;
|
|
45
45
|
const v = value;
|
|
46
|
-
return (v.version === 1 &&
|
|
46
|
+
return ((v.version === 1 || v.version === 2) &&
|
|
47
47
|
typeof v.overall_pass_rate === 'number' &&
|
|
48
48
|
(v.status === 'pass' || v.status === 'fail') &&
|
|
49
|
-
Array.isArray(v.groups)
|
|
49
|
+
Array.isArray(v.groups) &&
|
|
50
|
+
(v.version === 1 || (typeof v.overall_mean_reward === 'number'
|
|
51
|
+
&& hasValidAttemptCounts(v)
|
|
52
|
+
&& v.groups.every(group => typeof group.mean_reward === 'number'))));
|
|
53
|
+
}
|
|
54
|
+
function hasValidAttemptCounts(report) {
|
|
55
|
+
const requested = report.attempts_requested;
|
|
56
|
+
const completed = report.attempts_completed;
|
|
57
|
+
return typeof requested === 'number'
|
|
58
|
+
&& Number.isSafeInteger(requested)
|
|
59
|
+
&& requested >= 1
|
|
60
|
+
&& typeof completed === 'number'
|
|
61
|
+
&& Number.isSafeInteger(completed)
|
|
62
|
+
&& completed >= 0
|
|
63
|
+
&& completed <= requested;
|
|
50
64
|
}
|
|
51
65
|
async function loadReport(resolvedPath) {
|
|
52
66
|
if (!(await fs.pathExists(resolvedPath))) {
|
|
@@ -66,8 +80,8 @@ function printMarkdownAndPassRate(markdown, passRate) {
|
|
|
66
80
|
console.log(String(passRate));
|
|
67
81
|
}
|
|
68
82
|
/**
|
|
69
|
-
* Entry point for `pathgrade report`.
|
|
70
|
-
*
|
|
83
|
+
* Entry point for `pathgrade report`. The default display-only mode never
|
|
84
|
+
* fails CI. Strict mode is intended for orchestrators that own publication.
|
|
71
85
|
*/
|
|
72
86
|
export async function runReport(cwd, opts = {}) {
|
|
73
87
|
const resolvedPath = path.resolve(cwd, opts.resultsPath ?? DEFAULT_RESULTS_PATH);
|
|
@@ -77,6 +91,9 @@ export async function runReport(cwd, opts = {}) {
|
|
|
77
91
|
// Branch point: when true, post the comment and print only the pass rate.
|
|
78
92
|
const shouldPost = inCI && !noComment;
|
|
79
93
|
const prContext = shouldPost ? resolvePrContext(process.env) : null;
|
|
94
|
+
if (opts.strict && shouldPost && !prContext) {
|
|
95
|
+
throw new Error('pathgrade report: strict PR commenting requires GITHUB_TOKEN, GITHUB_REPOSITORY, and pull-request context');
|
|
96
|
+
}
|
|
80
97
|
let report = null;
|
|
81
98
|
let loadError = null;
|
|
82
99
|
try {
|
|
@@ -95,6 +112,8 @@ export async function runReport(cwd, opts = {}) {
|
|
|
95
112
|
await postOrUpdateComment(prContext, {
|
|
96
113
|
commentId,
|
|
97
114
|
body: markdown,
|
|
115
|
+
strict: opts.strict,
|
|
116
|
+
expectedHeadSha: opts.expectedHeadSha,
|
|
98
117
|
});
|
|
99
118
|
console.log('0');
|
|
100
119
|
}
|
|
@@ -108,15 +127,31 @@ export async function runReport(cwd, opts = {}) {
|
|
|
108
127
|
await postOrUpdateComment(prContext, {
|
|
109
128
|
commentId,
|
|
110
129
|
body: MISSING_RESULTS_BODY,
|
|
130
|
+
strict: opts.strict,
|
|
131
|
+
expectedHeadSha: opts.expectedHeadSha,
|
|
111
132
|
});
|
|
112
133
|
}
|
|
113
134
|
// Always emit a pass rate line so downstream capture doesn't explode.
|
|
114
135
|
console.log('0');
|
|
136
|
+
if (opts.strict)
|
|
137
|
+
throw new Error(`pathgrade report: ${loadError}`);
|
|
115
138
|
return;
|
|
116
139
|
}
|
|
117
|
-
const markdown = formatReportMarkdown(report, {
|
|
140
|
+
const markdown = formatReportMarkdown(report, {
|
|
141
|
+
commentId,
|
|
142
|
+
detailsUrl: opts.detailsUrl,
|
|
143
|
+
notice: opts.notice,
|
|
144
|
+
});
|
|
118
145
|
if (prContext) {
|
|
119
|
-
await postOrUpdateComment(prContext, {
|
|
146
|
+
const postResult = await postOrUpdateComment(prContext, {
|
|
147
|
+
commentId,
|
|
148
|
+
body: markdown,
|
|
149
|
+
strict: opts.strict,
|
|
150
|
+
expectedHeadSha: opts.expectedHeadSha,
|
|
151
|
+
});
|
|
152
|
+
if (postResult === 'stale') {
|
|
153
|
+
console.error('pathgrade report: pull-request head advanced; skipped stale comment update');
|
|
154
|
+
}
|
|
120
155
|
// Only the pass rate goes to stdout — the markdown lives on the PR.
|
|
121
156
|
console.log(String(report.overall_pass_rate));
|
|
122
157
|
return;
|
|
@@ -14,6 +14,7 @@ export function parsePathgradeRunArgs(args) {
|
|
|
14
14
|
let since;
|
|
15
15
|
let changedFilesPath;
|
|
16
16
|
let adapterName;
|
|
17
|
+
let attempts;
|
|
17
18
|
let passthrough = false;
|
|
18
19
|
for (const arg of args) {
|
|
19
20
|
if (passthrough) {
|
|
@@ -44,6 +45,10 @@ export function parsePathgradeRunArgs(args) {
|
|
|
44
45
|
adapterName = arg.slice('--adapter='.length);
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
48
|
+
if (arg.startsWith('--attempts=')) {
|
|
49
|
+
attempts = parseAttempts(arg.slice('--attempts='.length));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
47
52
|
if (arg.startsWith('--since=')) {
|
|
48
53
|
since = arg.slice('--since='.length);
|
|
49
54
|
continue;
|
|
@@ -68,8 +73,16 @@ export function parsePathgradeRunArgs(args) {
|
|
|
68
73
|
changed,
|
|
69
74
|
quiet,
|
|
70
75
|
adapterName,
|
|
76
|
+
attempts,
|
|
71
77
|
since,
|
|
72
78
|
changedFilesPath,
|
|
73
79
|
};
|
|
74
80
|
return warnings.length > 0 ? { ...base, warnings } : base;
|
|
75
81
|
}
|
|
82
|
+
function parseAttempts(value) {
|
|
83
|
+
const attempts = Number(value);
|
|
84
|
+
if (!Number.isSafeInteger(attempts) || attempts < 1) {
|
|
85
|
+
throw new Error('pathgrade: --attempts must be an integer greater than or equal to 1');
|
|
86
|
+
}
|
|
87
|
+
return attempts;
|
|
88
|
+
}
|
|
@@ -27,13 +27,13 @@ export async function runChanged(opts) {
|
|
|
27
27
|
const runnerEnv = buildRunnerEnv(parsed, {
|
|
28
28
|
PATHGRADE_SELECTION_INVOCATION_ID: selectionInvocationId,
|
|
29
29
|
});
|
|
30
|
-
const configPath = findVitestConfigArg(parsed.runnerArgs);
|
|
31
30
|
let config;
|
|
32
31
|
let runnerInvocation;
|
|
33
32
|
try {
|
|
34
33
|
config = await resolvePathgradeConfig({
|
|
35
34
|
cwd,
|
|
36
|
-
|
|
35
|
+
cli: parsed.attempts === undefined ? undefined : { attempts: parsed.attempts },
|
|
36
|
+
runnerArgs: parsed.runnerArgs,
|
|
37
37
|
warn: w => {
|
|
38
38
|
if (!parsed.quiet)
|
|
39
39
|
process.stderr.write(`${w}\n`);
|
|
@@ -107,6 +107,7 @@ export async function runChanged(opts) {
|
|
|
107
107
|
totalEvals: evalFiles.length,
|
|
108
108
|
changedCount: changedFiles.length,
|
|
109
109
|
result,
|
|
110
|
+
attempts: config.attempts,
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
// Persist the sidecar immediately — even on empty selection, so the
|
|
@@ -128,7 +129,7 @@ export async function runChanged(opts) {
|
|
|
128
129
|
cwd,
|
|
129
130
|
runnerArgs,
|
|
130
131
|
selectedFiles,
|
|
131
|
-
env: runnerEnv,
|
|
132
|
+
env: { ...runnerEnv, PATHGRADE_ATTEMPT_COUNT: String(config.attempts) },
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
function printRunStartSummary(input) {
|
|
@@ -140,6 +141,7 @@ function printRunStartSummary(input) {
|
|
|
140
141
|
const globalLabel = result.globalMatch ? `\`${result.globalMatch}\`` : 'none';
|
|
141
142
|
process.stderr.write(` global matches: ${globalLabel}\n`);
|
|
142
143
|
process.stderr.write(` selected: ${result.selected.length} / ${totalEvals} evals\n`);
|
|
144
|
+
process.stderr.write(` attempts: ${input.attempts} per selected case\n`);
|
|
143
145
|
for (const entry of result.selected) {
|
|
144
146
|
process.stderr.write(` ${entry.file}\n`);
|
|
145
147
|
}
|
|
@@ -158,15 +160,3 @@ function readChangedFilesList(filePath) {
|
|
|
158
160
|
function errMsg(err) {
|
|
159
161
|
return err instanceof Error ? err.message : String(err);
|
|
160
162
|
}
|
|
161
|
-
function findVitestConfigArg(args) {
|
|
162
|
-
for (let i = 0; i < args.length; i++) {
|
|
163
|
-
const arg = args[i];
|
|
164
|
-
if (arg === '--config' || arg === '-c')
|
|
165
|
-
return args[i + 1];
|
|
166
|
-
if (arg.startsWith('--config='))
|
|
167
|
-
return arg.slice('--config='.length);
|
|
168
|
-
if (arg.startsWith('-c='))
|
|
169
|
-
return arg.slice('-c='.length);
|
|
170
|
-
}
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export interface PathgradeConfig {
|
|
2
|
+
attempts?: number;
|
|
2
3
|
runner?: {
|
|
3
4
|
adapter?: string;
|
|
4
5
|
args?: string[];
|
|
@@ -18,6 +19,7 @@ export interface PathgradeConfig {
|
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
export interface ResolvedPathgradeConfig {
|
|
22
|
+
attempts: number;
|
|
21
23
|
runner: {
|
|
22
24
|
adapter: string;
|
|
23
25
|
args: string[];
|
|
@@ -44,5 +46,6 @@ export declare function resolvePathgradeConfig(input: {
|
|
|
44
46
|
cli?: PathgradeConfig;
|
|
45
47
|
configPath?: string;
|
|
46
48
|
legacyVitestConfigPath?: string;
|
|
49
|
+
runnerArgs?: readonly string[];
|
|
47
50
|
warn?: (message: string) => void;
|
|
48
51
|
}): Promise<ResolvedPathgradeConfig>;
|
package/dist/config/pathgrade.js
CHANGED
|
@@ -17,6 +17,7 @@ const PATHGRADE_CONFIG_CANDIDATES = [
|
|
|
17
17
|
];
|
|
18
18
|
export function defaultPathgradeConfig() {
|
|
19
19
|
return {
|
|
20
|
+
attempts: 1,
|
|
20
21
|
runner: {
|
|
21
22
|
adapter: 'vitest',
|
|
22
23
|
args: [],
|
|
@@ -35,13 +36,30 @@ export function defaultPathgradeConfig() {
|
|
|
35
36
|
}
|
|
36
37
|
export async function resolvePathgradeConfig(input) {
|
|
37
38
|
const fileConfig = await loadPathgradeConfigFile(input.cwd, input.configPath);
|
|
38
|
-
const legacyConfig = await loadLegacyVitestConfig(input.cwd, input.legacyVitestConfigPath
|
|
39
|
+
const legacyConfig = await loadLegacyVitestConfig(input.cwd, input.legacyVitestConfigPath ?? findVitestConfigArg([
|
|
40
|
+
...(fileConfig?.runner?.args ?? []),
|
|
41
|
+
...(input.runnerArgs ?? []),
|
|
42
|
+
]), input.warn);
|
|
39
43
|
return mergePathgradeConfig(mergePathgradeConfig(mergePathgradeConfig(defaultPathgradeConfig(), legacyConfig), fileConfig), input.cli);
|
|
40
44
|
}
|
|
45
|
+
function findVitestConfigArg(args) {
|
|
46
|
+
let configPath;
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i];
|
|
49
|
+
if (arg === '--config' || arg === '-c')
|
|
50
|
+
configPath = args[i + 1];
|
|
51
|
+
else if (arg.startsWith('--config='))
|
|
52
|
+
configPath = arg.slice('--config='.length);
|
|
53
|
+
else if (arg.startsWith('-c='))
|
|
54
|
+
configPath = arg.slice('-c='.length);
|
|
55
|
+
}
|
|
56
|
+
return configPath;
|
|
57
|
+
}
|
|
41
58
|
function mergePathgradeConfig(base, override) {
|
|
42
59
|
if (!override)
|
|
43
60
|
return base;
|
|
44
61
|
return {
|
|
62
|
+
attempts: override.attempts ?? base.attempts,
|
|
45
63
|
runner: {
|
|
46
64
|
adapter: override.runner?.adapter ?? base.runner.adapter,
|
|
47
65
|
args: override.runner?.args ?? base.runner.args,
|
|
@@ -91,6 +109,7 @@ function validatePathgradeConfig(value, label) {
|
|
|
91
109
|
throw invalidConfig(label, 'default export must be an object');
|
|
92
110
|
}
|
|
93
111
|
validateOptionalObject(value.runner, label, 'runner');
|
|
112
|
+
validateOptionalPositiveInteger(value.attempts, label, 'attempts');
|
|
94
113
|
const runner = asOptionalObject(value.runner);
|
|
95
114
|
validateOptionalString(runner?.adapter, label, 'runner.adapter');
|
|
96
115
|
validateOptionalStringArray(runner?.args, label, 'runner.args');
|
|
@@ -139,6 +158,11 @@ function validateOptionalNumber(value, label, field) {
|
|
|
139
158
|
throw invalidConfig(label, `${field} must be a number`);
|
|
140
159
|
}
|
|
141
160
|
}
|
|
161
|
+
function validateOptionalPositiveInteger(value, label, field) {
|
|
162
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || Number(value) < 1)) {
|
|
163
|
+
throw invalidConfig(label, `${field} must be an integer greater than or equal to 1`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
142
166
|
function invalidConfig(label, reason) {
|
|
143
167
|
return new InvalidPathgradeConfigError(`pathgrade: invalid ${label}: ${reason}`);
|
|
144
168
|
}
|
|
@@ -179,6 +203,14 @@ async function loadLegacyVitestConfig(cwd, configPath, warn = () => { }) {
|
|
|
179
203
|
if (!isObject(opts))
|
|
180
204
|
return undefined;
|
|
181
205
|
return {
|
|
206
|
+
...(opts.reporter === 'cli' || opts.reporter === 'browser' || opts.reporter === 'json'
|
|
207
|
+
? { reporter: opts.reporter }
|
|
208
|
+
: {}),
|
|
209
|
+
...(typeof opts.diagnostics === 'boolean' ? { diagnostics: opts.diagnostics } : {}),
|
|
210
|
+
...(typeof opts.verbose === 'boolean' ? { verbose: opts.verbose } : {}),
|
|
211
|
+
...(isObject(opts.ci) && typeof opts.ci.threshold === 'number'
|
|
212
|
+
? { ci: { threshold: opts.ci.threshold } }
|
|
213
|
+
: {}),
|
|
182
214
|
evals: {
|
|
183
215
|
...(Array.isArray(opts.include) ? { include: opts.include } : {}),
|
|
184
216
|
...(Array.isArray(opts.exclude) ? { exclude: opts.exclude } : {}),
|
package/dist/pathgrade.js
CHANGED
|
@@ -156,10 +156,27 @@ export async function runPathgradeCli(options = {}) {
|
|
|
156
156
|
const reportArgs = args.slice(1);
|
|
157
157
|
const resultsPathFlag = reportArgs.find(a => a.startsWith('--results-path='));
|
|
158
158
|
const commentIdFlag = reportArgs.find(a => a.startsWith('--comment-id='));
|
|
159
|
+
const detailsUrlFlag = reportArgs.find(a => a.startsWith('--details-url='));
|
|
160
|
+
const noticeFlag = reportArgs.find(a => a.startsWith('--notice='));
|
|
161
|
+
const expectedHeadShaFlag = reportArgs.find(a => a.startsWith('--expected-head-sha='));
|
|
159
162
|
const noComment = reportArgs.includes('--no-comment');
|
|
163
|
+
const strict = reportArgs.includes('--strict');
|
|
160
164
|
const resultsPath = resultsPathFlag ? resultsPathFlag.split('=').slice(1).join('=') : undefined;
|
|
161
165
|
const commentId = commentIdFlag ? commentIdFlag.split('=').slice(1).join('=') : undefined;
|
|
162
|
-
|
|
166
|
+
const detailsUrl = detailsUrlFlag ? detailsUrlFlag.split('=').slice(1).join('=') : undefined;
|
|
167
|
+
const notice = noticeFlag ? noticeFlag.split('=').slice(1).join('=') : undefined;
|
|
168
|
+
const expectedHeadSha = expectedHeadShaFlag
|
|
169
|
+
? expectedHeadShaFlag.split('=').slice(1).join('=')
|
|
170
|
+
: undefined;
|
|
171
|
+
await runReport(process.cwd(), {
|
|
172
|
+
resultsPath,
|
|
173
|
+
commentId,
|
|
174
|
+
detailsUrl,
|
|
175
|
+
notice,
|
|
176
|
+
expectedHeadSha,
|
|
177
|
+
noComment,
|
|
178
|
+
strict,
|
|
179
|
+
});
|
|
163
180
|
return;
|
|
164
181
|
}
|
|
165
182
|
if (command === 'run' || !command || command.startsWith('-')) {
|
|
@@ -184,7 +201,14 @@ export async function runPathgradeCli(options = {}) {
|
|
|
184
201
|
await clearSidecar(process.cwd());
|
|
185
202
|
const env = buildRunnerEnv(parsed);
|
|
186
203
|
try {
|
|
187
|
-
const config = await resolvePathgradeConfig({
|
|
204
|
+
const config = await resolvePathgradeConfig({
|
|
205
|
+
cwd: process.cwd(),
|
|
206
|
+
cli: parsed.attempts === undefined ? undefined : { attempts: parsed.attempts },
|
|
207
|
+
runnerArgs: parsed.runnerArgs,
|
|
208
|
+
});
|
|
209
|
+
if (!parsed.quiet && config.attempts > 1) {
|
|
210
|
+
console.error(`pathgrade: ${config.attempts} sequential attempts per selected case`);
|
|
211
|
+
}
|
|
188
212
|
const runner = await loadRunnerInvocationAdapter({
|
|
189
213
|
adapterName: parsed.adapterName ?? config.runner.adapter,
|
|
190
214
|
cwd: process.cwd(),
|
|
@@ -193,7 +217,7 @@ export async function runPathgradeCli(options = {}) {
|
|
|
193
217
|
process.exitCode = await runner.run({
|
|
194
218
|
cwd: process.cwd(),
|
|
195
219
|
runnerArgs: [...config.runner.args, ...parsed.runnerArgs],
|
|
196
|
-
env,
|
|
220
|
+
env: { ...env, PATHGRADE_ATTEMPT_COUNT: String(config.attempts) },
|
|
197
221
|
});
|
|
198
222
|
}
|
|
199
223
|
catch (err) {
|
|
@@ -216,6 +240,7 @@ function printHelp(cliName) {
|
|
|
216
240
|
[--since=<ref>] Override base ref (implies git mode)
|
|
217
241
|
[--changed-files=<path>] Use an explicit newline-delimited file list
|
|
218
242
|
[--adapter=<name|path>] Select built-in or third-party runner adapter
|
|
243
|
+
[--attempts=N] Run every selected case N times sequentially
|
|
219
244
|
[--diagnostics] Print full diagnostics for passing evals too
|
|
220
245
|
[--quiet] Suppress the run-start summary
|
|
221
246
|
[--verbose|-v] Stream live per-turn events to stderr during the run
|
|
@@ -234,6 +259,10 @@ function printHelp(cliName) {
|
|
|
234
259
|
[--results-path=<path>] Override results.json location
|
|
235
260
|
[--no-comment] Print markdown to stdout; do not post
|
|
236
261
|
[--comment-id=<id>] Override comment marker (default: $GITHUB_WORKFLOW:$GITHUB_JOB)
|
|
262
|
+
[--details-url=<url>] Add a provider-neutral full-report link
|
|
263
|
+
[--notice=<text>] Add a provider-neutral report status notice
|
|
264
|
+
[--expected-head-sha=<sha>] Skip stale PR comment updates
|
|
265
|
+
[--strict] Fail on missing context, results, or GitHub errors
|
|
237
266
|
${cliName} affected Print eval files affected by a change-set (one per line)
|
|
238
267
|
[--since=<ref>] Diff <ref>...HEAD (overrides git auto-detection)
|
|
239
268
|
[--changed-files=<path>] Newline-delimited repo-relative file list
|
package/dist/reporters/cli.js
CHANGED
|
@@ -18,8 +18,8 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
18
18
|
}
|
|
19
19
|
console.log(`\n${fmt.bold('pathgrade preview')} ${fmt.dim(`${entries.length} reports from ${resolved}`)}\n`);
|
|
20
20
|
for (const { file, ...report } of entries) {
|
|
21
|
-
const
|
|
22
|
-
const isPass =
|
|
21
|
+
const meanReward = report.mean_reward ?? report.pass_rate ?? 0;
|
|
22
|
+
const isPass = report.status === undefined ? meanReward >= 0.5 : report.status === 'pass';
|
|
23
23
|
const trials = report.trials || [];
|
|
24
24
|
const avgDur = trials.reduce((s, t) => s + (t.duration_ms || 0), 0) / (trials.length || 1);
|
|
25
25
|
const totalTokens = trials.reduce((s, t) => s + (t.input_tokens || 0) + (t.output_tokens || 0) + (t.conversation_input_tokens || 0) + (t.conversation_output_tokens || 0), 0);
|
|
@@ -33,9 +33,9 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
33
33
|
console.log();
|
|
34
34
|
// ── Summary metrics
|
|
35
35
|
const metrics = [
|
|
36
|
-
['
|
|
37
|
-
['
|
|
38
|
-
['pass
|
|
36
|
+
['Mean Reward', `${(meanReward * 100).toFixed(1)}%`],
|
|
37
|
+
['Success Rate', report.success_rate != null ? `${(report.success_rate * 100).toFixed(1)}%` : '—'],
|
|
38
|
+
['pass@k', formatPassAtK(report.pass_at_k, report.pass_at_k_unavailable_reason)],
|
|
39
39
|
['Avg Duration', `${(avgDur / 1000).toFixed(1)}s`],
|
|
40
40
|
['Total Tokens', `~${totalTokens}`],
|
|
41
41
|
['Skills', report.skills_used?.join(', ') || 'none'],
|
|
@@ -47,7 +47,7 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
47
47
|
// ── Trials
|
|
48
48
|
for (const trial of trials) {
|
|
49
49
|
const evaluated = trial.reward !== undefined;
|
|
50
|
-
const tp = evaluated && trial.reward >= 0.5;
|
|
50
|
+
const tp = evaluated && (trial.runner_outcome === undefined ? trial.reward >= 0.5 : trial.runner_outcome === 'passed' && trial.reward === 1);
|
|
51
51
|
const trialStatus = !evaluated ? fmt.dim('N/A') : tp ? fmt.pass('PASS') : fmt.fail('FAIL');
|
|
52
52
|
const reward = fmt.bold(evaluated ? trial.reward.toFixed(2) : 'n/a');
|
|
53
53
|
const dur = `${((trial.duration_ms || 0) / 1000).toFixed(1)}s`;
|
|
@@ -120,6 +120,13 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
120
120
|
console.log();
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
function formatPassAtK(value, reason) {
|
|
124
|
+
if (typeof value === 'number')
|
|
125
|
+
return `${(value * 100).toFixed(1)}% (legacy v1)`;
|
|
126
|
+
if (value)
|
|
127
|
+
return Object.entries(value).map(([k, metric]) => `@${k} ${(metric * 100).toFixed(1)}%`).join(', ');
|
|
128
|
+
return reason ? `— (${reason.replaceAll('_', ' ')})` : '—';
|
|
129
|
+
}
|
|
123
130
|
function formatScorerStatus(status) {
|
|
124
131
|
switch (status) {
|
|
125
132
|
case 'error':
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
import type { PathgradeReport, PathgradeSelectionReport } from '../types.js';
|
|
11
11
|
export interface FormatOptions {
|
|
12
12
|
commentId: string;
|
|
13
|
+
/** Provider-neutral URL for a richer external report. */
|
|
14
|
+
detailsUrl?: string;
|
|
15
|
+
/** Provider-neutral status notice rendered above report details. */
|
|
16
|
+
notice?: string;
|
|
13
17
|
}
|
|
14
18
|
/** Minimal body posted when `.pathgrade/results.json` is missing. */
|
|
15
19
|
export declare const MISSING_RESULTS_BODY = "Pathgrade evals did not produce results. Check the workflow logs.";
|
|
@@ -55,14 +59,19 @@ export interface PostOptions {
|
|
|
55
59
|
commentId: string;
|
|
56
60
|
/** Body WITHOUT the marker — the marker is prepended if not already present. */
|
|
57
61
|
body: string;
|
|
62
|
+
/** Throw instead of swallowing GitHub API failures. */
|
|
63
|
+
strict?: boolean;
|
|
64
|
+
/** Skip the update if the pull request has advanced to another head. */
|
|
65
|
+
expectedHeadSha?: string;
|
|
58
66
|
}
|
|
67
|
+
export type PostResult = 'created' | 'updated' | 'stale';
|
|
59
68
|
/**
|
|
60
69
|
* Find an existing PR comment carrying `<!-- pathgrade:${commentId} -->`
|
|
61
|
-
* and update it; otherwise create a new one.
|
|
62
|
-
*
|
|
70
|
+
* and update it; otherwise create a new one. Default mode logs and swallows
|
|
71
|
+
* transport errors; strict mode rethrows them for an owning orchestrator.
|
|
63
72
|
*
|
|
64
73
|
* Bodies longer than GitHub's 65,536-character limit are truncated with a
|
|
65
74
|
* sentinel pointing to workflow artifacts, preserving the leading dedup
|
|
66
75
|
* marker so subsequent runs still find and update this comment.
|
|
67
76
|
*/
|
|
68
|
-
export declare function postOrUpdateComment(ctx: PrContext, opts: PostOptions): Promise<
|
|
77
|
+
export declare function postOrUpdateComment(ctx: PrContext, opts: PostOptions): Promise<PostResult | undefined>;
|