@wix/pathgrade 1.0.38 → 1.0.39
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 +1 -1
- package/dist/agents/claude/sdk-message-projector.js +4 -5
- package/dist/agents/claude/tool-permission-bridge.js +2 -1
- package/dist/agents/codex-app-server/item-projection.js +2 -2
- package/dist/agents/codex-app-server/mcp-approval-correlator.js +3 -2
- package/dist/agents/opencode.js +4 -5
- package/dist/commands/report.js +5 -25
- package/dist/internal/direct-mcp-v2/acp-author-projector.js +20 -7
- package/dist/reporters/cli.js +20 -1
- package/dist/reporters/github-comment.js +18 -7
- package/dist/reporters/loader.d.ts +4 -0
- package/dist/reporters/loader.js +26 -9
- package/dist/reporting/core.js +17 -7
- package/dist/reporting/report-parser.js +57 -2
- package/dist/reporting/types.d.ts +2 -1
- package/dist/runners/orchestrator.js +3 -4
- package/dist/runners/repeated-invocation.js +2 -5
- package/dist/runners/report-projection.js +1 -0
- package/dist/sdk/agent.js +10 -1
- package/dist/sdk/evaluate.js +58 -3
- package/dist/sdk/judge-prompt-builder.js +11 -7
- package/dist/sdk/mcp-event-input.d.ts +1 -0
- package/dist/sdk/mcp-event-input.js +3 -0
- package/dist/sdk/mcp-evidence.js +16 -3
- package/dist/sdk/mcp-safety.js +2 -2
- package/dist/sdk/scripted-mcp-events.js +3 -2
- package/dist/sdk/tool-event-log.js +18 -3
- package/dist/sdk/tool-event-secrets.d.ts +4 -0
- package/dist/sdk/tool-event-secrets.js +33 -2
- package/dist/sdk/types.d.ts +2 -0
- package/dist/tool-event-results.d.ts +3 -0
- package/dist/tool-event-results.js +153 -23
- package/dist/types.d.ts +9 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -461,7 +461,7 @@ export default {
|
|
|
461
461
|
|
|
462
462
|
Pathgrade reads `pathgrade.config.*` for CLI and affected-selection behavior. `runner.adapter` and `--adapter=<name|path>` select the runner; `--adapter` wins over config. `attempts` defaults to `1` and must be a positive integer. Built-in adapters `vitest`, `jest`, and `node-test` support repeated attempts; third-party invocation adapters must advertise `supportsRepeatedAttempts: true` and produce the normalized child snapshot contract.
|
|
463
463
|
|
|
464
|
-
New reports use schema version 2. They preserve every attempt with `case_id`, `attempt_id`, and `attempt_index`; include the display-safe `runner_outcome` that determined binary success; publish `mean_reward` for partial scores; and publish finite-sample pass@k only for complete binary attempts. Runner assertions, runner diagnostics, and native runner references remain in the normalized run model and are not copied into public report artifacts; evaluation diagnostics retain their existing report behavior. A partial reward or incomplete attempt makes pass@k explicitly unavailable. Version-1 reports remain readable, but Pathgrade no longer computes pass@k or pass^k by pooling heterogeneous cases.
|
|
464
|
+
New reports use schema version 2. They preserve every attempt with `case_id`, `attempt_id`, and `attempt_index`; include the display-safe `runner_outcome` that determined binary success; publish `mean_reward` for partial scores; and publish finite-sample pass@k only for complete binary attempts. `runner_status` and `threshold_status` expose the two gates independently, and canonical `status` passes only when runner assertions pass and any configured threshold passes. Runner assertions, runner diagnostics, and native runner references remain in the normalized run model and are not copied into public report artifacts; evaluation diagnostics retain their existing report behavior. A partial reward or incomplete attempt makes pass@k explicitly unavailable. Version-1 and older version-2 reports remain readable with missing gate fields derived during loading, but Pathgrade no longer computes pass@k or pass^k by pooling heterogeneous cases.
|
|
465
465
|
|
|
466
466
|
Third-party runner adapters are supported through `@wix/pathgrade/adapter-kit`. Adapter names resolve as follows:
|
|
467
467
|
|
|
@@ -22,10 +22,9 @@ const SDK_ERROR_SUBTYPES = [
|
|
|
22
22
|
'error_max_structured_output_retries',
|
|
23
23
|
];
|
|
24
24
|
import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
|
|
25
|
-
import { sanitizePersistenceValue } from '../../tool-event-results.js';
|
|
25
|
+
import { sanitizePersistenceValue, sanitizeUntrustedPersistenceValue } from '../../tool-event-results.js';
|
|
26
26
|
import { attachTurnResultSensitiveValues } from '../../sdk/turn-result-secrets.js';
|
|
27
|
-
import { attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
28
|
-
import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
|
|
27
|
+
import { attachLiveMcpInput, attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
29
28
|
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
30
29
|
import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
|
|
31
30
|
export function projectSdkMessages(input) {
|
|
@@ -145,7 +144,7 @@ export function projectSdkMessages(input) {
|
|
|
145
144
|
...(input.deniedMcpEvents?.all() ?? []),
|
|
146
145
|
]);
|
|
147
146
|
const finalToolEvents = prependSlashCommandSkillEvent(enriched, input.firstMessage, initSkills).map((event) => attachToolEventSensitiveValues(event, input.sensitiveValues ?? []));
|
|
148
|
-
const traceOutput =
|
|
147
|
+
const traceOutput = sanitizeUntrustedPersistenceValue(input.messages, input.sensitiveValues)
|
|
149
148
|
.map((message) => JSON.stringify(message))
|
|
150
149
|
.join('\n');
|
|
151
150
|
const result = {
|
|
@@ -216,7 +215,7 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
|
|
|
216
215
|
tool: mcpTool.tool,
|
|
217
216
|
status: 'incomplete',
|
|
218
217
|
};
|
|
219
|
-
return
|
|
218
|
+
return attachLiveMcpInput({
|
|
220
219
|
action: 'mcp_tool_call',
|
|
221
220
|
provider: 'claude',
|
|
222
221
|
providerToolName: normalizedProviderToolName,
|
|
@@ -3,6 +3,7 @@ import { createAskUserBridge, } from './ask-user-bridge.js';
|
|
|
3
3
|
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
4
4
|
import { canonicalizeJson } from '../../core/canonical-json.js';
|
|
5
5
|
import { buildScriptedMcpApprovalEvent, buildScriptedMcpDeniedCallEvent, } from '../../sdk/scripted-mcp-events.js';
|
|
6
|
+
import { attachLiveMcpInput } from '../../sdk/tool-event-secrets.js';
|
|
6
7
|
export function createClaudeToolPermissionBridge(deps) {
|
|
7
8
|
const askUserBridge = createAskUserBridge({
|
|
8
9
|
askBus: deps.askBus,
|
|
@@ -149,5 +150,5 @@ function recordDeniedMcpEvent(opts) {
|
|
|
149
150
|
input: opts.input,
|
|
150
151
|
})).slice(0, 200),
|
|
151
152
|
};
|
|
152
|
-
opts.store.record(opts.toolUseId, event);
|
|
153
|
+
opts.store.record(opts.toolUseId, attachLiveMcpInput(event, opts.input));
|
|
153
154
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildSummary, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
|
|
2
2
|
import { sanitizeToolEventResult } from '../../tool-event-results.js';
|
|
3
|
-
import {
|
|
3
|
+
import { attachLiveMcpInput } from '../../sdk/tool-event-secrets.js';
|
|
4
4
|
export function projectItemIntoTurn(item, turn, sensitiveValues, timing = {}) {
|
|
5
5
|
if (item.type === 'agentMessage') {
|
|
6
6
|
const message = item;
|
|
@@ -76,7 +76,7 @@ function projectMcpCall(call, turn, sensitiveValues, timing) {
|
|
|
76
76
|
const status = call.status === 'completed' && error === undefined
|
|
77
77
|
? 'completed' : call.status === 'failed' || error !== undefined ? 'error' : 'incomplete';
|
|
78
78
|
const resultContent = call.result === undefined ? undefined : JSON.stringify(call.result);
|
|
79
|
-
turn.nonAskToolEvents.push(
|
|
79
|
+
turn.nonAskToolEvents.push(attachLiveMcpInput({
|
|
80
80
|
action: 'mcp_tool_call', provider: 'codex', providerToolName, toolUseId: call.id,
|
|
81
81
|
turnNumber: turn.turnNumber, status, ...projectToolTiming(timing, finiteNumber(call.durationMs)),
|
|
82
82
|
arguments: { ...args, server: call.server, tool: call.tool, status: call.status ?? 'unknown' },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
2
2
|
import { canonicalizeJson } from '../../core/canonical-json.js';
|
|
3
3
|
import { buildScriptedMcpApprovalEvent, buildScriptedMcpDeniedCallEvent, } from '../../sdk/scripted-mcp-events.js';
|
|
4
|
+
import { attachLiveMcpInput } from '../../sdk/tool-event-secrets.js';
|
|
4
5
|
function isRecord(value) {
|
|
5
6
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
6
7
|
}
|
|
@@ -64,7 +65,7 @@ export function queuePolicyDeniedMcpToolCall(turn, request, decision, rawParams)
|
|
|
64
65
|
export function buildPolicyDeniedMcpToolEvent(turnNumber, pending, terminal) {
|
|
65
66
|
const args = redactMcpSecrets(pending.request.arguments);
|
|
66
67
|
const providerToolName = `${pending.request.serverName}.${pending.request.toolName}`;
|
|
67
|
-
return {
|
|
68
|
+
return attachLiveMcpInput({
|
|
68
69
|
action: 'mcp_tool_call', provider: 'codex', providerToolName,
|
|
69
70
|
...(terminal ? { toolUseId: terminal.id } : {}),
|
|
70
71
|
turnNumber, status: 'error',
|
|
@@ -79,7 +80,7 @@ export function buildPolicyDeniedMcpToolEvent(turnNumber, pending, terminal) {
|
|
|
79
80
|
},
|
|
80
81
|
summary: `MCP tool ${providerToolName} policy_denied`, confidence: 'high',
|
|
81
82
|
rawSnippet: JSON.stringify(redactMcpSecrets(terminal ? { approval: pending.rawParams, terminal } : pending.rawParams)),
|
|
82
|
-
};
|
|
83
|
+
}, pending.request.arguments);
|
|
83
84
|
}
|
|
84
85
|
export function consumeMatchingMcpDenial(turn, serverName, toolName, args) {
|
|
85
86
|
const argsKey = canonicalizeJson(args);
|
package/dist/agents/opencode.js
CHANGED
|
@@ -3,12 +3,11 @@ import * as path from 'node:path';
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
|
|
5
5
|
import { buildSummary, enrichSkillEvents } from '../tool-events.js';
|
|
6
|
-
import { collectSensitiveEnvValues,
|
|
6
|
+
import { collectSensitiveEnvValues, sanitizeToolEventResult, sanitizeUntrustedPersistenceValue, } from '../tool-event-results.js';
|
|
7
7
|
import { readStagedMcpServers } from '../providers/mcp-config.js';
|
|
8
8
|
import { removeSandboxRoot } from '../providers/sandbox-lifecycle.js';
|
|
9
9
|
import { attachTurnResultSensitiveValues, cloneTurnResultWithSensitiveValues } from '../sdk/turn-result-secrets.js';
|
|
10
|
-
import {
|
|
11
|
-
import { attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
|
|
10
|
+
import { attachLiveMcpInput, attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
|
|
12
11
|
import { currentOpenCodePlatformKey, OPENCODE_RUNTIME_LOCK, } from './opencode/contract.js';
|
|
13
12
|
import { OpenCodeRuntimePolicy, OPENCODE_PERMISSION } from './opencode/runtime-policy.js';
|
|
14
13
|
import { killOpenCodeProcessGroup, registerOpenCodeProcessGroup, unregisterOpenCodeProcessGroup, } from './opencode/process-groups.js';
|
|
@@ -217,7 +216,7 @@ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames, sensiti
|
|
|
217
216
|
rawSnippet: JSON.stringify({ tool, status: state.status, input }).slice(0, 2_000),
|
|
218
217
|
};
|
|
219
218
|
toolEvents.push(action === 'mcp_tool_call' && input
|
|
220
|
-
?
|
|
219
|
+
? attachLiveMcpInput(toolEvent, input)
|
|
221
220
|
: toolEvent);
|
|
222
221
|
sanitizedTrace.push({ type, tool, status, input });
|
|
223
222
|
continue;
|
|
@@ -256,7 +255,7 @@ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames, sensiti
|
|
|
256
255
|
if (stepFinishCount === 0)
|
|
257
256
|
throw new Error('OpenCode protocol error: missing step_finish');
|
|
258
257
|
const assistantMessage = textParts.join('');
|
|
259
|
-
const traceOutput =
|
|
258
|
+
const traceOutput = sanitizeUntrustedPersistenceValue(sanitizedTrace, sensitiveValues)
|
|
260
259
|
.map((event) => JSON.stringify(event))
|
|
261
260
|
.join('\n');
|
|
262
261
|
return {
|
package/dist/commands/report.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import * as path from 'path';
|
|
21
21
|
import fs from 'fs-extra';
|
|
22
22
|
import { readSidecar } from '../affected/sidecar.js';
|
|
23
|
+
import { parsePathgradeReport } from '../reporting/report-parser.js';
|
|
23
24
|
import { formatNoAffectedEvalsMarkdown, formatReportMarkdown, MISSING_RESULTS_BODY, postOrUpdateComment, resolvePrContext, } from '../reporters/github-comment.js';
|
|
24
25
|
const DEFAULT_RESULTS_PATH = path.join('.pathgrade', 'results.json');
|
|
25
26
|
/**
|
|
@@ -39,38 +40,17 @@ function resolveCommentId(explicit) {
|
|
|
39
40
|
return workflow;
|
|
40
41
|
return 'default';
|
|
41
42
|
}
|
|
42
|
-
function isPathgradeReport(value) {
|
|
43
|
-
if (!value || typeof value !== 'object')
|
|
44
|
-
return false;
|
|
45
|
-
const v = value;
|
|
46
|
-
return ((v.version === 1 || v.version === 2) &&
|
|
47
|
-
typeof v.overall_pass_rate === 'number' &&
|
|
48
|
-
(v.status === 'pass' || v.status === 'fail') &&
|
|
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;
|
|
64
|
-
}
|
|
65
43
|
async function loadReport(resolvedPath) {
|
|
66
44
|
if (!(await fs.pathExists(resolvedPath))) {
|
|
67
45
|
throw new Error(`results file not found at ${resolvedPath}`);
|
|
68
46
|
}
|
|
69
47
|
const raw = await fs.readJSON(resolvedPath);
|
|
70
|
-
|
|
48
|
+
try {
|
|
49
|
+
return parsePathgradeReport(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
71
52
|
throw new Error(`results file at ${resolvedPath} is not a valid pathgrade report`);
|
|
72
53
|
}
|
|
73
|
-
return raw;
|
|
74
54
|
}
|
|
75
55
|
function printMarkdownAndPassRate(markdown, passRate) {
|
|
76
56
|
// Strip trailing newlines so we control spacing precisely:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { buildSummary } from '../../tool-events.js';
|
|
2
|
-
import { sanitizePersistenceValue, sanitizeToolEventResult } from '../../tool-event-results.js';
|
|
2
|
+
import { sanitizePersistenceValue, sanitizeToolEventResult, sanitizeUntrustedPersistenceValue, } from '../../tool-event-results.js';
|
|
3
|
+
import { getOriginalMcpInput } from '../../sdk/mcp-event-input.js';
|
|
4
|
+
import { attachLiveMcpInput, attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
3
5
|
export const ACP_AUTHOR_TRACE_MAX_CHARS = 64 * 1024;
|
|
4
6
|
function object(value) {
|
|
5
7
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
@@ -92,7 +94,7 @@ export function projectAcpAuthorTurn(input) {
|
|
|
92
94
|
sessionUpdate: current.sessionUpdate,
|
|
93
95
|
title: current.title,
|
|
94
96
|
...(current.status ? { status: current.status } : {}),
|
|
95
|
-
...(current.rawInput ? { rawInput: current.rawInput } : {}),
|
|
97
|
+
...(current.rawInput ? { rawInput: sanitizeUntrustedPersistenceValue(current.rawInput, redactionValues) } : {}),
|
|
96
98
|
});
|
|
97
99
|
if (current.status !== 'completed' && current.status !== 'failed') {
|
|
98
100
|
if (current.toolCallId)
|
|
@@ -114,7 +116,7 @@ export function projectAcpAuthorTurn(input) {
|
|
|
114
116
|
toolCalls.set(current.toolCallId, { ...correlated, terminalFingerprint });
|
|
115
117
|
const normalized = input.normalizeToolName(correlated.title);
|
|
116
118
|
const resultText = textValue(correlated.status === 'completed' ? correlated.output : correlated.error);
|
|
117
|
-
|
|
119
|
+
const event = {
|
|
118
120
|
action: normalized.action,
|
|
119
121
|
provider: input.provider,
|
|
120
122
|
providerToolName: normalized.providerToolName,
|
|
@@ -124,18 +126,21 @@ export function projectAcpAuthorTurn(input) {
|
|
|
124
126
|
...(resultText !== undefined ? { result: sanitizeToolEventResult({ content: resultText }, redactionValues) } : {}),
|
|
125
127
|
summary: buildSummary(normalized.action, normalized.providerToolName, correlated.rawInput),
|
|
126
128
|
confidence: normalized.action === 'unknown' ? 'low' : 'high',
|
|
127
|
-
rawSnippet: JSON.stringify(
|
|
128
|
-
}
|
|
129
|
+
rawSnippet: JSON.stringify(sanitizeUntrustedPersistenceValue({ title: normalized.providerToolName, status: correlated.status, rawInput: correlated.rawInput }, redactionValues)).slice(0, 2_000),
|
|
130
|
+
};
|
|
131
|
+
toolEvents.push(normalized.action === 'mcp_tool_call' && correlated.rawInput
|
|
132
|
+
? attachLiveMcpInput(event, correlated.rawInput)
|
|
133
|
+
: event);
|
|
129
134
|
}
|
|
130
135
|
const assistantMessage = sanitizePersistenceValue(assistantChunks.join(''), redactionValues);
|
|
131
|
-
const rawTrace = JSON.stringify(
|
|
136
|
+
const rawTrace = JSON.stringify(sanitizeUntrustedPersistenceValue({
|
|
132
137
|
terminal: { stopReason: input.terminal.stopReason },
|
|
133
138
|
updates: trace,
|
|
134
139
|
}, redactionValues));
|
|
135
140
|
const traceOutput = rawTrace.length > ACP_AUTHOR_TRACE_MAX_CHARS
|
|
136
141
|
? `${rawTrace.slice(0, ACP_AUTHOR_TRACE_MAX_CHARS - 1)}…`
|
|
137
142
|
: rawTrace;
|
|
138
|
-
|
|
143
|
+
const result = sanitizePersistenceValue({
|
|
139
144
|
rawOutput: traceOutput,
|
|
140
145
|
traceOutput,
|
|
141
146
|
assistantMessage,
|
|
@@ -144,4 +149,12 @@ export function projectAcpAuthorTurn(input) {
|
|
|
144
149
|
exitCode: input.terminal.stopReason === 'end_turn' && !providerError ? 0 : 1,
|
|
145
150
|
toolEvents,
|
|
146
151
|
}, redactionValues);
|
|
152
|
+
result.toolEvents.forEach((event, index) => {
|
|
153
|
+
const originalInput = toolEvents[index] ? getOriginalMcpInput(toolEvents[index]) : undefined;
|
|
154
|
+
if (event.action === 'mcp_tool_call' && originalInput) {
|
|
155
|
+
attachLiveMcpInput(event, originalInput);
|
|
156
|
+
attachToolEventSensitiveValues(event, redactionValues);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return result;
|
|
147
160
|
}
|
package/dist/reporters/cli.js
CHANGED
|
@@ -19,7 +19,8 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
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
21
|
const meanReward = report.mean_reward ?? report.pass_rate ?? 0;
|
|
22
|
-
const isPass = report.
|
|
22
|
+
const isPass = report.run_runner_status === 'fail'
|
|
23
|
+
? false : report.status === undefined ? meanReward >= 0.5 : report.status === 'pass';
|
|
23
24
|
const trials = report.trials || [];
|
|
24
25
|
const avgDur = trials.reduce((s, t) => s + (t.duration_ms || 0), 0) / (trials.length || 1);
|
|
25
26
|
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);
|
|
@@ -43,6 +44,17 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
43
44
|
for (const [label, value] of metrics) {
|
|
44
45
|
console.log(` ${fmt.dim(label.padEnd(14))} ${fmt.bold(value)}`);
|
|
45
46
|
}
|
|
47
|
+
if (report.runner_status !== undefined || report.status !== undefined) {
|
|
48
|
+
console.log(` ${fmt.dim('Runner assertions'.padEnd(20))} ${gateLabel(report.runner_status ?? report.status)}`);
|
|
49
|
+
}
|
|
50
|
+
if (report.run_runner_status !== undefined) {
|
|
51
|
+
console.log(` ${fmt.dim('Run assertions'.padEnd(20))} ${gateLabel(report.run_runner_status)}`);
|
|
52
|
+
}
|
|
53
|
+
if (report.threshold != null) {
|
|
54
|
+
const thresholdStatus = report.threshold_status === 'pass' || report.threshold_status === 'fail' ? report.threshold_status : meanReward >= report.threshold ? 'pass' : 'fail';
|
|
55
|
+
console.log(` ${fmt.dim('Aggregate threshold'.padEnd(20))} ${gateLabel(thresholdStatus)} — ${(meanReward * 100).toFixed(1)}% ${thresholdStatus === 'pass' ? '>=' : '<'} ${(report.threshold * 100).toFixed(1)}%`);
|
|
56
|
+
}
|
|
57
|
+
console.log(` ${fmt.dim('Overall'.padEnd(20))} ${gateLabel(isPass ? 'pass' : 'fail')}`);
|
|
46
58
|
console.log();
|
|
47
59
|
// ── Trials
|
|
48
60
|
for (const trial of trials) {
|
|
@@ -85,6 +97,10 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
85
97
|
}
|
|
86
98
|
console.log();
|
|
87
99
|
}
|
|
100
|
+
if (trials.some((trial) => trial.scorer_results?.some(scorer => scorer.status === 'skipped'))) {
|
|
101
|
+
console.log(` ${fmt.dim('Skipped scorers keep their declared weight and contribute zero.')}`);
|
|
102
|
+
console.log();
|
|
103
|
+
}
|
|
88
104
|
// ── LLM scorer details
|
|
89
105
|
const hasLlm = trials.some((t) => t.scorer_results?.some((g) => g.scorer_type === 'llm_rubric'));
|
|
90
106
|
if (hasLlm) {
|
|
@@ -120,6 +136,9 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
120
136
|
console.log();
|
|
121
137
|
}
|
|
122
138
|
}
|
|
139
|
+
function gateLabel(status) {
|
|
140
|
+
return status === 'pass' ? fmt.pass('PASS') : fmt.fail('FAIL');
|
|
141
|
+
}
|
|
123
142
|
function formatPassAtK(value, reason) {
|
|
124
143
|
if (typeof value === 'number')
|
|
125
144
|
return `${(value * 100).toFixed(1)}% (legacy v1)`;
|
|
@@ -83,15 +83,22 @@ export function formatReportMarkdown(report, opts) {
|
|
|
83
83
|
}
|
|
84
84
|
if (report.threshold != null) {
|
|
85
85
|
lines.push('');
|
|
86
|
-
lines.push(
|
|
86
|
+
lines.push(`**Runner assertions:** ${(report.runner_status ?? report.status).toUpperCase()}`);
|
|
87
|
+
lines.push(`**Aggregate threshold:** ${(report.threshold_status ?? (meanReward >= report.threshold ? 'pass' : 'fail')).toUpperCase()} — ${pct(meanReward)} ${meanReward >= report.threshold ? '>=' : '<'} ${pct(report.threshold)}`);
|
|
88
|
+
lines.push(`**Overall:** ${report.status.toUpperCase()}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push(`**Runner assertions:** ${(report.runner_status ?? report.status).toUpperCase()}`);
|
|
93
|
+
lines.push(`**Overall:** ${report.status.toUpperCase()}`);
|
|
87
94
|
}
|
|
88
95
|
lines.push('');
|
|
89
|
-
lines.push('| Group | Mean reward | Success rate | pass@k | Skills | Avg duration |');
|
|
90
|
-
lines.push('
|
|
96
|
+
lines.push('| Group | Status | Mean reward | Success rate | pass@k | Skills | Avg duration |');
|
|
97
|
+
lines.push('|---|---|---|---|---|---|---|');
|
|
91
98
|
for (const group of report.groups) {
|
|
92
99
|
const skills = group.skills_used.length > 0 ? group.skills_used.join(', ') : '—';
|
|
93
100
|
const avg = computeAvgDuration(group.trials);
|
|
94
|
-
lines.push(`| ${escapeTableCell(group.task)} | ${pct(group.mean_reward ?? group.pass_rate ?? 0)} | ${group.success_rate === undefined ? '—' : pct(group.success_rate)} | ${escapeTableCell(formatPassAtK(group.pass_at_k, group.pass_at_k_unavailable_reason))} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
|
|
101
|
+
lines.push(`| ${escapeTableCell(group.task)} | ${(group.status ?? (group.pass_rate === 1 ? 'pass' : 'fail')).toUpperCase()} | ${pct(group.mean_reward ?? group.pass_rate ?? 0)} | ${group.success_rate === undefined ? '—' : pct(group.success_rate)} | ${escapeTableCell(formatPassAtK(group.pass_at_k, group.pass_at_k_unavailable_reason))} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
|
|
95
102
|
}
|
|
96
103
|
if (report.selection) {
|
|
97
104
|
lines.push('');
|
|
@@ -320,10 +327,14 @@ function formatGroupDetails(group) {
|
|
|
320
327
|
out.push(`reward: **${trial.reward === undefined ? 'n/a' : trial.reward.toFixed(2)}** | duration: ${durationSeconds(trial.duration_ms)} | completion: \`${reason}\``);
|
|
321
328
|
if (trial.scorer_results.length > 0) {
|
|
322
329
|
out.push('');
|
|
323
|
-
out.push('| Scorer | Score | Weight | Details |');
|
|
324
|
-
out.push('
|
|
330
|
+
out.push('| Scorer | Status | Score | Weight | Details |');
|
|
331
|
+
out.push('|---|---|---|---|---|');
|
|
325
332
|
for (const s of trial.scorer_results) {
|
|
326
|
-
out.push(`| ${escapeTableCell(s.scorer_type)} | ${s.score.toFixed(2)} | ${s.weight.toFixed(2)} | ${escapeTableCell(s.details ?? '')} |`);
|
|
333
|
+
out.push(`| ${escapeTableCell(s.scorer_type)} | ${(s.status ?? 'ok').toUpperCase()} | ${s.score.toFixed(2)} | ${s.weight.toFixed(2)} | ${escapeTableCell(s.details ?? '')} |`);
|
|
334
|
+
}
|
|
335
|
+
if (trial.scorer_results.some(scorer => scorer.status === 'skipped')) {
|
|
336
|
+
out.push('');
|
|
337
|
+
out.push('Skipped scorers keep their declared weight and contribute zero.');
|
|
327
338
|
}
|
|
328
339
|
}
|
|
329
340
|
const warnings = trial.diagnostics?.warnings ?? [];
|
|
@@ -3,6 +3,10 @@ export interface LoadedReport extends EvalReport {
|
|
|
3
3
|
file: string;
|
|
4
4
|
timestamp?: string;
|
|
5
5
|
status?: 'pass' | 'fail';
|
|
6
|
+
runner_status?: 'pass' | 'fail';
|
|
7
|
+
run_runner_status?: 'pass' | 'fail';
|
|
8
|
+
threshold_status?: 'pass' | 'fail' | 'not_configured';
|
|
9
|
+
threshold?: number;
|
|
6
10
|
}
|
|
7
11
|
export declare function loadReports(resultsDir: string, opts?: {
|
|
8
12
|
skipTraces?: boolean;
|
package/dist/reporters/loader.js
CHANGED
|
@@ -34,11 +34,18 @@ export async function loadReports(resultsDir, opts) {
|
|
|
34
34
|
if (raw.version !== 1 && raw.version !== 2)
|
|
35
35
|
continue;
|
|
36
36
|
for (const group of raw.groups) {
|
|
37
|
+
const runnerStatus = groupRunnerStatus(group);
|
|
38
|
+
const thresholdStatus = groupThresholdStatus(raw, group);
|
|
37
39
|
const report = {
|
|
38
40
|
file,
|
|
39
41
|
timestamp: raw.timestamp,
|
|
42
|
+
threshold: raw.threshold,
|
|
40
43
|
...group,
|
|
41
|
-
|
|
44
|
+
runner_status: runnerStatus,
|
|
45
|
+
...(raw.runner_status === 'pass' || raw.runner_status === 'fail'
|
|
46
|
+
? { run_runner_status: raw.runner_status } : {}),
|
|
47
|
+
threshold_status: thresholdStatus,
|
|
48
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
42
49
|
};
|
|
43
50
|
if (!opts?.skipTraces && group.trace_file) {
|
|
44
51
|
await hydrateTraces(report, group.trace_file, resolved);
|
|
@@ -50,15 +57,25 @@ export async function loadReports(resultsDir, opts) {
|
|
|
50
57
|
}
|
|
51
58
|
return results;
|
|
52
59
|
}
|
|
53
|
-
function
|
|
60
|
+
function groupRunnerStatus(group) {
|
|
61
|
+
if (group.runner_status === 'pass' || group.runner_status === 'fail')
|
|
62
|
+
return group.runner_status;
|
|
63
|
+
const outcomes = Array.isArray(group.trials)
|
|
64
|
+
? group.trials.map((trial) => trial.runner_outcome).filter((outcome) => outcome !== undefined)
|
|
65
|
+
: [];
|
|
66
|
+
if (outcomes.length > 0) {
|
|
67
|
+
return outcomes.every((outcome) => outcome === 'passed') ? 'pass' : 'fail';
|
|
68
|
+
}
|
|
54
69
|
if (group.status === 'pass' || group.status === 'fail')
|
|
55
70
|
return group.status;
|
|
56
71
|
const meanReward = group.mean_reward ?? group.pass_rate ?? 0;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
return meanReward === 1 ? 'pass' : 'fail';
|
|
73
|
+
}
|
|
74
|
+
function groupThresholdStatus(raw, group) {
|
|
75
|
+
if (group.threshold_status === 'pass' || group.threshold_status === 'fail'
|
|
76
|
+
|| group.threshold_status === 'not_configured')
|
|
77
|
+
return group.threshold_status;
|
|
78
|
+
if (typeof raw.threshold !== 'number')
|
|
79
|
+
return 'not_configured';
|
|
80
|
+
return (group.mean_reward ?? group.pass_rate ?? 0) >= raw.threshold ? 'pass' : 'fail';
|
|
64
81
|
}
|
package/dist/reporting/core.js
CHANGED
|
@@ -52,9 +52,12 @@ export function buildPathgradeReport(input) {
|
|
|
52
52
|
.map(attempt => attempt.score)
|
|
53
53
|
.filter((score) => score !== undefined))));
|
|
54
54
|
const overallMeanReward = average(scores);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const runnerStatus = (input.runStatus === undefined || input.runStatus === 'completed')
|
|
56
|
+
&& reportableGroups.every(group => group.cases.every(testCase => (testCase.state !== 'failed'
|
|
57
|
+
&& testCase.attempts.every(attempt => attempt.outcome.kind === 'passed')))) ? 'pass' : 'fail';
|
|
58
|
+
const thresholdStatus = input.threshold == null
|
|
59
|
+
? 'not_configured'
|
|
60
|
+
: overallMeanReward >= input.threshold ? 'pass' : 'fail';
|
|
58
61
|
return {
|
|
59
62
|
report: {
|
|
60
63
|
version: 2,
|
|
@@ -64,7 +67,9 @@ export function buildPathgradeReport(input) {
|
|
|
64
67
|
attempts_completed: attemptsCompleted,
|
|
65
68
|
overall_mean_reward: overallMeanReward,
|
|
66
69
|
overall_pass_rate: overallMeanReward,
|
|
67
|
-
|
|
70
|
+
runner_status: runnerStatus,
|
|
71
|
+
threshold_status: thresholdStatus,
|
|
72
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
68
73
|
groups: consolidatedGroups,
|
|
69
74
|
...(input.selection ? { selection: input.selection } : {}),
|
|
70
75
|
},
|
|
@@ -185,15 +190,20 @@ function buildEvalReport(groupName, cases, run) {
|
|
|
185
190
|
.filter((score) => score !== undefined);
|
|
186
191
|
const meanReward = average(rewards);
|
|
187
192
|
const metrics = binaryMetrics(cases, run);
|
|
193
|
+
const runnerStatus = cases.every(testCase => (testCase.state !== 'failed'
|
|
194
|
+
&& testCase.attempts.every(attempt => attempt.outcome.kind === 'passed'))) ? 'pass' : 'fail';
|
|
195
|
+
const thresholdStatus = run.threshold == null
|
|
196
|
+
? 'not_configured'
|
|
197
|
+
: meanReward >= run.threshold ? 'pass' : 'fail';
|
|
188
198
|
const skills = new Set();
|
|
189
199
|
for (const trial of trials)
|
|
190
200
|
for (const skill of trial.skills_used ?? [])
|
|
191
201
|
skills.add(skill);
|
|
192
202
|
return {
|
|
193
203
|
task: groupName,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
204
|
+
runner_status: runnerStatus,
|
|
205
|
+
threshold_status: thresholdStatus,
|
|
206
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
197
207
|
mean_reward: meanReward,
|
|
198
208
|
...(metrics.successRate !== undefined ? { success_rate: metrics.successRate } : {}),
|
|
199
209
|
...(metrics.passAtK ? {
|
|
@@ -6,18 +6,44 @@ export function parsePathgradeReport(value) {
|
|
|
6
6
|
|| (value.version === 2 && (typeof value.overall_mean_reward !== 'number' || !validAttemptCounts(value)))
|
|
7
7
|
|| !optionalNumber(value.threshold)
|
|
8
8
|
|| (value.status !== 'pass' && value.status !== 'fail')
|
|
9
|
+
|| !optionalGateStatus(value.runner_status)
|
|
10
|
+
|| !optionalThresholdStatus(value.threshold_status)
|
|
9
11
|
|| (value.run_kind !== undefined && value.run_kind !== 'evaluation' && value.run_kind !== 'no-affected')
|
|
10
12
|
|| !Array.isArray(value.groups)
|
|
11
13
|
|| !value.groups.every(group => isReportGroup(group, value.version))
|
|
12
14
|
|| !isSelection(value.selection)) {
|
|
13
15
|
throw new Error('PathGrade results.json is missing or has an unsupported schema');
|
|
14
16
|
}
|
|
15
|
-
|
|
17
|
+
const threshold = value.threshold;
|
|
18
|
+
const groups = value.groups.map(group => {
|
|
19
|
+
const runnerStatus = deriveRunnerStatus(group);
|
|
20
|
+
const thresholdStatus = deriveThresholdStatus(group.mean_reward ?? group.pass_rate, threshold);
|
|
21
|
+
return {
|
|
22
|
+
...group,
|
|
23
|
+
runner_status: runnerStatus,
|
|
24
|
+
threshold_status: thresholdStatus,
|
|
25
|
+
status: composeStatus(runnerStatus, thresholdStatus),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
const legacyFailure = value.runner_status === undefined && value.status === 'fail' && groups.length === 0;
|
|
29
|
+
const runnerStatus = value.runner_status !== 'fail' && !legacyFailure
|
|
30
|
+
&& (groups.length > 0 || value.runner_status === 'pass' || value.status === 'pass')
|
|
31
|
+
&& groups.every(group => group.runner_status === 'pass') ? 'pass' : 'fail';
|
|
32
|
+
const thresholdStatus = deriveThresholdStatus(value.overall_mean_reward ?? value.overall_pass_rate, threshold);
|
|
33
|
+
return {
|
|
34
|
+
...value,
|
|
35
|
+
groups,
|
|
36
|
+
runner_status: runnerStatus,
|
|
37
|
+
threshold_status: thresholdStatus,
|
|
38
|
+
status: composeStatus(runnerStatus, thresholdStatus),
|
|
39
|
+
};
|
|
16
40
|
}
|
|
17
41
|
function isReportGroup(value, version) {
|
|
18
42
|
return isRecord(value)
|
|
19
43
|
&& typeof value.task === 'string'
|
|
20
44
|
&& (version === 1 ? legacyMetrics(value) : schemaV2Metrics(value))
|
|
45
|
+
&& optionalGateStatus(value.runner_status)
|
|
46
|
+
&& optionalThresholdStatus(value.threshold_status)
|
|
21
47
|
&& optionalString(value.source_file)
|
|
22
48
|
&& typeof value.trace_file === 'string'
|
|
23
49
|
&& Array.isArray(value.skills_used)
|
|
@@ -26,6 +52,34 @@ function isReportGroup(value, version) {
|
|
|
26
52
|
&& value.trials.every(isTrial)
|
|
27
53
|
&& (value.comparison_contract === undefined || isComparisonContract(value.comparison_contract));
|
|
28
54
|
}
|
|
55
|
+
function deriveRunnerStatus(value) {
|
|
56
|
+
if (value.runner_status === 'fail')
|
|
57
|
+
return 'fail';
|
|
58
|
+
const outcomes = value.trials
|
|
59
|
+
.map(trial => trial.runner_outcome)
|
|
60
|
+
.filter(outcome => outcome !== undefined);
|
|
61
|
+
if (outcomes.length > 0)
|
|
62
|
+
return outcomes.every(outcome => outcome === 'passed') ? 'pass' : 'fail';
|
|
63
|
+
if (value.runner_status === 'pass')
|
|
64
|
+
return value.runner_status;
|
|
65
|
+
if (value.status === 'pass' || value.status === 'fail')
|
|
66
|
+
return value.status;
|
|
67
|
+
return value.pass_rate === 1 ? 'pass' : 'fail';
|
|
68
|
+
}
|
|
69
|
+
function deriveThresholdStatus(reward, threshold) {
|
|
70
|
+
if (threshold === undefined)
|
|
71
|
+
return 'not_configured';
|
|
72
|
+
return typeof reward === 'number' && reward >= threshold ? 'pass' : 'fail';
|
|
73
|
+
}
|
|
74
|
+
function composeStatus(runnerStatus, thresholdStatus) {
|
|
75
|
+
return runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass';
|
|
76
|
+
}
|
|
77
|
+
function optionalGateStatus(value) {
|
|
78
|
+
return value === undefined || value === 'pass' || value === 'fail';
|
|
79
|
+
}
|
|
80
|
+
function optionalThresholdStatus(value) {
|
|
81
|
+
return optionalGateStatus(value) || value === 'not_configured';
|
|
82
|
+
}
|
|
29
83
|
function isTrial(value) {
|
|
30
84
|
return isRecord(value)
|
|
31
85
|
&& typeof value.trial_id === 'number'
|
|
@@ -40,7 +94,8 @@ function isTrial(value) {
|
|
|
40
94
|
&& value.scorer_results.every(scorer => isRecord(scorer)
|
|
41
95
|
&& typeof scorer.scorer_type === 'string'
|
|
42
96
|
&& typeof scorer.score === 'number'
|
|
43
|
-
&& typeof scorer.weight === 'number'
|
|
97
|
+
&& typeof scorer.weight === 'number'
|
|
98
|
+
&& (scorer.status === undefined || scorer.status === 'ok' || scorer.status === 'error' || scorer.status === 'skipped'));
|
|
44
99
|
}
|
|
45
100
|
function legacyMetrics(value) {
|
|
46
101
|
return typeof value.pass_rate === 'number'
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { DiagnosticsReport } from '../sdk/diagnostics.js';
|
|
2
2
|
import type { AgentExecutionMetadata, EvaluationResultKind } from '../sdk/types.js';
|
|
3
|
-
import type { AttemptOutcome } from '../runners/model.js';
|
|
3
|
+
import type { AttemptOutcome, RunStatus } from '../runners/model.js';
|
|
4
4
|
import type { PathgradeReport, PathgradeSelectionReport, TrialResult } from '../types.js';
|
|
5
5
|
export type ReportCaseState = 'passed' | 'failed' | 'skipped' | 'pending';
|
|
6
6
|
export interface ReportRunInput {
|
|
7
|
+
runStatus?: RunStatus;
|
|
7
8
|
threshold?: number;
|
|
8
9
|
selection?: PathgradeSelectionReport;
|
|
9
10
|
attemptsRequested?: number;
|
|
@@ -43,8 +43,8 @@ export async function runWithAdapter(input) {
|
|
|
43
43
|
if (loadedSelection) {
|
|
44
44
|
built = buildPathgradeReport({
|
|
45
45
|
threshold: options.threshold,
|
|
46
|
+
...reportInput,
|
|
46
47
|
selection: loadedSelection,
|
|
47
|
-
groups: reportInput.groups,
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
const mode = options.reporterMode ?? 'cli';
|
|
@@ -56,14 +56,13 @@ export async function runWithAdapter(input) {
|
|
|
56
56
|
if (mode === 'browser') {
|
|
57
57
|
await options.openBrowser?.();
|
|
58
58
|
}
|
|
59
|
-
if (options.threshold != null && built.report.
|
|
59
|
+
if (options.threshold != null && built.report.threshold_status === 'fail') {
|
|
60
60
|
options.onThresholdFailure?.({
|
|
61
61
|
overallPassRate: built.report.overall_pass_rate,
|
|
62
62
|
threshold: options.threshold,
|
|
63
63
|
});
|
|
64
|
-
return runExitCode === 0 ? 1 : runExitCode;
|
|
65
64
|
}
|
|
66
|
-
return runExitCode;
|
|
65
|
+
return built.report.status === 'fail' && runExitCode === 0 ? 1 : runExitCode;
|
|
67
66
|
}
|
|
68
67
|
finally {
|
|
69
68
|
await lifecycle.cleanupRun();
|
|
@@ -80,13 +80,10 @@ export function withRepeatedAttempts(input) {
|
|
|
80
80
|
if (input.config.reporter === 'browser') {
|
|
81
81
|
await (input.openBrowser ?? openBrowserViewer)();
|
|
82
82
|
}
|
|
83
|
-
if (input.config.ci.threshold != null && built.report.
|
|
83
|
+
if (input.config.ci.threshold != null && built.report.threshold_status === 'fail') {
|
|
84
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
85
|
}
|
|
86
|
+
resultCode = firstNonzeroExit || (built.report.status === 'fail' ? 1 : 0);
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
89
|
catch (error) {
|
|
@@ -41,6 +41,7 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
|
|
|
41
41
|
}
|
|
42
42
|
return {
|
|
43
43
|
...(options.selection ? { selection: options.selection } : {}),
|
|
44
|
+
runStatus: snapshot.model.run.status,
|
|
44
45
|
attemptsRequested: snapshot.model.run.attemptsRequested ?? 1,
|
|
45
46
|
attemptsCompleted: snapshot.model.run.attemptsCompleted ?? 1,
|
|
46
47
|
groups: [...groups.values()],
|