dsh-continual-evolve 0.1.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/README.zh.md +240 -0
  4. package/cordis.patch.yml +9 -0
  5. package/lib/apply.d.ts +24 -0
  6. package/lib/apply.js +131 -0
  7. package/lib/approval.d.ts +14 -0
  8. package/lib/approval.js +27 -0
  9. package/lib/auto.d.ts +34 -0
  10. package/lib/auto.js +217 -0
  11. package/lib/benchmark.d.ts +72 -0
  12. package/lib/benchmark.js +167 -0
  13. package/lib/command.d.ts +36 -0
  14. package/lib/command.js +549 -0
  15. package/lib/evaluate.d.ts +38 -0
  16. package/lib/evaluate.js +142 -0
  17. package/lib/goal.d.ts +72 -0
  18. package/lib/goal.js +72 -0
  19. package/lib/index.d.ts +93 -0
  20. package/lib/index.js +116 -0
  21. package/lib/inject.d.ts +124 -0
  22. package/lib/inject.js +231 -0
  23. package/lib/logfile.d.ts +71 -0
  24. package/lib/logfile.js +159 -0
  25. package/lib/mount.d.ts +42 -0
  26. package/lib/mount.js +198 -0
  27. package/lib/notify.d.ts +31 -0
  28. package/lib/notify.js +42 -0
  29. package/lib/plan.d.ts +16 -0
  30. package/lib/plan.js +121 -0
  31. package/lib/planner.d.ts +30 -0
  32. package/lib/planner.js +110 -0
  33. package/lib/pool.d.ts +7 -0
  34. package/lib/pool.js +25 -0
  35. package/lib/render.d.ts +15 -0
  36. package/lib/render.js +83 -0
  37. package/lib/review.d.ts +37 -0
  38. package/lib/review.js +127 -0
  39. package/lib/rollback.d.ts +11 -0
  40. package/lib/rollback.js +69 -0
  41. package/lib/rubric.d.ts +29 -0
  42. package/lib/rubric.js +119 -0
  43. package/lib/score.d.ts +31 -0
  44. package/lib/score.js +81 -0
  45. package/lib/service.d.ts +30 -0
  46. package/lib/service.js +42 -0
  47. package/lib/skill.d.ts +10 -0
  48. package/lib/skill.js +75 -0
  49. package/lib/source.d.ts +29 -0
  50. package/lib/source.js +42 -0
  51. package/lib/state.d.ts +34 -0
  52. package/lib/state.js +154 -0
  53. package/lib/store.d.ts +20 -0
  54. package/lib/store.js +74 -0
  55. package/lib/tool.d.ts +15 -0
  56. package/lib/tool.js +163 -0
  57. package/lib/types.d.ts +137 -0
  58. package/lib/types.js +62 -0
  59. package/lib/validate.d.ts +11 -0
  60. package/lib/validate.js +55 -0
  61. package/package.json +67 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The human-facing `/evolve` command: inspect and drive the continual
3
+ * harness from the chat UI without the model in between.
4
+ */
5
+ import type { Context } from "@deepseek-ai/cordis";
6
+ import type { HarnessEntry, HarnessState, RefinementKind } from "./types.js";
7
+ import type { EvolutionEngine } from "./service.js";
8
+ export interface CommandGateOptions {
9
+ requireGlobalApproval: boolean;
10
+ }
11
+ export interface CommandRuntimeOptions {
12
+ rubricKey: Buffer;
13
+ /** When a benchmark decision rejects a candidate, roll the refinement back automatically. */
14
+ autoRollbackOnReject: boolean;
15
+ }
16
+ export declare function registerEvolveCommand(ctx: Context, engine: EvolutionEngine, opts: CommandGateOptions, runtime: CommandRuntimeOptions): void;
17
+ /**
18
+ * Tokenize a command's raw input with shell-like quoting:
19
+ * - a `#` outside quotes starts a comment (rest of the line is dropped);
20
+ * - whitespace separates tokens;
21
+ * - double or single quotes group words into one token and are stripped.
22
+ *
23
+ * This lets users paste help-text examples verbatim, e.g.
24
+ * `/evolve benchmark add-case <bid> "<title>" "<statement>" "<rubric>"`.
25
+ */
26
+ export declare function tokenizeEvolveInput(rawInput: string): string[];
27
+ /** Accept both `<id>` (help-text placeholder form) and bare `id`. */
28
+ export declare function stripAngleBrackets(value: string): string;
29
+ /**
30
+ * Locate an entry by id across every kind of a store. Ids are only unique
31
+ * within a kind, so the lookup scans all four and returns the first match
32
+ * (kind + entry) or undefined. Used by archive/unarchive, which take a bare
33
+ * id from the user.
34
+ */
35
+ export declare function findEntryById(state: HarnessState, id: string): [RefinementKind, HarnessEntry] | undefined;
36
+ //# sourceMappingURL=command.d.ts.map
package/lib/command.js ADDED
@@ -0,0 +1,549 @@
1
+ import { ARCHIVED_AT_KEY } from "./types.js";
2
+ import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
3
+ import { planWithLlm } from "./planner.js";
4
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { requireGlobalApproval } from "./approval.js";
6
+ import { saveHarnessState } from "./state.js";
7
+ import { loadLedger, mountSkill, unmountSkill } from "./mount.js";
8
+ import { blockEvolutionGoal, completeEvolutionGoal, goalServiceOf, goalStatusText, upsertEvolutionGoal } from "./goal.js";
9
+ import { appendResult, storePaths } from "./store.js";
10
+ import { addCase, createBenchmark, listBenchmarks, listCases, loadBenchmark, loadScoreboard, rollbackRejectedCandidate, saveScoreboard } from "./benchmark.js";
11
+ import { decide, decisionReport, entryFromCells } from "./score.js";
12
+ import { evaluateState } from "./evaluate.js";
13
+ import { entrySourceOf } from "./source.js";
14
+ import { filterLogBySession, formatLogLine, pluginLogFilePath } from "./logfile.js";
15
+ const USAGE = `Usage:
16
+ /evolve show this help and the current local store
17
+ /evolve list [global] list entries (add "global" for the cross-session store)
18
+ /evolve history [global] show applied refinements (rollback ids)
19
+ /evolve rollback <id> [global] deterministically revert a refinement
20
+ /evolve plan [msg] run the LLM planner against the current store
21
+ /evolve archive <id> [global] hide an entry from injection (data kept, restorable)
22
+ /evolve unarchive <id> [global] restore an archived entry
23
+ /evolve log [tail N] show the recent plugin log (default 50 lines)
24
+ /evolve export [global] <path> backup a store to a JSON file
25
+ /evolve import [global] <path> restore a store from an export file
26
+ /evolve mount <skillId> hot-mount a skill entry as a live cordis plugin
27
+ /evolve mount list list hot-mounted plugins
28
+ /evolve unmount <id> remove a hot-mounted plugin
29
+ /evolve goal show the evolution goal (round-driven auto-review)
30
+ /evolve goal <objective> create/update the evolution goal
31
+ /evolve goal done complete the evolution goal`;
32
+ export function registerEvolveCommand(ctx, engine, opts, runtime) {
33
+ ctx.commands.register({
34
+ name: "evolve",
35
+ description: "inspect and evolve the continual harness state (memories, skills, prompt notes, subagent specs)",
36
+ input: { hint: "[list [global] | history [global] | rollback <id> [global] | plan [msg]]" },
37
+ handler: (invocation) => executeEvolveCommand(ctx, engine, invocation, opts, runtime),
38
+ });
39
+ }
40
+ function scopeArg(tokens) {
41
+ if (tokens[0] === "global") {
42
+ return { scope: "global", rest: tokens.slice(1) };
43
+ }
44
+ return { scope: "local", rest: tokens };
45
+ }
46
+ /**
47
+ * Tokenize a command's raw input with shell-like quoting:
48
+ * - a `#` outside quotes starts a comment (rest of the line is dropped);
49
+ * - whitespace separates tokens;
50
+ * - double or single quotes group words into one token and are stripped.
51
+ *
52
+ * This lets users paste help-text examples verbatim, e.g.
53
+ * `/evolve benchmark add-case <bid> "<title>" "<statement>" "<rubric>"`.
54
+ */
55
+ export function tokenizeEvolveInput(rawInput) {
56
+ const tokens = [];
57
+ let current = "";
58
+ let quote = null;
59
+ for (const char of rawInput) {
60
+ if (quote !== null) {
61
+ if (char === quote) {
62
+ quote = null;
63
+ }
64
+ else {
65
+ current += char;
66
+ }
67
+ continue;
68
+ }
69
+ if (char === '"' || char === "'") {
70
+ quote = char;
71
+ continue;
72
+ }
73
+ if (char === "#") {
74
+ break; // rest of the line is a comment
75
+ }
76
+ if (/\s/.test(char)) {
77
+ if (current.length > 0) {
78
+ tokens.push(current);
79
+ current = "";
80
+ }
81
+ continue;
82
+ }
83
+ current += char;
84
+ }
85
+ if (current.length > 0) {
86
+ tokens.push(current);
87
+ }
88
+ return tokens;
89
+ }
90
+ /** Accept both `<id>` (help-text placeholder form) and bare `id`. */
91
+ export function stripAngleBrackets(value) {
92
+ return value.replace(/^<|>$/g, "");
93
+ }
94
+ /**
95
+ * Locate an entry by id across every kind of a store. Ids are only unique
96
+ * within a kind, so the lookup scans all four and returns the first match
97
+ * (kind + entry) or undefined. Used by archive/unarchive, which take a bare
98
+ * id from the user.
99
+ */
100
+ export function findEntryById(state, id) {
101
+ for (const kind of Object.keys(state.entries)) {
102
+ const entry = state.entries[kind][id];
103
+ if (entry) {
104
+ return [kind, entry];
105
+ }
106
+ }
107
+ return undefined;
108
+ }
109
+ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
110
+ const tokens = tokenizeEvolveInput(invocation.rawInput);
111
+ const sub = tokens[0] ?? "";
112
+ const rest = tokens.slice(1);
113
+ const sessionId = invocation.agent.id;
114
+ try {
115
+ switch (sub) {
116
+ case "":
117
+ case "help":
118
+ return success(`${USAGE}\n\n${formatHarnessStateForPrompt(engine.load("local", sessionId))}`);
119
+ case "list": {
120
+ const { scope } = scopeArg(rest);
121
+ return success(formatHarnessStateForPrompt(engine.load(scope, sessionId)));
122
+ }
123
+ case "history": {
124
+ const { scope } = scopeArg(rest);
125
+ const history = engine.history(scope, sessionId);
126
+ return success(historyForPrompt(history) || "(no refinements yet)");
127
+ }
128
+ case "rollback": {
129
+ const { scope, rest: after } = scopeArg(rest);
130
+ const id = stripAngleBrackets(after[0] ?? "");
131
+ if (!id) {
132
+ return error(`rollback requires a refinement id.\n${USAGE}`);
133
+ }
134
+ const result = engine.rollback(scope, sessionId, id);
135
+ return success(renderResult(result));
136
+ }
137
+ case "archive":
138
+ case "unarchive": {
139
+ const { scope, rest: after } = scopeArg(rest);
140
+ const id = stripAngleBrackets(after[0] ?? "");
141
+ if (!id) {
142
+ return error(`${sub} requires an entry id.\n${USAGE}`);
143
+ }
144
+ const state = engine.load(scope, sessionId);
145
+ const found = findEntryById(state, id);
146
+ if (!found) {
147
+ return error(`entry ${id} not found in the ${scope} store`);
148
+ }
149
+ const [kind, entry] = found;
150
+ const metadata = { ...entry.metadata };
151
+ if (sub === "archive") {
152
+ metadata[ARCHIVED_AT_KEY] = new Date().toISOString();
153
+ }
154
+ else {
155
+ delete metadata[ARCHIVED_AT_KEY];
156
+ }
157
+ const archived = sub === "archive";
158
+ const result = engine.apply(scope, sessionId, {
159
+ summary: `${archived ? "Archive" : "Unarchive"} entry ${kind}:${id}`,
160
+ rationale: "Human-invoked archive/unarchive via the /evolve command.",
161
+ expectedOutcome: `Entry ${archived ? "is hidden from injection (data kept, restorable)" : "is injected again"}.`,
162
+ edits: [{ action: "update", kind, id, title: entry.title, content: entry.content, metadata }],
163
+ }, { scope });
164
+ return success(renderResult(result));
165
+ }
166
+ case "log": {
167
+ // /evolve log [tail N] [session <sessionId>]
168
+ let tail = 50;
169
+ let sessionFilter;
170
+ for (let i = 0; i < rest.length; i += 1) {
171
+ const token = rest[i] ?? "";
172
+ if (token === "session") {
173
+ sessionFilter = stripAngleBrackets(rest[i + 1] ?? "");
174
+ if (!sessionFilter) {
175
+ return error(`log session requires a session id (e.g. /evolve log session session-abc123).\n${USAGE}`);
176
+ }
177
+ i += 1;
178
+ }
179
+ else {
180
+ tail = Math.min(Math.max(parsePositiveInt(token, "tail"), 1), 1000);
181
+ }
182
+ }
183
+ const path = pluginLogFilePath(engine.baseDir);
184
+ if (!existsSync(path)) {
185
+ return success(`(no plugin log yet — ${path} is created on the first log message)`);
186
+ }
187
+ const lines = readFileSync(path, "utf8").trimEnd().split("\n").filter((line) => line.length > 0);
188
+ if (lines.length === 0) {
189
+ return success(`(empty plugin log: ${path})`);
190
+ }
191
+ const filtered = sessionFilter ? filterLogBySession(lines, sessionFilter) : lines;
192
+ const shown = filtered.slice(-tail);
193
+ const scopeNote = sessionFilter ? `, ${filtered.length} for session ${sessionFilter}` : "";
194
+ return success(`plugin log ${path} (${lines.length} lines${scopeNote}, showing last ${shown.length}):\n${shown.map(formatLogLine).join("\n")}`);
195
+ }
196
+ case "export": {
197
+ const { scope, rest: after } = scopeArg(rest);
198
+ const path = after[0];
199
+ if (!path) {
200
+ return error(`export requires an output path.\n${USAGE}`);
201
+ }
202
+ const state = engine.load(scope, sessionId);
203
+ const history = engine.history(scope, sessionId);
204
+ const payload = {
205
+ version: 1,
206
+ scope,
207
+ schema: state.schema,
208
+ entries: state.entries,
209
+ refinements: state.refinements,
210
+ history,
211
+ };
212
+ writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
213
+ return success(`exported ${scope} store (${Object.values(state.entries).reduce((n, e) => n + Object.keys(e).length, 0)} entries, ${history.length} refinements) to ${path}`);
214
+ }
215
+ case "import": {
216
+ const { scope, rest: after } = scopeArg(rest);
217
+ const path = after[0];
218
+ if (!path) {
219
+ return error(`import requires an input path.\n${USAGE}`);
220
+ }
221
+ const payload = JSON.parse(readFileSync(path, "utf8"));
222
+ if (!isValidExport(payload)) {
223
+ return error(`invalid export file shape: expected {version, entries: {prompt, memory, skill, subagent}, refinements, history}`);
224
+ }
225
+ const state = {
226
+ schema: typeof payload["schema"] === "number" ? payload["schema"] : 1,
227
+ entries: {
228
+ prompt: toEntryRecord(payload["entries"]["prompt"]),
229
+ memory: toEntryRecord(payload["entries"]["memory"]),
230
+ skill: toEntryRecord(payload["entries"]["skill"]),
231
+ subagent: toEntryRecord(payload["entries"]["subagent"]),
232
+ },
233
+ refinements: Array.isArray(payload["refinements"]) ? payload["refinements"] : [],
234
+ };
235
+ const paths = storePaths(engine.baseDir, scope, sessionId);
236
+ saveHarnessState(paths.stateDir, state);
237
+ if (Array.isArray(payload["history"])) {
238
+ for (const result of payload["history"]) {
239
+ if (isResultRecord(result)) {
240
+ appendResult(paths, result);
241
+ }
242
+ }
243
+ }
244
+ return success(`imported ${scope} store from ${path}`);
245
+ }
246
+ case "plan": {
247
+ const { scope, rest: after } = scopeArg(rest);
248
+ const instructions = after.length > 0 ? after.join(" ") : undefined;
249
+ const state = engine.load(scope, sessionId);
250
+ const history = engine.history(scope, sessionId);
251
+ const proposal = await planWithLlm(ctx, {
252
+ agent: invocation.agent,
253
+ state,
254
+ history,
255
+ ...(instructions ? { instructions } : {}),
256
+ global: scope === "global",
257
+ signal: invocation.signal,
258
+ });
259
+ if (scope === "global" && opts.requireGlobalApproval && proposal.edits.length > 0) {
260
+ await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan global 将应用 ${proposal.edits.length} 条编辑到跨会话 store:${proposal.summary}`);
261
+ }
262
+ const result = engine.apply(scope, sessionId, proposal, {
263
+ scope,
264
+ baselineState: state,
265
+ ...(entrySourceOf(invocation.agent, sessionId) ? { source: entrySourceOf(invocation.agent, sessionId) } : {}),
266
+ });
267
+ return success(renderResult(result));
268
+ }
269
+ case "goal": {
270
+ return executeGoalCommand(ctx, invocation, rest);
271
+ }
272
+ case "mount": {
273
+ return executeMountCommand(ctx, engine, invocation, rest);
274
+ }
275
+ case "unmount": {
276
+ const id = stripAngleBrackets(rest[0] ?? "");
277
+ if (!id) {
278
+ return error(`unmount requires a mount id (see /evolve mount list).`);
279
+ }
280
+ const record = await unmountSkill(ctx, engine.baseDir, id);
281
+ return record ? success(`unmounted ${record.id} (${record.entryId})`) : error(`no mount found for ${id}`);
282
+ }
283
+ case "benchmark": {
284
+ return executeBenchmarkCommand(ctx, engine, invocation, rest, runtime);
285
+ }
286
+ default:
287
+ return error(`unknown subcommand: ${sub}\n${USAGE}`);
288
+ }
289
+ }
290
+ catch (cause) {
291
+ return error(cause instanceof Error ? cause.message : String(cause));
292
+ }
293
+ }
294
+ function executeGoalCommand(ctx, invocation, rest) {
295
+ const agent = invocation.agent;
296
+ const goals = goalServiceOf(ctx);
297
+ if (!goals) {
298
+ return error(`/evolve goal requires the goals service (load @deepseek-ai/dsh-goal)`);
299
+ }
300
+ const sub = rest[0] ?? "";
301
+ try {
302
+ if (sub === "done") {
303
+ const view = completeEvolutionGoal(ctx, agent);
304
+ return view ? success(`evolution goal completed: ${goalStatusText(view)}`) : success("(no goal to complete)");
305
+ }
306
+ if (sub === "block") {
307
+ const reason = rest.slice(1).join(" ") || "user requested block";
308
+ const view = blockEvolutionGoal(ctx, agent, reason);
309
+ return view ? success(`evolution goal blocked: ${goalStatusText(view)}`) : success("(no active goal to block)");
310
+ }
311
+ if (sub.length === 0) {
312
+ const current = goals.get(agent);
313
+ return current ? success(goalStatusText(current)) : success("(no evolution goal — /evolve goal <objective> to create one)");
314
+ }
315
+ const objective = rest.join(" ");
316
+ const view = upsertEvolutionGoal(ctx, agent, objective);
317
+ return success(`evolution goal ready: ${goalStatusText(view)}\n(active goal drives the review gate every round)`);
318
+ }
319
+ catch (cause) {
320
+ return error(cause instanceof Error ? cause.message : String(cause));
321
+ }
322
+ }
323
+ async function executeMountCommand(ctx, engine, invocation, rest) {
324
+ const sub = rest[0] ?? "";
325
+ if (sub === "list") {
326
+ const ledger = loadLedger(engine.baseDir);
327
+ if (ledger.mounted.length === 0) {
328
+ return success("(no hot-mounted plugins — /evolve mount <skillId>)");
329
+ }
330
+ return success(ledger.mounted.map((m) => `- ${m.id} (${m.entryId}, v${m.version}, ${m.mountedAt})`).join("\n"));
331
+ }
332
+ const skillId = stripAngleBrackets(sub);
333
+ if (!skillId) {
334
+ return error(`mount requires a skill entry id.\nUsage: /evolve mount <skillId> | /evolve mount list`);
335
+ }
336
+ const sessionId = invocation.agent.id;
337
+ const local = engine.load("local", sessionId);
338
+ const globalState = engine.load("global", undefined);
339
+ const entry = local.entries.skill[skillId] ??
340
+ globalState.entries.skill[skillId] ??
341
+ Object.values(local.entries.skill).find((e) => e.id === skillId) ??
342
+ Object.values(globalState.entries.skill).find((e) => e.id === skillId);
343
+ if (!entry) {
344
+ return error(`skill entry ${skillId} not found in local or global store`);
345
+ }
346
+ try {
347
+ const record = await mountSkill(ctx, engine.baseDir, entry);
348
+ return success(`mounted ${record.id} as ${record.entryId} (v${record.version}) — tool: skill_${record.id.replace(/_/g, "-")}`);
349
+ }
350
+ catch (cause) {
351
+ return error(cause instanceof Error ? cause.message : String(cause));
352
+ }
353
+ }
354
+ async function executeBenchmarkCommand(ctx, engine, invocation, rest, runtime) {
355
+ const sub = rest[0] ?? "";
356
+ const args = rest.slice(1);
357
+ const sessionId = invocation.agent.id;
358
+ const baseDir = engine.baseDir;
359
+ switch (sub) {
360
+ case "":
361
+ case "help":
362
+ return success(BENCHMARK_USAGE);
363
+ case "new": {
364
+ const title = args[0] ?? "";
365
+ if (!title) {
366
+ return error(`benchmark new requires a title.\n${BENCHMARK_USAGE}`);
367
+ }
368
+ const runs = args[1] !== undefined ? parsePositiveInt(args[1], "runs") : undefined;
369
+ const definition = createBenchmark(baseDir, { title, ...(runs !== undefined ? { runs } : {}) });
370
+ return success(`benchmark ${definition.id} created (runs=${definition.runs}, passThreshold=${definition.passThreshold})\nAdd cases with: /evolve benchmark add-case ${definition.id} "<title>" "<statement>" "<rubric>"`);
371
+ }
372
+ case "list": {
373
+ const benchmarks = listBenchmarks(baseDir);
374
+ if (benchmarks.length === 0) {
375
+ return success("(no benchmarks yet — use /evolve benchmark new <title>)");
376
+ }
377
+ const lines = benchmarks.map((b) => {
378
+ const cases = listCases(baseDir, b.id);
379
+ const board = loadScoreboard(baseDir, b.id);
380
+ const ref = board.reference ? ` ref=${board.reference.overall ?? "?"}` : " no-reference";
381
+ return `- ${b.id} (${cases.length} cases, runs=${b.runs})${ref}`;
382
+ });
383
+ return success(lines.join("\n"));
384
+ }
385
+ case "add-case": {
386
+ const bid = stripAngleBrackets(args[0] ?? "");
387
+ const title = args[1] ?? "";
388
+ const statement = args[2] ?? "";
389
+ const rubric = args[3] ?? "";
390
+ if (!bid || !title || !statement || !rubric) {
391
+ return error(`benchmark add-case needs <bid> <title> <statement> <rubric>.\n${BENCHMARK_USAGE}`);
392
+ }
393
+ const caseItem = addCase(baseDir, bid, title, statement, rubric, runtime.rubricKey);
394
+ return success(`case ${caseItem.id} added to ${bid}`);
395
+ }
396
+ case "reset": {
397
+ const bid = stripAngleBrackets(args[0] ?? "");
398
+ if (!bid) {
399
+ return error(`benchmark reset needs a <bid>.\n${BENCHMARK_USAGE}`);
400
+ }
401
+ if (!loadBenchmark(baseDir, bid)) {
402
+ return error(`benchmark ${bid} not found`);
403
+ }
404
+ saveScoreboard(baseDir, bid, { candidates: [], decisions: [] });
405
+ return success(`scoreboard reset for ${bid} — run /evolve benchmark run ${bid} to record a fresh reference`);
406
+ }
407
+ case "status": {
408
+ const bid = stripAngleBrackets(args[0] ?? "");
409
+ const board = loadScoreboard(baseDir, bid);
410
+ const lines = [];
411
+ if (board.reference) {
412
+ lines.push(`reference "${board.reference.label}": overall=${board.reference.overall ?? "?"} cells=${board.reference.cells.length}`);
413
+ }
414
+ else {
415
+ lines.push("(no reference evaluation yet)");
416
+ }
417
+ for (const c of board.candidates) {
418
+ lines.push(`candidate "${c.label}": overall=${c.overall ?? "?"} cells=${c.cells.length}${c.refinementId ? ` (${c.refinementId})` : ""}`);
419
+ }
420
+ for (const d of board.decisions) {
421
+ lines.push(`decision: ${d.accepted ? "ACCEPTED" : "rejected"} ${d.candidateLabel} — ${d.reasons.join("; ") || "ok"}`);
422
+ }
423
+ return success(lines.join("\n") || "(empty scoreboard)");
424
+ }
425
+ case "run": {
426
+ const bid = stripAngleBrackets(args[0] ?? "");
427
+ const candidateId = args.includes("candidate") ? stripAngleBrackets(args[args.indexOf("candidate") + 1] ?? "") : undefined;
428
+ const definition = loadBenchmark(baseDir, bid);
429
+ if (!definition) {
430
+ return error(`benchmark ${bid} not found`);
431
+ }
432
+ const cases = listCases(baseDir, bid);
433
+ if (cases.length === 0) {
434
+ return error(`benchmark ${bid} has no cases — use /evolve benchmark add-case`);
435
+ }
436
+ const board = loadScoreboard(baseDir, bid);
437
+ const label = candidateId ? `candidate:${candidateId}` : "reference";
438
+ if (!candidateId && board.reference) {
439
+ return error(`reference already evaluated (${board.reference.overall ?? "?"}); evaluate a candidate instead: /evolve benchmark run ${bid} candidate <refinementId>`);
440
+ }
441
+ const overview = formatHarnessStateForPrompt(engine.load("local", sessionId));
442
+ const outcome = await evaluateState(ctx, invocation.agent, {
443
+ cases,
444
+ rubricKey: runtime.rubricKey,
445
+ runs: definition.runs,
446
+ passThreshold: definition.passThreshold,
447
+ harnessOverview: overview,
448
+ label,
449
+ signal: invocation.signal,
450
+ });
451
+ const entry = entryFromCells(label, outcome.cells, candidateId);
452
+ const lines = [
453
+ `evaluation "${label}": ${outcome.cells.length} cells, overall=${entry.overall ?? "?"}`,
454
+ ...Object.entries(entry.aggregate)
455
+ .filter(([key]) => key !== "overall")
456
+ .map(([key, value]) => ` ${key}: ${value ?? "?"}`),
457
+ ];
458
+ if (candidateId) {
459
+ if (!board.reference) {
460
+ lines.push("(no reference yet — this run only recorded the candidate)");
461
+ board.candidates.push(entry);
462
+ }
463
+ else {
464
+ const decision = decide(board.reference, entry, { passThreshold: definition.passThreshold, regressionTolerance: 0 });
465
+ board.candidates.push(entry);
466
+ board.decisions.push({
467
+ candidateLabel: label,
468
+ refinementId: candidateId,
469
+ accepted: decision.accepted,
470
+ reasons: decision.reasons,
471
+ createdAt: new Date().toISOString(),
472
+ });
473
+ lines.push(...decisionReport(board.reference, entry, decision));
474
+ if (!decision.accepted) {
475
+ lines.push(`Consider rolling back the candidate: /evolve rollback <${candidateId}>`);
476
+ if (runtime.autoRollbackOnReject) {
477
+ const outcome = rollbackRejectedCandidate(engine, sessionId, candidateId);
478
+ lines.push(outcome.message);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ else {
484
+ board.reference = entry;
485
+ lines.push("reference evaluation recorded as the baseline");
486
+ }
487
+ saveScoreboard(baseDir, bid, board);
488
+ return success(lines.join("\n"));
489
+ }
490
+ default:
491
+ return error(`unknown benchmark subcommand: ${sub}\n${BENCHMARK_USAGE}`);
492
+ }
493
+ }
494
+ const BENCHMARK_USAGE = `Usage:
495
+ /evolve benchmark new <title> create a benchmark (runs=1)
496
+ /evolve benchmark add-case <bid> <title> <statement> <rubric>
497
+ /evolve benchmark list list benchmarks + reference status
498
+ /evolve benchmark status <bid> show scoreboard + decisions
499
+ /evolve benchmark reset <bid> clear the scoreboard (fresh reference)
500
+ /evolve benchmark run <bid> evaluate current state as the reference
501
+ /evolve benchmark run <bid> candidate <refinementId> evaluate the post-refinement state and decide`;
502
+ function renderResult(result) {
503
+ const applied = result.appliedEdits.filter((e) => e.applied);
504
+ const failed = result.appliedEdits.filter((e) => !e.applied);
505
+ const lines = [
506
+ `refinement ${result.id}${result.rollbackOf ? ` (rollback of ${result.rollbackOf})` : ""}: ${applied.length} applied, ${failed.length} failed`,
507
+ `summary: ${result.summary}`,
508
+ ];
509
+ for (const e of applied) {
510
+ lines.push(`- ${e.action} ${e.kind}:${e.id} (v${(e.after?.version ?? e.before?.version) ?? "?"})`);
511
+ }
512
+ for (const e of failed) {
513
+ lines.push(`- failed ${e.action} ${e.kind}:${e.id ?? "(computed)"} — ${e.error ?? "unknown error"}`);
514
+ }
515
+ lines.push(`expected outcome: ${result.expectedOutcome}`);
516
+ return lines.join("\n");
517
+ }
518
+ function toEntryRecord(value) {
519
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
520
+ return {};
521
+ }
522
+ return value;
523
+ }
524
+ function parsePositiveInt(value, what) {
525
+ const n = Number(value);
526
+ if (!Number.isInteger(n) || n < 1) {
527
+ throw new Error(`${what} must be a positive integer, got "${value}"`);
528
+ }
529
+ return n;
530
+ }
531
+ function isValidExport(payload) {
532
+ if (typeof payload !== "object" || payload === null)
533
+ return false;
534
+ const entries = payload["entries"];
535
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries))
536
+ return false;
537
+ const kinds = ["prompt", "memory", "skill", "subagent"];
538
+ return kinds.every((kind) => Object.prototype.hasOwnProperty.call(entries, kind));
539
+ }
540
+ function isResultRecord(value) {
541
+ return typeof value === "object" && value !== null && "id" in value && "appliedEdits" in value;
542
+ }
543
+ function success(text) {
544
+ return { kind: "success", text };
545
+ }
546
+ function error(text) {
547
+ return { kind: "error", text };
548
+ }
549
+ //# sourceMappingURL=command.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The evaluation matrix runner: executes every case × run as a fresh
3
+ * structured-output subagent, with the provider/model frozen to the calling
4
+ * agent's own route. Raw per-cell scores come back to the host; aggregation
5
+ * and acceptance happen in code (`src/score.ts`).
6
+ *
7
+ * Uses the host-plane `subagents` service (available in every profile) with
8
+ * the native `outputSchema` structured-output seam: the provider validates
9
+ * the child's reply against the cell schema, so the host never parses
10
+ * model text for evaluations. (The workflow engine was rejected because the
11
+ * web profile keeps it in a per-agent isolated realm a host plugin cannot
12
+ * resolve.)
13
+ */
14
+ import type { Context } from "@deepseek-ai/cordis";
15
+ import type { Agent } from "@deepseek-ai/dsh-agent";
16
+ import type { BenchmarkCase, CellScore } from "./benchmark.js";
17
+ export interface EvaluateOptions {
18
+ cases: readonly BenchmarkCase[];
19
+ runs: number;
20
+ passThreshold: number;
21
+ /** Serialized harness state under test (the candidate's guidance). */
22
+ harnessOverview: string;
23
+ label: string;
24
+ /** AES-256 key for the encrypted rubric envelopes (see src/rubric.ts). */
25
+ rubricKey?: Buffer;
26
+ signal?: AbortSignal;
27
+ }
28
+ export interface EvaluationOutcome {
29
+ label: string;
30
+ cells: CellScore[];
31
+ stopReason: string;
32
+ }
33
+ /** How many evaluation units may run concurrently (bounded subagent fan-out). */
34
+ export declare const DEFAULT_EVALUATION_CONCURRENCY = 4;
35
+ export declare function evaluateState(ctx: Context, agent: Agent, options: EvaluateOptions): Promise<EvaluationOutcome>;
36
+ /** Validate a provider-validated structured cell; returns undefined when malformed. */
37
+ export declare function normalizeCell(value: unknown, caseId: string, run: number, passThreshold: number): CellScore | undefined;
38
+ //# sourceMappingURL=evaluate.d.ts.map