@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.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/dist/adapters/jest/results.d.ts +1 -0
  3. package/dist/adapters/jest/results.js +36 -14
  4. package/dist/adapters/jest/runner-adapter.d.ts +1 -0
  5. package/dist/adapters/jest/runner-adapter.js +5 -1
  6. package/dist/adapters/node-test/runner-adapter.js +6 -5
  7. package/dist/affected/meta.d.ts +4 -1
  8. package/dist/affected/meta.js +9 -5
  9. package/dist/affected/select.js +1 -1
  10. package/dist/agents/claude/sdk-message-projector.js +4 -5
  11. package/dist/agents/claude/tool-permission-bridge.js +2 -1
  12. package/dist/agents/codex-app-server/item-projection.js +2 -2
  13. package/dist/agents/codex-app-server/mcp-approval-correlator.js +3 -2
  14. package/dist/agents/opencode.js +4 -5
  15. package/dist/commands/report.js +5 -26
  16. package/dist/internal/direct-mcp-v2/acp-author-projector.js +20 -7
  17. package/dist/reporters/cli.js +20 -1
  18. package/dist/reporters/github-comment.js +18 -7
  19. package/dist/reporters/loader.d.ts +4 -0
  20. package/dist/reporters/loader.js +26 -9
  21. package/dist/reporting/comparison-contract.d.ts +3 -2
  22. package/dist/reporting/comparison-contract.js +66 -33
  23. package/dist/reporting/core.js +35 -8
  24. package/dist/reporting/reliability-contract.d.ts +1 -0
  25. package/dist/reporting/report-parser.js +125 -5
  26. package/dist/reporting/source-metadata.d.ts +3 -0
  27. package/dist/reporting/source-metadata.js +67 -2
  28. package/dist/reporting/types.d.ts +5 -1
  29. package/dist/runners/model-builders.d.ts +1 -0
  30. package/dist/runners/model-builders.js +36 -12
  31. package/dist/runners/model.d.ts +2 -0
  32. package/dist/runners/orchestrator.js +3 -4
  33. package/dist/runners/repeated-invocation.js +2 -5
  34. package/dist/runners/report-projection.js +84 -0
  35. package/dist/runners/vitest-adapter.js +6 -2
  36. package/dist/sdk/agent.js +10 -1
  37. package/dist/sdk/evaluate.js +58 -3
  38. package/dist/sdk/judge-prompt-builder.js +11 -7
  39. package/dist/sdk/mcp-event-input.d.ts +1 -0
  40. package/dist/sdk/mcp-event-input.js +3 -0
  41. package/dist/sdk/mcp-evidence.js +16 -3
  42. package/dist/sdk/mcp-safety.js +2 -2
  43. package/dist/sdk/scorers.d.ts +5 -0
  44. package/dist/sdk/scorers.js +4 -0
  45. package/dist/sdk/scripted-mcp-events.js +3 -2
  46. package/dist/sdk/tool-event-log.js +18 -3
  47. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  48. package/dist/sdk/tool-event-secrets.js +33 -2
  49. package/dist/sdk/types.d.ts +2 -0
  50. package/dist/tool-event-results.d.ts +3 -0
  51. package/dist/tool-event-results.js +153 -23
  52. package/dist/types.d.ts +14 -7
  53. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
- import { resolveSourceMetadata } from '../reporting/source-metadata.js';
1
+ import { createSourceMetadataResolver } from '../reporting/source-metadata.js';
2
2
  export function buildNormalizedRunSnapshotFromReportGroups(run, groups, options = {}) {
3
3
  const cwd = options.cwd ?? process.cwd();
4
+ const resolveSourceMetadata = createSourceMetadataResolver(cwd);
4
5
  return {
5
6
  version: 1,
6
7
  completeness: 'final',
@@ -14,18 +15,41 @@ export function buildNormalizedRunSnapshotFromReportGroups(run, groups, options
14
15
  message: diagnostic.message,
15
16
  })) } : {}),
16
17
  },
17
- units: groups.map((group, index) => {
18
- const source = group.sourceFile && group.sourceRevision
19
- ? { sourceFile: group.sourceFile, sourceRevision: group.sourceRevision }
20
- : resolveSourceMetadata(group.sourceFile ?? group.cases.map(testCase => testCase.filePath ?? testCase.sourceRef).find(Boolean), cwd);
21
- return {
22
- id: unitId(group.groupName, index),
18
+ units: [
19
+ ...groups.map((group, index) => {
20
+ const source = group.sourceFile && group.sourceRevision
21
+ ? {
22
+ sourceFile: group.sourceFile,
23
+ sourceRevision: group.sourceRevision,
24
+ ...(group.comparisonInputs ? { comparisonInputs: group.comparisonInputs } : {}),
25
+ }
26
+ : resolveSourceMetadata(group.sourceFile ?? group.cases.map(testCase => testCase.filePath ?? testCase.sourceRef).find(Boolean));
27
+ return {
28
+ id: unitId(group.groupName, index),
29
+ runId: `${run.adapterName}:run`,
30
+ displayName: group.groupName,
31
+ collection: run.status === 'completed' || run.status === 'failed'
32
+ ? { state: 'complete' }
33
+ : { state: 'incomplete', reason: 'run-incomplete' },
34
+ ...source,
35
+ groupingHints: [{ kind: 'suite', key: group.groupName, label: group.groupName, order: index }],
36
+ };
37
+ }),
38
+ ...(options.discoveredFiles ?? [])
39
+ .filter(file => !groups.some(group => group.sourceFile === file
40
+ || group.cases.some(testCase => testCase.filePath === file || testCase.sourceRef === file)))
41
+ .map((file, index) => ({
42
+ id: unitId(file, groups.length + index),
23
43
  runId: `${run.adapterName}:run`,
24
- displayName: group.groupName,
25
- ...source,
26
- groupingHints: [{ kind: 'suite', key: group.groupName, label: group.groupName, order: index }],
27
- };
28
- }),
44
+ displayName: file,
45
+ collection: {
46
+ state: 'incomplete',
47
+ reason: 'adapter-cannot-prove-completeness',
48
+ },
49
+ ...resolveSourceMetadata(file),
50
+ groupingHints: [{ kind: 'source', key: file, label: file, order: groups.length + index }],
51
+ })),
52
+ ],
29
53
  cases: groups.flatMap((group, groupIndex) => group.cases.map((testCase, caseIndex) => {
30
54
  const caseId = testCase.caseId ?? `${unitId(group.groupName, groupIndex)}:case-${caseIndex + 1}`;
31
55
  const attemptId = `${caseId}:attempt-1`;
@@ -1,6 +1,7 @@
1
1
  import type { DiagnosticsReport } from '../sdk/diagnostics.js';
2
2
  import type { TrialResult } from '../types.js';
3
3
  import type { CollectionIncompleteReason } from '../reporting/reliability-contract.js';
4
+ import type { ComparisonInputsState } from '../reporting/reliability-contract.js';
4
5
  export interface NormalizedRunSnapshot {
5
6
  version: 1;
6
7
  completeness: SnapshotCompleteness;
@@ -29,6 +30,7 @@ export interface EvalUnitRecord {
29
30
  collection?: EvalUnitCollection;
30
31
  sourceFile?: string;
31
32
  sourceRevision?: string;
33
+ comparisonInputs?: ComparisonInputsState;
32
34
  diagnostics?: Diagnostic[];
33
35
  nativeReferences?: NativeReference[];
34
36
  groupingHints?: GroupingHint[];
@@ -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.status === 'fail') {
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.status === 'fail') {
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) {
@@ -1,3 +1,4 @@
1
+ import { TASK_INVENTORY_VERSION } from '../reporting/reliability-contract.js';
1
2
  import { validateNormalizedRunSnapshot } from './model-validation.js';
2
3
  export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}) {
3
4
  const validation = validateNormalizedRunSnapshot(snapshot, { completeness: 'final' });
@@ -20,6 +21,7 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
20
21
  groupName: key,
21
22
  ...(unit?.sourceFile ? { sourceFile: unit.sourceFile } : {}),
22
23
  ...(unit?.sourceRevision ? { sourceRevision: unit.sourceRevision } : {}),
24
+ ...(unit?.comparisonInputs ? { comparisonInputs: unit.comparisonInputs } : {}),
23
25
  cases: [],
24
26
  };
25
27
  group.cases.push({
@@ -41,11 +43,93 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
41
43
  }
42
44
  return {
43
45
  ...(options.selection ? { selection: options.selection } : {}),
46
+ runStatus: snapshot.model.run.status,
44
47
  attemptsRequested: snapshot.model.run.attemptsRequested ?? 1,
45
48
  attemptsCompleted: snapshot.model.run.attemptsCompleted ?? 1,
49
+ taskInventory: buildTaskInventory(snapshot, [...groups.values()]),
46
50
  groups: [...groups.values()],
47
51
  };
48
52
  }
53
+ function buildTaskInventory(snapshot, groups) {
54
+ const unitsBySource = new Map(snapshot.model.units
55
+ .filter(unit => unit.sourceFile)
56
+ .map(unit => [unit.sourceFile, unit]));
57
+ const files = new Map();
58
+ for (const unit of snapshot.model.units) {
59
+ if (!unit.sourceFile)
60
+ continue;
61
+ const collection = unit.collection ?? {
62
+ state: 'incomplete',
63
+ reason: 'adapter-cannot-prove-completeness',
64
+ };
65
+ const existing = files.get(unit.sourceFile);
66
+ if (!existing) {
67
+ files.set(unit.sourceFile, {
68
+ tasks: [],
69
+ completeness: collection.state,
70
+ ...(collection.state === 'incomplete' ? { reason: collection.reason } : {}),
71
+ });
72
+ }
73
+ else if (collection.state === 'incomplete') {
74
+ existing.completeness = 'incomplete';
75
+ existing.reason = collection.reason;
76
+ }
77
+ }
78
+ for (const group of groups) {
79
+ if (!group.sourceFile)
80
+ continue;
81
+ const unit = unitsBySource.get(group.sourceFile);
82
+ const collection = unit?.collection ?? {
83
+ state: 'incomplete',
84
+ reason: 'adapter-cannot-prove-completeness',
85
+ };
86
+ const file = files.get(group.sourceFile) ?? {
87
+ tasks: [],
88
+ completeness: collection.state,
89
+ ...(collection.state === 'incomplete' ? { reason: collection.reason } : {}),
90
+ };
91
+ file.tasks.push(taskInventoryEntry(group));
92
+ if (collection.state === 'incomplete') {
93
+ file.completeness = 'incomplete';
94
+ file.reason = collection.reason;
95
+ }
96
+ files.set(group.sourceFile, file);
97
+ }
98
+ return {
99
+ version: TASK_INVENTORY_VERSION,
100
+ files: [...files.entries()].toSorted(([left], [right]) => left.localeCompare(right)).map(([eval_file, file]) => ({
101
+ eval_file,
102
+ tasks: file.tasks.toSorted((left, right) => left.task_key.localeCompare(right.task_key)),
103
+ ...(file.completeness === 'complete'
104
+ ? { completeness: 'complete' }
105
+ : { completeness: 'incomplete', reason: file.reason ?? 'adapter-cannot-prove-completeness' }),
106
+ })),
107
+ };
108
+ }
109
+ function taskInventoryEntry(group) {
110
+ const states = group.cases.map(testCase => testCase.state);
111
+ if (states.every(state => state === 'skipped')) {
112
+ return { task_key: group.groupName, state: 'not-scored', reason: 'skipped' };
113
+ }
114
+ if (states.every(state => state === 'pending')) {
115
+ return { task_key: group.groupName, state: 'not-scored', reason: 'pending' };
116
+ }
117
+ if (group.cases.some(testCase => testCase.attempts?.some(attempt => attempt.outcome.kind === 'timed-out'))) {
118
+ return { task_key: group.groupName, state: 'not-scored', reason: 'timed-out' };
119
+ }
120
+ if (group.cases.some(isPromotableCase))
121
+ return { task_key: group.groupName, state: 'scored' };
122
+ return { task_key: group.groupName, state: 'not-scored', reason: 'failed-before-evaluation' };
123
+ }
124
+ function isPromotableCase(testCase) {
125
+ if (testCase.state === 'skipped' || testCase.state === 'pending')
126
+ return false;
127
+ const evaluations = testCase.attempts?.flatMap(attempt => attempt.evaluations ?? []) ?? testCase.evaluations;
128
+ if (evaluations === undefined)
129
+ return true;
130
+ return evaluations.some(evaluation => evaluation.score !== undefined
131
+ && evaluation.resultKind !== 'synthetic_no_evaluation');
132
+ }
49
133
  function totalDurationMs(runCase) {
50
134
  return runCase.attempts.reduce((sum, attempt) => sum + (attempt.durationMs ?? 0), 0);
51
135
  }
@@ -31,11 +31,15 @@ export function createVitestAdapter(options = {}) {
31
31
  native: {
32
32
  testModules: options.testModules ?? [],
33
33
  cwd: discoveryCwd,
34
+ discoveredFiles: input.discovered.units.flatMap(unit => unit.sourceRef ? [unit.sourceRef] : []),
34
35
  },
35
36
  };
36
37
  },
37
38
  async collectNormalizedRunSnapshot(run) {
38
- return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules), { cwd: readVitestRunNative(run).cwd });
39
+ return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules), {
40
+ cwd: readVitestRunNative(run).cwd,
41
+ discoveredFiles: readVitestRunNative(run).discoveredFiles,
42
+ });
39
43
  },
40
44
  };
41
45
  }
@@ -59,7 +63,7 @@ export function collectVitestReportGroups(testModules) {
59
63
  function readVitestRunNative(run) {
60
64
  if (isVitestRunNative(run.native))
61
65
  return run.native;
62
- return { testModules: [], cwd: process.cwd() };
66
+ return { testModules: [], cwd: process.cwd(), discoveredFiles: [] };
63
67
  }
64
68
  function isVitestRunNative(native) {
65
69
  return typeof native === 'object'
package/dist/sdk/agent.js CHANGED
@@ -20,6 +20,8 @@ import { collectOpenCodeMcpToolNames } from '../agents/opencode/contract.js';
20
20
  import { collectSensitiveEnvValues } from '../tool-event-results.js';
21
21
  import { createAskUserHandler } from './ask-bus/handler.js';
22
22
  import { resolveAgentRuntimeOptions } from './agent-runtime-options.js';
23
+ import { extractToolEventsFromLog } from '../tool-events.js';
24
+ import { clearToolEventRuntimeMetadata } from './tool-event-secrets.js';
23
25
  /**
24
26
  * Test-only injection point: override the sink used by the next emitter
25
27
  * built inside `createAgent`. Pass `null` to restore the default (stderr).
@@ -392,7 +394,14 @@ class AgentImpl {
392
394
  }
393
395
  }
394
396
  finally {
395
- await this.ws.dispose();
397
+ try {
398
+ await this.ws.dispose();
399
+ }
400
+ finally {
401
+ for (const event of extractToolEventsFromLog(this._log)) {
402
+ clearToolEventRuntimeMetadata(event);
403
+ }
404
+ }
396
405
  }
397
406
  }
398
407
  }
@@ -7,6 +7,9 @@ import { runScorer } from './run-scorer.js';
7
7
  import { createLLMClient } from '../utils/llm.js';
8
8
  import { sandboxExec } from '../providers/sandbox-exec.js';
9
9
  import { buildTranscript, loadRunSnapshot, WorkspaceMissingError } from './snapshots.js';
10
+ import { sanitizePersistenceValue } from '../tool-event-results.js';
11
+ import { getOriginalMcpInput } from './mcp-event-input.js';
12
+ import { cloneToolEventWithRuntimeMetadata, collectToolEventSensitiveValues, } from './tool-event-secrets.js';
10
13
  import fs from 'fs-extra';
11
14
  import path from 'path';
12
15
  import { summarizeFlow } from './agent-flow.js';
@@ -154,6 +157,9 @@ export function evaluateStepScorers(agent, scorers, opts) {
154
157
  return evaluateAgent(agent, scorers, opts, false);
155
158
  }
156
159
  async function fromSnapshot(snapshotPath, scorers, opts) {
160
+ if (opts?.deterministicToolEvidence === 'live') {
161
+ throw new TypeError("evaluate.fromSnapshot() does not support deterministicToolEvidence: 'live'");
162
+ }
157
163
  const snapshot = await loadRunSnapshot(snapshotPath);
158
164
  const trackedLLM = opts?.llm ?? createLLMClient({ adapters: [{
159
165
  name: 'runtime', isAvailable: async () => true,
@@ -201,6 +207,9 @@ async function evaluateWithContext(ctx, scorers, opts) {
201
207
  call: (prompt, callOpts) => getRuntime().llm.call(prompt, callOpts),
202
208
  }] });
203
209
  const onScorerError = opts?.onScorerError ?? 'skip';
210
+ const deterministicCtx = opts?.deterministicToolEvidence === 'live'
211
+ ? createLiveDeterministicContext(ctx)
212
+ : ctx;
204
213
  const phase1 = [];
205
214
  const phase2 = [];
206
215
  const phase3 = [];
@@ -219,7 +228,7 @@ async function evaluateWithContext(ctx, scorers, opts) {
219
228
  }
220
229
  }
221
230
  const results = [];
222
- const phase1Results = await Promise.all(phase1.map((g) => runScorer(g, ctx)));
231
+ const phase1Results = await Promise.all(phase1.map((g) => runScorer(g, deterministicCtx)));
223
232
  results.push(...phase1Results);
224
233
  const anyCheckFailed = failFast && phase1Results.some((r) => r.type === 'check' && r.status !== 'error' && r.score === 0);
225
234
  if (anyCheckFailed) {
@@ -230,7 +239,7 @@ async function evaluateWithContext(ctx, scorers, opts) {
230
239
  else {
231
240
  const judgeResults = await runJudgePipeline(phase2, ctx, { llm: trackedLLM });
232
241
  results.push(...judgeResults);
233
- const phase3Results = await Promise.all(phase3.map((g) => runScorer(g, ctx)));
242
+ const phase3Results = await Promise.all(phase3.map((g) => runScorer(g, deterministicCtx)));
234
243
  results.push(...phase3Results);
235
244
  }
236
245
  const tokenUsage = trackedLLM.tokenUsage ?? { inputTokens: 0, outputTokens: 0 };
@@ -240,12 +249,58 @@ async function evaluateWithContext(ctx, scorers, opts) {
240
249
  const totalWeight = scoringResults.reduce((sum, r) => sum + r.weight, 0);
241
250
  const weightedSum = scoringResults.reduce((sum, r) => sum + r.score * r.weight, 0);
242
251
  const score = totalWeight > 0 ? weightedSum / totalWeight : 0;
243
- return {
252
+ return sanitizePersistenceValue({
244
253
  score,
245
254
  scorers: results,
246
255
  tokenUsage,
256
+ }, collectToolEventSensitiveValues(ctx.toolEvents));
257
+ }
258
+ function createLiveDeterministicContext(ctx) {
259
+ return {
260
+ ...ctx,
261
+ toolEvents: ctx.toolEvents.map((event) => {
262
+ const originalInput = event.action === 'mcp_tool_call'
263
+ ? getOriginalMcpInput(event)
264
+ : undefined;
265
+ const mcp = normalizedMcpClassification(event);
266
+ return originalInput
267
+ ? cloneToolEventWithRuntimeMetadata(event, { arguments: {
268
+ ...event.arguments,
269
+ server: event.arguments?.server,
270
+ tool: event.arguments?.tool,
271
+ status: event.arguments?.status,
272
+ ...structuredClone(originalInput),
273
+ }, ...(mcp ? { mcp } : {}) })
274
+ : event;
275
+ }),
247
276
  };
248
277
  }
278
+ function normalizedMcpClassification(event) {
279
+ if (event.mcp)
280
+ return event.mcp;
281
+ const args = event.arguments ?? {};
282
+ const separator = event.providerToolName.indexOf('.');
283
+ const serverName = typeof args.server === 'string' ? args.server
284
+ : separator > 0 ? event.providerToolName.slice(0, separator) : undefined;
285
+ const toolName = typeof args.tool === 'string' ? args.tool
286
+ : separator > 0 && separator < event.providerToolName.length - 1
287
+ ? event.providerToolName.slice(separator + 1) : undefined;
288
+ const status = event.status ?? args.status;
289
+ if (!serverName || !toolName)
290
+ return undefined;
291
+ if (status === 'completed')
292
+ return { serverName, toolName, invocation: 'confirmed', outcome: 'completed' };
293
+ if (status === 'error' || status === 'failed') {
294
+ return { serverName, toolName, invocation: 'confirmed', outcome: 'tool_error' };
295
+ }
296
+ if (status === 'user_denied' || status === 'policy_denied') {
297
+ return { serverName, toolName, invocation: 'not_invoked', outcome: status };
298
+ }
299
+ if (status === 'protocol_error') {
300
+ return { serverName, toolName, invocation: 'unknown', outcome: 'protocol_error' };
301
+ }
302
+ return undefined;
303
+ }
249
304
  function maybeThrowOnScorerErrors(result, mode) {
250
305
  if (mode !== 'fail')
251
306
  return;
@@ -1,13 +1,15 @@
1
1
  import { sanitizePersistenceValue } from '../tool-event-results.js';
2
- import { getToolEventSensitiveValues } from './tool-event-secrets.js';
2
+ import { collectToolEventSensitiveValues, } from './tool-event-secrets.js';
3
3
  export function buildJudgePrompt(scorer, ctx, input) {
4
4
  const sections = [];
5
- sections.push(`## Session Transcript\n${ctx.transcript}`);
5
+ const sensitiveValues = collectToolEventSensitiveValues(ctx.toolEvents);
6
+ sections.push(`## Session Transcript\n${sanitizePersistenceValue(ctx.transcript, sensitiveValues)}`);
6
7
  if (scorer.includeToolEvents && ctx.toolEvents.length > 0) {
7
8
  sections.push(`## Tool Events\n${formatToolEvents(ctx)}`);
8
9
  }
9
10
  if (input) {
10
- for (const [key, value] of Object.entries(input)) {
11
+ const sanitizedInput = sanitizePersistenceValue(input, sensitiveValues);
12
+ for (const [key, value] of Object.entries(sanitizedInput)) {
11
13
  const body = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
12
14
  sections.push(`## ${key}\n${body}`);
13
15
  }
@@ -22,10 +24,10 @@ ${scorer.rubric}
22
24
  Respond with ONLY a JSON object: {"score": <number>, "details": "<brief explanation>"}`;
23
25
  }
24
26
  function formatToolEvents(ctx) {
27
+ const sensitiveValues = collectToolEventSensitiveValues(ctx.toolEvents);
25
28
  return ctx.toolEvents
26
29
  .map((event) => {
27
30
  const turn = event.turnNumber ? `turn ${event.turnNumber}` : 'instruction';
28
- const sensitiveValues = getToolEventSensitiveValues(event);
29
31
  const details = [
30
32
  event.status ? ` status: ${event.status}` : undefined,
31
33
  event.mcp ? ` mcp: ${JSON.stringify(event.mcp)}` : undefined,
@@ -41,7 +43,8 @@ function formatToolEvents(ctx) {
41
43
  }
42
44
  export function buildBatchedJudgePrompt(judges, ctx, inputs) {
43
45
  const sections = [];
44
- sections.push(`## Session Transcript\n${ctx.transcript}`);
46
+ const sensitiveValues = collectToolEventSensitiveValues(ctx.toolEvents);
47
+ sections.push(`## Session Transcript\n${sanitizePersistenceValue(ctx.transcript, sensitiveValues)}`);
45
48
  if (judges.some((j) => j.includeToolEvents) && ctx.toolEvents.length > 0) {
46
49
  sections.push(`## Tool Events\n${formatToolEvents(ctx)}`);
47
50
  }
@@ -49,7 +52,8 @@ export function buildBatchedJudgePrompt(judges, ctx, inputs) {
49
52
  const parts = [`### Rubric ${i + 1}: "${j.name}"\n${j.rubric}`];
50
53
  const input = inputs[i];
51
54
  if (input) {
52
- for (const [key, value] of Object.entries(input)) {
55
+ const sanitizedInput = sanitizePersistenceValue(input, sensitiveValues);
56
+ for (const [key, value] of Object.entries(sanitizedInput)) {
53
57
  const body = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
54
58
  parts.push(`#### ${key}\n${body}`);
55
59
  }
@@ -77,7 +81,7 @@ export function buildToolUseJudgePrompt(scorer, ctx) {
77
81
  ].join('\n');
78
82
  const parts = [];
79
83
  parts.push('## Session Transcript');
80
- parts.push(ctx.transcript);
84
+ parts.push(sanitizePersistenceValue(ctx.transcript, collectToolEventSensitiveValues(ctx.toolEvents)));
81
85
  if (scorer.includeToolEvents && ctx.toolEvents.length > 0) {
82
86
  parts.push('## Tool Events');
83
87
  parts.push(formatToolEvents(ctx));
@@ -1,3 +1,4 @@
1
1
  import type { ToolEvent } from '../tool-events.js';
2
2
  export declare function attachOriginalMcpInput(event: ToolEvent, input: Record<string, unknown>): ToolEvent;
3
3
  export declare function getOriginalMcpInput(event: ToolEvent): Record<string, unknown> | undefined;
4
+ export declare function clearOriginalMcpInput(event: ToolEvent): void;
@@ -6,3 +6,6 @@ export function attachOriginalMcpInput(event, input) {
6
6
  export function getOriginalMcpInput(event) {
7
7
  return originalInputs.get(event);
8
8
  }
9
+ export function clearOriginalMcpInput(event) {
10
+ originalInputs.delete(event);
11
+ }
@@ -2,9 +2,12 @@ export function getMcpToolCall(event) {
2
2
  if (event.action !== 'mcp_tool_call')
3
3
  return undefined;
4
4
  const args = event.arguments ?? {};
5
- const serverName = typeof args.server === 'string' ? args.server : undefined;
6
- const toolName = typeof args.tool === 'string' ? args.tool : undefined;
7
- const status = typeof args.status === 'string' ? args.status : undefined;
5
+ const serverName = event.mcp?.serverName
6
+ ?? (typeof args.server === 'string' ? args.server : undefined);
7
+ const toolName = event.mcp?.toolName
8
+ ?? (typeof args.tool === 'string' ? args.tool : undefined);
9
+ const argumentStatus = typeof args.status === 'string' ? args.status : undefined;
10
+ const status = mcpClassificationStatus(event.mcp, argumentStatus) ?? argumentStatus;
8
11
  if (!serverName || !toolName || !status)
9
12
  return undefined;
10
13
  return {
@@ -15,6 +18,16 @@ export function getMcpToolCall(event) {
15
18
  event,
16
19
  };
17
20
  }
21
+ function mcpClassificationStatus(mcp, argumentStatus) {
22
+ if (!mcp)
23
+ return undefined;
24
+ if (mcp.invocation === 'confirmed') {
25
+ if (mcp.outcome === 'completed')
26
+ return 'completed';
27
+ return argumentStatus === 'failed' || argumentStatus === 'error' ? argumentStatus : 'error';
28
+ }
29
+ return mcp.outcome;
30
+ }
18
31
  export function isMcpToolCall(event, expected = {}) {
19
32
  const call = getMcpToolCall(event);
20
33
  return call !== undefined && matchesMcpToolCall(call, expected);
@@ -1,4 +1,4 @@
1
- import { sanitizePersistenceValue } from '../tool-event-results.js';
1
+ import { sanitizeUntrustedPersistenceValue } from '../tool-event-results.js';
2
2
  export function decideMcpToolCall(options, request) {
3
3
  const runMode = options?.runMode ?? 'mock';
4
4
  if (runMode === 'mock')
@@ -31,7 +31,7 @@ export function decideMcpToolCall(options, request) {
31
31
  return { action: 'allow' };
32
32
  }
33
33
  export function redactMcpSecrets(value) {
34
- return sanitizePersistenceValue(value);
34
+ return sanitizeUntrustedPersistenceValue(value);
35
35
  }
36
36
  function ruleMatches(rule, request) {
37
37
  if (rule.serverName !== undefined && rule.serverName !== request.serverName)
@@ -1,15 +1,18 @@
1
1
  import type { CheckScorer, CodeJudgeToolName, ScorerContext, JudgeScorer, ScoreScorer, ScoreResult, ToolExpectation, ToolUsageScorer } from './types.js';
2
+ import type { JsonValue } from '../internal/direct-mcp-v2/types.js';
2
3
  /**
3
4
  * Create a check scorer — a boolean gate that passes (1.0) or fails (0.0).
4
5
  */
5
6
  export declare function check(name: string, fn: (ctx: ScorerContext) => boolean | Promise<boolean>, opts?: {
6
7
  weight?: number;
8
+ revision?: JsonValue;
7
9
  }): CheckScorer;
8
10
  /**
9
11
  * Create a score scorer — returns a number (0-1) or { score, details } for partial credit.
10
12
  */
11
13
  export declare function score(name: string, fn: (ctx: ScorerContext) => number | ScoreResult | Promise<number | ScoreResult>, opts?: {
12
14
  weight?: number;
15
+ revision?: JsonValue;
13
16
  }): ScoreScorer;
14
17
  /**
15
18
  * Create a judge scorer — uses an LLM to evaluate against a rubric.
@@ -55,10 +58,12 @@ export declare function judge(name: string, opts: {
55
58
  maxRounds?: number;
56
59
  /** Anthropic prompt caching. Default: true when tools is set, unchanged otherwise. */
57
60
  cacheControl?: boolean;
61
+ revision?: JsonValue;
58
62
  }): JudgeScorer;
59
63
  /**
60
64
  * Create a tool usage scorer — matches tool events against expectations.
61
65
  */
62
66
  export declare function toolUsage(name: string, expectations: ToolExpectation[], opts?: {
63
67
  weight?: number;
68
+ revision?: JsonValue;
64
69
  }): ToolUsageScorer;
@@ -7,6 +7,7 @@ export function check(name, fn, opts) {
7
7
  name,
8
8
  weight: opts?.weight ?? 1,
9
9
  fn,
10
+ ...(opts?.revision === undefined ? {} : { revision: opts.revision }),
10
11
  };
11
12
  }
12
13
  /**
@@ -18,6 +19,7 @@ export function score(name, fn, opts) {
18
19
  name,
19
20
  weight: opts?.weight ?? 1,
20
21
  fn,
22
+ ...(opts?.revision === undefined ? {} : { revision: opts.revision }),
21
23
  };
22
24
  }
23
25
  /**
@@ -65,6 +67,7 @@ export function judge(name, opts) {
65
67
  tools,
66
68
  maxRounds: opts.maxRounds,
67
69
  cacheControl,
70
+ ...(opts.revision === undefined ? {} : { revision: opts.revision }),
68
71
  };
69
72
  }
70
73
  /**
@@ -76,5 +79,6 @@ export function toolUsage(name, expectations, opts) {
76
79
  name,
77
80
  weight: opts?.weight ?? 1,
78
81
  expectations,
82
+ ...(opts?.revision === undefined ? {} : { revision: opts.revision }),
79
83
  };
80
84
  }
@@ -1,4 +1,5 @@
1
1
  import { redactMcpSecrets } from './mcp-safety.js';
2
+ import { attachLiveMcpInput } from './tool-event-secrets.js';
2
3
  export function buildScriptedMcpApprovalEvent(opts) {
3
4
  const providerToolName = `${opts.serverName}.${opts.toolName}`;
4
5
  return {
@@ -18,7 +19,7 @@ export function buildScriptedMcpApprovalEvent(opts) {
18
19
  export function buildScriptedMcpDeniedCallEvent(opts) {
19
20
  const providerToolName = `${opts.serverName}.${opts.toolName}`;
20
21
  const args = redactMcpSecrets(opts.args);
21
- return {
22
+ return attachLiveMcpInput({
22
23
  action: 'mcp_tool_call', provider: opts.provider, providerToolName,
23
24
  toolUseId: opts.toolUseId, turnNumber: opts.turnNumber, status: 'error',
24
25
  mcp: { serverName: opts.serverName, toolName: opts.toolName, invocation: 'not_invoked', outcome: opts.outcome },
@@ -26,5 +27,5 @@ export function buildScriptedMcpDeniedCallEvent(opts) {
26
27
  summary: `MCP tool ${providerToolName} ${opts.outcome}`,
27
28
  confidence: 'high',
28
29
  rawSnippet: JSON.stringify({ status: opts.outcome }),
29
- };
30
+ }, opts.args);
30
31
  }
@@ -1,7 +1,22 @@
1
- import { sanitizePersistenceValue } from '../tool-event-results.js';
2
- import { getToolEventSensitiveValues } from './tool-event-secrets.js';
1
+ import { collectStructuredSensitiveValues, isSensitiveValueScanLimitError, sanitizePersistenceValue, } from '../tool-event-results.js';
2
+ import { attachToolEventSensitiveValues, cloneToolEventWithRuntimeMetadata, getToolEventSensitiveValues, redactToolEventPayload, } from './tool-event-secrets.js';
3
3
  export function buildToolEventLogEntry(toolEvent, fallbackTimestamp) {
4
- const persistedEvent = sanitizePersistenceValue(toolEvent, getToolEventSensitiveValues(toolEvent));
4
+ let event = toolEvent;
5
+ let discoveredValues;
6
+ try {
7
+ discoveredValues = collectStructuredSensitiveValues(event);
8
+ }
9
+ catch (error) {
10
+ if (!isSensitiveValueScanLimitError(error))
11
+ throw error;
12
+ event = redactToolEventPayload(event);
13
+ discoveredValues = [];
14
+ }
15
+ const sensitiveValues = [...new Set([
16
+ ...getToolEventSensitiveValues(toolEvent),
17
+ ...discoveredValues,
18
+ ])];
19
+ const persistedEvent = attachToolEventSensitiveValues(cloneToolEventWithRuntimeMetadata(event, sanitizePersistenceValue(event, sensitiveValues)), sensitiveValues);
5
20
  return {
6
21
  type: 'tool_event',
7
22
  timestamp: persistedEvent.startedAt ?? fallbackTimestamp,
@@ -1,4 +1,8 @@
1
1
  import type { ToolEvent } from '../tool-events.js';
2
2
  export declare function attachToolEventSensitiveValues(event: ToolEvent, sensitiveValues: readonly string[]): ToolEvent;
3
3
  export declare function getToolEventSensitiveValues(event: ToolEvent): readonly string[];
4
+ export declare function attachLiveMcpInput(event: ToolEvent, input: Record<string, unknown>): ToolEvent;
5
+ export declare function redactToolEventPayload(event: ToolEvent): ToolEvent;
6
+ export declare function collectToolEventSensitiveValues(events: readonly ToolEvent[]): string[];
7
+ export declare function clearToolEventRuntimeMetadata(event: ToolEvent): void;
4
8
  export declare function cloneToolEventWithRuntimeMetadata(source: ToolEvent, overrides: Partial<ToolEvent>): ToolEvent;