dsh-continual-evolve 0.2.0 → 0.3.0
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/README.md +45 -7
- package/README.zh.md +44 -7
- package/lib/apply.js +1 -1
- package/lib/approval.d.ts +6 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +38 -4
- package/lib/auto.js +58 -5
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +331 -0
- package/lib/benchmark.d.ts +70 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.js +25 -442
- package/lib/evaluate.d.ts +7 -0
- package/lib/evaluate.js +22 -7
- package/lib/evolve-event.d.ts +38 -0
- package/lib/evolve-event.js +49 -0
- package/lib/failures.d.ts +39 -0
- package/lib/failures.js +170 -0
- package/lib/fate.d.ts +3 -1
- package/lib/fate.js +8 -4
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +29 -25
- package/lib/index.js +14 -0
- package/lib/inject.d.ts +8 -0
- package/lib/inject.js +51 -4
- package/lib/llm-text.d.ts +30 -0
- package/lib/llm-text.js +49 -0
- package/lib/mount-command.d.ts +10 -0
- package/lib/mount-command.js +48 -0
- package/lib/plan.js +5 -0
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +13 -39
- package/lib/render.d.ts +1 -3
- package/lib/render.js +0 -4
- package/lib/review.d.ts +4 -1
- package/lib/review.js +10 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +15 -0
- package/lib/score.js +74 -5
- package/lib/service.d.ts +2 -2
- package/lib/service.js +5 -2
- package/lib/skill-render.d.ts +15 -0
- package/lib/skill-render.js +30 -0
- package/lib/skill.d.ts +2 -5
- package/lib/skill.js +2 -29
- package/lib/skillquality.d.ts +1 -2
- package/lib/skillquality.js +2 -2
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +22 -1
- package/lib/types.d.ts +8 -0
- package/lib/usage.d.ts +32 -0
- package/lib/usage.js +84 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +26 -1
- package/lib/wrapup-command.d.ts +8 -0
- package/lib/wrapup-command.js +211 -0
- package/lib/wrapup.d.ts +14 -9
- package/lib/wrapup.js +24 -36
- package/package.json +8 -8
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { formatHarnessStateForPrompt } from "./render.js";
|
|
2
|
+
import { stripAngleBrackets } from "./command.js";
|
|
3
|
+
import { addCase, caseCheckProblems, createBenchmark, listBenchmarks, listCases, loadBenchmark, loadCaseMeta, loadScoreboard, rollbackRejectedCandidate, saveCaseMeta, saveScoreboard, transitionCaseStatus } from "./benchmark.js";
|
|
4
|
+
import { decide, decisionReport, entryFromCells, flagMaterialDrift } from "./score.js";
|
|
5
|
+
import { evaluateState } from "./evaluate.js";
|
|
6
|
+
function success(text) {
|
|
7
|
+
return { kind: "success", text };
|
|
8
|
+
}
|
|
9
|
+
function error(text) {
|
|
10
|
+
return { kind: "error", text };
|
|
11
|
+
}
|
|
12
|
+
function parsePositiveInt(value, what) {
|
|
13
|
+
const n = Number(value);
|
|
14
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
15
|
+
throw new Error(`${what} must be a positive integer, got "${value}"`);
|
|
16
|
+
}
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
/** " (N failed)" suffix when an evaluation entry carries failed cells. */
|
|
20
|
+
function failedTextOf(entry) {
|
|
21
|
+
const failed = entry.cells.filter((cell) => cell.status === "failed").length;
|
|
22
|
+
return failed > 0 ? ` (${failed} failed)` : "";
|
|
23
|
+
}
|
|
24
|
+
const BENCHMARK_USAGE = `Usage:
|
|
25
|
+
/evolve benchmark new <title> create a benchmark (runs=1)
|
|
26
|
+
/evolve benchmark add-case <bid> <title> <statement> <rubric>
|
|
27
|
+
/evolve benchmark list list benchmarks + reference status
|
|
28
|
+
/evolve benchmark status <bid> show scoreboard + decisions
|
|
29
|
+
/evolve benchmark reset <bid> clear the scoreboard (fresh reference)
|
|
30
|
+
/evolve benchmark run <bid> evaluate current state as the reference
|
|
31
|
+
/evolve benchmark run <bid> candidate <refinementId> evaluate the post-refinement state and decide
|
|
32
|
+
/evolve benchmark casecheck <bid> quality-gate check all cases
|
|
33
|
+
/evolve benchmark pilot <bid> <cid> single pilot run for calibration
|
|
34
|
+
/evolve benchmark freeze <bid> <cid> freeze a case as formal baseline
|
|
35
|
+
/evolve benchmark meta <bid> <cid> [field value ...] set case metadata (capability/distinguisher/shortcuts)`;
|
|
36
|
+
export async function executeBenchmarkCommand(ctx, engine, invocation, rest, runtime) {
|
|
37
|
+
const sub = rest[0] ?? "";
|
|
38
|
+
const args = rest.slice(1);
|
|
39
|
+
const sessionId = invocation.agent.id;
|
|
40
|
+
const baseDir = engine.baseDir;
|
|
41
|
+
switch (sub) {
|
|
42
|
+
case "":
|
|
43
|
+
case "help":
|
|
44
|
+
return success(BENCHMARK_USAGE);
|
|
45
|
+
case "new": {
|
|
46
|
+
const title = args[0] ?? "";
|
|
47
|
+
if (!title) {
|
|
48
|
+
return error(`benchmark new requires a title.\n${BENCHMARK_USAGE}`);
|
|
49
|
+
}
|
|
50
|
+
const runs = args[1] !== undefined ? parsePositiveInt(args[1], "runs") : undefined;
|
|
51
|
+
const definition = createBenchmark(baseDir, { title, ...(runs !== undefined ? { runs } : {}) });
|
|
52
|
+
return success(`benchmark ${definition.id} created (runs=${definition.runs}, passThreshold=${definition.passThreshold})\nAdd cases with: /evolve benchmark add-case ${definition.id} "<title>" "<statement>" "<rubric>"`);
|
|
53
|
+
}
|
|
54
|
+
case "list": {
|
|
55
|
+
const benchmarks = listBenchmarks(baseDir);
|
|
56
|
+
if (benchmarks.length === 0) {
|
|
57
|
+
return success("(no benchmarks yet — use /evolve benchmark new <title>)");
|
|
58
|
+
}
|
|
59
|
+
const lines = benchmarks.map((b) => {
|
|
60
|
+
const cases = listCases(baseDir, b.id);
|
|
61
|
+
const board = loadScoreboard(baseDir, b.id);
|
|
62
|
+
const ref = board.reference ? ` ref=${board.reference.overall ?? "?"}` : " no-reference";
|
|
63
|
+
return `- ${b.id} (${cases.length} cases, runs=${b.runs})${ref}`;
|
|
64
|
+
});
|
|
65
|
+
return success(lines.join("\n"));
|
|
66
|
+
}
|
|
67
|
+
case "add-case": {
|
|
68
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
69
|
+
const title = args[1] ?? "";
|
|
70
|
+
const statement = args[2] ?? "";
|
|
71
|
+
const rubric = args[3] ?? "";
|
|
72
|
+
if (!bid || !title || !statement || !rubric) {
|
|
73
|
+
return error(`benchmark add-case needs <bid> <title> <statement> <rubric>.\n${BENCHMARK_USAGE}`);
|
|
74
|
+
}
|
|
75
|
+
if (!loadBenchmark(baseDir, bid)) {
|
|
76
|
+
return error(`benchmark ${bid} not found`);
|
|
77
|
+
}
|
|
78
|
+
const caseItem = addCase(baseDir, bid, title, statement, rubric, runtime.rubricKey);
|
|
79
|
+
return success(`case ${caseItem.id} added to ${bid} (status: draft)`);
|
|
80
|
+
}
|
|
81
|
+
case "reset": {
|
|
82
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
83
|
+
if (!bid) {
|
|
84
|
+
return error(`benchmark reset needs a <bid>.\n${BENCHMARK_USAGE}`);
|
|
85
|
+
}
|
|
86
|
+
if (!loadBenchmark(baseDir, bid)) {
|
|
87
|
+
return error(`benchmark ${bid} not found`);
|
|
88
|
+
}
|
|
89
|
+
saveScoreboard(baseDir, bid, { candidates: [], decisions: [] });
|
|
90
|
+
return success(`scoreboard reset for ${bid} — run /evolve benchmark run ${bid} to record a fresh reference`);
|
|
91
|
+
}
|
|
92
|
+
case "status": {
|
|
93
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
94
|
+
const board = loadScoreboard(baseDir, bid);
|
|
95
|
+
const lines = [];
|
|
96
|
+
if (board.reference) {
|
|
97
|
+
lines.push(`reference "${board.reference.label}": overall=${board.reference.overall ?? "?"} cells=${board.reference.cells.length}${failedTextOf(board.reference)}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
lines.push("(no reference evaluation yet)");
|
|
101
|
+
}
|
|
102
|
+
for (const c of board.candidates) {
|
|
103
|
+
lines.push(`candidate "${c.label}": overall=${c.overall ?? "?"} cells=${c.cells.length}${failedTextOf(c)}${c.refinementId ? ` (${c.refinementId})` : ""}`);
|
|
104
|
+
}
|
|
105
|
+
for (const d of board.decisions) {
|
|
106
|
+
lines.push(`decision: ${d.accepted ? "ACCEPTED" : "rejected"} ${d.candidateLabel} — ${d.reasons.join("; ") || "ok"}`);
|
|
107
|
+
}
|
|
108
|
+
return success(lines.join("\n") || "(empty scoreboard)");
|
|
109
|
+
}
|
|
110
|
+
case "run": {
|
|
111
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
112
|
+
const candidateId = args.includes("candidate") ? stripAngleBrackets(args[args.indexOf("candidate") + 1] ?? "") : undefined;
|
|
113
|
+
const definition = loadBenchmark(baseDir, bid);
|
|
114
|
+
if (!definition) {
|
|
115
|
+
return error(`benchmark ${bid} not found`);
|
|
116
|
+
}
|
|
117
|
+
const cases = listCases(baseDir, bid);
|
|
118
|
+
if (cases.length === 0) {
|
|
119
|
+
return error(`benchmark ${bid} has no cases — use /evolve benchmark add-case`);
|
|
120
|
+
}
|
|
121
|
+
const board = loadScoreboard(baseDir, bid);
|
|
122
|
+
const label = candidateId ? `candidate:${candidateId}` : "reference";
|
|
123
|
+
if (!candidateId && board.reference) {
|
|
124
|
+
return error(`reference already evaluated (${board.reference.overall ?? "?"}); evaluate a candidate instead: /evolve benchmark run ${bid} candidate <refinementId>`);
|
|
125
|
+
}
|
|
126
|
+
const overview = formatHarnessStateForPrompt(engine.load("local", sessionId));
|
|
127
|
+
const outcome = await evaluateState(ctx, invocation.agent, {
|
|
128
|
+
cases,
|
|
129
|
+
rubricKey: runtime.rubricKey,
|
|
130
|
+
runs: definition.runs,
|
|
131
|
+
passThreshold: definition.passThreshold,
|
|
132
|
+
harnessOverview: overview,
|
|
133
|
+
label,
|
|
134
|
+
signal: invocation.signal,
|
|
135
|
+
});
|
|
136
|
+
// Gap A3 (version_changed semantics): when a reference exists, re-check
|
|
137
|
+
// the candidate's cells for material drift — a case whose statement/rubric
|
|
138
|
+
// hash differs from the reference run is re-marked failed (never counted
|
|
139
|
+
// as a score, can reject the round via the failure-cell protocol).
|
|
140
|
+
const cells = candidateId && board.reference ? flagMaterialDrift(board.reference, outcome.cells) : outcome.cells;
|
|
141
|
+
const entry = entryFromCells(label, cells, candidateId);
|
|
142
|
+
const failedCells = cells.filter((cell) => cell.status === "failed").length;
|
|
143
|
+
const lines = [
|
|
144
|
+
`evaluation "${label}": ${outcome.cells.length} cells${failedCells > 0 ? `, ${failedCells} failed` : ""}, overall=${entry.overall ?? "?"}`,
|
|
145
|
+
...Object.entries(entry.aggregate)
|
|
146
|
+
.filter(([key]) => key !== "overall" && key !== "failed" && key !== "total")
|
|
147
|
+
.map(([key, value]) => ` ${key}: ${value ?? "?"}`),
|
|
148
|
+
];
|
|
149
|
+
if (failedCells > 0) {
|
|
150
|
+
for (const cell of cells.filter((cell) => cell.status === "failed")) {
|
|
151
|
+
lines.push(` [failed] ${cell.caseId} r${cell.run}: ${cell.notes}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (candidateId) {
|
|
155
|
+
if (!board.reference) {
|
|
156
|
+
lines.push("(no reference yet — this run only recorded the candidate)");
|
|
157
|
+
board.candidates.push(entry);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const decision = decide(board.reference, entry, {
|
|
161
|
+
passThreshold: definition.passThreshold,
|
|
162
|
+
regressionTolerance: 0,
|
|
163
|
+
maxFailedCells: 0,
|
|
164
|
+
});
|
|
165
|
+
board.candidates.push(entry);
|
|
166
|
+
board.decisions.push({
|
|
167
|
+
candidateLabel: label,
|
|
168
|
+
refinementId: candidateId,
|
|
169
|
+
accepted: decision.accepted,
|
|
170
|
+
reasons: decision.reasons,
|
|
171
|
+
createdAt: new Date().toISOString(),
|
|
172
|
+
});
|
|
173
|
+
lines.push(...decisionReport(board.reference, entry, decision));
|
|
174
|
+
if (!decision.accepted) {
|
|
175
|
+
lines.push(`Consider rolling back the candidate: /evolve rollback <${candidateId}>`);
|
|
176
|
+
if (runtime.autoRollbackOnReject) {
|
|
177
|
+
const outcome = rollbackRejectedCandidate(engine, sessionId, candidateId);
|
|
178
|
+
lines.push(outcome.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
board.reference = entry;
|
|
185
|
+
lines.push("reference evaluation recorded as the baseline");
|
|
186
|
+
}
|
|
187
|
+
saveScoreboard(baseDir, bid, board);
|
|
188
|
+
return success(lines.join("\n"));
|
|
189
|
+
}
|
|
190
|
+
case "casecheck": {
|
|
191
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
192
|
+
if (!bid) {
|
|
193
|
+
return error(`benchmark casecheck needs a <bid>.\n${BENCHMARK_USAGE}`);
|
|
194
|
+
}
|
|
195
|
+
const definition = loadBenchmark(baseDir, bid);
|
|
196
|
+
if (!definition) {
|
|
197
|
+
return error(`benchmark ${bid} not found`);
|
|
198
|
+
}
|
|
199
|
+
const cases = listCases(baseDir, bid);
|
|
200
|
+
if (cases.length === 0) {
|
|
201
|
+
return error(`benchmark ${bid} has no cases`);
|
|
202
|
+
}
|
|
203
|
+
const lines = [];
|
|
204
|
+
let totalProblems = 0;
|
|
205
|
+
for (const c of cases) {
|
|
206
|
+
const problems = caseCheckProblems(baseDir, bid, c.id);
|
|
207
|
+
const meta = loadCaseMeta(baseDir, bid, c.id);
|
|
208
|
+
const status = meta?.status ?? "draft";
|
|
209
|
+
if (problems.length === 0) {
|
|
210
|
+
lines.push(` ${c.id} [${status}] ✓`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
lines.push(` ${c.id} [${status}] ✗ (${problems.length} problem${problems.length > 1 ? "s" : ""}):`);
|
|
214
|
+
for (const p of problems) {
|
|
215
|
+
lines.push(` - ${p}`);
|
|
216
|
+
}
|
|
217
|
+
totalProblems += problems.length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const verdict = totalProblems === 0 ? "✓ all cases pass quality gate" : `✗ ${totalProblems} problem${totalProblems > 1 ? "s" : ""} found`;
|
|
221
|
+
return success(`casecheck ${bid}: ${verdict}\n${cases.length} cases checked\n${lines.join("\n")}`);
|
|
222
|
+
}
|
|
223
|
+
case "pilot": {
|
|
224
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
225
|
+
const cid = stripAngleBrackets(args[1] ?? "");
|
|
226
|
+
if (!bid || !cid) {
|
|
227
|
+
return error(`benchmark pilot needs <bid> <cid>.\n${BENCHMARK_USAGE}`);
|
|
228
|
+
}
|
|
229
|
+
const definition = loadBenchmark(baseDir, bid);
|
|
230
|
+
if (!definition) {
|
|
231
|
+
return error(`benchmark ${bid} not found`);
|
|
232
|
+
}
|
|
233
|
+
const cases = listCases(baseDir, bid);
|
|
234
|
+
const target = cases.find((c) => c.id === cid);
|
|
235
|
+
if (!target) {
|
|
236
|
+
return error(`case ${cid} not found in ${bid}`);
|
|
237
|
+
}
|
|
238
|
+
// Transition to calibrating if currently draft.
|
|
239
|
+
const meta = loadCaseMeta(baseDir, bid, cid);
|
|
240
|
+
if (meta?.status === "frozen") {
|
|
241
|
+
return error(`case ${cid} is frozen and cannot be calibrated`);
|
|
242
|
+
}
|
|
243
|
+
if (meta?.status !== "calibrating") {
|
|
244
|
+
transitionCaseStatus(baseDir, bid, cid, "calibrating");
|
|
245
|
+
}
|
|
246
|
+
// Run a single evaluation on just this case (1 run).
|
|
247
|
+
const overview = formatHarnessStateForPrompt(engine.load("local", sessionId));
|
|
248
|
+
const outcome = await evaluateState(ctx, invocation.agent, {
|
|
249
|
+
cases: [target],
|
|
250
|
+
rubricKey: runtime.rubricKey,
|
|
251
|
+
runs: 1,
|
|
252
|
+
passThreshold: definition.passThreshold,
|
|
253
|
+
harnessOverview: overview,
|
|
254
|
+
label: `pilot:${cid}`,
|
|
255
|
+
signal: invocation.signal,
|
|
256
|
+
});
|
|
257
|
+
const cell = outcome.cells[0];
|
|
258
|
+
const scoreText = cell?.status === "ok" ? `${cell.score} (${cell.passed ? "passed" : "below threshold"})` : `failed: ${cell?.notes ?? "unknown"}`;
|
|
259
|
+
// Record in calibration history.
|
|
260
|
+
const updatedMeta = loadCaseMeta(baseDir, bid, cid) ?? { status: "calibrating", capability: "", distinguisher: "", shortcuts: "", calibrationHistory: [] };
|
|
261
|
+
updatedMeta.calibrationHistory.push({
|
|
262
|
+
runAt: new Date().toISOString(),
|
|
263
|
+
score: cell?.status === "ok" ? cell.score : 0,
|
|
264
|
+
passed: cell?.passed ?? false,
|
|
265
|
+
notes: cell?.notes ?? "",
|
|
266
|
+
modified: false,
|
|
267
|
+
});
|
|
268
|
+
saveCaseMeta(baseDir, bid, cid, updatedMeta);
|
|
269
|
+
const lines = [
|
|
270
|
+
`pilot ${cid}: ${scoreText}`,
|
|
271
|
+
`status: calibrating`,
|
|
272
|
+
`calibration runs: ${updatedMeta.calibrationHistory.length}`,
|
|
273
|
+
];
|
|
274
|
+
if (cell?.status === "ok" && cell.sessionId) {
|
|
275
|
+
lines.push(`session: ${cell.sessionId}`);
|
|
276
|
+
}
|
|
277
|
+
lines.push(`next: set meta fields, then /evolve benchmark freeze ${bid} ${cid}`);
|
|
278
|
+
return success(lines.join("\n"));
|
|
279
|
+
}
|
|
280
|
+
case "freeze": {
|
|
281
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
282
|
+
const cid = stripAngleBrackets(args[1] ?? "");
|
|
283
|
+
if (!bid || !cid) {
|
|
284
|
+
return error(`benchmark freeze needs <bid> <cid>.\n${BENCHMARK_USAGE}`);
|
|
285
|
+
}
|
|
286
|
+
const definition = loadBenchmark(baseDir, bid);
|
|
287
|
+
if (!definition) {
|
|
288
|
+
return error(`benchmark ${bid} not found`);
|
|
289
|
+
}
|
|
290
|
+
const meta = loadCaseMeta(baseDir, bid, cid);
|
|
291
|
+
if (!meta) {
|
|
292
|
+
return error(`case ${cid} not found in ${bid}`);
|
|
293
|
+
}
|
|
294
|
+
if (meta.status === "frozen") {
|
|
295
|
+
return error(`case ${cid} is already frozen`);
|
|
296
|
+
}
|
|
297
|
+
// Require quality check to pass before freezing.
|
|
298
|
+
const problems = caseCheckProblems(baseDir, bid, cid);
|
|
299
|
+
if (problems.length > 0) {
|
|
300
|
+
return error(`case ${cid} has ${problems.length} quality problem${problems.length > 1 ? "s" : ""}:\n${problems.map((p) => ` - ${p}`).join("\n")}\nFix these before freezing.`);
|
|
301
|
+
}
|
|
302
|
+
transitionCaseStatus(baseDir, bid, cid, "frozen");
|
|
303
|
+
return success(`case ${cid} frozen as formal baseline (immutable)`);
|
|
304
|
+
}
|
|
305
|
+
case "meta": {
|
|
306
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
307
|
+
const cid = stripAngleBrackets(args[1] ?? "");
|
|
308
|
+
const field = args[2] ?? "";
|
|
309
|
+
const value = args.slice(3).join(" ");
|
|
310
|
+
if (!bid || !cid || !field || !value) {
|
|
311
|
+
return error(`benchmark meta needs <bid> <cid> <field> <value> (fields: capability, distinguisher, shortcuts).\n${BENCHMARK_USAGE}`);
|
|
312
|
+
}
|
|
313
|
+
const meta = loadCaseMeta(baseDir, bid, cid);
|
|
314
|
+
if (!meta) {
|
|
315
|
+
return error(`case ${cid} not found in ${bid}`);
|
|
316
|
+
}
|
|
317
|
+
if (meta.status === "frozen") {
|
|
318
|
+
return error(`case ${cid} is frozen and cannot be modified`);
|
|
319
|
+
}
|
|
320
|
+
if (field !== "capability" && field !== "distinguisher" && field !== "shortcuts") {
|
|
321
|
+
return error(`unknown meta field "${field}" — valid fields: capability, distinguisher, shortcuts`);
|
|
322
|
+
}
|
|
323
|
+
meta[field] = value;
|
|
324
|
+
saveCaseMeta(baseDir, bid, cid, meta);
|
|
325
|
+
return success(`case ${cid} ${field} updated`);
|
|
326
|
+
}
|
|
327
|
+
default:
|
|
328
|
+
return error(`unknown benchmark subcommand: ${sub}\n${BENCHMARK_USAGE}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=benchmark-command.js.map
|
package/lib/benchmark.d.ts
CHANGED
|
@@ -4,6 +4,37 @@ export interface BenchmarkCase {
|
|
|
4
4
|
title: string;
|
|
5
5
|
statement: string;
|
|
6
6
|
rubric: string;
|
|
7
|
+
/**
|
|
8
|
+
* Case lifecycle state (gap A5): draft → calibrating → frozen.
|
|
9
|
+
* - draft: newly added, not yet quality-checked or calibrated
|
|
10
|
+
* - calibrating: pilot run in progress (case may be edited)
|
|
11
|
+
* - frozen: calibrated, locked as a formal baseline (immutable)
|
|
12
|
+
*/
|
|
13
|
+
status?: "draft" | "calibrating" | "frozen";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Persistent per-case metadata (stored in `cases/<cid>/meta.json`).
|
|
17
|
+
* Carries quality-gate annotations and calibration history.
|
|
18
|
+
*/
|
|
19
|
+
export interface CaseMeta {
|
|
20
|
+
status: "draft" | "calibrating" | "frozen";
|
|
21
|
+
/** What this case tests — the capability contract. */
|
|
22
|
+
capability: string;
|
|
23
|
+
/** What distinguishes a pass from a fail. */
|
|
24
|
+
distinguisher: string;
|
|
25
|
+
/** Known shortcuts the agent might use to game the rubric. */
|
|
26
|
+
shortcuts: string;
|
|
27
|
+
/** Calibration run history (appended on each pilot run). */
|
|
28
|
+
calibrationHistory: CalibrationRecord[];
|
|
29
|
+
}
|
|
30
|
+
/** One pilot-run record in the calibration history. */
|
|
31
|
+
export interface CalibrationRecord {
|
|
32
|
+
runAt: string;
|
|
33
|
+
score: number;
|
|
34
|
+
passed: boolean;
|
|
35
|
+
notes: string;
|
|
36
|
+
/** Whether the case was modified after this run. */
|
|
37
|
+
modified: boolean;
|
|
7
38
|
}
|
|
8
39
|
export interface BenchmarkDefinition {
|
|
9
40
|
id: string;
|
|
@@ -33,6 +64,27 @@ export interface CellScore {
|
|
|
33
64
|
* back to the exact session steps that earned it.
|
|
34
65
|
*/
|
|
35
66
|
sessionId?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Runtime evidence verification (gap A3): the actual provider and model
|
|
69
|
+
* used by the evaluation units — written from the host (not the model),
|
|
70
|
+
* so it reflects reality. Combined with `caseHash`, these make material
|
|
71
|
+
* and route drift between reference and candidate runs detectable:
|
|
72
|
+
* `score.flagMaterialDrift` re-marks a candidate cell failed when its
|
|
73
|
+
* case hash no longer matches the reference (version_changed semantics).
|
|
74
|
+
*/
|
|
75
|
+
provider?: string;
|
|
76
|
+
model?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Material hash: SHA-256 prefix of the case statement + rubric envelope,
|
|
79
|
+
* so a material change between reference and candidate runs is detectable.
|
|
80
|
+
* absence means pre-A3 cell (backward compatible).
|
|
81
|
+
*/
|
|
82
|
+
caseHash?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Gap C3: wall-clock duration of this cell's evaluation (both executor +
|
|
85
|
+
* reviewer subagents) in milliseconds. Absent means pre-C3 cell.
|
|
86
|
+
*/
|
|
87
|
+
durationMs?: number;
|
|
36
88
|
}
|
|
37
89
|
export interface EvaluationEntry {
|
|
38
90
|
label: string;
|
|
@@ -82,5 +134,23 @@ export declare function addCase(baseDir: string, bid: string, title: string, sta
|
|
|
82
134
|
export declare function listCases(baseDir: string, bid: string): BenchmarkCase[];
|
|
83
135
|
export declare function loadScoreboard(baseDir: string, bid: string): Scoreboard;
|
|
84
136
|
export declare function saveScoreboard(baseDir: string, bid: string, board: Scoreboard): void;
|
|
137
|
+
export declare function loadCaseMeta(baseDir: string, bid: string, cid: string): CaseMeta | undefined;
|
|
138
|
+
export declare function saveCaseMeta(baseDir: string, bid: string, cid: string, meta: CaseMeta): void;
|
|
139
|
+
/** List case metas for all cases in a benchmark (missing meta → defaults). */
|
|
140
|
+
export declare function listCaseMetas(baseDir: string, bid: string): Map<string, CaseMeta>;
|
|
141
|
+
/** Check whether a case is frozen (immutable). */
|
|
142
|
+
export declare function isCaseFrozen(baseDir: string, bid: string, cid: string): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Transition a case's lifecycle state. Throws on illegal transitions.
|
|
145
|
+
* draft → calibrating (start pilot)
|
|
146
|
+
* calibrating → draft (abandon calibration)
|
|
147
|
+
* calibrating → frozen (lock baseline)
|
|
148
|
+
*/
|
|
149
|
+
export declare function transitionCaseStatus(baseDir: string, bid: string, cid: string, to: "draft" | "calibrating" | "frozen"): CaseMeta;
|
|
150
|
+
/**
|
|
151
|
+
* Quality-check a case: mechanical validation without LLM calls.
|
|
152
|
+
* Returns human-readable problems; empty array means the case passes.
|
|
153
|
+
*/
|
|
154
|
+
export declare function caseCheckProblems(baseDir: string, bid: string, cid: string): string[];
|
|
85
155
|
export declare function removeBenchmark(baseDir: string, bid: string): void;
|
|
86
156
|
//# sourceMappingURL=benchmark.d.ts.map
|
package/lib/benchmark.js
CHANGED
|
@@ -106,6 +106,8 @@ export function addCase(baseDir, bid, title, statement, rubric, rubricKey) {
|
|
|
106
106
|
// resolveRubricKey) and the dev key here is only a defensive last resort.
|
|
107
107
|
const stored = rubricKey ? encryptRubric(rubric, rubricKey) : encryptRubric(rubric, deriveKey(DEV_RUBRIC_KEY));
|
|
108
108
|
writeFileSync(join(caseDir, "rubric.json"), `${JSON.stringify(stored, null, 2)}\n`, "utf8");
|
|
109
|
+
// A5: initialize case metadata for quality gate.
|
|
110
|
+
saveCaseMeta(baseDir, bid, id, defaultCaseMeta());
|
|
109
111
|
return { id, title: title.trim(), statement, rubric };
|
|
110
112
|
}
|
|
111
113
|
export function listCases(baseDir, bid) {
|
|
@@ -121,7 +123,8 @@ export function listCases(baseDir, bid) {
|
|
|
121
123
|
try {
|
|
122
124
|
const statement = readFileSync(statementPath, "utf8");
|
|
123
125
|
const rubric = JSON.parse(readFileSync(rubricPath, "utf8"));
|
|
124
|
-
|
|
126
|
+
const meta = loadCaseMeta(baseDir, bid, id);
|
|
127
|
+
return { id, title: id, statement, rubric, ...(meta ? { status: meta.status } : {}) };
|
|
125
128
|
}
|
|
126
129
|
catch {
|
|
127
130
|
return undefined;
|
|
@@ -149,6 +152,109 @@ export function loadScoreboard(baseDir, bid) {
|
|
|
149
152
|
export function saveScoreboard(baseDir, bid, board) {
|
|
150
153
|
writeFileSync(join(benchmarkDir(baseDir, bid), "scoreboard.json"), `${JSON.stringify(board, null, 2)}\n`, "utf8");
|
|
151
154
|
}
|
|
155
|
+
// ── Case meta (gap A5: quality gate + calibration history) ────────────
|
|
156
|
+
export function loadCaseMeta(baseDir, bid, cid) {
|
|
157
|
+
const path = join(benchmarkDir(baseDir, bid), "cases", cid, "meta.json");
|
|
158
|
+
if (!existsSync(path))
|
|
159
|
+
return undefined;
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function saveCaseMeta(baseDir, bid, cid, meta) {
|
|
168
|
+
const metaDir = join(benchmarkDir(baseDir, bid), "cases", cid);
|
|
169
|
+
mkdirSync(metaDir, { recursive: true });
|
|
170
|
+
writeFileSync(join(metaDir, "meta.json"), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
171
|
+
}
|
|
172
|
+
/** List case metas for all cases in a benchmark (missing meta → defaults). */
|
|
173
|
+
export function listCaseMetas(baseDir, bid) {
|
|
174
|
+
const result = new Map();
|
|
175
|
+
const cases = listCases(baseDir, bid);
|
|
176
|
+
for (const c of cases) {
|
|
177
|
+
const meta = loadCaseMeta(baseDir, bid, c.id);
|
|
178
|
+
if (meta) {
|
|
179
|
+
result.set(c.id, meta);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
/** Check whether a case is frozen (immutable). */
|
|
185
|
+
export function isCaseFrozen(baseDir, bid, cid) {
|
|
186
|
+
const meta = loadCaseMeta(baseDir, bid, cid);
|
|
187
|
+
return meta?.status === "frozen";
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Transition a case's lifecycle state. Throws on illegal transitions.
|
|
191
|
+
* draft → calibrating (start pilot)
|
|
192
|
+
* calibrating → draft (abandon calibration)
|
|
193
|
+
* calibrating → frozen (lock baseline)
|
|
194
|
+
*/
|
|
195
|
+
export function transitionCaseStatus(baseDir, bid, cid, to) {
|
|
196
|
+
const meta = loadCaseMeta(baseDir, bid, cid) ?? defaultCaseMeta();
|
|
197
|
+
const from = meta.status;
|
|
198
|
+
const valid = (from === "draft" && to === "calibrating") ||
|
|
199
|
+
(from === "calibrating" && to === "draft") ||
|
|
200
|
+
(from === "calibrating" && to === "frozen");
|
|
201
|
+
if (!valid) {
|
|
202
|
+
throw new Error(`illegal case status transition: ${from} → ${to}`);
|
|
203
|
+
}
|
|
204
|
+
meta.status = to;
|
|
205
|
+
saveCaseMeta(baseDir, bid, cid, meta);
|
|
206
|
+
return meta;
|
|
207
|
+
}
|
|
208
|
+
function defaultCaseMeta() {
|
|
209
|
+
return {
|
|
210
|
+
status: "draft",
|
|
211
|
+
capability: "",
|
|
212
|
+
distinguisher: "",
|
|
213
|
+
shortcuts: "",
|
|
214
|
+
calibrationHistory: [],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Quality-check a case: mechanical validation without LLM calls.
|
|
219
|
+
* Returns human-readable problems; empty array means the case passes.
|
|
220
|
+
*/
|
|
221
|
+
export function caseCheckProblems(baseDir, bid, cid) {
|
|
222
|
+
const problems = [];
|
|
223
|
+
const statementPath = join(benchmarkDir(baseDir, bid), "cases", cid, "statement.md");
|
|
224
|
+
if (!existsSync(statementPath)) {
|
|
225
|
+
problems.push("statement.md missing");
|
|
226
|
+
return problems;
|
|
227
|
+
}
|
|
228
|
+
const statement = readFileSync(statementPath, "utf8").trim();
|
|
229
|
+
if (statement.length < 20) {
|
|
230
|
+
problems.push(`statement too short (${statement.length} chars, minimum 20)`);
|
|
231
|
+
}
|
|
232
|
+
const rubricPath = join(benchmarkDir(baseDir, bid), "cases", cid, "rubric.json");
|
|
233
|
+
if (!existsSync(rubricPath)) {
|
|
234
|
+
problems.push("rubric.json missing");
|
|
235
|
+
return problems;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const raw = readFileSync(rubricPath, "utf8");
|
|
239
|
+
JSON.parse(raw);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
problems.push("rubric.json is not valid JSON");
|
|
243
|
+
}
|
|
244
|
+
const meta = loadCaseMeta(baseDir, bid, cid);
|
|
245
|
+
if (!meta) {
|
|
246
|
+
problems.push("meta.json missing (run /evolve benchmark casecheck to initialize)");
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
if (!meta.capability)
|
|
250
|
+
problems.push("capability contract is empty");
|
|
251
|
+
if (!meta.distinguisher)
|
|
252
|
+
problems.push("distinguisher is empty");
|
|
253
|
+
if (!meta.shortcuts)
|
|
254
|
+
problems.push("shortcuts annotation is empty");
|
|
255
|
+
}
|
|
256
|
+
return problems;
|
|
257
|
+
}
|
|
152
258
|
export function removeBenchmark(baseDir, bid) {
|
|
153
259
|
const dir = benchmarkDir(baseDir, bid);
|
|
154
260
|
if (existsSync(dir)) {
|