@mutagent/evaluator 0.1.0-alpha.5 → 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 (34) hide show
  1. package/.claude/skills/mutagent-evaluator/SKILL.md +5 -5
  2. package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +16 -16
  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/schemas/methodology-review.schema.yaml +1 -1
  9. package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
  10. package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
  11. package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
  12. package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
  13. package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +60 -68
  14. package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
  15. package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +39 -0
  16. package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
  17. package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
  18. package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
  19. package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
  20. package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
  21. package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
  22. package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
  23. package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
  24. package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
  25. package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
  26. package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +9 -91
  27. package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
  28. package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +2 -2
  29. package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
  30. package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
  31. package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +2 -2
  32. package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
  33. package/bin/mutagent-cli.mjs +9 -5
  34. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ /**
2
+ * scripts/report-fragments.ts — EV-3 shared client-JS render fragments.
3
+ * ---------------------------------------------------------------------------
4
+ * The step<->criterion side-by-side render functions, extracted VERBATIM from
5
+ * `render-eval-report.ts` so BOTH the eval report AND the `*review` UI
6
+ * (`build-review-ui.ts`) emit the identical browser-side renderer.
7
+ *
8
+ * `SIDE_BY_SIDE_FRAGMENT_JS` is the BYTE-IDENTICAL block (`cvRefs` +
9
+ * `critiqueBlock` + `sideBySide` — the latter carrying its local
10
+ * `refExaminesStep` + `covEntry`). render-eval-report splices it into its
11
+ * client script IN PLACE, so the report's emitted HTML is unchanged (guarded by
12
+ * the render-eval-report byte-identity tests + a before/after HTML diff).
13
+ *
14
+ * The fragment is provider-of-record for the render logic; its runtime deps
15
+ * (`esc`, `resRouting`, `resBadge`, and the globals `C` / `RES`) are
16
+ * supplied by whichever host script embeds it (render-eval-report defines them
17
+ * above the splice; the review UI defines compatible copies — see
18
+ * `REVIEW_SIDE_BY_SIDE_DEPS_JS`).
19
+ *
20
+ * `STEP_ROW_FRAGMENT_JS` is the review-UI VIRTUALIZATION mirror: standalone
21
+ * `refExaminesStepR` / `covEntryR` / `stepRowR` that emit ONE step triplet
22
+ * with markup IDENTICAL to `sideBySide`'s per-step output — so the review UI
23
+ * can window a 1,686-step card without materializing the whole DOM. A drift
24
+ * guard (`tests/report-fragments.test.ts`) evaluates both paths and asserts the
25
+ * per-step HTML matches, so the mirror can never silently diverge from the
26
+ * frozen fragment.
27
+ */
28
+
29
+ /**
30
+ * BYTE-IDENTICAL extraction of the eval report's `cvRefs` + `critiqueBlock` +
31
+ * `sideBySide` client functions (`sideBySide` contains its local
32
+ * `refExaminesStep` + `covEntry`). Spliced verbatim by render-eval-report and
33
+ * reused by the review UI. DO NOT EDIT — regenerate from render-eval-report if
34
+ * that block ever changes, and re-verify byte-identity.
35
+ */
36
+ export const SIDE_BY_SIDE_FRAGMENT_JS = `function cvRefs(refs){if(!refs||!refs.length)return '';
37
+ return '<div class="cvrefs">'+refs.map(function(rf){var s=(typeof rf==='string')?rf:[rf.obs,rf.path,rf.value].filter(Boolean).join(' · ');return s?'<span class="cvref">'+esc(s)+'</span>':'';}).join('')+'</div>';}
38
+ function critiqueBlock(cvs){if(!cvs||!cvs.length)return '';
39
+ var order={fail:0,uncertain:1,indeterminate:1,pass:2,na:3};
40
+ var rows=cvs.slice().sort(function(a,b){return (order[a.result]==null?9:order[a.result])-(order[b.result]==null?9:order[b.result]);}).map(function(v){
41
+ var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
42
+ var conf=(v.confidence!=null)?'<span class="cvconf">conf '+esc(v.confidence)+(v.confidenceBand?' · '+esc(v.confidenceBand):'')+'</span>':'';
43
+ var crit=v.critique?'<div class="cvcrit">'+esc(v.critique)+'</div>':'<div class="cvcrit dim">— no critique recorded for this criterion</div>';
44
+ return '<div class="cvrow '+esc(res)+'"><div class="cvh"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="cvid">'+esc(v.criterionId||'')+'</span>'+conf+'</div>'+crit+cvRefs(v.refs)+'</div>';}).join('');
45
+ return '<div class="ctx cvblock"><div class="ctx-h">◇ how the judge reasoned — per-criterion verdict · critique-before-verdict · grounding refs ('+cvs.length+')</div><div class="cvbody">'+rows+'</div></div>';}
46
+ function sideBySide(d){var ctx=d.context||{};
47
+ // Gap A — the §2 "input + scenario" cell renders the RAW triggering INPUT (the
48
+ // thing that fired the agent) ABOVE the judge's scenario LABEL. Long inputs
49
+ // collapse into a <details> (lean, no JS) so the cell stays compact; short ones
50
+ // render inline (clamped). ABSENT input ⇒ the cell shows the scenario alone (or
51
+ // "—"). Font stays at the --fs-2xs (11px) floor + brand mono.
52
+ var inputCell=function(raw,scen){
53
+ var sc=scen?'<div class="iscn">scenario · '+esc(scen)+'</div>':'';
54
+ if(!raw)return (scen?'<div class="ival">'+esc(scen)+'</div>':'—');
55
+ var long=String(raw).length>180;
56
+ var body=long
57
+ ? '<details class="iexp"><summary>raw input · '+String(raw).length+' chars</summary><pre class="iraw">'+esc(raw)+'</pre></details>'
58
+ : '<pre class="iraw clamp">'+esc(raw)+'</pre>';
59
+ return body+sc;};
60
+ var refStr=function(rf){if(!rf)return '';if(typeof rf==='string')return rf;return [rf.obs,rf.path,rf.value].filter(Boolean).join(':');};
61
+ // UI-4 — surface the judge's REASONING (why the verdict + why the exit-states were
62
+ // concluded) from the EXISTING judge walk: an ordered why-chain band + the decide/bind
63
+ // text inlined where it explains a conclusion. No emit-contract change — read-only over
64
+ // the judge_steps already on the verdict file.
65
+ var KORD={gather:0,context:1,examine:2,detect:3,bind:4,ground:5,critique:6,decide:7,verify:8};
66
+ var allJs=(d.judgeSteps||[]).slice();
67
+ var stepText=function(kind){return allJs.filter(function(s){return s.kind===kind;}).map(function(s){return s.text||'';}).filter(Boolean).join(' · ');};
68
+ var decideWhy=stepText('decide')||stepText('critique');
69
+ var stateWhy=[stepText('bind'),stepText('gather')].filter(Boolean).join(' · ');
70
+ var chainSteps=allJs.slice().sort(function(a,b){var x=KORD[a.kind];var y=KORD[b.kind];return (x==null?9:x)-(y==null?9:y);});
71
+ var whyChain=chainSteps.length?('<div class="ctx whychain"><div class="ctx-h">◇ judge reasoning — full why-chain (gather → bind → ground → decide)</div><div class="wc-body">'+chainSteps.map(function(s){var rs=refStr(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+(rs?' <span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('')+'</div></div>'):'';
72
+ var verdictWhy=decideWhy?('<div class="band whyverdict"><div class="bh" style="color:var(--primary-soft)">◇ why this verdict</div><div class="wv-t">'+esc(decideWhy)+'</div></div>'):'';
73
+ // -- §2 judge lane = per-step EVAL COVERAGE --
74
+ // The judge lane (.step-r) used to filter judgeSteps by anchor === a.n -- but
75
+ // judges never emit an anchor, so EVERY step rendered a bare dash. Instead we
76
+ // map each agent step -> the per-criterion verdicts (d.criterionVerdicts) whose
77
+ // grounding refs EXAMINED that step: a precise ref.obs === step.obs match plus
78
+ // the tool-name fallback (ref.path === 'name' && ref.value === step.tool).
79
+ // Each examining criterion renders one compact entry -- result + CODE/JUDGE tag
80
+ // + criterionId + the judge reasoning (critique). A step no criterion references
81
+ // says 'not examined by any eval' (honest), never a bare dash. Any judge step
82
+ // that DOES carry a real anchor is still honored (future-proof).
83
+ var cvAll=(d.criterionVerdicts||[]);
84
+ var refExaminesStep=function(rf,a){
85
+ if(!rf||typeof rf==='string')return false;
86
+ if(a.obs&&rf.obs&&String(rf.obs)===String(a.obs))return true;
87
+ if(rf.path==='name'&&rf.value!=null&&a.tool&&String(rf.value)===String(a.tool))return true;
88
+ return false;};
89
+ var covEntry=function(v,a){
90
+ var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
91
+ // the router carries TWO vocabularies: matrix-derived ('deterministic') and
92
+ // mined ('code-based'); 'hybrid' is shared. A '[code-eval ...]'-prefixed critique
93
+ // is the deterministic-logic fallback when the method is unset.
94
+ var method=((typeof C!=='undefined'&&C[v.criterionId])||{}).m||'';
95
+ var isCode=method==='deterministic'||method==='code-based'||method==='hybrid'||String(v.critique||'').trim().indexOf('[code-eval')===0;
96
+ var tag=method==='hybrid'?'HYBRID':(isCode?'CODE':'JUDGE');
97
+ var matched=(v.refs||[]).filter(function(rf){return typeof rf!=='string'&&refExaminesStep(rf,a);});
98
+ var crit=v.critique?esc(v.critique):'<span class="dim">— no critique recorded</span>';
99
+ return '<div class="jcov '+esc(res)+'"><div class="jcov-h"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="jm '+(isCode?'code':'judge')+'" title="'+(isCode?'deterministically checked by a code-eval (not LLM-judged)':'reasoned by the LLM judge')+'">'+esc(tag)+'</span><span class="jcid">'+esc(v.criterionId||'')+'</span></div><div class="jcrit">'+crit+'</div>'+cvRefs(matched)+'</div>';};
100
+ var rowsHtml='';(d.agentSteps||[]).forEach(function(a){
101
+ // future-proof: a judge step that carries a REAL anchor still renders.
102
+ var js=(d.judgeSteps||[]).filter(function(s){return s.anchor!=null&&String(s.anchor)===String(a.n)&&s.kind!=='context';});
103
+ var anchoredHtml=js.map(function(s){var rs=refStr(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+' '+(rs?'<span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('');
104
+ var examiners=cvAll.filter(function(v){return (v.refs||[]).some(function(rf){return refExaminesStep(rf,a);});});
105
+ var covHtml=examiners.map(function(v){return covEntry(v,a);}).join('');
106
+ var jhtml=anchoredHtml+covHtml;
107
+ if(!jhtml)jhtml='<div class="jstep noexam"><span class="t">— not examined by any eval</span></div>';
108
+ rowsHtml+='<div class="step-l"><div class="evb"><div class="top"><span class="tool">'+esc(a.tool||'')+'</span><span class="st '+esc(a.status||'')+'">'+esc(a.status||'')+'</span></div><div class="det">'+esc(a.detail||'')+'</div></div></div><div class="node '+esc(a.status||'')+'"><div class="ln"></div><div class="n">'+esc(a.n)+'</div><div class="ln"></div></div><div class="step-r"><div class="evb r">'+jhtml+'</div></div>';});
109
+ var h=d.health||{};
110
+ var sp=d.subjectProfile;
111
+ var spHtml=sp?'<div class="ctx"><div class="ctx-h">◇ judge · subject profile (M1) · '+esc(sp.provenance||'')+'</div><div class="ctx-g"><div class="ctx-c"><div class="l">identity</div><div class="v">'+esc(sp.identity||'—')+'</div></div><div class="ctx-c"><div class="l">purpose</div><div class="v">'+esc(sp.purpose||'—')+'</div></div><div class="ctx-c"><div class="l">scope · harness</div><div class="v">'+esc((sp.scope||'—'))+' · harness: '+esc(sp.harness||'—')+'</div></div></div></div>':'';
112
+ var u=d.understanding;
113
+ var uHtml=u?'<div class="band"><div class="bh" style="color:var(--cyan)">◇ node-0 GATHER — understanding (M2)</div><div class="re-rephrase" style="margin-top:4px">“'+esc(u.rephrase||'')+'”</div></div>':'';
114
+ var et=d.expectedTrajectory||[];
115
+ var etHtml=et.length?'<div class="band"><div class="bh" style="color:var(--primary-soft)">◇ node-0.5 EXPECTED-TRAJECTORY (M3) — how it SHOULD have acted</div><ol class="re-ul" style="margin-top:4px">'+et.map(function(s,i){return '<li><b>'+esc(s.step||i+1)+'.</b> '+esc(s.expected||'')+(s.rationale?' <span class=re-rat>— '+esc(s.rationale)+'</span>':'')+'</li>';}).join('')+'</ol></div>':'';
116
+ return '<div class="drillbox"><div style="display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-bottom:6px"><b class="mono">'+esc(d.traceId)+'</b><span class="chip">'+esc(d.route||'all')+'</span><span class="verd '+(d.verdict=='FAIL'?'fail':d.verdict=='PASS'?'pass':'inc')+'">'+esc(d.verdict)+'</span></div>'+
117
+ resRouting(d.res||'judge-walk')+
118
+ spHtml+
119
+ verdictWhy+
120
+ critiqueBlock(d.criterionVerdicts)+
121
+ '<div class="ctx"><div class="ctx-h">◇ judge · gather context</div><div class="ctx-g"><div class="ctx-c"><div class="l">harness</div><div class="v">'+esc(ctx.harness||'—')+'</div></div><div class="ctx-c"><div class="l">input + scenario</div><div class="v">'+inputCell(d.input,ctx.scenario)+'</div></div><div class="ctx-c"><div class="l">exit states</div><div class="v">'+esc(ctx.exitStates||'—')+(stateWhy?'<div class="why-note"><span class="ref">why concluded:</span> '+esc(stateWhy)+'</div>':'')+'</div></div></div></div>'+
122
+ whyChain+
123
+ uHtml+etHtml+
124
+ '<div class="lanehdr"><div class="a">target agent — what it did</div><div class="x">step</div><div class="j">judge — eval coverage (which criteria examined this step)</div></div>'+
125
+ '<div class="grid2">'+rowsHtml+'<div class="band loc"><div class="bh">↯ localize (root, not symptom)</div><div style="font-size:var(--fs-sm);margin-top:4px">'+esc(d.localize||'—')+'</div></div></div>'+
126
+ '<div class="health"><div class="hc"><div class="l">context</div><div class="v good">'+(h.contextGathered?'✓':'—')+'</div></div><div class="hc"><div class="l">grounded</div><div class="v good">'+esc(h.grounded||0)+'</div></div><div class="hc"><div class="l">assumed</div><div class="v '+((h.assumed||0)>0?'warn':'good')+'">'+esc(h.assumed||0)+'</div></div><div class="hc"><div class="l">root vs symptom</div><div class="v good">'+(h.stoppedAtSymptom?'symptom':'✓ root')+'</div></div></div></div>';}`;
127
+
128
+ /**
129
+ * The runtime deps the extracted `sideBySide` needs — `esc` / `resBadge` /
130
+ * `resRouting` — copied BYTE-IDENTICALLY from render-eval-report's client script
131
+ * so the reused `sideBySide` behaves identically wherever it is embedded. The
132
+ * review UI embeds these (render-eval-report already defines them above its own
133
+ * splice). `RES` may be an empty map in the review UI — `resRouting`/`resBadge`
134
+ * degrade to '' when a resolution key is absent.
135
+ */
136
+ export const REVIEW_SIDE_BY_SIDE_DEPS_JS = `function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
137
+ function resBadge(res){var m=RES[res];if(!m)return '';return '<span class="resbadge '+m.cls+'" title="'+esc(m.routing)+'">'+esc(m.badge)+'</span>';}
138
+ function resRouting(res){var m=RES[res];if(!m)return '';return '<div class="routing '+m.cls+'"><span class="routing-k">resolution</span><span class="routing-b '+m.cls+'">'+esc(m.badge)+'</span><span class="routing-v">'+esc(m.routing)+'</span></div>';}`;
139
+
140
+ /**
141
+ * The review-UI VIRTUALIZATION mirror. `stepRowR` emits ONE step triplet
142
+ * (`.step-l` + `.node` + `.step-r`) whose markup is CHARACTER-IDENTICAL to the
143
+ * per-step output of the frozen `sideBySide` (compare render-eval-report's
144
+ * `rowsHtml` loop). This lets the review UI window a 1,686-step card — rendering
145
+ * only the visible slice — instead of materializing the whole DOM (the EV-3
146
+ * wall). `refExaminesStepR` / `covEntryR` mirror `sideBySide`'s local
147
+ * `refExaminesStep` / `covEntry`. Depends on `esc` + `cvRefs` (from the deps +
148
+ * the frozen fragment). The drift guard in `tests/report-fragments.test.ts`
149
+ * evaluates both paths and asserts equality, so this mirror can never diverge.
150
+ */
151
+ export const STEP_ROW_FRAGMENT_JS = `function refStrR(rf){if(!rf)return '';if(typeof rf==='string')return rf;return [rf.obs,rf.path,rf.value].filter(Boolean).join(':');}
152
+ function refExaminesStepR(rf,a){
153
+ if(!rf||typeof rf==='string')return false;
154
+ if(a.obs&&rf.obs&&String(rf.obs)===String(a.obs))return true;
155
+ if(rf.path==='name'&&rf.value!=null&&a.tool&&String(rf.value)===String(a.tool))return true;
156
+ return false;}
157
+ function covEntryR(v,a){
158
+ var res=v.result||'na';var disp=res==='uncertain'?'indeterminate':res;
159
+ var method=((typeof C!=='undefined'&&C&&C[v.criterionId])||{}).m||'';
160
+ var isCode=method==='deterministic'||method==='code-based'||method==='hybrid'||String(v.critique||'').trim().indexOf('[code-eval')===0;
161
+ var tag=method==='hybrid'?'HYBRID':(isCode?'CODE':'JUDGE');
162
+ var matched=(v.refs||[]).filter(function(rf){return typeof rf!=='string'&&refExaminesStepR(rf,a);});
163
+ var crit=v.critique?esc(v.critique):'<span class="dim">— no critique recorded</span>';
164
+ return '<div class="jcov '+esc(res)+'"><div class="jcov-h"><span class="cvb '+esc(res)+'">'+esc(disp)+'</span><span class="jm '+(isCode?'code':'judge')+'" title="'+(isCode?'deterministically checked by a code-eval (not LLM-judged)':'reasoned by the LLM judge')+'">'+esc(tag)+'</span><span class="jcid">'+esc(v.criterionId||'')+'</span></div><div class="jcrit">'+crit+'</div>'+cvRefs(matched)+'</div>';}
165
+ function stepRowInnerR(a,cvAll,judgeSteps){
166
+ var js=(judgeSteps||[]).filter(function(s){return s.anchor!=null&&String(s.anchor)===String(a.n)&&s.kind!=='context';});
167
+ var anchoredHtml=js.map(function(s){var rs=refStrR(s.ref);return '<div class="jstep"><span class="k '+esc(s.kind)+'">'+esc(s.kind)+'</span><span class="t">'+esc(s.text||'')+' '+(rs?'<span class=ref>'+esc(rs)+'</span>':'')+'</span></div>';}).join('');
168
+ var examiners=(cvAll||[]).filter(function(v){return (v.refs||[]).some(function(rf){return refExaminesStepR(rf,a);});});
169
+ var covHtml=examiners.map(function(v){return covEntryR(v,a);}).join('');
170
+ var jhtml=anchoredHtml+covHtml;
171
+ if(!jhtml)jhtml='<div class="jstep noexam"><span class="t">— not examined by any eval</span></div>';
172
+ return '<div class="step-l"><div class="evb"><div class="top"><span class="tool">'+esc(a.tool||'')+'</span><span class="st '+esc(a.status||'')+'">'+esc(a.status||'')+'</span></div><div class="det">'+esc(a.detail||'')+'</div></div></div><div class="node '+esc(a.status||'')+'"><div class="ln"></div><div class="n">'+esc(a.n)+'</div><div class="ln"></div></div><div class="step-r"><div class="evb r">'+jhtml+'</div></div>';}
173
+ function stepExaminedByR(a,cvAll){return (cvAll||[]).some(function(v){return (v.refs||[]).some(function(rf){return refExaminesStepR(rf,a);});});}`;
@@ -35,7 +35,7 @@ export const AdlStage = {
35
35
  Build: "build",
36
36
  Evaluate: "evaluate",
37
37
  Diagnose: "diagnose",
38
- Improve: "improve",
38
+ Optimize: "optimize",
39
39
  Audit: "audit",
40
40
  } as const;
41
41
  export type AdlStageValue = (typeof AdlStage)[keyof typeof AdlStage];
@@ -142,7 +142,7 @@ const HandoverBundleSchema = Type.Object(
142
142
  Type.Literal(AdlStage.Build),
143
143
  Type.Literal(AdlStage.Evaluate),
144
144
  Type.Literal(AdlStage.Diagnose),
145
- Type.Literal(AdlStage.Improve),
145
+ Type.Literal(AdlStage.Optimize),
146
146
  Type.Literal(AdlStage.Audit),
147
147
  ]),
148
148
  subject: SubjectSchema,
@@ -22,6 +22,12 @@
22
22
  import type { JudgeInvoke } from "./determine-outcome.ts";
23
23
  import { determineOutcome, runJudge } from "./judge-prompt-template.ts";
24
24
  import { profileSubject, type SubjectProfile } from "./profile-subject.ts";
25
+ import { buildSubjectProfile, deriveSubjectFrame } from "./subject-profile.ts";
26
+ import {
27
+ normativeCriterionIds,
28
+ pendingRatifications,
29
+ type CalibrationDecision,
30
+ } from "./memory/ratify.ts";
25
31
  import { splitTrainEval, buildJudgeSpec, type JudgePin } from "./build-evals.ts";
26
32
  import { rollupScorecard, type GradedCriterion, type Scorecard } from "./evaluate.ts";
27
33
  import {
@@ -64,6 +70,12 @@ export interface PipelineOptions {
64
70
  severityById?: Record<string, string>;
65
71
  /** artifact refs to enumerate on the handoff (caller-injected locators). */
66
72
  artifacts?: ArtifactRef[];
73
+ /**
74
+ * EV-4 — prior operator ratify/eliminate decisions (the review UI's `calibration.json`,
75
+ * routed via `memory/ratify.ts`). A NORMATIVE criterion with no `verify` here is
76
+ * `needs-ratification` → the verdict is HELD (surfaced in `pendingRatification`).
77
+ */
78
+ ratifications?: CalibrationDecision[];
67
79
  }
68
80
 
69
81
  export interface PipelineResult {
@@ -73,6 +85,12 @@ export interface PipelineResult {
73
85
  verdicts: CriterionVerdict[];
74
86
  scorecard: Scorecard;
75
87
  handoff: HandoverBundle;
88
+ /**
89
+ * EV-4 — normative criteria still `needs-ratification` (no operator `verify`).
90
+ * NON-EMPTY ⇒ the verdict is HELD: a value call the judge cannot settle is
91
+ * unresolved, so the run's verdict is not yet final.
92
+ */
93
+ pendingRatification: string[];
76
94
  }
77
95
 
78
96
  /** Map a determiner outcome → a sampling LabeledTrace. */
@@ -101,11 +119,20 @@ export async function runEvalPipeline(
101
119
  // EV-049 — subject profile (carries the subject vocab the engine reads).
102
120
  const profile = profileSubject(traces, opts.vocab);
103
121
 
122
+ // EV-5 CUT WIRE 2 — build the M1 judge-packet profile ONCE (identity · purpose ·
123
+ // tools + reversibility). This is `buildSubjectProfile`'s first PRODUCTION caller;
124
+ // it flows to the per-criterion judge preamble (EV-5). The determiner (EV-1) reads
125
+ // the SAME frame via `deriveSubjectFrame` so PREP (Stage A) and this pipeline
126
+ // (Stage B) render byte-identical determiner prompts (cross-stage cache parity).
127
+ const m1Profile = buildSubjectProfile({ subjectName: opts.subject.name, traces });
128
+ const subjectFrame = deriveSubjectFrame(opts.subject.name, traces);
129
+
104
130
  // EV-042 — determine an outcome per trace (sequential → deterministic order).
105
- // The determiner reads the subject vocab off the profile (EV-002).
131
+ // The determiner reads the subject vocab off the profile (EV-002) and, EV-1, the
132
+ // subject frame so it derives THIS subject's expected outcome (not email signals).
106
133
  const outcomes: OutcomeResult[] = [];
107
134
  for (const t of traces) {
108
- outcomes.push(await determineOutcome(t, judge, profile.vocab));
135
+ outcomes.push(await determineOutcome(t, judge, profile.vocab, subjectFrame));
109
136
  }
110
137
  const labeled = traces.map((t, i) => toLabeled(t, outcomes[i]));
111
138
 
@@ -125,7 +152,7 @@ export async function runEvalPipeline(
125
152
  const spec = buildJudgeSpec(criterion, exemplarsFromTrain(train), trainIds);
126
153
  // judge a representative held-out subject trace (first eval target).
127
154
  const subjectTrace = judgeTargets[0]?.trace ?? sample[0]?.trace ?? traces[0];
128
- const verdict = await runJudge(spec, subjectTrace, judge, opts.pin);
155
+ const verdict = await runJudge(spec, subjectTrace, judge, opts.pin, m1Profile);
129
156
  verdicts.push(verdict);
130
157
  graded.push({
131
158
  criterionId: criterion.id,
@@ -171,5 +198,13 @@ export async function runEvalPipeline(
171
198
  producedAt: opts.producedAt,
172
199
  });
173
200
 
174
- return { profile, outcomes, sample, verdicts, scorecard, handoff };
201
+ // EV-4 a normative criterion (a value/standard call the judge cannot settle)
202
+ // must be operator-RATIFIED before its verdict is final. Compute which normative
203
+ // criteria have no `verify` decision → the verdict is HELD on them.
204
+ const pendingRatification = pendingRatifications(
205
+ normativeCriterionIds(verdicts),
206
+ opts.ratifications ?? [],
207
+ );
208
+
209
+ return { profile, outcomes, sample, verdicts, scorecard, handoff, pendingRatification };
175
210
  }
@@ -22,11 +22,62 @@
22
22
  import {
23
23
  PROFILE_UNKNOWN,
24
24
  SubjectProfileProvenance,
25
+ ToolReversibility,
25
26
  type SubjectProfile,
27
+ type ToolReversibilityEntry,
28
+ type ToolReversibilityValue,
26
29
  } from "./contracts/eval-matrix.ts";
27
30
  import { inferToolInventory, inferSystemPrompt } from "./profile-subject.ts";
28
31
  import type { EvalTrace } from "./contracts/eval-types.ts";
29
32
 
33
+ // ── EV-5 — tool reversibility classification ─────────────────────────────────
34
+ //
35
+ // LEAN start (operator: "classify its tools by what they do — irreversible external
36
+ // or not; keep the start lean"). A GIVEN classification (from the subject definition)
37
+ // ALWAYS wins; absent, a conservative NAME heuristic flags well-known irreversible-
38
+ // external verbs and well-known read-only verbs, and marks everything else `unknown`
39
+ // (HONEST — never a false "reversible"). The heavier per-trace side-effect harness
40
+ // (decision option b) can supersede this later without changing the field shape.
41
+
42
+ /** Verb stems whose presence in a tool name signals an IRREVERSIBLE EXTERNAL action. */
43
+ const IRREVERSIBLE_VERBS = [
44
+ "send", "email", "post", "publish", "delete", "remove", "destroy", "drop",
45
+ "charge", "pay", "refund", "transfer", "deploy", "release", "submit", "create",
46
+ "insert", "write", "update", "patch", "put", "execute", "provision", "terminate",
47
+ "cancel", "notify", "dispatch", "message", "reply", "comment", "merge", "push",
48
+ ];
49
+ /** Verb stems whose presence signals a READ-ONLY / safely-repeatable action. */
50
+ const REVERSIBLE_VERBS = [
51
+ "get", "list", "read", "search", "fetch", "lookup", "view", "query", "find",
52
+ "check", "inspect", "describe", "count", "peek", "preview", "validate",
53
+ ];
54
+
55
+ /**
56
+ * Classify one tool's reversibility (EV-5). A GIVEN `override` (subject definition)
57
+ * wins; else a conservative name heuristic — irreversible-external verbs first (the
58
+ * safety-relevant class), then read-only verbs, else `unknown` (never guessed). PURE.
59
+ */
60
+ export function classifyToolReversibility(
61
+ name: string,
62
+ override?: Record<string, ToolReversibilityValue>,
63
+ ): ToolReversibilityValue {
64
+ const given = override?.[name];
65
+ if (given !== undefined) return given;
66
+ const n = name.toLowerCase();
67
+ const hits = (verbs: string[]): boolean => verbs.some((v) => n.includes(v));
68
+ if (hits(IRREVERSIBLE_VERBS)) return ToolReversibility.IrreversibleExternal;
69
+ if (hits(REVERSIBLE_VERBS)) return ToolReversibility.Reversible;
70
+ return ToolReversibility.Unknown;
71
+ }
72
+
73
+ /** Classify every tool in the inventory (EV-5). Deterministic; preserves order. */
74
+ export function classifyToolList(
75
+ tools: string[],
76
+ override?: Record<string, ToolReversibilityValue>,
77
+ ): ToolReversibilityEntry[] {
78
+ return tools.map((name) => ({ name, reversibility: classifyToolReversibility(name, override) }));
79
+ }
80
+
30
81
  /** The GIVEN facts a caller with code/metadata access can supply (M1). Any field
31
82
  * left absent is RECONSTRUCTED from the trace batch or MARKED `unknown`. */
32
83
  export interface GivenSubjectFacts {
@@ -41,6 +92,9 @@ export interface GivenSubjectFacts {
41
92
  version?: string;
42
93
  /** the agent's system prompt (code access). Rendered COLLAPSED; never confabulated. */
43
94
  systemPrompt?: string;
95
+ /** EV-5 — a GIVEN per-tool reversibility classification (from the subject definition);
96
+ * WINS over the name heuristic. Any tool not named here falls back to the heuristic. */
97
+ toolReversibility?: Record<string, ToolReversibilityValue>;
44
98
  }
45
99
 
46
100
  export interface BuildSubjectProfileParams {
@@ -117,10 +171,14 @@ export function buildSubjectProfile(params: BuildSubjectProfileParams): SubjectP
117
171
  if (systemPrompt === undefined) systemPrompt = inferSystemPrompt(traces);
118
172
  if (given?.systemPrompt === undefined) inferredFields.push("systemPrompt");
119
173
 
174
+ // EV-5 — classify each tool's reversibility (GIVEN override wins; else name heuristic).
175
+ const toolReversibility = classifyToolList(tools, given?.toolReversibility);
176
+
120
177
  const profile: SubjectProfile = {
121
178
  identity,
122
179
  purpose,
123
180
  tools,
181
+ toolReversibility,
124
182
  scope,
125
183
  harness,
126
184
  provenance: hasGiven ? SubjectProfileProvenance.Given : SubjectProfileProvenance.Reconstructed,
@@ -132,3 +190,27 @@ export function buildSubjectProfile(params: BuildSubjectProfileParams): SubjectP
132
190
  };
133
191
  return profile;
134
192
  }
193
+
194
+ /** The compact determiner subject frame (EV-1) — a structural subset of the M1 profile. */
195
+ export interface SubjectFrameShape {
196
+ identity: string;
197
+ purpose: string;
198
+ tools: string[];
199
+ toolReversibility?: ToolReversibilityEntry[];
200
+ }
201
+
202
+ /**
203
+ * Derive the determiner/judge subject frame (EV-1 / EV-5) from a subject name + trace
204
+ * batch. THE single derivation used by run-pipeline (Stage-B) AND prep (Stage-A) so
205
+ * the determiner prompt is byte-identical across the two stages (the cross-stage cache
206
+ * would miss on any divergence). PURE — same inputs ⇒ same frame.
207
+ */
208
+ export function deriveSubjectFrame(subjectName: string, traces: EvalTrace[]): SubjectFrameShape {
209
+ const m1 = buildSubjectProfile({ subjectName, traces });
210
+ return {
211
+ identity: m1.identity,
212
+ purpose: m1.purpose,
213
+ tools: m1.tools,
214
+ ...(m1.toolReversibility !== undefined ? { toolReversibility: m1.toolReversibility } : {}),
215
+ };
216
+ }
@@ -2,7 +2,7 @@
2
2
  * scripts/sync-eval-criteria.ts — the EVAL-LEG reconcile CONTRACT (Wave-2 W2I5 · KP-003).
3
3
  * ---------------------------------------------------------------------------
4
4
  * The THIRD leg of the def → impl → eval triad. Today `#sync-spec` reconciles two
5
- * legs — spec ↔ impl. When an implementation amends (the ⑤ IMPROVE loop's ai-engineer,
5
+ * legs — spec ↔ impl. When an implementation amends (the ⑤ OPTIMIZE loop's ai-engineer,
6
6
  * or a brownfield drift), the eval criteria that GROUND the subject's evaluation can go
7
7
  * stale w.r.t. the changed impl. This module is the EVALUATOR-SIDE CONTRACT that the
8
8
  * reconcile (ai-architect REASONING, session-dispatched — Model-B) READS and WRITES:
@@ -35,7 +35,7 @@ import { assertMonotonicGrowth } from "./living-suite.ts";
35
35
  // ── subject kind → eval leg ──────────────────────────────────────────────────
36
36
 
37
37
  /** The subject kinds the triad reconciles. `agent|skill|composite` come from the
38
- * agentspec `definition.identity.kind`; `code` is the ⑤ IMPROVE code-target subject. */
38
+ * agentspec `definition.identity.kind`; `code` is the ⑤ OPTIMIZE code-target subject. */
39
39
  export const EvalSubjectKind = {
40
40
  Agent: "agent",
41
41
  Skill: "skill",
@@ -164,6 +164,34 @@ function effectiveRole(span: UnitfSpan): UnitfRole {
164
164
  }
165
165
  }
166
166
 
167
+ // ── Message-text pick (INF-1) ────────────────────────────────────────────────
168
+ //
169
+ // ROOT CAUSE of the Claude Code loss: the Claude Code exporter writes BOTH user
170
+ // and assistant TEXT into `span.output` (a user turn is `kind:"event", role:"user",
171
+ // output:<text>`; an assistant turn is `kind:"llm", role:"assistant", output:<text>`
172
+ // with `input` UNSET). The old projection read a user/system span's `.input` — empty
173
+ // for EVERY Claude Code trace ⇒ `input.prompt` silently empty for all 21/21.
174
+ //
175
+ // This VENDORS the shared reader's role-conditional pick + fallback
176
+ // (@mutagent/tools `messagesView`, derive.ts:173-174): a user/system turn's text is
177
+ // canonically in `input`, everything else in `output`, but the `?? input ?? output`
178
+ // fallback recovers the text WHICHEVER field a producer used. Vendored — never
179
+ // imported: the standalone-publish guard forbids the evaluator reaching into
180
+ // @mutagent/tools (MIGRATION-diagnostics-evaluator.md:417); diagnostics vendors its
181
+ // own copy and this mirrors that.
182
+
183
+ /**
184
+ * The span's effective message TEXT: the role-conditional field pick with the
185
+ * `?? input ?? output` fallback (mirrors @mutagent/tools `messagesView`). Recovers
186
+ * a Claude Code turn (text in `output`) AND a Langfuse turn (text in `input`).
187
+ * PURE; "" when the span carries no text.
188
+ */
189
+ function messageText(span: UnitfSpan): string {
190
+ const role = effectiveRole(span);
191
+ const primary = role === "user" || role === "system" ? span.input : span.output;
192
+ return asText(primary ?? span.input ?? span.output);
193
+ }
194
+
167
195
  // ── Token total fallback (XF-FIX Finding D) ──────────────────────────────────
168
196
  //
169
197
  // MIRRORS diagnostics `unitf-adapter.ts` `fallbackTotalTokens` so both lanes
@@ -185,10 +213,11 @@ function fallbackTotalTokens(t: UnitfTokens | undefined): number | undefined {
185
213
  *
186
214
  * id ← ut.traceId
187
215
  * name ← ut.agentName
188
- * output ← first EFFECTIVE-assistant llm span → { response: <text of span.output> }
189
- * (XF-FIX A: role-less GENERATION spans read as assistant via kind)
190
- * input ← first user|system span's input, ELSE the response span's own
191
- * input { prompt } (XF-FIX A: recovers the Langfuse prompt)
216
+ * output ← LAST EFFECTIVE-assistant llm span WITH text → { response: <messageText> }
217
+ * (XF-FIX A: role-less GENERATION spans read as assistant via kind;
218
+ * INF-1: last-with-text, and messageText recovers Claude Code's `output`)
219
+ * input first user|system span's messageText, ELSE the response span's own
220
+ * input → { prompt } (XF-FIX A: Langfuse prompt; INF-1: Claude Code `output`)
192
221
  * observations ← ut.spans → { type: kind.toUpperCase(), name, input, output }
193
222
  * errored/status ← computed.hasError ?? (status==="error" || any span error) / ut.status (XF-FIX C)
194
223
  * tokens/totalTokens← ut.tokens split / computed.totalTokens ?? tokens.total ?? in+out (XF-FIX D)
@@ -216,31 +245,45 @@ export function projectUnitfToEvalTrace(ut: UnifiedTraceLike): EvalTrace {
216
245
 
217
246
  if (ut.agentName !== undefined) out.name = ut.agentName;
218
247
 
219
- // output ← first assistant LLM span's output, wrapped to { response }. The
220
- // assistant role is now EFFECTIVE (XF-FIX Finding A): a role-less GENERATION
221
- // span (kind==="llm", the Langfuse shape) reads as assistant via `effectiveRole`,
222
- // so its output is picked up instead of being silently dropped.
223
- const responseSpan = ut.spans.find(
248
+ // output ← the LAST assistant LLM span WITH text, wrapped to { response }. The
249
+ // assistant role is EFFECTIVE (XF-FIX Finding A): a role-less GENERATION span
250
+ // (kind==="llm", the Langfuse shape) reads as assistant via `effectiveRole`. The
251
+ // second INF-1 miss: the old code took the FIRST assistant span — on Claude Code
252
+ // that is usually a tool_use turn with EMPTY output text, so the real reply (a
253
+ // LATER assistant span) was dropped. Take the LAST assistant span that carries
254
+ // text; the `messageText` pick recovers it from `output` (Claude Code) or `input`
255
+ // (whichever field the producer used).
256
+ const assistantSpans = ut.spans.filter(
224
257
  (s) => effectiveRole(s) === "assistant" && s.kind === "llm",
225
258
  );
226
- if (responseSpan !== undefined && responseSpan.output !== undefined) {
227
- out.output = { response: asText(responseSpan.output) };
259
+ let responseSpan: UnitfSpan | undefined;
260
+ for (const s of assistantSpans) {
261
+ if (messageText(s).length > 0) responseSpan = s; // keep the LAST with text
262
+ }
263
+ // Fall back to the last assistant span even without text, so the response SLOT is
264
+ // still derived from a real span (the empty case is WARNED at intake, not here).
265
+ const lastAssistant =
266
+ responseSpan ?? (assistantSpans.length > 0 ? assistantSpans[assistantSpans.length - 1] : undefined);
267
+ if (responseSpan !== undefined) {
268
+ out.output = { response: messageText(responseSpan) };
228
269
  }
229
270
 
230
- // input ← first user|system span's input, wrapped to { prompt }. A role-less
231
- // span never infers to user/system, so when there is no explicit user/system
232
- // turn (again the Langfuse shape one GENERATION span carrying prompt in its
233
- // OWN `input` and response in `output`) the prompt is recovered from the
234
- // response span's `input`. Without this fallback `input.prompt` was empty for
235
- // EVERY Langfuse trace (Finding A).
271
+ // input ← the first user|system span's TEXT, wrapped to { prompt }. `messageText`
272
+ // recovers the text from `output` (Claude Code writes the user turn there) or
273
+ // `input` (Langfuse). When there is no explicit user/system turn (the Langfuse
274
+ // shape one GENERATION span carrying the prompt in its OWN `input` and the
275
+ // response in `output`) the prompt is recovered from the response span's `input`
276
+ // SPECIFICALLY (not `messageText`, which for an assistant span would return the
277
+ // response). Without these fallbacks `input.prompt` was empty for every Claude
278
+ // Code trace (INF-1) and every Langfuse trace (Finding A).
236
279
  const promptSpan = ut.spans.find(
237
280
  (s) => effectiveRole(s) === "user" || effectiveRole(s) === "system",
238
281
  );
239
- if (promptSpan !== undefined && promptSpan.input !== undefined) {
240
- out.input = { prompt: asText(promptSpan.input) };
241
- } else if (responseSpan !== undefined && responseSpan.input !== undefined) {
242
- out.input = { prompt: asText(responseSpan.input) };
282
+ let promptText = promptSpan !== undefined ? messageText(promptSpan) : "";
283
+ if (promptText.length === 0 && lastAssistant !== undefined && lastAssistant.input !== undefined) {
284
+ promptText = asText(lastAssistant.input);
243
285
  }
286
+ if (promptText.length > 0) out.input = { prompt: promptText };
244
287
 
245
288
  // Finding C — carry structured error/status so an error-KIND criterion has a
246
289
  // field to read (previously error state survived only as textual span output).
@@ -5045,8 +5045,12 @@ var SpanSchema = Type.Object({
5045
5045
  role: Type.Optional(RoleSchema),
5046
5046
  toolName: Type.Optional(Type.String()),
5047
5047
  toolCallId: Type.Optional(Type.String()),
5048
- input: Type.Optional(Type.Unknown()),
5049
- output: Type.Optional(Type.Unknown()),
5048
+ input: Type.Optional(Type.Unknown({
5049
+ description: "Message text for the INPUT-side layers (role user\xB7system\xB7tool = user-input / system-input / tool-call arguments), or a chat-messages array on a multi-layer LLM span. See the field-per-layer rule; enforced by checkFieldConformance()."
5050
+ })),
5051
+ output: Type.Optional(Type.Unknown({
5052
+ description: "Message text for the OUTPUT-side layers (role assistant\xB7toolResult = assistant-output / tool-result). See the field-per-layer rule; enforced by checkFieldConformance()."
5053
+ })),
5050
5054
  tokens: Type.Optional(TokensSchema),
5051
5055
  costUsd: Type.Optional(Type.Number()),
5052
5056
  attributes: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
@@ -6527,7 +6531,7 @@ function normalizeRecords(records, malformedIn) {
6527
6531
  if (ev.timestamp)
6528
6532
  span.startTime = ev.timestamp;
6529
6533
  if (text)
6530
- span.output = text;
6534
+ span.input = text;
6531
6535
  spans.push(span);
6532
6536
  spanIdx++;
6533
6537
  }
@@ -6542,7 +6546,7 @@ function normalizeRecords(records, malformedIn) {
6542
6546
  if (ev.timestamp)
6543
6547
  span.startTime = ev.timestamp;
6544
6548
  if (typeof content === "string" && content)
6545
- span.output = content;
6549
+ span.input = content;
6546
6550
  spans.push(span);
6547
6551
  spanIdx++;
6548
6552
  }
@@ -6797,7 +6801,7 @@ function normalizeRecords2(records, malformedIn) {
6797
6801
  span.startTime = ts;
6798
6802
  const msg = str2(p, "message");
6799
6803
  if (msg)
6800
- span.output = msg;
6804
+ span.input = msg;
6801
6805
  spans.push(span);
6802
6806
  spanIdx++;
6803
6807
  } else if (pType === "agent_message") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutagent/evaluator",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "mutagent-evaluator: a generic, subject-agnostic AI-agent auditor — reviewer, never executor. Audits any skill/agent against a generated subject profile and emits a 4-tab master-audit report.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",