@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.
- package/.claude/skills/mutagent-evaluator/SKILL.md +6 -5
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +76 -12
- package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
- package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
- package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
- package/.claude/skills/mutagent-evaluator/references/eval-stage.md +36 -3
- package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
- package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +1 -0
- package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
- package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
- package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +6 -3
- package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
- package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +277 -0
- package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +39 -0
- package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
- package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
- package/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
- package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
- package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
- package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
- package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +9 -91
- package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +14 -4
- package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +114 -18
- package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
- package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
- package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
- package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
- package/bin/mutagent-cli.mjs +659 -8
- 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/
|
|
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/
|
|
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/
|
|
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-
|
|
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
|
|
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 ③
|
|
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
|
-
|
|
78
|
-
const
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|