dsh-continual-evolve 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -7
- package/README.zh.md +44 -7
- package/lib/apply.js +1 -1
- package/lib/approval.d.ts +6 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +38 -4
- package/lib/auto.js +58 -5
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +331 -0
- package/lib/benchmark.d.ts +70 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.js +25 -442
- package/lib/evaluate.d.ts +7 -0
- package/lib/evaluate.js +22 -7
- package/lib/evolve-event.d.ts +38 -0
- package/lib/evolve-event.js +49 -0
- package/lib/failures.d.ts +39 -0
- package/lib/failures.js +170 -0
- package/lib/fate.d.ts +3 -1
- package/lib/fate.js +8 -4
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +29 -25
- package/lib/index.js +14 -0
- package/lib/inject.d.ts +8 -0
- package/lib/inject.js +51 -4
- package/lib/llm-text.d.ts +30 -0
- package/lib/llm-text.js +49 -0
- package/lib/mount-command.d.ts +10 -0
- package/lib/mount-command.js +48 -0
- package/lib/plan.js +5 -0
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +13 -39
- package/lib/render.d.ts +1 -3
- package/lib/render.js +0 -4
- package/lib/review.d.ts +4 -1
- package/lib/review.js +10 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +15 -0
- package/lib/score.js +74 -5
- package/lib/service.d.ts +2 -2
- package/lib/service.js +5 -2
- package/lib/skill-render.d.ts +15 -0
- package/lib/skill-render.js +30 -0
- package/lib/skill.d.ts +2 -5
- package/lib/skill.js +2 -29
- package/lib/skillquality.d.ts +1 -2
- package/lib/skillquality.js +2 -2
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +22 -1
- package/lib/types.d.ts +8 -0
- package/lib/usage.d.ts +32 -0
- package/lib/usage.js +84 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +26 -1
- package/lib/wrapup-command.d.ts +8 -0
- package/lib/wrapup-command.js +211 -0
- package/lib/wrapup.d.ts +14 -9
- package/lib/wrapup.js +24 -36
- package/package.json +8 -8
package/lib/command.js
CHANGED
|
@@ -4,16 +4,15 @@ import { planWithLlm } from "./planner.js";
|
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { requireGlobalApproval } from "./approval.js";
|
|
7
|
-
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals } from "./wrapup.js";
|
|
8
7
|
import { saveHarnessState } from "./state.js";
|
|
9
|
-
import { loadLedger, mountSkill, unmountSkill } from "./mount.js";
|
|
10
|
-
import { blockEvolutionGoal, completeEvolutionGoal, goalServiceOf, goalStatusText, upsertEvolutionGoal } from "./goal.js";
|
|
11
8
|
import { appendResult, storePaths } from "./store.js";
|
|
12
|
-
import { addCase, createBenchmark, listBenchmarks, listCases, loadBenchmark, loadScoreboard, rollbackRejectedCandidate, saveScoreboard } from "./benchmark.js";
|
|
13
|
-
import { decide, decisionReport, entryFromCells } from "./score.js";
|
|
14
|
-
import { evaluateState } from "./evaluate.js";
|
|
15
9
|
import { entrySourceOf } from "./source.js";
|
|
16
10
|
import { filterLogBySession, formatLogLine, pluginLogFilePath } from "./logfile.js";
|
|
11
|
+
import { readBenchmarkFailures, readReviewFailures, summarizeFailures, formatFailureSummary } from "./failures.js";
|
|
12
|
+
import { executeGoalCommand } from "./goal-command.js";
|
|
13
|
+
import { executeMountCommand, executeUnmountCommand } from "./mount-command.js";
|
|
14
|
+
import { executeBenchmarkCommand } from "./benchmark-command.js";
|
|
15
|
+
import { executeWrapupCommand } from "./wrapup-command.js";
|
|
17
16
|
const USAGE = `Usage:
|
|
18
17
|
/evolve show this help and the current local store
|
|
19
18
|
/evolve list [global] list entries (add "global" for the cross-session store)
|
|
@@ -25,6 +24,7 @@ const USAGE = `Usage:
|
|
|
25
24
|
/evolve archive <id> [global] hide an entry from injection (data kept, restorable)
|
|
26
25
|
/evolve unarchive <id> [global] restore an archived entry
|
|
27
26
|
/evolve log [tail N] show the recent plugin log (default 50 lines)
|
|
27
|
+
/evolve failures aggregated failure counts (gate + benchmark, by class)
|
|
28
28
|
/evolve export [global] <path> backup a store to a JSON file
|
|
29
29
|
/evolve import [global] <path> restore a store from an export file
|
|
30
30
|
/evolve mount <skillId> hot-mount a skill entry as a live cordis plugin
|
|
@@ -167,6 +167,22 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
167
167
|
}, { scope });
|
|
168
168
|
return success(renderResult(result));
|
|
169
169
|
}
|
|
170
|
+
case "failures": {
|
|
171
|
+
// /evolve failures — failure-signature aggregation (D1 observation):
|
|
172
|
+
// failed review-gate records + failed benchmark cells, counted by class.
|
|
173
|
+
const failed = [...readReviewFailures(engine.baseDir), ...readBenchmarkFailures(engine.baseDir)];
|
|
174
|
+
const summary = summarizeFailures(failed);
|
|
175
|
+
const parts = formatFailureSummary(summary).split("\n");
|
|
176
|
+
const recent = failed
|
|
177
|
+
.sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))
|
|
178
|
+
.slice(0, 10)
|
|
179
|
+
.map((f) => ` [${f.timestamp ?? "(benchmark)"}] ${f.kind} · ${f.source}: ${f.message.slice(0, 140)}`);
|
|
180
|
+
if (recent.length > 0) {
|
|
181
|
+
parts.push("recent 10:");
|
|
182
|
+
parts.push(...recent);
|
|
183
|
+
}
|
|
184
|
+
return success(parts.join("\n"));
|
|
185
|
+
}
|
|
170
186
|
case "log": {
|
|
171
187
|
// /evolve log [tail N] [session <sessionId>]
|
|
172
188
|
let tail = 50;
|
|
@@ -279,18 +295,13 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
279
295
|
return executeGoalCommand(ctx, invocation, rest);
|
|
280
296
|
}
|
|
281
297
|
case "mount": {
|
|
282
|
-
return executeMountCommand(ctx, engine, invocation, rest);
|
|
298
|
+
return await executeMountCommand(ctx, engine, invocation, rest);
|
|
283
299
|
}
|
|
284
300
|
case "unmount": {
|
|
285
|
-
|
|
286
|
-
if (!id) {
|
|
287
|
-
return error(`unmount requires a mount id (see /evolve mount list).`);
|
|
288
|
-
}
|
|
289
|
-
const record = await unmountSkill(ctx, engine.baseDir, id);
|
|
290
|
-
return record ? success(`unmounted ${record.id} (${record.entryId})`) : error(`no mount found for ${id}`);
|
|
301
|
+
return await executeUnmountCommand(ctx, engine, rest);
|
|
291
302
|
}
|
|
292
303
|
case "benchmark": {
|
|
293
|
-
return executeBenchmarkCommand(ctx, engine, invocation, rest, runtime);
|
|
304
|
+
return await executeBenchmarkCommand(ctx, engine, invocation, rest, runtime);
|
|
294
305
|
}
|
|
295
306
|
default:
|
|
296
307
|
return error(`unknown subcommand: ${sub}\n${USAGE}`);
|
|
@@ -300,434 +311,6 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
300
311
|
return error(cause instanceof Error ? cause.message : String(cause));
|
|
301
312
|
}
|
|
302
313
|
}
|
|
303
|
-
function executeGoalCommand(ctx, invocation, rest) {
|
|
304
|
-
const agent = invocation.agent;
|
|
305
|
-
const goals = goalServiceOf(ctx);
|
|
306
|
-
if (!goals) {
|
|
307
|
-
return error(`/evolve goal requires the goals service (load @deepseek-ai/dsh-goal)`);
|
|
308
|
-
}
|
|
309
|
-
const sub = rest[0] ?? "";
|
|
310
|
-
try {
|
|
311
|
-
if (sub === "done") {
|
|
312
|
-
const view = completeEvolutionGoal(ctx, agent);
|
|
313
|
-
return view ? success(`evolution goal completed: ${goalStatusText(view)}`) : success("(no goal to complete)");
|
|
314
|
-
}
|
|
315
|
-
if (sub === "block") {
|
|
316
|
-
const reason = rest.slice(1).join(" ") || "user requested block";
|
|
317
|
-
const view = blockEvolutionGoal(ctx, agent, reason);
|
|
318
|
-
return view ? success(`evolution goal blocked: ${goalStatusText(view)}`) : success("(no active goal to block)");
|
|
319
|
-
}
|
|
320
|
-
if (sub.length === 0) {
|
|
321
|
-
const current = goals.get(agent);
|
|
322
|
-
return current ? success(goalStatusText(current)) : success("(no evolution goal — /evolve goal <objective> to create one)");
|
|
323
|
-
}
|
|
324
|
-
const objective = rest.join(" ");
|
|
325
|
-
const view = upsertEvolutionGoal(ctx, agent, objective);
|
|
326
|
-
return success(`evolution goal ready: ${goalStatusText(view)}\n(active goal drives the review gate every round)`);
|
|
327
|
-
}
|
|
328
|
-
catch (cause) {
|
|
329
|
-
return error(cause instanceof Error ? cause.message : String(cause));
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
async function executeWrapupCommand(ctx, engine, invocation) {
|
|
333
|
-
const sessionId = invocation.agent.id;
|
|
334
|
-
const localState = engine.load("local", sessionId);
|
|
335
|
-
const globalState = engine.load("global", undefined);
|
|
336
|
-
const candidates = listLocalCandidates(localState, globalState);
|
|
337
|
-
if (candidates.length === 0) {
|
|
338
|
-
return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
|
|
339
|
-
}
|
|
340
|
-
// 1. Classify: the model judges each audited candidate's fate.
|
|
341
|
-
const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, { signal: invocation.signal });
|
|
342
|
-
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
343
|
-
// 2. Partition by action. Deterministic guards re-check the LIVE global
|
|
344
|
-
// store right before anything lands (state may have changed mid-call).
|
|
345
|
-
const { promotable, skipped } = filterPromotable(assessment.items, globalState, candidates);
|
|
346
|
-
const promoteItems = promotable.filter((item) => item.verdict === "promote");
|
|
347
|
-
const archiveItems = assessment.items.filter((item) => item.verdict === "archive");
|
|
348
|
-
// Split promotion (A-form): archive a mixed entry but promote ONLY the
|
|
349
|
-
// cleaned durable part the model extracted. Guarded the same way as whole
|
|
350
|
-
// promotes — a split that would duplicate a globally covered topic is
|
|
351
|
-
// dropped and the entry archives plain.
|
|
352
|
-
const splitItems = [];
|
|
353
|
-
const splitSkipped = [];
|
|
354
|
-
for (const item of archiveItems) {
|
|
355
|
-
if (!item.promote)
|
|
356
|
-
continue;
|
|
357
|
-
const candidate = byKey.get(item.key);
|
|
358
|
-
if (!candidate) {
|
|
359
|
-
splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
const blocked = splitPromoteBlocked(item, globalState, candidate.kind);
|
|
363
|
-
if (blocked) {
|
|
364
|
-
splitSkipped.push({ key: item.key, reason: blocked });
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
splitItems.push({ item, candidate });
|
|
368
|
-
}
|
|
369
|
-
// Plain archives (no split payload): the symmetric guard — an archive that
|
|
370
|
-
// is NOT globally covered AND was distilled from real user messages must
|
|
371
|
-
// not proceed silently.
|
|
372
|
-
const plainArchives = archiveItems.filter((item) => !item.promote);
|
|
373
|
-
const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
|
|
374
|
-
const keepItems = assessment.items.filter((item) => item.verdict === "keep");
|
|
375
|
-
// 3. Report the assessment before touching anything.
|
|
376
|
-
const lines = [
|
|
377
|
-
`wrapup assessment (${sessionId}): ${candidates.length} candidates${candidates.some((c) => c.coveredGlobally) ? `, ${candidates.filter((c) => c.coveredGlobally).length} covered globally` : ""}`,
|
|
378
|
-
`${assessment.rationale}`,
|
|
379
|
-
];
|
|
380
|
-
for (const [heading, items] of [
|
|
381
|
-
["PROMOTE (to global)", promoteItems],
|
|
382
|
-
["SPLIT (archive + promote durable part)", splitItems.map((split) => split.item)],
|
|
383
|
-
["ARCHIVE", silentArchives],
|
|
384
|
-
["ARCHIVE (needs review)", reviewArchives],
|
|
385
|
-
["KEEP", keepItems],
|
|
386
|
-
]) {
|
|
387
|
-
lines.push(`${heading}: ${items.length}`);
|
|
388
|
-
for (const item of items) {
|
|
389
|
-
const candidate = byKey.get(item.key);
|
|
390
|
-
const title = candidate ? candidate.title : item.key;
|
|
391
|
-
const splitNote = item.promote ? ` → 拆出提升「${item.promote.title}」` : "";
|
|
392
|
-
lines.push(`- ${item.key} "${title}"${splitNote} — ${item.reason}`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
for (const skip of skipped) {
|
|
396
|
-
lines.push(`- promote skipped: ${skip.key} — ${skip.reason}`);
|
|
397
|
-
}
|
|
398
|
-
for (const skip of splitSkipped) {
|
|
399
|
-
lines.push(`- split skipped: ${skip.key} — ${skip.reason}`);
|
|
400
|
-
}
|
|
401
|
-
lines.push("");
|
|
402
|
-
const applied = [];
|
|
403
|
-
// 4. Global writes: governed resource — ONE human approval gate covers
|
|
404
|
-
// every create (whole promotes AND split promotions). On approval:
|
|
405
|
-
// - whole promote → create global copy + stamp local promotedTo+archivedAt;
|
|
406
|
-
// - split → create the cleaned durable part + archive the original with
|
|
407
|
-
// promotedTo. On rejection: whole promotes are not written, and each
|
|
408
|
-
// split's original STILL archives plain (its snapshot half deserves
|
|
409
|
-
// the archive; the durable half is reported for manual handling).
|
|
410
|
-
const wholeCreates = promoteItems.map((item) => ({ item, candidate: byKey.get(item.key) }));
|
|
411
|
-
const splitCreates = splitItems;
|
|
412
|
-
const allCreates = new Set([...wholeCreates.map((c) => c.item.key), ...splitCreates.map((c) => c.item.key)]);
|
|
413
|
-
if (allCreates.size > 0) {
|
|
414
|
-
const what = `wrapup 将写入跨会话 global store(共 ${allCreates.size} 条:${promoteItems.length} 条整条提升 + ${splitItems.length} 条拆解提升):\n${[
|
|
415
|
-
...promoteItems.map((item) => `- 整条提升 ${item.key} "${byKey.get(item.key)?.title ?? item.key}"`),
|
|
416
|
-
...splitItems.map((split) => `- 拆解提升 ${split.item.key} → 清洗「${split.item.promote?.title}」(原条目随之归档)`),
|
|
417
|
-
].join("\n")}`;
|
|
418
|
-
let promoteAllowed = true;
|
|
419
|
-
try {
|
|
420
|
-
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what);
|
|
421
|
-
}
|
|
422
|
-
catch (cause) {
|
|
423
|
-
promoteAllowed = false;
|
|
424
|
-
const message = `global 写入未批准 — 整条提升与拆解提升均未写入 (${cause instanceof Error ? cause.message : String(cause)})`;
|
|
425
|
-
applied.push(message);
|
|
426
|
-
lines.push(message);
|
|
427
|
-
}
|
|
428
|
-
if (promoteAllowed) {
|
|
429
|
-
// Whole promotes: create global entry, retire the local copy.
|
|
430
|
-
// Shared proposal builders keep the wrap-up command and the gate's
|
|
431
|
-
// local-fate dimension writing IDENTICAL edits.
|
|
432
|
-
for (const { item, candidate } of wholeCreates) {
|
|
433
|
-
if (!candidate)
|
|
434
|
-
continue;
|
|
435
|
-
const proposals = wholePromoteProposals(item, candidate, sessionId);
|
|
436
|
-
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
437
|
-
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
438
|
-
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
439
|
-
scope: "local",
|
|
440
|
-
baselineState: localState,
|
|
441
|
-
});
|
|
442
|
-
applied.push(`promoted ${item.key} → global:${createdId} (${globalResult.id}; local stamped ${localResult.id})`);
|
|
443
|
-
}
|
|
444
|
-
// Split promotions: create the cleaned durable part, retire the
|
|
445
|
-
// original local entry (its snapshot half is archived along).
|
|
446
|
-
for (const { item, candidate } of splitCreates) {
|
|
447
|
-
if (!item.promote)
|
|
448
|
-
continue;
|
|
449
|
-
const proposals = splitPromoteProposals(item, candidate, sessionId);
|
|
450
|
-
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
451
|
-
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
452
|
-
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
453
|
-
scope: "local",
|
|
454
|
-
baselineState: localState,
|
|
455
|
-
});
|
|
456
|
-
applied.push(`split ${item.key}: promoted cleaned part → global:${createdId} (${globalResult.id}); original archived (${localResult.id})`);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
else {
|
|
460
|
-
// Rejected: whole promotes stay un-written; each split's original
|
|
461
|
-
// still archives plain (reported, data restorable).
|
|
462
|
-
for (const { item, candidate } of splitCreates) {
|
|
463
|
-
if (!candidate)
|
|
464
|
-
continue;
|
|
465
|
-
const result = engine.apply("local", sessionId, {
|
|
466
|
-
summary: `wrapup: split promotion not approved — archive original ${item.key} plain`,
|
|
467
|
-
rationale: item.reason,
|
|
468
|
-
expectedOutcome: `The original leaves injection; the cleaned part was NOT written (reported for manual handling).`,
|
|
469
|
-
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
470
|
-
}, { scope: "local", baselineState: localState });
|
|
471
|
-
applied.push(`split ${item.key}: promotion not approved — original archived plain (${result.id})`);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// 5. Silent archives: deterministic local action (hidden from injection,
|
|
476
|
-
// data kept restorable) — covered topics and operational entries need no
|
|
477
|
-
// confirmation, matching the original behavior.
|
|
478
|
-
for (const item of silentArchives) {
|
|
479
|
-
const candidate = byKey.get(item.key);
|
|
480
|
-
if (!candidate)
|
|
481
|
-
continue;
|
|
482
|
-
const result = engine.apply("local", sessionId, {
|
|
483
|
-
summary: `wrapup: archive local ${item.key} — ${item.reason}`,
|
|
484
|
-
rationale: item.reason,
|
|
485
|
-
expectedOutcome: `The entry stops being injected but stays restorable.`,
|
|
486
|
-
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
487
|
-
}, { scope: "local", baselineState: localState });
|
|
488
|
-
applied.push(`archived ${item.key} (${result.id})`);
|
|
489
|
-
}
|
|
490
|
-
// 6. Review archives (symmetric guard): not covered globally + distilled
|
|
491
|
-
// from real user messages — the user decides before this content is
|
|
492
|
-
// hidden from future sessions. No question service → conservative keep.
|
|
493
|
-
const userQuestions = ctx.userQuestions;
|
|
494
|
-
for (const item of reviewArchives) {
|
|
495
|
-
const candidate = byKey.get(item.key);
|
|
496
|
-
if (!candidate)
|
|
497
|
-
continue;
|
|
498
|
-
if (!userQuestions) {
|
|
499
|
-
applied.push(`kept ${item.key} — archive pending user confirmation (no question service)`);
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
const questionId = "evolve-wrapup-archive-review";
|
|
503
|
-
let archiveConfirmed = false;
|
|
504
|
-
try {
|
|
505
|
-
const answer = await userQuestions.ask({
|
|
506
|
-
questions: [
|
|
507
|
-
{
|
|
508
|
-
id: questionId,
|
|
509
|
-
question: `wrapup:条目「${candidate.title}」未被全局覆盖且源自真实对话,直接归档会隐藏它(数据保留、可恢复)。确认归档?`,
|
|
510
|
-
options: [{ label: "归档" }, { label: "保留" }],
|
|
511
|
-
},
|
|
512
|
-
],
|
|
513
|
-
agent: invocation.agent,
|
|
514
|
-
signal: invocation.signal,
|
|
515
|
-
});
|
|
516
|
-
archiveConfirmed = answer.answers?.find((entry) => entry.id === questionId)?.selected?.includes("归档") ?? false;
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
archiveConfirmed = false;
|
|
520
|
-
}
|
|
521
|
-
if (archiveConfirmed) {
|
|
522
|
-
const result = engine.apply("local", sessionId, {
|
|
523
|
-
summary: `wrapup: archive local ${item.key} (user-confirmed) — ${item.reason}`,
|
|
524
|
-
rationale: item.reason,
|
|
525
|
-
expectedOutcome: `The entry stops being injected but stays restorable.`,
|
|
526
|
-
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
527
|
-
}, { scope: "local", baselineState: localState });
|
|
528
|
-
applied.push(`archived ${item.key} (user-confirmed, ${result.id})`);
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
applied.push(`kept ${item.key} — user declined the archive`);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
lines.push(...(applied.length > 0 ? applied : ["(no changes applied — all entries kept)"]));
|
|
535
|
-
return success(lines.join("\n"));
|
|
536
|
-
}
|
|
537
|
-
async function executeMountCommand(ctx, engine, invocation, rest) {
|
|
538
|
-
const sub = rest[0] ?? "";
|
|
539
|
-
if (sub === "list") {
|
|
540
|
-
const ledger = loadLedger(engine.baseDir);
|
|
541
|
-
if (ledger.mounted.length === 0) {
|
|
542
|
-
return success("(no hot-mounted plugins — /evolve mount <skillId>)");
|
|
543
|
-
}
|
|
544
|
-
return success(ledger.mounted.map((m) => `- ${m.id} (${m.entryId}, v${m.version}, ${m.mountedAt})`).join("\n"));
|
|
545
|
-
}
|
|
546
|
-
const skillId = stripAngleBrackets(sub);
|
|
547
|
-
if (!skillId) {
|
|
548
|
-
return error(`mount requires a skill entry id.\nUsage: /evolve mount <skillId> | /evolve mount list`);
|
|
549
|
-
}
|
|
550
|
-
const sessionId = invocation.agent.id;
|
|
551
|
-
const local = engine.load("local", sessionId);
|
|
552
|
-
const globalState = engine.load("global", undefined);
|
|
553
|
-
const entry = local.entries.skill[skillId] ??
|
|
554
|
-
globalState.entries.skill[skillId] ??
|
|
555
|
-
Object.values(local.entries.skill).find((e) => e.id === skillId) ??
|
|
556
|
-
Object.values(globalState.entries.skill).find((e) => e.id === skillId);
|
|
557
|
-
if (!entry) {
|
|
558
|
-
return error(`skill entry ${skillId} not found in local or global store`);
|
|
559
|
-
}
|
|
560
|
-
try {
|
|
561
|
-
const record = await mountSkill(ctx, engine.baseDir, entry);
|
|
562
|
-
return success(`mounted ${record.id} as ${record.entryId} (v${record.version}) — tool: skill_${record.id.replace(/_/g, "-")}`);
|
|
563
|
-
}
|
|
564
|
-
catch (cause) {
|
|
565
|
-
return error(cause instanceof Error ? cause.message : String(cause));
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
/** " (N failed)" suffix when an evaluation entry carries failed cells. */
|
|
569
|
-
function failedTextOf(entry) {
|
|
570
|
-
const failed = entry.cells.filter((cell) => cell.status === "failed").length;
|
|
571
|
-
return failed > 0 ? ` (${failed} failed)` : "";
|
|
572
|
-
}
|
|
573
|
-
async function executeBenchmarkCommand(ctx, engine, invocation, rest, runtime) {
|
|
574
|
-
const sub = rest[0] ?? "";
|
|
575
|
-
const args = rest.slice(1);
|
|
576
|
-
const sessionId = invocation.agent.id;
|
|
577
|
-
const baseDir = engine.baseDir;
|
|
578
|
-
switch (sub) {
|
|
579
|
-
case "":
|
|
580
|
-
case "help":
|
|
581
|
-
return success(BENCHMARK_USAGE);
|
|
582
|
-
case "new": {
|
|
583
|
-
const title = args[0] ?? "";
|
|
584
|
-
if (!title) {
|
|
585
|
-
return error(`benchmark new requires a title.\n${BENCHMARK_USAGE}`);
|
|
586
|
-
}
|
|
587
|
-
const runs = args[1] !== undefined ? parsePositiveInt(args[1], "runs") : undefined;
|
|
588
|
-
const definition = createBenchmark(baseDir, { title, ...(runs !== undefined ? { runs } : {}) });
|
|
589
|
-
return success(`benchmark ${definition.id} created (runs=${definition.runs}, passThreshold=${definition.passThreshold})\nAdd cases with: /evolve benchmark add-case ${definition.id} "<title>" "<statement>" "<rubric>"`);
|
|
590
|
-
}
|
|
591
|
-
case "list": {
|
|
592
|
-
const benchmarks = listBenchmarks(baseDir);
|
|
593
|
-
if (benchmarks.length === 0) {
|
|
594
|
-
return success("(no benchmarks yet — use /evolve benchmark new <title>)");
|
|
595
|
-
}
|
|
596
|
-
const lines = benchmarks.map((b) => {
|
|
597
|
-
const cases = listCases(baseDir, b.id);
|
|
598
|
-
const board = loadScoreboard(baseDir, b.id);
|
|
599
|
-
const ref = board.reference ? ` ref=${board.reference.overall ?? "?"}` : " no-reference";
|
|
600
|
-
return `- ${b.id} (${cases.length} cases, runs=${b.runs})${ref}`;
|
|
601
|
-
});
|
|
602
|
-
return success(lines.join("\n"));
|
|
603
|
-
}
|
|
604
|
-
case "add-case": {
|
|
605
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
606
|
-
const title = args[1] ?? "";
|
|
607
|
-
const statement = args[2] ?? "";
|
|
608
|
-
const rubric = args[3] ?? "";
|
|
609
|
-
if (!bid || !title || !statement || !rubric) {
|
|
610
|
-
return error(`benchmark add-case needs <bid> <title> <statement> <rubric>.\n${BENCHMARK_USAGE}`);
|
|
611
|
-
}
|
|
612
|
-
const caseItem = addCase(baseDir, bid, title, statement, rubric, runtime.rubricKey);
|
|
613
|
-
return success(`case ${caseItem.id} added to ${bid}`);
|
|
614
|
-
}
|
|
615
|
-
case "reset": {
|
|
616
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
617
|
-
if (!bid) {
|
|
618
|
-
return error(`benchmark reset needs a <bid>.\n${BENCHMARK_USAGE}`);
|
|
619
|
-
}
|
|
620
|
-
if (!loadBenchmark(baseDir, bid)) {
|
|
621
|
-
return error(`benchmark ${bid} not found`);
|
|
622
|
-
}
|
|
623
|
-
saveScoreboard(baseDir, bid, { candidates: [], decisions: [] });
|
|
624
|
-
return success(`scoreboard reset for ${bid} — run /evolve benchmark run ${bid} to record a fresh reference`);
|
|
625
|
-
}
|
|
626
|
-
case "status": {
|
|
627
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
628
|
-
const board = loadScoreboard(baseDir, bid);
|
|
629
|
-
const lines = [];
|
|
630
|
-
if (board.reference) {
|
|
631
|
-
lines.push(`reference "${board.reference.label}": overall=${board.reference.overall ?? "?"} cells=${board.reference.cells.length}${failedTextOf(board.reference)}`);
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
lines.push("(no reference evaluation yet)");
|
|
635
|
-
}
|
|
636
|
-
for (const c of board.candidates) {
|
|
637
|
-
lines.push(`candidate "${c.label}": overall=${c.overall ?? "?"} cells=${c.cells.length}${failedTextOf(c)}${c.refinementId ? ` (${c.refinementId})` : ""}`);
|
|
638
|
-
}
|
|
639
|
-
for (const d of board.decisions) {
|
|
640
|
-
lines.push(`decision: ${d.accepted ? "ACCEPTED" : "rejected"} ${d.candidateLabel} — ${d.reasons.join("; ") || "ok"}`);
|
|
641
|
-
}
|
|
642
|
-
return success(lines.join("\n") || "(empty scoreboard)");
|
|
643
|
-
}
|
|
644
|
-
case "run": {
|
|
645
|
-
const bid = stripAngleBrackets(args[0] ?? "");
|
|
646
|
-
const candidateId = args.includes("candidate") ? stripAngleBrackets(args[args.indexOf("candidate") + 1] ?? "") : undefined;
|
|
647
|
-
const definition = loadBenchmark(baseDir, bid);
|
|
648
|
-
if (!definition) {
|
|
649
|
-
return error(`benchmark ${bid} not found`);
|
|
650
|
-
}
|
|
651
|
-
const cases = listCases(baseDir, bid);
|
|
652
|
-
if (cases.length === 0) {
|
|
653
|
-
return error(`benchmark ${bid} has no cases — use /evolve benchmark add-case`);
|
|
654
|
-
}
|
|
655
|
-
const board = loadScoreboard(baseDir, bid);
|
|
656
|
-
const label = candidateId ? `candidate:${candidateId}` : "reference";
|
|
657
|
-
if (!candidateId && board.reference) {
|
|
658
|
-
return error(`reference already evaluated (${board.reference.overall ?? "?"}); evaluate a candidate instead: /evolve benchmark run ${bid} candidate <refinementId>`);
|
|
659
|
-
}
|
|
660
|
-
const overview = formatHarnessStateForPrompt(engine.load("local", sessionId));
|
|
661
|
-
const outcome = await evaluateState(ctx, invocation.agent, {
|
|
662
|
-
cases,
|
|
663
|
-
rubricKey: runtime.rubricKey,
|
|
664
|
-
runs: definition.runs,
|
|
665
|
-
passThreshold: definition.passThreshold,
|
|
666
|
-
harnessOverview: overview,
|
|
667
|
-
label,
|
|
668
|
-
signal: invocation.signal,
|
|
669
|
-
});
|
|
670
|
-
const entry = entryFromCells(label, outcome.cells, candidateId);
|
|
671
|
-
const failedCells = outcome.cells.filter((cell) => cell.status === "failed").length;
|
|
672
|
-
const lines = [
|
|
673
|
-
`evaluation "${label}": ${outcome.cells.length} cells${failedCells > 0 ? `, ${failedCells} failed` : ""}, overall=${entry.overall ?? "?"}`,
|
|
674
|
-
...Object.entries(entry.aggregate)
|
|
675
|
-
.filter(([key]) => key !== "overall" && key !== "failed" && key !== "total")
|
|
676
|
-
.map(([key, value]) => ` ${key}: ${value ?? "?"}`),
|
|
677
|
-
];
|
|
678
|
-
if (failedCells > 0) {
|
|
679
|
-
for (const cell of outcome.cells.filter((cell) => cell.status === "failed")) {
|
|
680
|
-
lines.push(` [failed] ${cell.caseId} r${cell.run}: ${cell.notes}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
if (candidateId) {
|
|
684
|
-
if (!board.reference) {
|
|
685
|
-
lines.push("(no reference yet — this run only recorded the candidate)");
|
|
686
|
-
board.candidates.push(entry);
|
|
687
|
-
}
|
|
688
|
-
else {
|
|
689
|
-
const decision = decide(board.reference, entry, {
|
|
690
|
-
passThreshold: definition.passThreshold,
|
|
691
|
-
regressionTolerance: 0,
|
|
692
|
-
maxFailedCells: 0,
|
|
693
|
-
});
|
|
694
|
-
board.candidates.push(entry);
|
|
695
|
-
board.decisions.push({
|
|
696
|
-
candidateLabel: label,
|
|
697
|
-
refinementId: candidateId,
|
|
698
|
-
accepted: decision.accepted,
|
|
699
|
-
reasons: decision.reasons,
|
|
700
|
-
createdAt: new Date().toISOString(),
|
|
701
|
-
});
|
|
702
|
-
lines.push(...decisionReport(board.reference, entry, decision));
|
|
703
|
-
if (!decision.accepted) {
|
|
704
|
-
lines.push(`Consider rolling back the candidate: /evolve rollback <${candidateId}>`);
|
|
705
|
-
if (runtime.autoRollbackOnReject) {
|
|
706
|
-
const outcome = rollbackRejectedCandidate(engine, sessionId, candidateId);
|
|
707
|
-
lines.push(outcome.message);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
else {
|
|
713
|
-
board.reference = entry;
|
|
714
|
-
lines.push("reference evaluation recorded as the baseline");
|
|
715
|
-
}
|
|
716
|
-
saveScoreboard(baseDir, bid, board);
|
|
717
|
-
return success(lines.join("\n"));
|
|
718
|
-
}
|
|
719
|
-
default:
|
|
720
|
-
return error(`unknown benchmark subcommand: ${sub}\n${BENCHMARK_USAGE}`);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
const BENCHMARK_USAGE = `Usage:
|
|
724
|
-
/evolve benchmark new <title> create a benchmark (runs=1)
|
|
725
|
-
/evolve benchmark add-case <bid> <title> <statement> <rubric>
|
|
726
|
-
/evolve benchmark list list benchmarks + reference status
|
|
727
|
-
/evolve benchmark status <bid> show scoreboard + decisions
|
|
728
|
-
/evolve benchmark reset <bid> clear the scoreboard (fresh reference)
|
|
729
|
-
/evolve benchmark run <bid> evaluate current state as the reference
|
|
730
|
-
/evolve benchmark run <bid> candidate <refinementId> evaluate the post-refinement state and decide`;
|
|
731
314
|
function renderResult(result) {
|
|
732
315
|
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
733
316
|
const failed = result.appliedEdits.filter((e) => !e.applied);
|
package/lib/evaluate.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ export interface ExecutorResult {
|
|
|
60
60
|
run: number;
|
|
61
61
|
evidence: string;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Deterministic hash of a case's statement + rubric envelope (gap A3):
|
|
65
|
+
* a 16-char SHA-256 prefix, hex-encoded. Used to detect material changes
|
|
66
|
+
* between reference and candidate evaluation runs (see
|
|
67
|
+
* `score.flagMaterialDrift`).
|
|
68
|
+
*/
|
|
69
|
+
export declare function caseHash(caseItem: BenchmarkCase): string;
|
|
63
70
|
/** Validate a provider-validated executor result; returns undefined when malformed. */
|
|
64
71
|
export declare function normalizeExecutor(value: unknown, caseId: string, run: number): ExecutorResult | undefined;
|
|
65
72
|
/** Validate a provider-validated structured cell; returns undefined when malformed. */
|
package/lib/evaluate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { mapPool } from "./pool.js";
|
|
2
3
|
import { decryptRubric, deriveKey, DEV_RUBRIC_KEY } from "./rubric.js";
|
|
3
4
|
/** Key used when the caller did not resolve one: mirrors resolveRubricKey's last-resort dev fallback. */
|
|
@@ -78,7 +79,19 @@ export async function evaluateState(ctx, agent, options) {
|
|
|
78
79
|
function failedCell(caseId, run, message) {
|
|
79
80
|
return { caseId, run, status: "failed", score: 0, passed: false, notes: message };
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Deterministic hash of a case's statement + rubric envelope (gap A3):
|
|
84
|
+
* a 16-char SHA-256 prefix, hex-encoded. Used to detect material changes
|
|
85
|
+
* between reference and candidate evaluation runs (see
|
|
86
|
+
* `score.flagMaterialDrift`).
|
|
87
|
+
*/
|
|
88
|
+
export function caseHash(caseItem) {
|
|
89
|
+
const material = `${caseItem.statement}\n${caseItem.rubric}`;
|
|
90
|
+
return createHash("sha256").update(material).digest("hex").slice(0, 16);
|
|
91
|
+
}
|
|
81
92
|
async function runUnit(subagents, agent, options, c, run) {
|
|
93
|
+
// Gap C3: track wall-clock duration of the entire cell evaluation.
|
|
94
|
+
const cellStart = Date.now();
|
|
82
95
|
// The ONLY rubric decryption point: the envelope is opened here, in the
|
|
83
96
|
// host, and the plaintext goes ONLY into the reviewer prompt — the
|
|
84
97
|
// executor branch never touches it (gap A1).
|
|
@@ -87,8 +100,13 @@ async function runUnit(subagents, agent, options, c, run) {
|
|
|
87
100
|
rubric = decryptRubric(c.rubric, options.rubricKey ?? devRubricKey());
|
|
88
101
|
}
|
|
89
102
|
catch (cause) {
|
|
90
|
-
return failedCell(c.id, run, `rubric decrypt failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
103
|
+
return { ...failedCell(c.id, run, `rubric decrypt failed: ${cause instanceof Error ? cause.message : String(cause)}`), durationMs: Date.now() - cellStart };
|
|
91
104
|
}
|
|
105
|
+
// Runtime evidence (gap A3): record actual provider/model from the host,
|
|
106
|
+
// and compute a material hash of the case for change detection.
|
|
107
|
+
const actualProvider = agent.options.provider ?? "unknown";
|
|
108
|
+
const actualModel = agent.options.model ?? "unknown";
|
|
109
|
+
const hash = caseHash(c);
|
|
92
110
|
// Stage 1: executor — task + evidence, NO rubric.
|
|
93
111
|
let evidence;
|
|
94
112
|
let sessionId;
|
|
@@ -130,7 +148,7 @@ async function runUnit(subagents, agent, options, c, run) {
|
|
|
130
148
|
}
|
|
131
149
|
}
|
|
132
150
|
catch (cause) {
|
|
133
|
-
return failedCell(c.id, run, `executor failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
151
|
+
return { ...failedCell(c.id, run, `executor failed: ${cause instanceof Error ? cause.message : String(cause)}`), provider: actualProvider, model: actualModel, caseHash: hash, durationMs: Date.now() - cellStart };
|
|
134
152
|
}
|
|
135
153
|
// Stage 2: independent reviewer — rubric + evidence, NO task execution.
|
|
136
154
|
try {
|
|
@@ -166,17 +184,14 @@ async function runUnit(subagents, agent, options, c, run) {
|
|
|
166
184
|
if (!cell) {
|
|
167
185
|
throw new Error("reviewer returned neither a structured value nor usable text");
|
|
168
186
|
}
|
|
169
|
-
|
|
170
|
-
return { ...cell, sessionId };
|
|
171
|
-
}
|
|
172
|
-
return cell;
|
|
187
|
+
return { ...cell, ...(sessionId !== undefined ? { sessionId } : {}), provider: actualProvider, model: actualModel, caseHash: hash, durationMs: Date.now() - cellStart };
|
|
173
188
|
}
|
|
174
189
|
finally {
|
|
175
190
|
reviewerRun.dispose();
|
|
176
191
|
}
|
|
177
192
|
}
|
|
178
193
|
catch (cause) {
|
|
179
|
-
return failedCell(c.id, run, `reviewer failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
194
|
+
return { ...failedCell(c.id, run, `reviewer failed: ${cause instanceof Error ? cause.message : String(cause)}`), provider: actualProvider, model: actualModel, caseHash: hash, durationMs: Date.now() - cellStart };
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
197
|
/** Cap the evidence handed to the reviewer so the grading call stays bounded. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RefinementResult } from "./types.js";
|
|
2
|
+
/** The structured event payload emitted after every successful refinement. */
|
|
3
|
+
export interface EvolveCompleteEvent {
|
|
4
|
+
/** Event discriminator for consumers. */
|
|
5
|
+
type: "evolve_complete";
|
|
6
|
+
/** The refinement id (same as the result). */
|
|
7
|
+
refinementId: string;
|
|
8
|
+
/** One-line summary of the refinement. */
|
|
9
|
+
summary: string;
|
|
10
|
+
/** Number of edits that were actually applied. */
|
|
11
|
+
appliedEdits: number;
|
|
12
|
+
/** Number of edits that failed to apply. */
|
|
13
|
+
failedEdits: number;
|
|
14
|
+
/** Scope of the refinement ("local" or "global"). */
|
|
15
|
+
scope: string;
|
|
16
|
+
/** What triggered this refinement (e.g. "auto_review", "manual_plan", "manual_tool"). */
|
|
17
|
+
trigger: string;
|
|
18
|
+
/** Session id that owns the refinement (auto or manual). */
|
|
19
|
+
sessionId: string;
|
|
20
|
+
/** ISO timestamp. */
|
|
21
|
+
timestamp: string;
|
|
22
|
+
/** Per-edit summaries (kind + id + action) for consumers that want detail. */
|
|
23
|
+
edits: {
|
|
24
|
+
action: string;
|
|
25
|
+
kind: string;
|
|
26
|
+
id: string;
|
|
27
|
+
applied: boolean;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/** Build a structured evolve-complete event from a refinement result. */
|
|
31
|
+
export declare function buildEvolveCompleteEvent(result: RefinementResult, trigger: string, sessionId: string): EvolveCompleteEvent;
|
|
32
|
+
/**
|
|
33
|
+
* Emit an evolve_complete event to the reviews.jsonl audit trail. The event
|
|
34
|
+
* is JSONL-formatted (one line) so consumers can tail and parse it. This is
|
|
35
|
+
* a best-effort write — failure never blocks the refinement path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function emitEvolveComplete(baseDir: string, event: EvolveCompleteEvent): void;
|
|
38
|
+
//# sourceMappingURL=evolve-event.d.ts.map
|