@wix/pathgrade 1.0.38 → 1.0.40
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 +5 -5
- package/dist/adapters/jest/results.d.ts +1 -0
- package/dist/adapters/jest/results.js +36 -14
- package/dist/adapters/jest/runner-adapter.d.ts +1 -0
- package/dist/adapters/jest/runner-adapter.js +5 -1
- package/dist/adapters/node-test/runner-adapter.js +6 -5
- package/dist/affected/meta.d.ts +4 -1
- package/dist/affected/meta.js +9 -5
- package/dist/affected/select.js +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 -26
- 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/comparison-contract.d.ts +3 -2
- package/dist/reporting/comparison-contract.js +66 -33
- package/dist/reporting/core.js +35 -8
- package/dist/reporting/reliability-contract.d.ts +1 -0
- package/dist/reporting/report-parser.js +125 -5
- package/dist/reporting/source-metadata.d.ts +3 -0
- package/dist/reporting/source-metadata.js +67 -2
- package/dist/reporting/types.d.ts +5 -1
- package/dist/runners/model-builders.d.ts +1 -0
- package/dist/runners/model-builders.js +36 -12
- package/dist/runners/model.d.ts +2 -0
- package/dist/runners/orchestrator.js +3 -4
- package/dist/runners/repeated-invocation.js +2 -5
- package/dist/runners/report-projection.js +84 -0
- package/dist/runners/vitest-adapter.js +6 -2
- 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/scorers.d.ts +5 -0
- package/dist/sdk/scorers.js +4 -0
- 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 +14 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ Use `check()` for binary requirements, `score()` for partial credit, `judge()` f
|
|
|
178
178
|
check('tests-pass', async ({ runCommand }) => {
|
|
179
179
|
const { exitCode } = await runCommand('npm test');
|
|
180
180
|
return exitCode === 0;
|
|
181
|
-
});
|
|
181
|
+
}, { revision: { contract: 1 } });
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
### `score()` - Partial credit
|
|
@@ -187,7 +187,7 @@ check('tests-pass', async ({ runCommand }) => {
|
|
|
187
187
|
score('coverage', async ({ runCommand }) => {
|
|
188
188
|
const { stdout } = await runCommand('npx coverage-summary');
|
|
189
189
|
return parseFloat(stdout) / 100;
|
|
190
|
-
});
|
|
190
|
+
}, { revision: { contract: 1 } });
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
### `judge()` - Rubric evaluation
|
|
@@ -199,6 +199,7 @@ Was the fix minimal and correct? (0-0.5)`,
|
|
|
199
199
|
});
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
Function-bearing `check()` and `score()` scorers require explicit canonical-JSON `revision` data for authoritative baseline comparisons. A `judge()` with a function-valued `input` requires the same declaration. Imported scorer helpers belong in `comparisonInputs`; closed-over values belong in `revision`.
|
|
202
203
|
Judge scorers also support:
|
|
203
204
|
|
|
204
205
|
- `retry` for transient judge failures
|
|
@@ -440,9 +441,8 @@ Useful details:
|
|
|
440
441
|
- `pathgrade preview browser` starts a local viewer on `http://localhost:3847`.
|
|
441
442
|
- `pathgrade report` posts or updates a PR comment in GitHub Actions; locally it prints the markdown report and then the numeric pass rate. Provider orchestrators can add a `--details-url`, suppress stale updates with `--expected-head-sha`, and opt into surfaced API failures with `--strict`.
|
|
442
443
|
- `pathgrade validate --affected` is a strict mode for CI: every discovered eval must either live under a `SKILL.md` anchor or export valid `__pathgradeMeta`.
|
|
443
|
-
|
|
444
|
+
Declare methodology files separately from affected-selection dependencies with `__pathgradeMeta.comparisonInputs`, for example `comparisonInputs: ['evals/fixtures/**', 'evals/scorers/**']`. An empty array explicitly declares a self-contained eval. Pathgrade hashes the raw eval source plus each declared glob, matched repository-relative path, and raw file bytes; `.git` internals and `.pathgrade` outputs are excluded. Missing, invalid, unmatched, escaping, or unreadable inputs suppress numeric deltas. `deps`, `extraDeps`, inferred skill roots, and global triggers affect selection only and never definition identity.
|
|
444
445
|
Run `pathgrade --help` for the full help text.
|
|
445
|
-
|
|
446
446
|
## Configuration
|
|
447
447
|
|
|
448
448
|
```typescript
|
|
@@ -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
|
|
464
|
+
New reports use schema version 3. They add a versioned task inventory with every selected eval file, collection completeness, stable task keys, and bounded non-scoring reasons. They also preserve every attempt with `case_id`, `attempt_id`, and `attempt_index`; include the display-safe `runner_outcome`; publish `mean_reward`; 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, native runner references, and full flow traces remain transient or in trace artifacts rather than being copied into consolidated public reports. A partial reward or incomplete attempt makes pass@k explicitly unavailable. Report versions 1 and 2 remain readable, but they cannot be promoted as authoritative task-accounted baselines and must be rerun.
|
|
465
465
|
|
|
466
466
|
Third-party runner adapters are supported through `@wix/pathgrade/adapter-kit`. Adapter names resolve as follows:
|
|
467
467
|
|
|
@@ -23,6 +23,7 @@ export declare function normalizeJestRunResults(input: {
|
|
|
23
23
|
run: AdapterRunHandle;
|
|
24
24
|
results: JestAggregatedResult;
|
|
25
25
|
metadataByCaseId?: Map<string, PathgradeTestMeta[]>;
|
|
26
|
+
discoveredFiles?: readonly string[];
|
|
26
27
|
cwd?: string;
|
|
27
28
|
}): NormalizedRunSnapshot;
|
|
28
29
|
export declare function jestCaseId(input: {
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSourceMetadataResolver } from '../../reporting/source-metadata.js';
|
|
2
2
|
export function normalizeJestRunResults(input) {
|
|
3
3
|
const runId = `${input.run.adapterName}:run`;
|
|
4
|
+
const cwd = input.cwd ?? process.cwd();
|
|
5
|
+
const resolveSourceMetadata = createSourceMetadataResolver(cwd);
|
|
4
6
|
const files = input.results.testResults ?? [];
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
...
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
const collection = input.run.status === 'completed'
|
|
8
|
+
? { state: 'complete' }
|
|
9
|
+
: { state: 'incomplete', reason: 'run-incomplete' };
|
|
10
|
+
const units = [
|
|
11
|
+
...files.map((fileResult, index) => ({
|
|
12
|
+
id: unitId(index),
|
|
13
|
+
runId,
|
|
14
|
+
displayName: fileResult.testFilePath,
|
|
15
|
+
collection,
|
|
16
|
+
...resolveSourceMetadata(fileResult.testFilePath),
|
|
17
|
+
groupingHints: [{
|
|
18
|
+
kind: 'source',
|
|
19
|
+
key: fileResult.testFilePath,
|
|
20
|
+
label: fileResult.testFilePath,
|
|
21
|
+
order: index,
|
|
22
|
+
}],
|
|
23
|
+
nativeReferences: [{ kind: 'jest-file', id: fileResult.testFilePath }],
|
|
24
|
+
})),
|
|
25
|
+
...(input.discoveredFiles ?? [])
|
|
26
|
+
.filter(file => !files.some(result => normalizePath(result.testFilePath, resolveSourceMetadata) === file))
|
|
27
|
+
.map((file, index) => ({
|
|
28
|
+
id: unitId(files.length + index),
|
|
29
|
+
runId,
|
|
30
|
+
displayName: file,
|
|
31
|
+
collection,
|
|
32
|
+
...resolveSourceMetadata(file),
|
|
33
|
+
groupingHints: [{ kind: 'source', key: file, label: file, order: files.length + index }],
|
|
34
|
+
nativeReferences: [{ kind: 'jest-file', id: file }],
|
|
35
|
+
})),
|
|
36
|
+
];
|
|
18
37
|
const occurrenceCounts = new Map();
|
|
19
38
|
return {
|
|
20
39
|
version: 1,
|
|
@@ -45,6 +64,9 @@ export function normalizeJestRunResults(input) {
|
|
|
45
64
|
},
|
|
46
65
|
};
|
|
47
66
|
}
|
|
67
|
+
function normalizePath(file, resolveSourceMetadata) {
|
|
68
|
+
return resolveSourceMetadata(file).sourceFile ?? file.replaceAll('\\', '/');
|
|
69
|
+
}
|
|
48
70
|
function assertionsForFile(fileResult) {
|
|
49
71
|
return fileResult.assertionResults ?? fileResult.testResults ?? [];
|
|
50
72
|
}
|
|
@@ -3,6 +3,7 @@ import { type PathgradeTestMeta, RunnerAdapter } from '@wix/pathgrade/adapter-ki
|
|
|
3
3
|
interface JestRunNative {
|
|
4
4
|
results?: JestAggregatedResult;
|
|
5
5
|
metadataByCaseId?: Map<string, PathgradeTestMeta[]>;
|
|
6
|
+
discoveredFiles?: string[];
|
|
6
7
|
}
|
|
7
8
|
export declare function createJestAdapter(options?: JestRunNative): RunnerAdapter;
|
|
8
9
|
export declare function createPathgradeAdapter(): RunnerAdapter;
|
|
@@ -27,7 +27,10 @@ export function createJestAdapter(options = {}) {
|
|
|
27
27
|
adapterName: this.name,
|
|
28
28
|
status: input.signal?.aborted ? 'cancelled' : 'completed',
|
|
29
29
|
exitCode: input.signal?.aborted ? 1 : 0,
|
|
30
|
-
native:
|
|
30
|
+
native: {
|
|
31
|
+
...options,
|
|
32
|
+
discoveredFiles: input.discovered.units.flatMap(unit => unit.sourceRef ? [unit.sourceRef] : []),
|
|
33
|
+
},
|
|
31
34
|
};
|
|
32
35
|
},
|
|
33
36
|
async collectNormalizedRunSnapshot(run) {
|
|
@@ -36,6 +39,7 @@ export function createJestAdapter(options = {}) {
|
|
|
36
39
|
run,
|
|
37
40
|
results: native.results ?? { testResults: [] },
|
|
38
41
|
metadataByCaseId: native.metadataByCaseId,
|
|
42
|
+
discoveredFiles: native.discoveredFiles,
|
|
39
43
|
cwd: discoveryCwd,
|
|
40
44
|
});
|
|
41
45
|
},
|
|
@@ -37,7 +37,7 @@ export function createNodeTestAdapter() {
|
|
|
37
37
|
adapterName: this.name,
|
|
38
38
|
status: 'completed',
|
|
39
39
|
exitCode: 0,
|
|
40
|
-
native: { resultsPath, resultsDir, cwd },
|
|
40
|
+
native: { resultsPath, resultsDir, cwd, discoveredFiles: files },
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
const exitCode = await spawnNodeTest({
|
|
@@ -54,7 +54,7 @@ export function createNodeTestAdapter() {
|
|
|
54
54
|
adapterName: this.name,
|
|
55
55
|
status: input.signal?.aborted ? 'cancelled' : (exitCode === 0 ? 'completed' : 'failed'),
|
|
56
56
|
exitCode,
|
|
57
|
-
native: { resultsPath, resultsDir, cwd },
|
|
57
|
+
native: { resultsPath, resultsDir, cwd, discoveredFiles: files },
|
|
58
58
|
};
|
|
59
59
|
},
|
|
60
60
|
async collectNormalizedRunSnapshot(run) {
|
|
@@ -71,7 +71,7 @@ export function createNodeTestAdapter() {
|
|
|
71
71
|
return buildNormalizedRunSnapshotFromReportGroups(run, Array.from(groupMap.entries()).map(([groupName, groupedCases]) => ({
|
|
72
72
|
groupName,
|
|
73
73
|
cases: groupedCases,
|
|
74
|
-
})), { cwd: native.cwd });
|
|
74
|
+
})), { cwd: native.cwd, discoveredFiles: native.discoveredFiles });
|
|
75
75
|
}
|
|
76
76
|
finally {
|
|
77
77
|
if (native.resultsDir)
|
|
@@ -104,10 +104,11 @@ function readNodeTestRunNative(run) {
|
|
|
104
104
|
&& run.native !== null
|
|
105
105
|
&& typeof run.native.resultsPath === 'string'
|
|
106
106
|
&& typeof run.native.resultsDir === 'string'
|
|
107
|
-
&& typeof run.native.cwd === 'string'
|
|
107
|
+
&& typeof run.native.cwd === 'string'
|
|
108
|
+
&& Array.isArray(run.native.discoveredFiles)) {
|
|
108
109
|
return run.native;
|
|
109
110
|
}
|
|
110
|
-
return { resultsPath: '', resultsDir: '', cwd: process.cwd() };
|
|
111
|
+
return { resultsPath: '', resultsDir: '', cwd: process.cwd(), discoveredFiles: [] };
|
|
111
112
|
}
|
|
112
113
|
async function readCases(resultsPath) {
|
|
113
114
|
if (!resultsPath || !(await fs.pathExists(resultsPath)))
|
package/dist/affected/meta.d.ts
CHANGED
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
export interface ParsedMeta {
|
|
18
18
|
deps?: string[];
|
|
19
19
|
extraDeps?: string[];
|
|
20
|
+
comparisonInputs?: string[];
|
|
20
21
|
alwaysRun?: boolean;
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
24
|
* Parse `__pathgradeMeta` from an eval file's AST. Returns `null` if the
|
|
24
25
|
* export is not present.
|
|
25
26
|
*/
|
|
26
|
-
export declare function parsePathgradeMeta(evalFile: string
|
|
27
|
+
export declare function parsePathgradeMeta(evalFile: string, options?: {
|
|
28
|
+
validateComparisonInputs?: boolean;
|
|
29
|
+
}): ParsedMeta | null;
|
package/dist/affected/meta.js
CHANGED
|
@@ -21,14 +21,14 @@ import picomatch from 'picomatch';
|
|
|
21
21
|
* Parse `__pathgradeMeta` from an eval file's AST. Returns `null` if the
|
|
22
22
|
* export is not present.
|
|
23
23
|
*/
|
|
24
|
-
export function parsePathgradeMeta(evalFile) {
|
|
24
|
+
export function parsePathgradeMeta(evalFile, options = {}) {
|
|
25
25
|
const source = fs.readFileSync(evalFile, 'utf-8');
|
|
26
26
|
const sourceFile = ts.createSourceFile(evalFile, source, ts.ScriptTarget.Latest,
|
|
27
27
|
/* setParentNodes */ true, ts.ScriptKind.TS);
|
|
28
28
|
for (const stmt of sourceFile.statements) {
|
|
29
29
|
const initializer = findMetaInitializer(stmt);
|
|
30
30
|
if (initializer) {
|
|
31
|
-
return extractMeta(initializer, evalFile);
|
|
31
|
+
return extractMeta(initializer, evalFile, options);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
return null;
|
|
@@ -50,7 +50,7 @@ function findMetaInitializer(stmt) {
|
|
|
50
50
|
}
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
|
-
function extractMeta(expr, evalFile) {
|
|
53
|
+
function extractMeta(expr, evalFile, options) {
|
|
54
54
|
// Optionally unwrap a type assertion: `{...} as PathgradeMeta` / `<PathgradeMeta>{...}`.
|
|
55
55
|
let node = expr;
|
|
56
56
|
while (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
@@ -64,7 +64,9 @@ function extractMeta(expr, evalFile) {
|
|
|
64
64
|
if (!ts.isPropertyAssignment(prop))
|
|
65
65
|
continue;
|
|
66
66
|
const name = propertyKeyName(prop.name);
|
|
67
|
-
if (name === 'deps' || name === 'extraDeps') {
|
|
67
|
+
if (name === 'deps' || name === 'extraDeps' || name === 'comparisonInputs') {
|
|
68
|
+
if (name === 'comparisonInputs' && options.validateComparisonInputs === false)
|
|
69
|
+
continue;
|
|
68
70
|
const globs = extractStringArray(prop.initializer, evalFile, name);
|
|
69
71
|
for (const g of globs)
|
|
70
72
|
validateGlob(g, evalFile, name);
|
|
@@ -89,7 +91,9 @@ function validateGlob(glob, evalFile, field) {
|
|
|
89
91
|
if (glob.length === 0) {
|
|
90
92
|
throw new Error(`${evalFile}: __pathgradeMeta.${field} contains an empty glob.`);
|
|
91
93
|
}
|
|
92
|
-
|
|
94
|
+
const normalized = glob.replaceAll('\\', '/');
|
|
95
|
+
if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)
|
|
96
|
+
|| normalized.startsWith('../') || normalized.includes('/../') || normalized === '..') {
|
|
93
97
|
throw new Error(`${evalFile}: __pathgradeMeta.${field} entry "${glob}" escapes the repo root — ` +
|
|
94
98
|
`globs must be repo-root-relative.`);
|
|
95
99
|
}
|
package/dist/affected/select.js
CHANGED
|
@@ -44,7 +44,7 @@ export function selectAffected(input) {
|
|
|
44
44
|
for (const evalFile of evalFiles) {
|
|
45
45
|
const absEval = path.resolve(repoRoot, evalFile);
|
|
46
46
|
const skillRoot = findSkillRoot(absEval, repoRoot);
|
|
47
|
-
const meta = parsePathgradeMeta(absEval);
|
|
47
|
+
const meta = parsePathgradeMeta(absEval, { validateComparisonInputs: false });
|
|
48
48
|
// Precedence #2: alwaysRun wins over dep matching.
|
|
49
49
|
if (meta?.alwaysRun === true) {
|
|
50
50
|
selected.push({ file: evalFile, reason: 'always-run' });
|
|
@@ -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,16 @@ 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
|
-
|
|
70
|
-
|
|
47
|
+
try {
|
|
48
|
+
return parsePathgradeReport(await fs.readJSON(resolvedPath));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
71
51
|
throw new Error(`results file at ${resolvedPath} is not a valid pathgrade report`);
|
|
72
52
|
}
|
|
73
|
-
return raw;
|
|
74
53
|
}
|
|
75
54
|
function printMarkdownAndPassRate(markdown, passRate) {
|
|
76
55
|
// 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;
|