@mutagent/evaluator 0.1.0-alpha.6 → 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +23 -25
  2. package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +6 -5
  3. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +77 -14
  4. package/.claude/skills/mutagent-evaluator/assets/templates/discover-report.template.html +397 -0
  5. package/.claude/skills/mutagent-evaluator/assets/templates/eval-report.template.html +594 -0
  6. package/.claude/skills/mutagent-evaluator/assets/templates/review-report.template.html +560 -0
  7. package/.claude/skills/mutagent-evaluator/golden/judge-trajectory.prose.md +69 -0
  8. package/.claude/skills/mutagent-evaluator/references/data-registry.md +43 -0
  9. package/.claude/skills/mutagent-evaluator/references/eval-layers.md +52 -0
  10. package/.claude/skills/mutagent-evaluator/references/eval-stage.md +1 -1
  11. package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +254 -9
  12. package/.claude/skills/mutagent-evaluator/scripts/audience.ts +84 -0
  13. package/.claude/skills/mutagent-evaluator/scripts/code-eval-library.ts +201 -0
  14. package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +101 -39
  15. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +287 -3
  16. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +30 -4
  17. package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +130 -68
  18. package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +219 -1
  19. package/.claude/skills/mutagent-evaluator/scripts/merge-criteria.ts +849 -0
  20. package/.claude/skills/mutagent-evaluator/scripts/render-discover-report-v3.ts +1155 -0
  21. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report-v3.ts +610 -0
  22. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +2 -1
  23. package/.claude/skills/mutagent-evaluator/scripts/render-review-report-v3.ts +691 -0
  24. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +10 -7
  25. package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +46 -1
  26. package/.claude/skills/mutagent-evaluator/scripts/run-review.ts +266 -0
  27. package/.claude/skills/mutagent-evaluator/scripts/verify-render.ts +728 -0
  28. package/package.json +2 -2
@@ -37,16 +37,19 @@ export const AdlStage = {
37
37
  Diagnose: "diagnose",
38
38
  Optimize: "optimize",
39
39
  Audit: "audit",
40
+ // ⑥ SHIP (#1202) — mirrors the orchestrator handover-contract AdlStage. The evaluator
41
+ // never emits a ship handover (route-failures always routes to diagnose), but the mirror
42
+ // stays in lock-step with the canonical enum by design.
43
+ Ship: "ship",
40
44
  } as const;
41
45
  export type AdlStageValue = (typeof AdlStage)[keyof typeof AdlStage];
42
46
 
43
47
  // Mirrors the orchestrator handover-contract SubjectKind. The CANONICAL inter-stage
44
- // subject vocabulary is `agent | skill | code` (matches config-schema TargetSubject).
45
- // `code` (Wave-2 follow-up) is ADDITIVE every existing skill/agent handoff still
46
- // validates; it only lets a `code` subject route through *evaluate's failure handoff
47
- // the SAME way it flows through the orchestrator handover-contract (the two are kept
48
- // in lock-step by design; the evaluator re-implements the shape, never imports it).
49
- export const SubjectKind = { Skill: "skill", Agent: "agent", Code: "code" } as const;
48
+ // subject vocabulary is `agent | skill` how the subject is REALIZED (its substrate)
49
+ // is a separate axis, NOT conflated into `kind`. The #1202 / FU-69 §1.7 / ORCH-08 cleanup
50
+ // DROPPED `code` from this union in lock-step with the orchestrator handover-contract (the
51
+ // evaluator re-implements the shape, never imports it).
52
+ export const SubjectKind = { Skill: "skill", Agent: "agent" } as const;
50
53
  export type SubjectKindValue = (typeof SubjectKind)[keyof typeof SubjectKind];
51
54
 
52
55
  export const ArtifactKind = {
@@ -76,7 +79,6 @@ const SubjectSchema = Type.Object(
76
79
  kind: Type.Union([
77
80
  Type.Literal(SubjectKind.Skill),
78
81
  Type.Literal(SubjectKind.Agent),
79
- Type.Literal(SubjectKind.Code),
80
82
  ]),
81
83
  name: Type.String({ minLength: 1 }),
82
84
  path: Type.String({ minLength: 1 }),
@@ -144,6 +146,7 @@ const HandoverBundleSchema = Type.Object(
144
146
  Type.Literal(AdlStage.Diagnose),
145
147
  Type.Literal(AdlStage.Optimize),
146
148
  Type.Literal(AdlStage.Audit),
149
+ Type.Literal(AdlStage.Ship),
147
150
  ]),
148
151
  subject: SubjectSchema,
149
152
  intent: IntentSchema,
@@ -56,6 +56,7 @@ import {
56
56
  renderEvalReport,
57
57
  } from "./render-eval-report.ts";
58
58
  import { maskedCanonicalJson } from "./mask.ts";
59
+ import { renderEvalReportV3 } from "./render-eval-report-v3.ts";
59
60
  import {
60
61
  calibrationItems,
61
62
  routeFailures,
@@ -95,6 +96,12 @@ export interface EvaluateRunInput {
95
96
  * ABSENT ⇒ byte-stable legacy packets (the judge reconstructs at reason-time).
96
97
  */
97
98
  subjectProfile?: SubjectProfile;
99
+ /**
100
+ * v3 D1=B+ — the pipeable `*evaluate-<layer>` filter carried into every packet:
101
+ * ABSENT/empty ⇒ the FULL layered walk (byte-stable legacy packets). Values are
102
+ * layer ids (L0..L4); the judge runs ONLY those phases — criteria still verdicted.
103
+ */
104
+ layerScope?: string[];
98
105
  pin: PinnedEnvelope;
99
106
  /** where PREP writes packet files. */
100
107
  packetDir: string;
@@ -291,6 +298,7 @@ export function prepMatrixPackets(input: EvaluateRunInput): string[] {
291
298
  entry.residualCriteria,
292
299
  input.pin,
293
300
  input.subjectProfile,
301
+ input.layerScope,
294
302
  );
295
303
  ids.push(writeMatrixPacket(input.packetDir, packet));
296
304
  }
@@ -593,7 +601,44 @@ export function writeRunReport(
593
601
  const dir = reportDir(runId, cwd);
594
602
  mkdirSync(dir, { recursive: true });
595
603
  const outPath = join(dir, "evaluation-report.html");
596
- writeFileSync(outPath, renderEvalReport(reportInput));
604
+ // W3 — the production report is the FROZEN-CONTRACT template render (v3). The v2
605
+ // renderer stays available (evaluation-report.v2.html) as the transition fallback
606
+ // so nothing the operator relied on disappears in one step.
607
+ const gate = result.scorecard.gate as unknown as {
608
+ passed?: boolean;
609
+ runVerdict?: string;
610
+ failedCriteria?: { criterionId: string; severity?: string }[];
611
+ gatedBy?: { criterionId: string; severity?: string }[];
612
+ indeterminateBy?: { criterionId: string; severity?: string }[];
613
+ };
614
+ writeFileSync(
615
+ outPath,
616
+ renderEvalReportV3({
617
+ subjectName: input.subject.name,
618
+ runId,
619
+ audience: audience === "internal" ? "internal" : "external",
620
+ criteria: input.criteria,
621
+ files: matrixVerdictFiles,
622
+ ...(input.subjectProfile !== undefined ? { subjectProfile: input.subjectProfile as unknown as Record<string, unknown> } : {}),
623
+ pin: input.pin,
624
+ gate: {
625
+ passed: gate.passed ?? false,
626
+ runVerdict: gate.runVerdict ?? "fail",
627
+ failedCriteria: gate.failedCriteria ?? [],
628
+ ...(gate.gatedBy !== undefined ? { gatedBy: gate.gatedBy } : {}),
629
+ ...(gate.indeterminateBy !== undefined ? { indeterminateBy: gate.indeterminateBy } : {}),
630
+ },
631
+ groundedPct: result.groundingReadiness.groundedPctOfDecided,
632
+ traces: input.trajectories as unknown as { id: string }[],
633
+ generatedAt: input.producedAt,
634
+ independentVerify: result.independentVerify.map((r) => ({
635
+ criterionId: r.criterionId ?? "?",
636
+ upheld: r.upheld,
637
+ reason: r.reason,
638
+ })),
639
+ }),
640
+ );
641
+ writeFileSync(join(dir, "evaluation-report.v2.html"), renderEvalReport(reportInput));
597
642
  return outPath;
598
643
  }
599
644
 
@@ -0,0 +1,266 @@
1
+ /**
2
+ * run-review — the `*review` RUN COMPOSER.
3
+ *
4
+ * WHAT THIS CLOSES. `render-review-report-v3.ts` could render the W4 review surface,
5
+ * but nothing ASSEMBLED its input: producing the report meant hand-building a
6
+ * `RenderReviewV3Input` in a session driver. This module reads an existing run
7
+ * directory — the artifacts `*evaluate` already persists, no new ones — and produces
8
+ * `review-report.html` beside `evaluation-report.html`. After this, the SYSTEM
9
+ * produces the review report; a human no longer does.
10
+ *
11
+ * It is the analogue of run-evaluate's `writeRunReport` and the discovery composer,
12
+ * and it reuses their conventions: the same run/report directory layout, the same
13
+ * strict verdict-file parser, the same "v2 output kept alongside" transition rule.
14
+ *
15
+ * ── WHAT IT READS (all pre-existing) ──────────────────────────────────────────
16
+ * <runDir>/run-input.full.json subject · criteria · pin · producedAt, and the
17
+ * INGESTED trajectories (with their observations,
18
+ * which give the drill's left lane real tool I/O)
19
+ * <runDir>/run-input.json the same minus the heavy arrays — the fallback,
20
+ * used only when the full file is absent
21
+ * <runDir>/verdicts/*.verdict.json one judged trajectory each
22
+ *
23
+ * `*.verify.json` files live in the same directory and are NOT verdicts (they are
24
+ * the independent-verify ledger); they are excluded by suffix, not by guesswork.
25
+ *
26
+ * ── THE JOIN, AND WHY IT IS NOT THE FILENAME ──────────────────────────────────
27
+ * A verdict file is named for its PACKET, not its trajectory: `ffc2f7c7.verdict.json`
28
+ * carries `trajectoryId: "b90818a24afc5069"`. The join is therefore on the parsed
29
+ * `trajectoryId` — never on the file stem, which would silently produce a report
30
+ * whose traces match nothing.
31
+ *
32
+ * ── HONESTY ───────────────────────────────────────────────────────────────────
33
+ * An ingested trajectory with no verdict file is NOT silently dropped: the composer
34
+ * counts it and emits a `scopeNote` naming how many of the ingested trajectories were
35
+ * judged, which the surface renders as a declared scope reduction. `run-input.json`'s
36
+ * placeholder strings (it stores `"<5 EvalTraces>"` in place of the arrays) are
37
+ * detected and treated as ABSENT rather than parsed as data.
38
+ *
39
+ * ── DELIBERATELY OUT OF SCOPE — this is NOT W4 ────────────────────────────────
40
+ * No feedback store, no ruling persistence, no propagation of a ruling into
41
+ * `*validate`, no calibration application. This module PRODUCES the artifact; it does
42
+ * not consume a reviewer's answer. That loop is task #9 and is a separate contract.
43
+ *
44
+ * PURE except the file reads and the report write.
45
+ */
46
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
47
+ import { join } from "node:path";
48
+ import { parseMatrixVerdictFile } from "./contracts/eval-matrix.ts";
49
+ import type { MatrixCriterion, MatrixVerdictFile } from "./contracts/eval-matrix.ts";
50
+ import type { EvalTrace } from "./contracts/eval-types.ts";
51
+ import { runDir as defaultRunDir } from "./artifact-paths.ts";
52
+ import { renderReviewReportV3, writeReviewRunReportV3, type RenderReviewV3Input } from "./render-review-report-v3.ts";
53
+
54
+ /** the trajectory shape the review surface needs — id plus real observations. */
55
+ interface IngestedTrace {
56
+ id: string;
57
+ observations?: { type?: string; name?: string; input?: unknown; output?: unknown }[];
58
+ }
59
+
60
+ /** the subset of a persisted run-input this composer reads. */
61
+ interface PersistedRunInput {
62
+ subject?: { name?: string; kind?: string };
63
+ trajectories?: unknown;
64
+ criteria?: MatrixCriterion[];
65
+ subjectProfile?: unknown;
66
+ pin?: { model?: string; temperature?: number };
67
+ producedAt?: string;
68
+ }
69
+
70
+ export interface ReviewRunArgs {
71
+ runId: string;
72
+ /** repo root; defaults to process.cwd(). Reports land under its `.mutagent`. */
73
+ cwd?: string;
74
+ /** override the run directory (defaults to the standard runs/<runId> path). */
75
+ runDir?: string;
76
+ /** see render-review-report-v3: this surface renders identically either way. */
77
+ audience?: "internal" | "external";
78
+ devFeedback?: boolean;
79
+ /** the living-suite version these criteria came from, when known. */
80
+ suiteVersion?: string;
81
+ /** appended to the composer's own derived scope note. */
82
+ scopeNote?: string;
83
+ }
84
+
85
+ export interface ReviewRunResult {
86
+ report: string;
87
+ fallback: string | null;
88
+ runId: string;
89
+ trajectoriesIngested: number;
90
+ trajectoriesJudged: number;
91
+ verdicts: number;
92
+ criteria: number;
93
+ /** verdict files whose trajectoryId matched no ingested trajectory. */
94
+ unjoinedTrajectoryIds: string[];
95
+ }
96
+
97
+ /** `run-input.json` stores `"<5 EvalTraces>"` where the full file stores the array. */
98
+ function asArray<T>(v: unknown): T[] | null {
99
+ return Array.isArray(v) ? (v as T[]) : null;
100
+ }
101
+
102
+ /** Read the persisted run-input, preferring the full file. FAIL-LOUD when neither exists. */
103
+ function readRunInput(dir: string): { data: PersistedRunInput; path: string } {
104
+ for (const name of ["run-input.full.json", "run-input.json"]) {
105
+ const p = join(dir, name);
106
+ if (existsSync(p)) return { data: JSON.parse(readFileSync(p, "utf8")) as PersistedRunInput, path: p };
107
+ }
108
+ throw new Error(
109
+ `run-review: no run-input.full.json or run-input.json in '${dir}'. ` +
110
+ "The review surface is composed from an EXISTING *evaluate run — point it at a run directory that has one.",
111
+ );
112
+ }
113
+
114
+ /** Every `*.verdict.json` in the run's verdict dir, parsed strictly. */
115
+ function readVerdictFiles(dir: string): MatrixVerdictFile[] {
116
+ const vdir = join(dir, "verdicts");
117
+ if (!existsSync(vdir)) throw new Error(`run-review: no verdicts/ directory in '${dir}' — nothing has been judged for this run.`);
118
+ // `.verify.json` is the independent-verify ledger, NOT a verdict — excluded by
119
+ // suffix so a future sibling artifact cannot silently be parsed as a verdict.
120
+ const names = readdirSync(vdir).filter((f) => f.endsWith(".verdict.json")).sort();
121
+ if (names.length === 0) throw new Error(`run-review: no *.verdict.json files in '${vdir}' — nothing has been judged for this run.`);
122
+ return names.map((n) => {
123
+ try {
124
+ return parseMatrixVerdictFile(readFileSync(join(vdir, n), "utf8"));
125
+ } catch (e) {
126
+ // name the FILE — a schema violation with no filename is unactionable when
127
+ // a run carries dozens of verdicts.
128
+ throw new Error(`run-review: ${n} is not a valid verdict file — ${String((e as Error).message).slice(0, 200)}`);
129
+ }
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Assemble a `RenderReviewV3Input` from a run directory. Pure apart from the reads,
135
+ * and exported so the composition can be asserted without writing a report.
136
+ */
137
+ export function readReviewRunInput(args: ReviewRunArgs): { input: RenderReviewV3Input; stats: Omit<ReviewRunResult, "report" | "fallback"> } {
138
+ const dir = args.runDir ?? defaultRunDir(args.runId, args.cwd);
139
+ if (!existsSync(dir)) throw new Error(`run-review: run directory not found: '${dir}'`);
140
+ const { data } = readRunInput(dir);
141
+
142
+ const criteria = asArray<MatrixCriterion>(data.criteria);
143
+ if (criteria === null || criteria.length === 0)
144
+ throw new Error(`run-review: the run-input in '${dir}' carries no criteria array — the review surface has nothing to show rows for.`);
145
+
146
+ const files = readVerdictFiles(dir);
147
+ // the full run-input holds real trajectories; the lean one holds a placeholder
148
+ // STRING, which must read as absent rather than be coerced into a trace list.
149
+ const ingested = asArray<IngestedTrace>(data.trajectories) ?? [];
150
+ const ingestedIds = new Set(ingested.map((t) => t.id));
151
+ const judgedIds = new Set(files.map((f) => f.trajectoryId));
152
+ const unjoined = [...judgedIds].filter((id) => !ingestedIds.has(id));
153
+
154
+ // NAMED scope: an ingested-but-unjudged trajectory is declared, never dropped.
155
+ const notes: string[] = [];
156
+ if (ingested.length > 0 && judgedIds.size < ingested.length)
157
+ notes.push(`${judgedIds.size} of ${ingested.length} ingested trajectories carry a judge verdict; the rest are not shown`);
158
+ if (ingested.length === 0)
159
+ notes.push("the run-input carried no trajectory array, so the drill shows the judge's own step record rather than raw tool I/O");
160
+ if (unjoined.length > 0)
161
+ notes.push(`${unjoined.length} verdict file(s) reference a trajectory absent from the run-input (${unjoined.join(", ")})`);
162
+ if (args.scopeNote !== undefined) notes.push(args.scopeNote);
163
+
164
+ const input: RenderReviewV3Input = {
165
+ subjectName: data.subject?.name ?? args.runId,
166
+ runId: args.runId,
167
+ audience: args.audience ?? "internal",
168
+ criteria,
169
+ files,
170
+ ...(ingested.length > 0 ? { traces: ingested } : {}),
171
+ ...(data.subjectProfile !== undefined && typeof data.subjectProfile === "object" && data.subjectProfile !== null
172
+ ? { subjectProfile: data.subjectProfile as Record<string, unknown> }
173
+ : {}),
174
+ pin: { model: data.pin?.model ?? "(judge model not recorded in the run-input)", temperature: 0 },
175
+ ...(data.producedAt !== undefined ? { generatedAt: data.producedAt } : {}),
176
+ ...(args.suiteVersion !== undefined ? { suiteVersion: args.suiteVersion } : {}),
177
+ ...(notes.length > 0 ? { scopeNote: notes.join(" · ") } : {}),
178
+ ...(args.devFeedback !== undefined ? { devFeedback: args.devFeedback } : {}),
179
+ };
180
+
181
+ return {
182
+ input,
183
+ stats: {
184
+ runId: args.runId,
185
+ trajectoriesIngested: ingested.length,
186
+ trajectoriesJudged: judgedIds.size,
187
+ verdicts: files.reduce((a, f) => a + f.verdicts.length, 0),
188
+ criteria: criteria.length,
189
+ unjoinedTrajectoryIds: unjoined,
190
+ },
191
+ };
192
+ }
193
+
194
+ /**
195
+ * THE COMPOSER — read a run directory and write `review-report.html` (plus the v1
196
+ * `review-report.v2.html` fallback when the run carries ingested traces the v1 UI
197
+ * can consume). Returns the paths and what was actually composed.
198
+ */
199
+ export function writeReviewRunReport(args: ReviewRunArgs): ReviewRunResult {
200
+ const { input, stats } = readReviewRunInput(args);
201
+ const repoRoot = args.cwd ?? process.cwd();
202
+ // the v1 annotation UI consumes EvalTrace[]; the ingested trajectories ARE that
203
+ // shape in a persisted run-input. Absent ⇒ no fallback is written and the result
204
+ // says so with `fallback: null`, rather than an empty file pretending to be one.
205
+ const evalTraces = (input.traces ?? []) as unknown as EvalTrace[];
206
+ const { report, fallback } = writeReviewRunReportV3(input, repoRoot, evalTraces.length > 0 ? evalTraces : undefined);
207
+ return { ...stats, report, fallback };
208
+ }
209
+
210
+ /** Render without writing — for tests and for callers that want the HTML in hand. */
211
+ export function renderReviewRunReport(args: ReviewRunArgs): string {
212
+ return renderReviewReportV3(readReviewRunInput(args).input);
213
+ }
214
+
215
+ /* ── CLI entrypoint ────────────────────────────────────────────────────────────
216
+ *
217
+ * bun scripts/run-review.ts <runId> [--cwd <repoRoot>] [--run-dir <dir>]
218
+ * [--suite <version>] [--dev-feedback]
219
+ *
220
+ * This is the production caller the `*review` command's parent Bash invokes, mirroring
221
+ * build-review-ui.ts's CLI. Costs nothing: it reads artifacts an `*evaluate` run has
222
+ * already produced and dispatches no judge.
223
+ */
224
+ declare const Bun: { argv: string[] } | undefined;
225
+
226
+ function parseArgv(argv: string[]): ReviewRunArgs | null {
227
+ const [runId] = argv;
228
+ if (runId === undefined || runId.startsWith("--")) return null;
229
+ const flag = (name: string): string | undefined => {
230
+ const i = argv.indexOf(`--${name}`);
231
+ return i >= 0 ? argv[i + 1] : undefined;
232
+ };
233
+ const cwd = flag("cwd");
234
+ const rd = flag("run-dir");
235
+ const suite = flag("suite");
236
+ return {
237
+ runId,
238
+ ...(cwd !== undefined ? { cwd } : {}),
239
+ ...(rd !== undefined ? { runDir: rd } : {}),
240
+ ...(suite !== undefined ? { suiteVersion: suite } : {}),
241
+ ...(argv.includes("--dev-feedback") ? { devFeedback: true } : {}),
242
+ };
243
+ }
244
+
245
+ async function main(): Promise<void> {
246
+ const argv = typeof Bun !== "undefined" ? Bun.argv.slice(2) : process.argv.slice(2);
247
+ const args = parseArgv(argv);
248
+ if (args === null) {
249
+ console.error("usage: run-review.ts <runId> [--cwd <repoRoot>] [--run-dir <dir>] [--suite <version>] [--dev-feedback]");
250
+ process.exit(2);
251
+ return;
252
+ }
253
+ const r = writeReviewRunReport(args);
254
+ console.info(
255
+ `review report written: ${r.report}\n` +
256
+ ` ${r.trajectoriesJudged}/${r.trajectoriesIngested || r.trajectoriesJudged} trajectories judged · ` +
257
+ `${r.verdicts} judge verdicts · ${r.criteria} criteria` +
258
+ (r.fallback !== null ? `\n v1 fallback: ${r.fallback}` : "\n v1 fallback: not written (the run carries no ingested traces)") +
259
+ (r.unjoinedTrajectoryIds.length > 0 ? `\n WARNING: ${r.unjoinedTrajectoryIds.length} verdict(s) reference an unknown trajectory` : ""),
260
+ );
261
+ process.exit(0);
262
+ }
263
+
264
+ if (typeof Bun !== "undefined" && Bun.argv[1]?.endsWith("run-review.ts") === true) {
265
+ await main();
266
+ }