@wix/pathgrade 1.0.25 → 1.0.27

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 (89) hide show
  1. package/README.md +44 -2
  2. package/dist/agents/claude/sdk-message-projector.js +5 -0
  3. package/dist/agents/claude/sdk-options.d.ts +1 -2
  4. package/dist/agents/claude.js +2 -1
  5. package/dist/agents/codex-app-server/agent.js +89 -52
  6. package/dist/agents/codex-app-server/scenario-mount.d.ts +40 -0
  7. package/dist/agents/codex-app-server/scenario-mount.js +238 -0
  8. package/dist/agents/codex.js +1 -0
  9. package/dist/agents/cursor-scenario.d.ts +20 -0
  10. package/dist/agents/cursor-scenario.js +96 -0
  11. package/dist/agents/cursor.js +21 -0
  12. package/dist/agents/opencode/scenario.d.ts +25 -0
  13. package/dist/agents/opencode/scenario.js +195 -0
  14. package/dist/agents/opencode.js +54 -2
  15. package/dist/commands/report.d.ts +10 -2
  16. package/dist/commands/report.js +25 -4
  17. package/dist/internal/direct-mcp-v2/acp-author-projector.d.ts +20 -0
  18. package/dist/internal/direct-mcp-v2/acp-author-projector.js +147 -0
  19. package/dist/internal/direct-mcp-v2/acp-author-session.d.ts +39 -0
  20. package/dist/internal/direct-mcp-v2/acp-author-session.js +201 -0
  21. package/dist/internal/direct-mcp-v2/acp-client.d.ts +70 -0
  22. package/dist/internal/direct-mcp-v2/acp-client.js +310 -0
  23. package/dist/internal/direct-mcp-v2/acp-direct-mcp.d.ts +38 -0
  24. package/dist/internal/direct-mcp-v2/acp-direct-mcp.js +326 -0
  25. package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +68 -0
  26. package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +456 -0
  27. package/dist/internal/direct-mcp-v2/claude-profile.d.ts +9 -0
  28. package/dist/internal/direct-mcp-v2/claude-profile.js +97 -0
  29. package/dist/internal/direct-mcp-v2/cleanup.d.ts +62 -0
  30. package/dist/internal/direct-mcp-v2/cleanup.js +182 -0
  31. package/dist/internal/direct-mcp-v2/codex-direct-mcp.d.ts +41 -0
  32. package/dist/internal/direct-mcp-v2/codex-direct-mcp.js +436 -0
  33. package/dist/internal/direct-mcp-v2/codex-profile.d.ts +12 -0
  34. package/dist/internal/direct-mcp-v2/codex-profile.js +81 -0
  35. package/dist/internal/direct-mcp-v2/compiler.d.ts +12 -0
  36. package/dist/internal/direct-mcp-v2/compiler.js +449 -0
  37. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.d.ts +14 -0
  38. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.js +60 -0
  39. package/dist/internal/direct-mcp-v2/cursor-profile.d.ts +38 -0
  40. package/dist/internal/direct-mcp-v2/cursor-profile.js +79 -0
  41. package/dist/internal/direct-mcp-v2/evidence.d.ts +50 -0
  42. package/dist/internal/direct-mcp-v2/evidence.js +147 -0
  43. package/dist/internal/direct-mcp-v2/host-reducer.d.ts +61 -0
  44. package/dist/internal/direct-mcp-v2/host-reducer.js +214 -0
  45. package/dist/internal/direct-mcp-v2/json.d.ts +9 -0
  46. package/dist/internal/direct-mcp-v2/json.js +220 -0
  47. package/dist/internal/direct-mcp-v2/materialize.d.ts +39 -0
  48. package/dist/internal/direct-mcp-v2/materialize.js +22 -0
  49. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.d.ts +14 -0
  50. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.js +71 -0
  51. package/dist/internal/direct-mcp-v2/opencode-profile.d.ts +8 -0
  52. package/dist/internal/direct-mcp-v2/opencode-profile.js +52 -0
  53. package/dist/internal/direct-mcp-v2/projection.d.ts +40 -0
  54. package/dist/internal/direct-mcp-v2/projection.js +46 -0
  55. package/dist/internal/direct-mcp-v2/public-scenario-runtime.d.ts +51 -0
  56. package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +353 -0
  57. package/dist/internal/direct-mcp-v2/runtime-lock.d.ts +51 -0
  58. package/dist/internal/direct-mcp-v2/runtime-lock.js +203 -0
  59. package/dist/internal/direct-mcp-v2/runtime-profile.d.ts +28 -0
  60. package/dist/internal/direct-mcp-v2/runtime-profile.js +163 -0
  61. package/dist/internal/direct-mcp-v2/scenario-http-host.d.ts +24 -0
  62. package/dist/internal/direct-mcp-v2/scenario-http-host.js +254 -0
  63. package/dist/internal/direct-mcp-v2/types.d.ts +233 -0
  64. package/dist/internal/direct-mcp-v2/types.js +1 -0
  65. package/dist/pathgrade.js +22 -1
  66. package/dist/reporters/github-comment.d.ts +12 -3
  67. package/dist/reporters/github-comment.js +80 -9
  68. package/dist/reporting/artifacts.js +5 -2
  69. package/dist/sdk/agent.js +56 -4
  70. package/dist/sdk/evaluate.d.ts +2 -0
  71. package/dist/sdk/evaluate.js +20 -12
  72. package/dist/sdk/index.d.ts +6 -0
  73. package/dist/sdk/index.js +4 -0
  74. package/dist/sdk/lifecycle.js +8 -3
  75. package/dist/sdk/managed-session.d.ts +6 -0
  76. package/dist/sdk/managed-session.js +157 -50
  77. package/dist/sdk/scenario-evidence.d.ts +27 -0
  78. package/dist/sdk/scenario-evidence.js +94 -0
  79. package/dist/sdk/scenario-machine-v2.d.ts +9 -0
  80. package/dist/sdk/scenario-machine-v2.js +37 -0
  81. package/dist/sdk/snapshots.d.ts +3 -0
  82. package/dist/sdk/snapshots.js +2 -0
  83. package/dist/sdk/types.d.ts +11 -1
  84. package/dist/tool-event-results.d.ts +1 -1
  85. package/dist/tool-event-results.js +2 -1
  86. package/dist/tool-events.d.ts +5 -0
  87. package/dist/tool-events.js +5 -0
  88. package/dist/types.d.ts +8 -0
  89. package/package.json +2 -2
package/dist/pathgrade.js CHANGED
@@ -156,10 +156,27 @@ export async function runPathgradeCli(options = {}) {
156
156
  const reportArgs = args.slice(1);
157
157
  const resultsPathFlag = reportArgs.find(a => a.startsWith('--results-path='));
158
158
  const commentIdFlag = reportArgs.find(a => a.startsWith('--comment-id='));
159
+ const detailsUrlFlag = reportArgs.find(a => a.startsWith('--details-url='));
160
+ const noticeFlag = reportArgs.find(a => a.startsWith('--notice='));
161
+ const expectedHeadShaFlag = reportArgs.find(a => a.startsWith('--expected-head-sha='));
159
162
  const noComment = reportArgs.includes('--no-comment');
163
+ const strict = reportArgs.includes('--strict');
160
164
  const resultsPath = resultsPathFlag ? resultsPathFlag.split('=').slice(1).join('=') : undefined;
161
165
  const commentId = commentIdFlag ? commentIdFlag.split('=').slice(1).join('=') : undefined;
162
- await runReport(process.cwd(), { resultsPath, commentId, noComment });
166
+ const detailsUrl = detailsUrlFlag ? detailsUrlFlag.split('=').slice(1).join('=') : undefined;
167
+ const notice = noticeFlag ? noticeFlag.split('=').slice(1).join('=') : undefined;
168
+ const expectedHeadSha = expectedHeadShaFlag
169
+ ? expectedHeadShaFlag.split('=').slice(1).join('=')
170
+ : undefined;
171
+ await runReport(process.cwd(), {
172
+ resultsPath,
173
+ commentId,
174
+ detailsUrl,
175
+ notice,
176
+ expectedHeadSha,
177
+ noComment,
178
+ strict,
179
+ });
163
180
  return;
164
181
  }
165
182
  if (command === 'run' || !command || command.startsWith('-')) {
@@ -234,6 +251,10 @@ function printHelp(cliName) {
234
251
  [--results-path=<path>] Override results.json location
235
252
  [--no-comment] Print markdown to stdout; do not post
236
253
  [--comment-id=<id>] Override comment marker (default: $GITHUB_WORKFLOW:$GITHUB_JOB)
254
+ [--details-url=<url>] Add a provider-neutral full-report link
255
+ [--notice=<text>] Add a provider-neutral report status notice
256
+ [--expected-head-sha=<sha>] Skip stale PR comment updates
257
+ [--strict] Fail on missing context, results, or GitHub errors
237
258
  ${cliName} affected Print eval files affected by a change-set (one per line)
238
259
  [--since=<ref>] Diff <ref>...HEAD (overrides git auto-detection)
239
260
  [--changed-files=<path>] Newline-delimited repo-relative file list
@@ -10,6 +10,10 @@
10
10
  import type { PathgradeReport, PathgradeSelectionReport } from '../types.js';
11
11
  export interface FormatOptions {
12
12
  commentId: string;
13
+ /** Provider-neutral URL for a richer external report. */
14
+ detailsUrl?: string;
15
+ /** Provider-neutral status notice rendered above report details. */
16
+ notice?: string;
13
17
  }
14
18
  /** Minimal body posted when `.pathgrade/results.json` is missing. */
15
19
  export declare const MISSING_RESULTS_BODY = "Pathgrade evals did not produce results. Check the workflow logs.";
@@ -55,14 +59,19 @@ export interface PostOptions {
55
59
  commentId: string;
56
60
  /** Body WITHOUT the marker — the marker is prepended if not already present. */
57
61
  body: string;
62
+ /** Throw instead of swallowing GitHub API failures. */
63
+ strict?: boolean;
64
+ /** Skip the update if the pull request has advanced to another head. */
65
+ expectedHeadSha?: string;
58
66
  }
67
+ export type PostResult = 'created' | 'updated' | 'stale';
59
68
  /**
60
69
  * Find an existing PR comment carrying `<!-- pathgrade:${commentId} -->`
61
- * and update it; otherwise create a new one. Swallows all errors (logs
62
- * to stderr) `pathgrade report` must never fail CI.
70
+ * and update it; otherwise create a new one. Default mode logs and swallows
71
+ * transport errors; strict mode rethrows them for an owning orchestrator.
63
72
  *
64
73
  * Bodies longer than GitHub's 65,536-character limit are truncated with a
65
74
  * sentinel pointing to workflow artifacts, preserving the leading dedup
66
75
  * marker so subsequent runs still find and update this comment.
67
76
  */
68
- export declare function postOrUpdateComment(ctx: PrContext, opts: PostOptions): Promise<void>;
77
+ export declare function postOrUpdateComment(ctx: PrContext, opts: PostOptions): Promise<PostResult | undefined>;
@@ -26,6 +26,12 @@ export function formatNoAffectedEvalsMarkdown(selection) {
26
26
  return lines.join('\n');
27
27
  }
28
28
  export function commentMarker(commentId) {
29
+ if (commentId.length === 0
30
+ || commentId.length > 128
31
+ || /[<>\r\n]/.test(commentId)
32
+ || commentId.includes('--')) {
33
+ throw new Error('Pathgrade comment id must be 1-128 characters and cannot contain HTML comment delimiters');
34
+ }
29
35
  return `<!-- pathgrade:${commentId} -->`;
30
36
  }
31
37
  function pct(n) {
@@ -59,9 +65,19 @@ export function formatReportMarkdown(report, opts) {
59
65
  lines.push('');
60
66
  lines.push(`### ${icon} Pathgrade report`);
61
67
  lines.push('');
68
+ lines.push(`**Last run:** \`${report.timestamp}\``);
69
+ lines.push('');
62
70
  lines.push(`**Pass rate:** ${pct(p)} | ` +
63
71
  `**pass@${totalTrials}:** ${pct(overallPassAtK)} | ` +
64
72
  `**pass^${totalTrials}:** ${pct(overallPassPowK)}`);
73
+ if (opts.detailsUrl) {
74
+ lines.push('');
75
+ lines.push(`**Details:** [Open the full report](${formatLinkDestination(opts.detailsUrl)})`);
76
+ }
77
+ if (opts.notice) {
78
+ lines.push('');
79
+ lines.push(`> ${opts.notice.replace(/[\r\n]+/g, ' ').trim()}`);
80
+ }
65
81
  if (report.threshold != null) {
66
82
  lines.push('');
67
83
  lines.push(`Threshold: ${pct(report.threshold)} — ${report.status.toUpperCase()}`);
@@ -100,14 +116,14 @@ export function formatSelectionSection(selection) {
100
116
  out.push('### Selection');
101
117
  const total = selection.selected.length + selection.skipped.length;
102
118
  if (selection.global_match) {
103
- out.push(`Ran **all ${total}** evals — global trigger \`${selection.global_match}\` matched.`);
119
+ out.push(`Selected **all ${total}** eval files — global trigger \`${selection.global_match}\` matched.`);
104
120
  return out.join('\n');
105
121
  }
106
122
  if (selection.skipped.length === 0 && selection.selected.length > 0) {
107
- out.push(`Ran **all ${total}** evals — every eval had a matching change.`);
123
+ out.push(`Selected **all ${total}** eval files — every eval had a matching change.`);
108
124
  return out.join('\n');
109
125
  }
110
- out.push(`Ran **${selection.selected.length} of ${total}** evals based on changes vs \`${selection.base_ref}\`.`);
126
+ out.push(`Selected **${selection.selected.length} of ${total}** eval files based on changes vs \`${selection.base_ref}\`.`);
111
127
  if (selection.skipped.length > 0) {
112
128
  out.push('');
113
129
  out.push(`<details><summary>${selection.skipped.length} skipped (unaffected)</summary>`);
@@ -171,13 +187,36 @@ function apiHeaders(token) {
171
187
  };
172
188
  }
173
189
  async function listPrComments(ctx) {
174
- const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/${ctx.prNumber}/comments?per_page=100`;
190
+ const comments = [];
191
+ for (let page = 1; page <= 100; page += 1) {
192
+ const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/${ctx.prNumber}/comments?per_page=100&page=${page}`;
193
+ const res = await fetch(url, { headers: apiHeaders(ctx.token) });
194
+ if (!res.ok) {
195
+ throw new Error(`GET ${url} returned ${res.status}`);
196
+ }
197
+ const data = await res.json();
198
+ if (!Array.isArray(data)) {
199
+ throw new Error(`GET ${url} returned a non-array response`);
200
+ }
201
+ for (const value of data) {
202
+ if (isGithubComment(value))
203
+ comments.push(value);
204
+ }
205
+ if (data.length < 100)
206
+ break;
207
+ }
208
+ return comments;
209
+ }
210
+ async function currentPrHeadSha(ctx) {
211
+ const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/pulls/${ctx.prNumber}`;
175
212
  const res = await fetch(url, { headers: apiHeaders(ctx.token) });
176
- if (!res.ok) {
213
+ if (!res.ok)
177
214
  throw new Error(`GET ${url} returned ${res.status}`);
215
+ const data = await res.json();
216
+ if (!isRecord(data) || !isRecord(data.head) || typeof data.head.sha !== 'string') {
217
+ throw new Error(`GET ${url} returned no pull-request head SHA`);
178
218
  }
179
- const data = (await res.json());
180
- return Array.isArray(data) ? data : [];
219
+ return data.head.sha;
181
220
  }
182
221
  async function createPrComment(ctx, body) {
183
222
  const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/${ctx.prNumber}/comments`;
@@ -206,8 +245,8 @@ const GITHUB_COMMENT_MAX = 65536;
206
245
  const TRUNCATION_SENTINEL = '\n\n_…output truncated to fit GitHub\'s comment size limit — see the workflow artifacts for the full report._';
207
246
  /**
208
247
  * Find an existing PR comment carrying `<!-- pathgrade:${commentId} -->`
209
- * and update it; otherwise create a new one. Swallows all errors (logs
210
- * to stderr) `pathgrade report` must never fail CI.
248
+ * and update it; otherwise create a new one. Default mode logs and swallows
249
+ * transport errors; strict mode rethrows them for an owning orchestrator.
211
250
  *
212
251
  * Bodies longer than GitHub's 65,536-character limit are truncated with a
213
252
  * sentinel pointing to workflow artifacts, preserving the leading dedup
@@ -221,19 +260,51 @@ export async function postOrUpdateComment(ctx, opts) {
221
260
  : withMarker.slice(0, GITHUB_COMMENT_MAX - TRUNCATION_SENTINEL.length) +
222
261
  TRUNCATION_SENTINEL;
223
262
  try {
263
+ if (opts.expectedHeadSha) {
264
+ const currentHead = await currentPrHeadSha(ctx);
265
+ if (currentHead !== opts.expectedHeadSha)
266
+ return 'stale';
267
+ }
224
268
  const existing = await listPrComments(ctx);
225
269
  const match = existing.find((c) => typeof c.body === 'string' && c.body.includes(marker));
226
270
  if (match) {
227
271
  await updatePrComment(ctx, match.id, body);
272
+ return 'updated';
228
273
  }
229
274
  else {
230
275
  await createPrComment(ctx, body);
276
+ return 'created';
231
277
  }
232
278
  }
233
279
  catch (err) {
234
280
  const message = err instanceof Error ? err.message : String(err);
235
281
  console.error(`pathgrade report: failed to post PR comment — ${message}`);
282
+ if (opts.strict)
283
+ throw err;
284
+ }
285
+ }
286
+ function formatLinkDestination(value) {
287
+ let url;
288
+ try {
289
+ url = new URL(value);
236
290
  }
291
+ catch {
292
+ throw new Error('Pathgrade details URL must be an absolute HTTP(S) URL');
293
+ }
294
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
295
+ throw new Error('Pathgrade details URL must be an absolute HTTP(S) URL');
296
+ }
297
+ return url.toString().replace(/\)/g, '%29');
298
+ }
299
+ function isRecord(value) {
300
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
301
+ }
302
+ function isGithubComment(value) {
303
+ return isRecord(value)
304
+ && typeof value.id === 'number'
305
+ && Number.isSafeInteger(value.id)
306
+ && value.id > 0
307
+ && typeof value.body === 'string';
237
308
  }
238
309
  function formatGroupDetails(group) {
239
310
  const out = [];
@@ -1,18 +1,21 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
+ import { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../tool-event-results.js';
3
4
  export async function writePathgradeArtifacts(artifactRoot, built) {
4
5
  await fs.ensureDir(path.join(artifactRoot, 'traces'));
6
+ const sensitiveValues = collectSensitiveEnvValues(process.env);
7
+ const report = sanitizePersistenceValue(built.report, sensitiveValues);
5
8
  const gitignorePath = path.join(artifactRoot, '.gitignore');
6
9
  if (!(await fs.pathExists(gitignorePath))) {
7
10
  await fs.writeFile(gitignorePath, '*\n');
8
11
  }
9
12
  const traceFiles = [];
10
13
  for (const trace of built.traces) {
11
- await fs.writeJson(path.join(artifactRoot, trace.traceFile), trace.trials, { spaces: 2 });
14
+ await fs.writeJson(path.join(artifactRoot, trace.traceFile), sanitizePersistenceValue(trace.trials, sensitiveValues), { spaces: 2 });
12
15
  traceFiles.push(trace.traceFile);
13
16
  }
14
17
  const resultsPath = path.join(artifactRoot, 'results.json');
15
- await fs.writeJson(resultsPath, built.report, { spaces: 2 });
18
+ await fs.writeJson(resultsPath, report, { spaces: 2 });
16
19
  return {
17
20
  resultsPath,
18
21
  traceFiles,
package/dist/sdk/agent.js CHANGED
@@ -4,7 +4,7 @@ import { lifecycleCore } from './lifecycle.js';
4
4
  import { ChatSessionImpl } from './chat.js';
5
5
  import { runConversation } from './converse.js';
6
6
  import { createPersona } from './persona.js';
7
- import { evaluate } from './evaluate.js';
7
+ import { evaluateStepScorers } from './evaluate.js';
8
8
  import { createManagedSession } from './managed-session.js';
9
9
  import { createAgentLLM } from '../utils/llm.js';
10
10
  import { buildRunSnapshot } from './snapshots.js';
@@ -19,6 +19,10 @@ import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } fr
19
19
  import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode/contract.js';
20
20
  import { collectSensitiveEnvValues } from '../tool-event-results.js';
21
21
  import { compileMcpMockApprovalSession, } from './mcp-mock-approvals.js';
22
+ import { compileScenario } from './scenario-machine-v2.js';
23
+ import { startPublicCodexScenarioRuntime } from '../agents/codex-app-server/scenario-mount.js';
24
+ import { createPublicOpenCodeScenarioRuntimeFactory } from '../agents/opencode/scenario.js';
25
+ import { createPublicCursorScenarioRuntimeFactory } from '../agents/cursor-scenario.js';
22
26
  import { createAskUserHandler } from './ask-bus/handler.js';
23
27
  /**
24
28
  * Test-only injection point: override the sink used by the next emitter
@@ -35,6 +39,7 @@ class AgentImpl {
35
39
  llm;
36
40
  timeoutSetting;
37
41
  modelOpt;
42
+ resolvedModel;
38
43
  conversationWindowOpt;
39
44
  interactionMode = null;
40
45
  _messages = [];
@@ -52,6 +57,9 @@ class AgentImpl {
52
57
  activeChatSession;
53
58
  scriptedMcp;
54
59
  sensitiveValues;
60
+ scenarioArtifact;
61
+ scenarioRuntimeFactory;
62
+ _scenarioEvidence = [];
55
63
  constructor(opts) {
56
64
  this.ws = opts.workspace;
57
65
  this.agentName = opts.agentName;
@@ -69,6 +77,8 @@ class AgentImpl {
69
77
  this.opencodeMcpToolNames = opts.opencodeMcpToolNames;
70
78
  this.scriptedMcp = opts.scriptedMcp;
71
79
  this.sensitiveValues = opts.sensitiveValues;
80
+ this.scenarioArtifact = opts.scenarioArtifact;
81
+ this.scenarioRuntimeFactory = opts.scenarioRuntimeFactory;
72
82
  }
73
83
  get messages() {
74
84
  return this._messages;
@@ -79,6 +89,9 @@ class AgentImpl {
79
89
  get workspace() {
80
90
  return this.ws.path;
81
91
  }
92
+ get scenarioEvidence() {
93
+ return structuredClone(this._scenarioEvidence);
94
+ }
82
95
  get executionMetadata() {
83
96
  const interactionMode = this.interactionMode === 'startChat'
84
97
  ? 'start_chat'
@@ -88,7 +101,10 @@ class AgentImpl {
88
101
  return {
89
102
  name: this.agentName,
90
103
  ...(this.modelOpt ? { requestedModel: this.modelOpt } : {}),
91
- transport: resolveExecutionTransport(this.agentName, this.transport),
104
+ ...(this.resolvedModel ? { resolvedModel: this.resolvedModel } : {}),
105
+ transport: this.scenarioArtifact && (this.agentName === 'opencode' || this.agentName === 'cursor')
106
+ ? 'acp'
107
+ : resolveExecutionTransport(this.agentName, this.transport),
92
108
  ...(interactionMode ? { interactionMode } : {}),
93
109
  };
94
110
  }
@@ -114,6 +130,9 @@ class AgentImpl {
114
130
  ...(this.opencodeExecutable !== undefined ? { opencodeExecutable: this.opencodeExecutable } : {}),
115
131
  ...(this.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: this.opencodeMcpToolNames } : {}),
116
132
  ...(this.scriptedMcp !== undefined ? { scriptedMcp: this.scriptedMcp } : {}),
133
+ ...(this.scenarioArtifact !== undefined ? { scenarioArtifact: this.scenarioArtifact } : {}),
134
+ ...(this.scenarioRuntimeFactory !== undefined ? { scenarioRuntimeFactory: this.scenarioRuntimeFactory } : {}),
135
+ onScenarioEvidence: (evidence) => { this._scenarioEvidence = structuredClone(evidence); },
117
136
  sensitiveValues: this.sensitiveValues,
118
137
  });
119
138
  }
@@ -128,6 +147,9 @@ class AgentImpl {
128
147
  return Math.ceil((turns * 80_000 + 200_000) / 1000);
129
148
  }
130
149
  accumulateTurnUsage(turnResult) {
150
+ if (turnResult.resolvedModel) {
151
+ this.resolvedModel = turnResult.resolvedModel;
152
+ }
131
153
  if (turnResult.inputTokens || turnResult.outputTokens) {
132
154
  this.llm.addTokens?.(turnResult.inputTokens ?? 0, turnResult.outputTokens ?? 0);
133
155
  }
@@ -286,7 +308,7 @@ class AgentImpl {
286
308
  // scorer judge calls accumulate on the same tracker.
287
309
  const agent = this;
288
310
  const runStepScorers = async (scorers) => {
289
- const result = await evaluate(agent, scorers, { llm: this.llm });
311
+ const result = await evaluateStepScorers(agent, scorers, { llm: this.llm });
290
312
  const score = result.score;
291
313
  if (score === undefined) {
292
314
  throw new Error('Step scorer evaluation did not produce a score');
@@ -362,6 +384,7 @@ class AgentImpl {
362
384
  conversationResult: this.lastConversationResult,
363
385
  workspace: dest,
364
386
  sensitiveValues: this.sensitiveValues,
387
+ scenarioEvidence: this._scenarioEvidence,
365
388
  });
366
389
  await fs.writeJSON(path.join(dest, 'run-snapshot.json'), snapshot, { spaces: 2 });
367
390
  }
@@ -409,10 +432,37 @@ export async function createAgent(opts) {
409
432
  }
410
433
  }
411
434
  const agentName = resolveAgentName(opts, process.env);
435
+ let scenarioArtifact;
436
+ if (opts.mcpScenario !== undefined) {
437
+ if (opts.agent === undefined)
438
+ throw new Error('mcpScenario requires an explicit agent');
439
+ if (opts.mcpMock !== undefined || opts.mcpMockApprovalRules !== undefined || opts.mcpConfigFile !== undefined || opts.mcpSafety !== undefined) {
440
+ throw new Error('mcpScenario cannot be combined with legacy or live MCP options');
441
+ }
442
+ const compiled = compileScenario(opts.mcpScenario);
443
+ if (!compiled.ok)
444
+ throw new Error(`Scenario compilation failed: ${JSON.stringify(compiled.diagnostics)}`);
445
+ scenarioArtifact = compiled.artifact;
446
+ }
412
447
  validateOpenCodeDeclaration(agentName, opts);
413
448
  const transport = agentName === 'codex'
414
449
  ? resolveCodexTransport(opts, process.env)
415
450
  : undefined;
451
+ if (scenarioArtifact && agentName === 'codex' && transport !== 'app-server') {
452
+ throw new Error('mcpScenario requires Codex transport app-server; Codex exec is unsupported');
453
+ }
454
+ const scenarioRuntimeFactory = !scenarioArtifact
455
+ ? undefined
456
+ : agentName === 'codex'
457
+ ? startPublicCodexScenarioRuntime
458
+ : agentName === 'opencode'
459
+ ? createPublicOpenCodeScenarioRuntimeFactory({
460
+ opencodeExecutable: opts.opencodeExecutable,
461
+ requestedModel: opts.model,
462
+ })
463
+ : agentName === 'cursor'
464
+ ? createPublicCursorScenarioRuntimeFactory({ requestedModel: opts.model })
465
+ : undefined;
416
466
  let scriptedMcp;
417
467
  if (opts.mcpMockApprovalRules !== undefined) {
418
468
  if (opts.mcpMock === undefined)
@@ -434,7 +484,7 @@ export async function createAgent(opts) {
434
484
  const timeoutSetting = opts.timeout ?? 300;
435
485
  // Capture runner context now; adapters own installation and restoration.
436
486
  const testCtx = opts.debug ? resolveCaseDebugContext() : { name: '', dir: '' };
437
- const { timeout: _, mcpMock, mcpConfigFile, mcpMockApprovalRules: ________, agent: __, debug: ___, transport: _____, mcpSafety: ______, opencodeExecutable, ...rest } = opts;
487
+ const { timeout: _, mcpMock, mcpScenario: _________, mcpConfigFile, mcpMockApprovalRules: ________, agent: __, debug: ___, transport: _____, mcpSafety: ______, opencodeExecutable, ...rest } = opts;
438
488
  const workspace = await prepareWorkspace({
439
489
  ...rest,
440
490
  agent: agentName,
@@ -471,6 +521,8 @@ export async function createAgent(opts) {
471
521
  opencodeExecutable,
472
522
  opencodeMcpToolNames: agentName === 'opencode' ? collectOpenCodeMcpToolNames(mcpMock) : undefined,
473
523
  scriptedMcp,
524
+ scenarioArtifact,
525
+ scenarioRuntimeFactory,
474
526
  sensitiveValues: workspace.sensitiveValues ?? collectSensitiveEnvValues(workspace.env),
475
527
  });
476
528
  lifecycleCore.registerAgent(agent);
@@ -9,5 +9,7 @@ type EvaluateFromSnapshot = (snapshotPath: string, scorers: Scorer[], opts?: Eva
9
9
  type EvaluateFn = ((agent: Agent, scorers: Scorer[], opts?: EvaluateOptions) => Promise<RecordedEvalResult>) & {
10
10
  fromSnapshot: EvaluateFromSnapshot;
11
11
  };
12
+ /** Internal runConversation hook: step scorers must not consume final-run attribution. */
13
+ export declare function evaluateStepScorers(agent: Agent, scorers: Scorer[], opts?: EvaluateOptions): Promise<RecordedEvalResult>;
12
14
  export declare const evaluate: EvaluateFn;
13
15
  export {};
@@ -1,4 +1,4 @@
1
- import { extractSkillsFromLog, extractToolEventsFromLog } from '../tool-events.js';
1
+ import { countShellCommandsFromLog, extractSkillsFromLog, extractToolEventsFromLog } from '../tool-events.js';
2
2
  import { getRuntime } from './eval-runtime.js';
3
3
  import { emitEvalResult } from './result-capture.js';
4
4
  import { runJudgePipeline } from './judge-pipeline.js';
@@ -33,7 +33,7 @@ export class EvalScorerError extends Error {
33
33
  // Local per-call: no module-level state, no cleanup export needed.
34
34
  function makeEvaluateAgent() {
35
35
  const conversationAttributed = new WeakSet();
36
- return async function evaluateAgent(agent, scorers, opts) {
36
+ return async function evaluateAgent(agent, scorers, opts, attributeConversation = true) {
37
37
  const toolEvents = extractToolEventsFromLog(agent.log);
38
38
  const ctx = {
39
39
  workspace: agent.workspace,
@@ -42,15 +42,16 @@ function makeEvaluateAgent() {
42
42
  toolEvents,
43
43
  runCommand: (cmd) => agent.exec(cmd),
44
44
  artifacts: createSessionArtifacts(agent.workspace, toolEvents),
45
+ scenarioEvidence: agent.scenarioEvidence,
45
46
  };
46
47
  const trackedLLM = opts?.llm ?? agent.llm;
47
48
  // Snapshot conversation tokens BEFORE running scorers, for first-eval attribution.
48
- const before = trackedLLM.tokenUsage ?? { inputTokens: 0, outputTokens: 0 };
49
+ const before = agent.llm.tokenUsage ?? { inputTokens: 0, outputTokens: 0 };
49
50
  // Snapshot conversation cost too. AgentImpl's `sendTurn` accumulates
50
51
  // per-turn `costUsd` onto `trackedLLM` via `addCost`, so by the time
51
52
  // `evaluate()` runs the pre-evaluate cost is the conversation's
52
53
  // accumulated agent-turn cost.
53
- const beforeCostUsd = trackedLLM.costUsd ?? 0;
54
+ const beforeCostUsd = agent.llm.costUsd ?? 0;
54
55
  // measure() returns the delta consumed by this evaluate call.
55
56
  const scoringStartedAt = performance.now();
56
57
  const { result: evalResult, tokens: deltaTokenUsage } = trackedLLM.measure
@@ -59,12 +60,13 @@ function makeEvaluateAgent() {
59
60
  const r = await evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM });
60
61
  return { result: r, tokens: r.tokenUsage ?? { inputTokens: 0, outputTokens: 0 } };
61
62
  })();
62
- // Attribute conversation tokens on the first evaluate() for this agent.
63
- const isFirstEval = !conversationAttributed.has(agent);
63
+ // Attribute conversation tokens on the first public evaluate() for this agent.
64
+ const isFirstEval = attributeConversation && !conversationAttributed.has(agent);
64
65
  const conversationTokens = isFirstEval && (before.inputTokens > 0 || before.outputTokens > 0)
65
66
  ? { conversation_input_tokens: before.inputTokens, conversation_output_tokens: before.outputTokens }
66
67
  : undefined;
67
- // Same first-eval attribution rule for cost. Omitted entirely when
68
+ // Agent usage is independent of the optional scorer LLM. Same
69
+ // first-eval attribution rule for cost; omit it when unavailable.
68
70
  // no conversation cost was captured (Codex / Cursor today).
69
71
  const conversationCost = isFirstEval && beforeCostUsd > 0
70
72
  ? { conversation_cost_usd: beforeCostUsd }
@@ -80,7 +82,7 @@ function makeEvaluateAgent() {
80
82
  ...(opts?.evaluationDefinitionKey
81
83
  ? { evaluationDefinitionKey: opts.evaluationDefinitionKey }
82
84
  : {}),
83
- trial: buildTrialResult(agent.log, { ...evalResult, tokenUsage: deltaTokenUsage }, conversationTokens, conversationCost),
85
+ trial: buildTrialResult(agent.log, { ...evalResult, tokenUsage: deltaTokenUsage }, agent.scenarioEvidence, conversationTokens, conversationCost),
84
86
  };
85
87
  emitEvalResult({ result: recordedResult, agent });
86
88
  maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
@@ -88,6 +90,10 @@ function makeEvaluateAgent() {
88
90
  };
89
91
  }
90
92
  const evaluateAgent = makeEvaluateAgent();
93
+ /** Internal runConversation hook: step scorers must not consume final-run attribution. */
94
+ export function evaluateStepScorers(agent, scorers, opts) {
95
+ return evaluateAgent(agent, scorers, opts, false);
96
+ }
91
97
  async function fromSnapshot(snapshotPath, scorers, opts) {
92
98
  const snapshot = await loadRunSnapshot(snapshotPath);
93
99
  const trackedLLM = opts?.llm ?? createLLMClient({ adapters: [{
@@ -110,6 +116,7 @@ async function fromSnapshot(snapshotPath, scorers, opts) {
110
116
  return sandboxExec(cmd, { cwd: workspace, env: getProcessEnv() });
111
117
  },
112
118
  artifacts: createSessionArtifacts(snapshot.workspace ?? '', snapshot.toolEvents),
119
+ scenarioEvidence: snapshot.scenarioEvidence ?? [],
113
120
  };
114
121
  const scoringStartedAt = performance.now();
115
122
  const evalResult = await evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM });
@@ -121,7 +128,7 @@ async function fromSnapshot(snapshotPath, scorers, opts) {
121
128
  ...(opts?.evaluationDefinitionKey
122
129
  ? { evaluationDefinitionKey: opts.evaluationDefinitionKey }
123
130
  : {}),
124
- trial: buildTrialResult(snapshot.log, evalResult),
131
+ trial: buildTrialResult(snapshot.log, evalResult, snapshot.scenarioEvidence),
125
132
  };
126
133
  maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
127
134
  return recordedResult;
@@ -265,8 +272,8 @@ function matchesArtifactPattern(artifactPath, pattern) {
265
272
  return artifactPath.includes(pattern);
266
273
  return pattern.test(artifactPath);
267
274
  }
268
- function buildTrialResult(log, result, conversationTokens, conversationCost) {
269
- const nCommands = log.filter((entry) => entry.type === 'command').length;
275
+ function buildTrialResult(log, result, scenarioEvidence, conversationTokens, conversationCost) {
276
+ const nCommands = countShellCommandsFromLog(log);
270
277
  const skills = extractSkillsFromLog(log);
271
278
  return {
272
279
  trial_id: 0,
@@ -285,6 +292,7 @@ function buildTrialResult(log, result, conversationTokens, conversationCost) {
285
292
  // field.
286
293
  ...conversationCost,
287
294
  session_log: [...log],
295
+ ...(scenarioEvidence && scenarioEvidence.length > 0 ? { scenario_evidence: structuredClone(scenarioEvidence) } : {}),
288
296
  ...(skills.length > 0 ? { skills_used: skills } : {}),
289
297
  };
290
298
  }
@@ -297,7 +305,7 @@ function getProcessEnv() {
297
305
  }
298
306
  return env;
299
307
  }
300
- export const evaluate = Object.assign(evaluateAgent, {
308
+ export const evaluate = Object.assign((agent, scorers, opts) => evaluateAgent(agent, scorers, opts), {
301
309
  fromSnapshot,
302
310
  });
303
311
  function toTrialScorerResult(result) {
@@ -2,8 +2,12 @@ export { createAgent } from './agent.js';
2
2
  export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
3
3
  export { AgentCrashError } from './agent-crash.js';
4
4
  export { check, score, judge, toolUsage } from './scorers.js';
5
+ export { compileScenario, compileScenarioText } from './scenario-machine-v2.js';
6
+ export { findScenarioCaseMatches, getFinalScenarioState, getScenarioStateTimeline, ScenarioEvidenceError, wasScenarioCaseMatched, } from './scenario-evidence.js';
5
7
  export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
6
8
  export type { McpMockApprovalRule, McpMockJsonValue } from './mcp-mock-approvals.js';
9
+ export type { CompileScenarioResult, EvidenceEnvelope, JsonObject, JsonPrimitive, JsonValue, MatchExpr, MatchPredicate, McpContent, ScenarioArtifact, ScenarioCase, ScenarioDiagnostic, ScenarioEvidence, ScenarioMachineV2, ScenarioRejection, ScenarioSuccess, ScenarioToolError, } from './scenario-machine-v2.js';
10
+ export type { ExpectedScenarioCaseMatch, ScenarioCaseMatchEvidence, ScenarioStateTransitionEvidence, } from './scenario-evidence.js';
7
11
  export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
8
12
  export { evaluate, EvalScorerError } from './evaluate.js';
9
13
  export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
@@ -15,6 +19,8 @@ export { previewReactions } from './reaction-preview.js';
15
19
  export { setRuntime, resetRuntime } from './eval-runtime.js';
16
20
  export { DEFAULT_COPY_IGNORE } from '../providers/copy-filter.js';
17
21
  export { extractToolEventsFromLog } from '../tool-events.js';
22
+ export { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../tool-event-results.js';
23
+ export { parseEnvFile } from '../utils/env.js';
18
24
  export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from './ask-bus/bus.js';
19
25
  export { toAskUserToolEvent } from './ask-bus/projection.js';
20
26
  export type { AskUserToolEvent, AskUserToolEventArguments, AskUserToolEventQuestionArgument, } from './ask-bus/projection.js';
package/dist/sdk/index.js CHANGED
@@ -3,6 +3,8 @@ export { createAgent } from './agent.js';
3
3
  export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
4
4
  export { AgentCrashError } from './agent-crash.js';
5
5
  export { check, score, judge, toolUsage } from './scorers.js';
6
+ export { compileScenario, compileScenarioText } from './scenario-machine-v2.js';
7
+ export { findScenarioCaseMatches, getFinalScenarioState, getScenarioStateTimeline, ScenarioEvidenceError, wasScenarioCaseMatched, } from './scenario-evidence.js';
6
8
  export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
7
9
  export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
8
10
  export { evaluate, EvalScorerError } from './evaluate.js';
@@ -15,6 +17,8 @@ export { previewReactions } from './reaction-preview.js';
15
17
  export { setRuntime, resetRuntime } from './eval-runtime.js';
16
18
  export { DEFAULT_COPY_IGNORE } from '../providers/copy-filter.js';
17
19
  export { extractToolEventsFromLog } from '../tool-events.js';
20
+ export { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../tool-event-results.js';
21
+ export { parseEnvFile } from '../utils/env.js';
18
22
  export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from './ask-bus/bus.js';
19
23
  export { toAskUserToolEvent } from './ask-bus/projection.js';
20
24
  export { buildAskBatchLogEntries } from './agent-result-log.js';
@@ -1,5 +1,6 @@
1
1
  import { getCurrentCaseContext } from './case-context.js';
2
2
  import { buildDiagnosticsReport } from './diagnostics.js';
3
+ import { countShellCommandsFromLog } from '../tool-events.js';
3
4
  const pendingAgents = new Set();
4
5
  const agentOwners = new WeakMap();
5
6
  const agentResults = new WeakMap();
@@ -131,7 +132,7 @@ async function flushCase(input) {
131
132
  function synthesizeTrialFromAgent(agent) {
132
133
  if (agent.log.length === 0)
133
134
  return null;
134
- const nCommands = agent.log.filter((entry) => entry.type === 'command').length;
135
+ const nCommands = countShellCommandsFromLog(agent.log);
135
136
  const tokenUsage = agent.llm.tokenUsage;
136
137
  const conversationEnd = [...agent.log].reverse().find((entry) => entry.type === 'conversation_end');
137
138
  const completionReason = conversationEnd?.completion_reason ?? (agent.log.some((entry) => entry.type === 'agent_result') ? 'completed' : undefined);
@@ -147,8 +148,12 @@ function synthesizeTrialFromAgent(agent) {
147
148
  n_commands: nCommands,
148
149
  input_tokens: 0,
149
150
  output_tokens: 0,
150
- conversation_input_tokens: tokenUsage?.inputTokens ?? 0,
151
- conversation_output_tokens: tokenUsage?.outputTokens ?? 0,
151
+ ...(tokenUsage && (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0)
152
+ ? {
153
+ conversation_input_tokens: tokenUsage.inputTokens,
154
+ conversation_output_tokens: tokenUsage.outputTokens,
155
+ }
156
+ : {}),
152
157
  session_log: [...agent.log],
153
158
  },
154
159
  diagnostics: buildDiagnosticsReport({
@@ -5,6 +5,8 @@ import type { McpSafetyOptions } from './mcp-safety.js';
5
5
  import type { LLMPort } from '../utils/llm-types.js';
6
6
  import type { AskBus } from './ask-bus/types.js';
7
7
  import type { CompiledMcpMockSession } from './mcp-mock-approvals.js';
8
+ import { type PublicScenarioRuntimeFactory } from '../internal/direct-mcp-v2/public-scenario-runtime.js';
9
+ import type { EvidenceEnvelope, ScenarioArtifact } from '../internal/direct-mcp-v2/types.js';
8
10
  export interface ManagedSessionDeps {
9
11
  ws: Workspace;
10
12
  agentName: AgentName;
@@ -36,6 +38,10 @@ export interface ManagedSessionDeps {
36
38
  opencodeMcpToolNames?: string[];
37
39
  /** Trusted pre-workspace compiled generated-MCP declaration. */
38
40
  scriptedMcp?: CompiledMcpMockSession;
41
+ scenarioArtifact?: ScenarioArtifact;
42
+ /** Provider adapter entrypoint; defaults to the preserved Claude bridge. */
43
+ scenarioRuntimeFactory?: PublicScenarioRuntimeFactory;
44
+ onScenarioEvidence?: (evidence: readonly EvidenceEnvelope[]) => void;
39
45
  /** Runtime-only values that must not enter session logs or persisted results. */
40
46
  sensitiveValues?: readonly string[];
41
47
  }