@post-print/agent-harness 1.1.9 → 2.0.1

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 (113) hide show
  1. package/README.md +11 -93
  2. package/dist/agent-definition.d.ts +77 -0
  3. package/dist/agent-definition.d.ts.map +1 -0
  4. package/dist/agent-definition.js +46 -0
  5. package/dist/agent-definition.js.map +1 -0
  6. package/dist/agent-session.d.ts +17 -0
  7. package/dist/agent-session.d.ts.map +1 -0
  8. package/dist/agent-session.js +165 -0
  9. package/dist/agent-session.js.map +1 -0
  10. package/dist/agent-worker.d.ts +2 -0
  11. package/dist/agent-worker.d.ts.map +1 -0
  12. package/dist/agent-worker.js +167 -0
  13. package/dist/agent-worker.js.map +1 -0
  14. package/dist/capture.d.ts +1 -0
  15. package/dist/capture.d.ts.map +1 -1
  16. package/dist/capture.js +119 -101
  17. package/dist/capture.js.map +1 -1
  18. package/dist/claude-capture.d.ts +0 -1
  19. package/dist/claude-capture.d.ts.map +1 -1
  20. package/dist/claude-capture.js +96 -106
  21. package/dist/claude-capture.js.map +1 -1
  22. package/dist/claude-run.d.ts +3 -11
  23. package/dist/claude-run.d.ts.map +1 -1
  24. package/dist/claude-run.js +178 -166
  25. package/dist/claude-run.js.map +1 -1
  26. package/dist/cursor-auth.d.ts +0 -1
  27. package/dist/cursor-auth.d.ts.map +1 -1
  28. package/dist/cursor-auth.js +5 -3
  29. package/dist/cursor-auth.js.map +1 -1
  30. package/dist/cursor-run.d.ts +6 -34
  31. package/dist/cursor-run.d.ts.map +1 -1
  32. package/dist/cursor-run.js +123 -175
  33. package/dist/cursor-run.js.map +1 -1
  34. package/dist/index.d.ts +8 -40
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +6 -47
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp.d.ts.map +1 -1
  39. package/dist/mcp.js +37 -39
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/openai-capture.d.ts.map +1 -1
  42. package/dist/openai-capture.js +66 -57
  43. package/dist/openai-capture.js.map +1 -1
  44. package/dist/openai-run.d.ts +4 -13
  45. package/dist/openai-run.d.ts.map +1 -1
  46. package/dist/openai-run.js +168 -159
  47. package/dist/openai-run.js.map +1 -1
  48. package/dist/sealed-workspace.d.ts +0 -8
  49. package/dist/sealed-workspace.d.ts.map +1 -1
  50. package/dist/sealed-workspace.js +32 -65
  51. package/dist/sealed-workspace.js.map +1 -1
  52. package/dist/skills-context.d.ts +0 -24
  53. package/dist/skills-context.d.ts.map +1 -1
  54. package/dist/skills-context.js +12 -156
  55. package/dist/skills-context.js.map +1 -1
  56. package/dist/types.d.ts +0 -84
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/user-skills.d.ts +6 -6
  59. package/dist/user-skills.d.ts.map +1 -1
  60. package/dist/user-skills.js +32 -27
  61. package/dist/user-skills.js.map +1 -1
  62. package/dist/working-tree-guard.d.ts.map +1 -1
  63. package/dist/working-tree-guard.js +3 -1
  64. package/dist/working-tree-guard.js.map +1 -1
  65. package/package.json +3 -3
  66. package/dist/adapters/index.d.ts +0 -28
  67. package/dist/adapters/index.d.ts.map +0 -1
  68. package/dist/adapters/index.js +0 -271
  69. package/dist/adapters/index.js.map +0 -1
  70. package/dist/classifier.d.ts +0 -15
  71. package/dist/classifier.d.ts.map +0 -1
  72. package/dist/classifier.js +0 -102
  73. package/dist/classifier.js.map +0 -1
  74. package/dist/context.d.ts +0 -26
  75. package/dist/context.d.ts.map +0 -1
  76. package/dist/context.js +0 -227
  77. package/dist/context.js.map +0 -1
  78. package/dist/conversation.d.ts +0 -28
  79. package/dist/conversation.d.ts.map +0 -1
  80. package/dist/conversation.js +0 -191
  81. package/dist/conversation.js.map +0 -1
  82. package/dist/host-registry.d.ts +0 -11
  83. package/dist/host-registry.d.ts.map +0 -1
  84. package/dist/host-registry.js +0 -36
  85. package/dist/host-registry.js.map +0 -1
  86. package/dist/judge.d.ts +0 -132
  87. package/dist/judge.d.ts.map +0 -1
  88. package/dist/judge.js +0 -795
  89. package/dist/judge.js.map +0 -1
  90. package/dist/logger.d.ts +0 -11
  91. package/dist/logger.d.ts.map +0 -1
  92. package/dist/logger.js +0 -20
  93. package/dist/logger.js.map +0 -1
  94. package/dist/main.d.ts +0 -9
  95. package/dist/main.d.ts.map +0 -1
  96. package/dist/main.js +0 -7
  97. package/dist/main.js.map +0 -1
  98. package/dist/retry.d.ts +0 -14
  99. package/dist/retry.d.ts.map +0 -1
  100. package/dist/retry.js +0 -52
  101. package/dist/retry.js.map +0 -1
  102. package/dist/user-simulator.d.ts +0 -10
  103. package/dist/user-simulator.d.ts.map +0 -1
  104. package/dist/user-simulator.js +0 -47
  105. package/dist/user-simulator.js.map +0 -1
  106. package/dist/worktree-leak.d.ts +0 -16
  107. package/dist/worktree-leak.d.ts.map +0 -1
  108. package/dist/worktree-leak.js +0 -110
  109. package/dist/worktree-leak.js.map +0 -1
  110. package/dist/worktree.d.ts +0 -10
  111. package/dist/worktree.d.ts.map +0 -1
  112. package/dist/worktree.js +0 -58
  113. package/dist/worktree.js.map +0 -1
package/dist/judge.js DELETED
@@ -1,795 +0,0 @@
1
- import { missingClassifierAuth, runClassifier } from "./classifier.js";
2
- import { isTransientInfraError, resolveRetryMaxAttempts, withRetry } from "./retry.js";
3
- import { sumUsageParts } from "./usage-breakdown.js";
4
- // Optional info-string (`json`, `js`, `typescript`, …) on the opening fence line.
5
- // Without this, ` ```js\n"yes"\n``` ` extracts `js\n"yes"` and never latches.
6
- const JSON_FENCE_PATTERN = /```[^\n`]*\s*([\s\S]*?)```/;
7
- const JSON_OBJECT_PATTERN = /\{[\s\S]*\}/;
8
- const JUDGE_TOOL_RESULT_MAX_CHARS = 4_000;
9
- function truncateJudgeText(text, max = JUDGE_TOOL_RESULT_MAX_CHARS) {
10
- if (text.length <= max) {
11
- return text;
12
- }
13
- return `${text.slice(0, max)}…`;
14
- }
15
- function formatOneToolCallForJudge(call, index) {
16
- const args = JSON.stringify(call.args ?? {});
17
- const lines = [`${index + 1}. ${call.name} ${truncateJudgeText(args, 2_000)}`];
18
- if (call.result) {
19
- lines.push(` result: ${truncateJudgeText(call.result)}`);
20
- }
21
- return lines.join("\n");
22
- }
23
- function formatToolCallsForJudge(trace) {
24
- if (trace.toolCalls.length === 0) {
25
- return "";
26
- }
27
- return [
28
- "Tool calls:",
29
- ...trace.toolCalls.map((call, index) => formatOneToolCallForJudge(call, index)),
30
- ].join("\n");
31
- }
32
- function formatTraceChronologically(trace) {
33
- const blocks = [];
34
- let tie = 0;
35
- for (const message of trace.messages) {
36
- blocks.push({
37
- seq: message.seq ?? Number.MAX_SAFE_INTEGER,
38
- tie: tie++,
39
- text: `${message.role}:\n${message.content}`,
40
- });
41
- }
42
- for (const [index, call] of trace.toolCalls.entries()) {
43
- blocks.push({
44
- seq: call.seq ?? Number.MAX_SAFE_INTEGER,
45
- tie: tie++,
46
- text: `Tool calls:\n${formatOneToolCallForJudge(call, index)}`,
47
- });
48
- }
49
- blocks.sort((a, b) => a.seq - b.seq || a.tie - b.tie);
50
- return blocks.map((block) => block.text).join("\n\n---\n\n");
51
- }
52
- /** Full live transcript for a judge, including tool names, args, and results. */
53
- export function formatTraceForJudge(trace) {
54
- const hasSeq = trace.messages.some((message) => message.seq !== undefined) ||
55
- trace.toolCalls.some((call) => call.seq !== undefined);
56
- const body = hasSeq
57
- ? formatTraceChronologically(trace)
58
- : [...trace.messages.map((m) => `${m.role}:\n${m.content}`), formatToolCallsForJudge(trace)]
59
- .filter(Boolean)
60
- .join("\n\n---\n\n");
61
- return [
62
- body,
63
- trace.prBody ?? "",
64
- trace.gitDiff ? `Git diff:\n${trace.gitDiff}` : "",
65
- trace.shellCommands.length > 0 ? `Shell commands:\n${trace.shellCommands.join("\n")}` : "",
66
- ]
67
- .filter(Boolean)
68
- .join("\n\n---\n\n");
69
- }
70
- /** Judge questions for mustInvokeSkill — replaces prose regex heuristics. */
71
- export function skillInvokeJudgeCriteria(skillNames) {
72
- return skillNames.map((name) => ({
73
- id: `invoke-skill:${name}`,
74
- question: [
75
- `Did the agent actually follow the "${name}" skill?`,
76
- "Use tool calls, file reads, and later actions as evidence.",
77
- "Naming the skill or using similar words is not enough.",
78
- "A host skill load counts as obtaining the skill body.",
79
- "Do not fail only because a confirmatory SKILL.md read happens after the reply.",
80
- ].join(" "),
81
- }));
82
- }
83
- function normalizeVerdict(value) {
84
- const verdict = String(value ?? "")
85
- .trim()
86
- .toLowerCase();
87
- if (verdict === "yes" || verdict === "no") {
88
- return verdict;
89
- }
90
- return undefined;
91
- }
92
- // Whole-reply opening fence is a contract attempt regardless of info-string
93
- // (`json`, `js`, `typescript`, bare, …). Mid-prose fence mentions do not match.
94
- const JUDGE_JSON_FENCE_OPEN_PATTERN = /^```/;
95
- const JUDGE_VERDICT_KEY_PATTERN = /"verdict"\s*:/;
96
- /** Same prefixes legacy YES/NO strips — gate same-line JSON peels. */
97
- const JUDGE_VERDICT_PREFIX_PATTERN = /^(?:answer|verdict|line\s*1)\s*:\s*/i;
98
- /** Complete JSON number token (rejects bare `1.` with no fractional digits). */
99
- const JUDGE_JSON_NUMBER_TOKEN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
100
- const JUDGE_JSON_BOOL_NULL_TOKEN = /^(?:true|false|null)/i;
101
- /** Closed fences, or a trailing unclosed fence (prose preamble + open contract). */
102
- const MARKDOWN_FENCE_STRIP_PATTERN = /```[^\n`]*\s*[\s\S]*?(?:```|$)/g;
103
- /**
104
- * After a complete JSON number/bool/null token: whether the remainder is
105
- * contract junk (latch) vs English continuation (allow YES/NO salvage).
106
- *
107
- * `true\nYES` / `3 YES` / `42,` / `3, YES` → latch.
108
- * `true story` / `3 findings` / `3, findings` / `true, the agent` → salvage.
109
- */
110
- function jsonPrimitiveRemainderIsContract(rest) {
111
- if (rest === "") {
112
- return true;
113
- }
114
- const restTrim = rest.trimStart();
115
- if (restTrim === "") {
116
- return true;
117
- }
118
- // Comma after a primitive is JSON-ish only when nothing English follows
119
- // (`42,` / `3, YES`). List/prose continuations (`3, findings`, `1, 2, and 3`)
120
- // must still reach YES/NO salvage.
121
- if (restTrim.startsWith(",")) {
122
- const afterComma = restTrim.slice(1).trimStart();
123
- if (afterComma === "") {
124
- return true;
125
- }
126
- if (/^(?:yes|no)\b/i.test(afterComma)) {
127
- return true;
128
- }
129
- if (/[A-Za-z]/.test(afterComma)) {
130
- return false;
131
- }
132
- return true;
133
- }
134
- const firstWord = /^([A-Za-z]+)/.exec(restTrim)?.[1]?.toLowerCase();
135
- if (firstWord === "yes" || firstWord === "no") {
136
- return true;
137
- }
138
- if (/^[A-Za-z]/.test(restTrim)) {
139
- return false;
140
- }
141
- return true;
142
- }
143
- /**
144
- * Detect truncated/unparseable JSON-shaped contract attempts. Only replies
145
- * that *are* JSON-shaped (value prefix or open with a fence) count — a mid-prose
146
- * mention of ```json or an incidental blob is not a contract attempt.
147
- *
148
- * Number/bool/null prefixes latch only for a complete JSON token (optionally
149
- * with trailing junk). Numbered-list prose (`1. …`) and English continuations
150
- * (`true story`, `3 findings`) must not latch — YES/NO salvage still applies.
151
- */
152
- function looksLikeJudgeJsonAttempt(text) {
153
- const trimmed = text.trim();
154
- if (JUDGE_JSON_FENCE_OPEN_PATTERN.test(trimmed)) {
155
- return true;
156
- }
157
- if (/^["{[]/.test(trimmed)) {
158
- return true;
159
- }
160
- const number = JUDGE_JSON_NUMBER_TOKEN.exec(trimmed);
161
- if (number) {
162
- const rest = trimmed.slice(number[0].length);
163
- // `1. prose` / `10) prose` — list markers, not number-then-junk.
164
- if (/^[.)]/.test(rest)) {
165
- return false;
166
- }
167
- return jsonPrimitiveRemainderIsContract(rest);
168
- }
169
- const boolNull = JUDGE_JSON_BOOL_NULL_TOKEN.exec(trimmed);
170
- if (boolNull) {
171
- return jsonPrimitiveRemainderIsContract(trimmed.slice(boolNull[0].length));
172
- }
173
- return false;
174
- }
175
- /** Drop fenced bodies so legacy YES/NO does not match inside quoted JSON strings. */
176
- function stripMarkdownFencesForLegacy(text) {
177
- return text.replace(MARKDOWN_FENCE_STRIP_PATTERN, "\n");
178
- }
179
- /**
180
- * True when the JSON contract body is array-shaped (`[` before any `{`),
181
- * including truncated arrays. Object-extraction must not peel an inner
182
- * `{…}` out of those replies.
183
- */
184
- function isArrayShapedJudgeJson(text) {
185
- const trimmed = text.trim();
186
- const fenced = trimmed.match(JSON_FENCE_PATTERN);
187
- const body = fenced?.[1]?.trim() ?? trimmed;
188
- const bracket = body.indexOf("[");
189
- if (bracket === -1) {
190
- return false;
191
- }
192
- const brace = body.indexOf("{");
193
- return brace === -1 || bracket < brace;
194
- }
195
- const INVALID_JUDGE_JSON = {
196
- pass: false,
197
- rationale: "judge returned invalid JSON (expected { verdict, evidence, rationale })",
198
- evidence: [],
199
- valid: false,
200
- };
201
- function arrayLooksLikeJudgeContract(parsed) {
202
- if (parsed.some((item) => item !== null &&
203
- typeof item === "object" &&
204
- !Array.isArray(item) &&
205
- "verdict" in item)) {
206
- return true;
207
- }
208
- // Wrong-shaped contract answers like `["yes"]` / `["no"]` — not incidental
209
- // lists such as `[1,2,3]` or `["intro","summary"]`.
210
- return (parsed.length > 0 &&
211
- parsed.every((item) => typeof item === "string" && normalizeVerdict(item) !== undefined));
212
- }
213
- /** Peeled objects with judge-schema keys but no `verdict` are contract attempts. */
214
- function objectLooksLikeJudgeSchema(record) {
215
- return "evidence" in record || "rationale" in record;
216
- }
217
- /**
218
- * Try structured judge JSON. `structured` latches only for real contract
219
- * attempts: whole-text/fenced JSON bodies, verdict-shaped arrays or objects,
220
- * or failed candidates carrying a `verdict` key — not incidental arrays,
221
- * mid-prose truncated blobs, or fence mentions inside prose.
222
- */
223
- function tryParseJudgeJson(text) {
224
- const trimmed = text.trim();
225
- const candidates = [trimmed];
226
- const fenced = trimmed.match(JSON_FENCE_PATTERN);
227
- const fencedBody = fenced?.[1]?.trim();
228
- if (fencedBody) {
229
- candidates.unshift(fencedBody);
230
- }
231
- const arrayShaped = isArrayShapedJudgeJson(trimmed);
232
- // Prose-prefixed single objects may still use greedy `{…}` extraction.
233
- // Array-shaped replies (including truncated `[{…}`) must not peel inner objects.
234
- if (!arrayShaped) {
235
- const objectMatch = trimmed.match(JSON_OBJECT_PATTERN);
236
- if (objectMatch?.[0]) {
237
- candidates.push(objectMatch[0]);
238
- }
239
- const brace = trimmed.indexOf("{");
240
- if (brace > 0) {
241
- candidates.push(trimmed.slice(brace));
242
- }
243
- }
244
- else {
245
- const bracket = trimmed.indexOf("[");
246
- if (bracket >= 0) {
247
- candidates.push(trimmed.slice(bracket));
248
- }
249
- }
250
- // Contract peels under a prose preamble (not mid-prose incidental quotes):
251
- // - line-leading `"…` / `[…` (`Answer:\n"yes"`)
252
- // - line-leading JSON number/bool/null (`Answer:\n42\nYES`)
253
- // - same-line after answer|verdict|line 1: (`Answer: "yes"`, `Verdict: 42`)
254
- // Mid-line quotes without that prefix (`YES` + ```js\n"yes"\n```) stay
255
- // incidental — fence bodies use `fencedBody` / whole-reply fence primary.
256
- const lineLeadingJsonValues = new Set();
257
- let insideFence = false;
258
- const pushContractPeel = (value) => {
259
- const peel = value.trim();
260
- if (!peel) {
261
- return;
262
- }
263
- candidates.push(peel);
264
- lineLeadingJsonValues.add(peel);
265
- };
266
- const isLineLeadingJsonPrimitive = (lineTrim) => {
267
- if (lineTrim.startsWith('"') || lineTrim.startsWith("[")) {
268
- return true;
269
- }
270
- // Complete number/bool/null lines (optional contract junk) — not English
271
- // (`3 findings`, `true story`) which looksLikeJudgeJsonAttempt rejects.
272
- return (looksLikeJudgeJsonAttempt(lineTrim) &&
273
- (JUDGE_JSON_NUMBER_TOKEN.test(lineTrim) || JUDGE_JSON_BOOL_NULL_TOKEN.test(lineTrim)));
274
- };
275
- for (const line of trimmed.split("\n")) {
276
- const lineTrim = line.trim();
277
- if (lineTrim.startsWith("```")) {
278
- insideFence = !insideFence;
279
- continue;
280
- }
281
- if (insideFence) {
282
- continue;
283
- }
284
- if (isLineLeadingJsonPrimitive(lineTrim)) {
285
- pushContractPeel(lineTrim);
286
- }
287
- const prefix = JUDGE_VERDICT_PREFIX_PATTERN.exec(lineTrim);
288
- if (prefix) {
289
- const remainder = lineTrim.slice(prefix[0].length).trim();
290
- if (remainder.startsWith('"') ||
291
- remainder.startsWith("[") ||
292
- remainder.startsWith("{") ||
293
- isLineLeadingJsonPrimitive(remainder)) {
294
- pushContractPeel(remainder);
295
- }
296
- }
297
- }
298
- let structured = false;
299
- let failedJsonShape = false;
300
- // Only whole-reply fences are primary contract attempts. Mid-prose fences
301
- // (e.g. YES + ```js\n"yes"\n```) stay incidental like unfenced peels.
302
- const wholeReplyFence = JUDGE_JSON_FENCE_OPEN_PATTERN.test(trimmed);
303
- for (const candidate of candidates) {
304
- const candidateTrim = candidate.trim();
305
- const primary = candidateTrim === trimmed ||
306
- (wholeReplyFence && fencedBody !== undefined && candidateTrim === fencedBody);
307
- const lineLeading = lineLeadingJsonValues.has(candidateTrim);
308
- try {
309
- const parsed = JSON.parse(candidate);
310
- if (parsed === null || typeof parsed !== "object") {
311
- // Whole-text/fenced JSON primitives (`"yes"`, `42`, `true`, `null`)
312
- // are contract attempts that violate the object contract — refuse
313
- // and latch so callers do not YES/NO-salvage a quoted verdict.
314
- // Preamble peels (`Answer:\n"yes"`, `Answer: "yes"`, `Answer:\n42`)
315
- // are the same wrong-shaped contract; mid-line incidental quote
316
- // peels stay out of `lineLeadingJsonValues`.
317
- if (primary || lineLeading) {
318
- return { result: { ...INVALID_JUDGE_JSON }, structured: true };
319
- }
320
- continue;
321
- }
322
- if (Array.isArray(parsed)) {
323
- // Whole-text/fenced arrays are contract attempts; peeled arrays count
324
- // when verdict-shaped or yes/no string arrays (`["yes"]`). Incidental
325
- // lists (e.g. `Scores: [1,2,3]`) still allow YES/NO salvage.
326
- if (primary || arrayLooksLikeJudgeContract(parsed)) {
327
- return { result: { ...INVALID_JUDGE_JSON }, structured: true };
328
- }
329
- continue;
330
- }
331
- const record = parsed;
332
- // Objects without `verdict`: schema-shaped peels (`evidence`/`rationale`)
333
- // are failed contract attempts; bare quote blobs stay incidental.
334
- if (!("verdict" in record)) {
335
- if (primary || objectLooksLikeJudgeSchema(record)) {
336
- return { result: { ...INVALID_JUDGE_JSON }, structured: true };
337
- }
338
- continue;
339
- }
340
- structured = true;
341
- const verdict = normalizeVerdict(record.verdict);
342
- if (verdict !== undefined) {
343
- return {
344
- result: {
345
- pass: verdict === "yes",
346
- rationale: String(record.rationale ?? "").trim() || "no rationale",
347
- evidence: Array.isArray(record.evidence)
348
- ? record.evidence.map((item) => String(item)).filter(Boolean)
349
- : [],
350
- valid: true,
351
- },
352
- structured: true,
353
- };
354
- }
355
- return { result: { ...INVALID_JUDGE_JSON }, structured: true };
356
- }
357
- catch {
358
- // A failed parse latches `structured` only for real contract attempts:
359
- // the whole/fenced body is a JSON value (object/array/primitive),
360
- // including trailing junk after a primitive (`"yes" clearly`), or a
361
- // peeled candidate carries a `"verdict":` key (truncated contract).
362
- // Line-leading string/array peels (`Answer:\n"yes" clearly`,
363
- // `Answer:\n["yes"`) latch the same way. Mid-prose incidental peels
364
- // (e.g. `Evidence: {"quote":"hello"`) keep YES/NO salvage available.
365
- if (primary) {
366
- if (looksLikeJudgeJsonAttempt(candidateTrim)) {
367
- failedJsonShape = true;
368
- }
369
- }
370
- else if ((candidateTrim.startsWith("{") || candidateTrim.startsWith("[")) &&
371
- JUDGE_VERDICT_KEY_PATTERN.test(candidateTrim)) {
372
- failedJsonShape = true;
373
- }
374
- else if (lineLeading && looksLikeJudgeJsonAttempt(candidateTrim)) {
375
- failedJsonShape = true;
376
- }
377
- }
378
- }
379
- return {
380
- result: { ...INVALID_JUDGE_JSON },
381
- structured: structured || failedJsonShape || looksLikeJudgeJsonAttempt(trimmed),
382
- };
383
- }
384
- /** Parse structured judge JSON (`verdict`, `evidence`, `rationale`). */
385
- export function parseJudgeJsonResponse(text) {
386
- return tryParseJudgeJson(text).result;
387
- }
388
- const JUDGE_MARKDOWN_TRIM_PATTERN = /^\*+\s*|\s*\*+$/g;
389
- const JUDGE_TRAILING_PUNCT_PATTERN = /[.:!]+$/g;
390
- const JUDGE_TAIL_VERDICT_PATTERN = /\b(YES|NO)\.?\s*$/i;
391
- const JUDGE_INLINE_VERDICT_PATTERN = /\b(YES|NO)\b/i;
392
- function extractLegacyVerdict(line) {
393
- const stripped = line
394
- .replace(JUDGE_MARKDOWN_TRIM_PATTERN, "")
395
- .replace(JUDGE_VERDICT_PREFIX_PATTERN, "")
396
- .trim();
397
- const head = stripped.replace(JUDGE_TRAILING_PUNCT_PATTERN, "").toUpperCase();
398
- if (head === "YES" || head.startsWith("YES ")) {
399
- return "yes";
400
- }
401
- if (head === "NO" || head.startsWith("NO ")) {
402
- return "no";
403
- }
404
- const tailMatch = stripped.match(JUDGE_TAIL_VERDICT_PATTERN);
405
- if (tailMatch?.[1]) {
406
- return tailMatch[1].toLowerCase();
407
- }
408
- const inlineMatch = stripped.match(JUDGE_INLINE_VERDICT_PATTERN);
409
- if (inlineMatch?.[1]) {
410
- return inlineMatch[1].toLowerCase();
411
- }
412
- return undefined;
413
- }
414
- /** Fallback when model ignores JSON contract — parse YES/NO prose. */
415
- export function parseJudgeLegacyResponse(text) {
416
- const lines = text
417
- .trim()
418
- .split("\n")
419
- .map((line) => line.trim())
420
- .filter(Boolean);
421
- for (let index = 0; index < Math.min(lines.length, 3); index++) {
422
- const line = lines[index];
423
- if (!line) {
424
- continue;
425
- }
426
- const verdict = extractLegacyVerdict(line);
427
- if (verdict !== undefined) {
428
- const rationale = lines
429
- .filter((_, i) => i !== index)
430
- .join(" ")
431
- .trim() || line.replace(JUDGE_TAIL_VERDICT_PATTERN, "").trim();
432
- return {
433
- pass: verdict === "yes",
434
- rationale: rationale || line,
435
- evidence: [],
436
- valid: true,
437
- };
438
- }
439
- }
440
- const fallback = extractLegacyVerdict(lines[0] ?? text.trim());
441
- if (fallback !== undefined) {
442
- return {
443
- pass: fallback === "yes",
444
- rationale: lines.slice(1).join(" ").trim() || lines[0] || text,
445
- evidence: [],
446
- valid: true,
447
- };
448
- }
449
- return {
450
- pass: false,
451
- rationale: "judge returned invalid JSON (expected { verdict, evidence, rationale })",
452
- evidence: [],
453
- valid: false,
454
- };
455
- }
456
- /**
457
- * Parse structured JSON first, then legacy YES/NO prose.
458
- * Once a real JSON contract attempt is detected (array, object with `verdict`,
459
- * or truncated JSON-shaped text), never salvage YES/NO from the body — invalid
460
- * contracts are infra failures, not rubric answers.
461
- */
462
- export function parseJudgeResponse(text) {
463
- const { result, structured } = tryParseJudgeJson(text);
464
- if (result.valid || structured) {
465
- return result;
466
- }
467
- // Strip fences so preamble + ```json\n"yes"\n``` cannot salvage from the
468
- // quoted primitive; outer YES/NO beside incidental fences still works.
469
- return parseJudgeLegacyResponse(stripMarkdownFencesForLegacy(text));
470
- }
471
- function buildJudgePrompt(transcript, question, workspaces) {
472
- return [
473
- "You are a test harness classifier. You may inspect the explicitly listed read-only workspace evidence with file-read tools or read-only shell commands such as cat, sed, rg, ls, and stat. Shell commands may only inspect those evidence paths. Do not execute project code, edit files, install packages, use network access, or inspect any other path.",
474
- "Decide whether the transcript and workspace evidence satisfy the criterion.",
475
- "The transcript includes assistant text, tool names, tool args, and tool results.",
476
- "A tool result is an outcome. Use it as evidence when the criterion asks about one.",
477
- "Reply with one JSON object only — no markdown fences, no text before or after:",
478
- '{"verdict":"yes"|"no","evidence":["exact direct quote from transcript"],"rationale":"one sentence"}',
479
- "Evidence must contain one or more exact, contiguous excerpts copied from the transcript in double quotes.",
480
- "Do not paraphrase, summarize, infer, or invent evidence. If no exact quote supports the verdict, return an empty evidence array.",
481
- 'Use verdict "yes" only when evidence clearly supports the criterion.',
482
- ...formatJudgeWorkspaces(workspaces),
483
- "",
484
- "Transcript:",
485
- transcript,
486
- "",
487
- `Criterion: ${question}`,
488
- ].join("\n");
489
- }
490
- function formatJudgeWorkspaces(workspaces) {
491
- if (!workspaces?.length)
492
- return ["Workspace evidence: none was provided."];
493
- return [
494
- "Read-only workspace evidence (use only these paths; each path is immutable):",
495
- ...workspaces.map((workspace) => `- ${workspace.name}: ${workspace.path}`),
496
- ];
497
- }
498
- export function buildCompareJudgePrompt(options) {
499
- return [
500
- "You are a test harness classifier. You may inspect the explicitly listed read-only workspace evidence with file-read tools or read-only shell commands such as cat, sed, rg, ls, and stat. Shell commands may only inspect those evidence paths. Do not execute project code, edit files, install packages, use network access, or inspect any other path.",
501
- "Decide whether the transcripts and workspace evidence satisfy the criterion.",
502
- "Arm A and arm B are two agent runs of the same test.",
503
- "The transcripts include assistant text, tool names, tool args, and tool results.",
504
- "A tool result is an outcome. Use it as evidence when the criterion asks about one.",
505
- "Reply with one JSON object only — no markdown fences, no text before or after:",
506
- '{"verdict":"yes"|"no","evidence":["exact direct quote from a transcript"],"rationale":"one sentence"}',
507
- "Evidence must contain one or more exact, contiguous excerpts copied from a transcript in double quotes.",
508
- "Do not paraphrase, summarize, infer, or invent evidence. If no exact quote supports the verdict, return an empty evidence array.",
509
- 'Use verdict "yes" only when evidence clearly supports the criterion.',
510
- ...formatJudgeWorkspaces(options.workspaces),
511
- "",
512
- `Arm A (${options.aLabel}):`,
513
- options.aTranscript,
514
- "",
515
- `Arm B (${options.bLabel}):`,
516
- options.bTranscript,
517
- "",
518
- `Criterion: ${options.question}`,
519
- ].join("\n");
520
- }
521
- export function buildMultiArmCompareJudgePrompt(options) {
522
- const armBlocks = options.arms.flatMap((arm) => [`Arm ${arm.label}:`, arm.transcript, ""]);
523
- return [
524
- "You are a test harness classifier. You may inspect the explicitly listed read-only workspace evidence with file-read tools or read-only shell commands such as cat, sed, rg, ls, and stat. Shell commands may only inspect those evidence paths. Do not execute project code, edit files, install packages, use network access, or inspect any other path.",
525
- "Decide whether the transcripts and workspace evidence satisfy the criterion.",
526
- "Each arm is one agent run of the same test.",
527
- "Do not pick a single winner unless the criterion asks for one.",
528
- "The transcripts include assistant text, tool names, tool args, and tool results.",
529
- "A tool result is an outcome. Use it as evidence when the criterion asks about one.",
530
- "Reply with one JSON object only — no markdown fences, no text before or after:",
531
- '{"verdict":"yes"|"no","evidence":["exact direct quote from a transcript"],"rationale":"one sentence"}',
532
- "Evidence must contain one or more exact, contiguous excerpts copied from a transcript in double quotes.",
533
- "Do not paraphrase, summarize, infer, or invent evidence. If no exact quote supports the verdict, return an empty evidence array.",
534
- 'Use verdict "yes" only when evidence clearly supports the criterion.',
535
- ...formatJudgeWorkspaces(options.workspaces),
536
- "",
537
- ...armBlocks,
538
- `Criterion: ${options.question}`,
539
- ].join("\n");
540
- }
541
- function formatJudgeInfraError(result) {
542
- const details = [];
543
- if (result.rawStatus) {
544
- details.push(`sdk: ${result.rawStatus}`);
545
- }
546
- if (result.sdkError?.code) {
547
- details.push(`code: ${result.sdkError.code}`);
548
- }
549
- if (result.sdkError?.message) {
550
- details.push(`error: ${result.sdkError.message}`);
551
- }
552
- if (details.length === 0) {
553
- return `judge run status: ${result.status}`;
554
- }
555
- return `judge run status: ${result.status} (${details.join(", ")})`;
556
- }
557
- async function runJudgePromptOnce(prompt, options) {
558
- const started = performance.now();
559
- const host = options.host ?? "cursor";
560
- const classify = options.classify ?? runClassifier;
561
- const result = await classify({
562
- host,
563
- cwd: options.workspaces?.[0]?.path ?? options.cwd,
564
- prompt,
565
- apiKey: options.apiKey,
566
- workspaces: options.workspaces,
567
- onText: options.onText,
568
- });
569
- const durationMs = Math.round(performance.now() - started);
570
- const usage = result.usage;
571
- if (result.status !== "completed") {
572
- const infraError = formatJudgeInfraError(result);
573
- return {
574
- pass: false,
575
- rationale: infraError,
576
- evidence: [],
577
- error: infraError,
578
- infraError,
579
- rawSdkStatus: result.rawStatus ?? result.status,
580
- sdkError: result.sdkError,
581
- durationMs,
582
- usage,
583
- retryable: isTransientInfraError(infraError),
584
- };
585
- }
586
- const parsed = parseJudgeResponse(result.text);
587
- if (!parsed.valid) {
588
- const rationale = `${parsed.rationale} (raw: ${result.text.slice(0, 160) || "empty"})`;
589
- return {
590
- pass: false,
591
- rationale,
592
- evidence: [],
593
- response: result.text,
594
- error: rationale,
595
- parseError: rationale,
596
- durationMs,
597
- usage,
598
- retryable: false,
599
- };
600
- }
601
- return {
602
- pass: parsed.pass,
603
- rationale: parsed.rationale,
604
- evidence: parsed.evidence,
605
- response: result.text,
606
- durationMs,
607
- usage,
608
- retryable: false,
609
- };
610
- }
611
- async function runJudgePrompt(prompt, options) {
612
- const host = options.host ?? "cursor";
613
- const missing = missingClassifierAuth(host, options.apiKey);
614
- if (missing) {
615
- return {
616
- pass: false,
617
- rationale: missing,
618
- evidence: [],
619
- error: "missing api key",
620
- infraError: "missing api key",
621
- durationMs: 0,
622
- attempt: 1,
623
- };
624
- }
625
- const maxAttempts = resolveRetryMaxAttempts();
626
- const started = performance.now();
627
- let lastAttempt = 1;
628
- try {
629
- const { result, attempt } = await withRetry(async (attemptNumber) => {
630
- lastAttempt = attemptNumber;
631
- const outcome = await runJudgePromptOnce(prompt, options);
632
- if (outcome.retryable && attemptNumber < maxAttempts) {
633
- throw new Error(outcome.infraError ?? outcome.error ?? "judge infra error");
634
- }
635
- return outcome;
636
- }, {
637
- maxAttempts,
638
- shouldRetry: (error) => isTransientInfraError(error instanceof Error ? error.message : String(error)),
639
- });
640
- return {
641
- ...result,
642
- durationMs: Math.round(performance.now() - started),
643
- attempt,
644
- };
645
- }
646
- catch (error) {
647
- const message = error instanceof Error ? error.message : "judge prompt failed";
648
- return {
649
- pass: false,
650
- rationale: message,
651
- evidence: [],
652
- error: message,
653
- infraError: message,
654
- durationMs: Math.round(performance.now() - started),
655
- attempt: lastAttempt,
656
- };
657
- }
658
- }
659
- /** Score fuzzy rubric criteria with a structured JSON judge (live runs only). */
660
- export async function judgeTrace(trace, criteria, options) {
661
- if (criteria.length === 0) {
662
- return { verdicts: [], skipped: true };
663
- }
664
- const host = options.host ?? "cursor";
665
- const missing = missingClassifierAuth(host, options.apiKey);
666
- if (missing) {
667
- return {
668
- verdicts: [],
669
- skipped: true,
670
- error: `${missing} — judge criteria skipped`,
671
- };
672
- }
673
- const transcript = formatTraceForJudge(trace);
674
- return judgeCriteria(criteria, (question) => buildJudgePrompt(transcript, question, options.workspaces), options, {
675
- transcriptChars: transcript.length,
676
- workspaceEvidence: options.workspaces?.map((workspace) => workspace.name),
677
- });
678
- }
679
- function compareJudgeArms(input) {
680
- if ("arms" in input) {
681
- return input.arms;
682
- }
683
- return [
684
- { label: input.aLabel, trace: input.a },
685
- { label: input.bLabel, trace: input.b },
686
- ];
687
- }
688
- function isCompareJudgePair(input) {
689
- return "a" in input && "b" in input;
690
- }
691
- /** Score criteria against every arm transcript. */
692
- export async function judgeCompareTraces(input, criteria, options) {
693
- if (criteria.length === 0) {
694
- return { verdicts: [], skipped: true };
695
- }
696
- const host = options.host ?? "cursor";
697
- const missing = missingClassifierAuth(host, options.apiKey);
698
- if (missing) {
699
- return {
700
- verdicts: [],
701
- skipped: true,
702
- error: `${missing} — judge criteria skipped`,
703
- };
704
- }
705
- const pair = isCompareJudgePair(input)
706
- ? input
707
- : input.arms.length === 2 && input.arms[0] && input.arms[1]
708
- ? {
709
- aLabel: input.arms[0].label,
710
- a: input.arms[0].trace,
711
- bLabel: input.arms[1].label,
712
- b: input.arms[1].trace,
713
- }
714
- : undefined;
715
- if (pair) {
716
- const aTranscript = formatTraceForJudge(pair.a);
717
- const bTranscript = formatTraceForJudge(pair.b);
718
- return judgeCriteria(criteria, (question) => buildCompareJudgePrompt({
719
- aLabel: pair.aLabel,
720
- aTranscript,
721
- bLabel: pair.bLabel,
722
- bTranscript,
723
- question,
724
- workspaces: options.workspaces,
725
- }), options, {
726
- transcriptChars: aTranscript.length + bTranscript.length,
727
- workspaceEvidence: options.workspaces?.map((workspace) => workspace.name),
728
- });
729
- }
730
- const arms = compareJudgeArms(input).map((arm) => ({
731
- label: arm.label,
732
- transcript: formatTraceForJudge(arm.trace),
733
- }));
734
- return judgeCriteria(criteria, (question) => buildMultiArmCompareJudgePrompt({ arms, question, workspaces: options.workspaces }), options, {
735
- transcriptChars: arms.reduce((sum, arm) => sum + arm.transcript.length, 0),
736
- workspaceEvidence: options.workspaces?.map((workspace) => workspace.name),
737
- });
738
- }
739
- async function judgeCriteria(criteria, buildPrompt, options, meta) {
740
- const verdicts = [];
741
- for (const criterion of criteria) {
742
- const prompt = buildPrompt(criterion.question);
743
- options.onEvent?.({
744
- type: "criterion_started",
745
- id: criterion.id,
746
- question: criterion.question,
747
- });
748
- const parsed = await runJudgePrompt(prompt, {
749
- ...options,
750
- onText: (text) => {
751
- options.onText?.(text);
752
- options.onEvent?.({
753
- type: "text",
754
- id: criterion.id,
755
- question: criterion.question,
756
- text,
757
- });
758
- },
759
- });
760
- const verdict = {
761
- id: criterion.id,
762
- pass: parsed.pass,
763
- rationale: parsed.rationale,
764
- evidence: parsed.evidence,
765
- prompt,
766
- response: parsed.response,
767
- infraError: parsed.infraError,
768
- parseError: parsed.parseError,
769
- rawSdkStatus: parsed.rawSdkStatus,
770
- sdkError: parsed.sdkError,
771
- attempt: parsed.attempt,
772
- durationMs: parsed.durationMs,
773
- usage: parsed.usage,
774
- transcriptChars: meta.transcriptChars,
775
- promptChars: prompt.length,
776
- workspaceEvidence: meta.workspaceEvidence,
777
- };
778
- verdicts.push(verdict);
779
- options.onEvent?.({ type: "criterion_finished", question: criterion.question, verdict });
780
- if (parsed.error) {
781
- return {
782
- verdicts,
783
- skipped: false,
784
- error: parsed.error,
785
- usage: sumUsageParts(verdicts.map((v) => v.usage)),
786
- };
787
- }
788
- }
789
- return {
790
- verdicts,
791
- skipped: false,
792
- usage: sumUsageParts(verdicts.map((v) => v.usage)),
793
- };
794
- }
795
- //# sourceMappingURL=judge.js.map