@alis-build/harness-eval 0.1.2 → 0.1.4
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.
- package/README.md +187 -30
- package/dist/adapters/claude-code/index.d.ts +2 -2
- package/dist/adapters/claude-code/index.js +2 -1
- package/dist/adapters/codex/index.d.ts +68 -0
- package/dist/adapters/codex/index.js +3 -0
- package/dist/{claude-code-DZ4Vkgp6.js → claude-code-C_7hxC8z.js} +3 -245
- package/dist/claude-code-C_7hxC8z.js.map +1 -0
- package/dist/cli/bin.js +131 -151
- package/dist/cli/bin.js.map +1 -1
- package/dist/codex-0cHO2te9.js +496 -0
- package/dist/codex-0cHO2te9.js.map +1 -0
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.js +2 -2
- package/dist/{index-V22PrR0p.d.ts → index-C56AEDUr.d.ts} +2 -2
- package/dist/index.d.ts +134 -6
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/{loader-DcI0KfRX.js → loader-CiBm4Kf6.js} +491 -209
- package/dist/loader-CiBm4Kf6.js.map +1 -0
- package/dist/loader-CrmzNwkq.d.ts +107 -0
- package/dist/{projections-BcX7w-f6.js → reporter-BKCJZRYr.js} +1475 -729
- package/dist/reporter-BKCJZRYr.js.map +1 -0
- package/dist/runner/suite.d.ts +1 -1
- package/dist/runner/suite.js +1 -1
- package/dist/{suite-Dlzl-HI0.js → suite-C3-8EjUW.js} +558 -4
- package/dist/suite-C3-8EjUW.js.map +1 -0
- package/dist/{suite-DPJMIEbu.d.ts → suite-qyOGre2g.d.ts} +2 -2
- package/dist/types-Bac8_Ixb.js +246 -0
- package/dist/types-Bac8_Ixb.js.map +1 -0
- package/dist/{types-CD3TwOtZ.d.ts → types-CLt4Yygc.d.ts} +2 -2
- package/dist/{types-B9H4IZtA.d.ts → types-D0HR2WnP.d.ts} +9 -2
- package/dist/types-DFMpv_HJ.d.ts +77 -0
- package/package.json +11 -2
- package/schemas/eval-run-envelope.schema.json +193 -183
- package/dist/claude-code-DZ4Vkgp6.js.map +0 -1
- package/dist/loader-C9yQHUPC.d.ts +0 -50
- package/dist/loader-DcI0KfRX.js.map +0 -1
- package/dist/projections-BcX7w-f6.js.map +0 -1
- package/dist/suite-Dlzl-HI0.js.map +0 -1
|
@@ -1,235 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { n as TrajectoryBuilder, t as AdapterError } from "./types-Bac8_Ixb.js";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
|
-
//#region src/types/stream.ts
|
|
7
|
-
/** Type guards. Prefer these over manual `e.type === "..."` checks at call sites. */
|
|
8
|
-
function isSystemInit(e) {
|
|
9
|
-
return e.type === "system" && e.subtype === "init";
|
|
10
|
-
}
|
|
11
|
-
function isSystemRetry(e) {
|
|
12
|
-
return e.type === "system" && e.subtype === "api_retry";
|
|
13
|
-
}
|
|
14
|
-
function isAssistantMessage(e) {
|
|
15
|
-
return e.type === "assistant";
|
|
16
|
-
}
|
|
17
|
-
function isUserMessage(e) {
|
|
18
|
-
return e.type === "user";
|
|
19
|
-
}
|
|
20
|
-
function isResult(e) {
|
|
21
|
-
return e.type === "result";
|
|
22
|
-
}
|
|
23
|
-
function isTextBlock(b) {
|
|
24
|
-
return b.type === "text";
|
|
25
|
-
}
|
|
26
|
-
function isToolUseBlock(b) {
|
|
27
|
-
return b.type === "tool_use";
|
|
28
|
-
}
|
|
29
|
-
function isToolResultBlock(b) {
|
|
30
|
-
return b.type === "tool_result";
|
|
31
|
-
}
|
|
32
|
-
//#endregion
|
|
33
|
-
//#region src/types/trajectory.ts
|
|
34
|
-
/**
|
|
35
|
-
* Extract the MCP namespace prefix from a tool name.
|
|
36
|
-
*
|
|
37
|
-
* Claude Code formats MCP tool names as `mcp__<server>__<tool>`. The namespace
|
|
38
|
-
* is the first two segments joined: `mcp__<server>`. Returns null for non-MCP
|
|
39
|
-
* tool names (built-ins like `Bash`, `Read`, `Edit`).
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* namespaceOf("mcp__api__search_skills") // "mcp__api"
|
|
43
|
-
* namespaceOf("Bash") // null
|
|
44
|
-
*/
|
|
45
|
-
function namespaceOf(toolName) {
|
|
46
|
-
if (!toolName.startsWith("mcp__")) return null;
|
|
47
|
-
const parts = toolName.split("__");
|
|
48
|
-
if (parts.length < 3) return null;
|
|
49
|
-
return `${parts[0]}__${parts[1]}`;
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/trajectory/builder.ts
|
|
53
|
-
/**
|
|
54
|
-
* TrajectoryBuilder — consumes a stream of {@link StreamEvent} values and
|
|
55
|
-
* produces a {@link TrajectoryView}.
|
|
56
|
-
*
|
|
57
|
-
* State machine: the builder is a small, tolerant state machine. Invariants:
|
|
58
|
-
*
|
|
59
|
-
* - Exactly one `system/init` event opens the session. The builder requires
|
|
60
|
-
* it to be present before `build()`.
|
|
61
|
-
* - Each `assistant` event begins a new turn. Text blocks accumulate into
|
|
62
|
-
* the turn's text; `tool_use` blocks become `ToolCall` records.
|
|
63
|
-
* - `user` events with `tool_result` blocks deliver tool results back. We
|
|
64
|
-
* match them to pending calls by `tool_use_id`.
|
|
65
|
-
* - One `result` event closes the session and carries aggregate usage.
|
|
66
|
-
*
|
|
67
|
-
* The builder is *tolerant of partial streams*: a process killed mid-run
|
|
68
|
-
* produces a coherent (but flagged) view. Tool calls without matching results
|
|
69
|
-
* keep `result: null`. The `success` flag reflects whether a successful result
|
|
70
|
-
* event was actually observed.
|
|
71
|
-
*
|
|
72
|
-
* Why a class (not a reducer)?
|
|
73
|
-
* The internal `pendingCalls` map is mutable by design — we modify ToolCall
|
|
74
|
-
* objects in place when results arrive, so other parts of the view (which
|
|
75
|
-
* hold references to the same objects) see the update for free. A reducer
|
|
76
|
-
* would force a deep copy per result event, which is wasteful and would
|
|
77
|
-
* complicate identity-based queries.
|
|
78
|
-
*/
|
|
79
|
-
var TrajectoryBuilder = class {
|
|
80
|
-
meta = null;
|
|
81
|
-
sessionStartTs = null;
|
|
82
|
-
turns = [];
|
|
83
|
-
allToolCalls = [];
|
|
84
|
-
/**
|
|
85
|
-
* tool_use_id → ToolCall, for matching results back to calls.
|
|
86
|
-
* Entries are removed once a result is observed.
|
|
87
|
-
*/
|
|
88
|
-
pendingCalls = /* @__PURE__ */ new Map();
|
|
89
|
-
retries = [];
|
|
90
|
-
finalUsage = null;
|
|
91
|
-
finalCostUsd = 0;
|
|
92
|
-
finalDurationMs = 0;
|
|
93
|
-
finalNumTurns = 0;
|
|
94
|
-
finalResultText = "";
|
|
95
|
-
sawResultEvent = false;
|
|
96
|
-
resultIsError = false;
|
|
97
|
-
/**
|
|
98
|
-
* Consume one event. Safe to call with events in stream order.
|
|
99
|
-
*
|
|
100
|
-
* Unknown event types are silently ignored — the schema evolves and we
|
|
101
|
-
* don't want CI to break on a new event type we haven't modelled.
|
|
102
|
-
*/
|
|
103
|
-
consume(event) {
|
|
104
|
-
if (isSystemInit(event)) {
|
|
105
|
-
this.meta = {
|
|
106
|
-
sessionId: event.session_id,
|
|
107
|
-
model: event.model,
|
|
108
|
-
cwd: event.cwd,
|
|
109
|
-
permissionMode: event.permissionMode,
|
|
110
|
-
availableTools: event.tools ?? [],
|
|
111
|
-
mcpServers: (event.mcp_servers ?? []).map((s) => ({
|
|
112
|
-
name: s.name,
|
|
113
|
-
status: s.status
|
|
114
|
-
}))
|
|
115
|
-
};
|
|
116
|
-
this.sessionStartTs = Date.now();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (event.type === "system" && event.subtype === "api_retry") {
|
|
120
|
-
this.retries.push({
|
|
121
|
-
offsetMs: this.sessionStartTs ? Date.now() - this.sessionStartTs : 0,
|
|
122
|
-
raw: event
|
|
123
|
-
});
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (isAssistantMessage(event)) {
|
|
127
|
-
this.handleAssistantMessage(event);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (isUserMessage(event)) {
|
|
131
|
-
this.handleUserMessage(event);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (isResult(event)) {
|
|
135
|
-
this.sawResultEvent = true;
|
|
136
|
-
this.resultIsError = event.is_error;
|
|
137
|
-
this.finalUsage = event.usage ?? null;
|
|
138
|
-
this.finalCostUsd = event.total_cost_usd ?? 0;
|
|
139
|
-
this.finalDurationMs = event.duration_ms ?? 0;
|
|
140
|
-
this.finalNumTurns = event.num_turns ?? 0;
|
|
141
|
-
this.finalResultText = event.result ?? "";
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Finalize the view. Call after consuming the last event from the stream.
|
|
147
|
-
*
|
|
148
|
-
* Throws if no `system/init` was observed — at that point we have no model,
|
|
149
|
-
* no session id, and no available-tools list, which means assertions like
|
|
150
|
-
* "called any mcp__api__* tool" can't even be evaluated meaningfully.
|
|
151
|
-
*/
|
|
152
|
-
build() {
|
|
153
|
-
if (this.meta === null) throw new Error("TrajectoryBuilder.build() called before any system/init event was observed. The harness may have failed to start, or the stream was truncated before init.");
|
|
154
|
-
const lastTurn = this.turns[this.turns.length - 1];
|
|
155
|
-
const accumulatedText = this.turns.map((t) => t.text).filter((t) => t.length > 0).join("\n\n").trim();
|
|
156
|
-
return {
|
|
157
|
-
meta: this.meta,
|
|
158
|
-
toolCalls: this.allToolCalls,
|
|
159
|
-
turns: this.turns,
|
|
160
|
-
finalResponse: accumulatedText || this.finalResultText,
|
|
161
|
-
finalStopReason: lastTurn?.stopReason ?? null,
|
|
162
|
-
usage: {
|
|
163
|
-
inputTokens: this.finalUsage?.input_tokens ?? 0,
|
|
164
|
-
outputTokens: this.finalUsage?.output_tokens ?? 0,
|
|
165
|
-
totalCostUsd: this.finalCostUsd,
|
|
166
|
-
durationMs: this.finalDurationMs,
|
|
167
|
-
numTurns: this.finalNumTurns || this.turns.length
|
|
168
|
-
},
|
|
169
|
-
retries: this.retries,
|
|
170
|
-
success: this.sawResultEvent && !this.resultIsError
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
handleAssistantMessage(event) {
|
|
174
|
-
const turnIndex = this.turns.length;
|
|
175
|
-
const textChunks = [];
|
|
176
|
-
const toolCallsThisTurn = [];
|
|
177
|
-
for (const block of event.message.content) {
|
|
178
|
-
if (isTextBlock(block)) {
|
|
179
|
-
textChunks.push(block.text);
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (isToolUseBlock(block)) {
|
|
183
|
-
const call = {
|
|
184
|
-
name: block.name,
|
|
185
|
-
namespace: namespaceOf(block.name),
|
|
186
|
-
callId: block.id,
|
|
187
|
-
args: block.input,
|
|
188
|
-
result: null,
|
|
189
|
-
isError: false,
|
|
190
|
-
turnIndex,
|
|
191
|
-
callIndex: this.allToolCalls.length
|
|
192
|
-
};
|
|
193
|
-
this.allToolCalls.push(call);
|
|
194
|
-
this.pendingCalls.set(block.id, call);
|
|
195
|
-
toolCallsThisTurn.push(call);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
this.turns.push({
|
|
200
|
-
turnIndex,
|
|
201
|
-
text: textChunks.join("").trim(),
|
|
202
|
-
toolCalls: toolCallsThisTurn,
|
|
203
|
-
stopReason: event.message.stop_reason ?? null
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
handleUserMessage(event) {
|
|
207
|
-
const content = event.message.content;
|
|
208
|
-
if (typeof content === "string") return;
|
|
209
|
-
for (const block of content) {
|
|
210
|
-
if (!isToolResultBlock(block)) continue;
|
|
211
|
-
const call = this.pendingCalls.get(block.tool_use_id);
|
|
212
|
-
if (!call) continue;
|
|
213
|
-
call.result = block.content;
|
|
214
|
-
call.isError = block.is_error ?? false;
|
|
215
|
-
this.pendingCalls.delete(block.tool_use_id);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
/**
|
|
220
|
-
* Convenience: drain an async iterable of events through a fresh builder.
|
|
221
|
-
*
|
|
222
|
-
* Suitable when you have the full event stream and just want the view.
|
|
223
|
-
* For interactive/incremental scenarios (e.g. surfacing partial state in a UI)
|
|
224
|
-
* instantiate {@link TrajectoryBuilder} directly and call `consume()` /
|
|
225
|
-
* `build()` yourself.
|
|
226
|
-
*/
|
|
227
|
-
async function buildTrajectory(events) {
|
|
228
|
-
const builder = new TrajectoryBuilder();
|
|
229
|
-
for await (const event of events) builder.consume(event);
|
|
230
|
-
return builder.build();
|
|
231
|
-
}
|
|
232
|
-
//#endregion
|
|
233
7
|
//#region src/parsers/stream-json.ts
|
|
234
8
|
/**
|
|
235
9
|
* Parse a readable stream of NDJSON into a sequence of typed stream-json events.
|
|
@@ -281,22 +55,6 @@ function tryParseLine(line) {
|
|
|
281
55
|
}
|
|
282
56
|
}
|
|
283
57
|
//#endregion
|
|
284
|
-
//#region src/adapters/types.ts
|
|
285
|
-
/**
|
|
286
|
-
* Thrown when the harness fails to produce a usable trajectory.
|
|
287
|
-
*
|
|
288
|
-
* Most commonly this means the process failed before emitting a usable
|
|
289
|
-
* session init event. Inspect `diagnostics.stderr` for the cause.
|
|
290
|
-
*/
|
|
291
|
-
var AdapterError = class extends Error {
|
|
292
|
-
diagnostics;
|
|
293
|
-
constructor(message, diagnostics) {
|
|
294
|
-
super(message);
|
|
295
|
-
this.diagnostics = diagnostics;
|
|
296
|
-
this.name = "AdapterError";
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
//#endregion
|
|
300
58
|
//#region src/adapters/claude-code/flags.ts
|
|
301
59
|
/** Append repeated `--flag value` pairs for array config fields. */
|
|
302
60
|
function pushRepeatableFlag(args, flag, values) {
|
|
@@ -587,6 +345,6 @@ const claudeCodeAdapter = {
|
|
|
587
345
|
run: runClaudeCode
|
|
588
346
|
};
|
|
589
347
|
//#endregion
|
|
590
|
-
export {
|
|
348
|
+
export { parseStreamJson as a, buildJudgeArgs as i, claude_code_exports as n, runClaudeCode as r, claudeCodeAdapter as t };
|
|
591
349
|
|
|
592
|
-
//# sourceMappingURL=claude-code-
|
|
350
|
+
//# sourceMappingURL=claude-code-C_7hxC8z.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-code-C_7hxC8z.js","names":[],"sources":["../src/parsers/stream-json.ts","../src/adapters/claude-code/flags.ts","../src/adapters/claude-code/process.ts","../src/adapters/claude-code/index.ts"],"sourcesContent":["/**\n * Line-buffered NDJSON parser for Claude Code's `--output-format stream-json`.\n *\n * Claude Code emits one JSON object per line on stdout. The parser:\n * - buffers across chunk boundaries (a single JSON line may arrive in two reads)\n * - skips empty lines (defensive — shouldn't occur, but harmless if it does)\n * - emits a discriminated `ParseResult` per line so callers can decide whether\n * a malformed line should abort the run or just be logged.\n *\n * Why a generator (and not a Transform stream)?\n * The eval adapter consumes events sequentially and synchronously updates a\n * builder. Async iteration is the simplest interface for that pattern and\n * composes cleanly with `for await` in the adapter. A Transform would force\n * the builder into event-handler style.\n */\n\nimport type { Readable } from \"node:stream\";\nimport type { StreamEvent } from \"../types/stream\";\n\n/**\n * Result of attempting to parse a single line.\n *\n * Successful parses yield `{ ok: true }` with the typed event and the raw line\n * (kept for diagnostics and OTel `events.attributes.raw`). Failed parses yield\n * `{ ok: false }` with the parse error and the raw line — callers can log,\n * skip, or fail the run as they see fit.\n */\nexport type ParseResult =\n | { ok: true; event: StreamEvent; rawLine: string }\n | { ok: false; error: Error; rawLine: string };\n\n/**\n * Parse a readable stream of NDJSON into a sequence of typed stream-json events.\n *\n * @example\n * const child = spawn(\"claude\", [\"-p\", prompt, \"--output-format\", \"stream-json\", \"--verbose\"]);\n * for await (const result of parseStreamJson(child.stdout)) {\n * if (result.ok) builder.consume(result.event);\n * else console.warn(\"malformed stream line:\", result.rawLine, result.error);\n * }\n */\nexport async function* parseStreamJson(\n stream: Readable,\n): AsyncGenerator<ParseResult, void, void> {\n let buffer = \"\";\n // The Node child_process stdout is a binary stream by default. Setting the\n // encoding here means `for await (const chunk of stream)` yields strings.\n stream.setEncoding(\"utf8\");\n\n for await (const chunk of stream) {\n buffer += chunk as string;\n\n // Drain every complete line currently in the buffer before reading more.\n // Multiple JSON objects can arrive in one chunk (e.g. when the harness\n // emits a burst of events at session start).\n let newlineIdx: number;\n while ((newlineIdx = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, newlineIdx).trim();\n buffer = buffer.slice(newlineIdx + 1);\n if (line.length === 0) continue;\n yield tryParseLine(line);\n }\n }\n\n // Flush any trailing content that arrived without a final newline. Stream-json\n // typically ends with a newline-terminated `result` event, but a killed\n // process may not flush, so we still try to emit what we have.\n const trailing = buffer.trim();\n if (trailing.length > 0) {\n yield tryParseLine(trailing);\n }\n}\n\n/**\n * Parse a single line. Extracted as a helper so the generator stays readable.\n *\n * Note: we do not validate the event structure beyond `JSON.parse`. Runtime\n * validation (e.g. zod) is overkill here — the schema is stable enough at\n * runtime, and the TrajectoryBuilder is tolerant of missing fields. Adding\n * validation would be premature.\n */\nfunction tryParseLine(line: string): ParseResult {\n try {\n const event = JSON.parse(line) as StreamEvent;\n return { ok: true, event, rawLine: line };\n } catch (err) {\n return {\n ok: false,\n error: err instanceof Error ? err : new Error(String(err)),\n rawLine: line,\n };\n }\n}\n","/**\n * Build CLI args for Claude Code judge subprocesses (JSON output, not stream-json).\n *\n * Shared flag assembly for harness runs (`buildArgs`) and LLM grading judges\n * (`buildJudgeArgs`).\n */\n\nimport type { ClaudeCodeAdapterConfig, ClaudeCodeOptions } from \"./types\";\n\n/** Append repeated `--flag value` pairs for array config fields. */\nfunction pushRepeatableFlag(args: string[], flag: string, values?: string[]): void {\n if (!values) return;\n for (const value of values) {\n args.push(flag, value);\n }\n}\n\n/**\n * Append an optional CLI flag. Boolean `true` emits the flag alone; other\n * scalars emit `--flag value`.\n */\nfunction pushOptionalFlag(\n args: string[],\n flag: string,\n value: string | number | boolean | undefined,\n): void {\n if (value === undefined) return;\n if (typeof value === \"boolean\") {\n if (value) args.push(flag);\n return;\n }\n args.push(flag, String(value));\n}\n\n/** Append Claude Code CLI flags shared by harness runs and grading judges. */\nexport function appendClaudeCodeFlags(\n args: string[],\n config: ClaudeCodeOptions & { model?: string },\n): void {\n pushRepeatableFlag(args, \"--plugin-dir\", config.pluginDirs);\n pushRepeatableFlag(args, \"--plugin-url\", config.pluginUrls);\n pushRepeatableFlag(args, \"--add-dir\", config.addDirs);\n\n pushOptionalFlag(args, \"--mcp-config\", config.mcpConfig);\n pushOptionalFlag(args, \"--model\", config.model);\n pushOptionalFlag(args, \"--permission-mode\", config.permissionMode);\n pushOptionalFlag(args, \"--effort\", config.effort);\n pushOptionalFlag(args, \"--agent\", config.agent);\n pushOptionalFlag(args, \"--fallback-model\", config.fallbackModel);\n pushOptionalFlag(args, \"--tools\", config.tools);\n pushOptionalFlag(args, \"--settings\", config.settings);\n pushOptionalFlag(args, \"--setting-sources\", config.settingSources);\n pushOptionalFlag(args, \"--max-turns\", config.maxTurns);\n pushOptionalFlag(args, \"--max-budget-usd\", config.maxBudgetUsd);\n pushOptionalFlag(args, \"--system-prompt\", config.systemPrompt);\n pushOptionalFlag(args, \"--system-prompt-file\", config.systemPromptFile);\n pushOptionalFlag(args, \"--append-system-prompt\", config.appendSystemPrompt);\n pushOptionalFlag(\n args,\n \"--append-system-prompt-file\",\n config.appendSystemPromptFile,\n );\n pushOptionalFlag(args, \"--debug\", config.debug);\n pushOptionalFlag(args, \"--debug-file\", config.debugFile);\n\n if (config.allowedTools && config.allowedTools.length > 0) {\n args.push(\"--allowedTools\", config.allowedTools.join(\",\"));\n }\n\n if (config.disallowedTools && config.disallowedTools.length > 0) {\n args.push(\"--disallowedTools\", config.disallowedTools.join(\",\"));\n }\n\n pushOptionalFlag(args, \"--strict-mcp-config\", config.strictMcpConfig);\n pushOptionalFlag(args, \"--include-hook-events\", config.includeHookEvents);\n pushOptionalFlag(args, \"--no-session-persistence\", config.noSessionPersistence);\n pushOptionalFlag(args, \"--disable-slash-commands\", config.disableSlashCommands);\n pushOptionalFlag(args, \"--bare\", config.bare);\n pushOptionalFlag(args, \"--safe-mode\", config.safeMode);\n pushOptionalFlag(\n args,\n \"--allow-dangerously-skip-permissions\",\n config.allowDangerouslySkipPermissions,\n );\n pushOptionalFlag(\n args,\n \"--dangerously-skip-permissions\",\n config.dangerouslySkipPermissions,\n );\n}\n\n/**\n * Build the argument vector for spawning `claude`.\n *\n * Order matters only for flags that take values — value flags must come\n * after their flag name. Everything else is order-independent.\n */\nexport function buildArgs(config: ClaudeCodeAdapterConfig): string[] {\n const args: string[] = [\n \"-p\",\n config.prompt,\n \"--output-format\",\n \"stream-json\",\n \"--verbose\",\n ];\n\n appendClaudeCodeFlags(args, config);\n\n return args;\n}\n\n/**\n * Build args for an LLM judge subprocess (`--output-format json`).\n *\n * Defaults permission mode to `bypassPermissions` so the judge does not\n * block on tool permission prompts during single-shot JSON grading.\n */\nexport function buildJudgeArgs(\n prompt: string,\n config: ClaudeCodeOptions & { model?: string } = {},\n): string[] {\n const args: string[] = [\"-p\", prompt, \"--output-format\", \"json\"];\n const permissionMode = config.permissionMode ?? \"bypassPermissions\";\n appendClaudeCodeFlags(args, {\n ...config,\n permissionMode,\n });\n return args;\n}\n","/**\n * Process management for the Claude Code adapter.\n *\n * This module owns spawning, timeout, abort signal handling, and process-tree\n * teardown. The orchestrator (`index.ts`) consumes the returned handle —\n * reading stdout and waiting for completion — but doesn't worry about how\n * the process gets killed or how its config gets isolated.\n *\n * Why a separate module? Process management is the one part of the adapter\n * with real I/O complexity (process groups, signal escalation, temp-dir\n * lifecycle, env merging). Isolating it makes the orchestrator easy to read\n * and lets us swap the spawning logic if we later need to, e.g., wrap claude\n * in a sandbox runner.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Readable } from \"node:stream\";\n\nimport { buildArgs } from \"./flags\";\nimport type { ClaudeCodeAdapterConfig } from \"./types\";\n\n/** Default hard timeout per run. Tunable via config.timeoutMs. */\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Grace period between SIGTERM and SIGKILL. Most processes shut down cleanly\n * within a few seconds; this gives them that chance while preventing CI from\n * hanging indefinitely on a stuck child.\n */\nconst KILL_GRACE_MS = 5_000;\n\n/**\n * Handle to a spawned `claude` process. The orchestrator drives it:\n * - Read `stdout` (typically via parseStreamJson).\n * - Await `done` to learn the exit state.\n * - Await `stderrCollected` for diagnostic stderr.\n * - Check `timedOut()` after exit to distinguish kill-by-timeout from\n * normal termination.\n * - Call `cleanup()` after all of the above to remove the temp config dir.\n */\nexport interface SpawnedClaude {\n stdout: Readable;\n done: Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>;\n stderrCollected: Promise<string>;\n timedOut: () => boolean;\n cleanup: () => Promise<void>;\n}\n\n/**\n * Spawn `claude` in headless mode with isolated config and a process-group\n * lifecycle. See {@link SpawnedClaude} for how to consume the result.\n *\n * **Kill sequence:** timeout and abort both follow the same two-step path:\n * `SIGTERM` to the process group, then `SIGKILL` after {@link KILL_GRACE_MS}\n * if the group is still alive. This avoids leaving MCP/tool subprocesses\n * running while still giving claude a chance to flush stream-json output.\n *\n * @param config - Adapter options; `timeoutMs`, `signal`, and `isolateConfig`\n * control lifecycle and config isolation.\n */\nexport async function spawnClaude(\n config: ClaudeCodeAdapterConfig,\n): Promise<SpawnedClaude> {\n const binary = config.binary ?? \"claude\";\n const args = buildArgs(config);\n\n const isolateConfig = config.isolateConfig !== false;\n\n // Isolated runs use a fresh temp dir so plugins/settings don't leak between\n // reps. Non-isolated runs inherit the caller's Claude login and plugins.\n const tempConfigDir = isolateConfig\n ? await mkdtemp(join(tmpdir(), \"harness-eval-\"))\n : null;\n\n const env: Record<string, string | undefined> = {\n ...process.env,\n ...config.env,\n };\n if (tempConfigDir) {\n // Override after ...env so callers can't accidentally un-isolate.\n env.CLAUDE_CONFIG_DIR = tempConfigDir;\n }\n\n const child = spawn(binary, args, {\n cwd: config.cwd ?? process.cwd(),\n env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n // detached: true means the child becomes the leader of its own process\n // group. We exploit this to kill the entire group (including any MCP\n // server subprocesses and tool processes) on timeout/abort.\n detached: true,\n });\n\n\n // `timedOut` is set only by the hard timeout timer, not by abort — callers\n // use it to distinguish \"ran too long\" from user cancellation or normal exit.\n let timedOut = false;\n let killEscalation: NodeJS.Timeout | null = null;\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n /**\n * Arm (or re-arm) the SIGKILL fallback. Each SIGTERM attempt gets its own\n * grace window so a slow shutdown doesn't leave orphaned MCP servers.\n */\n const scheduleKillEscalation = () => {\n if (killEscalation) clearTimeout(killEscalation);\n killEscalation = setTimeout(\n () => killTree(child, \"SIGKILL\"),\n KILL_GRACE_MS,\n );\n };\n\n const timeoutTimer = setTimeout(() => {\n timedOut = true;\n killTree(child, \"SIGTERM\");\n scheduleKillEscalation();\n }, timeoutMs);\n\n // AbortSignal cancellation mirrors timeout kills but does not flip `timedOut`.\n const onAbort = () => {\n killTree(child, \"SIGTERM\");\n scheduleKillEscalation();\n };\n config.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\n // Drain stderr eagerly so the OS-level buffer never fills and stalls the\n // child (Node child processes will block on a full pipe).\n const stderrChunks: string[] = [];\n child.stderr?.setEncoding(\"utf8\");\n child.stderr?.on(\"data\", (chunk: string) => {\n stderrChunks.push(chunk);\n });\n\n const stderrCollected = new Promise<string>((resolve) => {\n const finalize = () => resolve(stderrChunks.join(\"\"));\n child.stderr?.on(\"end\", finalize);\n // Errors during stderr capture shouldn't fail the whole run; we just\n // return what we've buffered so far.\n child.stderr?.on(\"error\", finalize);\n });\n\n\n // Resolve once the process exits or fails to spawn. Guard against double\n // settlement because both `close` and `error` can fire in edge cases.\n const done = new Promise<{\n exitCode: number | null;\n signal: NodeJS.Signals | null;\n }>((resolve) => {\n let settled = false;\n const finalize = (\n exitCode: number | null,\n signal: NodeJS.Signals | null,\n ) => {\n if (settled) return;\n settled = true;\n // Tear down timers/listeners so a late timeout cannot SIGKILL a reused PID.\n clearTimeout(timeoutTimer);\n if (killEscalation) clearTimeout(killEscalation);\n config.signal?.removeEventListener(\"abort\", onAbort);\n resolve({ exitCode, signal });\n };\n\n child.on(\"close\", (code, signal) => finalize(code, signal));\n // ENOENT and other spawn failures emit `error` — `close` may not follow.\n child.on(\"error\", () => finalize(null, null));\n });\n\n\n const cleanup = async () => {\n if (!tempConfigDir) return;\n try {\n await rm(tempConfigDir, { recursive: true, force: true });\n } catch {\n // Best-effort. A leftover temp dir is annoying but not catastrophic;\n // we don't want to fail the run for it.\n }\n };\n\n // stdout is guaranteed non-null because we passed `stdio: [..., \"pipe\", ...]`.\n // The `!` is safe; the alternative would be a redundant runtime check that\n // could never fire.\n return {\n stdout: child.stdout!,\n done,\n stderrCollected,\n timedOut: () => timedOut,\n cleanup,\n };\n}\n\n/**\n * Kill the child's process group, then fall back to the bare PID if the\n * group is already gone. This catches MCP server subprocesses and tool\n * processes spawned by claude.\n *\n * **Signal escalation:** callers typically invoke this first with `SIGTERM`,\n * then again with `SIGKILL` after {@link KILL_GRACE_MS}. The group kill is\n * essential — a bare `child.kill()` would leave MCP servers running.\n *\n * **Platform edge case:** when the group leader exits first, `kill(-pid)`\n * throws `ESRCH`. The single-PID fallback covers that without failing the\n * adapter run.\n *\n * @param child - Spawned process handle from {@link spawn}.\n * @param signal - POSIX signal to deliver (`SIGTERM` or `SIGKILL` in practice).\n */\nfunction killTree(child: ChildProcess, signal: NodeJS.Signals): void {\n if (child.pid === undefined) return;\n try {\n // Negative PID targets the entire process group (requires detached spawn).\n process.kill(-child.pid, signal);\n } catch {\n try {\n // Group already reaped — try the leader PID directly.\n child.kill(signal);\n } catch {\n // Process fully gone; nothing to do.\n }\n }\n}\n","/**\n * Claude Code adapter — public API.\n */\n\nimport { parseStreamJson } from \"../../parsers/stream-json\";\nimport { TrajectoryBuilder } from \"../../trajectory/builder\";\nimport type { StreamEvent } from \"../../types/stream\";\n\nimport { AdapterError } from \"../types\";\nimport { spawnClaude } from \"./process\";\nimport type {\n AdapterDiagnostics,\n ClaudeCodeAdapterConfig,\n ClaudeCodeAdapterResult,\n ParseErrorRecord,\n} from \"./types\";\nimport type { HarnessAdapter } from \"../types\";\n\nexport { AdapterError } from \"../types\";\nexport type {\n AdapterDiagnostics,\n AdapterResult,\n ClaudeCodeAdapterConfig,\n ClaudeCodeAdapterResult,\n ClaudeCodeOptions,\n ParseErrorRecord,\n PermissionMode,\n} from \"./types\";\n\n/**\n * Run Claude Code in headless mode and return a trajectory.\n */\nexport async function runClaudeCode(\n config: ClaudeCodeAdapterConfig,\n): Promise<ClaudeCodeAdapterResult> {\n const startTs = Date.now();\n const spawned = await spawnClaude(config);\n\n const builder = new TrajectoryBuilder();\n const rawEvents: StreamEvent[] = [];\n const parseErrors: ParseErrorRecord[] = [];\n\n try {\n for await (const result of parseStreamJson(spawned.stdout)) {\n if (result.ok) {\n builder.consume(result.event);\n rawEvents.push(result.event);\n } else {\n parseErrors.push({\n line: result.rawLine,\n error: result.error.message,\n });\n }\n }\n\n const [{ exitCode, signal }, stderr] = await Promise.all([\n spawned.done,\n spawned.stderrCollected,\n ]);\n\n const diagnostics: AdapterDiagnostics = {\n exitCode,\n signal,\n stderr,\n parseErrors,\n timedOut: spawned.timedOut(),\n durationMs: Date.now() - startTs,\n };\n\n let view;\n try {\n view = builder.build();\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new AdapterError(\n `harness produced no usable trajectory: ${message}`,\n diagnostics,\n );\n }\n\n return { view, diagnostics, rawEvents };\n } finally {\n await spawned.cleanup();\n }\n}\n\n/** Registered {@link HarnessAdapter} for Claude Code headless runs. */\nexport const claudeCodeAdapter: HarnessAdapter<ClaudeCodeAdapterConfig> = {\n id: \"claude-code\",\n run: runClaudeCode,\n};\n"],"mappings":"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"}
|
package/dist/cli/bin.js
CHANGED
|
@@ -1,152 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { t as runSuite, u as getAdapter } from "../suite-
|
|
4
|
-
import { i as loadGradingConfig, t as loadSuite } from "../loader-
|
|
2
|
+
import { F as loadSuiteReport, M as gradingReportPassed, N as resolveGradeOptions, P as gradeReport, a as envelopeCommand, c as getOptionInt, i as runPipeline, j as formatGradingConsole, l as hasOption, o as parseEnvelopeProjection, p as suiteDirectoryFromPath, r as trajectoryToOtlp, s as getOption, t as formatReport, u as parseArgs } from "../reporter-BKCJZRYr.js";
|
|
3
|
+
import { t as runSuite, u as getAdapter } from "../suite-C3-8EjUW.js";
|
|
4
|
+
import { i as loadGradingConfig, o as loadSuiteDocument, t as loadSuite } from "../loader-CiBm4Kf6.js";
|
|
5
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
6
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
//#region src/cli/args.ts
|
|
9
|
-
/** Parse process argv into command, positional args, and options. */
|
|
10
|
-
function parseArgs(argv) {
|
|
11
|
-
const positional = [];
|
|
12
|
-
const options = {};
|
|
13
|
-
let command;
|
|
14
|
-
const args = [...argv];
|
|
15
|
-
if (args.length > 0 && !args[0].startsWith("-")) command = args.shift();
|
|
16
|
-
for (let i = 0; i < args.length; i++) {
|
|
17
|
-
const arg = args[i];
|
|
18
|
-
if (arg === "--") {
|
|
19
|
-
positional.push(...args.slice(i + 1));
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
if (arg.startsWith("--")) {
|
|
23
|
-
const key = arg.slice(2);
|
|
24
|
-
const next = args[i + 1];
|
|
25
|
-
if (next && !next.startsWith("-")) {
|
|
26
|
-
options[key] = next;
|
|
27
|
-
i++;
|
|
28
|
-
} else options[key] = true;
|
|
29
|
-
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
30
|
-
const key = arg.slice(1);
|
|
31
|
-
const next = args[i + 1];
|
|
32
|
-
if (next && !next.startsWith("-")) {
|
|
33
|
-
options[key] = next;
|
|
34
|
-
i++;
|
|
35
|
-
} else options[key] = true;
|
|
36
|
-
} else positional.push(arg);
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
command,
|
|
40
|
-
positional,
|
|
41
|
-
options
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/** Return a string option value, or undefined when absent or boolean. */
|
|
45
|
-
function getOption(options, name) {
|
|
46
|
-
const v = options[name];
|
|
47
|
-
return typeof v === "string" ? v : void 0;
|
|
48
|
-
}
|
|
49
|
-
/** Parse an integer option with fallback when absent or non-numeric. */
|
|
50
|
-
function getOptionInt(options, name, defaultValue) {
|
|
51
|
-
const v = getOption(options, name);
|
|
52
|
-
if (v === void 0) return defaultValue;
|
|
53
|
-
const n = Number.parseInt(v, 10);
|
|
54
|
-
if (!Number.isFinite(n)) return defaultValue;
|
|
55
|
-
return n;
|
|
56
|
-
}
|
|
57
|
-
/** True when a boolean flag is set or explicitly `"true"`. */
|
|
58
|
-
function hasOption(options, name) {
|
|
59
|
-
const v = options[name];
|
|
60
|
-
return v === true || typeof v === "string" && v === "true";
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/cli/commands/envelope.ts
|
|
64
|
-
/**
|
|
65
|
-
* `harness-eval envelope` — build EvalRunEnvelope and interchange projections.
|
|
66
|
-
*
|
|
67
|
-
* Reads a suite run report (and optional grading JSON), builds a versioned
|
|
68
|
-
* {@link EvalRunEnvelope}, and serializes one of three projections:
|
|
69
|
-
*
|
|
70
|
-
* - `envelope` — full nested JSON document (default)
|
|
71
|
-
* - `trajectory` — JSONL of {@link EvalDatasetRow} per repetition
|
|
72
|
-
* - `instances` — JSONL of {@link InstancesJsonlRow} for Vertex batch upload
|
|
73
|
-
*
|
|
74
|
-
* Exit code 0 when behavioral pass, 1 when any cell failed assertions.
|
|
75
|
-
*/
|
|
76
|
-
const PROJECTIONS = /* @__PURE__ */ new Set([
|
|
77
|
-
"envelope",
|
|
78
|
-
"trajectory",
|
|
79
|
-
"instances"
|
|
80
|
-
]);
|
|
81
|
-
/**
|
|
82
|
-
* Parse and validate `--projection` CLI flag.
|
|
83
|
-
*
|
|
84
|
-
* @returns `"envelope"` when omitted; `undefined` when value is invalid.
|
|
85
|
-
*/
|
|
86
|
-
function parseEnvelopeProjection(value) {
|
|
87
|
-
if (value === void 0) return "envelope";
|
|
88
|
-
if (PROJECTIONS.has(value)) return value;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Serialize an envelope to stdout/file string for the chosen projection.
|
|
92
|
-
*
|
|
93
|
-
* Trajectory and instances projections emit NDJSON (one JSON object per line).
|
|
94
|
-
*/
|
|
95
|
-
function serializeEnvelopeProjection(envelope, projection) {
|
|
96
|
-
switch (projection) {
|
|
97
|
-
case "trajectory": return `${toTrajectory(envelope).map((row) => JSON.stringify(row)).join("\n")}\n`;
|
|
98
|
-
case "instances": return `${toInstancesJsonl(envelope).map((row) => JSON.stringify(row)).join("\n")}\n`;
|
|
99
|
-
default: return `${JSON.stringify(envelope, null, 2)}\n`;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/** Read harness-eval package version for envelope harness.frameworkVersion. */
|
|
103
|
-
async function readFrameworkVersion() {
|
|
104
|
-
try {
|
|
105
|
-
const text = await readFile(join(dirname(fileURLToPath(import.meta.url)), "../../../package.json"), "utf8");
|
|
106
|
-
return JSON.parse(text).version;
|
|
107
|
-
} catch {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* CLI entry point for the `envelope` subcommand.
|
|
113
|
-
*
|
|
114
|
-
* @returns Process exit code: 0 on behavioral pass, 1 on failure, 2 on usage/error.
|
|
115
|
-
*/
|
|
116
|
-
async function envelopeCommand(args) {
|
|
117
|
-
const reportPath = args.positional[0];
|
|
118
|
-
if (!reportPath) {
|
|
119
|
-
console.error("usage: harness-eval envelope <report.json> [--output path] [--grading path] [--suite path] [--projection envelope|trajectory|instances] [--include-raw-stream-events] [--no-transcript]");
|
|
120
|
-
return 2;
|
|
121
|
-
}
|
|
122
|
-
const outputPath = getOption(args.options, "output");
|
|
123
|
-
const gradingPath = getOption(args.options, "grading");
|
|
124
|
-
const suitePath = getOption(args.options, "suite");
|
|
125
|
-
const projection = parseEnvelopeProjection(getOption(args.options, "projection"));
|
|
126
|
-
if (!projection) {
|
|
127
|
-
console.error("invalid --projection; expected envelope, trajectory, or instances");
|
|
128
|
-
return 2;
|
|
129
|
-
}
|
|
130
|
-
let envelope;
|
|
131
|
-
try {
|
|
132
|
-
const frameworkVersion = await readFrameworkVersion();
|
|
133
|
-
envelope = await buildEvalRunEnvelopeFromFiles(reportPath, {
|
|
134
|
-
gradingPath,
|
|
135
|
-
suitePath,
|
|
136
|
-
includeTranscript: !hasOption(args.options, "no-transcript"),
|
|
137
|
-
includeRawStreamEvents: hasOption(args.options, "include-raw-stream-events"),
|
|
138
|
-
harness: { frameworkVersion }
|
|
139
|
-
});
|
|
140
|
-
} catch (err) {
|
|
141
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
142
|
-
return 2;
|
|
143
|
-
}
|
|
144
|
-
const serialized = serializeEnvelopeProjection(envelope, projection);
|
|
145
|
-
if (outputPath) await writeFile(outputPath, serialized, "utf8");
|
|
146
|
-
else process.stdout.write(serialized);
|
|
147
|
-
return envelope.summary.behavioralPass ? 0 : 1;
|
|
148
|
-
}
|
|
149
|
-
//#endregion
|
|
150
8
|
//#region src/cli/commands/format.ts
|
|
151
9
|
/**
|
|
152
10
|
* `harness-eval format` command.
|
|
@@ -507,10 +365,11 @@ function optionalOptionInt(options, name) {
|
|
|
507
365
|
async function gradeCommand(args) {
|
|
508
366
|
const reportPath = args.positional[0];
|
|
509
367
|
if (!reportPath) {
|
|
510
|
-
console.error("usage: harness-eval grade <report.json> [--config grading.yaml] [--expectations path] [--output path] [--model id] [--timeout-ms N] [--max-concurrent N]");
|
|
368
|
+
console.error("usage: harness-eval grade <report.json> [--config grading.yaml] [--suite suite.yaml] [--expectations path] [--output path] [--model id] [--timeout-ms N] [--max-concurrent N]");
|
|
511
369
|
return 2;
|
|
512
370
|
}
|
|
513
371
|
const configPath = getOption(args.options, "config");
|
|
372
|
+
const suitePath = getOption(args.options, "suite");
|
|
514
373
|
const expectationsPath = getOption(args.options, "expectations");
|
|
515
374
|
const outputPath = getOption(args.options, "output");
|
|
516
375
|
const model = getOption(args.options, "model");
|
|
@@ -521,8 +380,13 @@ async function gradeCommand(args) {
|
|
|
521
380
|
const progressMode = resolveProgressMode(args.options);
|
|
522
381
|
const useProgressColor = progressMode !== "json" && resolveProgressColor(args.options);
|
|
523
382
|
let fileConfig;
|
|
524
|
-
|
|
525
|
-
|
|
383
|
+
const gradingConfigPath = configPath ?? suitePath;
|
|
384
|
+
if (configPath && suitePath) {
|
|
385
|
+
console.error("grade: use only one of --config or --suite");
|
|
386
|
+
return 2;
|
|
387
|
+
}
|
|
388
|
+
if (gradingConfigPath) try {
|
|
389
|
+
fileConfig = await loadGradingConfig(gradingConfigPath);
|
|
526
390
|
} catch (err) {
|
|
527
391
|
console.error(err instanceof Error ? err.message : String(err));
|
|
528
392
|
return 2;
|
|
@@ -543,7 +407,7 @@ async function gradeCommand(args) {
|
|
|
543
407
|
binary,
|
|
544
408
|
timeoutMs,
|
|
545
409
|
maxConcurrent
|
|
546
|
-
}, configPath);
|
|
410
|
+
}, configPath ?? suitePath);
|
|
547
411
|
} catch (err) {
|
|
548
412
|
console.error(err instanceof Error ? err.message : String(err));
|
|
549
413
|
return 2;
|
|
@@ -570,6 +434,120 @@ async function gradeCommand(args) {
|
|
|
570
434
|
return gradingReportPassed(grading) ? 0 : 1;
|
|
571
435
|
}
|
|
572
436
|
//#endregion
|
|
437
|
+
//#region src/cli/commands/pipeline.ts
|
|
438
|
+
/**
|
|
439
|
+
* `harness-eval pipeline` — orchestrate run → grade → envelope from suite.yaml.
|
|
440
|
+
*/
|
|
441
|
+
/** Read package version for envelope provenance (best-effort). */
|
|
442
|
+
async function readFrameworkVersion() {
|
|
443
|
+
try {
|
|
444
|
+
const text = await readFile(join(dirname(fileURLToPath(import.meta.url)), "../../../package.json"), "utf8");
|
|
445
|
+
return JSON.parse(text).version;
|
|
446
|
+
} catch {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Resolve CLI path overrides relative to the suite directory unless absolute or `~/`. */
|
|
451
|
+
function resolveOverridePath(value, suiteDir) {
|
|
452
|
+
if (!value) return void 0;
|
|
453
|
+
return isAbsolute(value) || value.startsWith("~/") ? value : join(suiteDir, value);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Execute `harness-eval pipeline`.
|
|
457
|
+
*
|
|
458
|
+
* @returns Step exit code (0 pass, 1 eval fail, 2 usage/load error).
|
|
459
|
+
*/
|
|
460
|
+
async function pipelineCommand(args) {
|
|
461
|
+
const suitePath = args.positional[0];
|
|
462
|
+
if (!suitePath) {
|
|
463
|
+
console.error("usage: harness-eval pipeline <suite.yaml|dir> [--steps run,grade,envelope] [--output path] [--grading path] [--report path] [--max-concurrent N] [--progress default|quiet|verbose|json]");
|
|
464
|
+
return 2;
|
|
465
|
+
}
|
|
466
|
+
let doc;
|
|
467
|
+
try {
|
|
468
|
+
doc = await loadSuiteDocument(suitePath);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
471
|
+
return 2;
|
|
472
|
+
}
|
|
473
|
+
if (!doc.pipeline) {
|
|
474
|
+
console.error("suite.yaml has no pipeline block; use run, grade, and envelope commands separately");
|
|
475
|
+
return 2;
|
|
476
|
+
}
|
|
477
|
+
const suiteDir = suiteDirectoryFromPath(doc.suitePath);
|
|
478
|
+
const steps = getOption(args.options, "steps");
|
|
479
|
+
const maxConcurrent = getOptionInt(args.options, "max-concurrent", 4);
|
|
480
|
+
const progressMode = resolveProgressMode(args.options);
|
|
481
|
+
const useProgressColor = progressMode !== "json" && resolveProgressColor(args.options);
|
|
482
|
+
const projection = parseEnvelopeProjection(getOption(args.options, "projection"));
|
|
483
|
+
if (getOption(args.options, "projection") && !projection) {
|
|
484
|
+
console.error("invalid --projection; expected envelope, trajectory, or instances");
|
|
485
|
+
return 2;
|
|
486
|
+
}
|
|
487
|
+
const overrides = {};
|
|
488
|
+
const runOutput = getOption(args.options, "output");
|
|
489
|
+
if (runOutput) overrides.run = {
|
|
490
|
+
output: resolveOverridePath(runOutput, suiteDir),
|
|
491
|
+
maxConcurrent
|
|
492
|
+
};
|
|
493
|
+
const reportOverride = getOption(args.options, "report");
|
|
494
|
+
if (reportOverride) {
|
|
495
|
+
overrides.grade = {
|
|
496
|
+
...overrides.grade,
|
|
497
|
+
input: resolveOverridePath(reportOverride, suiteDir)
|
|
498
|
+
};
|
|
499
|
+
overrides.envelope = {
|
|
500
|
+
...overrides.envelope,
|
|
501
|
+
report: resolveOverridePath(reportOverride, suiteDir)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const gradingOutput = getOption(args.options, "grading-output");
|
|
505
|
+
if (gradingOutput) overrides.grade = {
|
|
506
|
+
...overrides.grade,
|
|
507
|
+
output: resolveOverridePath(gradingOutput, suiteDir)
|
|
508
|
+
};
|
|
509
|
+
const gradingInput = getOption(args.options, "grading");
|
|
510
|
+
if (gradingInput) overrides.envelope = {
|
|
511
|
+
...overrides.envelope,
|
|
512
|
+
grading: resolveOverridePath(gradingInput, suiteDir)
|
|
513
|
+
};
|
|
514
|
+
const envelopeOutput = getOption(args.options, "envelope-output");
|
|
515
|
+
if (envelopeOutput) overrides.envelope = {
|
|
516
|
+
...overrides.envelope,
|
|
517
|
+
output: resolveOverridePath(envelopeOutput, suiteDir)
|
|
518
|
+
};
|
|
519
|
+
if (projection) overrides.envelope = {
|
|
520
|
+
...overrides.envelope,
|
|
521
|
+
projection
|
|
522
|
+
};
|
|
523
|
+
if (doc.pipeline.grade && !doc.judge) {
|
|
524
|
+
console.error("pipeline grade step requires inline judge: block in suite.yaml");
|
|
525
|
+
return 2;
|
|
526
|
+
}
|
|
527
|
+
const frameworkVersion = await readFrameworkVersion();
|
|
528
|
+
try {
|
|
529
|
+
return (await runPipeline(doc, {
|
|
530
|
+
steps,
|
|
531
|
+
maxConcurrent,
|
|
532
|
+
overrides,
|
|
533
|
+
frameworkVersion,
|
|
534
|
+
onRunProgress: createRunProgressHandler({
|
|
535
|
+
mode: progressMode,
|
|
536
|
+
maxConcurrent,
|
|
537
|
+
color: useProgressColor
|
|
538
|
+
}),
|
|
539
|
+
onGradeProgress: createGradeProgressHandler({
|
|
540
|
+
mode: progressMode,
|
|
541
|
+
maxConcurrent,
|
|
542
|
+
color: useProgressColor
|
|
543
|
+
})
|
|
544
|
+
})).exitCode;
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
547
|
+
return 2;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
//#endregion
|
|
573
551
|
//#region src/cli/commands/otel-output.ts
|
|
574
552
|
/**
|
|
575
553
|
* Write OTLP JSON artifacts from a suite report.
|
|
@@ -670,8 +648,9 @@ const USAGE = `harness-eval — harness-level eval framework
|
|
|
670
648
|
|
|
671
649
|
Usage:
|
|
672
650
|
harness-eval run <suite.yaml> [--max-concurrent N] [--baseline path] [--output path] [--otel-output dir] [--format console|markdown|json] [--adapter id] [--quiet] [--verbose] [--progress default|quiet|verbose|json]
|
|
673
|
-
harness-eval grade <report.json> [--config grading.yaml] [--expectations path] [--output path] [--model id] [--timeout-ms N] [--max-concurrent N] [--format console|json] [--quiet] [--verbose] [--progress default|quiet|verbose|json]
|
|
651
|
+
harness-eval grade <report.json> [--config grading.yaml] [--suite suite.yaml] [--expectations path] [--output path] [--model id] [--timeout-ms N] [--max-concurrent N] [--format console|json] [--quiet] [--verbose] [--progress default|quiet|verbose|json]
|
|
674
652
|
harness-eval envelope <report.json> [--output path] [--grading path] [--suite path] [--projection envelope|trajectory|instances] [--include-raw-stream-events] [--no-transcript]
|
|
653
|
+
harness-eval pipeline <suite.yaml|dir> [--steps run,grade,envelope] [--output path] [--grading path] [--grading-output path] [--envelope-output path] [--report path] [--projection envelope|trajectory|instances] [--max-concurrent N] [--progress default|quiet|verbose|json]
|
|
675
654
|
harness-eval format <report.json> [--format console|markdown|json] [--baseline path]
|
|
676
655
|
harness-eval --help
|
|
677
656
|
|
|
@@ -698,6 +677,7 @@ async function main(argv) {
|
|
|
698
677
|
case "run": return await runCommand(parsed);
|
|
699
678
|
case "grade": return await gradeCommand(parsed);
|
|
700
679
|
case "envelope": return await envelopeCommand(parsed);
|
|
680
|
+
case "pipeline": return await pipelineCommand(parsed);
|
|
701
681
|
case "format": return await formatCommand(parsed);
|
|
702
682
|
case void 0:
|
|
703
683
|
console.error(USAGE);
|