Learned guidance · {props.environmentLabel}
-
value={project?.id ?? null}
- onValueChange={(value) => {
- const selected = projects.find((item) => item.id === value);
- if (selected) setChosenProjectId(selected.id);
- }}
+ onValueChange={setChosenProjectId}
disabled={projects.length === 0 || (projectScoped && projects.length === 1)}
>
) : (
- Choose a project to inspect its observations. Guidance stays within that project.
+ Choose a project to view its evaluations and guidance.
)}
@@ -391,16 +371,15 @@ function ProjectEvaluationReport(props: {
) : query.data && !query.error ? (
<>
- Observations from up to {query.data.windowLimit} tasks in the past 90 days. Scores range
- from 0 to 4; unknown dimensions stay unscored. Small samples and judge opinions are
- advisory.
+ Up to {query.data.windowLimit} evaluations from the past 90 days. Scores use 0–4;
+ unknown dimensions stay unscored. Small samples and judge opinions are advisory.
{config?.settings.enableModelEvaluations === false
? " Evaluations are off; saved history is still available."
: ""}
{query.data.records.length === 0 ? (
- No evaluations yet. Complete a task with evaluations enabled to collect evidence.
+ No evaluations yet. Complete a task with evaluations enabled to collect scores.
) : null}
{query.data.guidance.map((guidance) => (
diff --git a/evals/model-evaluations/README.md b/evals/model-evaluations/README.md
index a5f9b293ebb9d..c7af808393d87 100644
--- a/evals/model-evaluations/README.md
+++ b/evals/model-evaluations/README.md
@@ -19,8 +19,9 @@ pnpm model-evaluations --model haiku --samples 3 --out runs/judge-baseline
pnpm model-evaluations --model haiku --samples 3 --baseline runs/judge-baseline/results.json --out runs/judge-candidate
```
-`--dry-run` validates every selected packet and writes the real prompts, with **zero model calls**.
-It neither creates a provider client nor reports a quality pass. Real runs require an installed,
+`--dry-run` validates every selected packet and writes only `prompts.json`, with **zero model calls**.
+It neither creates a provider client nor produces scored results. Use a new output directory to keep
+dry-run prompts separate from any earlier results. Real runs require an installed,
authenticated Claude CLI that supports `--safe-mode`. `--model` defaults to the CLI's `haiku` alias;
use `sonnet` or a configured Claude model ID to compare another judge.
@@ -31,7 +32,7 @@ unknown for real runs. A dry run costs $0. CI runs only the deterministic grader
## Results
-The output directory contains `prompts.json`, `results.json`, `summary.json`, and `summary.md`.
+Real runs write `prompts.json`, `results.json`, and `summary.md`.
Raw structured verdicts and failure reasons remain inspectable. Call errors count as failed
samples. A case passes only if more than half its samples pass; a tie fails. A real run exits with
status 1 if any case fails.
diff --git a/evals/model-evaluations/grade.ts b/evals/model-evaluations/grade.ts
index 66d5027cdc24b..d44cee81e2381 100644
--- a/evals/model-evaluations/grade.ts
+++ b/evals/model-evaluations/grade.ts
@@ -1,16 +1,7 @@
-import { ModelEvaluationVerdict } from "@t3tools/contracts";
import { validateModelEvaluationVerdict } from "@t3tools/provider-core/server/modelEvaluation";
-import * as Option from "effect/Option";
-import * as Schema from "effect/Schema";
import type { ModelEvaluationCase } from "./cases.ts";
-const decode = Schema.decodeUnknownOption(ModelEvaluationVerdict);
-
-export function gradeModelEvaluation(fixture: ModelEvaluationCase, output: unknown) {
- const decoded = decode(output);
- if (Option.isNone(decoded))
- return { pass: false, failures: ["Output does not match the verdict schema."] };
- const verdict = decoded.value;
+export function gradeModelEvaluation(fixture: ModelEvaluationCase, verdict: unknown) {
if (!validateModelEvaluationVerdict(verdict, fixture.packet)) {
return {
pass: false,
@@ -59,11 +50,6 @@ export function gradeModelEvaluation(fixture: ModelEvaluationCase, output: unkno
case "incomplete-artifact":
require(verdict.outcome ===
"insufficient_evidence", "Missing specification, implementation, and results require abstention.");
- require(Object.values(verdict.scores).every(
- (score) => score === null,
- ), "All dimensions must remain unknown.");
- require(verdict.confidence ===
- "low", "Incomplete evidence cannot justify confident judgment.");
break;
}
return { pass: failures.length === 0, failures };
diff --git a/evals/model-evaluations/run.ts b/evals/model-evaluations/run.ts
index 93f0fde85db82..00506efd7a95a 100644
--- a/evals/model-evaluations/run.ts
+++ b/evals/model-evaluations/run.ts
@@ -1,23 +1,17 @@
#!/usr/bin/env node
-// @effect-diagnostics nodeBuiltinImport:off - command-line eval arguments and reproducible input digests.
+// @effect-diagnostics nodeBuiltinImport:off - command-line eval arguments, files, and input digests.
import * as NodeCrypto from "node:crypto";
+import * as NodeFS from "node:fs";
+import * as NodePath from "node:path";
import * as NodeUtil from "node:util";
import * as NodeServices from "@effect/platform-node/NodeServices";
-import {
- BlindModelEvaluationPacket,
- ClaudeSettings,
- defaultInstanceIdForDriver,
- ProviderDriverKind,
- type ModelEvaluationVerdict,
-} from "@t3tools/contracts";
+import { BlindModelEvaluationPacket, ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";
import {
buildModelEvaluationPrompt,
MODEL_EVALUATION_RUBRIC_VERSION,
} from "@t3tools/provider-core/server/modelEvaluation";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
-import * as FileSystem from "effect/FileSystem";
-import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { modelEvaluationCases } from "./cases.ts";
import { gradeModelEvaluation } from "./grade.ts";
@@ -46,191 +40,135 @@ const unknown = [...requested].filter((id) => !selected.some((entry) => entry.id
if (unknown.length || selected.length === 0)
throw new Error(`Unknown or empty --case selection: ${unknown.join(", ")}`);
-const Baseline = Schema.fromJsonString(
- Schema.Array(
- Schema.Struct({
- case: Schema.String,
- model: Schema.String,
- fixtureHash: Schema.String,
- graderHash: Schema.String,
- pass: Schema.NullOr(Schema.Boolean),
- }),
+const decodeBaseline = Schema.decodeUnknownSync(
+ Schema.fromJsonString(
+ Schema.Array(
+ Schema.Struct({
+ case: Schema.String,
+ model: Schema.String,
+ fixtureHash: Schema.String,
+ graderHash: Schema.String,
+ pass: Schema.Boolean,
+ }),
+ ),
),
);
const hash = (value: string) => NodeCrypto.createHash("sha256").update(value).digest("hex");
-const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
+const writeJson = (name: string, value: unknown) =>
+ NodeFS.writeFileSync(NodePath.join(outDirectory, name), JSON.stringify(value, null, 2) + "\n");
const decodePacket = Schema.decodeUnknownSync(BlindModelEvaluationPacket);
-const decodeClaudeSettings = Schema.decodeUnknownEffect(ClaudeSettings);
-const decodeBaseline = Schema.decodeUnknownEffect(Baseline);
-
-interface Row {
- readonly case: string;
- readonly source: string;
- readonly model: string;
- readonly sample: number;
- readonly rubricVersion: string;
- readonly fixtureHash: string;
- readonly graderHash: string;
- readonly promptHash: string;
- readonly pass: boolean | null;
- readonly failures: readonly string[];
- readonly verdict: ModelEvaluationVerdict | null;
- readonly error: string | null;
- readonly latencyMs: number | null;
-}
+const decodeClaudeSettings = Schema.decodeUnknownSync(ClaudeSettings);
+const graderHash = hash(NodeFS.readFileSync(new URL("./grade.ts", import.meta.url), "utf8"));
+const baseline = values.baseline
+ ? decodeBaseline(NodeFS.readFileSync(values.baseline, "utf8"))
+ : [];
+const prepared = selected.map((fixture) => {
+ const packet = decodePacket(fixture.packet);
+ const { prompt } = buildModelEvaluationPrompt(packet);
+ const fixtureHash = hash(JSON.stringify({ kind: fixture.kind, packet }));
+ const previous = baseline.filter((row) => row.case === fixture.id);
+ if (
+ values.baseline &&
+ (previous.length === 0 ||
+ previous.some(
+ (row) =>
+ row.model !== values.model ||
+ row.fixtureHash !== fixtureHash ||
+ row.graderHash !== graderHash,
+ ))
+ )
+ throw new Error(`Baseline for ${fixture.id} must use the same model, fixture, and grader.`);
+ return { fixture, packet, prompt, promptHash: hash(prompt), fixtureHash, previous };
+});
-await Effect.runPromise(
- Effect.gen(function* () {
- const fs = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
- const graderHash = hash(yield* fs.readFileString(path.join(import.meta.dirname, "grade.ts")));
- const baseline = values.baseline
- ? yield* fs.readFileString(values.baseline).pipe(Effect.flatMap(decodeBaseline))
- : [];
- const prepared = selected.map((fixture) => {
- const packet = decodePacket(fixture.packet);
- const { prompt } = buildModelEvaluationPrompt(packet);
- const fixtureHash = hash(encodeJson({ kind: fixture.kind, packet }));
- const previous = baseline.filter((row) => row.case === fixture.id);
- if (
- values.baseline &&
- (previous.length === 0 ||
- previous.some(
- (row) =>
- row.model !== values.model ||
- row.fixtureHash !== fixtureHash ||
- row.graderHash !== graderHash ||
- row.pass === null,
- ))
- ) {
- throw new Error(
- `Baseline for ${fixture.id} must use the same model, fixture, and grader, with real graded samples.`,
- );
- }
- return { fixture, packet, prompt, promptHash: hash(prompt), fixtureHash, previous };
- });
- yield* fs.makeDirectory(outDirectory, { recursive: true });
- yield* fs.writeFileString(
- path.join(outDirectory, "prompts.json"),
- encodeJson(
- prepared.map(({ fixture, packet, prompt, promptHash }) => ({
- id: fixture.id,
- packet,
- prompt,
- promptHash,
- })),
- ),
- );
- const generation = values["dry-run"]
- ? null
- : yield* Effect.gen(function* () {
- const { makeClaudeTextGeneration } = yield* Effect.promise(
- () => import("../../apps/server/src/textGeneration/ClaudeTextGeneration.ts"),
- );
- return yield* makeClaudeTextGeneration(yield* decodeClaudeSettings({}));
- });
- const cwd =
- generation === null
- ? ""
- : yield* fs.makeTempDirectoryScoped({ prefix: "t3-model-evaluation-eval-" });
- const rows: Row[] = [];
- for (const item of prepared) {
- for (let sample = 0; sample < samples; sample++) {
- const base = {
- case: item.fixture.id,
- source: item.fixture.source,
- model: values.model,
- sample,
- rubricVersion: MODEL_EVALUATION_RUBRIC_VERSION,
- fixtureHash: item.fixtureHash,
- graderHash,
- promptHash: item.promptHash,
- };
- if (generation === null) {
- rows.push({
- ...base,
- pass: null,
- failures: [],
- verdict: null,
- error: null,
- latencyMs: null,
- });
- } else {
+NodeFS.mkdirSync(outDirectory, { recursive: true });
+writeJson(
+ "prompts.json",
+ prepared.map(({ fixture, prompt, promptHash }) => ({ id: fixture.id, prompt, promptHash })),
+);
+if (values["dry-run"]) {
+ process.stdout.write(
+ `Validated ${prepared.length} cases and wrote prompts.json only. No scored results or model calls. Cost: $0.\n`,
+ );
+} else {
+ const { makeClaudeTextGeneration } =
+ await import("../../apps/server/src/textGeneration/ClaudeTextGeneration.ts");
+ await Effect.runPromise(
+ Effect.gen(function* () {
+ const generation = yield* makeClaudeTextGeneration(decodeClaudeSettings({}));
+ const modelSelection = {
+ instanceId: ProviderInstanceId.make("claudeAgent"),
+ model: values.model,
+ };
+ const rows = [];
+ for (const item of prepared) {
+ for (let sample = 0; sample < samples; sample++) {
const result = yield* generation
- .generateModelEvaluation({
- cwd,
- packet: item.packet,
- modelSelection: {
- instanceId: defaultInstanceIdForDriver(ProviderDriverKind.make("claudeAgent")),
- model: values.model,
- },
- })
+ .generateModelEvaluation({ cwd: process.cwd(), packet: item.packet, modelSelection })
.pipe(
Effect.timed,
- Effect.map(([duration, verdict]) => ({
- verdict,
- error: null,
- latencyMs: Duration.toMillis(duration),
- })),
- Effect.orElseSucceed((error) => ({
- verdict: null,
- error: String(error).slice(0, 2_000),
- latencyMs: null,
- })),
+ Effect.match({
+ onSuccess: ([duration, verdict]) => ({
+ ...gradeModelEvaluation(item.fixture, verdict),
+ verdict,
+ error: null,
+ latencyMs: Duration.toMillis(duration),
+ }),
+ onFailure: (error) => ({
+ pass: false,
+ failures: ["Evaluation call failed."],
+ verdict: null,
+ error: String(error).slice(0, 2_000),
+ latencyMs: null,
+ }),
+ }),
);
- const grading =
- result.verdict === null
- ? { pass: false, failures: ["Evaluation call failed."] }
- : gradeModelEvaluation(item.fixture, result.verdict);
- rows.push({ ...base, ...result, ...grading });
+ rows.push({
+ case: item.fixture.id,
+ source: item.fixture.source,
+ model: values.model,
+ sample,
+ rubricVersion: MODEL_EVALUATION_RUBRIC_VERSION,
+ fixtureHash: item.fixtureHash,
+ graderHash,
+ promptHash: item.promptHash,
+ ...result,
+ });
+ writeJson("results.json", rows);
}
- yield* fs.writeFileString(path.join(outDirectory, "results.json"), encodeJson(rows));
}
- }
- const summaries = prepared.map((item) => {
- const own = rows.filter((row) => row.case === item.fixture.id);
- const passed = own.filter((row) => row.pass === true).length;
- const baselinePassed = item.previous.filter((row) => row.pass === true).length;
- return {
- id: item.fixture.id,
- passed,
- samples: own.length,
- pass: generation === null ? null : passed > own.length / 2,
- baseline: item.previous.length ? `${baselinePassed}/${item.previous.length}` : "—",
- };
- });
- const calls = generation === null ? 0 : rows.length;
- const report = [
- "# Blind model evaluation",
- "",
- `Model: ${values.model}. Rubric: ${MODEL_EVALUATION_RUBRIC_VERSION}. ${generation === null ? "Dry run; no model calls or quality scores." : "Each case passes only when a strict majority of its samples pass."}`,
- "",
- "| Case | Passing samples | Result | Baseline samples |",
- "| --- | --- | --- | --- |",
- ...summaries.map(
- (item) =>
- `| ${item.id} | ${generation === null ? "—" : `${item.passed}/${item.samples}`} | ${item.pass === null ? "Dry run" : item.pass ? "Pass" : "Fail"} | ${item.baseline} |`,
- ),
- "",
- `Provider calls: ${calls}. ${calls === 0 ? "Cost: $0." : "Token usage and dollar cost are not exposed by this CLI operation; consult the configured provider's usage records."}`,
- "",
- "The fixture, grader, and prompt hashes are saved with every sample. Baseline comparison requires identical fixtures, grader, and model. Review the raw verdicts before interpreting these guards as general judge quality.",
- "",
- ].join("\n");
- yield* fs.writeFileString(path.join(outDirectory, "summary.md"), report);
- yield* fs.writeFileString(
- path.join(outDirectory, "summary.json"),
- encodeJson({
- model: values.model,
- rubricVersion: MODEL_EVALUATION_RUBRIC_VERSION,
- dryRun: generation === null,
- providerCalls: calls,
- tokenUsage: null,
- costUSD: calls === 0 ? 0 : null,
- cases: summaries,
- }),
- );
- process.stdout.write(report);
- if (summaries.some((item) => item.pass === false)) process.exitCode = 1;
- }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
-);
+ const report = [
+ "# Blind model evaluation",
+ "",
+ `Model: ${values.model}. Rubric: ${MODEL_EVALUATION_RUBRIC_VERSION}. Each case passes only when a strict majority of its samples pass.`,
+ "",
+ "| Case | Passing samples | Result | Baseline samples |",
+ "| --- | --- | --- | --- |",
+ ];
+ let failed = false;
+ for (const item of prepared) {
+ const own = rows.filter((row) => row.case === item.fixture.id);
+ const passed = own.filter((row) => row.pass).length;
+ const pass = passed > own.length / 2;
+ failed ||= !pass;
+ const previous = item.previous.length
+ ? `${item.previous.filter((row) => row.pass).length}/${item.previous.length}`
+ : "—";
+ report.push(
+ `| ${item.fixture.id} | ${passed}/${own.length} | ${pass ? "Pass" : "Fail"} | ${previous} |`,
+ );
+ }
+ report.push(
+ "",
+ `Provider calls: ${rows.length}. Token usage and dollar cost are not exposed by this CLI operation; consult the configured provider's usage records.`,
+ "",
+ "Review the raw verdicts before interpreting these guards as general judge quality.",
+ "",
+ );
+ const summary = report.join("\n");
+ NodeFS.writeFileSync(NodePath.join(outDirectory, "summary.md"), summary);
+ process.stdout.write(summary);
+ if (failed) process.exitCode = 1;
+ }).pipe(Effect.provide(NodeServices.layer)),
+ );
+}
diff --git a/packages/contracts/src/modelEvaluation.ts b/packages/contracts/src/modelEvaluation.ts
index f0307afc5da4e..5c73f1ac3ac6e 100644
--- a/packages/contracts/src/modelEvaluation.ts
+++ b/packages/contracts/src/modelEvaluation.ts
@@ -3,7 +3,7 @@ import * as Schema from "effect/Schema";
import { IsoDateTime, NonNegativeInt, ProjectId, RunId, ThreadId } from "./baseSchemas.ts";
import { ModelSelection } from "./modelSelection.ts";
-export const ModelEvaluationTaskCategory = Schema.Literals([
+const ModelEvaluationTaskCategory = Schema.Literals([
"coding",
"debugging",
"review",
@@ -13,9 +13,7 @@ export const ModelEvaluationTaskCategory = Schema.Literals([
"planning",
"other",
]);
-export type ModelEvaluationTaskCategory = typeof ModelEvaluationTaskCategory.Type;
-
-export const ModelEvaluationConfidence = Schema.Literals(["low", "medium", "high"]);
+const ModelEvaluationConfidence = Schema.Literals(["low", "medium", "high"]);
export const ModelEvaluationRunOutcome = Schema.Literals([
"completed",
"failed",
@@ -27,15 +25,13 @@ export type ModelEvaluationRunOutcome = typeof ModelEvaluationRunOutcome.Type;
const Score = Schema.NullOr(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 4 })));
const MeanScore = Schema.NullOr(Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 4 })));
-export const ModelEvaluationScores = Schema.Struct({
+const ModelEvaluationScores = Schema.Struct({
taskCompletion: Score,
correctness: Score,
instructionFollowing: Score,
verification: Score,
});
-export type ModelEvaluationScores = typeof ModelEvaluationScores.Type;
-
-export const ModelEvaluationObservation = Schema.Struct({
+const ModelEvaluationObservation = Schema.Struct({
text: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
citations: Schema.Array(Schema.String.check(Schema.isPattern(/^[UEC][1-9]\d{0,2}$/))).check(
Schema.isMaxLength(5),
diff --git a/packages/provider-core/src/server/modelEvaluation.test.ts b/packages/provider-core/src/server/modelEvaluation.test.ts
index 54ea4df62ff5d..77bd8f588bb6e 100644
--- a/packages/provider-core/src/server/modelEvaluation.test.ts
+++ b/packages/provider-core/src/server/modelEvaluation.test.ts
@@ -1,7 +1,7 @@
import { expect, it } from "vite-plus/test";
import {
MessageId,
- ModelEvaluationVerdict,
+ type ModelEvaluationVerdict,
NodeId,
ProviderInstanceId,
ProviderThreadId,
@@ -13,7 +13,6 @@ import {
type BlindModelEvaluationPacket,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
-import * as Schema from "effect/Schema";
import {
buildModelEvaluationEvidence,
@@ -35,7 +34,6 @@ const run = {
status: "completed" as const,
};
const now = DateTime.makeUnsafe("2026-01-02T00:00:00Z");
-const isModelEvaluationVerdict = Schema.is(ModelEvaluationVerdict);
function message(
role: "user" | "assistant",
@@ -285,12 +283,16 @@ it("abstains when there is no finalized outcome and rejects fabricated citations
expect(
validateModelEvaluationVerdict({ ...verdict(), outcome: "insufficient_evidence" }, packet),
).toBe(false);
- expect(
- isModelEvaluationVerdict({
+ for (const invalid of [
+ null,
+ {},
+ {
...verdict(),
scores: { ...verdict().scores, correctness: 4.5 },
- }),
- ).toBe(false);
+ },
+ ]) {
+ expect(validateModelEvaluationVerdict(invalid, packet)).toBe(false);
+ }
const extraTopLevelField = { ...verdict(), model: "Invented attribution" };
expect(validateModelEvaluationVerdict(extraTopLevelField, packet)).toBe(false);
const extraNestedField = {
diff --git a/packages/provider-core/src/server/modelEvaluation.ts b/packages/provider-core/src/server/modelEvaluation.ts
index 8c0105934f2b6..fc9dcaa1cb487 100644
--- a/packages/provider-core/src/server/modelEvaluation.ts
+++ b/packages/provider-core/src/server/modelEvaluation.ts
@@ -321,10 +321,12 @@ export function buildModelEvaluationEvidence(input: {
}
export function validateModelEvaluationVerdict(
- verdict: ModelEvaluationVerdict,
+ input: unknown,
packet: BlindModelEvaluationPacket,
-): boolean {
- if (Option.isNone(decodeModelEvaluationVerdict(verdict))) return false;
+): input is ModelEvaluationVerdict {
+ const decoded = decodeModelEvaluationVerdict(input);
+ if (Option.isNone(decoded)) return false;
+ const verdict = decoded.value;
const refs = new Set([
packet.request.ref,
...packet.context.map((entry) => entry.ref),
From 06550eeedfc4092aaaffdcdb30c1e768be4dc111 Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy
Date: Tue, 3 Nov 2026 13:57:48 -0800
Subject: [PATCH 04/10] test(mobile): isolate labs imports in settings controls
tests
Co-Authored-By: GPT-6.1 Sol
---
.../SettingsServerControlsRouteScreen.test.tsx | 1 +
.../src/modelEvaluations/ModelEvaluations.test.ts | 15 +++++++++++++++
2 files changed, 16 insertions(+)
diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx
index 1f91c761c432a..9dd15827e5d53 100644
--- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx
+++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx
@@ -45,6 +45,7 @@ vi.mock("../../state/preferences", () => ({
mobilePreferencesAtom: {},
updateMobilePreferencesAtom: {},
}));
+vi.mock("./ModelEvaluationSettings", () => ({ ModelEvaluationSettings: () => null }));
vi.mock("./components/AutoSettleDaysField", () => ({ AutoSettleDaysField: () => null }));
vi.mock("./components/SourceControlSignInSection", () => ({
SourceControlSignInSection: () => null,
diff --git a/apps/server/src/modelEvaluations/ModelEvaluations.test.ts b/apps/server/src/modelEvaluations/ModelEvaluations.test.ts
index 049c43205a54a..3bbec319a4bac 100644
--- a/apps/server/src/modelEvaluations/ModelEvaluations.test.ts
+++ b/apps/server/src/modelEvaluations/ModelEvaluations.test.ts
@@ -291,9 +291,24 @@ it.effect(
});
yield* service.enqueueRun(input);
assert.equal(Option.getOrThrow(yield* outbox.get(effectId)).status, "pending");
+ const projections = yield* ProjectionStore.ProjectionStoreV2;
+ const sql = yield* SqlClient.SqlClient;
+ const before = yield* projections.getThreadRecords(threadId, ["runs", "messages", "turnItems"]);
+ const counts = yield* sql`SELECT
+ (SELECT COUNT(*) FROM orchestration_events) AS events,
+ (SELECT COUNT(*) FROM orchestration_v2_projection_threads) AS threads,
+ (SELECT COUNT(*) FROM scheduled_tasks) AS scheduled`;
yield* Effect.all([service.evaluateRun(input), service.evaluateRun(input)], {
concurrency: "unbounded",
});
+ assert.deepEqual(
+ yield* projections.getThreadRecords(threadId, ["runs", "messages", "turnItems"]),
+ before,
+ );
+ assert.deepEqual(yield* sql`SELECT
+ (SELECT COUNT(*) FROM orchestration_events) AS events,
+ (SELECT COUNT(*) FROM orchestration_v2_projection_threads) AS threads,
+ (SELECT COUNT(*) FROM scheduled_tasks) AS scheduled`, counts);
assert.equal(calls.length, 1);
const sent = calls[0]!;
assert.notInclude(sent.cwd, "private/project-with-model-config");
From ebef16773c7619c01f5afc6beb646239b19ed8b7 Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy
Date: Tue, 3 Nov 2026 14:01:07 -0800
Subject: [PATCH 05/10] fix(labs): redact bare Claude model family names from
judge evidence
Extend the existing identity regression and retain evaluation isolation assertions.
Co-Authored-By: GPT-6.1 Sol
---
.../src/modelEvaluations/ModelEvaluations.test.ts | 13 ++++++++++---
.../src/server/modelEvaluation.test.ts | 5 ++++-
.../provider-core/src/server/modelEvaluation.ts | 3 +++
3 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/apps/server/src/modelEvaluations/ModelEvaluations.test.ts b/apps/server/src/modelEvaluations/ModelEvaluations.test.ts
index 3bbec319a4bac..f980df8bd6654 100644
--- a/apps/server/src/modelEvaluations/ModelEvaluations.test.ts
+++ b/apps/server/src/modelEvaluations/ModelEvaluations.test.ts
@@ -293,7 +293,11 @@ it.effect(
assert.equal(Option.getOrThrow(yield* outbox.get(effectId)).status, "pending");
const projections = yield* ProjectionStore.ProjectionStoreV2;
const sql = yield* SqlClient.SqlClient;
- const before = yield* projections.getThreadRecords(threadId, ["runs", "messages", "turnItems"]);
+ const before = yield* projections.getThreadRecords(threadId, [
+ "runs",
+ "messages",
+ "turnItems",
+ ]);
const counts = yield* sql`SELECT
(SELECT COUNT(*) FROM orchestration_events) AS events,
(SELECT COUNT(*) FROM orchestration_v2_projection_threads) AS threads,
@@ -305,10 +309,13 @@ it.effect(
yield* projections.getThreadRecords(threadId, ["runs", "messages", "turnItems"]),
before,
);
- assert.deepEqual(yield* sql`SELECT
+ assert.deepEqual(
+ yield* sql`SELECT
(SELECT COUNT(*) FROM orchestration_events) AS events,
(SELECT COUNT(*) FROM orchestration_v2_projection_threads) AS threads,
- (SELECT COUNT(*) FROM scheduled_tasks) AS scheduled`, counts);
+ (SELECT COUNT(*) FROM scheduled_tasks) AS scheduled`,
+ counts,
+ );
assert.equal(calls.length, 1);
const sent = calls[0]!;
assert.notInclude(sent.cwd, "private/project-with-model-config");
diff --git a/packages/provider-core/src/server/modelEvaluation.test.ts b/packages/provider-core/src/server/modelEvaluation.test.ts
index 77bd8f588bb6e..e58b90baab434 100644
--- a/packages/provider-core/src/server/modelEvaluation.test.ts
+++ b/packages/provider-core/src/server/modelEvaluation.test.ts
@@ -81,7 +81,7 @@ function fixture() {
message(
"assistant",
"result",
- "I am DeepSeek/solver.v2+local in private-thread-id/private-run-id, session private-provider-thread-id. The parser is repaired. All tests passed.",
+ "I am DeepSeek/solver.v2+local in private-thread-id/private-run-id, session private-provider-thread-id. The parser is repaired. All tests passed. Previously I used Claude Sonnet, Haiku, and Opus.",
),
];
const turnItems: OrchestrationV2TurnItem[] = [
@@ -166,6 +166,9 @@ it("separates identities from evidence and preserves tool outcomes and task data
"private-node",
"hidden-provider",
"hidden chain of thought",
+ "Sonnet",
+ "Haiku",
+ "Opus",
]) {
expect(prompt.toLowerCase()).not.toContain(identity.toLowerCase());
}
diff --git a/packages/provider-core/src/server/modelEvaluation.ts b/packages/provider-core/src/server/modelEvaluation.ts
index fc9dcaa1cb487..f3f6f5b762c02 100644
--- a/packages/provider-core/src/server/modelEvaluation.ts
+++ b/packages/provider-core/src/server/modelEvaluation.ts
@@ -22,6 +22,9 @@ const IDENTITY_NAMES = [
"Codex",
"Anthropic",
"Claude",
+ "Sonnet",
+ "Haiku",
+ "Opus",
"DeepSeek",
"OpenCode",
"Antigravity",
From af37b9c5ff8554f6ffdd39d76ddda78a59bec38a Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy
Date: Tue, 3 Nov 2026 14:11:55 -0800
Subject: [PATCH 06/10] test: isolate inherited T3 shim settings from provider
replays
The ambient GitHub shim enabled extra config reads absent from recorded provider frames.
Co-Authored-By: GPT-6.1 Sol
---
packages/shared/src/testing/providerEnv.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/shared/src/testing/providerEnv.ts b/packages/shared/src/testing/providerEnv.ts
index 21bf59e52b500..ac07867c20473 100644
--- a/packages/shared/src/testing/providerEnv.ts
+++ b/packages/shared/src/testing/providerEnv.ts
@@ -2,7 +2,7 @@
// ANTHROPIC_BASE_URL) and the agent's own T3 state. Tests that read the ambient environment
// then pass or fail by who runs them. Tests that need one of these variables set it
// themselves. Set T3CODE_TEST_KEEP_PROVIDER_ENV=1 to keep them, for example for a live run.
-const PROVIDER_ENV_PREFIXES = ["ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "T3CODE_"];
+const PROVIDER_ENV_PREFIXES = ["ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "T3CODE_", "T3_"];
const KEPT_ENV = new Set([
// CI sets these so the transfer budget integration test writes its report for the job summary.
From f9a29edd74ef653fa8461745fa353c1df390c7a2 Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy
Date: Tue, 3 Nov 2026 14:42:18 -0800
Subject: [PATCH 07/10] fix(labs): keep identity names out of judge evidence
after escaped newlines
Names that followed a newline in command output, diffs, or subagent reports kept
their escaped line break as a word character and slipped past the redactor. Names
followed by version digits (Qwen3), bare GPT, and several newer model families also
survived. Redaction now runs on each string before serialization, derives family
tokens from the evaluated model id, and drops Claude's subagent usage trailer.
Co-Authored-By: Claude Sonnet 5.5
---
docs/user/model-evaluations.md | 4 +-
.../src/server/modelEvaluation.test.ts | 39 +++++++++++++++++
.../src/server/modelEvaluation.ts | 42 +++++++++++++++----
3 files changed, 75 insertions(+), 10 deletions(-)
diff --git a/docs/user/model-evaluations.md b/docs/user/model-evaluations.md
index 6a5433b25f945..346047d706e7e 100644
--- a/docs/user/model-evaluations.md
+++ b/docs/user/model-evaluations.md
@@ -16,7 +16,9 @@ which keeps account authentication while disabling custom instructions and exten
After a task ends, T3 Code gives the judge the request, relevant conversation context, and available
outcome evidence with model and provider labels removed. The same process covers subagent threads
run through T3's orchestration. Model names mentioned inside free-form text may be harder to remove
-completely, so blind evaluation reduces identity cues without guaranteeing anonymity.
+completely, and tool output keeps its structure and any token, cost, or timing figures a tool
+reports. Those can hint at which harness ran the task, so blind evaluation reduces identity cues
+without guaranteeing anonymity.
The judge scores task completion, correctness, instruction following, and verification from 0 to 4.
When the evidence cannot establish a score, that dimension stays unscored. A claim that tests passed
diff --git a/packages/provider-core/src/server/modelEvaluation.test.ts b/packages/provider-core/src/server/modelEvaluation.test.ts
index e58b90baab434..cb440d346eb2d 100644
--- a/packages/provider-core/src/server/modelEvaluation.test.ts
+++ b/packages/provider-core/src/server/modelEvaluation.test.ts
@@ -187,6 +187,45 @@ it("separates identities from evidence and preserves tool outcomes and task data
durationMs: 40,
},
});
+ const glued = fixture();
+ const gluedEvidence = buildModelEvaluationEvidence({
+ ...glued,
+ turnItems: [
+ ...glued.turnItems,
+ {
+ ...itemBase("glued", 4),
+ type: "command_execution",
+ input: "cat notes.txt",
+ output: "done\nClaude wrote this\r\n\tOpus too\nQwen3, Llama3.1, gpt-oss and GPT answered",
+ exitCode: 0,
+ },
+ {
+ ...itemBase("patch", 4),
+ type: "file_change",
+ fileName: "notes.txt",
+ diffStr: "+first\n+Gemini and Grok4 wrote this\n+Google and Kimi built me",
+ },
+ ],
+ })!;
+ const family = fixture();
+ const familyEvidence = buildModelEvaluationEvidence({
+ ...family,
+ run: { ...run, modelSelection: { ...run.modelSelection, model: "vexel-9/zorn-pro" } },
+ messages: [
+ family.messages[0]!,
+ {
+ ...family.messages[1]!,
+ text: "Zorn from Vexel Labs fixed it.\nagentId: a1b2 (use SendMessage with to: 'a1b2' to continue this agent)\nsubagent_tokens: 28485\nduration_ms: 1824 ",
+ },
+ ],
+ })!;
+ expect(JSON.stringify(familyEvidence.packet).toLowerCase()).not.toMatch(
+ /zorn|vexel|sendmessage|28485|subagent_tokens/,
+ );
+ const gluedPrompt = buildModelEvaluationPrompt(gluedEvidence.packet).prompt.toLowerCase();
+ for (const name of ["claude", "opus", "qwen", "llama", "gpt", "gemini", "grok", "google", "kimi"])
+ expect(gluedPrompt).not.toContain(name);
+ expect(gluedPrompt).toContain("wrote this");
const input = fixture();
const failedEdit = buildModelEvaluationEvidence({
...input,
diff --git a/packages/provider-core/src/server/modelEvaluation.ts b/packages/provider-core/src/server/modelEvaluation.ts
index f3f6f5b762c02..692b026435419 100644
--- a/packages/provider-core/src/server/modelEvaluation.ts
+++ b/packages/provider-core/src/server/modelEvaluation.ts
@@ -36,22 +36,43 @@ const IDENTITY_NAMES = [
"Cursor",
"Muse",
"Pi",
+ "GPT",
+ "Google",
+ "xAI",
+ "Kimi",
+ "Moonshot",
+ "GLM",
+ "Zhipu",
+ "MiniMax",
+ "Gemma",
+ "Composer",
+ "Fable",
];
+const escapePattern = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+
+function familyTokens(model: string) {
+ return model.split("/").flatMap((segment) => segment.match(/^\p{L}{3,}/u) ?? []);
+}
function identityRedactor(identities: ReadonlyArray) {
- const aliases = [
- ...new Set([...IDENTITY_NAMES, ...identities].filter((value) => value.trim().length > 1)),
- ]
+ const aliases = [...new Set(identities.filter((value) => value.trim().length > 1))]
.toSorted((a, b) => b.length - a.length)
- .map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
+ .map(escapePattern);
const pattern = new RegExp(
`(?
text
+ .replace(/\n?agentId: .*? to continue this agent\)/g, "")
+ .replace(/\n?[\s\S]*?<\/usage>/g, "")
+ .replace(/(? string, depth = 0):
}
function evidenceText(item: OrchestrationV2TurnItem, redact: (text: string) => string) {
+ const json = (value: unknown) =>
+ JSON.stringify(value, (_key, child) => (typeof child === "string" ? redact(child) : child));
switch (item.type) {
case "command_execution":
return {
kind: "command" as const,
- text: JSON.stringify({
+ text: json({
status: item.status,
input: item.input,
output: item.output ?? "[output unavailable]",
@@ -101,7 +124,7 @@ function evidenceText(item: OrchestrationV2TurnItem, redact: (text: string) => s
case "file_change":
return {
kind: "file_change" as const,
- text: JSON.stringify({
+ text: json({
status: item.status,
path: item.fileName,
patch: item.diffStr ?? null,
@@ -113,7 +136,7 @@ function evidenceText(item: OrchestrationV2TurnItem, redact: (text: string) => s
case "dynamic_tool":
return {
kind: "tool" as const,
- text: JSON.stringify({
+ text: json({
status: item.status,
input: toolValue(item.input, redact),
output: toolValue(item.output, redact),
@@ -123,7 +146,7 @@ function evidenceText(item: OrchestrationV2TurnItem, redact: (text: string) => s
case "subagent":
return {
kind: "subagent" as const,
- text: JSON.stringify({
+ text: json({
status: item.status,
request: item.prompt,
reportedResult: item.result,
@@ -193,6 +216,7 @@ export function buildModelEvaluationEvidence(input: {
input.run.modelSelection.model,
input.run.providerInstanceId,
...input.run.modelSelection.model.split("/"),
+ ...familyTokens(input.run.modelSelection.model),
...items
.flatMap((item) => [
item.id,
From 000e6268eceae4a91fc838755ab726a5108aca7c Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy
Date: Tue, 3 Nov 2026 15:22:41 -0800
Subject: [PATCH 08/10] fix(web): unavailable-model warning no longer points at
another environment
With one environment in scope, "Select that environment" had nowhere to go.
Co-Authored-By: Claude Opus 5.5
---
.../src/components/settings/useScopedModelAvailability.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/components/settings/useScopedModelAvailability.ts b/apps/web/src/components/settings/useScopedModelAvailability.ts
index 5a42661f74f57..078c223dd604a 100644
--- a/apps/web/src/components/settings/useScopedModelAvailability.ts
+++ b/apps/web/src/components/settings/useScopedModelAvailability.ts
@@ -58,7 +58,11 @@ export function useScopedModelDisabledReason(
entry.driverKind !== sourceEntry?.driverKind ||
!options?.some((option) => option.slug === model && !option.isUnavailable)
) {
- return `This model is unavailable on ${environment?.label ?? "a selected environment"}. Select that environment to choose its model separately.`;
+ const next =
+ targets.length > 1
+ ? "Select that environment to choose its model separately."
+ : "Choose another model.";
+ return `This model is unavailable on ${environment?.label ?? "a selected environment"}. ${next}`;
}
const provider = providers.find((provider) => provider.instanceId === instanceId);
const unsupported = requiredOperations.find((operation) =>
From 5962abc726db60cb2cebf1b8ed7e658a3f347600 Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy
Date: Tue, 3 Nov 2026 17:05:02 -0800
Subject: [PATCH 09/10] fix(labs): build model evaluations against the merged
main
Kiro arrived from upstream without a judge entry and reports it as unsupported, like other drivers without text generation. Two tests adapt to effect 4.1 command args and crypto layers.
Co-Authored-By: Claude Opus 5.5
---
apps/server/src/mcp/OrchestratorMcpService.test.ts | 1 +
packages/provider-kiro/src/server/driver.ts | 1 +
packages/provider-pi/src/server/textGeneration.test.ts | 2 +-
3 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts
index 1f3e334a32dbb..a8fc3c9a179b5 100644
--- a/apps/server/src/mcp/OrchestratorMcpService.test.ts
+++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts
@@ -1890,6 +1890,7 @@ describe("OrchestratorMcpService provider resolution", () => {
];
const dependencies = Layer.mergeAll(
NodeServices.layer,
+ NodeCrypto.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadRecords: () => Effect.succeed(parentProjection([])),
}),
diff --git a/packages/provider-kiro/src/server/driver.ts b/packages/provider-kiro/src/server/driver.ts
index 2603438233824..bc0dfb7dc4fe8 100644
--- a/packages/provider-kiro/src/server/driver.ts
+++ b/packages/provider-kiro/src/server/driver.ts
@@ -56,6 +56,7 @@ const unsupportedTextGeneration: ProviderTextGenerationRuntime = (() => {
generateReviewGuide: () => unsupported("generateReviewGuide"),
generateKnowledgeCandidates: () => unsupported("generateKnowledgeCandidates"),
generateConversationSummary: () => unsupported("generateConversationSummary"),
+ generateModelEvaluation: () => unsupported("generateModelEvaluation"),
};
})();
diff --git a/packages/provider-pi/src/server/textGeneration.test.ts b/packages/provider-pi/src/server/textGeneration.test.ts
index ffb39796c2a96..5121193027c14 100644
--- a/packages/provider-pi/src/server/textGeneration.test.ts
+++ b/packages/provider-pi/src/server/textGeneration.test.ts
@@ -55,7 +55,7 @@ const makeFakePi = (
}
});
const spawner = ChildProcessSpawner.make((command) => {
- if (ChildProcess.isStandardCommand(command)) launchArguments.push(command.args);
+ if (ChildProcess.isStandardCommand(command)) launchArguments.push(command.args.map(String));
return Effect.succeed(
ChildProcessSpawner.makeHandle({
// Outside the valid pid range, so PiRpc's process-group kill never lands.
From a09a9c9390393b106f10aabe178d62b04f8f361b Mon Sep 17 00:00:00 2001
From: ScriptedAlchemy
Date: Tue, 3 Nov 2026 19:03:17 -0800
Subject: [PATCH 10/10] test(labs): include the renumbered evaluation migration
in preview reconciliation
Co-Authored-By: Claude Opus 5.5
---
apps/server/src/persistence/reconcileV2PreviewMigration.test.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts b/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts
index 2bc07a121057c..ee568d9a9dd75 100644
--- a/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts
+++ b/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts
@@ -67,6 +67,7 @@ describe("V2 preview upgrade", () => {
[85, "ProjectionProjectsPinArchive"],
[86, "RepairBooleanSettledOverride"],
[87, "DropOrchestrationEventCorrelationIndex"],
+ [88, "ModelEvaluations"],
]);
assert.deepStrictEqual(yield* runMigrations(), []);
assert.deepStrictEqual(yield* sql`SELECT * FROM orchestration_v2_legacy_imports`, imports);
@@ -177,6 +178,7 @@ describe("V2 preview upgrade", () => {
[85, "ProjectionProjectsPinArchive"],
[86, "RepairBooleanSettledOverride"],
[87, "DropOrchestrationEventCorrelationIndex"],
+ [88, "ModelEvaluations"],
]);
}).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))),
);