@evo-dev/core 0.0.1-alpha.1 → 0.0.1-alpha.3

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.
@@ -1,16 +1,24 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { readCodeAgentTraceRef } from "../code-agent-traces/index.ts";
4
5
  import { resolveEvoDevPaths } from "../config/paths.ts";
5
- import { resolveProjectLogKey } from "../runtime-logs/index.ts";
6
+ import {
7
+ type OkfKnowledgeActivationResult,
8
+ activateOkfKnowledgePlan,
9
+ createOkfKnowledgePlanFromDistillationBatch,
10
+ } from "../knowledge/index.ts";
11
+ import { type EvoDevExecutionEventV1, resolveProjectLogKey } from "../runtime-logs/index.ts";
6
12
 
7
13
  export type EvolutionEvidenceSourceKind =
14
+ | "evodev-execution-event"
8
15
  | "team-agent-trace"
9
16
  | "session-trace"
10
17
  | "task-contract"
11
18
  | "verification"
12
19
  | "review-summary"
13
- | "user-feedback";
20
+ | "user-feedback"
21
+ | "code-agent-trace-ref";
14
22
 
15
23
  export type EvolutionEvidenceEventKind =
16
24
  | "trace"
@@ -77,10 +85,13 @@ export type EvolutionKnowledgeKind =
77
85
  | "verification-pattern";
78
86
 
79
87
  export type EvolutionReviewState =
88
+ | "auto-accepted"
80
89
  | "auto-stored/unreviewed"
90
+ | "needs-human"
81
91
  | "accepted"
82
92
  | "rejected"
83
93
  | "deferred"
94
+ | "stale"
84
95
  | "deprecated"
85
96
  | "superseded"
86
97
  | "revoked";
@@ -129,6 +140,7 @@ export interface EvolutionEvidenceSourceRef {
129
140
  path: string;
130
141
  roleId: string | null;
131
142
  rawContentStored: false;
143
+ externalContentCopied: false;
132
144
  }
133
145
 
134
146
  export interface EvolutionEvidenceEvent {
@@ -320,6 +332,34 @@ export interface EvolutionRepoProposal {
320
332
  privacy: EvolutionPrivacyFields;
321
333
  }
322
334
 
335
+ export interface EvolutionReviewCandidate {
336
+ schemaVersion: 1;
337
+ kind: "evolution-review-candidate";
338
+ id: string;
339
+ projectKey: string;
340
+ runId: string;
341
+ createdAt: string;
342
+ candidateKind: string;
343
+ title: string;
344
+ targetStore: string;
345
+ targetPath: string;
346
+ stableKey: string;
347
+ reviewState: "needs-human";
348
+ reasons: string[];
349
+ candidate: unknown;
350
+ provenance: {
351
+ runId: string;
352
+ evidenceWindowId: string;
353
+ evidenceRefs: string[];
354
+ createdBy: "evodev";
355
+ rawLogsStored: false;
356
+ rawPromptsStored: false;
357
+ sourceDumpsStored: false;
358
+ rawCommandOutputStored: false;
359
+ };
360
+ privacy: EvolutionPrivacyFields;
361
+ }
362
+
323
363
  export interface EvolutionDistillationBatch {
324
364
  schemaVersion: 1;
325
365
  id: string;
@@ -356,14 +396,29 @@ export interface EvolutionWriteResult {
356
396
  evosIndexPath: string;
357
397
  }
358
398
 
399
+ export interface EvolutionActivationResult {
400
+ evidenceWindowPath: string;
401
+ batchPath: string;
402
+ okf: OkfKnowledgeActivationResult;
403
+ evosCasePaths: string[];
404
+ repoProposalPaths: string[];
405
+ evosIndexPath: string;
406
+ }
407
+
359
408
  export interface EvolutionReviewSnapshot {
360
409
  projectKey: string | null;
361
410
  knowledgeRecords: EvolutionKnowledgeRecord[];
362
411
  evosCases: EvolutionEvosCase[];
363
412
  repoProposals: EvolutionRepoProposal[];
413
+ reviewCandidates: EvolutionReviewCandidate[];
364
414
  triggers: EvolutionTriggerRecord[];
365
415
  }
366
416
 
417
+ export interface EvolutionEvosCaseQueryResult {
418
+ cases: EvolutionEvosCase[];
419
+ warnings: string[];
420
+ }
421
+
367
422
  export interface EvolutionResolvedPaths {
368
423
  projectKey: string;
369
424
  runId: string;
@@ -372,6 +427,7 @@ export interface EvolutionResolvedPaths {
372
427
  evidenceWindowPath: string;
373
428
  batchPath: string;
374
429
  triggersDir: string;
430
+ reviewCandidatesDir: string;
375
431
  repoProposalsDir: string;
376
432
  repoProposalsIndexPath: string;
377
433
  knowledgeProjectDir: string;
@@ -426,10 +482,13 @@ const MAX_EVIDENCE_EVENTS = 200;
426
482
  const MAX_TEXT_LENGTH = 600;
427
483
  const PROCESS_LOCK_STALE_MS = 5 * 60 * 1000;
428
484
  const REVIEW_STATES: readonly EvolutionReviewState[] = [
485
+ "auto-accepted",
429
486
  "auto-stored/unreviewed",
487
+ "needs-human",
430
488
  "accepted",
431
489
  "rejected",
432
490
  "deferred",
491
+ "stale",
433
492
  "deprecated",
434
493
  "superseded",
435
494
  "revoked",
@@ -548,9 +607,9 @@ const FORBIDDEN_RAW_KEYS = new Set([
548
607
  "transcripttext",
549
608
  ]);
550
609
  const SENSITIVE_TEXT_PATTERN =
551
- /https?:\/\/\S+|\b(secret|token|password|passwd|api[_-]?key|apikey|credential|credentials|raw log|raw logs|raw output|raw source|raw prompt|shell history|command history)\b/i;
610
+ /https?:\/\/\S+|\b(secret|token|password|passwd|api[_-]?key|apikey|credential|credentials|secret[\s_-]*token(?:[\s_-]*repro)?|raw[\s_-]*(?:log|logs|output|source|prompt)(?:[\s_-]*repro)?|shell[\s_-]*history|command[\s_-]*history)\b/i;
552
611
  const SENSITIVE_TEXT_REPLACE_PATTERN =
553
- /https?:\/\/\S+|\b(secret|token|password|passwd|api[_-]?key|apikey|credential|credentials|raw log|raw logs|raw output|raw source|raw prompt|shell history|command history)\b/gi;
612
+ /https?:\/\/\S+|\b(secret|token|password|passwd|api[_-]?key|apikey|credential|credentials|secret[\s_-]*token(?:[\s_-]*repro)?|raw[\s_-]*(?:log|logs|output|source|prompt)(?:[\s_-]*repro)?|shell[\s_-]*history|command[\s_-]*history)\b/gi;
554
613
 
555
614
  export async function analyzeEvolutionRun(
556
615
  input: EvolutionAnalyzeInput,
@@ -560,23 +619,54 @@ export async function analyzeEvolutionRun(
560
619
  const paths = resolveEvolutionPaths({ homeDir: input.homeDir, projectKey, runId });
561
620
  const warnings: string[] = [];
562
621
  const sourceRefs: EvolutionEvidenceSourceRef[] = [];
622
+ const linkedTraceRefIds = new Set<string>();
563
623
  const events: EvolutionEvidenceEvent[] = [];
564
- const projectRunAgentsDir = join(
565
- resolveEvoDevPaths(input.homeDir).logsDir,
566
- projectKey,
567
- runId,
568
- "agents",
569
- );
570
-
571
- if (!(await pathExists(projectRunAgentsDir))) {
624
+ const logsDir = resolveEvoDevPaths(input.homeDir).logsDir;
625
+ const projectRunAgentsDir = join(logsDir, "teams", projectKey, runId, "agents");
626
+ const legacyProjectRunAgentsDir = join(logsDir, projectKey, runId, "agents");
627
+ const agentDirs = (await pathExists(projectRunAgentsDir))
628
+ ? await readdir(projectRunAgentsDir, { withFileTypes: true })
629
+ : [];
630
+ const legacyAgentDirs = (await pathExists(legacyProjectRunAgentsDir))
631
+ ? await readdir(legacyProjectRunAgentsDir, { withFileTypes: true })
632
+ : [];
633
+
634
+ if (agentDirs.length === 0 && legacyAgentDirs.length === 0) {
572
635
  warnings.push(
573
- `No agent trace directory found: ${displayPath(input.homeDir, projectRunAgentsDir)}`,
636
+ `No agent execution event directory found: ${displayPath(input.homeDir, projectRunAgentsDir)}`,
574
637
  );
575
638
  } else {
576
- const roleDirs = await readdir(projectRunAgentsDir, { withFileTypes: true });
577
- for (const roleDir of roleDirs.filter((entry) => entry.isDirectory())) {
639
+ const rolesWithExecutionEvents = new Set<string>();
640
+ for (const roleDir of agentDirs.filter((entry) => entry.isDirectory())) {
578
641
  const roleId = sanitizeId(roleDir.name);
579
- const tracePath = join(projectRunAgentsDir, roleDir.name, "trace.log");
642
+ const eventsPath = join(projectRunAgentsDir, roleDir.name, "events.jsonl");
643
+ if (!(await pathExists(eventsPath))) continue;
644
+ rolesWithExecutionEvents.add(roleId);
645
+
646
+ const sourceRef: EvolutionEvidenceSourceRef = {
647
+ id: `source-${sourceRefs.length + 1}`,
648
+ kind: "evodev-execution-event",
649
+ path: displayPath(input.homeDir, eventsPath),
650
+ roleId,
651
+ rawContentStored: false,
652
+ externalContentCopied: false,
653
+ };
654
+ sourceRefs.push(sourceRef);
655
+ const parsed = await readExecutionEvidenceEvents({
656
+ path: eventsPath,
657
+ roleId,
658
+ sourceRefId: sourceRef.id,
659
+ existingCount: events.length,
660
+ });
661
+ events.push(...parsed.events);
662
+ for (const traceRefId of parsed.traceRefIds) linkedTraceRefIds.add(traceRefId);
663
+ warnings.push(...parsed.warnings);
664
+ }
665
+
666
+ for (const roleDir of legacyAgentDirs.filter((entry) => entry.isDirectory())) {
667
+ const roleId = sanitizeId(roleDir.name);
668
+ if (rolesWithExecutionEvents.has(roleId)) continue;
669
+ const tracePath = join(legacyProjectRunAgentsDir, roleDir.name, "trace.log");
580
670
  if (!(await pathExists(tracePath))) continue;
581
671
 
582
672
  const sourceRef: EvolutionEvidenceSourceRef = {
@@ -585,6 +675,7 @@ export async function analyzeEvolutionRun(
585
675
  path: displayPath(input.homeDir, tracePath),
586
676
  roleId,
587
677
  rawContentStored: false,
678
+ externalContentCopied: false,
588
679
  };
589
680
  sourceRefs.push(sourceRef);
590
681
  const parsed = await readTraceEvidenceEvents({
@@ -598,6 +689,24 @@ export async function analyzeEvolutionRun(
598
689
  }
599
690
  }
600
691
 
692
+ for (const traceRefId of [...linkedTraceRefIds].sort()) {
693
+ try {
694
+ const record = await readCodeAgentTraceRef({ homeDir: input.homeDir, id: traceRefId });
695
+ sourceRefs.push({
696
+ id: `source-${sourceRefs.length + 1}`,
697
+ kind: "code-agent-trace-ref",
698
+ path: displayPath(input.homeDir, record.path),
699
+ roleId: record.ref.roleId,
700
+ rawContentStored: false,
701
+ externalContentCopied: false,
702
+ });
703
+ } catch {
704
+ warnings.push(
705
+ `Code Agent trace ref metadata not found for linked ref: ${sanitizeText(traceRefId)}.`,
706
+ );
707
+ }
708
+ }
709
+
601
710
  if (events.length > MAX_EVIDENCE_EVENTS) {
602
711
  warnings.push(
603
712
  `Evidence events truncated from ${events.length} to ${MAX_EVIDENCE_EVENTS} metadata events.`,
@@ -675,7 +784,7 @@ export function createEvolutionDistillationBatch(input: {
675
784
  body: [
676
785
  `Run ${input.evidenceWindow.runId} produced ${input.evidenceWindow.events.length} redacted metadata evidence event(s).`,
677
786
  `Observed role(s): ${roleIds.length === 0 ? "none" : roleIds.join(", ")}.`,
678
- "This record is auto-stored and unreviewed; role agents may use it only as contextual evidence.",
787
+ "This record is derived from metadata-only evidence; active use depends on OKF plan review state.",
679
788
  ].join("\n"),
680
789
  roleTags: roleIds,
681
790
  tags: ["project-run", "auto-stored", "unreviewed"],
@@ -718,7 +827,7 @@ export function createEvolutionDistillationBatch(input: {
718
827
  summary: createTriggerSummary(input.evidenceWindow),
719
828
  },
720
829
  intervention: {
721
- summary: `Collected ${input.evidenceWindow.events.length} redacted event(s) and ${input.evidenceWindow.episodes.length} episode(s) from role trace logs.`,
830
+ summary: `Collected ${input.evidenceWindow.events.length} metadata event(s) and ${input.evidenceWindow.episodes.length} episode(s) from EvoDev execution logs.`,
722
831
  roleIds,
723
832
  },
724
833
  result: {
@@ -891,6 +1000,106 @@ export async function writeEvolutionDistillationBatch(input: {
891
1000
  };
892
1001
  }
893
1002
 
1003
+ export async function activateEvolutionDistillationBatch(input: {
1004
+ homeDir: string;
1005
+ batch: EvolutionDistillationBatch;
1006
+ overwrite?: boolean;
1007
+ }): Promise<EvolutionActivationResult> {
1008
+ validateEvolutionDistillationBatch(input.batch);
1009
+ const paths = resolveEvolutionPaths({
1010
+ homeDir: input.homeDir,
1011
+ projectKey: input.batch.projectKey,
1012
+ runId: input.batch.runId,
1013
+ });
1014
+ const overwrite = input.overwrite === true;
1015
+ await writeJson(paths.evidenceWindowPath, input.batch.evidenceWindow, { overwrite });
1016
+ await writeJson(paths.batchPath, input.batch, { overwrite });
1017
+
1018
+ const plan = createOkfKnowledgePlanFromDistillationBatch(input.batch, {
1019
+ homeDir: input.homeDir,
1020
+ });
1021
+ const okf = await activateOkfKnowledgePlan({
1022
+ homeDir: input.homeDir,
1023
+ plan,
1024
+ overwrite,
1025
+ evidenceWindowPath: paths.evidenceWindowPath,
1026
+ });
1027
+
1028
+ const evosCasePaths: string[] = [];
1029
+ for (const evosCase of input.batch.evosCases) {
1030
+ const matchingCandidate = plan.candidates.find((candidate) => candidate.id === evosCase.id);
1031
+ const persistedEvosCase =
1032
+ matchingCandidate?.decision === "auto-accept"
1033
+ ? { ...evosCase, reviewState: "auto-accepted" as const }
1034
+ : matchingCandidate?.decision === "needs-human"
1035
+ ? { ...evosCase, reviewState: "needs-human" as const }
1036
+ : evosCase;
1037
+ validateEvolutionEvosCase(persistedEvosCase);
1038
+ const path = join(paths.evosCasesProjectDir, `${evosCase.id}.json`);
1039
+ await writeJson(path, persistedEvosCase, { overwrite });
1040
+ evosCasePaths.push(path);
1041
+ }
1042
+
1043
+ const repoProposalPaths: string[] = [];
1044
+ for (const proposal of input.batch.repoProposals) {
1045
+ const path = join(paths.repoProposalsDir, `${proposal.id}.json`);
1046
+ await writeJson(path, proposal, { overwrite });
1047
+ repoProposalPaths.push(path);
1048
+ }
1049
+
1050
+ const evosCasesRootDir = resolveEvoDevPaths(input.homeDir).evosCasesDir;
1051
+ const evosProjectKeys = await listDirectoryNames(evosCasesRootDir);
1052
+ const evosProjects = await Promise.all(
1053
+ evosProjectKeys.map(async (projectKey) => {
1054
+ const cases = await readJsonFiles(join(evosCasesRootDir, projectKey), parseEvosCase);
1055
+ return {
1056
+ projectKey,
1057
+ cases: cases.map((evosCase) => ({
1058
+ id: evosCase.id,
1059
+ title: evosCase.title,
1060
+ reviewState: evosCase.reviewState,
1061
+ })),
1062
+ };
1063
+ }),
1064
+ );
1065
+ await writeJson(
1066
+ paths.evosIndexPath,
1067
+ {
1068
+ schemaVersion: 1,
1069
+ updatedAt: input.batch.createdAt,
1070
+ projects: evosProjects,
1071
+ },
1072
+ { overwrite: true },
1073
+ );
1074
+
1075
+ const allRunProposals = await readJsonFiles(paths.repoProposalsDir, parseRepoProposal);
1076
+ await writeJson(
1077
+ paths.repoProposalsIndexPath,
1078
+ {
1079
+ schemaVersion: 1,
1080
+ projectKey: input.batch.projectKey,
1081
+ runId: input.batch.runId,
1082
+ updatedAt: input.batch.createdAt,
1083
+ proposals: allRunProposals.map((proposal) => ({
1084
+ id: proposal.id,
1085
+ kind: proposal.kind,
1086
+ title: proposal.title,
1087
+ reviewState: proposal.reviewState,
1088
+ })),
1089
+ },
1090
+ { overwrite: true },
1091
+ );
1092
+
1093
+ return {
1094
+ evidenceWindowPath: paths.evidenceWindowPath,
1095
+ batchPath: paths.batchPath,
1096
+ okf,
1097
+ evosCasePaths,
1098
+ repoProposalPaths,
1099
+ evosIndexPath: paths.evosIndexPath,
1100
+ };
1101
+ }
1102
+
894
1103
  export async function readEvolutionReviewSnapshot(input: {
895
1104
  homeDir: string;
896
1105
  projectKey?: string;
@@ -898,11 +1107,12 @@ export async function readEvolutionReviewSnapshot(input: {
898
1107
  const paths = resolveEvoDevPaths(input.homeDir);
899
1108
  const projectKeys =
900
1109
  input.projectKey === undefined
901
- ? await listDirectoryNames(paths.knowledgeDir)
1110
+ ? await listEvolutionReviewProjectKeys(paths)
902
1111
  : [sanitizeStorageId("projectKey", input.projectKey)];
903
1112
  const knowledgeRecords: EvolutionKnowledgeRecord[] = [];
904
1113
  const evosCases: EvolutionEvosCase[] = [];
905
1114
  const repoProposals: EvolutionRepoProposal[] = [];
1115
+ const reviewCandidates: EvolutionReviewCandidate[] = [];
906
1116
  const triggers: EvolutionTriggerRecord[] = [];
907
1117
 
908
1118
  for (const projectKey of projectKeys) {
@@ -927,7 +1137,9 @@ export async function readEvolutionReviewSnapshot(input: {
927
1137
  const runIds = await listDirectoryNames(projectStateDir);
928
1138
  for (const runId of runIds) {
929
1139
  const proposalsDir = join(projectStateDir, runId, "proposals");
1140
+ const reviewCandidatesDir = join(projectStateDir, runId, "review-candidates");
930
1141
  repoProposals.push(...(await readJsonFiles(proposalsDir, parseRepoProposal)));
1142
+ reviewCandidates.push(...(await readJsonFiles(reviewCandidatesDir, parseReviewCandidate)));
931
1143
  triggers.push(
932
1144
  ...(await readJsonFiles(join(projectStateDir, runId, "triggers"), parseTrigger)),
933
1145
  );
@@ -940,10 +1152,29 @@ export async function readEvolutionReviewSnapshot(input: {
940
1152
  knowledgeRecords,
941
1153
  evosCases,
942
1154
  repoProposals,
1155
+ reviewCandidates,
943
1156
  triggers,
944
1157
  };
945
1158
  }
946
1159
 
1160
+ async function listEvolutionReviewProjectKeys(
1161
+ paths: ReturnType<typeof resolveEvoDevPaths>,
1162
+ ): Promise<string[]> {
1163
+ const legacyKnowledgeProjectKeys = (
1164
+ await Promise.all(
1165
+ (
1166
+ await listDirectoryNames(paths.knowledgeDir)
1167
+ ).map(async (projectKey) =>
1168
+ (await pathExists(join(paths.knowledgeDir, projectKey, "records"))) ? projectKey : null,
1169
+ ),
1170
+ )
1171
+ ).filter((projectKey): projectKey is string => projectKey !== null);
1172
+ return uniqueSorted([
1173
+ ...legacyKnowledgeProjectKeys,
1174
+ ...(await listDirectoryNames(paths.evosCasesDir)),
1175
+ ]);
1176
+ }
1177
+
947
1178
  export async function listEvolutionKnowledgeRecords(input: {
948
1179
  homeDir: string;
949
1180
  projectKey?: string;
@@ -956,6 +1187,59 @@ export async function listEvolutionKnowledgeRecords(input: {
956
1187
  ).knowledgeRecords.sort((left, right) => left.id.localeCompare(right.id));
957
1188
  }
958
1189
 
1190
+ export async function listEvolutionEvosCases(input: {
1191
+ homeDir: string;
1192
+ projectKey?: string;
1193
+ roleId?: string;
1194
+ reviewStates?: EvolutionReviewState[];
1195
+ }): Promise<EvolutionEvosCaseQueryResult> {
1196
+ const paths = resolveEvoDevPaths(input.homeDir);
1197
+ const projectKeys =
1198
+ input.projectKey === undefined
1199
+ ? await listDirectoryNames(paths.evosCasesDir)
1200
+ : [sanitizeStorageId("projectKey", input.projectKey)];
1201
+ const roleId = input.roleId === undefined ? null : sanitizeId(input.roleId);
1202
+ const reviewStates = input.reviewStates ?? ["accepted", "auto-accepted"];
1203
+ const cases: EvolutionEvosCase[] = [];
1204
+ const warnings: string[] = [];
1205
+
1206
+ for (const projectKey of projectKeys) {
1207
+ const projectDir = join(paths.evosCasesDir, projectKey);
1208
+ if (!(await pathExists(projectDir))) continue;
1209
+ const entries = await readdir(projectDir, { withFileTypes: true });
1210
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
1211
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
1212
+ const sourceLink = `cases/${projectKey}/${entry.name}`;
1213
+ try {
1214
+ const value = JSON.parse(await readFile(join(projectDir, entry.name), "utf8")) as unknown;
1215
+ const evosCase = parseEvosCase(value);
1216
+ if (!reviewStates.includes(evosCase.reviewState)) continue;
1217
+ if (
1218
+ roleId !== null &&
1219
+ evosCase.roleTags.length > 0 &&
1220
+ !evosCase.roleTags.includes(roleId)
1221
+ ) {
1222
+ continue;
1223
+ }
1224
+ cases.push(evosCase);
1225
+ } catch {
1226
+ warnings.push(
1227
+ `Omitted unsafe or invalid evos case: ${sourceLink}. Run evodev knowledge lint.`,
1228
+ );
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+ return {
1234
+ cases: cases.sort((left, right) => {
1235
+ const project = left.projectKey.localeCompare(right.projectKey);
1236
+ if (project !== 0) return project;
1237
+ return left.id.localeCompare(right.id);
1238
+ }),
1239
+ warnings,
1240
+ };
1241
+ }
1242
+
959
1243
  export async function readEvolutionKnowledgeRecordById(input: {
960
1244
  homeDir: string;
961
1245
  knowledgeId: string;
@@ -1057,8 +1341,8 @@ export function formatEvolutionDistillationBatch(batch: EvolutionDistillationBat
1057
1341
  `Knowledge records: ${batch.knowledgeRecords.length}`,
1058
1342
  `Evos cases: ${batch.evosCases.length}`,
1059
1343
  `Repo proposals: ${batch.repoProposals.length}`,
1060
- "Default review state: auto-stored/unreviewed for knowledge and evos cases.",
1061
- "Runtime authority: contextual only; hardBlocking=false.",
1344
+ "Default batch review state: contextual until the OKF plan auto-accepts or queues review.",
1345
+ "Runtime authority: accepted/auto-accepted only; hardBlocking=false.",
1062
1346
  ...(batch.knowledgeRecords.length === 0
1063
1347
  ? ["Knowledge: none"]
1064
1348
  : [
@@ -1109,6 +1393,30 @@ export function formatEvolutionWriteResult(result: EvolutionWriteResult): string
1109
1393
  ].join("\n");
1110
1394
  }
1111
1395
 
1396
+ export function formatEvolutionActivationResult(result: EvolutionActivationResult): string {
1397
+ return [
1398
+ "EvoDev evolution activate",
1399
+ "",
1400
+ `Evidence window: ${result.evidenceWindowPath}`,
1401
+ `Distillation batch: ${result.batchPath}`,
1402
+ `OKF concepts: ${result.okf.conceptPaths.length}`,
1403
+ ...result.okf.conceptPaths.map((path) => ` - ${path}`),
1404
+ `OKF overlays: ${result.okf.overlayPaths.length}`,
1405
+ ...result.okf.overlayPaths.map((path) => ` - ${path}`),
1406
+ `Skipped candidates: ${result.okf.skippedCandidates.length}`,
1407
+ ...result.okf.skippedCandidates.map((id) => ` - ${id}`),
1408
+ `Needs human: ${result.okf.needsHumanCandidates.length}`,
1409
+ ...result.okf.needsHumanCandidates.map((id) => ` - ${id}`),
1410
+ `Evos cases: ${result.evosCasePaths.length}`,
1411
+ ...result.evosCasePaths.map((path) => ` - ${path}`),
1412
+ `Repo proposals: ${result.repoProposalPaths.length}`,
1413
+ ...result.repoProposalPaths.map((path) => ` - ${path}`),
1414
+ `OKF derived indexes: ${result.okf.derivedIndexPaths.length}`,
1415
+ ...result.okf.derivedIndexPaths.map((path) => ` - ${path}`),
1416
+ `Evos index: ${result.evosIndexPath}`,
1417
+ ].join("\n");
1418
+ }
1419
+
1112
1420
  export function formatEvolutionProcessResult(result: EvolutionProcessResult): string {
1113
1421
  return [
1114
1422
  "EvoDev evolution process",
@@ -1147,6 +1455,11 @@ export function formatEvolutionReviewSnapshot(snapshot: EvolutionReviewSnapshot)
1147
1455
  (proposal) =>
1148
1456
  ` - ${proposal.id} (${proposal.projectKey}, ${proposal.kind}, ${proposal.reviewState}, ${proposal.confidence}): ${proposal.title}`,
1149
1457
  ),
1458
+ `Review candidates: ${snapshot.reviewCandidates.length}`,
1459
+ ...snapshot.reviewCandidates.map(
1460
+ (candidate) =>
1461
+ ` - ${candidate.id} (${candidate.projectKey}, ${candidate.candidateKind}, ${candidate.reviewState}): ${candidate.title}`,
1462
+ ),
1150
1463
  `Triggers: ${snapshot.triggers.length}`,
1151
1464
  ...snapshot.triggers.map(
1152
1465
  (trigger) =>
@@ -1166,9 +1479,11 @@ export function resolveEvolutionPaths(input: {
1166
1479
  const evolutionStateDir = join(paths.stateDir, "evolution");
1167
1480
  const runStateDir = join(evolutionStateDir, projectKey, runId);
1168
1481
  const repoProposalsDir = join(runStateDir, "proposals");
1482
+ const reviewCandidatesDir = join(runStateDir, "review-candidates");
1169
1483
  const knowledgeProjectDir = join(paths.knowledgeDir, projectKey);
1170
1484
  const evosCasesProjectDir = join(paths.evosCasesDir, projectKey);
1171
1485
  assertPathDescendant(evolutionStateDir, runStateDir, "runStateDir");
1486
+ assertPathDescendant(evolutionStateDir, reviewCandidatesDir, "reviewCandidatesDir");
1172
1487
  assertPathDescendant(paths.knowledgeDir, knowledgeProjectDir, "knowledgeProjectDir");
1173
1488
  assertPathDescendant(paths.evosCasesDir, evosCasesProjectDir, "evosCasesProjectDir");
1174
1489
  return {
@@ -1179,6 +1494,7 @@ export function resolveEvolutionPaths(input: {
1179
1494
  evidenceWindowPath: join(runStateDir, "evidence-window.json"),
1180
1495
  batchPath: join(runStateDir, "distillation-batch.json"),
1181
1496
  triggersDir: join(runStateDir, "triggers"),
1497
+ reviewCandidatesDir,
1182
1498
  repoProposalsDir,
1183
1499
  repoProposalsIndexPath: join(runStateDir, "proposals.index.json"),
1184
1500
  knowledgeProjectDir,
@@ -1204,6 +1520,9 @@ export function validateEvolutionEvidenceWindow(window: EvolutionEvidenceWindow)
1204
1520
  assertString("sourceRefs.path", sourceRef.path);
1205
1521
  if (sourceRef.rawContentStored !== false)
1206
1522
  throw new Error("sourceRefs.rawContentStored must be false.");
1523
+ if (sourceRef.externalContentCopied !== false) {
1524
+ throw new Error("sourceRefs.externalContentCopied must be false.");
1525
+ }
1207
1526
  }
1208
1527
  if (!Array.isArray(window.events)) throw new Error("events must be an array.");
1209
1528
  for (const event of window.events) {
@@ -1246,7 +1565,15 @@ export function validateEvolutionKnowledgeRecord(record: EvolutionKnowledgeRecor
1246
1565
  assertEnum("kind", record.kind, KNOWLEDGE_KINDS);
1247
1566
  assertEnum("reviewState", record.reviewState, REVIEW_STATES);
1248
1567
  assertEnum("authority", record.authority, ["contextual", "reviewed"]);
1249
- if (record.reviewState === "auto-stored/unreviewed" && record.authority !== "contextual") {
1568
+ if (record.reviewState === "accepted" && record.authority !== "reviewed") {
1569
+ throw new Error("Accepted knowledge must use reviewed authority.");
1570
+ }
1571
+ if (
1572
+ (record.reviewState === "auto-stored/unreviewed" ||
1573
+ record.reviewState === "auto-accepted" ||
1574
+ record.reviewState === "needs-human") &&
1575
+ record.authority !== "contextual"
1576
+ ) {
1250
1577
  throw new Error("Unreviewed knowledge must remain contextual.");
1251
1578
  }
1252
1579
  if (record.runtime.hardBlocking !== false) {
@@ -1466,7 +1793,7 @@ export async function processEvolutionTriggers(
1466
1793
  warnings: analysis.warnings,
1467
1794
  now,
1468
1795
  });
1469
- await writeEvolutionDistillationBatch({
1796
+ await activateEvolutionDistillationBatch({
1470
1797
  homeDir: input.homeDir,
1471
1798
  batch,
1472
1799
  overwrite: true,
@@ -1597,6 +1924,57 @@ function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal): void {
1597
1924
  assertNoForbiddenRawFields(proposal);
1598
1925
  }
1599
1926
 
1927
+ function validateEvolutionReviewCandidate(candidate: EvolutionReviewCandidate): void {
1928
+ if (!isRecord(candidate)) throw new Error("Review candidate must be an object.");
1929
+ if (candidate.schemaVersion !== 1) {
1930
+ throw new Error("Review candidate schemaVersion must be 1.");
1931
+ }
1932
+ if (candidate.kind !== "evolution-review-candidate") {
1933
+ throw new Error("Review candidate kind is invalid.");
1934
+ }
1935
+ assertString("reviewCandidate.id", candidate.id);
1936
+ assertString("reviewCandidate.projectKey", candidate.projectKey);
1937
+ assertString("reviewCandidate.runId", candidate.runId);
1938
+ assertString("reviewCandidate.createdAt", candidate.createdAt);
1939
+ assertString("reviewCandidate.candidateKind", candidate.candidateKind);
1940
+ assertString("reviewCandidate.title", candidate.title);
1941
+ assertString("reviewCandidate.targetStore", candidate.targetStore);
1942
+ assertString("reviewCandidate.targetPath", candidate.targetPath);
1943
+ assertString("reviewCandidate.stableKey", candidate.stableKey);
1944
+ if (candidate.reviewState !== "needs-human") {
1945
+ throw new Error("Review candidate reviewState must be needs-human.");
1946
+ }
1947
+ if (!Array.isArray(candidate.reasons))
1948
+ throw new Error("Review candidate reasons must be an array.");
1949
+ for (const reason of candidate.reasons) assertString("reviewCandidate.reasons", reason);
1950
+ if (!isRecord(candidate.provenance))
1951
+ throw new Error("Review candidate provenance must be an object.");
1952
+ assertString("reviewCandidate.provenance.runId", candidate.provenance.runId);
1953
+ assertString(
1954
+ "reviewCandidate.provenance.evidenceWindowId",
1955
+ candidate.provenance.evidenceWindowId,
1956
+ );
1957
+ if (candidate.provenance.createdBy !== "evodev") {
1958
+ throw new Error("Review candidate provenance.createdBy must be evodev.");
1959
+ }
1960
+ if (!Array.isArray(candidate.provenance.evidenceRefs)) {
1961
+ throw new Error("Review candidate evidenceRefs must be an array.");
1962
+ }
1963
+ for (const evidenceRef of candidate.provenance.evidenceRefs) {
1964
+ assertString("reviewCandidate.provenance.evidenceRefs", evidenceRef);
1965
+ }
1966
+ if (
1967
+ candidate.provenance.rawLogsStored !== false ||
1968
+ candidate.provenance.rawPromptsStored !== false ||
1969
+ candidate.provenance.sourceDumpsStored !== false ||
1970
+ candidate.provenance.rawCommandOutputStored !== false
1971
+ ) {
1972
+ throw new Error("Review candidate provenance must remain metadata-only.");
1973
+ }
1974
+ assertPrivacy(candidate.privacy);
1975
+ assertNoForbiddenRawFields(candidate);
1976
+ }
1977
+
1600
1978
  function validateEvolutionTriggerRecord(trigger: EvolutionTriggerRecord): void {
1601
1979
  if (!isRecord(trigger)) throw new Error("Evolution trigger must be an object.");
1602
1980
  if (trigger.schemaVersion !== 1) throw new Error("Evolution trigger schemaVersion must be 1.");
@@ -1620,6 +1998,90 @@ function validateEvolutionTriggerRecord(trigger: EvolutionTriggerRecord): void {
1620
1998
  assertNoForbiddenRawFields(trigger);
1621
1999
  }
1622
2000
 
2001
+ async function readExecutionEvidenceEvents(input: {
2002
+ path: string;
2003
+ roleId: string;
2004
+ sourceRefId: string;
2005
+ existingCount: number;
2006
+ }): Promise<{ events: EvolutionEvidenceEvent[]; warnings: string[]; traceRefIds: string[] }> {
2007
+ const raw = await readFile(input.path, "utf8");
2008
+ const lines = raw.split("\n");
2009
+ const events: EvolutionEvidenceEvent[] = [];
2010
+ const warnings: string[] = [];
2011
+ const traceRefIds = new Set<string>();
2012
+
2013
+ for (let index = 0; index < lines.length; index += 1) {
2014
+ const line = lines[index]?.trim();
2015
+ if (line === undefined || line === "") continue;
2016
+
2017
+ try {
2018
+ const value = JSON.parse(line) as unknown;
2019
+ if (!isRecord(value) || value.kind !== "evodev-execution-event") continue;
2020
+ const codeAgentTraceRefId = optionalString(value.codeAgentTraceRefId);
2021
+ if (codeAgentTraceRefId !== null) traceRefIds.add(codeAgentTraceRefId);
2022
+ const event = createEvidenceEventFromExecutionEvent({
2023
+ executionEvent: value as unknown as EvoDevExecutionEventV1,
2024
+ roleId: input.roleId,
2025
+ sourceRefId: input.sourceRefId,
2026
+ lineNumber: index + 1,
2027
+ sequence: input.existingCount + events.length + 1,
2028
+ });
2029
+ if (event !== null) events.push(event);
2030
+ } catch {
2031
+ warnings.push(
2032
+ `Skipped malformed execution event JSON at ${basename(input.path)}:${index + 1}.`,
2033
+ );
2034
+ }
2035
+ }
2036
+
2037
+ return { events, warnings, traceRefIds: [...traceRefIds] };
2038
+ }
2039
+
2040
+ function createEvidenceEventFromExecutionEvent(input: {
2041
+ executionEvent: EvoDevExecutionEventV1;
2042
+ roleId: string;
2043
+ sourceRefId: string;
2044
+ lineNumber: number;
2045
+ sequence: number;
2046
+ }): EvolutionEvidenceEvent | null {
2047
+ const metadata = isRecord(input.executionEvent.metadata) ? input.executionEvent.metadata : {};
2048
+ const phase = optionalString(metadata.phase);
2049
+ const target = optionalString(input.executionEvent.target);
2050
+ const eventType = optionalString(input.executionEvent.eventType);
2051
+ const summarySource =
2052
+ optionalString(input.executionEvent.summary) ?? [target, phase].filter(Boolean).join(" ");
2053
+ if (summarySource.trim() === "") return null;
2054
+ const normalizedType = normalizeEvolutionEventType(eventType);
2055
+ const kind = classifyEvidenceEvent({
2056
+ error: null,
2057
+ summary: summarySource,
2058
+ target,
2059
+ eventType,
2060
+ phase,
2061
+ });
2062
+ const trigger = decideEvolutionTrigger({
2063
+ eventType: normalizedType,
2064
+ summary: summarySource,
2065
+ kind,
2066
+ });
2067
+
2068
+ return {
2069
+ id: `event-${String(input.sequence).padStart(4, "0")}`,
2070
+ kind,
2071
+ eventType: normalizedType,
2072
+ hookEventId: sanitizeOptionalId(input.executionEvent.eventId),
2073
+ occurredAt: optionalString(input.executionEvent.timestamp),
2074
+ summary: sanitizeText(summarySource),
2075
+ roleId: sanitizeOptionalId(input.executionEvent.roleId) ?? sanitizeId(input.roleId),
2076
+ taskId: sanitizeOptionalId(input.executionEvent.taskId),
2077
+ evidenceRef: `${input.sourceRefId}#L${input.lineNumber}`,
2078
+ episodeIds: [],
2079
+ triggerStrength: trigger.strength,
2080
+ triggerReason: trigger.reason,
2081
+ rawContentStored: false,
2082
+ };
2083
+ }
2084
+
1623
2085
  async function readTraceEvidenceEvents(input: {
1624
2086
  path: string;
1625
2087
  roleId: string;
@@ -2034,6 +2496,11 @@ function parseRepoProposal(value: unknown): EvolutionRepoProposal {
2034
2496
  return value as EvolutionRepoProposal;
2035
2497
  }
2036
2498
 
2499
+ function parseReviewCandidate(value: unknown): EvolutionReviewCandidate {
2500
+ validateEvolutionReviewCandidate(value as EvolutionReviewCandidate);
2501
+ return value as EvolutionReviewCandidate;
2502
+ }
2503
+
2037
2504
  function parseTrigger(value: unknown): EvolutionTriggerRecord {
2038
2505
  validateEvolutionTriggerRecord(value as EvolutionTriggerRecord);
2039
2506
  return value as EvolutionTriggerRecord;
@@ -2146,6 +2613,10 @@ async function listDirectoryNames(dir: string): Promise<string[]> {
2146
2613
  .sort();
2147
2614
  }
2148
2615
 
2616
+ function uniqueSorted(values: string[]): string[] {
2617
+ return [...new Set(values)].sort();
2618
+ }
2619
+
2149
2620
  async function writeJson(
2150
2621
  path: string,
2151
2622
  value: unknown,