@mutagent/evaluator 0.1.0-alpha.4 → 0.1.0-alpha.6

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 (38) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +6 -5
  2. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +76 -12
  3. package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
  4. package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
  5. package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
  6. package/.claude/skills/mutagent-evaluator/references/eval-stage.md +36 -3
  7. package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
  8. package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +1 -0
  9. package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
  10. package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
  11. package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
  12. package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +6 -3
  13. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
  14. package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
  15. package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +277 -0
  16. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
  17. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +39 -0
  18. package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
  19. package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
  20. package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
  21. package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
  22. package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
  23. package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
  24. package/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
  25. package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
  26. package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
  27. package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
  28. package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
  29. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +9 -91
  30. package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
  31. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +14 -4
  32. package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +114 -18
  33. package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
  34. package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
  35. package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
  36. package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
  37. package/bin/mutagent-cli.mjs +659 -8
  38. package/package.json +2 -2
@@ -0,0 +1,277 @@
1
+ /**
2
+ * scripts/code-quality-verdict.ts — the CODE-QUALITY judge mode (Wave-2 W2I1).
3
+ * ---------------------------------------------------------------------------
4
+ * A JUDGE mode that scores a CODE subject's QUALITY (the amended/scaffolded
5
+ * implementation of a `code`-kind subject) and emits a binary PASS/FAIL quality
6
+ * verdict with a critique-before-verdict per criterion. It is the (b) half of the
7
+ * ⑤ OPTIMIZE loop's code-target BOTH-gate: "converged" for a `code` subject = the
8
+ * subject's OWN test suite green (a deterministic gate the session records as
9
+ * `testsGreen`) AND this code-quality verdict PASSes. Neither alone converges.
10
+ *
11
+ * DISTINCT FROM `code-eval.ts`. That file is the DETERMINISTIC code-track primitive
12
+ * library (presence/string-equality/... over an EvalTrace — no LLM, byte-identical).
13
+ * This file is an LLM JUDGE mode: it scores the irreducibly-subjective quality of a
14
+ * code implementation (spec-faithfulness, maintainability, error-handling, ...) that
15
+ * a deterministic primitive cannot decide.
16
+ *
17
+ * INVARIANTS (consistent with the existing evaluator cell):
18
+ * - JUDGE-ONLY (EV-051). It scores; it NEVER fixes. Failures route to the ⑤ loop's
19
+ * ai-engineer amend (via the loop, not from here).
20
+ * - HOST-RUNTIME. Under the DEFAULT agent-dispatch substrate the authoritative
21
+ * rubric lives in the evaluator agent def (`#mode-judge-code-quality`), and the
22
+ * verdict comes from a dispatched subagent — this file's prompt is the
23
+ * provider-callable MIRROR for the OPTIONAL in-house / export substrate (same
24
+ * operator-named exception as judge-prompt-template.ts). No provider key on the
25
+ * default path.
26
+ * - PINNED model + temperature 0 (C-PIN). `runCodeQualityJudge` THROWS if the pin
27
+ * is not (modelId present AND temperature === 0) — reruns must be byte-identical.
28
+ * - PURE renderer + a thin injected-seam run wrapper. No clock, no random, no
29
+ * network; the LLM call is the injected `JudgeInvoke` seam so tests drive it.
30
+ */
31
+ import { readFileSync } from "node:fs";
32
+ import { fileURLToPath, URL } from "node:url";
33
+ import { parse as parseYaml } from "yaml";
34
+ import { parseCritiqueVerdict, type JudgeInvoke } from "./determine-outcome.ts";
35
+ import type { JudgePin } from "./build-evals.ts";
36
+ import type { CritiqueVerdict } from "./contracts/eval-types.ts";
37
+
38
+ /** Severity of a code-quality criterion — gates the aggregate (CRIT/HIGH block). */
39
+ export const CodeQualitySeverity = {
40
+ Critical: "critical",
41
+ High: "high",
42
+ Medium: "medium",
43
+ } as const;
44
+ export type CodeQualitySeverityValue =
45
+ (typeof CodeQualitySeverity)[keyof typeof CodeQualitySeverity];
46
+
47
+ /** One binary code-quality criterion the judge scores. */
48
+ export interface CodeQualityCriterion {
49
+ /** Stable id (byte-identity key + report anchor). */
50
+ id: string;
51
+ /** The one-line binary statement the judge decides pass/fail on. */
52
+ statement: string;
53
+ /** What a PASS looks like (concrete, so the judge is not guessing). */
54
+ passDefinition: string;
55
+ /** What a FAIL looks like. */
56
+ failDefinition: string;
57
+ /** Gating weight — a `fail`/`uncertain` on a CRIT/HIGH criterion blocks the gate. */
58
+ severity: CodeQualitySeverityValue;
59
+ }
60
+
61
+ /**
62
+ * ⚠️ FLAGGED-FOR-OPERATOR-SIGN-OFF (Wave-2 W2I1). The DRAFT default set of acceptance
63
+ * criteria that define "code quality-pass" BEYOND tests-green.
64
+ *
65
+ * The rubric itself now lives in `../assets/code-quality-criteria.yaml` — a versioned,
66
+ * operator-editable ARTIFACT — so the criteria + severities can be tuned WITHOUT a code
67
+ * change (FU-61.1). This module only LOADS and VALIDATES it; the shape/aggregation stay
68
+ * in code. Ids/statements/severities are unchanged from the original inline set.
69
+ *
70
+ * Aggregation (also FLAGGED, still in code): severity-gated — a `fail` OR `uncertain`
71
+ * on any CRITICAL/HIGH criterion blocks the quality-pass; MEDIUM is advisory.
72
+ *
73
+ * Fail-loud: a malformed/missing rubric THROWS at import. There is no silent fallback —
74
+ * a broken rubric must never quietly degrade the gate.
75
+ */
76
+ const CODE_QUALITY_CRITERIA_PATH = fileURLToPath(
77
+ new URL("../assets/code-quality-criteria.yaml", import.meta.url),
78
+ );
79
+
80
+ const VALID_SEVERITIES: readonly string[] = Object.values(CodeQualitySeverity);
81
+
82
+ /** Parse + validate the rubric artifact. PURE given the file; THROWS on any defect. */
83
+ export function parseCodeQualityCriteria(raw: string, source = CODE_QUALITY_CRITERIA_PATH): readonly CodeQualityCriterion[] {
84
+ // NOTE: a function DECLARATION (not a const arrow) so TS narrows through the
85
+ // never-returning calls below — control-flow analysis requires this form.
86
+ function bad(msg: string): never {
87
+ throw new Error(`code-quality rubric invalid (${source}): ${msg}`);
88
+ }
89
+ const doc = parseYaml(raw) as { version?: unknown; criteria?: unknown } | null;
90
+ if (doc === null || typeof doc !== "object") bad("not a YAML mapping");
91
+ if (typeof doc.version !== "number") bad("`version` must be a number");
92
+ if (!Array.isArray(doc.criteria) || doc.criteria.length === 0) bad("`criteria` must be a non-empty list");
93
+
94
+ const seen = new Set<string>();
95
+ const out = (doc.criteria as unknown[]).map((c, i) => {
96
+ const at = `criteria[${i}]`;
97
+ if (c === null || typeof c !== "object") return bad(`${at} is not a mapping`);
98
+ const r = c as Record<string, unknown>;
99
+ for (const k of ["id", "statement", "passDefinition", "failDefinition", "severity"]) {
100
+ if (typeof r[k] !== "string" || (r[k] as string).trim() === "") bad(`${at}.${k} must be a non-empty string`);
101
+ }
102
+ const id = r.id as string;
103
+ if (seen.has(id)) bad(`${at}.id "${id}" is duplicated`);
104
+ seen.add(id);
105
+ if (!VALID_SEVERITIES.includes(r.severity as string)) {
106
+ bad(`${at}.severity "${String(r.severity)}" must be one of ${VALID_SEVERITIES.join(" | ")}`);
107
+ }
108
+ return {
109
+ id,
110
+ statement: r.statement as string,
111
+ passDefinition: r.passDefinition as string,
112
+ failDefinition: r.failDefinition as string,
113
+ severity: r.severity as CodeQualitySeverityValue,
114
+ } satisfies CodeQualityCriterion;
115
+ });
116
+ return Object.freeze(out);
117
+ }
118
+
119
+ export const DEFAULT_CODE_QUALITY_CRITERIA: readonly CodeQualityCriterion[] =
120
+ parseCodeQualityCriteria(readFileSync(CODE_QUALITY_CRITERIA_PATH, "utf8"));
121
+
122
+ /** The code subject under quality review (minimal, subject-agnostic). */
123
+ export interface CodeQualitySubject {
124
+ /** Subject id (spec_id / target name) — provenance only. */
125
+ subjectId: string;
126
+ /** The target framework/harness the impl was built into (context for the judge). */
127
+ framework: string;
128
+ /** A compact summary of the agentspec DEFINITION the impl must realize (the SSoT). */
129
+ specSummary: string;
130
+ /** The applied change under review — the amend diff, or the scaffold summary. */
131
+ appliedChange: string;
132
+ /**
133
+ * The diagnosed root-cause / remedy locus this change was meant to address (feeds
134
+ * Q2). Empty for a fresh scaffold with no prior diagnosis.
135
+ */
136
+ diagnosisLocus?: string;
137
+ }
138
+
139
+ /** One criterion's judged result (critique-before-verdict, binary). */
140
+ export interface CodeQualityCriterionVerdict {
141
+ criterionId: string;
142
+ severity: CodeQualitySeverityValue;
143
+ result: CritiqueVerdict["result"]; // pass | fail | uncertain
144
+ critique: string;
145
+ confidence: number;
146
+ }
147
+
148
+ /** The aggregated code-quality verdict for one subject. */
149
+ export interface CodeQualityVerdict {
150
+ subjectId: string;
151
+ /** The binary quality gate — the (b) half of the ⑤ code-target BOTH-gate. */
152
+ pass: boolean;
153
+ perCriterion: CodeQualityCriterionVerdict[];
154
+ /** C-PIN provenance stamped on the verdict (byte-identity guarantee). */
155
+ pinned: JudgePin;
156
+ }
157
+
158
+ /**
159
+ * Render the code-quality judge prompt for ONE criterion — the provider-callable
160
+ * mirror of the `#mode-judge-code-quality` rubric. Critique-BEFORE-verdict, strictly
161
+ * BINARY (no Likert), judge-only. No decision is made here.
162
+ */
163
+ export function buildCodeQualityPrompt(
164
+ criterion: CodeQualityCriterion,
165
+ subject: CodeQualitySubject,
166
+ ): { system: string; user: string } {
167
+ const system = [
168
+ "You are a BINARY Pass/Fail code-QUALITY judge for ONE criterion of a CODE",
169
+ "subject (an agent/tooling implementation). Judge exactly this criterion and",
170
+ "nothing else. You are a JUDGE ONLY — you NEVER propose or write a fix.",
171
+ "",
172
+ "Quality is assessed BEYOND the subject's test suite (tests-green is a separate",
173
+ "deterministic gate). Judge the code as written against the criterion.",
174
+ "",
175
+ `Criterion (${criterion.id}, severity ${criterion.severity}): ${criterion.statement}`,
176
+ criterion.passDefinition,
177
+ criterion.failDefinition,
178
+ "",
179
+ "Outcomes are strictly BINARY: pass or fail (use uncertain ONLY when the change",
180
+ "genuinely lacks the evidence to decide). NO Likert scales, NO 1-5 / letter",
181
+ "grades, NO partial credit.",
182
+ "",
183
+ "Output STRICT JSON with the critique BEFORE the verdict (reason first, then",
184
+ "commit):",
185
+ '{ "critique": "<evidence-citing assessment>", "result": "pass"|"fail"|"uncertain",',
186
+ ' "confidence": <0..1> }',
187
+ ].join("\n");
188
+
189
+ const user = [
190
+ `Subject: ${subject.subjectId} · target framework: ${subject.framework}`,
191
+ "",
192
+ "Agentspec definition the implementation must realize (the SSoT):",
193
+ subject.specSummary,
194
+ "",
195
+ subject.diagnosisLocus !== undefined && subject.diagnosisLocus.length > 0
196
+ ? `Diagnosed root-cause / remedy locus (for the addresses-diagnosis check):\n${subject.diagnosisLocus}\n`
197
+ : "",
198
+ "Applied change under review (diff or scaffold summary):",
199
+ subject.appliedChange,
200
+ ]
201
+ .filter((l) => l !== "")
202
+ .join("\n");
203
+
204
+ return { system, user };
205
+ }
206
+
207
+ /**
208
+ * The PURE aggregation: severity-gate the per-criterion verdicts into ONE binary
209
+ * quality-pass. FLAGGED default rule — a `fail` OR `uncertain` on any CRITICAL/HIGH
210
+ * criterion blocks the pass; MEDIUM is advisory (never blocks). Deterministic.
211
+ */
212
+ export function codeQualityGatePass(
213
+ perCriterion: readonly CodeQualityCriterionVerdict[],
214
+ ): boolean {
215
+ for (const v of perCriterion) {
216
+ const gating =
217
+ v.severity === CodeQualitySeverity.Critical || v.severity === CodeQualitySeverity.High;
218
+ if (gating && v.result !== "pass") return false;
219
+ }
220
+ return true;
221
+ }
222
+
223
+ /**
224
+ * Run the code-quality judge over the criteria set under a PINNED model. THROWS if
225
+ * the pin is not (modelId present AND temperature === 0) — C-PIN (byte-identical
226
+ * reruns). Renders each criterion → the injected `JudgeInvoke` seam → parses
227
+ * (critique-before-verdict) → severity-gated aggregate. Under the DEFAULT
228
+ * agent-dispatch substrate the verdicts instead come from a dispatched
229
+ * `#mode-judge-code-quality` subagent (verdict files) and this wrapper is not used.
230
+ * Deterministic given (subject, judge, pin, criteria).
231
+ */
232
+ export async function runCodeQualityJudge(
233
+ subject: CodeQualitySubject,
234
+ judge: JudgeInvoke,
235
+ pin: JudgePin,
236
+ criteria: readonly CodeQualityCriterion[] = DEFAULT_CODE_QUALITY_CRITERIA,
237
+ ): Promise<CodeQualityVerdict> {
238
+ if (typeof pin.modelId !== "string" || pin.modelId.length === 0) {
239
+ throw new Error(
240
+ "runCodeQualityJudge: judge is not pinned (missing modelId). MODEL INTENT IS " +
241
+ "SACRED — a non-pinned judge can never produce a verdict (C-PIN).",
242
+ );
243
+ }
244
+ if (pin.temperature !== 0) {
245
+ throw new Error(
246
+ `runCodeQualityJudge: judge temperature=${pin.temperature} (!= 0) — not pinned. ` +
247
+ "MODEL INTENT IS SACRED: reruns must be byte-identical (C-PIN); refusing.",
248
+ );
249
+ }
250
+ if (criteria.length === 0) {
251
+ throw new Error(
252
+ "runCodeQualityJudge: empty criteria set — a code-quality verdict with no " +
253
+ "criteria would vacuously pass (a false-green). Refusing.",
254
+ );
255
+ }
256
+
257
+ const perCriterion: CodeQualityCriterionVerdict[] = [];
258
+ for (const criterion of criteria) {
259
+ const { system, user } = buildCodeQualityPrompt(criterion, subject);
260
+ const raw = await judge(system, user);
261
+ const v = parseCritiqueVerdict(raw);
262
+ perCriterion.push({
263
+ criterionId: criterion.id,
264
+ severity: criterion.severity,
265
+ result: v.result,
266
+ critique: v.critique,
267
+ confidence: v.confidence,
268
+ });
269
+ }
270
+
271
+ return {
272
+ subjectId: subject.subjectId,
273
+ pass: codeQualityGatePass(perCriterion),
274
+ perCriterion,
275
+ pinned: { modelId: pin.modelId, temperature: pin.temperature },
276
+ };
277
+ }
@@ -235,9 +235,9 @@ export type GlobalSource = Static<typeof GlobalSourceSchema>;
235
235
 
236
236
  /**
237
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)
238
+ * RESERVED — consumed by Build/Optimize (future). The evaluator is JUDGE-ONLY (EV-051)
239
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
240
+ * has NO evaluator consumer today. PURPOSE (future): tells BUILD/OPTIMIZE which files
241
241
  * realize the agent so a fix lands on the right def.
242
242
  */
243
243
  export const CodeRefSchema = Type.Object(
@@ -253,7 +253,7 @@ export type CodeRef = Static<typeof CodeRefSchema>;
253
253
  * HOW a fix is applied for a target. `kind` gates the report-only branch.
254
254
  * CONSUMER: the diagnostics APPLY path (not the evaluator — judge-only). PURPOSE:
255
255
  * selects the write mechanism for a fix.
256
- * versioning / pr — RESERVED, consumed by Build/Improve (future). No evaluator
256
+ * versioning / pr — RESERVED, consumed by Build/Optimize (future). No evaluator
257
257
  * consumer; ported for parity. PURPOSE (future): toggle spec-bump /
258
258
  * open-a-PR on apply.
259
259
  */
@@ -184,6 +184,41 @@ export type SubjectProfileProvenanceValue =
184
184
  * harness/skill marked UNKNOWN, never confabulated). */
185
185
  export const PROFILE_UNKNOWN = "unknown" as const;
186
186
 
187
+ // ── EV-5 — tool reversibility (does a tool take an IRREVERSIBLE EXTERNAL action?) ─
188
+ //
189
+ // A routing failure that only READS is materially less severe than one that also
190
+ // sends an email or mutates a live system — but nothing surfaced "this session could
191
+ // take irreversible external actions." This classifies each subject tool so the judge
192
+ // preamble can flag it. HONEST: `unknown` when the tool's effect can't be established
193
+ // (never confabulated), never coerced to a false "reversible".
194
+ export const ToolReversibility = {
195
+ /** a pure/no-op tool (no external effect). */
196
+ None: "none",
197
+ /** an external action that can be undone / re-run safely (a read, an idempotent write). */
198
+ Reversible: "reversible",
199
+ /** an external action that CANNOT be undone (send/email/charge/delete/deploy/post). */
200
+ IrreversibleExternal: "irreversible-external",
201
+ /** the tool's effect can't be established from the inputs — flagged, never guessed. */
202
+ Unknown: "unknown",
203
+ } as const;
204
+ export type ToolReversibilityValue =
205
+ (typeof ToolReversibility)[keyof typeof ToolReversibility];
206
+
207
+ /** One tool's reversibility classification (EV-5). */
208
+ export const ToolReversibilitySchema = Type.Object(
209
+ {
210
+ name: Type.String({ minLength: 1 }),
211
+ reversibility: Type.Union([
212
+ Type.Literal(ToolReversibility.None),
213
+ Type.Literal(ToolReversibility.Reversible),
214
+ Type.Literal(ToolReversibility.IrreversibleExternal),
215
+ Type.Literal(ToolReversibility.Unknown),
216
+ ]),
217
+ },
218
+ { additionalProperties: false },
219
+ );
220
+ export type ToolReversibilityEntry = Static<typeof ToolReversibilitySchema>;
221
+
187
222
  export const SubjectProfileSchema = Type.Object(
188
223
  {
189
224
  /** what the agent IS (name / role identity). */
@@ -195,6 +230,10 @@ export const SubjectProfileSchema = Type.Object(
195
230
  purpose: Type.String({ minLength: 1 }),
196
231
  /** the tools the agent wields (GIVEN inventory or reconstructed from the trace batch). */
197
232
  tools: Type.Array(Type.String({ minLength: 1 })),
233
+ /** EV-5 — per-tool reversibility (none · reversible · irreversible-external · unknown)
234
+ * so the judge sees which tools take IRREVERSIBLE EXTERNAL actions BEFORE it weighs a
235
+ * failure. ADDITIVE/OPTIONAL: absent ⇒ the judge reads the bare tool list. */
236
+ toolReversibility: Type.Optional(Type.Array(ToolReversibilitySchema)),
198
237
  /** the skill the agent runs under, when known (`unknown` ≠ confabulated). */
199
238
  skill: Type.Optional(Type.String()),
200
239
  /** the agent's responsibility boundary / scope. */
@@ -39,6 +39,14 @@ import {
39
39
  export type JudgeInvoke = (
40
40
  systemPrompt: string,
41
41
  userPrompt: string,
42
+ /**
43
+ * INF-3 — an OPTIONAL session/trace id the DETERMINER passes so its verdict is
44
+ * keyed per-trace: two sessions with an identical (e.g. empty) determiner prompt
45
+ * never collapse onto one verdict file. Absent for the per-criterion judges (their
46
+ * prompts are already distinct; keying salt-free keeps their hashes byte-identical,
47
+ * C-PIN). Both agent-dispatch seams forward it to `verdictFileName(system,user,keyId)`.
48
+ */
49
+ keyId?: string,
42
50
  ) => Promise<string>;
43
51
 
44
52
  // ── Event classification (the intended-goal half of the read) ───────────────
@@ -149,7 +149,7 @@ export interface EddLoopDecision {
149
149
  * 1. FULL GREEN — varianceStable AND accuracyMet ⇒ DONE (success).
150
150
  * 2. MAX SWINGS — swing ≥ budget.maxSwings ⇒ STOP (report delta).
151
151
  * 3. MAX WALLCLOCK— elapsedMs ≥ budget.maxWallclockMs ⇒ STOP.
152
- * 4. NO-IMPROVE — noImprovementStreak ≥ budget.noImprovementStreakLimit ⇒ STOP.
152
+ * 4. NO-OPTIMIZE — noImprovementStreak ≥ budget.noImprovementStreakLimit ⇒ STOP.
153
153
  * else CONTINUE — keep swinging; the next phase is variance until it's stable,
154
154
  * then accuracy (F19 ordering is preserved by `nextPhaseAfterVariance`, but the
155
155
  * loop controller mirrors it: not-stable ⇒ variance, stable ⇒ accuracy).
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * scripts/edd/edd-types.ts
3
3
  * ---------------------------------------------------------------------------
4
- * Shared categorical constants + TypeBox contracts for the ADL IMPROVE stage —
4
+ * Shared categorical constants + TypeBox contracts for the ADL OPTIMIZE stage —
5
5
  * the Eval-Driven-Development (EDD) loop (F18 closure + F19 variance-first).
6
6
  *
7
- * EDD is the ③ IMPROVE stage of the ADL: after the initial *build, the loop
7
+ * EDD is the ③ OPTIMIZE stage of the ADL: after the initial *build, the loop
8
8
  * drives spec ↔ impl ↔ eval to full green. Two findings live here:
9
9
  *
10
10
  * F19 — VARIANCE-FIRST. The FIRST focus after the build is eliminating per-case
@@ -48,20 +48,94 @@ function promptOf(trace: EvalTrace): string {
48
48
 
49
49
  // ── Determiner (EV-042) prompt — MIRRORS assets/agents/error-analyst.md ──────
50
50
 
51
+ /**
52
+ * EV-1 — the compact SUBJECT FRAME the determiner reads so it derives THIS subject's
53
+ * expected outcome (identity · purpose · tools + reversibility), instead of judging
54
+ * against a hardcoded email-agent signal block. Trajectory-LIGHT by design — it never
55
+ * carries the full step-by-step trajectory (that is the per-criterion judge's job).
56
+ */
57
+ export interface SubjectFrame {
58
+ identity: string;
59
+ purpose: string;
60
+ tools: string[];
61
+ toolReversibility?: { name: string; reversibility: string }[];
62
+ }
63
+
64
+ /** The distinct tool names actually invoked in a trace, in first-seen order (compact summary). */
65
+ function keyToolsUsed(trace: EvalTrace, limit = 8): string[] {
66
+ const seen: string[] = [];
67
+ for (const o of trace.observations) {
68
+ if (o.type === "TOOL" && typeof o.name === "string" && !seen.includes(o.name)) {
69
+ seen.push(o.name);
70
+ if (seen.length >= limit) break;
71
+ }
72
+ }
73
+ return seen;
74
+ }
75
+
76
+ /** Render the subject frame block for the determiner (EV-1). Trajectory-light. */
77
+ function subjectFrameBlock(subject: SubjectFrame | undefined): string[] {
78
+ if (subject === undefined) {
79
+ return [
80
+ "SUBJECT: not supplied — RECONSTRUCT what the agent is from the input event + the",
81
+ "tools it called, and derive the expected outcome from that. Do NOT assume any",
82
+ "particular domain (e.g. email).",
83
+ "",
84
+ ];
85
+ }
86
+ const rev = new Map((subject.toolReversibility ?? []).map((t) => [t.name, t.reversibility]));
87
+ const toolLine =
88
+ subject.tools.length > 0
89
+ ? subject.tools
90
+ .map((n) => {
91
+ const r = rev.get(n);
92
+ return r !== undefined && r !== "unknown" ? `${n} [${r}]` : n;
93
+ })
94
+ .join(", ")
95
+ : "(none)";
96
+ const irreversible = (subject.toolReversibility ?? [])
97
+ .filter((t) => t.reversibility === "irreversible-external")
98
+ .map((t) => t.name);
99
+ return [
100
+ "SUBJECT (derive the EXPECTED outcome for THIS subject before you judge):",
101
+ ` identity: ${subject.identity}`,
102
+ ` purpose: ${subject.purpose}`,
103
+ ` tools: ${toolLine}`,
104
+ ...(irreversible.length > 0
105
+ ? [` ⚠ irreversible-external tools: ${irreversible.join(", ")} — a failure that also fired one is materially worse.`]
106
+ : []),
107
+ "",
108
+ ];
109
+ }
110
+
51
111
  /**
52
112
  * Render the determiner judge prompt for the in-house/export substrate. The
53
113
  * AUTHORITATIVE "inaction can be success" rubric is `error-analyst.md`; this is
54
114
  * its provider-callable mirror. No decision is made here.
115
+ *
116
+ * EV-1 — subject-aware: given a `subject` frame the determiner derives the expected
117
+ * outcome from the subject's identity/purpose/tools (not a fixed email-agent signal
118
+ * block), and reads a COMPACT outcome summary (event · did-it-act · key tools ·
119
+ * terminal state) — deliberately NOT the full trajectory (the per-criterion judge's
120
+ * job). The domain-specific signals (send / guard / recovery) render ONLY when the
121
+ * subject vocab actually defines them, so a non-email subject no longer sees dead lines.
55
122
  */
56
123
  export function buildOutcomePrompt(
57
124
  trace: EvalTrace,
58
125
  signals: OutcomeSignals,
59
126
  vocab: SubjectVocab,
127
+ subject?: SubjectFrame,
60
128
  ): { system: string; user: string } {
61
129
  const system = [
62
130
  "You are a success/failure determiner for an autonomous agent session.",
63
131
  "Decide whether the session REACHED THE GOAL implied by its input event.",
64
132
  "",
133
+ "Derive the EXPECTED outcome from the SUBJECT (its identity · purpose · tools) +",
134
+ "the input event — do NOT assume any particular domain (e.g. email). This is a",
135
+ "FAST input→output outcome check: read the goal (input), the result (output/self-",
136
+ "summary) and the compact summary below — NOT the full step-by-step trajectory",
137
+ "(that is the per-criterion judge's job).",
138
+ "",
65
139
  "CRITICAL RULE — inaction can be success. Holding (sending nothing, calling",
66
140
  "no tool) is the CORRECT outcome when the event is a restraint directive",
67
141
  "(e.g. a guard/hold directive) or when acting would be wrong. You MUST NOT",
@@ -74,23 +148,39 @@ export function buildOutcomePrompt(
74
148
  "Reason first in `critique`, then commit to `result`.",
75
149
  ].join("\n");
76
150
 
77
- // Generic guard-counter label the attribute NAME comes from the subject vocab.
78
- const guardLabel = vocab.guardCounterAttr ?? "guard counter";
151
+ const responseText = typeof trace.output?.response === "string" ? trace.output.response : "";
152
+ const keyTools = keyToolsUsed(trace);
153
+
154
+ // EV-1 — domain signals render ONLY when the subject vocab defines them (else they
155
+ // were email-agent noise for every subject). honest-null preserved.
156
+ const conditionalSignals: string[] = [];
157
+ if (vocab.guardCounterAttr !== null) {
158
+ conditionalSignals.push(` Guard ${vocab.guardCounterAttr}: ${signals.guardConsecutive ?? "n/a"}`);
159
+ }
160
+ if (vocab.sendTool.length > 0) {
161
+ conditionalSignals.push(
162
+ ` Sent a message: ${signals.sentMessage === null ? "unknown" : signals.sentMessage}`,
163
+ ` Send succeeded: ${signals.sendSucceeded ?? "n/a (no send)"}`,
164
+ );
165
+ }
166
+ if (vocab.recoveryTools.length > 0) {
167
+ conditionalSignals.push(` Recovery tool present: ${signals.recoveryPresent}`);
168
+ }
169
+
79
170
  const user = [
80
- `Event kind: ${signals.eventKind}`,
81
- `Guard ${guardLabel}: ${signals.guardConsecutive ?? "n/a"}`,
82
- `Tools called: ${signals.toolCount}`,
83
- // EV-049 honest-null: render UNKNOWN as "unknown" (the subject's send tool is
84
- // unset/uninferred) never coerce it to "false", which would lie to the judge.
85
- `Sent a message: ${signals.sentMessage === null ? "unknown" : signals.sentMessage}`,
86
- `Send succeeded: ${signals.sendSucceeded ?? "n/a (no send)"}`,
87
- `Recovery tool present: ${signals.recoveryPresent}`,
171
+ ...subjectFrameBlock(subject),
172
+ "OUTCOME SUMMARY (compact NOT the full trajectory):",
173
+ ` Event kind: ${signals.eventKind}`,
174
+ ` Tools called: ${signals.toolCount}${keyTools.length > 0 ? ` · key tools: ${keyTools.join(", ")}` : ""}`,
175
+ ` Did the agent act: ${signals.toolCount > 0 ? "yes (called tools)" : "no (no tool calls)"}`,
176
+ ` Terminal state: ${responseText.length > 0 ? "produced a self-summary/response" : "no self-summary"}`,
177
+ ...conditionalSignals,
88
178
  "",
89
179
  "Input event prompt:",
90
180
  promptOf(trace),
91
181
  "",
92
182
  "Agent self-summary (output.response):",
93
- typeof trace.output?.response === "string" ? trace.output.response : "(none)",
183
+ responseText.length > 0 ? responseText : "(none)",
94
184
  ].join("\n");
95
185
 
96
186
  return { system, user };
@@ -107,10 +197,13 @@ export async function determineOutcome(
107
197
  trace: EvalTrace,
108
198
  judge: JudgeInvoke,
109
199
  vocab: SubjectVocab,
200
+ subject?: SubjectFrame,
110
201
  ): Promise<OutcomeResult> {
111
202
  const signals = extractOutcomeSignals(trace, vocab);
112
- const { system, user } = buildOutcomePrompt(trace, signals, vocab);
113
- const raw = await judge(system, user);
203
+ const { system, user } = buildOutcomePrompt(trace, signals, vocab, subject);
204
+ // INF-3 pass the trace id so the determiner verdict is keyed PER-TRACE (two
205
+ // sessions with an identical determiner prompt never share one verdict).
206
+ const raw = await judge(system, user, trace.id);
114
207
  const verdict = parseCritiqueVerdict(raw);
115
208
  return {
116
209
  traceId: trace.id,
@@ -153,7 +246,8 @@ function profilePreamble(profile?: SubjectProfile): string[] {
153
246
  ` identity: ${profile.identity}`,
154
247
  ` purpose: ${profile.purpose}`,
155
248
  ` scope: ${profile.scope}`,
156
- ` tools: ${(profile.tools ?? []).join(", ") || "(none observed)"}`,
249
+ ` tools: ${renderToolLine(profile)}`,
250
+ ...reversibilityCallout(profile),
157
251
  profile.skill !== undefined ? ` skill: ${profile.skill}` : "",
158
252
  ` harness: ${profile.harness}`,
159
253
  ` provenance: ${profile.provenance}${profile.version !== undefined ? ` · version ${profile.version}` : ""}`,
@@ -161,6 +255,36 @@ function profilePreamble(profile?: SubjectProfile): string[] {
161
255
  ].filter((l) => l !== "");
162
256
  }
163
257
 
258
+ /** Render the tools line, annotating each tool with its EV-5 reversibility when known. */
259
+ function renderToolLine(profile: SubjectProfile): string {
260
+ const tools = profile.tools ?? [];
261
+ if (tools.length === 0) return "(none observed)";
262
+ const rev = new Map((profile.toolReversibility ?? []).map((t) => [t.name, t.reversibility]));
263
+ return tools
264
+ .map((name) => {
265
+ const r = rev.get(name);
266
+ return r !== undefined && r !== "unknown" ? `${name} [${r}]` : name;
267
+ })
268
+ .join(", ");
269
+ }
270
+
271
+ /**
272
+ * EV-5 — a one-line callout naming the tools that take IRREVERSIBLE EXTERNAL actions,
273
+ * so a failure that ALSO fired one weighs heavier than a read-only failure. Absent
274
+ * reversibility data ⇒ no callout (the judge still reads the bare tool list).
275
+ */
276
+ function reversibilityCallout(profile: SubjectProfile): string[] {
277
+ const irreversible = (profile.toolReversibility ?? [])
278
+ .filter((t) => t.reversibility === "irreversible-external")
279
+ .map((t) => t.name);
280
+ if (irreversible.length === 0) return [];
281
+ return [
282
+ ` ⚠ irreversible-external tools: ${irreversible.join(", ")} — a failure that ALSO`,
283
+ " fired one of these (an email sent, a live system mutated) is MATERIALLY WORSE",
284
+ " than a read-only failure; weigh that in your critique.",
285
+ ];
286
+ }
287
+
164
288
  /**
165
289
  * Render the 4-component per-criterion judge prompt for the in-house/export
166
290
  * substrate. The AUTHORITATIVE 4-component / binary / critique-before-verdict
@@ -227,6 +351,11 @@ export async function runJudge(
227
351
  subjectTrace: EvalTrace,
228
352
  judge: JudgeInvoke,
229
353
  pin: JudgePin,
354
+ // EV-5 CUT WIRE 1 — the M1 subject profile the judge reads in its preamble (tools +
355
+ // reversibility). Pre-fix runJudge called buildJudgePrompt WITHOUT it, so the export
356
+ // path always hit the "profile not supplied — reconstruct" branch and never saw the
357
+ // reversibility flags. The pipeline now builds it once (buildSubjectProfile) and passes it.
358
+ subjectProfile?: SubjectProfile,
230
359
  ): Promise<CriterionVerdict> {
231
360
  if (typeof pin.modelId !== "string" || pin.modelId.length === 0) {
232
361
  throw new Error(
@@ -240,7 +369,7 @@ export async function runJudge(
240
369
  "MODEL INTENT IS SACRED: reruns must be byte-identical (C-PIN); refusing.",
241
370
  );
242
371
  }
243
- const { system, user } = buildJudgePrompt(spec, subjectTrace);
372
+ const { system, user } = buildJudgePrompt(spec, subjectTrace, subjectProfile);
244
373
  const raw = await judge(system, user);
245
374
  const verdict = parseCritiqueVerdict(raw);
246
375
  return {
@@ -37,7 +37,7 @@ export const Lifecycle = {
37
37
  Build: "build",
38
38
  Evaluate: "evaluate",
39
39
  Diagnose: "diagnose",
40
- Improve: "improve",
40
+ Optimize: "optimize",
41
41
  General: "general",
42
42
  } as const;
43
43
  export type LifecycleValue = (typeof Lifecycle)[keyof typeof Lifecycle];