dsh-continual-evolve 0.2.0 → 0.4.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 +83 -371
- package/README.zh.md +84 -235
- package/lib/apply.js +8 -2
- package/lib/approval.d.ts +6 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +55 -4
- package/lib/auto.js +61 -5
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +333 -0
- package/lib/benchmark.d.ts +70 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.d.ts +3 -0
- package/lib/command.js +62 -441
- 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 +5 -2
- package/lib/fate.js +13 -8
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +51 -25
- package/lib/index.js +33 -1
- package/lib/inject.d.ts +24 -1
- package/lib/inject.js +93 -5
- 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/promotion.d.ts +62 -0
- package/lib/promotion.js +102 -0
- 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 +7 -3
- package/lib/skill-render.d.ts +23 -0
- package/lib/skill-render.js +68 -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/state.js +6 -1
- 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 +45 -0
- package/lib/usage.js +115 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +26 -1
- package/lib/wrapup-command.d.ts +9 -0
- package/lib/wrapup-command.js +212 -0
- package/lib/wrapup.d.ts +29 -15
- package/lib/wrapup.js +69 -42
- package/package.json +10 -8
package/lib/auto.js
CHANGED
|
@@ -25,13 +25,14 @@ import { notifyAutoReview } from "./notify.js";
|
|
|
25
25
|
import { runLocalFatePhase } from "./fate.js";
|
|
26
26
|
import { entrySourceOf } from "./source.js";
|
|
27
27
|
import { mergeHarnessStates } from "./state.js";
|
|
28
|
+
import { questionServiceOf } from "./approval.js";
|
|
29
|
+
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
28
30
|
/** Turns a rejected skill candidate stays silent before being offered again. */
|
|
29
31
|
export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
|
|
30
32
|
/**
|
|
31
|
-
* Count completed turns from agent/status transitions
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* not reliably carry the agent, so it is NOT used for counting.
|
|
33
|
+
* Count completed turns from agent/status transitions (running → idle).
|
|
34
|
+
* Exported for unit testing; production counting uses agent/turn-stopping
|
|
35
|
+
* (see registerAutoReview) which empirically carries the agent subject.
|
|
35
36
|
*/
|
|
36
37
|
export function advanceGateState(state, status) {
|
|
37
38
|
if (status === "running") {
|
|
@@ -134,6 +135,22 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
134
135
|
});
|
|
135
136
|
});
|
|
136
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Gap C1: parse a "provider/model" or "model" string into its components.
|
|
140
|
+
* Returns undefined when the input is empty (no override). A bare model name
|
|
141
|
+
* falls back to the agent's provider, then "deepseek".
|
|
142
|
+
* Exported for unit testing (the advanceGateState precedent); production
|
|
143
|
+
* resolves it inside runReviewPhase.
|
|
144
|
+
*/
|
|
145
|
+
export function parseReviewModel(reviewModel, fallbackProvider) {
|
|
146
|
+
if (!reviewModel || reviewModel.trim().length === 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
const slash = reviewModel.indexOf("/");
|
|
149
|
+
if (slash > 0) {
|
|
150
|
+
return { provider: reviewModel.slice(0, slash), model: reviewModel.slice(slash + 1) };
|
|
151
|
+
}
|
|
152
|
+
return { provider: fallbackProvider ?? "deepseek", model: reviewModel };
|
|
153
|
+
}
|
|
137
154
|
function stateFor(map, sessionId) {
|
|
138
155
|
let state = map.get(sessionId);
|
|
139
156
|
if (!state) {
|
|
@@ -144,6 +161,7 @@ function stateFor(map, sessionId) {
|
|
|
144
161
|
skillRejects: new Map(),
|
|
145
162
|
lastFateAt: 0,
|
|
146
163
|
fateRejects: new Map(),
|
|
164
|
+
goalBlockStreak: 0,
|
|
147
165
|
};
|
|
148
166
|
map.set(sessionId, state);
|
|
149
167
|
}
|
|
@@ -171,8 +189,41 @@ export function loadGateHarnessView(engine, sessionId) {
|
|
|
171
189
|
*/
|
|
172
190
|
async function runGate(ctx, engine, agent, config, state, reason, record) {
|
|
173
191
|
await runReviewPhase(ctx, engine, agent, config, state, reason, record);
|
|
192
|
+
// D3: a goal stuck in "blocked" for consecutive gate runs gets one
|
|
193
|
+
// local-fate assessment (the pipeline below), so whatever led the goal
|
|
194
|
+
// astray is distilled before the session moves on.
|
|
195
|
+
await runGoalBlockedFate(ctx, engine, agent, config, state, reason, record);
|
|
174
196
|
await runLocalFatePhase(ctx, engine, agent, config, state, reason, record);
|
|
175
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
|
|
200
|
+
* gate runs whose goal is in phase "blocked"; when the streak reaches
|
|
201
|
+
* `goalBlockedWrapupTurns`, run ONE local-fate assessment (same pipeline as
|
|
202
|
+
* the normal fate dimension — audit, classify, consult, apply deterministically).
|
|
203
|
+
* The streak resets on any non-blocked run and after a triggered assessment;
|
|
204
|
+
* a declined proposal is then protected by the normal fate cooldown, so a
|
|
205
|
+
* blocked session can never be nagged into another dialog.
|
|
206
|
+
*
|
|
207
|
+
* Exported for unit testing (the advanceGateState precedent); production runs
|
|
208
|
+
* it from runGate.
|
|
209
|
+
*/
|
|
210
|
+
export async function runGoalBlockedFate(ctx, engine, agent, config, state, _reason, record) {
|
|
211
|
+
if (config.goalBlockedWrapupTurns <= 0)
|
|
212
|
+
return;
|
|
213
|
+
const goal = goalServiceOf(ctx)?.get(agent);
|
|
214
|
+
if (goal?.phase !== "blocked") {
|
|
215
|
+
state.goalBlockStreak = 0;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
state.goalBlockStreak += 1;
|
|
219
|
+
if (state.goalBlockStreak < config.goalBlockedWrapupTurns) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
state.goalBlockStreak = 0; // one assessment per streak; declines follow the fate cooldown
|
|
223
|
+
const logger = ctx.logger("continual-evolve");
|
|
224
|
+
logger.info(`auto-review goal-blocked trigger [${agent.id}]: ${config.goalBlockedWrapupTurns} consecutive blocked gate runs → local-fate assessment`);
|
|
225
|
+
await runLocalFatePhase(ctx, engine, agent, config, state, "goal_blocked", record);
|
|
226
|
+
}
|
|
176
227
|
async function runReviewPhase(ctx, engine, agent, config, state, reason, record) {
|
|
177
228
|
const sessionId = agent.id;
|
|
178
229
|
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
@@ -192,6 +243,8 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
192
243
|
const localState = engine.load("local", sessionId);
|
|
193
244
|
const harnessState = loadGateHarnessView(engine, sessionId);
|
|
194
245
|
const history = engine.history("local", sessionId);
|
|
246
|
+
// Gap C1: resolve optional review model override.
|
|
247
|
+
const reviewRoute = parseReviewModel(config.reviewModel, agent.options.provider);
|
|
195
248
|
const review = await reviewAutoRefine(ctx, {
|
|
196
249
|
agent,
|
|
197
250
|
state: harnessState,
|
|
@@ -199,6 +252,7 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
199
252
|
trajectory,
|
|
200
253
|
context: { reason, turnsSinceLastReview },
|
|
201
254
|
budgetTokens: config.budgetTokens,
|
|
255
|
+
...(reviewRoute ? { overrideProvider: reviewRoute.provider, overrideModel: reviewRoute.model } : {}),
|
|
202
256
|
});
|
|
203
257
|
state.lastReviewAt = state.turns;
|
|
204
258
|
if (!review.shouldRefine) {
|
|
@@ -243,6 +297,8 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
243
297
|
});
|
|
244
298
|
logger.info(`auto-review approved (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns; auto-refine ${result.id}: ${result.appliedEdits.filter((e) => e.applied).length} applied, ${result.appliedEdits.filter((e) => !e.applied).length} failed — ${review.rationale}`);
|
|
245
299
|
record({ sessionId, reason, turnsSinceLastReview, outcome: "approved", rationale: review.rationale, refinementId: result.id });
|
|
300
|
+
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
301
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId));
|
|
246
302
|
// Visibility: tell the user what the gate just persisted. Only the
|
|
247
303
|
// turn-interval path notifies — a compaction-triggered gate must not wake
|
|
248
304
|
// the agent mid-compaction — and only when something was actually applied
|
|
@@ -288,7 +344,7 @@ export async function consultSkillEdits(ctx, agent, skillEdits, gate) {
|
|
|
288
344
|
if (lastReject !== undefined && gate.turns - lastReject < SKILL_CONSULT_COOLDOWN_TURNS) {
|
|
289
345
|
return false;
|
|
290
346
|
}
|
|
291
|
-
const userQuestions = ctx
|
|
347
|
+
const userQuestions = questionServiceOf(ctx);
|
|
292
348
|
if (!userQuestions) {
|
|
293
349
|
return false;
|
|
294
350
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/evolve benchmark` subcommand handler. Extracted from command.ts (P2-2).
|
|
3
|
+
*/
|
|
4
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
5
|
+
import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
|
|
6
|
+
import type { EvolutionEngine } from "./service.js";
|
|
7
|
+
import type { CommandRuntimeOptions } from "./command.js";
|
|
8
|
+
export declare function executeBenchmarkCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, rest: string[], runtime: CommandRuntimeOptions): Promise<CommandResult>;
|
|
9
|
+
//# sourceMappingURL=benchmark-command.d.ts.map
|
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
// Older or externally-written meta.json may lack the history array.
|
|
262
|
+
updatedMeta.calibrationHistory ??= [];
|
|
263
|
+
updatedMeta.calibrationHistory.push({
|
|
264
|
+
runAt: new Date().toISOString(),
|
|
265
|
+
score: cell?.status === "ok" ? cell.score : 0,
|
|
266
|
+
passed: cell?.passed ?? false,
|
|
267
|
+
notes: cell?.notes ?? "",
|
|
268
|
+
modified: false,
|
|
269
|
+
});
|
|
270
|
+
saveCaseMeta(baseDir, bid, cid, updatedMeta);
|
|
271
|
+
const lines = [
|
|
272
|
+
`pilot ${cid}: ${scoreText}`,
|
|
273
|
+
`status: calibrating`,
|
|
274
|
+
`calibration runs: ${updatedMeta.calibrationHistory.length}`,
|
|
275
|
+
];
|
|
276
|
+
if (cell?.status === "ok" && cell.sessionId) {
|
|
277
|
+
lines.push(`session: ${cell.sessionId}`);
|
|
278
|
+
}
|
|
279
|
+
lines.push(`next: set meta fields, then /evolve benchmark freeze ${bid} ${cid}`);
|
|
280
|
+
return success(lines.join("\n"));
|
|
281
|
+
}
|
|
282
|
+
case "freeze": {
|
|
283
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
284
|
+
const cid = stripAngleBrackets(args[1] ?? "");
|
|
285
|
+
if (!bid || !cid) {
|
|
286
|
+
return error(`benchmark freeze needs <bid> <cid>.\n${BENCHMARK_USAGE}`);
|
|
287
|
+
}
|
|
288
|
+
const definition = loadBenchmark(baseDir, bid);
|
|
289
|
+
if (!definition) {
|
|
290
|
+
return error(`benchmark ${bid} not found`);
|
|
291
|
+
}
|
|
292
|
+
const meta = loadCaseMeta(baseDir, bid, cid);
|
|
293
|
+
if (!meta) {
|
|
294
|
+
return error(`case ${cid} not found in ${bid}`);
|
|
295
|
+
}
|
|
296
|
+
if (meta.status === "frozen") {
|
|
297
|
+
return error(`case ${cid} is already frozen`);
|
|
298
|
+
}
|
|
299
|
+
// Require quality check to pass before freezing.
|
|
300
|
+
const problems = caseCheckProblems(baseDir, bid, cid);
|
|
301
|
+
if (problems.length > 0) {
|
|
302
|
+
return error(`case ${cid} has ${problems.length} quality problem${problems.length > 1 ? "s" : ""}:\n${problems.map((p) => ` - ${p}`).join("\n")}\nFix these before freezing.`);
|
|
303
|
+
}
|
|
304
|
+
transitionCaseStatus(baseDir, bid, cid, "frozen");
|
|
305
|
+
return success(`case ${cid} frozen as formal baseline (immutable)`);
|
|
306
|
+
}
|
|
307
|
+
case "meta": {
|
|
308
|
+
const bid = stripAngleBrackets(args[0] ?? "");
|
|
309
|
+
const cid = stripAngleBrackets(args[1] ?? "");
|
|
310
|
+
const field = args[2] ?? "";
|
|
311
|
+
const value = args.slice(3).join(" ");
|
|
312
|
+
if (!bid || !cid || !field || !value) {
|
|
313
|
+
return error(`benchmark meta needs <bid> <cid> <field> <value> (fields: capability, distinguisher, shortcuts).\n${BENCHMARK_USAGE}`);
|
|
314
|
+
}
|
|
315
|
+
const meta = loadCaseMeta(baseDir, bid, cid);
|
|
316
|
+
if (!meta) {
|
|
317
|
+
return error(`case ${cid} not found in ${bid}`);
|
|
318
|
+
}
|
|
319
|
+
if (meta.status === "frozen") {
|
|
320
|
+
return error(`case ${cid} is frozen and cannot be modified`);
|
|
321
|
+
}
|
|
322
|
+
if (field !== "capability" && field !== "distinguisher" && field !== "shortcuts") {
|
|
323
|
+
return error(`unknown meta field "${field}" — valid fields: capability, distinguisher, shortcuts`);
|
|
324
|
+
}
|
|
325
|
+
meta[field] = value;
|
|
326
|
+
saveCaseMeta(baseDir, bid, cid, meta);
|
|
327
|
+
return success(`case ${cid} ${field} updated`);
|
|
328
|
+
}
|
|
329
|
+
default:
|
|
330
|
+
return error(`unknown benchmark subcommand: ${sub}\n${BENCHMARK_USAGE}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
//# 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
|