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.
Files changed (66) hide show
  1. package/README.md +83 -371
  2. package/README.zh.md +84 -235
  3. package/lib/apply.js +8 -2
  4. package/lib/approval.d.ts +6 -0
  5. package/lib/approval.js +9 -1
  6. package/lib/auto.d.ts +55 -4
  7. package/lib/auto.js +61 -5
  8. package/lib/benchmark-command.d.ts +9 -0
  9. package/lib/benchmark-command.js +333 -0
  10. package/lib/benchmark.d.ts +70 -0
  11. package/lib/benchmark.js +107 -1
  12. package/lib/command.d.ts +3 -0
  13. package/lib/command.js +62 -441
  14. package/lib/evaluate.d.ts +7 -0
  15. package/lib/evaluate.js +22 -7
  16. package/lib/evolve-event.d.ts +38 -0
  17. package/lib/evolve-event.js +49 -0
  18. package/lib/failures.d.ts +39 -0
  19. package/lib/failures.js +170 -0
  20. package/lib/fate.d.ts +5 -2
  21. package/lib/fate.js +13 -8
  22. package/lib/goal-command.d.ts +7 -0
  23. package/lib/goal-command.js +37 -0
  24. package/lib/index.d.ts +51 -25
  25. package/lib/index.js +33 -1
  26. package/lib/inject.d.ts +24 -1
  27. package/lib/inject.js +93 -5
  28. package/lib/llm-text.d.ts +30 -0
  29. package/lib/llm-text.js +49 -0
  30. package/lib/mount-command.d.ts +10 -0
  31. package/lib/mount-command.js +48 -0
  32. package/lib/plan.js +5 -0
  33. package/lib/planner.d.ts +1 -1
  34. package/lib/planner.js +13 -39
  35. package/lib/promotion.d.ts +62 -0
  36. package/lib/promotion.js +102 -0
  37. package/lib/render.d.ts +1 -3
  38. package/lib/render.js +0 -4
  39. package/lib/review.d.ts +4 -1
  40. package/lib/review.js +10 -38
  41. package/lib/rollback.d.ts +1 -3
  42. package/lib/rollback.js +0 -8
  43. package/lib/score.d.ts +15 -0
  44. package/lib/score.js +74 -5
  45. package/lib/service.d.ts +2 -2
  46. package/lib/service.js +7 -3
  47. package/lib/skill-render.d.ts +23 -0
  48. package/lib/skill-render.js +68 -0
  49. package/lib/skill.d.ts +2 -5
  50. package/lib/skill.js +2 -29
  51. package/lib/skillquality.d.ts +1 -2
  52. package/lib/skillquality.js +2 -2
  53. package/lib/state.js +6 -1
  54. package/lib/store.d.ts +1 -3
  55. package/lib/store.js +0 -7
  56. package/lib/tool.js +22 -1
  57. package/lib/types.d.ts +8 -0
  58. package/lib/usage.d.ts +45 -0
  59. package/lib/usage.js +115 -0
  60. package/lib/validate.d.ts +12 -2
  61. package/lib/validate.js +26 -1
  62. package/lib/wrapup-command.d.ts +9 -0
  63. package/lib/wrapup-command.js +212 -0
  64. package/lib/wrapup.d.ts +29 -15
  65. package/lib/wrapup.js +69 -42
  66. 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 alone. The runtime
32
- * emits `agent/status` with a `{status}` payload and (per host consumers like
33
- * dsh-host-apiproxy) an injected `agent` subject; `agent/turn-stopping` does
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.userQuestions;
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
@@ -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