@wix/pathgrade 1.0.27 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +12 -19
  2. package/dist/adapters/jest/invocation-adapter.js +5 -1
  3. package/dist/adapters/jest/reporter.js +4 -1
  4. package/dist/adapters/jest/results.js +8 -0
  5. package/dist/adapters/node-test/index.d.ts +5 -0
  6. package/dist/adapters/node-test/index.js +35 -7
  7. package/dist/adapters/node-test/invocation-adapter.js +3 -1
  8. package/dist/adapters/node-test/runner-adapter.js +22 -15
  9. package/dist/adapters/vitest/reporter.js +4 -1
  10. package/dist/agents/claude.js +5 -4
  11. package/dist/agents/codex-app-server/agent.js +1 -51
  12. package/dist/agents/codex-app-server/turn-notifications.d.ts +6 -0
  13. package/dist/agents/codex-app-server/turn-notifications.js +51 -0
  14. package/dist/agents/codex-app-server/turn-state.d.ts +19 -0
  15. package/dist/agents/codex-app-server/turn-state.js +1 -0
  16. package/dist/agents/opencode/protocol.d.ts +7 -0
  17. package/dist/agents/opencode/protocol.js +47 -0
  18. package/dist/agents/opencode.js +1 -47
  19. package/dist/analytics/engine.js +5 -2
  20. package/dist/commands/report.js +16 -2
  21. package/dist/commands/run-args.d.ts +1 -0
  22. package/dist/commands/run-args.js +13 -0
  23. package/dist/commands/run-changed.js +5 -15
  24. package/dist/config/pathgrade.d.ts +3 -0
  25. package/dist/config/pathgrade.js +33 -1
  26. package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +2 -1
  27. package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +35 -17
  28. package/dist/internal/direct-mcp-v2/claude-profile.d.ts +33 -7
  29. package/dist/internal/direct-mcp-v2/claude-profile.js +104 -72
  30. package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +13 -16
  31. package/dist/pathgrade.js +10 -2
  32. package/dist/reporters/cli.js +13 -6
  33. package/dist/reporters/github-comment.js +12 -9
  34. package/dist/reporters/loader.d.ts +1 -0
  35. package/dist/reporters/loader.js +27 -2
  36. package/dist/reporters/report-summary.js +13 -5
  37. package/dist/reporting/core.d.ts +1 -0
  38. package/dist/reporting/core.js +183 -105
  39. package/dist/reporting/types.d.ts +19 -3
  40. package/dist/runners/adapter-loader.js +17 -12
  41. package/dist/runners/direct-reporter-attempts.d.ts +1 -0
  42. package/dist/runners/direct-reporter-attempts.js +7 -0
  43. package/dist/runners/invocation.d.ts +2 -0
  44. package/dist/runners/model-builders.js +1 -0
  45. package/dist/runners/model-validation.js +31 -0
  46. package/dist/runners/model.d.ts +4 -0
  47. package/dist/runners/orchestrator.d.ts +2 -0
  48. package/dist/runners/orchestrator.js +11 -1
  49. package/dist/runners/repeated-attempts.d.ts +7 -0
  50. package/dist/runners/repeated-attempts.js +149 -0
  51. package/dist/runners/repeated-invocation.d.ts +7 -0
  52. package/dist/runners/repeated-invocation.js +129 -0
  53. package/dist/runners/report-projection.js +16 -6
  54. package/dist/runners/vitest-adapter.js +10 -0
  55. package/dist/runners/vitest-invocation.js +2 -0
  56. package/dist/sdk/agent-runtime-options.d.ts +12 -0
  57. package/dist/sdk/agent-runtime-options.js +67 -0
  58. package/dist/sdk/agent.js +4 -57
  59. package/dist/sdk/case-context.js +7 -2
  60. package/dist/sdk/lifecycle.js +8 -3
  61. package/dist/sdk/result-capture.js +4 -1
  62. package/dist/types.d.ts +34 -5
  63. package/dist/viewer.html +19 -19
  64. package/package.json +2 -2
@@ -8,7 +8,7 @@ import { projectEndpoints } from './projection.js';
8
8
  import { admitRuntimeLock } from './runtime-lock.js';
9
9
  import { startScenarioHttpHost } from './scenario-http-host.js';
10
10
  import { createClaudeParentBridges } from './claude-direct-mcp.js';
11
- import { CLAUDE_DIRECT_MCP_DARWIN_ARM64_LOCK, CLAUDE_DIRECT_MCP_NORMALIZED_HANDSHAKE, resolveClaudeScenarioExecutable } from './claude-profile.js';
11
+ import { observeClaudeRuntimeControlPlane, resolveClaudeScenarioExecutable, selectClaudeRuntimeTarget } from './claude-profile.js';
12
12
  const defaultCoordinatorDependencies = {
13
13
  startHost: startScenarioHttpHost,
14
14
  scanBearerSurfaces,
@@ -286,8 +286,10 @@ function scenarioScanRoots(workspacePath, environment) {
286
286
  ].filter((path) => typeof path === 'string' && path.length > 0))];
287
287
  }
288
288
  export async function startPublicClaudeScenarioRuntime(options, dependencies = {}) {
289
- const lock = dependencies.lock ?? CLAUDE_DIRECT_MCP_DARWIN_ARM64_LOCK;
290
- const resolveExecutable = dependencies.resolveExecutable ?? resolveClaudeScenarioExecutable;
289
+ const target = dependencies.lock ? undefined : selectClaudeRuntimeTarget();
290
+ const lock = dependencies.lock ?? target.lock;
291
+ const resolveExecutable = dependencies.resolveExecutable
292
+ ?? (async () => resolveClaudeScenarioExecutable(target));
291
293
  const coordinator = await startScenarioCoordinator({
292
294
  ...options,
293
295
  provider: 'claude',
@@ -301,16 +303,10 @@ export async function startPublicClaudeScenarioRuntime(options, dependencies = {
301
303
  environment: options.environment,
302
304
  forbiddenSecrets: [bearer.slice('Bearer '.length), bearer],
303
305
  ...(dependencies.commandVersion ? { commandVersion: dependencies.commandVersion } : {}),
304
- initializeControlPlane: async () => {
305
- if (typeof createSdkMcpServer !== 'function' || typeof sdkQuery !== 'function') {
306
- throw new Error('Claude SDK parent bridge methods unavailable');
307
- }
308
- return {
309
- normalizedHandshake: CLAUDE_DIRECT_MCP_NORMALIZED_HANDSHAKE,
310
- capabilities: lock.capabilities,
311
- close: async () => undefined,
312
- };
313
- },
306
+ initializeControlPlane: async () => observeClaudeRuntimeControlPlane({
307
+ createSdkMcpServer,
308
+ query: sdkQuery,
309
+ }),
314
310
  });
315
311
  const projected = projectEndpoints(context.endpoints, admission.profile);
316
312
  if (!Array.isArray(projected))
@@ -320,7 +316,7 @@ export async function startPublicClaudeScenarioRuntime(options, dependencies = {
320
316
  }));
321
317
  const bridges = createClaudeParentBridges({ artifact: options.artifact, projections: materialized });
322
318
  return {
323
- value: bridges,
319
+ value: { bridges, commandPath: admission.commandPath },
324
320
  async interrupt() { },
325
321
  async waitForTerminal() { },
326
322
  async close() {
@@ -340,9 +336,10 @@ export async function startPublicClaudeScenarioRuntime(options, dependencies = {
340
336
  },
341
337
  });
342
338
  return {
343
- claudeServers: coordinator.attachment,
339
+ claudeServers: coordinator.attachment.bridges,
344
340
  configureSession(sessionOptions) {
345
- sessionOptions.scenarioMcpServers = coordinator.attachment;
341
+ sessionOptions.scenarioMcpServers = coordinator.attachment.bridges;
342
+ sessionOptions.admittedClaudeCodeExecutable = coordinator.attachment.commandPath;
346
343
  },
347
344
  beginTurn: coordinator.beginTurn,
348
345
  settleTurn: coordinator.settleTurn,
package/dist/pathgrade.js CHANGED
@@ -201,7 +201,14 @@ export async function runPathgradeCli(options = {}) {
201
201
  await clearSidecar(process.cwd());
202
202
  const env = buildRunnerEnv(parsed);
203
203
  try {
204
- const config = await resolvePathgradeConfig({ cwd: process.cwd() });
204
+ const config = await resolvePathgradeConfig({
205
+ cwd: process.cwd(),
206
+ cli: parsed.attempts === undefined ? undefined : { attempts: parsed.attempts },
207
+ runnerArgs: parsed.runnerArgs,
208
+ });
209
+ if (!parsed.quiet && config.attempts > 1) {
210
+ console.error(`pathgrade: ${config.attempts} sequential attempts per selected case`);
211
+ }
205
212
  const runner = await loadRunnerInvocationAdapter({
206
213
  adapterName: parsed.adapterName ?? config.runner.adapter,
207
214
  cwd: process.cwd(),
@@ -210,7 +217,7 @@ export async function runPathgradeCli(options = {}) {
210
217
  process.exitCode = await runner.run({
211
218
  cwd: process.cwd(),
212
219
  runnerArgs: [...config.runner.args, ...parsed.runnerArgs],
213
- env,
220
+ env: { ...env, PATHGRADE_ATTEMPT_COUNT: String(config.attempts) },
214
221
  });
215
222
  }
216
223
  catch (err) {
@@ -233,6 +240,7 @@ function printHelp(cliName) {
233
240
  [--since=<ref>] Override base ref (implies git mode)
234
241
  [--changed-files=<path>] Use an explicit newline-delimited file list
235
242
  [--adapter=<name|path>] Select built-in or third-party runner adapter
243
+ [--attempts=N] Run every selected case N times sequentially
236
244
  [--diagnostics] Print full diagnostics for passing evals too
237
245
  [--quiet] Suppress the run-start summary
238
246
  [--verbose|-v] Stream live per-turn events to stderr during the run
@@ -18,8 +18,8 @@ export async function runCliPreview(resultsDir, opts) {
18
18
  }
19
19
  console.log(`\n${fmt.bold('pathgrade preview')} ${fmt.dim(`${entries.length} reports from ${resolved}`)}\n`);
20
20
  for (const { file, ...report } of entries) {
21
- const passRate = report.pass_rate ?? 0;
22
- const isPass = passRate >= 0.5;
21
+ const meanReward = report.mean_reward ?? report.pass_rate ?? 0;
22
+ const isPass = report.status === undefined ? meanReward >= 0.5 : report.status === 'pass';
23
23
  const trials = report.trials || [];
24
24
  const avgDur = trials.reduce((s, t) => s + (t.duration_ms || 0), 0) / (trials.length || 1);
25
25
  const totalTokens = trials.reduce((s, t) => s + (t.input_tokens || 0) + (t.output_tokens || 0) + (t.conversation_input_tokens || 0) + (t.conversation_output_tokens || 0), 0);
@@ -33,9 +33,9 @@ export async function runCliPreview(resultsDir, opts) {
33
33
  console.log();
34
34
  // ── Summary metrics
35
35
  const metrics = [
36
- ['Pass Rate', `${(passRate * 100).toFixed(1)}%`],
37
- ['pass@k', report.pass_at_k != null ? `${(report.pass_at_k * 100).toFixed(1)}%` : '—'],
38
- ['pass^k', report.pass_pow_k != null ? `${(report.pass_pow_k * 100).toFixed(1)}%` : '—'],
36
+ ['Mean Reward', `${(meanReward * 100).toFixed(1)}%`],
37
+ ['Success Rate', report.success_rate != null ? `${(report.success_rate * 100).toFixed(1)}%` : '—'],
38
+ ['pass@k', formatPassAtK(report.pass_at_k, report.pass_at_k_unavailable_reason)],
39
39
  ['Avg Duration', `${(avgDur / 1000).toFixed(1)}s`],
40
40
  ['Total Tokens', `~${totalTokens}`],
41
41
  ['Skills', report.skills_used?.join(', ') || 'none'],
@@ -47,7 +47,7 @@ export async function runCliPreview(resultsDir, opts) {
47
47
  // ── Trials
48
48
  for (const trial of trials) {
49
49
  const evaluated = trial.reward !== undefined;
50
- const tp = evaluated && trial.reward >= 0.5;
50
+ const tp = evaluated && (trial.runner_outcome === undefined ? trial.reward >= 0.5 : trial.runner_outcome === 'passed' && trial.reward === 1);
51
51
  const trialStatus = !evaluated ? fmt.dim('N/A') : tp ? fmt.pass('PASS') : fmt.fail('FAIL');
52
52
  const reward = fmt.bold(evaluated ? trial.reward.toFixed(2) : 'n/a');
53
53
  const dur = `${((trial.duration_ms || 0) / 1000).toFixed(1)}s`;
@@ -120,6 +120,13 @@ export async function runCliPreview(resultsDir, opts) {
120
120
  console.log();
121
121
  }
122
122
  }
123
+ function formatPassAtK(value, reason) {
124
+ if (typeof value === 'number')
125
+ return `${(value * 100).toFixed(1)}% (legacy v1)`;
126
+ if (value)
127
+ return Object.entries(value).map(([k, metric]) => `@${k} ${(metric * 100).toFixed(1)}%`).join(', ');
128
+ return reason ? `— (${reason.replaceAll('_', ' ')})` : '—';
129
+ }
123
130
  function formatScorerStatus(status) {
124
131
  switch (status) {
125
132
  case 'error':
@@ -37,6 +37,13 @@ export function commentMarker(commentId) {
37
37
  function pct(n) {
38
38
  return `${(n * 100).toFixed(1)}%`;
39
39
  }
40
+ function formatPassAtK(value, reason) {
41
+ if (typeof value === 'number')
42
+ return `${pct(value)} (legacy v1)`;
43
+ if (value)
44
+ return Object.entries(value).map(([k, metric]) => `pass@${k} ${pct(metric)}`).join(', ');
45
+ return reason ? `Unavailable (${reason.replaceAll('_', ' ')})` : 'Unavailable';
46
+ }
40
47
  function durationSeconds(ms) {
41
48
  return `${(ms / 1000).toFixed(1)}s`;
42
49
  }
@@ -55,10 +62,7 @@ function escapeTableCell(value) {
55
62
  * posted as a GitHub issue comment body.
56
63
  */
57
64
  export function formatReportMarkdown(report, opts) {
58
- const totalTrials = report.groups.reduce((n, g) => n + g.trials.length, 0);
59
- const p = report.overall_pass_rate;
60
- const overallPassAtK = totalTrials > 0 ? 1 - Math.pow(1 - p, totalTrials) : 0;
61
- const overallPassPowK = totalTrials > 0 ? Math.pow(p, totalTrials) : 0;
65
+ const meanReward = report.overall_mean_reward ?? report.overall_pass_rate;
62
66
  const icon = report.status === 'pass' ? '✅' : '❌';
63
67
  const lines = [];
64
68
  lines.push(commentMarker(opts.commentId));
@@ -67,9 +71,8 @@ export function formatReportMarkdown(report, opts) {
67
71
  lines.push('');
68
72
  lines.push(`**Last run:** \`${report.timestamp}\``);
69
73
  lines.push('');
70
- lines.push(`**Pass rate:** ${pct(p)} | ` +
71
- `**pass@${totalTrials}:** ${pct(overallPassAtK)} | ` +
72
- `**pass^${totalTrials}:** ${pct(overallPassPowK)}`);
74
+ lines.push(`**Mean reward:** ${pct(meanReward)}` +
75
+ (report.attempts_requested ? ` | **Attempts:** ${report.attempts_completed ?? 0}/${report.attempts_requested}` : ''));
73
76
  if (opts.detailsUrl) {
74
77
  lines.push('');
75
78
  lines.push(`**Details:** [Open the full report](${formatLinkDestination(opts.detailsUrl)})`);
@@ -83,12 +86,12 @@ export function formatReportMarkdown(report, opts) {
83
86
  lines.push(`Threshold: ${pct(report.threshold)} — ${report.status.toUpperCase()}`);
84
87
  }
85
88
  lines.push('');
86
- lines.push('| Group | Pass rate | pass@k | pass^k | Skills | Avg duration |');
89
+ lines.push('| Group | Mean reward | Success rate | pass@k | Skills | Avg duration |');
87
90
  lines.push('|---|---|---|---|---|---|');
88
91
  for (const group of report.groups) {
89
92
  const skills = group.skills_used.length > 0 ? group.skills_used.join(', ') : '—';
90
93
  const avg = computeAvgDuration(group.trials);
91
- lines.push(`| ${escapeTableCell(group.task)} | ${pct(group.pass_rate)} | ${pct(group.pass_at_k)} | ${pct(group.pass_pow_k)} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
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)} |`);
92
95
  }
93
96
  if (report.selection) {
94
97
  lines.push('');
@@ -2,6 +2,7 @@ import type { EvalReport } from '../types.js';
2
2
  export interface LoadedReport extends EvalReport {
3
3
  file: string;
4
4
  timestamp?: string;
5
+ status?: 'pass' | 'fail';
5
6
  }
6
7
  export declare function loadReports(resultsDir: string, opts?: {
7
8
  skipTraces?: boolean;
@@ -7,7 +7,13 @@ async function hydrateTraces(report, traceFile, resolved) {
7
7
  const traceTrials = await fs.readJSON(tracePath);
8
8
  if (!Array.isArray(traceTrials) || !Array.isArray(report.trials))
9
9
  return;
10
- report.trials = report.trials.map((t, i) => ({ ...t, ...traceTrials[i] }));
10
+ const tracesByAttemptId = new Map(traceTrials
11
+ .filter((trial) => typeof trial?.attempt_id === 'string')
12
+ .map((trial) => [trial.attempt_id, trial]));
13
+ report.trials = report.trials.map((trial, index) => ({
14
+ ...trial,
15
+ ...(trial.attempt_id ? tracesByAttemptId.get(trial.attempt_id) : traceTrials[index]),
16
+ }));
11
17
  }
12
18
  export async function loadReports(resultsDir, opts) {
13
19
  const resolved = path.resolve(resultsDir);
@@ -25,8 +31,15 @@ export async function loadReports(resultsDir, opts) {
25
31
  results.push({ file, ...raw });
26
32
  continue;
27
33
  }
34
+ if (raw.version !== 1 && raw.version !== 2)
35
+ continue;
28
36
  for (const group of raw.groups) {
29
- const report = { file, timestamp: raw.timestamp, ...group };
37
+ const report = {
38
+ file,
39
+ timestamp: raw.timestamp,
40
+ ...group,
41
+ status: groupStatus(raw, group),
42
+ };
30
43
  if (!opts?.skipTraces && group.trace_file) {
31
44
  await hydrateTraces(report, group.trace_file, resolved);
32
45
  }
@@ -37,3 +50,15 @@ export async function loadReports(resultsDir, opts) {
37
50
  }
38
51
  return results;
39
52
  }
53
+ function groupStatus(raw, group) {
54
+ if (group.status === 'pass' || group.status === 'fail')
55
+ return group.status;
56
+ const meanReward = group.mean_reward ?? group.pass_rate ?? 0;
57
+ if (typeof raw.threshold === 'number')
58
+ return meanReward >= raw.threshold ? 'pass' : 'fail';
59
+ const outcomes = Array.isArray(group.trials) ? group.trials.map((trial) => trial.runner_outcome) : [];
60
+ if (outcomes.length > 0 && outcomes.every((outcome) => (outcome === 'passed' || outcome === 'failed' || outcome === 'not-run'))) {
61
+ return outcomes.every((outcome) => outcome === 'passed') ? 'pass' : 'fail';
62
+ }
63
+ return meanReward >= 0.5 ? 'pass' : 'fail';
64
+ }
@@ -3,13 +3,21 @@ import { formatDiagnostics, formatDiagnosticsSummary } from './diagnostics.js';
3
3
  export function printReportSummary(groups, opts = {}) {
4
4
  console.log(`\n${fmt.bold('── pathgrade summary ')}${fmt.dim('─'.repeat(40))}\n`);
5
5
  for (const group of groups) {
6
- const prColor = group.pass_rate >= 0.5 ? fmt.green : fmt.red;
6
+ const rewardColor = group.mean_reward >= 0.5 ? fmt.green : fmt.red;
7
7
  console.log(` ${fmt.bold(group.task)}`);
8
- console.log(` ${fmt.dim('pass rate'.padEnd(12))} ${prColor((group.pass_rate * 100).toFixed(1) + '%')}`);
9
- console.log(` ${fmt.dim(`pass@${group.trial_count}`.padEnd(12))} ${(group.pass_at_k * 100).toFixed(1)}%`);
10
- console.log(` ${fmt.dim(`pass^${group.trial_count}`.padEnd(12))} ${(group.pass_pow_k * 100).toFixed(1)}%`);
8
+ console.log(` ${fmt.dim('mean reward'.padEnd(12))} ${rewardColor((group.mean_reward * 100).toFixed(1) + '%')}`);
9
+ console.log(` ${fmt.dim('success'.padEnd(12))} ${group.success_rate === undefined ? '—' : `${(group.success_rate * 100).toFixed(1)}%`}`);
10
+ if (group.pass_at_k) {
11
+ for (const [k, value] of Object.entries(group.pass_at_k)) {
12
+ console.log(` ${fmt.dim(`pass@${k}`.padEnd(12))} ${(value * 100).toFixed(1)}%`);
13
+ }
14
+ }
15
+ else {
16
+ const reason = group.pass_at_k_unavailable_reason?.replaceAll('_', ' ') ?? 'repeated binary attempts required';
17
+ console.log(` ${fmt.dim('pass@k'.padEnd(12))} — (${reason})`);
18
+ }
11
19
  console.log(` ${fmt.dim('avg time'.padEnd(12))} ${(group.average_duration_ms / 1000).toFixed(1)}s`);
12
- console.log(` ${fmt.dim('trials'.padEnd(12))} ${group.trial_count}`);
20
+ console.log(` ${fmt.dim('attempts'.padEnd(12))} ${group.trial_count}`);
13
21
  console.log();
14
22
  for (const diagnostic of group.diagnostics) {
15
23
  const shouldPrintFull = opts.forceVerbose
@@ -1,2 +1,3 @@
1
1
  import type { PathgradeReportBuildResult, ReportRunInput } from './types.js';
2
2
  export declare function buildPathgradeReport(input: ReportRunInput): PathgradeReportBuildResult;
3
+ export declare function finiteSamplePassAtK(n: number, successes: number, k: number): number;