@wix/pathgrade 0.37.0 → 1.0.0

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 (116) hide show
  1. package/README.md +116 -32
  2. package/dist/adapter-kit/index.d.ts +17 -0
  3. package/dist/adapter-kit/index.js +14 -0
  4. package/dist/adapters/jest/index.d.ts +3 -0
  5. package/dist/adapters/jest/index.js +3 -0
  6. package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
  7. package/dist/adapters/jest/invocation-adapter.js +115 -0
  8. package/dist/adapters/jest/lifecycle.d.ts +21 -0
  9. package/dist/adapters/jest/lifecycle.js +62 -0
  10. package/dist/adapters/jest/metadata.d.ts +6 -0
  11. package/dist/adapters/jest/metadata.js +35 -0
  12. package/dist/adapters/jest/reporter.cjs +10 -0
  13. package/dist/adapters/jest/reporter.d.ts +5 -0
  14. package/dist/adapters/jest/reporter.js +67 -0
  15. package/dist/adapters/jest/results.d.ts +31 -0
  16. package/dist/adapters/jest/results.js +144 -0
  17. package/dist/adapters/jest/runner-adapter.d.ts +9 -0
  18. package/dist/adapters/jest/runner-adapter.js +49 -0
  19. package/dist/adapters/jest/setup.d.ts +1 -0
  20. package/dist/adapters/jest/setup.js +10 -0
  21. package/dist/adapters/node-test/index.d.ts +8 -0
  22. package/dist/adapters/node-test/index.js +106 -0
  23. package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
  24. package/dist/adapters/node-test/invocation-adapter.js +47 -0
  25. package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
  26. package/dist/adapters/node-test/runner-adapter.js +116 -0
  27. package/dist/adapters/vitest/index.d.ts +9 -0
  28. package/dist/adapters/vitest/index.js +82 -0
  29. package/dist/adapters/vitest/lifecycle.d.ts +24 -0
  30. package/dist/adapters/vitest/lifecycle.js +41 -0
  31. package/dist/adapters/vitest/reporter.d.ts +12 -0
  32. package/dist/adapters/vitest/reporter.js +75 -0
  33. package/dist/adapters/vitest/setup.d.ts +1 -0
  34. package/dist/adapters/vitest/setup.js +3 -0
  35. package/dist/agents/codex-app-server/agent.js +5 -1
  36. package/dist/agents/codex-app-server/protocol/index.js +3 -3
  37. package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
  38. package/dist/agents/codex-app-server/turn-completion.js +9 -0
  39. package/dist/commands/affected.js +22 -15
  40. package/dist/commands/run-args.d.ts +4 -3
  41. package/dist/commands/run-args.js +12 -6
  42. package/dist/commands/run-changed.d.ts +4 -4
  43. package/dist/commands/run-changed.js +45 -58
  44. package/dist/config/pathgrade.d.ts +48 -0
  45. package/dist/config/pathgrade.js +204 -0
  46. package/dist/pathgrade.d.ts +2 -2
  47. package/dist/pathgrade.js +35 -34
  48. package/dist/plugin/index.d.ts +2 -9
  49. package/dist/plugin/index.js +1 -81
  50. package/dist/plugin/lifecycle.d.ts +1 -45
  51. package/dist/plugin/lifecycle.js +1 -146
  52. package/dist/plugin/reporter.d.ts +1 -37
  53. package/dist/plugin/reporter.js +1 -280
  54. package/dist/plugin/setup.d.ts +1 -1
  55. package/dist/plugin/setup.js +1 -3
  56. package/dist/providers/copy-filter.d.ts +5 -0
  57. package/dist/providers/copy-filter.js +9 -0
  58. package/dist/providers/sandbox.js +2 -2
  59. package/dist/providers/workspace.d.ts +6 -0
  60. package/dist/providers/workspace.js +34 -14
  61. package/dist/reporters/diagnostics.d.ts +2 -40
  62. package/dist/reporters/diagnostics.js +1 -80
  63. package/dist/reporters/report-summary.d.ts +6 -0
  64. package/dist/reporters/report-summary.js +29 -0
  65. package/dist/reporting/artifacts.d.ts +2 -0
  66. package/dist/reporting/artifacts.js +20 -0
  67. package/dist/reporting/core.d.ts +2 -0
  68. package/dist/reporting/core.js +173 -0
  69. package/dist/reporting/types.d.ts +57 -0
  70. package/dist/reporting/types.js +1 -0
  71. package/dist/reporting/vitest-edge.d.ts +1 -0
  72. package/dist/reporting/vitest-edge.js +1 -0
  73. package/dist/runners/adapter-loader.d.ts +17 -0
  74. package/dist/runners/adapter-loader.js +67 -0
  75. package/dist/runners/adapter.d.ts +65 -0
  76. package/dist/runners/adapter.js +1 -0
  77. package/dist/runners/invocation.d.ts +10 -0
  78. package/dist/runners/invocation.js +1 -0
  79. package/dist/runners/lifecycle-hooks.d.ts +2 -0
  80. package/dist/runners/lifecycle-hooks.js +18 -0
  81. package/dist/runners/model-builders.d.ts +4 -0
  82. package/dist/runners/model-builders.js +67 -0
  83. package/dist/runners/model-validation.d.ts +12 -0
  84. package/dist/runners/model-validation.js +213 -0
  85. package/dist/runners/model.d.ts +117 -0
  86. package/dist/runners/model.js +1 -0
  87. package/dist/runners/orchestrator.d.ts +30 -0
  88. package/dist/runners/orchestrator.js +74 -0
  89. package/dist/runners/report-projection.d.ts +6 -0
  90. package/dist/runners/report-projection.js +62 -0
  91. package/dist/runners/selection.d.ts +6 -0
  92. package/dist/runners/selection.js +10 -0
  93. package/dist/runners/vitest-adapter.d.ts +8 -0
  94. package/dist/runners/vitest-adapter.js +108 -0
  95. package/dist/runners/vitest-invocation.d.ts +10 -0
  96. package/dist/runners/vitest-invocation.js +44 -0
  97. package/dist/runners/vitest-lifecycle.d.ts +43 -0
  98. package/dist/runners/vitest-lifecycle.js +150 -0
  99. package/dist/sdk/agent.js +15 -16
  100. package/dist/sdk/case-context.d.ts +20 -0
  101. package/dist/sdk/case-context.js +31 -0
  102. package/dist/sdk/diagnostics.d.ts +40 -0
  103. package/dist/sdk/diagnostics.js +80 -0
  104. package/dist/sdk/eval-runtime.d.ts +4 -0
  105. package/dist/sdk/eval-runtime.js +11 -1
  106. package/dist/sdk/evaluate.js +2 -1
  107. package/dist/sdk/index.d.ts +2 -0
  108. package/dist/sdk/index.js +1 -0
  109. package/dist/sdk/lifecycle.d.ts +40 -0
  110. package/dist/sdk/lifecycle.js +170 -0
  111. package/dist/sdk/result-capture.d.ts +18 -0
  112. package/dist/sdk/result-capture.js +49 -0
  113. package/dist/sdk/types.d.ts +1 -1
  114. package/dist/types.d.ts +2 -2
  115. package/dist/utils/llm-providers/anthropic.js +9 -2
  116. package/package.json +56 -4
@@ -1,280 +1 @@
1
- import * as path from 'path';
2
- import { execSync } from 'child_process';
3
- import { extractSkillsFromLog } from '../tool-events.js';
4
- import { fmt } from '../utils/cli.js';
5
- import { getPathgradeDir } from '../reporters/results-path.js';
6
- import fs from 'fs-extra';
7
- import { formatDiagnostics, formatDiagnosticsSummary } from '../reporters/diagnostics.js';
8
- import { readSidecar } from '../affected/sidecar.js';
9
- /**
10
- * Custom vitest reporter that layers pathgrade aggregate statistics
11
- * on top of vitest's default output.
12
- */
13
- export class PathgradeReporter {
14
- opts;
15
- constructor(opts) {
16
- this.opts = opts ?? {};
17
- }
18
- async onTestRunEnd(testModules) {
19
- const groups = this.collectGroups(testModules);
20
- if (groups.length === 0)
21
- return;
22
- const mode = this.opts.reporter ?? 'cli';
23
- if (mode === 'cli' || mode === 'browser') {
24
- this.printCliSummary(groups);
25
- }
26
- await this.writeJsonResults(groups);
27
- if (mode === 'browser') {
28
- this.openBrowserViewer(groups);
29
- }
30
- // CI threshold check
31
- if (this.opts.ci?.threshold != null) {
32
- const allScores = groups.flatMap((g) => g.tests.map((t) => t.score));
33
- if (allScores.length > 0) {
34
- const avg = allScores.reduce((a, b) => a + b, 0) / allScores.length;
35
- if (avg < this.opts.ci.threshold) {
36
- console.log(`\n ${fmt.fail('CI THRESHOLD FAILED')} avg score ${fmt.bold(avg.toFixed(3))} < threshold ${fmt.bold(String(this.opts.ci.threshold))}\n`);
37
- process.exitCode = 1;
38
- }
39
- }
40
- }
41
- }
42
- collectGroups(testModules) {
43
- const groupMap = new Map();
44
- for (const mod of testModules) {
45
- for (const testCase of mod.children.allTests()) {
46
- const groupKey = this.getGroupKey(testCase);
47
- const entry = this.toTestEntry(testCase);
48
- if (!this.isReportableEntry(entry))
49
- continue;
50
- if (!groupMap.has(groupKey)) {
51
- groupMap.set(groupKey, []);
52
- }
53
- groupMap.get(groupKey).push(entry);
54
- }
55
- }
56
- return Array.from(groupMap.entries()).map(([groupName, tests]) => ({
57
- groupName,
58
- tests,
59
- }));
60
- }
61
- /**
62
- * Group key is the parent suite's full name (file + describe hierarchy).
63
- * Tests without a describe wrapper are grouped by module path.
64
- */
65
- getGroupKey(testCase) {
66
- const parent = testCase.parent;
67
- const modulePath = testCase.module.relativeModuleId;
68
- if (parent.type === 'suite') {
69
- return `${modulePath} > ${parent.fullName}`;
70
- }
71
- return modulePath;
72
- }
73
- toTestEntry(testCase) {
74
- const meta = testCase.meta();
75
- const pathgradeMeta = meta.pathgrade;
76
- const diag = testCase.diagnostic();
77
- const result = testCase.result();
78
- let score = 0;
79
- let trial;
80
- let diagnostics;
81
- if (pathgradeMeta === undefined) {
82
- // Test didn't use pathgrade — infer from test pass/fail
83
- score = result.state === 'passed' ? 1 : 0;
84
- }
85
- else if (pathgradeMeta.length === 0) {
86
- // Anomaly: pathgrade was set up but evaluate() was never called
87
- console.warn(` [pathgrade] warning: empty results for "${testCase.name}" — evaluate() may not have been called`);
88
- score = result.state === 'passed' ? 1 : 0;
89
- }
90
- else {
91
- // Use the last eval result's score
92
- score = pathgradeMeta[pathgradeMeta.length - 1].score;
93
- trial = pathgradeMeta[pathgradeMeta.length - 1].trial;
94
- diagnostics = pathgradeMeta[pathgradeMeta.length - 1].diagnostics;
95
- }
96
- return {
97
- name: testCase.name,
98
- score,
99
- durationMs: diag?.duration ?? 0,
100
- state: result.state === 'pending' ? 'pending' : result.state,
101
- trial,
102
- diagnostics,
103
- };
104
- }
105
- isReportableEntry(entry) {
106
- return entry.state !== 'skipped' && entry.state !== 'pending';
107
- }
108
- printCliSummary(groups) {
109
- console.log(`\n${fmt.bold('── pathgrade summary ')}${fmt.dim('─'.repeat(40))}\n`);
110
- const forceVerbose = this.opts.diagnostics === true || process.env.PATHGRADE_DIAGNOSTICS === '1';
111
- const currentTimeoutMs = this.opts.timeout != null ? this.opts.timeout * 1000 : undefined;
112
- for (const group of groups) {
113
- const k = group.tests.length;
114
- const passRate = this.computePassRate(group.tests);
115
- const passAtK = this.computePassAtK(passRate, k);
116
- const passPowK = this.computePassPowK(passRate, k);
117
- const avgDuration = this.computeAvgDuration(group.tests);
118
- const prColor = passRate >= 0.5 ? fmt.green : fmt.red;
119
- console.log(` ${fmt.bold(group.groupName)}`);
120
- console.log(` ${fmt.dim('pass rate'.padEnd(12))} ${prColor((passRate * 100).toFixed(1) + '%')}`);
121
- console.log(` ${fmt.dim(`pass@${k}`.padEnd(12))} ${(passAtK * 100).toFixed(1)}%`);
122
- console.log(` ${fmt.dim(`pass^${k}`.padEnd(12))} ${(passPowK * 100).toFixed(1)}%`);
123
- console.log(` ${fmt.dim('avg time'.padEnd(12))} ${(avgDuration / 1000).toFixed(1)}s`);
124
- console.log(` ${fmt.dim('trials'.padEnd(12))} ${k}`);
125
- console.log();
126
- for (const test of group.tests) {
127
- if (!test.diagnostics)
128
- continue;
129
- const shouldPrintFull = forceVerbose || test.state !== 'passed' || test.diagnostics.completionReason === 'timeout' || test.diagnostics.completionReason === 'agent_crashed';
130
- console.log(` ${fmt.bold(test.name)}`);
131
- const formatted = shouldPrintFull
132
- ? formatDiagnostics(test.diagnostics, { verbose: true, currentTimeoutMs })
133
- : formatDiagnosticsSummary(test.diagnostics);
134
- for (const line of formatted.split('\n')) {
135
- console.log(` ${line}`);
136
- }
137
- console.log();
138
- }
139
- }
140
- }
141
- async writeJsonResults(groups) {
142
- const outputDir = getPathgradeDir(process.cwd());
143
- const tracesDir = path.join(outputDir, 'traces');
144
- await fs.ensureDir(tracesDir);
145
- // Auto-create .gitignore if it doesn't exist
146
- const gitignorePath = path.join(outputDir, '.gitignore');
147
- if (!(await fs.pathExists(gitignorePath))) {
148
- await fs.writeFile(gitignorePath, '*\n');
149
- }
150
- const allScores = groups.flatMap((g) => g.tests.map((t) => t.score));
151
- const overallPassRate = allScores.length > 0
152
- ? allScores.reduce((a, b) => a + b, 0) / allScores.length
153
- : 0;
154
- const threshold = this.opts.ci?.threshold;
155
- const status = threshold != null
156
- ? (overallPassRate >= threshold ? 'pass' : 'fail')
157
- : (groups.every(g => g.tests.every(t => t.state === 'passed')) ? 'pass' : 'fail');
158
- const consolidatedGroups = [];
159
- for (const group of groups) {
160
- const report = this.buildEvalReport(group);
161
- const slug = this.slug(group.groupName);
162
- const traceFile = `traces/${slug}.json`;
163
- // Write full trace data (with session_log and conversation)
164
- await fs.writeJson(path.join(outputDir, traceFile), report.trials, { spaces: 2 });
165
- // Strip session_log and conversation from trials in consolidated report
166
- const strippedTrials = report.trials.map(t => {
167
- const { session_log, conversation, ...rest } = t;
168
- return rest;
169
- });
170
- const { trials: _omitted, ...rest } = report;
171
- consolidatedGroups.push({
172
- ...rest,
173
- trials: strippedTrials,
174
- trace_file: traceFile,
175
- });
176
- }
177
- // Merge selection metadata from the CLI-written sidecar, if any.
178
- // Missing sidecar → plain run; malformed → warn and proceed without it.
179
- const selection = await readSidecar(process.cwd(), msg => {
180
- console.warn(`[pathgrade] ${msg}`);
181
- });
182
- const consolidated = {
183
- version: 1,
184
- timestamp: new Date().toISOString(),
185
- ...(threshold != null ? { threshold } : {}),
186
- overall_pass_rate: overallPassRate,
187
- status,
188
- groups: consolidatedGroups,
189
- ...(selection ? { selection } : {}),
190
- };
191
- await fs.writeJson(path.join(outputDir, 'results.json'), consolidated, { spaces: 2 });
192
- console.log(`\n ${fmt.dim('Results written to')} ${outputDir}\n`);
193
- }
194
- buildEvalReport(group) {
195
- const trials = group.tests.map((test, index) => this.toTrialResult(test, index));
196
- const passRate = this.computePassRate(group.tests);
197
- // Aggregate skills_used across all trials (deduplicated)
198
- const allSkills = new Set();
199
- for (const trial of trials) {
200
- for (const skill of trial.skills_used ?? []) {
201
- allSkills.add(skill);
202
- }
203
- }
204
- return {
205
- task: group.groupName,
206
- pass_rate: passRate,
207
- pass_at_k: this.computePassAtK(passRate, trials.length),
208
- pass_pow_k: this.computePassPowK(passRate, trials.length),
209
- trials,
210
- skills_used: [...allSkills],
211
- };
212
- }
213
- toTrialResult(test, index) {
214
- const base = test.trial ?? {
215
- trial_id: index + 1,
216
- reward: test.score,
217
- scorer_results: [],
218
- duration_ms: test.durationMs,
219
- n_commands: 0,
220
- input_tokens: 0,
221
- output_tokens: 0,
222
- session_log: [],
223
- };
224
- // Extract skills_used from session_log tool events if not already present
225
- const skills = base.skills_used ?? extractSkillsFromLog(base.session_log);
226
- return {
227
- ...base,
228
- trial_id: index + 1,
229
- name: test.name,
230
- duration_ms: base.duration_ms || test.durationMs,
231
- diagnostics: test.diagnostics ?? base.diagnostics,
232
- ...(skills.length > 0 ? { skills_used: skills } : {}),
233
- };
234
- }
235
- slug(value) {
236
- return value
237
- .toLowerCase()
238
- .replace(/[^a-z0-9]+/g, '-')
239
- .replace(/^-+|-+$/g, '') || 'report';
240
- }
241
- openBrowserViewer(_groups) {
242
- // Write results JSON first, then open the viewer
243
- const viewerPath = path.resolve(import.meta.dirname, '..', 'viewer.html');
244
- try {
245
- const openCmd = process.platform === 'darwin' ? 'open'
246
- : process.platform === 'win32' ? 'start'
247
- : 'xdg-open';
248
- execSync(`${openCmd} "${viewerPath}"`, { stdio: 'ignore' });
249
- console.log(`\n ${fmt.dim('Opened viewer in browser')}\n`);
250
- }
251
- catch {
252
- console.log(`\n ${fmt.dim('Open manually:')} ${viewerPath}\n`);
253
- }
254
- }
255
- computePassRate(tests) {
256
- if (tests.length === 0)
257
- return 0;
258
- const passed = tests.filter((t) => t.state === 'passed').length;
259
- return passed / tests.length;
260
- }
261
- /**
262
- * pass@k = 1 - (1 - p)^k
263
- * Probability of at least 1 success in k trials.
264
- */
265
- computePassAtK(passRate, k) {
266
- return 1 - Math.pow(1 - passRate, k);
267
- }
268
- /**
269
- * pass^k = p^k
270
- * Probability of all k trials succeeding.
271
- */
272
- computePassPowK(passRate, k) {
273
- return Math.pow(passRate, k);
274
- }
275
- computeAvgDuration(tests) {
276
- if (tests.length === 0)
277
- return 0;
278
- return tests.reduce((sum, t) => sum + t.durationMs, 0) / tests.length;
279
- }
280
- }
1
+ export { PathgradeReporter } from '../adapters/vitest/reporter.js';
@@ -1 +1 @@
1
- export {};
1
+ import '../adapters/vitest/setup.js';
@@ -1,3 +1 @@
1
- import { afterAll, afterEach } from 'vitest';
2
- import { lifecycle } from './lifecycle.js';
3
- lifecycle.install(afterEach, afterAll);
1
+ import '../adapters/vitest/setup.js';
@@ -16,3 +16,8 @@ export declare const DEFAULT_COPY_IGNORE: readonly string[];
16
16
  * Pass `[]` to disable filtering. Invalid glob patterns are silently ignored.
17
17
  */
18
18
  export declare function createCopyFilter(ignorePatterns: readonly string[]): (src: string) => boolean;
19
+ /**
20
+ * fs-extra cannot copy sockets, FIFOs, or device files. Host config caches can
21
+ * contain transient sockets, so filter them out before copy() reaches them.
22
+ */
23
+ export declare function isPortableCopyEntry(src: string): boolean;
@@ -5,6 +5,7 @@
5
5
  * when copying workspace/skill directories into the sandbox.
6
6
  */
7
7
  import * as path from 'path';
8
+ import * as fs from 'fs';
8
9
  import picomatch from 'picomatch';
9
10
  /**
10
11
  * Default ignore patterns applied when copying workspace and skill directories.
@@ -64,3 +65,11 @@ export function createCopyFilter(ignorePatterns) {
64
65
  return true;
65
66
  };
66
67
  }
68
+ /**
69
+ * fs-extra cannot copy sockets, FIFOs, or device files. Host config caches can
70
+ * contain transient sockets, so filter them out before copy() reaches them.
71
+ */
72
+ export function isPortableCopyEntry(src) {
73
+ const stat = fs.lstatSync(src);
74
+ return stat.isFile() || stat.isDirectory() || stat.isSymbolicLink();
75
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import * as path from 'path';
3
3
  import * as os from 'os';
4
- import { DEFAULT_COPY_IGNORE, createCopyFilter } from './copy-filter.js';
4
+ import { DEFAULT_COPY_IGNORE, createCopyFilter, isPortableCopyEntry } from './copy-filter.js';
5
5
  export const SAFE_HOST_VARS = [
6
6
  'PATH', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'USER', 'LOGNAME',
7
7
  ];
@@ -56,7 +56,7 @@ export async function createSandbox(spec) {
56
56
  if (await fs.pathExists(srcPath)) {
57
57
  const destPath = path.join(homePath, relPath);
58
58
  await fs.ensureDir(path.dirname(destPath));
59
- await fs.copy(srcPath, destPath);
59
+ await fs.copy(srcPath, destPath, { filter: isPortableCopyEntry });
60
60
  }
61
61
  }
62
62
  }
@@ -11,5 +11,11 @@ export interface Workspace {
11
11
  }): Promise<CommandResult>;
12
12
  dispose(): Promise<void>;
13
13
  }
14
+ interface RemoveSandboxRootOptions {
15
+ remove?: (target: string) => Promise<void>;
16
+ sleep?: (ms: number) => Promise<void>;
17
+ retryDelaysMs?: readonly number[];
18
+ }
19
+ export declare function removeSandboxRoot(rootDir: string, opts?: RemoveSandboxRootOptions): Promise<void>;
14
20
  export declare function linkPathsFromHostHome(pathsToLink: string[], sandboxHomePath: string): Promise<void>;
15
21
  export declare function prepareWorkspace(spec: SandboxConfig): Promise<Workspace>;
@@ -5,6 +5,38 @@ import { createSandbox } from './sandbox.js';
5
5
  import { stageMcpConfig } from './mcp-config.js';
6
6
  import { sandboxExec } from './sandbox-exec.js';
7
7
  import { resolveCredentials } from './credentials.js';
8
+ import { isPortableCopyEntry } from './copy-filter.js';
9
+ const SANDBOX_REMOVE_RETRY_DELAYS_MS = [
10
+ 50,
11
+ 100,
12
+ 250,
13
+ 500,
14
+ 1_000,
15
+ 2_000,
16
+ 3_000,
17
+ 5_000,
18
+ ];
19
+ function isRetryableRemoveError(error) {
20
+ const code = error.code;
21
+ return code === 'ENOTEMPTY' || code === 'EBUSY' || code === 'EPERM';
22
+ }
23
+ export async function removeSandboxRoot(rootDir, opts = {}) {
24
+ const remove = opts.remove ?? ((target) => fs.remove(target));
25
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
26
+ const retryDelaysMs = opts.retryDelaysMs ?? SANDBOX_REMOVE_RETRY_DELAYS_MS;
27
+ for (let attempt = 0;; attempt++) {
28
+ try {
29
+ await remove(rootDir);
30
+ return;
31
+ }
32
+ catch (error) {
33
+ if (!isRetryableRemoveError(error) || attempt >= retryDelaysMs.length) {
34
+ throw error;
35
+ }
36
+ await sleep(retryDelaysMs[attempt]);
37
+ }
38
+ }
39
+ }
8
40
  async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
9
41
  const realHome = os.homedir();
10
42
  for (const relPath of pathsToCopy) {
@@ -13,7 +45,7 @@ async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
13
45
  continue;
14
46
  const destPath = path.join(sandboxHomePath, relPath);
15
47
  await fs.ensureDir(path.dirname(destPath));
16
- await fs.copy(srcPath, destPath);
48
+ await fs.copy(srcPath, destPath, { filter: isPortableCopyEntry });
17
49
  }
18
50
  }
19
51
  export async function linkPathsFromHostHome(pathsToLink, sandboxHomePath) {
@@ -50,19 +82,7 @@ export async function prepareWorkspace(spec) {
50
82
  if (disposed)
51
83
  return;
52
84
  disposed = true;
53
- for (let attempt = 0; attempt < 4; attempt++) {
54
- try {
55
- await fs.remove(rootDir);
56
- return;
57
- }
58
- catch (error) {
59
- const code = error.code;
60
- const retryable = code === 'ENOTEMPTY' || code === 'EBUSY' || code === 'EPERM';
61
- if (!retryable || attempt === 3)
62
- throw error;
63
- await new Promise((r) => setTimeout(r, 50 * (attempt + 1)));
64
- }
65
- }
85
+ await removeSandboxRoot(rootDir);
66
86
  },
67
87
  };
68
88
  }
@@ -1,46 +1,8 @@
1
- import type { LogEntry } from '../types.js';
2
- import type { ReactionFiredEntry, TurnDetail } from '../sdk/types.js';
3
- import { type RuntimePolicyAuditEntry } from '../sdk/runtime-policy.js';
4
- export interface DiagnosticsScorer {
5
- name: string;
6
- type: string;
7
- score: number;
8
- weight: number;
9
- details?: string;
10
- status?: 'ok' | 'error' | 'skipped';
11
- }
12
- export interface DiagnosticsTurnDetail extends TurnDetail {
13
- apiRetries: number;
14
- }
15
- export type DiagnosticsRuntimePolicyEntry = RuntimePolicyAuditEntry;
16
- export interface DiagnosticsReport {
17
- turns: number;
18
- totalDurationMs: number;
19
- completionReason?: string;
20
- completionDetail?: string;
21
- score?: number;
22
- turnDetails: DiagnosticsTurnDetail[];
23
- reactionsFired: ReactionFiredEntry[];
24
- runtimePoliciesApplied: DiagnosticsRuntimePolicyEntry[];
25
- recommendedTimeoutMs: number;
26
- warnings: string[];
27
- scorers: Array<DiagnosticsScorer & {
28
- status: 'ok' | 'error' | 'skipped';
29
- }>;
30
- }
31
- export interface BuildDiagnosticsReportInput {
32
- completionReason?: string;
33
- completionDetail?: string;
34
- score?: number;
35
- turnDetails?: TurnDetail[];
36
- reactionsFired?: ReactionFiredEntry[];
37
- scorers?: DiagnosticsScorer[];
38
- log: LogEntry[];
39
- }
1
+ import type { DiagnosticsReport } from '../sdk/diagnostics.js';
2
+ export { buildDiagnosticsReport } from '../sdk/diagnostics.js';
40
3
  export interface FormatDiagnosticsOptions {
41
4
  verbose?: boolean;
42
5
  currentTimeoutMs?: number;
43
6
  }
44
- export declare function buildDiagnosticsReport(input: BuildDiagnosticsReportInput): DiagnosticsReport;
45
7
  export declare function formatDiagnosticsSummary(report: DiagnosticsReport): string;
46
8
  export declare function formatDiagnostics(report: DiagnosticsReport, opts?: FormatDiagnosticsOptions): string;
@@ -1,35 +1,4 @@
1
- import { extractRuntimePolicyAuditEntries } from '../sdk/runtime-policy.js';
2
- export function buildDiagnosticsReport(input) {
3
- const baseTurnDetails = (input.turnDetails && input.turnDetails.length > 0)
4
- ? [...input.turnDetails]
5
- : deriveTurnDetailsFromLog(input.log);
6
- const retriesByTurn = countRetriesByTurn(input.log);
7
- const turnDetails = baseTurnDetails
8
- .sort((a, b) => a.turn - b.turn)
9
- .map((detail) => ({
10
- ...detail,
11
- apiRetries: retriesByTurn.get(detail.turn) ?? 0,
12
- }));
13
- const warnings = turnDetails
14
- .filter((detail) => detail.outputLines > 500)
15
- .map((detail) => `Turn ${detail.turn}: output exceeded 500 lines (${detail.outputLines} lines)`);
16
- return {
17
- turns: turnDetails.length,
18
- totalDurationMs: turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0),
19
- completionReason: input.completionReason,
20
- completionDetail: input.completionDetail,
21
- score: input.score,
22
- turnDetails,
23
- reactionsFired: input.reactionsFired ?? [],
24
- runtimePoliciesApplied: extractRuntimePolicyAuditEntries(input.log),
25
- recommendedTimeoutMs: recommendTimeoutMs(turnDetails),
26
- warnings,
27
- scorers: (input.scorers ?? []).map((scorer) => ({
28
- ...scorer,
29
- status: scorer.status ?? 'ok',
30
- })),
31
- };
32
- }
1
+ export { buildDiagnosticsReport } from '../sdk/diagnostics.js';
33
2
  export function formatDiagnosticsSummary(report) {
34
3
  const turnsLabel = report.turns === 1 ? '1 turn' : `${report.turns} turns`;
35
4
  const durationLabel = `${(report.totalDurationMs / 1000).toFixed(1)}s`;
@@ -84,54 +53,6 @@ export function formatDiagnostics(report, opts = {}) {
84
53
  lines.push('', formatTimeoutRecommendation(report.recommendedTimeoutMs, opts.currentTimeoutMs));
85
54
  return lines.join('\n');
86
55
  }
87
- function deriveTurnDetailsFromLog(log) {
88
- return log
89
- .filter((entry) => entry.type === 'agent_result'
90
- && entry.synthetic_blocked_prompt !== true
91
- && typeof entry.turn_number === 'number'
92
- && typeof entry.duration_ms === 'number'
93
- && typeof entry.output_lines === 'number'
94
- && typeof entry.output_chars === 'number')
95
- .map((entry) => ({
96
- turn: entry.turn_number,
97
- durationMs: entry.duration_ms,
98
- outputLines: entry.output_lines,
99
- outputChars: entry.output_chars,
100
- }));
101
- }
102
- function countRetriesByTurn(log) {
103
- const retriesByTurn = new Map();
104
- let pendingRetries = 0;
105
- for (const entry of log) {
106
- if (isRetryEntry(entry)) {
107
- pendingRetries++;
108
- continue;
109
- }
110
- if (entry.type === 'agent_result' && typeof entry.turn_number === 'number') {
111
- if (pendingRetries > 0) {
112
- retriesByTurn.set(entry.turn_number, (retriesByTurn.get(entry.turn_number) ?? 0) + pendingRetries);
113
- pendingRetries = 0;
114
- }
115
- if (!retriesByTurn.has(entry.turn_number)) {
116
- retriesByTurn.set(entry.turn_number, 0);
117
- }
118
- }
119
- }
120
- return retriesByTurn;
121
- }
122
- function isRetryEntry(entry) {
123
- if (entry.type !== 'agent_result')
124
- return false;
125
- const text = `${entry.assistant_message ?? ''} ${entry.output ?? ''}`.toLowerCase();
126
- return text.includes('retry') || text.includes('api_retry');
127
- }
128
- function recommendTimeoutMs(turnDetails) {
129
- if (turnDetails.length <= 1) {
130
- return (turnDetails[0]?.durationMs ?? 0) + 30_000;
131
- }
132
- const totalDurationMs = turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0);
133
- return Math.max(totalDurationMs + 200_000, 30_000);
134
- }
135
56
  function formatTimeoutRecommendation(recommendedTimeoutMs, currentTimeoutMs) {
136
57
  const recommendedSeconds = Math.round(recommendedTimeoutMs / 1000);
137
58
  if (currentTimeoutMs == null) {
@@ -0,0 +1,6 @@
1
+ import type { ReportSummaryGroup } from '../reporting/types.js';
2
+ export interface PrintReportSummaryOptions {
3
+ forceVerbose?: boolean;
4
+ currentTimeoutMs?: number;
5
+ }
6
+ export declare function printReportSummary(groups: ReportSummaryGroup[], opts?: PrintReportSummaryOptions): void;
@@ -0,0 +1,29 @@
1
+ import { fmt } from '../utils/cli.js';
2
+ import { formatDiagnostics, formatDiagnosticsSummary } from './diagnostics.js';
3
+ export function printReportSummary(groups, opts = {}) {
4
+ console.log(`\n${fmt.bold('── pathgrade summary ')}${fmt.dim('─'.repeat(40))}\n`);
5
+ for (const group of groups) {
6
+ const prColor = group.pass_rate >= 0.5 ? fmt.green : fmt.red;
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)}%`);
11
+ 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}`);
13
+ console.log();
14
+ for (const diagnostic of group.diagnostics) {
15
+ const shouldPrintFull = opts.forceVerbose
16
+ || diagnostic.state !== 'passed'
17
+ || diagnostic.report.completionReason === 'timeout'
18
+ || diagnostic.report.completionReason === 'agent_crashed';
19
+ console.log(` ${fmt.bold(diagnostic.caseName)}`);
20
+ const formatted = shouldPrintFull
21
+ ? formatDiagnostics(diagnostic.report, { verbose: true, currentTimeoutMs: opts.currentTimeoutMs })
22
+ : formatDiagnosticsSummary(diagnostic.report);
23
+ for (const line of formatted.split('\n')) {
24
+ console.log(` ${line}`);
25
+ }
26
+ console.log();
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { ArtifactWriteResult, PathgradeReportBuildResult } from './types.js';
2
+ export declare function writePathgradeArtifacts(artifactRoot: string, built: PathgradeReportBuildResult): Promise<ArtifactWriteResult>;
@@ -0,0 +1,20 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ export async function writePathgradeArtifacts(artifactRoot, built) {
4
+ await fs.ensureDir(path.join(artifactRoot, 'traces'));
5
+ const gitignorePath = path.join(artifactRoot, '.gitignore');
6
+ if (!(await fs.pathExists(gitignorePath))) {
7
+ await fs.writeFile(gitignorePath, '*\n');
8
+ }
9
+ const traceFiles = [];
10
+ for (const trace of built.traces) {
11
+ await fs.writeJson(path.join(artifactRoot, trace.traceFile), trace.trials, { spaces: 2 });
12
+ traceFiles.push(trace.traceFile);
13
+ }
14
+ const resultsPath = path.join(artifactRoot, 'results.json');
15
+ await fs.writeJson(resultsPath, built.report, { spaces: 2 });
16
+ return {
17
+ resultsPath,
18
+ traceFiles,
19
+ };
20
+ }
@@ -0,0 +1,2 @@
1
+ import type { PathgradeReportBuildResult, ReportRunInput } from './types.js';
2
+ export declare function buildPathgradeReport(input: ReportRunInput): PathgradeReportBuildResult;