@evo-dev/core 0.0.1-alpha.2 → 0.0.1-alpha.20

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 (79) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +5 -3
  2. package/assets/team/agents/code-reviewer.md +48 -0
  3. package/assets/team/agents/docs-maintainer.md +51 -0
  4. package/assets/team/agents/implementation-engineer.md +51 -0
  5. package/assets/team/agents/product-scope-analyst.md +58 -0
  6. package/assets/team/agents/release-engineer.md +55 -0
  7. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  8. package/assets/team/agents/solution-architect.md +51 -0
  9. package/assets/team/agents/verification-engineer.md +51 -0
  10. package/assets/team/team.md +102 -0
  11. package/dist/assets/index.js +5 -5
  12. package/dist/config/index.js +793 -241
  13. package/dist/index.js +20840 -12908
  14. package/dist/plugins/index.js +13 -13
  15. package/package.json +1 -1
  16. package/src/agents/index.ts +1 -265
  17. package/src/code-agent-traces/index.ts +11 -12
  18. package/src/config/index.ts +2 -0
  19. package/src/config/settings.ts +116 -7
  20. package/src/config/store.ts +1 -1
  21. package/src/daemon/index.ts +1 -41
  22. package/src/evolution/candidates/index.ts +730 -0
  23. package/src/evolution/control/index.ts +20 -0
  24. package/src/evolution/evidence/analysis.ts +533 -0
  25. package/src/evolution/evidence/index.ts +3 -0
  26. package/src/evolution/evidence/session-memory/analysis.ts +287 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +9 -0
  29. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  30. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  31. package/src/evolution/evidence/session-memory/retention.ts +643 -0
  32. package/src/evolution/evidence/session-memory/segment.ts +216 -0
  33. package/src/evolution/evidence/session-memory/semantic-packet.ts +408 -0
  34. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  35. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  36. package/src/evolution/evidence/session-memory/storage.ts +744 -0
  37. package/src/evolution/evidence/session-memory/types.ts +296 -0
  38. package/src/evolution/evidence/session-memory/updater.ts +199 -0
  39. package/src/evolution/formatters.ts +169 -0
  40. package/src/evolution/imports/apply.ts +435 -0
  41. package/src/evolution/imports/diff.ts +472 -0
  42. package/src/evolution/imports/index.ts +7 -0
  43. package/src/evolution/imports/materialize.ts +640 -0
  44. package/src/evolution/imports/paths.ts +129 -0
  45. package/src/evolution/imports/stage.ts +414 -0
  46. package/src/evolution/imports/storage.ts +952 -0
  47. package/src/evolution/imports/types.ts +226 -0
  48. package/src/evolution/index.ts +19 -2827
  49. package/src/evolution/knowledge/change-store.ts +558 -0
  50. package/src/evolution/knowledge/changes.ts +459 -0
  51. package/src/evolution/knowledge/freshness.ts +69 -0
  52. package/src/{knowledge → evolution/knowledge}/index.ts +1532 -206
  53. package/src/evolution/knowledge/review.ts +446 -0
  54. package/src/evolution/knowledge/support.ts +135 -0
  55. package/src/evolution/paths.ts +44 -0
  56. package/src/evolution/processor/distillation.ts +518 -0
  57. package/src/evolution/processor/index.ts +3 -0
  58. package/src/evolution/processor/process.ts +594 -0
  59. package/src/{learning → evolution/review}/index.ts +10 -14
  60. package/src/evolution/schema.ts +639 -0
  61. package/src/evolution/shared.ts +1053 -0
  62. package/src/evolution/triggers/classification.ts +102 -0
  63. package/src/evolution/triggers/index.ts +295 -0
  64. package/src/hooks/index.ts +281 -197
  65. package/src/index.ts +15 -4
  66. package/src/projects/index.ts +934 -0
  67. package/src/runtime-logs/index.ts +100 -13
  68. package/src/team/index.ts +582 -3
  69. package/src/utils/errors.ts +13 -0
  70. package/src/utils/fs.ts +40 -0
  71. package/src/utils/hash.ts +9 -0
  72. package/src/utils/ids.ts +12 -0
  73. package/src/utils/index.ts +7 -0
  74. package/src/utils/parsing.ts +11 -0
  75. package/src/utils/text.ts +18 -0
  76. package/src/utils/time.ts +5 -0
  77. package/src/workflow/index.ts +3 -21
  78. package/src/project/index.ts +0 -507
  79. package/src/task/index.ts +0 -840
@@ -0,0 +1,730 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { resolveEvoDevPaths } from "../../config/paths.ts";
5
+ import { pathExists, readJsonFiles, writeJsonFile as writeJson } from "../../utils/index.ts";
6
+ import { resolveEvolutionPaths } from "../paths.ts";
7
+ import type {
8
+ EvolutionEvosCase,
9
+ EvolutionEvosCaseQueryResult,
10
+ EvolutionImprovementEvalResult,
11
+ EvolutionImprovementEvalStatus,
12
+ EvolutionKnowledgeRecord,
13
+ EvolutionKnowledgeReviewHistoryRecord,
14
+ EvolutionRepoProposal,
15
+ EvolutionReviewCandidate,
16
+ EvolutionReviewSnapshot,
17
+ EvolutionReviewState,
18
+ EvolutionTriggerRecord,
19
+ } from "../schema.ts";
20
+ import {
21
+ REVIEW_STATES,
22
+ createEvolutionImprovementEval,
23
+ hasConcreteRepoProposalChanges,
24
+ isEvolutionImprovementEvalRecommended,
25
+ listDirectoryNames,
26
+ parseEvosCase,
27
+ parseKnowledgeRecord,
28
+ parseRepoProposal,
29
+ parseReviewCandidate,
30
+ parseTrigger,
31
+ sanitizeId,
32
+ sanitizeStorageId,
33
+ sanitizeText,
34
+ uniqueSorted,
35
+ validateEvolutionKnowledgeRecord,
36
+ validateEvolutionRepoProposal,
37
+ } from "../shared.ts";
38
+
39
+ const REVIEW_LOCK_WAIT_MS = 2_000;
40
+ const REVIEW_LOCK_RETRY_MS = 10;
41
+
42
+ export async function withEvolutionReviewDecisionLock<T>(
43
+ input: {
44
+ homeDir: string;
45
+ kind: "learning" | "knowledge" | "repo-proposal";
46
+ itemId: string;
47
+ },
48
+ operation: () => Promise<T>,
49
+ ): Promise<T> {
50
+ const paths = resolveEvoDevPaths(input.homeDir);
51
+ const key = createHash("sha256").update(`${input.kind}\0${input.itemId}`).digest("hex");
52
+ const lockPath = join(paths.stateDir, "evolution", ".review-locks", `${key}.lock`);
53
+ const deadline = Date.now() + REVIEW_LOCK_WAIT_MS;
54
+ await mkdir(dirname(lockPath), { recursive: true });
55
+
56
+ while (true) {
57
+ try {
58
+ await writeFile(
59
+ lockPath,
60
+ `${JSON.stringify({ version: 1, kind: input.kind, acquiredAt: new Date().toISOString(), pid: process.pid })}\n`,
61
+ { encoding: "utf8", flag: "wx" },
62
+ );
63
+ break;
64
+ } catch (error) {
65
+ if (!isAlreadyExistsError(error)) throw error;
66
+ if (Date.now() >= deadline) throw new Error("Review decision lock is busy or stale.");
67
+ await sleep(REVIEW_LOCK_RETRY_MS);
68
+ }
69
+ }
70
+
71
+ try {
72
+ return await operation();
73
+ } finally {
74
+ await rm(lockPath, { force: true });
75
+ }
76
+ }
77
+
78
+ export async function readEvolutionReviewSnapshot(input: {
79
+ homeDir: string;
80
+ projectKey?: string;
81
+ }): Promise<EvolutionReviewSnapshot> {
82
+ const paths = resolveEvoDevPaths(input.homeDir);
83
+ const projectKeys =
84
+ input.projectKey === undefined
85
+ ? await listEvolutionReviewProjectKeys(paths)
86
+ : [sanitizeStorageId("projectKey", input.projectKey)];
87
+ const knowledgeRecords: EvolutionKnowledgeRecord[] = [];
88
+ const evosCases: EvolutionEvosCase[] = [];
89
+ const repoProposals: EvolutionRepoProposal[] = [];
90
+ const reviewCandidates: EvolutionReviewCandidate[] = [];
91
+ const triggers: EvolutionTriggerRecord[] = [];
92
+
93
+ for (const projectKey of projectKeys) {
94
+ const resolved = resolveEvolutionPaths({
95
+ homeDir: input.homeDir,
96
+ projectKey,
97
+ runId: "review",
98
+ });
99
+ knowledgeRecords.push(
100
+ ...(await readJsonFiles(resolved.knowledgeRecordsDir, parseKnowledgeRecord)),
101
+ );
102
+ evosCases.push(...(await readJsonFiles(resolved.evosCasesProjectDir, parseEvosCase)));
103
+ }
104
+
105
+ const evolutionStateDir = join(paths.stateDir, "evolution");
106
+ const stateProjectKeys =
107
+ input.projectKey === undefined
108
+ ? await listDirectoryNames(evolutionStateDir)
109
+ : [sanitizeStorageId("projectKey", input.projectKey)];
110
+ for (const projectKey of stateProjectKeys) {
111
+ const projectStateDir = join(evolutionStateDir, projectKey);
112
+ const runIds = await listDirectoryNames(projectStateDir);
113
+ for (const runId of runIds) {
114
+ const proposalsDir = join(projectStateDir, runId, "proposals");
115
+ const reviewCandidatesDir = join(projectStateDir, runId, "review-candidates");
116
+ repoProposals.push(...(await readJsonFiles(proposalsDir, parseRepoProposal)));
117
+ reviewCandidates.push(...(await readJsonFiles(reviewCandidatesDir, parseReviewCandidate)));
118
+ triggers.push(
119
+ ...(await readJsonFiles(join(projectStateDir, runId, "triggers"), parseTrigger)),
120
+ );
121
+ }
122
+ }
123
+
124
+ return {
125
+ projectKey:
126
+ input.projectKey === undefined ? null : sanitizeStorageId("projectKey", input.projectKey),
127
+ knowledgeRecords,
128
+ evosCases,
129
+ repoProposals,
130
+ reviewCandidates,
131
+ triggers,
132
+ };
133
+ }
134
+
135
+ async function listEvolutionReviewProjectKeys(
136
+ paths: ReturnType<typeof resolveEvoDevPaths>,
137
+ ): Promise<string[]> {
138
+ const legacyKnowledgeProjectKeys = (
139
+ await Promise.all(
140
+ (
141
+ await listDirectoryNames(paths.knowledgeDir)
142
+ ).map(async (projectKey) =>
143
+ (await pathExists(join(paths.knowledgeDir, projectKey, "records"))) ? projectKey : null,
144
+ ),
145
+ )
146
+ ).filter((projectKey): projectKey is string => projectKey !== null);
147
+ return uniqueSorted([
148
+ ...legacyKnowledgeProjectKeys,
149
+ ...(await listDirectoryNames(paths.evosCasesDir)),
150
+ ]);
151
+ }
152
+
153
+ export async function listEvolutionKnowledgeRecords(input: {
154
+ homeDir: string;
155
+ projectKey?: string;
156
+ }): Promise<EvolutionKnowledgeRecord[]> {
157
+ return (
158
+ await readEvolutionReviewSnapshot({
159
+ homeDir: input.homeDir,
160
+ projectKey: input.projectKey,
161
+ })
162
+ ).knowledgeRecords.sort((left, right) => left.id.localeCompare(right.id));
163
+ }
164
+
165
+ export async function listEvolutionKnowledgeReviewHistory(input: {
166
+ homeDir: string;
167
+ projectKey?: string;
168
+ }): Promise<EvolutionKnowledgeReviewHistoryRecord[]> {
169
+ const paths = resolveEvoDevPaths(input.homeDir);
170
+ const projectKeys =
171
+ input.projectKey === undefined
172
+ ? await listDirectoryNames(join(paths.stateDir, "evolution"))
173
+ : [sanitizeStorageId("projectKey", input.projectKey)];
174
+ const records: EvolutionKnowledgeReviewHistoryRecord[] = [];
175
+
176
+ for (const projectKey of projectKeys) {
177
+ const historyPath = resolveEvolutionPaths({
178
+ homeDir: input.homeDir,
179
+ projectKey,
180
+ runId: "review",
181
+ }).knowledgeReviewHistoryPath;
182
+ if (!(await pathExists(historyPath))) continue;
183
+ for (const line of (await readFile(historyPath, "utf8")).split("\n")) {
184
+ if (line.trim() === "") continue;
185
+ records.push(parseKnowledgeReviewHistoryRecord(JSON.parse(line) as unknown));
186
+ }
187
+ }
188
+
189
+ return records.sort((left, right) => {
190
+ const changedAt = left.changedAt.localeCompare(right.changedAt);
191
+ if (changedAt !== 0) return changedAt;
192
+ return left.knowledgeId.localeCompare(right.knowledgeId);
193
+ });
194
+ }
195
+
196
+ export async function listEvolutionEvosCases(input: {
197
+ homeDir: string;
198
+ projectKey?: string;
199
+ roleId?: string;
200
+ reviewStates?: EvolutionReviewState[];
201
+ }): Promise<EvolutionEvosCaseQueryResult> {
202
+ const paths = resolveEvoDevPaths(input.homeDir);
203
+ const projectKeys =
204
+ input.projectKey === undefined
205
+ ? await listDirectoryNames(paths.evosCasesDir)
206
+ : [sanitizeStorageId("projectKey", input.projectKey)];
207
+ const roleId = input.roleId === undefined ? null : sanitizeId(input.roleId);
208
+ const reviewStates = input.reviewStates ?? ["accepted", "auto-accepted"];
209
+ const cases: EvolutionEvosCase[] = [];
210
+ const warnings: string[] = [];
211
+
212
+ for (const projectKey of projectKeys) {
213
+ const projectDir = join(paths.evosCasesDir, projectKey);
214
+ if (!(await pathExists(projectDir))) continue;
215
+ const entries = await readdir(projectDir, { withFileTypes: true });
216
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
217
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
218
+ const sourceLink = `cases/${projectKey}/${entry.name}`;
219
+ try {
220
+ const value = JSON.parse(await readFile(join(projectDir, entry.name), "utf8")) as unknown;
221
+ const evosCase = parseEvosCase(value);
222
+ if (!reviewStates.includes(evosCase.reviewState)) continue;
223
+ if (
224
+ roleId !== null &&
225
+ evosCase.roleTags.length > 0 &&
226
+ !evosCase.roleTags.includes(roleId)
227
+ ) {
228
+ continue;
229
+ }
230
+ cases.push(evosCase);
231
+ } catch {
232
+ warnings.push(
233
+ `Omitted unsafe or invalid evos case: ${sourceLink}. Run evodev knowledge lint.`,
234
+ );
235
+ }
236
+ }
237
+ }
238
+
239
+ return {
240
+ cases: cases.sort((left, right) => {
241
+ const project = left.projectKey.localeCompare(right.projectKey);
242
+ if (project !== 0) return project;
243
+ return left.id.localeCompare(right.id);
244
+ }),
245
+ warnings,
246
+ };
247
+ }
248
+
249
+ export async function readEvolutionKnowledgeRecordById(input: {
250
+ homeDir: string;
251
+ knowledgeId: string;
252
+ projectKey?: string;
253
+ }): Promise<EvolutionKnowledgeRecord> {
254
+ const matches = (await listEvolutionKnowledgeRecords(input)).filter(
255
+ (record) => record.id === input.knowledgeId,
256
+ );
257
+ if (matches.length === 0) throw new Error(`Knowledge record not found: ${input.knowledgeId}`);
258
+ if (matches.length > 1) {
259
+ throw new Error(`Knowledge record id is ambiguous across projects: ${input.knowledgeId}`);
260
+ }
261
+ return matches[0] as EvolutionKnowledgeRecord;
262
+ }
263
+
264
+ export async function updateEvolutionKnowledgeReviewState(input: {
265
+ homeDir: string;
266
+ knowledgeId: string;
267
+ projectKey?: string;
268
+ reviewState: "accepted" | "rejected" | "deferred";
269
+ expectedReviewState?: EvolutionReviewState;
270
+ now?: string | Date;
271
+ }): Promise<{ path: string; record: EvolutionKnowledgeRecord; changed: boolean }> {
272
+ return await withEvolutionReviewDecisionLock(
273
+ { homeDir: input.homeDir, kind: "knowledge", itemId: input.knowledgeId },
274
+ async () => {
275
+ const record = await readEvolutionKnowledgeRecordById(input);
276
+ if (
277
+ input.expectedReviewState !== undefined &&
278
+ record.reviewState !== input.expectedReviewState
279
+ ) {
280
+ throw new Error("Knowledge review state changed before the decision was applied.");
281
+ }
282
+ const paths = resolveEvolutionPaths({
283
+ homeDir: input.homeDir,
284
+ projectKey: record.projectKey,
285
+ runId: "review",
286
+ });
287
+ const path = join(paths.knowledgeRecordsDir, `${record.id}.json`);
288
+ if (record.reviewState === input.reviewState) return { path, record, changed: false };
289
+
290
+ const changedAt = normalizeTimestamp(input.now);
291
+ const next: EvolutionKnowledgeRecord = {
292
+ ...record,
293
+ reviewState: input.reviewState,
294
+ authority: input.reviewState === "accepted" ? "reviewed" : "contextual",
295
+ runtime: {
296
+ ...record.runtime,
297
+ // Legacy JSON review records are not active OKF concepts.
298
+ canLoad: false,
299
+ hardBlocking: false,
300
+ },
301
+ };
302
+ validateEvolutionKnowledgeRecord(next);
303
+ await writeJson(path, next, { overwrite: true });
304
+ const allProjectKnowledgeRecords = await readJsonFiles(
305
+ paths.knowledgeRecordsDir,
306
+ parseKnowledgeRecord,
307
+ );
308
+ await writeJson(
309
+ paths.knowledgeIndexPath,
310
+ {
311
+ version: 1,
312
+ kind: "evolution-knowledge-index",
313
+ projectKey: next.projectKey,
314
+ updatedAt: changedAt,
315
+ records: allProjectKnowledgeRecords.map((item) => ({
316
+ id: item.id,
317
+ kind: item.kind,
318
+ title: item.title,
319
+ roleTags: item.roleTags,
320
+ tags: item.tags,
321
+ reviewState: item.reviewState,
322
+ authority: item.authority,
323
+ confidence: item.confidence,
324
+ runtime: item.runtime,
325
+ })),
326
+ },
327
+ { overwrite: true },
328
+ );
329
+ const history: EvolutionKnowledgeReviewHistoryRecord = {
330
+ version: 1,
331
+ kind: "evolution-knowledge-review-state-changed",
332
+ knowledgeId: next.id,
333
+ projectKey: next.projectKey,
334
+ roleTags: uniqueSorted(next.roleTags),
335
+ artifactCreatedAt: next.provenance.createdAt,
336
+ previousReviewState: record.reviewState,
337
+ nextReviewState: next.reviewState,
338
+ changedAt,
339
+ metadataOnly: true,
340
+ };
341
+ await mkdir(dirname(paths.knowledgeReviewHistoryPath), { recursive: true });
342
+ await writeFile(paths.knowledgeReviewHistoryPath, `${JSON.stringify(history)}\n`, {
343
+ encoding: "utf8",
344
+ flag: "a",
345
+ });
346
+ return { path, record: next, changed: true };
347
+ },
348
+ );
349
+ }
350
+
351
+ export async function readEvolutionRepoProposalById(input: {
352
+ homeDir: string;
353
+ proposalId: string;
354
+ projectKey?: string;
355
+ }): Promise<EvolutionRepoProposal> {
356
+ const matches = (
357
+ await readEvolutionReviewSnapshot({
358
+ homeDir: input.homeDir,
359
+ projectKey: input.projectKey,
360
+ })
361
+ ).repoProposals.filter((proposal) => proposal.id === input.proposalId);
362
+ if (matches.length === 0) throw new Error(`Repo proposal not found: ${input.proposalId}`);
363
+ if (matches.length > 1) {
364
+ throw new Error(`Repo proposal id is ambiguous across runs: ${input.proposalId}`);
365
+ }
366
+ return matches[0] as EvolutionRepoProposal;
367
+ }
368
+
369
+ export async function updateEvolutionRepoProposalReviewState(input: {
370
+ homeDir: string;
371
+ proposalId: string;
372
+ projectKey?: string;
373
+ reviewState: "accepted" | "rejected" | "deferred";
374
+ expectedReviewState?: EvolutionRepoProposal["reviewState"];
375
+ improvementEvalRequested?: boolean;
376
+ reason?: string;
377
+ now?: string | Date;
378
+ }): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
379
+ return await withEvolutionReviewDecisionLock(
380
+ { homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
381
+ async () => {
382
+ const record = await readEvolutionRepoProposalById(input);
383
+ if (!hasConcreteRepoProposalChanges(record)) {
384
+ throw new Error("Repo proposal has no concrete repository changes to review.");
385
+ }
386
+ const reason = input.reason?.trim();
387
+ if (input.reviewState === "rejected" && reason === undefined) {
388
+ throw new Error("Rejecting a repo proposal requires a reason.");
389
+ }
390
+ if (reason !== undefined && (reason === "" || reason.length > 500)) {
391
+ throw new Error("Repo proposal review reason must be 1-500 characters.");
392
+ }
393
+ if (
394
+ input.expectedReviewState !== undefined &&
395
+ record.reviewState !== input.expectedReviewState
396
+ ) {
397
+ throw new Error("Repo proposal review state changed before the decision was applied.");
398
+ }
399
+ const paths = resolveEvolutionPaths({
400
+ homeDir: input.homeDir,
401
+ projectKey: record.projectKey,
402
+ runId: record.provenance.runId,
403
+ });
404
+ const path = join(paths.repoProposalsDir, `${record.id}.json`);
405
+ if (record.reviewState === input.reviewState) return { path, record, changed: false };
406
+
407
+ const changedAt = normalizeTimestamp(input.now);
408
+ const improvementEval =
409
+ input.reviewState === "accepted"
410
+ ? createEvolutionImprovementEval({
411
+ kind: record.kind,
412
+ requested:
413
+ input.improvementEvalRequested ??
414
+ isEvolutionImprovementEvalRecommended(record.kind),
415
+ now: changedAt,
416
+ })
417
+ : createEvolutionImprovementEval({
418
+ kind: record.kind,
419
+ requested: false,
420
+ now: changedAt,
421
+ });
422
+ const next: EvolutionRepoProposal = {
423
+ ...record,
424
+ reviewState: input.reviewState,
425
+ reviewStateChangedAt: changedAt,
426
+ improvementEval,
427
+ lastDecision: {
428
+ state: input.reviewState,
429
+ reason: reason === undefined ? null : sanitizeText(reason),
430
+ decidedAt: changedAt,
431
+ },
432
+ };
433
+ validateEvolutionRepoProposal(next);
434
+ await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
435
+ return { path, record: next, changed: true };
436
+ },
437
+ );
438
+ }
439
+
440
+ export async function supersedeEvolutionRepoProposal(input: {
441
+ homeDir: string;
442
+ proposalId: string;
443
+ projectKey?: string;
444
+ expectedReviewState?: EvolutionRepoProposal["reviewState"];
445
+ reason: string;
446
+ now?: string | Date;
447
+ }): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
448
+ return await withEvolutionReviewDecisionLock(
449
+ { homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
450
+ async () => {
451
+ const record = await readEvolutionRepoProposalById(input);
452
+ const paths = resolveEvolutionPaths({
453
+ homeDir: input.homeDir,
454
+ projectKey: record.projectKey,
455
+ runId: record.provenance.runId,
456
+ });
457
+ const path = join(paths.repoProposalsDir, `${record.id}.json`);
458
+ if (record.reviewState === "superseded") return { path, record, changed: false };
459
+ if (
460
+ input.expectedReviewState !== undefined &&
461
+ record.reviewState !== input.expectedReviewState
462
+ ) {
463
+ throw new Error("Repo proposal review state changed before supersession was applied.");
464
+ }
465
+ if (record.reviewState !== "pending" && record.reviewState !== "deferred") {
466
+ throw new Error("Only pending or deferred repo proposals can be superseded.");
467
+ }
468
+ const reason = input.reason.trim();
469
+ if (reason === "" || reason.length > 500) {
470
+ throw new Error("Repo proposal supersession reason must be 1-500 characters.");
471
+ }
472
+ const changedAt = normalizeTimestamp(input.now);
473
+ const next: EvolutionRepoProposal = {
474
+ ...record,
475
+ reviewState: "superseded",
476
+ reviewStateChangedAt: changedAt,
477
+ supersession: {
478
+ policy: "session-proposal-v2",
479
+ reason: sanitizeText(reason),
480
+ supersededAt: changedAt,
481
+ },
482
+ };
483
+ validateEvolutionRepoProposal(next);
484
+ await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
485
+ return { path, record: next, changed: true };
486
+ },
487
+ );
488
+ }
489
+
490
+ export async function updateEvolutionRepoProposalImprovementEval(input: {
491
+ homeDir: string;
492
+ proposalId: string;
493
+ projectKey?: string;
494
+ status: Exclude<EvolutionImprovementEvalStatus, "not-requested" | "awaiting-eval">;
495
+ expectedStatus?: EvolutionImprovementEvalStatus;
496
+ evidenceRef?: string | null;
497
+ result?: Omit<EvolutionImprovementEvalResult, "retention">;
498
+ now?: string | Date;
499
+ }): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
500
+ return await withEvolutionReviewDecisionLock(
501
+ { homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
502
+ async () => {
503
+ const record = await readEvolutionRepoProposalById(input);
504
+ if (record.reviewState !== "accepted") {
505
+ throw new Error("Only accepted repo proposals can be evaluated.");
506
+ }
507
+ const current = record.improvementEval;
508
+ if (current === undefined || !current.requested) {
509
+ throw new Error("Repo proposal improvement evaluation was not requested.");
510
+ }
511
+ if (input.expectedStatus !== undefined && current.status !== input.expectedStatus) {
512
+ throw new Error("Repo proposal improvement evaluation state changed before update.");
513
+ }
514
+ if (
515
+ input.status !== "evaluating" &&
516
+ input.status !== "effective" &&
517
+ input.status !== "ineffective" &&
518
+ input.status !== "inconclusive"
519
+ ) {
520
+ throw new Error("Repo proposal improvement evaluation state is invalid.");
521
+ }
522
+ if (
523
+ input.status === "evaluating" &&
524
+ current.status !== "awaiting-eval" &&
525
+ current.status !== "evaluating"
526
+ ) {
527
+ throw new Error("Only an awaiting improvement evaluation can start.");
528
+ }
529
+ if (input.status !== "evaluating" && current.status !== "evaluating") {
530
+ throw new Error("Only a running improvement evaluation can complete.");
531
+ }
532
+ const completed = input.status !== "evaluating";
533
+ if (completed !== (input.result !== undefined)) {
534
+ throw new Error("Completed improvement evaluation requires a result.");
535
+ }
536
+ const changedAt = normalizeTimestamp(input.now);
537
+ const result: EvolutionImprovementEvalResult | null =
538
+ input.result === undefined
539
+ ? null
540
+ : {
541
+ targetImproved: input.result.targetImproved,
542
+ hardRegression: input.result.hardRegression,
543
+ summary: sanitizeText(input.result.summary),
544
+ assertions: input.result.assertions.slice(0, 8).map((assertion) => ({
545
+ label: sanitizeText(assertion.label),
546
+ status: assertion.status,
547
+ })),
548
+ evaluatedBy: input.result.evaluatedBy,
549
+ metrics: { ...input.result.metrics },
550
+ retention: {
551
+ metadataOnly: true,
552
+ replayInputStored: false,
553
+ replayOutputStored: false,
554
+ },
555
+ };
556
+ const evidenceRef =
557
+ input.evidenceRef === undefined
558
+ ? current.evidenceRef
559
+ : input.evidenceRef === null
560
+ ? null
561
+ : sanitizeId(input.evidenceRef);
562
+ const next: EvolutionRepoProposal = {
563
+ ...record,
564
+ improvementEval: {
565
+ ...current,
566
+ status: input.status,
567
+ evaluatedAt: completed ? changedAt : null,
568
+ evidenceRef,
569
+ result,
570
+ },
571
+ };
572
+ validateEvolutionRepoProposal(next);
573
+ const paths = resolveEvolutionPaths({
574
+ homeDir: input.homeDir,
575
+ projectKey: record.projectKey,
576
+ runId: record.provenance.runId,
577
+ });
578
+ const path = join(paths.repoProposalsDir, `${record.id}.json`);
579
+ await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
580
+ return { path, record: next, changed: true };
581
+ },
582
+ );
583
+ }
584
+
585
+ export async function markEvolutionRepoProposalApplied(input: {
586
+ homeDir: string;
587
+ proposalId: string;
588
+ projectKey?: string;
589
+ expectedReviewState?: EvolutionRepoProposal["reviewState"];
590
+ now?: string | Date;
591
+ }): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
592
+ return await withEvolutionReviewDecisionLock(
593
+ { homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
594
+ async () => {
595
+ const record = await readEvolutionRepoProposalById(input);
596
+ if (!hasConcreteRepoProposalChanges(record)) {
597
+ throw new Error("Repo proposal has no concrete repository changes to apply.");
598
+ }
599
+ if (
600
+ input.expectedReviewState !== undefined &&
601
+ record.reviewState !== input.expectedReviewState
602
+ ) {
603
+ throw new Error("Repo proposal review state changed before it was marked applied.");
604
+ }
605
+ const paths = resolveEvolutionPaths({
606
+ homeDir: input.homeDir,
607
+ projectKey: record.projectKey,
608
+ runId: record.provenance.runId,
609
+ });
610
+ const path = join(paths.repoProposalsDir, `${record.id}.json`);
611
+ if (record.reviewState === "applied") return { path, record, changed: false };
612
+ if (record.reviewState !== "accepted") {
613
+ throw new Error("Only accepted repo proposals can be marked applied.");
614
+ }
615
+
616
+ const changedAt = normalizeTimestamp(input.now);
617
+ const next: EvolutionRepoProposal = {
618
+ ...record,
619
+ reviewState: "applied",
620
+ reviewStateChangedAt: changedAt,
621
+ };
622
+ validateEvolutionRepoProposal(next);
623
+ await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
624
+ return { path, record: next, changed: true };
625
+ },
626
+ );
627
+ }
628
+
629
+ async function writeRepoProposalAndIndex(input: {
630
+ homeDir: string;
631
+ proposal: EvolutionRepoProposal;
632
+ changedAt: string;
633
+ }): Promise<void> {
634
+ const paths = resolveEvolutionPaths({
635
+ homeDir: input.homeDir,
636
+ projectKey: input.proposal.projectKey,
637
+ runId: input.proposal.provenance.runId,
638
+ });
639
+ await writeJson(join(paths.repoProposalsDir, `${input.proposal.id}.json`), input.proposal, {
640
+ overwrite: true,
641
+ });
642
+ const allRunProposals = await readJsonFiles(paths.repoProposalsDir, parseRepoProposal);
643
+ await writeJson(
644
+ paths.repoProposalsIndexPath,
645
+ {
646
+ schemaVersion: 1,
647
+ projectKey: input.proposal.projectKey,
648
+ runId: input.proposal.provenance.runId,
649
+ updatedAt: input.changedAt,
650
+ proposals: allRunProposals.map((proposal) => ({
651
+ id: proposal.id,
652
+ kind: proposal.kind,
653
+ title: proposal.title,
654
+ reviewState: proposal.reviewState,
655
+ improvementEvalStatus: proposal.improvementEval?.status ?? "not-requested",
656
+ })),
657
+ },
658
+ { overwrite: true },
659
+ );
660
+ }
661
+
662
+ function parseKnowledgeReviewHistoryRecord(value: unknown): EvolutionKnowledgeReviewHistoryRecord {
663
+ if (!isRecord(value)) throw new Error("Knowledge review history record must be an object.");
664
+ const allowedKeys = new Set([
665
+ "version",
666
+ "kind",
667
+ "knowledgeId",
668
+ "projectKey",
669
+ "roleTags",
670
+ "artifactCreatedAt",
671
+ "previousReviewState",
672
+ "nextReviewState",
673
+ "changedAt",
674
+ "metadataOnly",
675
+ ]);
676
+ if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
677
+ throw new Error("Knowledge review history record contains unsupported fields.");
678
+ }
679
+ if (
680
+ value.version !== 1 ||
681
+ value.kind !== "evolution-knowledge-review-state-changed" ||
682
+ typeof value.knowledgeId !== "string" ||
683
+ typeof value.projectKey !== "string" ||
684
+ !Array.isArray(value.roleTags) ||
685
+ !value.roleTags.every((roleId) => typeof roleId === "string") ||
686
+ typeof value.artifactCreatedAt !== "string" ||
687
+ typeof value.previousReviewState !== "string" ||
688
+ !REVIEW_STATES.includes(value.previousReviewState as EvolutionReviewState) ||
689
+ typeof value.nextReviewState !== "string" ||
690
+ !REVIEW_STATES.includes(value.nextReviewState as EvolutionReviewState) ||
691
+ typeof value.changedAt !== "string" ||
692
+ value.metadataOnly !== true
693
+ ) {
694
+ throw new Error("Invalid knowledge review history record.");
695
+ }
696
+ normalizeTimestamp(value.artifactCreatedAt);
697
+ normalizeTimestamp(value.changedAt);
698
+ return {
699
+ version: 1,
700
+ kind: "evolution-knowledge-review-state-changed",
701
+ knowledgeId: sanitizeId(value.knowledgeId),
702
+ projectKey: sanitizeStorageId("projectKey", value.projectKey),
703
+ roleTags: uniqueSorted(value.roleTags.map(sanitizeId)),
704
+ artifactCreatedAt: value.artifactCreatedAt,
705
+ previousReviewState: value.previousReviewState as EvolutionReviewState,
706
+ nextReviewState: value.nextReviewState as EvolutionReviewState,
707
+ changedAt: value.changedAt,
708
+ metadataOnly: true,
709
+ };
710
+ }
711
+
712
+ function normalizeTimestamp(value: string | Date | undefined): string {
713
+ const date = value === undefined ? new Date() : value instanceof Date ? value : new Date(value);
714
+ if (Number.isNaN(date.getTime())) throw new Error("Invalid knowledge review history timestamp.");
715
+ return date.toISOString();
716
+ }
717
+
718
+ function isAlreadyExistsError(error: unknown): boolean {
719
+ return (
720
+ error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EEXIST"
721
+ );
722
+ }
723
+
724
+ async function sleep(milliseconds: number): Promise<void> {
725
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
726
+ }
727
+
728
+ function isRecord(value: unknown): value is Record<string, unknown> {
729
+ return typeof value === "object" && value !== null && !Array.isArray(value);
730
+ }