@crewhaus/eval-runner 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/exam.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type { ExamRunner, MemoryWiringFragment, ThredzConnection } from "@crewhaus/memory-service";
2
+ import type { RunContext } from "@crewhaus/run-context";
3
+ import type { AgentInvoker } from "./types";
4
+ /** The exam-session preamble appended to the harness instructions — the
5
+ * grounding contract the demo's graders (`grounded_not_bluffed`) test. */
6
+ export declare const EXAM_SESSION_PREAMBLE: string;
7
+ /** The exact runChatLoop option subset an exam session uses — a dedicated
8
+ * type so tests can inject a capturing `chatLoop` and pin every field
9
+ * (the dream-cli `DreamChatLoopOptions` pattern). */
10
+ export type ExamChatLoopOptions = {
11
+ readonly model: string;
12
+ readonly instructions: string;
13
+ readonly runContext: RunContext;
14
+ readonly singleTurn: true;
15
+ readonly seedMessages: ReadonlyArray<{
16
+ readonly role: "user";
17
+ readonly content: string;
18
+ }>;
19
+ readonly sessionName: string;
20
+ readonly sessionTarget: "eval";
21
+ readonly sessionRootDir: string;
22
+ readonly tools: ReadonlyArray<never>;
23
+ readonly hooks: ReadonlyArray<never>;
24
+ readonly memory?: {
25
+ readonly autoRecall: true;
26
+ readonly recallK: number;
27
+ readonly recallSeed: string;
28
+ readonly recall: (query: string, k: number) => Promise<readonly string[]>;
29
+ };
30
+ readonly spinner: false;
31
+ };
32
+ export type ExamChatLoopFn = (opts: ExamChatLoopOptions) => Promise<string>;
33
+ export type CreateExamRunnerOptions = {
34
+ readonly specName: string;
35
+ /** The harness's own model — exam sessions and (by default) the judge
36
+ * grader run on it. */
37
+ readonly model: string;
38
+ /** The harness's own instructions — the examinee is THIS harness. */
39
+ readonly instructions: string;
40
+ /** The memory fragment (`memoryFragmentFromIr(ir)`) — `wireWiki` grounds
41
+ * each question in the same wiki surface the harness runs with. */
42
+ readonly fragment: MemoryWiringFragment;
43
+ /** The harness working directory (stores + default artifact root). */
44
+ readonly cwd: string;
45
+ /** The live Thredz connection when the wiki backend is hosted (§4.3);
46
+ * null/absent → the local store (or no grounding when no wiki). */
47
+ readonly thredz?: ThredzConnection | null;
48
+ /** Judge model for `llm_judge` graders. Default: the harness model. */
49
+ readonly judgeModel?: string;
50
+ readonly concurrency?: number;
51
+ /** Wiki lines recalled per question. Default 6 (§9 `wiki.recallK`). */
52
+ readonly recallK?: number;
53
+ /** Artifact root override. Default `<cwd>/.crewhaus/evals/exam-<stamp>`. */
54
+ readonly outDir?: string;
55
+ /** Full invoker override (tests / bespoke examinees). */
56
+ readonly invoker?: AgentInvoker;
57
+ /** Session-runner seam under the default invoker (tests pin options). */
58
+ readonly chatLoop?: ExamChatLoopFn;
59
+ readonly now?: () => Date;
60
+ };
61
+ /**
62
+ * Build the injected exam runner (`wireMemory({ examRunner })`). Pure
63
+ * construction — nothing is read until the returned runner is invoked by
64
+ * the `run_exam` tool, so a harness that never sits its exam pays nothing.
65
+ */
66
+ export declare function createExamRunner(opts: CreateExamRunnerOptions): ExamRunner;
package/dist/exam.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * v0.3.0 Goal 2 (design §3.3 EXAM, PR 17) — `createExamRunner`: the
3
+ * reference implementation of memory-service's injected `ExamRunner` seam.
4
+ *
5
+ * The expert demo's `/exam` shelled out to `crewhaus eval` via Bash from
6
+ * inside the agent — the harness invoking its own CLI, requiring a Bash
7
+ * permission just to sit an exam. This is the first-class replacement: a
8
+ * PROGRAMMATIC eval invocation built from this package's own machinery
9
+ * (`loadDataset` + `parseGradersConfig` + `runSample`/`Semaphore`), consumed
10
+ * by the `run_exam` tool through `wireMemory({ examRunner })` on both the
11
+ * `crewhaus run` interpreter path and compiled cli bundles.
12
+ *
13
+ * Exam semantics: each dataset question runs in a FRESH single-turn session
14
+ * — the harness's own model + instructions plus an exam preamble — grounded
15
+ * in the REAL wiki through the backend-invariant `wireWiki(...).recall` seam
16
+ * (local store or Thredz MCP client; recall lines are injected through
17
+ * runChatLoop's `memory` seam, so recalled bodies still cross the boundary
18
+ * classifier + delimiter escaping like every other recall). No tools are
19
+ * exposed to the exam session: the question is "can you answer from what the
20
+ * wiki knows", graded by the spec's own graders — never "can you edit the
21
+ * wiki mid-exam". Unlike `runEval`'s per-sample fabric isolation (§7.2),
22
+ * the exam deliberately reads the live wiki — an isolated empty wiki would
23
+ * examine amnesia.
24
+ *
25
+ * Per-sample artifacts land under `.crewhaus/evals/exam-<stamp>/<sampleId>/`
26
+ * via `runSample` (transcript/events/grades/meta), so failed exams are
27
+ * triageable with the same tooling as any eval run.
28
+ */
29
+ import { readFileSync } from "node:fs";
30
+ import { mkdirSync } from "node:fs";
31
+ import { join } from "node:path";
32
+ import { loadDataset } from "@crewhaus/eval-dataset";
33
+ import { parseGradersConfig } from "@crewhaus/eval-grader";
34
+ import { createJudgeGrader, loadRubric } from "@crewhaus/eval-judge";
35
+ import { wireWiki } from "@crewhaus/memory-service";
36
+ import { runChatLoop } from "@crewhaus/runtime-core";
37
+ import { RunnerError } from "./errors";
38
+ import { runSample } from "./run-sample";
39
+ import { Semaphore } from "./semaphore";
40
+ /** Exam sessions run a couple at a time — an exam is an interactive,
41
+ * user-initiated pass, not a benchmark sweep. */
42
+ const DEFAULT_EXAM_CONCURRENCY = 2;
43
+ /** Wiki lines recalled per question (mirrors §9's `wiki.recallK` default). */
44
+ const DEFAULT_EXAM_RECALL_K = 6;
45
+ /** The exam-session preamble appended to the harness instructions — the
46
+ * grounding contract the demo's graders (`grounded_not_bluffed`) test. */
47
+ export const EXAM_SESSION_PREAMBLE = [
48
+ "You are sitting your competency exam. Answer the question in this one turn:",
49
+ "- answer ONLY from the recalled wiki context above, citing article slugs like (topic/slug);",
50
+ "- numbers, ranges, and units must be exact;",
51
+ "- if the recalled context does not support an answer, say plainly what you do not know instead of guessing.",
52
+ ].join("\n");
53
+ /**
54
+ * Build the injected exam runner (`wireMemory({ examRunner })`). Pure
55
+ * construction — nothing is read until the returned runner is invoked by
56
+ * the `run_exam` tool, so a harness that never sits its exam pays nothing.
57
+ */
58
+ export function createExamRunner(opts) {
59
+ return async ({ datasetPath, gradersPath }) => {
60
+ // 1. Graders — parse the spec's graders.yaml; `llm_judge` placeholders
61
+ // bind to the judge model (per-grader override > opts.judgeModel > the
62
+ // harness model), exactly like runEval's resolution.
63
+ let gradersYaml;
64
+ try {
65
+ gradersYaml = readFileSync(gradersPath, "utf-8");
66
+ }
67
+ catch (err) {
68
+ throw new RunnerError(`exam: cannot read graders "${gradersPath}" (learning.exam.graders): ${err.message}`);
69
+ }
70
+ const { compiled } = parseGradersConfig(gradersYaml);
71
+ const graders = compiled.map((g) => {
72
+ if (g.judgeSpec) {
73
+ const rubric = loadRubric(g.judgeSpec.rubric);
74
+ const model = g.judgeSpec.model ?? opts.judgeModel ?? opts.model;
75
+ return { name: g.name, grader: createJudgeGrader(rubric, { model }) };
76
+ }
77
+ // v0.3.0 final integration (PR 17 ∩ PR 19): `type: registry` graders
78
+ // resolve against RunEvalOptions.graderRegistry — a seam run_exam does
79
+ // not carry (the exam is invoked from inside a running harness, which
80
+ // has no grader registry to hand over). Reject LOUDLY at exam start,
81
+ // exactly like runEval does, instead of letting the compiled
82
+ // placeholder throw per-sample as confusing grader-infra noise.
83
+ if (g.registrySpec) {
84
+ throw new RunnerError(`exam: grader "${g.name}" is \`type: registry\` (→ "${g.registrySpec.grader}") — registry graders resolve via RunEvalOptions.graderRegistry under \`crewhaus eval\` and are not available to \`run_exam\`; use code/llm_judge graders in learning.exam.graders`);
85
+ }
86
+ return { name: g.name, grader: g.grader };
87
+ });
88
+ // 2. Dataset — materialized like runEval (exams are small by design).
89
+ let samples;
90
+ try {
91
+ const dataset = await loadDataset(datasetPath);
92
+ samples = [];
93
+ for await (const s of dataset.samples)
94
+ samples.push(s);
95
+ }
96
+ catch (err) {
97
+ throw err instanceof RunnerError
98
+ ? err
99
+ : new RunnerError(`exam: cannot load dataset "${datasetPath}" (learning.exam.dataset): ${err.message}`);
100
+ }
101
+ if (samples.length === 0) {
102
+ throw new RunnerError(`exam: dataset "${datasetPath}" yielded zero samples`);
103
+ }
104
+ const invoker = opts.invoker ?? buildExamInvoker(opts);
105
+ const outDir = opts.outDir ?? join(opts.cwd, ".crewhaus", "evals", `exam-${examStamp(opts.now)}`);
106
+ mkdirSync(outDir, { recursive: true });
107
+ // 3. Run — per-sample artifacts + grading via the shared runSample path.
108
+ const sem = new Semaphore(opts.concurrency ?? DEFAULT_EXAM_CONCURRENCY);
109
+ const results = await Promise.all(samples.map(async (sample) => {
110
+ const release = await sem.acquire();
111
+ try {
112
+ return await runSample({
113
+ sample,
114
+ invoker,
115
+ graders,
116
+ outDir,
117
+ model: opts.model,
118
+ // PR 19's artifact seam: stamp the examinee's spec name so any
119
+ // artifact-reading grader addresses the right state root.
120
+ specName: opts.specName,
121
+ });
122
+ }
123
+ finally {
124
+ release();
125
+ }
126
+ }));
127
+ const bySampleId = new Map(samples.map((s) => [s.id, s]));
128
+ const outcomes = results.map((r) => {
129
+ const sample = bySampleId.get(r.sampleId);
130
+ return {
131
+ sampleId: r.sampleId,
132
+ input: sample?.input ?? "",
133
+ passed: r.grades.overall.passed,
134
+ score: r.grades.overall.score,
135
+ rationale: r.grades.overall.rationale,
136
+ agentOutput: r.agentOutput,
137
+ ...(r.error !== undefined ? { error: r.error } : {}),
138
+ };
139
+ });
140
+ const passed = outcomes.filter((o) => o.passed).length;
141
+ const report = {
142
+ total: outcomes.length,
143
+ passed,
144
+ failed: outcomes.length - passed,
145
+ passRate: outcomes.length === 0 ? 0 : passed / outcomes.length,
146
+ outcomes,
147
+ outDir,
148
+ };
149
+ await Bun.write(join(outDir, "exam.json"), JSON.stringify(report, null, 2));
150
+ return report;
151
+ };
152
+ }
153
+ /** The default examinee: one fresh single-turn session per question,
154
+ * wiki-grounded through runChatLoop's memory seam, zero tools. */
155
+ function buildExamInvoker(opts) {
156
+ const chatLoop = opts.chatLoop ?? runChatLoop;
157
+ const wiki = wireWiki({
158
+ specName: opts.fragment.specName,
159
+ ...(opts.fragment.memory !== undefined ? { memory: opts.fragment.memory } : {}),
160
+ ...(opts.fragment.thredz !== undefined ? { thredz: opts.fragment.thredz } : {}),
161
+ }, {
162
+ // A registrar is required by the deps type but the exam session never
163
+ // advertises the wiki tools — recall is the only surface it gets.
164
+ catalog: { register: () => undefined },
165
+ cwd: opts.cwd,
166
+ ...(opts.thredz !== undefined ? { thredz: opts.thredz } : {}),
167
+ });
168
+ const recallK = opts.recallK ?? opts.fragment.memory?.wiki?.recallK ?? DEFAULT_EXAM_RECALL_K;
169
+ return async (req) => {
170
+ const agentOutput = await chatLoop({
171
+ model: opts.model,
172
+ instructions: `${opts.instructions}\n\n${EXAM_SESSION_PREAMBLE}`,
173
+ runContext: req.runContext,
174
+ singleTurn: true,
175
+ seedMessages: [{ role: "user", content: req.sample.input }],
176
+ sessionName: `${opts.specName}-exam`,
177
+ sessionTarget: "eval",
178
+ sessionRootDir: req.sessionRootDir,
179
+ tools: [],
180
+ hooks: [],
181
+ ...(wiki !== null
182
+ ? {
183
+ memory: {
184
+ autoRecall: true,
185
+ recallK,
186
+ recallSeed: req.sample.input,
187
+ recall: wiki.recall,
188
+ },
189
+ }
190
+ : {}),
191
+ spinner: false,
192
+ });
193
+ return { agentOutput };
194
+ };
195
+ }
196
+ function examStamp(now) {
197
+ const d = (now ?? (() => new Date()))();
198
+ return d
199
+ .toISOString()
200
+ .replace(/[:.]/g, "-")
201
+ .replace(/-\d{3}Z$/, "Z");
202
+ }
package/dist/index.d.ts CHANGED
@@ -5,11 +5,12 @@ import { aggregate } from "./aggregate";
5
5
  import { RunnerError } from "./errors";
6
6
  import { runSample } from "./run-sample";
7
7
  import { Semaphore } from "./semaphore";
8
- import type { AgentInvoker, EvalRunSummary, GraderEntry, RunEvalOptions, SampleResult } from "./types";
8
+ import type { AgentInvoker, EvalRunSummary, GraderEntry, GraderLookup, RunEvalOptions, SampleResult } from "./types";
9
9
  import { type SharedAgentDeps, wireRunOnce } from "./wire-once";
10
- export type { AgentInvoker, EvalRunSummary, GraderEntry, RunEvalOptions, SampleResult };
10
+ export type { AgentInvoker, EvalRunSummary, GraderEntry, GraderLookup, RunEvalOptions, SampleResult, };
11
11
  export type { SharedAgentDeps };
12
12
  export { wireRunOnce };
13
+ export { EXAM_SESSION_PREAMBLE, createExamRunner, type CreateExamRunnerOptions, type ExamChatLoopFn, type ExamChatLoopOptions, } from "./exam";
13
14
  export { Semaphore };
14
15
  export { aggregate };
15
16
  export { RunnerError };
package/dist/index.js CHANGED
@@ -24,7 +24,9 @@
24
24
  import { mkdirSync } from "node:fs";
25
25
  import { join } from "node:path";
26
26
  import { createJudgeGrader, loadRubric } from "@crewhaus/eval-judge";
27
+ import { memoryFragmentFromIr, wireMemory } from "@crewhaus/memory-service";
27
28
  import { runChatLoop } from "@crewhaus/runtime-core";
29
+ import { createSkillTool } from "@crewhaus/skills-registry";
28
30
  import { currentTenantContext } from "@crewhaus/tenancy";
29
31
  import { aggregate } from "./aggregate";
30
32
  import { RunnerError } from "./errors";
@@ -32,6 +34,9 @@ import { runSample } from "./run-sample";
32
34
  import { Semaphore } from "./semaphore";
33
35
  import { wireRunOnce } from "./wire-once";
34
36
  export { wireRunOnce };
37
+ // v0.3.0 Goal 2 (§3.3, PR 17) — the first-class competency exam: the
38
+ // reference implementation of memory-service's injected `ExamRunner` seam.
39
+ export { EXAM_SESSION_PREAMBLE, createExamRunner, } from "./exam";
35
40
  export { Semaphore };
36
41
  export { aggregate };
37
42
  export { RunnerError };
@@ -63,7 +68,9 @@ export async function runEval(args) {
63
68
  const outDir = resolveEvalOutDir(runId, opts.outDir);
64
69
  mkdirSync(outDir, { recursive: true });
65
70
  // Resolve graders. Replace any `llm_judge` placeholder with a real judge
66
- // grader bound to the runner's judgeModel (or the per-grader override).
71
+ // grader bound to the runner's judgeModel (or the per-grader override),
72
+ // and any `registry` placeholder with the named grader from the caller's
73
+ // grader registry (PR 19 — loud at run start, not per-sample).
67
74
  const graders = compiledGraders.map((g) => {
68
75
  if (g.judgeSpec) {
69
76
  const rubric = loadRubric(g.judgeSpec.rubric);
@@ -71,6 +78,12 @@ export async function runEval(args) {
71
78
  const grader = createJudgeGrader(rubric, model !== undefined ? { model } : {});
72
79
  return { name: g.name, grader };
73
80
  }
81
+ if (g.registrySpec) {
82
+ if (opts.graderRegistry === undefined) {
83
+ throw new RunnerError(`grader "${g.name}" resolves by registry name "${g.registrySpec.grader}" but no graderRegistry was supplied — pass RunEvalOptions.graderRegistry (and register the pack, e.g. registerContinuityGraders(registry))`);
84
+ }
85
+ return { name: g.name, grader: opts.graderRegistry.lookup(g.registrySpec.grader) };
86
+ }
74
87
  return { name: g.name, grader: g.grader };
75
88
  });
76
89
  // The default invoker calls runChatLoop with the per-sample fresh runContext.
@@ -125,6 +138,7 @@ export async function runEval(args) {
125
138
  graders,
126
139
  outDir,
127
140
  model: ir.agent.model,
141
+ specName: ir.name,
128
142
  ...(opts.seed !== undefined ? { seed: opts.seed } : {}),
129
143
  });
130
144
  const first = await runOnce();
@@ -194,17 +208,62 @@ export async function runEval(args) {
194
208
  }
195
209
  async function defaultInvoker(ir, opts) {
196
210
  const wired = await wireRunOnce(ir, opts.cwd !== undefined ? { cwd: opts.cwd } : {});
211
+ // v0.3.0 §7.2 — eval/optimizer state ISOLATION. When the IR carries the
212
+ // memory fabric (an enabled `memory` block and/or `continuity`, which is
213
+ // DEFAULT-ON on the cli shape since 0.3.0), each sample gets its OWN
214
+ // ephemeral stores rooted under its per-sample artifact directory
215
+ // (`.crewhaus/evals/<runId>/<sampleId>/.crewhaus/…`): plan/focus/handoff/
216
+ // facts written by sample N must never leak into sample N+1 — Pillar 2
217
+ // assumes spec patches are the only cross-run channel. The per-sample
218
+ // `sessionRootDir` doubles as the fabric's session-log root so proof
219
+ // evidence resolves against the sample's own transcript, and `homeDir`
220
+ // is pinned inside the sample dir so the operator's `~/.crewhaus` skills
221
+ // can never bleed into a measurement.
222
+ const fabricOn = (ir.memory !== undefined && ir.memory.enabled !== false) || ir.continuity !== undefined;
197
223
  return async (req) => {
224
+ let tools = [...wired.tools];
225
+ let skills = wired.skills;
226
+ let slashCommands = wired.slashCommands;
227
+ let memoryOpt;
228
+ let continuityOpt;
229
+ if (fabricOn) {
230
+ const memWired = await wireMemory(memoryFragmentFromIr(ir), {
231
+ catalog: {
232
+ register: (tool) => {
233
+ tools.push(tool);
234
+ },
235
+ },
236
+ cwd: req.sessionRootDir,
237
+ sessionRootDir: req.sessionRootDir,
238
+ homeDir: req.sessionRootDir,
239
+ });
240
+ memoryOpt = memWired.options.memory;
241
+ continuityOpt = memWired.options.continuity;
242
+ if (memWired.options.skills !== undefined) {
243
+ // The fabric owns the skill surface (builtin `continuity` skill at
244
+ // lowest precedence): replace any Skill tool wireRunOnce registered
245
+ // so the tool list never advertises two.
246
+ skills = memWired.options.skills;
247
+ tools = tools.filter((t) => t.name !== "Skill");
248
+ if (skills.length > 0)
249
+ tools.push(createSkillTool(skills));
250
+ }
251
+ if (memWired.options.slashCommands !== undefined) {
252
+ slashCommands = memWired.options.slashCommands;
253
+ }
254
+ }
198
255
  const agentOutput = await runChatLoop({
199
256
  model: wired.model,
200
257
  instructions: wired.instructions,
201
- tools: [...wired.tools],
258
+ tools,
202
259
  hooks: wired.hooks,
203
- skills: wired.skills,
204
- slashCommands: wired.slashCommands,
260
+ skills,
261
+ slashCommands,
205
262
  ...(wired.subAgents !== undefined && wired.spawnSubAgent !== undefined
206
263
  ? { subAgents: wired.subAgents, spawnSubAgent: wired.spawnSubAgent }
207
264
  : {}),
265
+ ...(memoryOpt !== undefined ? { memory: memoryOpt } : {}),
266
+ ...(continuityOpt !== undefined ? { continuity: continuityOpt } : {}),
208
267
  permissionRules: wired.permissionRules,
209
268
  permissionMode: "auto",
210
269
  sessionName: `${wired.sessionName}_${req.sample.id}`,
@@ -14,6 +14,9 @@ export declare function runSample(args: {
14
14
  outDir: string;
15
15
  model: string;
16
16
  seed?: number;
17
+ /** The spec name (`ir.name`) — threaded into `RunResult.artifacts` so
18
+ * artifact graders can address `.crewhaus/state/<specName>/` directly. */
19
+ specName?: string;
17
20
  }): Promise<SampleResult>;
18
21
  /** Helper: produce one combined grader from an array of CompiledGrader. */
19
22
  export declare function combineGraderEntries(graders: ReadonlyArray<CompiledGrader>): Grader;
@@ -69,7 +69,12 @@ export async function runSample(args) {
69
69
  const turns = transcript.filter((e) => e.kind === "assistant_message").length;
70
70
  const toolCalls = extractToolCalls(finalEvents);
71
71
  const tokens = sumTokens(finalEvents);
72
- // Apply graders.
72
+ // Apply graders. `artifacts` is the PR-19 seam for artifact-reading
73
+ // graders (grader-continuity): the sample's own directory — the primary
74
+ // session's `transcript.jsonl`, any extra session JSONLs a multi-session
75
+ // invoker wrote, and the isolated `.crewhaus/` fabric root (§7.2) — so
76
+ // graders measure the finished sample's files, never the host's live
77
+ // stores.
73
78
  const runResult = {
74
79
  agentOutput,
75
80
  events: finalEvents,
@@ -77,6 +82,13 @@ export async function runSample(args) {
77
82
  toolCalls,
78
83
  turns,
79
84
  latencyMs,
85
+ artifacts: {
86
+ sampleDir,
87
+ sessionId: runContext.sessionId,
88
+ transcriptPath,
89
+ stateRootDir: join(sampleDir, ".crewhaus"),
90
+ ...(args.specName !== undefined ? { specName: args.specName } : {}),
91
+ },
80
92
  };
81
93
  const perGrader = [];
82
94
  // A grader throwing is grader INFRA noise (judge 429/timeout), not a graded
package/dist/types.d.ts CHANGED
@@ -104,6 +104,14 @@ export type EvalRunSummary = {
104
104
  };
105
105
  readonly outDir: string;
106
106
  };
107
+ /**
108
+ * Structural view of `@crewhaus/grader-registry`'s `GraderRegistry` — just
109
+ * the lookup the runner needs to resolve `type: registry` grader entries,
110
+ * kept structural so this package takes no grader-registry dependency.
111
+ */
112
+ export type GraderLookup = {
113
+ lookup(name: string): Grader;
114
+ };
107
115
  export type RunEvalOptions = {
108
116
  readonly runId?: string;
109
117
  readonly concurrency?: number;
@@ -112,6 +120,14 @@ export type RunEvalOptions = {
112
120
  readonly judgeModel?: string;
113
121
  readonly invoker?: AgentInvoker;
114
122
  readonly cwd?: string;
123
+ /**
124
+ * v0.3.0 §7.3 (PR 19) — resolves `type: registry` grader entries by name.
125
+ * An eval config opts into registered grader packs (`continuity.*` after
126
+ * `registerContinuityGraders`, `twelve.*` after `register12MetricRubric`)
127
+ * without this package importing them; omitting the registry while the
128
+ * config carries a registry entry is a loud `RunnerError` at run start.
129
+ */
130
+ readonly graderRegistry?: GraderLookup;
115
131
  /**
116
132
  * Retry a sample ONCE, within the run, when its result is an ERROR
117
133
  * (`SampleResult.error` — the INVOKER failed: provider timeout, 429,
package/dist/wire-once.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { loadHooks } from "@crewhaus/hooks-engine";
4
4
  import { createLogger } from "@crewhaus/logging";
5
- import { McpHost } from "@crewhaus/mcp-host";
5
+ import { McpHost, resolveMcpServerConfig } from "@crewhaus/mcp-host";
6
6
  import { BUILTIN_DEFAULT_RULES, PermissionConfigError, parsePermissionsConfig, tagRules, } from "@crewhaus/permission-engine";
7
7
  import { createSkillTool, discoverSkills } from "@crewhaus/skills-registry";
8
8
  import { loadCommands } from "@crewhaus/slash-commands";
@@ -33,8 +33,11 @@ export async function wireRunOnce(ir, opts = {}) {
33
33
  if (Object.keys(ir.mcp_servers).length > 0) {
34
34
  const host = new McpHost({ logger });
35
35
  mcpHost = host;
36
- for (const [name, cfg] of Object.entries(ir.mcp_servers))
37
- host.addServer(name, cfg);
36
+ // 0.3.0 env/header values are IrSecretRef; resolve from the eval
37
+ // process's environment (fail-fast, names the variable).
38
+ for (const [name, cfg] of Object.entries(ir.mcp_servers)) {
39
+ host.addServer(name, resolveMcpServerConfig(cfg, { name }));
40
+ }
38
41
  const tempCatalog = new ToolCatalog();
39
42
  for (const t of tools)
40
43
  tempCatalog.register(t);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/eval-runner",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Eval runner — per-sample isolation, concurrency, grading, persistence",
6
6
  "main": "dist/index.js",
@@ -15,37 +15,43 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/agent-context-isolation": "0.2.4",
19
- "@crewhaus/compiler": "0.2.4",
20
- "@crewhaus/errors": "0.2.4",
21
- "@crewhaus/eval-dataset": "0.2.4",
22
- "@crewhaus/eval-grader": "0.2.4",
23
- "@crewhaus/eval-judge": "0.2.4",
24
- "@crewhaus/event-log": "0.2.4",
25
- "@crewhaus/hooks-engine": "0.2.4",
26
- "@crewhaus/ir": "0.2.4",
27
- "@crewhaus/logging": "0.2.4",
28
- "@crewhaus/mcp-host": "0.2.4",
29
- "@crewhaus/permission-engine": "0.2.4",
30
- "@crewhaus/run-context": "0.2.4",
31
- "@crewhaus/runtime-core": "0.2.4",
32
- "@crewhaus/skills-registry": "0.2.4",
33
- "@crewhaus/slash-commands": "0.2.4",
34
- "@crewhaus/spec": "0.2.4",
35
- "@crewhaus/sub-agent-spawner": "0.2.4",
36
- "@crewhaus/tenancy": "0.2.4",
37
- "@crewhaus/tool-bash": "0.2.4",
38
- "@crewhaus/tool-catalog": "0.2.4",
39
- "@crewhaus/tool-fetch": "0.2.4",
40
- "@crewhaus/tool-fs": "0.2.4",
41
- "@crewhaus/tool-image": "0.2.4",
42
- "@crewhaus/tool-mcp": "0.2.4",
43
- "@crewhaus/tool-task": "0.2.4",
44
- "@crewhaus/tool-todo": "0.2.4",
45
- "@crewhaus/tool-web": "0.2.4",
46
- "@crewhaus/trace-event-bus": "0.2.4",
18
+ "@crewhaus/agent-context-isolation": "0.3.0",
19
+ "@crewhaus/compiler": "0.3.0",
20
+ "@crewhaus/errors": "0.3.0",
21
+ "@crewhaus/eval-dataset": "0.3.0",
22
+ "@crewhaus/eval-grader": "0.3.0",
23
+ "@crewhaus/eval-judge": "0.3.0",
24
+ "@crewhaus/event-log": "0.3.0",
25
+ "@crewhaus/hooks-engine": "0.3.0",
26
+ "@crewhaus/ir": "0.3.0",
27
+ "@crewhaus/logging": "0.3.0",
28
+ "@crewhaus/mcp-host": "0.3.0",
29
+ "@crewhaus/memory-service": "0.3.0",
30
+ "@crewhaus/permission-engine": "0.3.0",
31
+ "@crewhaus/run-context": "0.3.0",
32
+ "@crewhaus/runtime-core": "0.3.0",
33
+ "@crewhaus/skills-registry": "0.3.0",
34
+ "@crewhaus/slash-commands": "0.3.0",
35
+ "@crewhaus/spec": "0.3.0",
36
+ "@crewhaus/sub-agent-spawner": "0.3.0",
37
+ "@crewhaus/tenancy": "0.3.0",
38
+ "@crewhaus/tool-bash": "0.3.0",
39
+ "@crewhaus/tool-catalog": "0.3.0",
40
+ "@crewhaus/tool-fetch": "0.3.0",
41
+ "@crewhaus/tool-fs": "0.3.0",
42
+ "@crewhaus/tool-image": "0.3.0",
43
+ "@crewhaus/tool-mcp": "0.3.0",
44
+ "@crewhaus/tool-task": "0.3.0",
45
+ "@crewhaus/tool-todo": "0.3.0",
46
+ "@crewhaus/tool-web": "0.3.0",
47
+ "@crewhaus/trace-event-bus": "0.3.0",
48
+ "@crewhaus/wiki-store": "0.3.0",
47
49
  "zod": "^3.23.8"
48
50
  },
51
+ "devDependencies": {
52
+ "@crewhaus/grader-continuity": "0.3.0",
53
+ "@crewhaus/grader-registry": "0.3.0"
54
+ },
49
55
  "license": "Apache-2.0",
50
56
  "author": {
51
57
  "name": "Max Meier",