@mutagent/evaluator 0.1.0-alpha.3 → 0.1.0-alpha.5
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/.claude/skills/mutagent-evaluator/SKILL.md +19 -7
- package/.claude/skills/mutagent-evaluator/assets/agents/discovery.md +2 -2
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +65 -1
- package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +1 -0
- package/.claude/skills/mutagent-evaluator/references/workflows/orchestrator-protocol.md +20 -7
- package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/artifact-paths.ts +16 -0
- package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +14 -7
- package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +18 -2
- package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +285 -0
- package/.claude/skills/mutagent-evaluator/scripts/config/load.ts +45 -9
- package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +63 -8
- package/.claude/skills/mutagent-evaluator/scripts/load-bundle.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-discover-report.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +21 -6
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -2
- package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +114 -18
- package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +6 -2
- package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
- package/.claude/skills/mutagent-evaluator/workflows/audit.workflow.js +1 -1
- package/bin/mutagent-cli.mjs +698 -7
- package/package.json +3 -3
|
@@ -93,16 +93,29 @@ export function detectLegacyConfig(obj: unknown): string[] {
|
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* How the evaluator's source was bound (or why it couldn't be). The SELECTION
|
|
96
|
-
* contract (v0.2.
|
|
97
|
-
* bound — exactly one source resolved (single entry ·
|
|
96
|
+
* contract (v0.2.2 — parity with orchestrator + diagnostics; F2 disambiguation UX):
|
|
97
|
+
* bound — exactly one source resolved with NO ambiguity (single entry ·
|
|
98
|
+
* `--source` hit). No prompt.
|
|
99
|
+
* confirm-default — >1 source with exactly one `default:true` (F2). The default is
|
|
100
|
+
* PRESELECTED (`source`) but the caller ISSUES A CONFIRM ASK before
|
|
101
|
+
* using it (distinct from a silent bind — be explicit around ambiguity).
|
|
102
|
+
* NON-fatal + bindable: `source` is usable as-is if the operator accepts
|
|
103
|
+
* or the run is non-interactive (`--source` / CI). Carries the full
|
|
104
|
+
* candidate list so the confirm can offer an override.
|
|
98
105
|
* none — zero sources (DISCOVER needs a pick; code/dataset runs don't need a source).
|
|
99
106
|
* needs-selection — >1 source, none `default:true`, no `--source` ⇒ the caller must PROMPT the
|
|
100
|
-
* operator to pick (NON-fatal — the source EXISTS,
|
|
107
|
+
* operator to pick with NO preselection (NON-fatal — the source EXISTS,
|
|
108
|
+
* only the pick is missing).
|
|
101
109
|
* multiple-defaults— >1 source with `default:true` — an AMBIGUOUS config the operator must fix.
|
|
102
110
|
* unknown-name — an explicit `--source <name>` that matched no catalog entry.
|
|
111
|
+
*
|
|
112
|
+
* confirm-default vs needs-selection: BOTH defer a run-time ASK, but confirm-default
|
|
113
|
+
* carries a PRESELECTED default (a yes/no confirm) whereas needs-selection has none (an
|
|
114
|
+
* open pick). Both are bindable-in-principle; the distinction drives the ASK shape.
|
|
103
115
|
*/
|
|
104
116
|
export type SourceBinding =
|
|
105
117
|
| { kind: "bound"; source: GlobalSource }
|
|
118
|
+
| { kind: "confirm-default"; source: GlobalSource; sources: GlobalSource[] }
|
|
106
119
|
| { kind: "none" }
|
|
107
120
|
| { kind: "needs-selection"; sources: GlobalSource[] }
|
|
108
121
|
| { kind: "multiple-defaults"; sources: GlobalSource[] }
|
|
@@ -128,6 +141,17 @@ export interface EvaluatorConfigResolved {
|
|
|
128
141
|
evaluatorContext: ContextLink[];
|
|
129
142
|
/** present targets (evaluator is judge-only — carried for completeness only). */
|
|
130
143
|
targets: GlobalTarget[];
|
|
144
|
+
/** PRD R-4 — the admin default render audience for the evaluation report. Client-by-default
|
|
145
|
+
* (leak-safe); an admin sets `internal` in config.yaml to show the §5 Self-Eval tab. */
|
|
146
|
+
defaultAudience: "client" | "internal";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** PRD R-4 — resolve the effective render audience: explicit flag → config → "client". */
|
|
150
|
+
export function resolveEvalAudience(
|
|
151
|
+
explicit: "client" | "internal" | undefined,
|
|
152
|
+
config?: { defaultAudience?: "client" | "internal" } | undefined,
|
|
153
|
+
): "client" | "internal" {
|
|
154
|
+
return explicit ?? config?.defaultAudience ?? "client";
|
|
131
155
|
}
|
|
132
156
|
|
|
133
157
|
export type LoadEvaluatorConfigResult =
|
|
@@ -142,14 +166,16 @@ export type LoadEvaluatorConfigResult =
|
|
|
142
166
|
*
|
|
143
167
|
* 1. explicit `selectName` (`--source`) WINS — find by name; miss ⇒ `unknown-name`.
|
|
144
168
|
* 2. 0 sources ⇒ `none`.
|
|
145
|
-
* 3. 1 source ⇒ `bound` (auto-bind).
|
|
169
|
+
* 3. 1 source ⇒ `bound` (auto-bind — no ambiguity, no prompt).
|
|
146
170
|
* 4. >1 sources:
|
|
147
|
-
* exactly one `default:true` ⇒ `
|
|
171
|
+
* exactly one `default:true` ⇒ `confirm-default` (F2 — preselected default; caller
|
|
172
|
+
* CONFIRMS before use, not a silent bind).
|
|
148
173
|
* >1 `default:true` ⇒ `multiple-defaults` (ambiguous — operator fixes config).
|
|
149
|
-
* 0 `default:true` ⇒ `needs-selection` (caller PROMPTS the operator).
|
|
174
|
+
* 0 `default:true` ⇒ `needs-selection` (caller PROMPTS the operator — open pick).
|
|
150
175
|
*
|
|
151
|
-
* Pure; no clock/random/network. NON-fatal on `needs-selection` —
|
|
152
|
-
* exists
|
|
176
|
+
* Pure; no clock/random/network. NON-fatal on `confirm-default`/`needs-selection` —
|
|
177
|
+
* the source exists; only the disambiguation confirm/pick is deferred to the run-start ASK.
|
|
178
|
+
* `--source` (explicit) always short-circuits both, satisfying the non-interactive/CI escape.
|
|
153
179
|
*/
|
|
154
180
|
function bindSourceByRole(
|
|
155
181
|
sources: GlobalSource[],
|
|
@@ -167,7 +193,11 @@ function bindSourceByRole(
|
|
|
167
193
|
if (sources.length === 1) return { kind: "bound", source: sources[0] as GlobalSource };
|
|
168
194
|
// (4) — multiple entries; disambiguate by `default:true`.
|
|
169
195
|
const defaults = sources.filter((s) => s.default === true);
|
|
170
|
-
|
|
196
|
+
// F2: a lone default is PRESELECTED but CONFIRMED (be explicit around ambiguity) —
|
|
197
|
+
// it is NOT silently bound. The caller (parent session) issues a yes/no confirm ask.
|
|
198
|
+
if (defaults.length === 1) {
|
|
199
|
+
return { kind: "confirm-default", source: defaults[0] as GlobalSource, sources };
|
|
200
|
+
}
|
|
171
201
|
if (defaults.length > 1) return { kind: "multiple-defaults", sources };
|
|
172
202
|
return { kind: "needs-selection", sources };
|
|
173
203
|
}
|
|
@@ -266,6 +296,11 @@ export function loadEvaluatorConfig(
|
|
|
266
296
|
typeof evaluatorSection.judge_runtime === "string" && evaluatorSection.judge_runtime !== ""
|
|
267
297
|
? evaluatorSection.judge_runtime
|
|
268
298
|
: undefined;
|
|
299
|
+
// PRD R-4 — admin audience override. Precedence: lifecycle.evaluator.default_audience >
|
|
300
|
+
// global.default_audience > "client". Only "internal" flips it; anything else stays client.
|
|
301
|
+
const rawAudience =
|
|
302
|
+
evaluatorSection.default_audience ?? globalBlock.default_audience;
|
|
303
|
+
const defaultAudience: "client" | "internal" = rawAudience === "internal" ? "internal" : "client";
|
|
269
304
|
|
|
270
305
|
return {
|
|
271
306
|
status: "ok",
|
|
@@ -278,6 +313,7 @@ export function loadEvaluatorConfig(
|
|
|
278
313
|
globalContext,
|
|
279
314
|
evaluatorContext,
|
|
280
315
|
targets,
|
|
316
|
+
defaultAudience,
|
|
281
317
|
},
|
|
282
318
|
};
|
|
283
319
|
}
|
|
@@ -45,7 +45,12 @@ export const LEGACY_CONFIG_VERSIONS = ["0.1.0"] as const;
|
|
|
45
45
|
|
|
46
46
|
// ── Categorical constants (no magic strings — coding-rules) ──────────────────
|
|
47
47
|
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Source platforms a `global.sources[]` entry can pull traces from.
|
|
50
|
+
* CONSUMER: the trace-fetch layer (`mutagent-cli trace fetch`, run by Helix BEFORE
|
|
51
|
+
* dispatch — the evaluator never fetches) selects the adapter by this value.
|
|
52
|
+
* PURPOSE: names WHERE the subject's traces live so they can be normalized to UniTF.
|
|
53
|
+
*/
|
|
49
54
|
export const SourcePlatform = {
|
|
50
55
|
Langfuse: "langfuse",
|
|
51
56
|
Otel: "otel",
|
|
@@ -56,12 +61,21 @@ export const SourcePlatform = {
|
|
|
56
61
|
export type SourcePlatformValue =
|
|
57
62
|
(typeof SourcePlatform)[keyof typeof SourcePlatform];
|
|
58
63
|
|
|
59
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Normalizer format hint for a source (how to parse the records).
|
|
66
|
+
* CONSUMER: the trace-fetch/normalize layer (`@mutagent/tools` adapters) — picks the
|
|
67
|
+
* parser for a source's records. PURPOSE: disambiguates record shape within a platform.
|
|
68
|
+
* `unitf` (F1) — the records are ALREADY conformant UniTF jsonl (e.g. emitted by the
|
|
69
|
+
* code-run capture WRAP, `captureUniTF` in `@mutagent/tools`). The fetch layer then
|
|
70
|
+
* SKIPS per-platform normalization and reads the jsonl DIRECTLY. Pairs with
|
|
71
|
+
* `platform: local-jsonl` + `paths:[…]`. PARITY PORT of the orchestrator's SourceFormat.
|
|
72
|
+
*/
|
|
60
73
|
export const SourceFormat = {
|
|
61
74
|
LangfuseExport: "langfuse-export",
|
|
62
75
|
ClaudeCode: "claude-code",
|
|
63
76
|
Codex: "codex",
|
|
64
77
|
Raw: "raw",
|
|
78
|
+
Unitf: "unitf",
|
|
65
79
|
} as const;
|
|
66
80
|
export type SourceFormatValue = (typeof SourceFormat)[keyof typeof SourceFormat];
|
|
67
81
|
|
|
@@ -137,7 +151,16 @@ export const ContextLinkSchema = Type.Object(
|
|
|
137
151
|
);
|
|
138
152
|
export type ContextLink = Static<typeof ContextLinkSchema>;
|
|
139
153
|
|
|
140
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* The default model + the pinned judge model (`judge_model` — was `pinned_judge`).
|
|
156
|
+
* CONSUMER: config/load.ts `resolveJudgeModel` → prep.ts `--model` gap-fill.
|
|
157
|
+
* default — the project-wide fallback judge model. PURPOSE: used ONLY when
|
|
158
|
+
* `judge_model` is absent (`from: "default"`). ASYMMETRY: diagnostics
|
|
159
|
+
* IGNORES `models.default` for its judge (it pins its own) — the field
|
|
160
|
+
* is a shared config key that the two skills consume differently.
|
|
161
|
+
* judge_model — the C-PIN pinned judge (model-intent-sacred; temp pinned 0). PURPOSE:
|
|
162
|
+
* the authoritative judge model; wins over `default` (`from: "judge_model"`).
|
|
163
|
+
*/
|
|
141
164
|
export const ModelsSchema = Type.Object(
|
|
142
165
|
{
|
|
143
166
|
default: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -154,7 +177,10 @@ export type Models = Static<typeof ModelsSchema>;
|
|
|
154
177
|
*/
|
|
155
178
|
export const GlobalSourceSchema = Type.Object(
|
|
156
179
|
{
|
|
180
|
+
// name — the catalog key. CONSUMER: config/load.ts `bindSourceByRole` (matched by
|
|
181
|
+
// `--source <name>`) + prep.ts surface messages. PURPOSE: stable id for selection.
|
|
157
182
|
name: Type.String({ minLength: 1 }),
|
|
183
|
+
// platform — see SourcePlatform. CONSUMER: the fetch/normalize adapter selector.
|
|
158
184
|
platform: Type.Union([
|
|
159
185
|
Type.Literal(SourcePlatform.Langfuse),
|
|
160
186
|
Type.Literal(SourcePlatform.Otel),
|
|
@@ -162,19 +188,34 @@ export const GlobalSourceSchema = Type.Object(
|
|
|
162
188
|
Type.Literal(SourcePlatform.ClaudeCode),
|
|
163
189
|
Type.Literal(SourcePlatform.Codex),
|
|
164
190
|
]),
|
|
191
|
+
// project — remote-source project/scope id. CONSUMER: the remote fetch adapter
|
|
192
|
+
// (e.g. Langfuse project). PURPOSE: scopes a pull on a multi-project backend.
|
|
165
193
|
project: Type.Optional(Type.String({ minLength: 1 })),
|
|
194
|
+
// endpoint — remote base URL. CONSUMER: the remote fetch adapter. PURPOSE: where to
|
|
195
|
+
// pull from (mutually exclusive with `paths`; onboarding enforces, schema tolerates).
|
|
166
196
|
endpoint: Type.Optional(Type.String({ minLength: 1 })),
|
|
197
|
+
// credential_ref — env-var NAME / {env,path} (NEVER a secret). CONSUMER: resolve-
|
|
198
|
+
// credential.ts at fetch-time. PURPOSE: names the key for a remote pull.
|
|
167
199
|
credential_ref: Type.Optional(CredentialRefSchema),
|
|
200
|
+
// paths — file-source globs. CONSUMER: the local-jsonl fetch adapter. PURPOSE: the
|
|
201
|
+
// on-disk jsonl location (pairs with `platform: local-jsonl`; see `format: unitf`).
|
|
168
202
|
paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
203
|
+
// format — see SourceFormat. CONSUMER: the normalize step (`unitf` ⇒ skip normalize,
|
|
204
|
+
// read jsonl direct). PURPOSE: record-shape hint within a platform.
|
|
169
205
|
format: Type.Optional(
|
|
170
206
|
Type.Union([
|
|
171
207
|
Type.Literal(SourceFormat.LangfuseExport),
|
|
172
208
|
Type.Literal(SourceFormat.ClaudeCode),
|
|
173
209
|
Type.Literal(SourceFormat.Codex),
|
|
174
210
|
Type.Literal(SourceFormat.Raw),
|
|
211
|
+
Type.Literal(SourceFormat.Unitf),
|
|
175
212
|
]),
|
|
176
213
|
),
|
|
214
|
+
// agent_field — record path naming the agent/subject. CONSUMER: the normalizer's
|
|
215
|
+
// subject-grouping. PURPOSE: which field identifies the agent when a trace is multi-agent.
|
|
177
216
|
agent_field: Type.Optional(Type.String({ minLength: 1 })),
|
|
217
|
+
// latency_unit — see LatencyUnit (`auto` = infer). CONSUMER: the normalizer's
|
|
218
|
+
// latency projection. PURPOSE: overrides ambiguous ms-vs-s latency records.
|
|
178
219
|
latency_unit: Type.Optional(
|
|
179
220
|
Type.Union([
|
|
180
221
|
Type.Literal(LatencyUnit.Auto),
|
|
@@ -182,16 +223,23 @@ export const GlobalSourceSchema = Type.Object(
|
|
|
182
223
|
Type.Literal(LatencyUnit.S),
|
|
183
224
|
]),
|
|
184
225
|
),
|
|
185
|
-
// PARITY (orchestrator SourceSchema): the catalog's DEFAULT pick when >1
|
|
186
|
-
// exists
|
|
187
|
-
// ⇒
|
|
226
|
+
// default — PARITY (orchestrator SourceSchema): the catalog's DEFAULT pick when >1
|
|
227
|
+
// entry exists. CONSUMER: config/load.ts `bindSourceByRole` — exactly one `default:true`
|
|
228
|
+
// ⇒ `confirm-default` (F2: preselected, operator confirms), not a silent bind. PURPOSE:
|
|
229
|
+
// marks the preferred source among many. Optional: absent ⇒ no default.
|
|
188
230
|
default: Type.Optional(Type.Boolean()),
|
|
189
231
|
},
|
|
190
232
|
{ additionalProperties: false },
|
|
191
233
|
);
|
|
192
234
|
export type GlobalSource = Static<typeof GlobalSourceSchema>;
|
|
193
235
|
|
|
194
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* A precise link to an Agent / Tooling definition file in a code target.
|
|
238
|
+
* RESERVED — consumed by Build/Improve (future). The evaluator is JUDGE-ONLY (EV-051)
|
|
239
|
+
* and never writes a target; `targets[].code_refs` is ported for shape parity only and
|
|
240
|
+
* has NO evaluator consumer today. PURPOSE (future): tells BUILD/IMPROVE which files
|
|
241
|
+
* realize the agent so a fix lands on the right def.
|
|
242
|
+
*/
|
|
195
243
|
export const CodeRefSchema = Type.Object(
|
|
196
244
|
{
|
|
197
245
|
path: Type.String({ minLength: 1 }),
|
|
@@ -201,7 +249,14 @@ export const CodeRefSchema = Type.Object(
|
|
|
201
249
|
);
|
|
202
250
|
export type CodeRef = Static<typeof CodeRefSchema>;
|
|
203
251
|
|
|
204
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* HOW a fix is applied for a target. `kind` gates the report-only branch.
|
|
254
|
+
* CONSUMER: the diagnostics APPLY path (not the evaluator — judge-only). PURPOSE:
|
|
255
|
+
* selects the write mechanism for a fix.
|
|
256
|
+
* versioning / pr — RESERVED, consumed by Build/Improve (future). No evaluator
|
|
257
|
+
* consumer; ported for parity. PURPOSE (future): toggle spec-bump /
|
|
258
|
+
* open-a-PR on apply.
|
|
259
|
+
*/
|
|
205
260
|
export const ApplySchema = Type.Object(
|
|
206
261
|
{
|
|
207
262
|
kind: Type.Union([
|
|
@@ -31,7 +31,7 @@ export const WELL_KNOWN_ARTIFACTS: Readonly<Record<string, string>> = {
|
|
|
31
31
|
tracesMetadata: "traces-metadata.json",
|
|
32
32
|
entityContext: "entity-context.json",
|
|
33
33
|
renderInput: "render-input.json",
|
|
34
|
-
report: "report.html",
|
|
34
|
+
report: "evaluation-report.html",
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/** Logical names whose VALUE is a sub-directory of evidence files. */
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/persist-eval-criteria.ts — the EVAL-LEG on-disk WRITER (Wave-2 FU57 · KP-003).
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* The IO half of the eval-leg reconcile. `sync-eval-criteria.ts` COMPUTES the grown
|
|
5
|
+
* criteria set as DATA (`reconcileEvalCriteria` — pure upsert-by-id, monotonic, never
|
|
6
|
+
* drops); it never touches disk. This module is the deterministic WRITER the evaluator
|
|
7
|
+
* SESSION calls to PERSIST that computed set back to the located eval-suite criteria
|
|
8
|
+
* artifact — the write that bumps the artifact's freshness and thereby returns the eval
|
|
9
|
+
* leg of `#sync-spec` to `in-sync` (the builder freshness probe reads the artifact's
|
|
10
|
+
* mtime / `updatedAt`; a fresh write makes the eval criteria as new as the amended impl).
|
|
11
|
+
*
|
|
12
|
+
* SEPARATION OF CONCERNS (mirrors `artifact-paths.ts` + `aggregate-discover.ts` doing IO
|
|
13
|
+
* while `living-suite.ts` stays pure): the reconcile COMPUTE is pure/C-PIN; this WRITE is
|
|
14
|
+
* the IO layer. The SERIALIZATION here is still deterministic (stable key order, no clock
|
|
15
|
+
* embedded unless the caller passes an explicit `updatedAt`), so a persisted artifact
|
|
16
|
+
* round-trips byte-faithful.
|
|
17
|
+
*
|
|
18
|
+
* JUDGE-ONLY PRESERVED (EV-051): this file writes CRITERIA (the evaluator maintaining its
|
|
19
|
+
* own eval-suite / code-quality criteria set) — it NEVER scores a subject, NEVER decides a
|
|
20
|
+
* pass/fail, and NEVER dispatches an agent (Model-B: code is a deterministic writer; the
|
|
21
|
+
* reconcile REASONING is the ai-architect's, session-dispatched).
|
|
22
|
+
*
|
|
23
|
+
* LOCATION + FORMAT (mirror the living-suite persistence; FLAGGED-for-operator, see
|
|
24
|
+
* `EVAL_CRITERIA_ARTIFACT_FILENAMES`): the artifact lands under the evaluator's namespaced
|
|
25
|
+
* `.mutagent/evaluator/living-suite/` root (`artifact-paths.ts` `livingSuiteDir`, auto-
|
|
26
|
+
* gitignored, no-spillover-guarded) as one JSON file per LEG — the same dir + extension the
|
|
27
|
+
* builder `check-sync-spec.ts` `locateEvalCriteria` auto-discovers.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { dirname, join } from "node:path";
|
|
31
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
32
|
+
import { Value } from "@sinclair/typebox/value";
|
|
33
|
+
|
|
34
|
+
import { assertUnderRoot, livingSuiteDir } from "./artifact-paths.ts";
|
|
35
|
+
import {
|
|
36
|
+
EvalCriterionSchema,
|
|
37
|
+
EvalLegKind,
|
|
38
|
+
reconcileEvalCriteria,
|
|
39
|
+
type EvalCriteriaReconcileRequest,
|
|
40
|
+
type EvalCriteriaReconcileResult,
|
|
41
|
+
type EvalCriterion,
|
|
42
|
+
type EvalLegKindValue,
|
|
43
|
+
} from "./sync-eval-criteria.ts";
|
|
44
|
+
|
|
45
|
+
// ── the on-disk artifact shape (mirror LivingSuite `{ …, provenance }`) ───────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The persisted eval-criteria artifact — one per LEG. Mirrors the living-suite shape
|
|
49
|
+
* (a keyed collection + provenance), specialized to the reconcile result: the maintained
|
|
50
|
+
* `criteria` set, the reconcile `provenance` counters, and an OPTIONAL `updatedAt`
|
|
51
|
+
* freshness marker. `updatedAt` is the ONE clock-bearing field and it is NEVER generated
|
|
52
|
+
* here — the caller (the session, Model-B) passes it when it wants a content-level
|
|
53
|
+
* freshness stamp the builder probe's `parseEvalUpdatedAt` can read; omitted, freshness
|
|
54
|
+
* rides on the file's mtime alone (write bumps it). `additionalProperties: false` keeps the
|
|
55
|
+
* header tight while each criterion stays forward-parsing (EvalCriterionSchema is open).
|
|
56
|
+
*/
|
|
57
|
+
export const EvalCriteriaArtifactSchema = Type.Object(
|
|
58
|
+
{
|
|
59
|
+
subjectId: Type.String({ minLength: 1 }),
|
|
60
|
+
leg: Type.Union([
|
|
61
|
+
Type.Literal(EvalLegKind.EvalSuite),
|
|
62
|
+
Type.Literal(EvalLegKind.CodeQuality),
|
|
63
|
+
]),
|
|
64
|
+
criteria: Type.Array(EvalCriterionSchema),
|
|
65
|
+
provenance: Type.Object(
|
|
66
|
+
{ added: Type.Number(), updated: Type.Number(), total: Type.Number() },
|
|
67
|
+
{ additionalProperties: false },
|
|
68
|
+
),
|
|
69
|
+
updatedAt: Type.Optional(Type.String({ minLength: 1 })),
|
|
70
|
+
},
|
|
71
|
+
{ additionalProperties: false },
|
|
72
|
+
);
|
|
73
|
+
export type EvalCriteriaArtifact = Static<typeof EvalCriteriaArtifactSchema>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* FLAGGED-for-operator: the per-leg artifact FILENAME under `.mutagent/evaluator/living-suite/`.
|
|
77
|
+
* The sensible default that mirrors the living-suite location AND the builder probe's
|
|
78
|
+
* conventional fallback names (`eval-criteria` / `code-quality-criteria`), one file per leg
|
|
79
|
+
* so the two legs never collide. Both are `.json` under the dir `check-sync-spec.ts`
|
|
80
|
+
* `locateEvalCriteria` walks (`*.{yaml,yml,json}`), so a written artifact is auto-discovered
|
|
81
|
+
* as the eval-leg freshness anchor with no extra wiring. (A workspace evaluates ONE subject,
|
|
82
|
+
* so only its leg's file exists; multi-subject-per-workspace would want a `<subjectId>/`
|
|
83
|
+
* sub-dir — deferred, flagged.)
|
|
84
|
+
*/
|
|
85
|
+
export const EVAL_CRITERIA_ARTIFACT_FILENAMES: Readonly<Record<EvalLegKindValue, string>> = {
|
|
86
|
+
[EvalLegKind.EvalSuite]: "eval-suite.criteria.json",
|
|
87
|
+
[EvalLegKind.CodeQuality]: "code-quality.criteria.json",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the located eval-criteria artifact path for a leg:
|
|
92
|
+
* `<cwd>/.mutagent/evaluator/living-suite/<leg>.criteria.json`. Routes through the
|
|
93
|
+
* `artifact-paths.ts` no-spillover guard (`assertUnderRoot`), so the writer can NEVER
|
|
94
|
+
* escape the evaluator's namespaced root. `cwd` passed in (pure path construction).
|
|
95
|
+
*/
|
|
96
|
+
export function evalCriteriaArtifactPath(leg: EvalLegKindValue, cwd?: string): string {
|
|
97
|
+
return assertUnderRoot(join(livingSuiteDir(cwd), EVAL_CRITERIA_ARTIFACT_FILENAMES[leg]), cwd);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── serialize / build (pure, deterministic — no clock) ───────────────────────
|
|
101
|
+
|
|
102
|
+
/** Build the on-disk artifact from a reconcile RESULT. Pure. `updatedAt` passed in. */
|
|
103
|
+
export function evalCriteriaArtifactFromResult(
|
|
104
|
+
result: EvalCriteriaReconcileResult,
|
|
105
|
+
updatedAt?: string,
|
|
106
|
+
): EvalCriteriaArtifact {
|
|
107
|
+
return {
|
|
108
|
+
subjectId: result.subjectId,
|
|
109
|
+
leg: result.leg,
|
|
110
|
+
criteria: result.criteria,
|
|
111
|
+
provenance: result.provenance,
|
|
112
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Serialize an artifact to its on-disk JSON — DETERMINISTIC: explicit key order, 2-space
|
|
118
|
+
* indent, trailing newline. Same artifact → byte-identical string (round-trip faithful);
|
|
119
|
+
* no clock unless the artifact already carries a caller-supplied `updatedAt`.
|
|
120
|
+
*/
|
|
121
|
+
export function serializeEvalCriteriaArtifact(artifact: EvalCriteriaArtifact): string {
|
|
122
|
+
const ordered: EvalCriteriaArtifact = {
|
|
123
|
+
subjectId: artifact.subjectId,
|
|
124
|
+
leg: artifact.leg,
|
|
125
|
+
criteria: artifact.criteria,
|
|
126
|
+
provenance: artifact.provenance,
|
|
127
|
+
...(artifact.updatedAt !== undefined ? { updatedAt: artifact.updatedAt } : {}),
|
|
128
|
+
};
|
|
129
|
+
return `${JSON.stringify(ordered, null, 2)}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Guarded parse of a persisted artifact. THROWS with the first schema error. PURE. */
|
|
133
|
+
export function parseEvalCriteriaArtifact(value: unknown): EvalCriteriaArtifact {
|
|
134
|
+
if (!Value.Check(EvalCriteriaArtifactSchema, value)) {
|
|
135
|
+
const first = [...Value.Errors(EvalCriteriaArtifactSchema, value)][0];
|
|
136
|
+
throw new Error(
|
|
137
|
+
`parseEvalCriteriaArtifact: schema violation at '${first?.path ?? "(root)"}': ` +
|
|
138
|
+
`${first?.message ?? "invalid eval-criteria artifact"}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── the WRITER + reader (the IO layer) ───────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/** The result of a persist: WHERE it landed + WHAT was written (artifact + bytes). */
|
|
147
|
+
export interface PersistEvalCriteriaResult {
|
|
148
|
+
/** the located artifact path the criteria were written to. */
|
|
149
|
+
path: string;
|
|
150
|
+
/** the artifact that was serialized. */
|
|
151
|
+
artifact: EvalCriteriaArtifact;
|
|
152
|
+
/** the exact bytes written (for a byte-faithful round-trip assertion). */
|
|
153
|
+
json: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* PERSIST a reconcile result's `criteria` back to the located eval-suite criteria artifact
|
|
158
|
+
* — the WRITE that returns the eval leg to `in-sync`. Resolves the per-leg path under
|
|
159
|
+
* `.mutagent/evaluator/living-suite/` (no-spillover-guarded via `artifact-paths.ts`),
|
|
160
|
+
* ensures the dir, and writes the deterministic JSON. Handles BOTH legs by `result.leg`:
|
|
161
|
+
* the eval-suite leg (agent/skill/composite subject) and the code-quality leg (a code
|
|
162
|
+
* subject, incl. an operator-overridden code-quality set). The freshness bump is the file
|
|
163
|
+
* write itself (mtime advances → the builder probe reads the eval leg as fresh); pass
|
|
164
|
+
* `options.updatedAt` (ISO 8601) to ALSO stamp a content-level freshness marker the probe's
|
|
165
|
+
* `parseEvalUpdatedAt` reads. IO — NOT pure (that is its job); serialization is deterministic.
|
|
166
|
+
*/
|
|
167
|
+
export function persistEvalCriteria(
|
|
168
|
+
result: EvalCriteriaReconcileResult,
|
|
169
|
+
options: { cwd?: string; updatedAt?: string } = {},
|
|
170
|
+
): PersistEvalCriteriaResult {
|
|
171
|
+
const artifact = evalCriteriaArtifactFromResult(result, options.updatedAt);
|
|
172
|
+
const path = evalCriteriaArtifactPath(result.leg, options.cwd);
|
|
173
|
+
const json = serializeEvalCriteriaArtifact(artifact);
|
|
174
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
175
|
+
writeFileSync(path, json);
|
|
176
|
+
return { path, artifact, json };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Read + validate a persisted artifact (round-trip). THROWS on a schema violation. */
|
|
180
|
+
export function readEvalCriteriaArtifact(path: string): EvalCriteriaArtifact {
|
|
181
|
+
return parseEvalCriteriaArtifact(JSON.parse(readFileSync(path, "utf8")) as unknown);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load the CURRENTLY-persisted criteria for a leg (the `existing` set the next reconcile
|
|
186
|
+
* grows). Returns `[]` when no artifact exists yet (the cold / first-reconcile case) so a
|
|
187
|
+
* caller can feed it straight into an `EvalCriteriaReconcileRequest.existing`. Reads only.
|
|
188
|
+
*/
|
|
189
|
+
export function loadExistingEvalCriteria(leg: EvalLegKindValue, cwd?: string): EvalCriterion[] {
|
|
190
|
+
const path = evalCriteriaArtifactPath(leg, cwd);
|
|
191
|
+
if (!existsSync(path)) return [];
|
|
192
|
+
return readEvalCriteriaArtifact(path).criteria;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The eval-criteria artifact's effective freshness epoch (seconds) — the evaluator-side
|
|
197
|
+
* MIRROR of the builder `check-sync-spec.ts` eval-leg compare: `max(updatedAt, mtime)`
|
|
198
|
+
* (git omitted — kept in-package + git-independent). `null` when the artifact is absent.
|
|
199
|
+
* Feed this as `evalCriteriaEpoch` to `flagEvalLegDrift` to confirm a fresh write closed
|
|
200
|
+
* the loop (`in-sync`). Reads mtime (IO).
|
|
201
|
+
*/
|
|
202
|
+
export function evalCriteriaEffectiveEpoch(path: string): number | null {
|
|
203
|
+
if (!existsSync(path)) return null;
|
|
204
|
+
const epochs: number[] = [];
|
|
205
|
+
try {
|
|
206
|
+
const parsed = readEvalCriteriaArtifact(path);
|
|
207
|
+
if (parsed.updatedAt !== undefined) {
|
|
208
|
+
const ms = Date.parse(parsed.updatedAt);
|
|
209
|
+
if (Number.isFinite(ms)) epochs.push(Math.floor(ms / 1000));
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// a corrupt artifact still has an mtime — fall back to it for freshness.
|
|
213
|
+
}
|
|
214
|
+
epochs.push(Math.floor(statSync(path).mtimeMs / 1000));
|
|
215
|
+
return Math.max(...epochs);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Convenience for the session: COMPUTE the reconcile (pure) THEN PERSIST it (IO) in one
|
|
220
|
+
* call — the exact "reconcile → write → in-sync" step the `#sync-spec` eval leg performs.
|
|
221
|
+
* Still Model-B: the criteria DELTA (`request.proposed`) is the ai-architect's reasoning;
|
|
222
|
+
* this only applies it deterministically and writes it. Returns the compute result + the
|
|
223
|
+
* persist result together.
|
|
224
|
+
*/
|
|
225
|
+
export function reconcileAndPersistEvalCriteria(
|
|
226
|
+
request: EvalCriteriaReconcileRequest,
|
|
227
|
+
options: { cwd?: string; updatedAt?: string } = {},
|
|
228
|
+
): { result: EvalCriteriaReconcileResult; persisted: PersistEvalCriteriaResult } {
|
|
229
|
+
const result = reconcileEvalCriteria(request);
|
|
230
|
+
const persisted = persistEvalCriteria(result, options);
|
|
231
|
+
return { result, persisted };
|
|
232
|
+
}
|
|
@@ -1566,7 +1566,7 @@ export function writeDiscoverRunReport(
|
|
|
1566
1566
|
return writeDiscoverReportFromFiles(
|
|
1567
1567
|
{
|
|
1568
1568
|
criteriaPath: join(dir, "criteria.json"),
|
|
1569
|
-
outPath: join(dir, "report.html"),
|
|
1569
|
+
outPath: join(dir, "discovery-report.html"),
|
|
1570
1570
|
groundingPath: join(dir, "grounding-check.json"),
|
|
1571
1571
|
triagePath: triageSummaryPath,
|
|
1572
1572
|
verdictsDir: join(dir, "verdicts"),
|
|
@@ -358,6 +358,9 @@ export interface CriterionResolution {
|
|
|
358
358
|
|
|
359
359
|
export interface EvalReportInput {
|
|
360
360
|
subject: { name: string; org?: string; source?: string; models?: string[] };
|
|
361
|
+
/** PRD R-4 — render audience. Default "client" (leak-safe): the §5 Self-Eval [INTERNAL]
|
|
362
|
+
* tab + nav button are NOT emitted. "internal" (admin opt-in) shows them. */
|
|
363
|
+
audience?: "client" | "internal";
|
|
361
364
|
/** the v2 *evaluate Scorecard (GATE + variance). */
|
|
362
365
|
scorecard: Scorecard;
|
|
363
366
|
/** the folded per-criterion verdicts. */
|
|
@@ -3479,6 +3482,16 @@ export function renderEvalReport(input: EvalReportInput): string {
|
|
|
3479
3482
|
const theme = readFileSync(join(BRAND_DIR, "theme.css"), "utf8");
|
|
3480
3483
|
const wordmark = readFileSync(join(BRAND_DIR, "wordmark.html"), "utf8");
|
|
3481
3484
|
|
|
3485
|
+
// PRD R-4 — audience gate. CLIENT is the DEFAULT (leak-safe): the §5 Self-Eval [INTERNAL]
|
|
3486
|
+
// tab + nav button are NOT emitted into the HTML at all (NODE-STRIP, not CSS-hidden).
|
|
3487
|
+
const internal = (input.audience ?? "client") === "internal";
|
|
3488
|
+
const selfEvalNav = internal
|
|
3489
|
+
? ` <button class="tab-btn internal" data-tab="t5">⑤ Self-Eval [INTERNAL]</button>\n`
|
|
3490
|
+
: "";
|
|
3491
|
+
const selfEvalPanel = internal
|
|
3492
|
+
? ` <section class="panel" id="t5">${selfEvalTab(input)}</section>\n`
|
|
3493
|
+
: "";
|
|
3494
|
+
|
|
3482
3495
|
const headerTitle = "evaluator · Eval Report";
|
|
3483
3496
|
const headerMeta =
|
|
3484
3497
|
`<span class="mk">subject</span> <span class="mv">${esc(input.subject.name)}</span>` +
|
|
@@ -3564,15 +3577,13 @@ ${header}
|
|
|
3564
3577
|
<button class="tab-btn" data-tab="t2">② Trajectory · Judge Behaviour</button>
|
|
3565
3578
|
<button class="tab-btn" data-tab="t3">③ Eval Scorecard</button>
|
|
3566
3579
|
<button class="tab-btn" data-tab="t4">④ Findings</button>
|
|
3567
|
-
|
|
3568
|
-
</nav>
|
|
3580
|
+
${selfEvalNav}</nav>
|
|
3569
3581
|
<main>
|
|
3570
3582
|
<section class="panel active" id="t1">${overviewTab(input)}</section>
|
|
3571
3583
|
<section class="panel" id="t2">${trajectoryTab(input)}</section>
|
|
3572
3584
|
<section class="panel" id="t3">${scorecardTab(input)}</section>
|
|
3573
3585
|
<section class="panel" id="t4">${findingsTab(input)}</section>
|
|
3574
|
-
|
|
3575
|
-
</main>
|
|
3586
|
+
${selfEvalPanel}</main>
|
|
3576
3587
|
<script data-pii="ledger">
|
|
3577
3588
|
${clientScript(ledgerJson, critJson, walksJson, resMetaJson)}
|
|
3578
3589
|
</script>
|
|
@@ -3635,6 +3646,8 @@ export function writeEvalReportFromFiles(
|
|
|
3635
3646
|
* one trace off the raw ndjson and passes it here; the system prompt is identical
|
|
3636
3647
|
* across the batch so one trace suffices. ABSENT ⇒ system prompt stays UNAVAILABLE. */
|
|
3637
3648
|
traces?: EvalTrace[];
|
|
3649
|
+
/** PRD R-4 — render audience (default "client", leak-safe). "internal" shows §5. */
|
|
3650
|
+
audience?: "client" | "internal";
|
|
3638
3651
|
},
|
|
3639
3652
|
io: EvalReportFilesIO,
|
|
3640
3653
|
): { report: string; completeness: EvalReportCompleteness } {
|
|
@@ -3746,6 +3759,8 @@ export function writeEvalReportFromFiles(
|
|
|
3746
3759
|
// WS-6 — forward the trace(s) so the system prompt is reconstructed for the hero.
|
|
3747
3760
|
...(params.traces !== undefined ? { traces: params.traces } : {}),
|
|
3748
3761
|
});
|
|
3762
|
+
// PRD R-4 — set audience on the built input (buildEvalReportInput does not forward it).
|
|
3763
|
+
if (params.audience !== undefined) input.audience = params.audience;
|
|
3749
3764
|
io.writeFile(params.outPath, renderEvalReport(input));
|
|
3750
3765
|
|
|
3751
3766
|
// 5) the data-completeness probe over the BOUND ledger (what the UI will render).
|
|
@@ -3803,9 +3818,9 @@ async function main(): Promise<void> {
|
|
|
3803
3818
|
const { join: pjoin } = await import("node:path");
|
|
3804
3819
|
const input = JSON.parse(rf(inputPath, "utf8")) as EvalReportInput;
|
|
3805
3820
|
// P8: a bare run DEFAULTS the report under the localized dot-root
|
|
3806
|
-
// `.mutagent/evaluator/reports/<runId>/report.html` (never /tmp or .memory).
|
|
3821
|
+
// `.mutagent/evaluator/reports/<runId>/evaluation-report.html` (never /tmp or .memory).
|
|
3807
3822
|
const runId = runIdArg ?? input.subject.name;
|
|
3808
|
-
const outPath = outArg ?? pjoin(reportDir(runId), "report.html");
|
|
3823
|
+
const outPath = outArg ?? pjoin(reportDir(runId), "evaluation-report.html");
|
|
3809
3824
|
mkdirSync(pjoin(outPath, ".."), { recursive: true });
|
|
3810
3825
|
console.info(renderEvalCards(input));
|
|
3811
3826
|
writeFileSync(outPath, renderEvalReport(input));
|
|
@@ -40,7 +40,13 @@ export const AdlStage = {
|
|
|
40
40
|
} as const;
|
|
41
41
|
export type AdlStageValue = (typeof AdlStage)[keyof typeof AdlStage];
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// 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;
|
|
44
50
|
export type SubjectKindValue = (typeof SubjectKind)[keyof typeof SubjectKind];
|
|
45
51
|
|
|
46
52
|
export const ArtifactKind = {
|
|
@@ -67,7 +73,11 @@ export type EscalationPolicyValue =
|
|
|
67
73
|
// ── TypeBox schemas (closed objects — auditable boundary) ───────────────────
|
|
68
74
|
const SubjectSchema = Type.Object(
|
|
69
75
|
{
|
|
70
|
-
kind: Type.Union([
|
|
76
|
+
kind: Type.Union([
|
|
77
|
+
Type.Literal(SubjectKind.Skill),
|
|
78
|
+
Type.Literal(SubjectKind.Agent),
|
|
79
|
+
Type.Literal(SubjectKind.Code),
|
|
80
|
+
]),
|
|
71
81
|
name: Type.String({ minLength: 1 }),
|
|
72
82
|
path: Type.String({ minLength: 1 }),
|
|
73
83
|
},
|