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

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/core",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.0.1-alpha.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,18 +10,22 @@
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./src/index.ts",
13
+ "bun": "./src/index.ts",
13
14
  "import": "./dist/index.js"
14
15
  },
15
16
  "./assets": {
16
17
  "types": "./src/assets/index.ts",
18
+ "bun": "./src/assets/index.ts",
17
19
  "import": "./dist/assets/index.js"
18
20
  },
19
21
  "./config": {
20
22
  "types": "./src/config/index.ts",
23
+ "bun": "./src/config/index.ts",
21
24
  "import": "./dist/config/index.js"
22
25
  },
23
26
  "./plugins": {
24
27
  "types": "./src/plugins/index.ts",
28
+ "bun": "./src/plugins/index.ts",
25
29
  "import": "./dist/plugins/index.js"
26
30
  }
27
31
  },
@@ -1,4 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import {
3
+ type ScopedKnowledgeContextPack,
4
+ createScopedKnowledgeContextPack,
5
+ formatScopedKnowledgePromptBlock,
6
+ } from "../knowledge/index.ts";
2
7
  import type { TaskContract, TaskExecutionMode } from "../task/index.ts";
3
8
 
4
9
  export type AgentPersistence = "named" | "dynamic";
@@ -96,6 +101,15 @@ export interface AgentContextDryRunBundle {
96
101
  advisories: string[];
97
102
  }
98
103
 
104
+ export interface AgentLoadedContextBundle {
105
+ profile: AgentProfile;
106
+ contextPack: ScopedKnowledgeContextPack | null;
107
+ promptBlock: string | null;
108
+ warnings: string[];
109
+ advisories: string[];
110
+ rawContentStored: false;
111
+ }
112
+
99
113
  export interface ReviewFindingOutput {
100
114
  summary: string;
101
115
  findings: Array<{
@@ -271,6 +285,48 @@ export function loadAgentContextDryRun(profile: AgentProfile): AgentContextDryRu
271
285
  };
272
286
  }
273
287
 
288
+ export async function loadAgentContext(input: {
289
+ homeDir: string;
290
+ profile: AgentProfile;
291
+ projectKey?: string;
292
+ roleId?: string;
293
+ workflowId?: string;
294
+ paths?: string[];
295
+ queryText?: string;
296
+ limit?: number;
297
+ }): Promise<AgentLoadedContextBundle> {
298
+ const parsed = parseAgentProfile(input.profile);
299
+ const pack = await createScopedKnowledgeContextPack({
300
+ homeDir: input.homeDir,
301
+ projectKey: input.projectKey,
302
+ roleId: input.roleId ?? parsed.id,
303
+ workflowId: input.workflowId,
304
+ paths: input.paths,
305
+ queryText: input.queryText,
306
+ limit: input.limit,
307
+ });
308
+ return {
309
+ profile: parsed,
310
+ contextPack: pack,
311
+ promptBlock: pack === null ? null : formatScopedKnowledgePromptBlock(pack),
312
+ warnings: pack?.warnings ?? [],
313
+ advisories: [],
314
+ rawContentStored: false,
315
+ };
316
+ }
317
+
318
+ export function formatAgentLoadedContextPromptBlock(bundle: AgentLoadedContextBundle): string {
319
+ if (bundle.promptBlock !== null) return bundle.promptBlock;
320
+ return [
321
+ "EvoDev Scoped Knowledge Context",
322
+ `Agent: ${bundle.profile.id}`,
323
+ "Raw content stored: false",
324
+ "",
325
+ "Applicable items:",
326
+ "- none",
327
+ ].join("\n");
328
+ }
329
+
274
330
  export function validateAgentOutput(
275
331
  schema: AgentOutputSchemaId,
276
332
  output: unknown,
@@ -0,0 +1,521 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, normalize, relative } from "node:path";
4
+ import { resolveEvoDevPaths } from "../config/paths.ts";
5
+ import { resolveTraceSessionKey, resolveTraceTeamContext } from "../runtime-logs/index.ts";
6
+
7
+ export type CodeAgentTraceTarget = "claude" | "codex";
8
+ export type CodeAgentTraceRefSource =
9
+ | "hook-payload"
10
+ | "environment"
11
+ | "adapter-discovery"
12
+ | "manual";
13
+
14
+ export interface CodeAgentTraceRefV1 {
15
+ version: 1;
16
+ id: string;
17
+ target: CodeAgentTraceTarget;
18
+ sessionKey: string;
19
+ nativeSessionIdHash: string | null;
20
+ projectKey: string | null;
21
+ runId: string | null;
22
+ roleId: string | null;
23
+ cwdHash: string | null;
24
+ discoveredAt: string;
25
+ source: CodeAgentTraceRefSource;
26
+ tracePath: string | null;
27
+ tracePathExistsAtDiscovery: boolean | null;
28
+ tracePathTrusted: boolean;
29
+ contentStoredByEvoDev: false;
30
+ contentReadableByDefault: false;
31
+ notes: string[];
32
+ }
33
+
34
+ export interface CodeAgentTraceRefPaths {
35
+ rootDir: string;
36
+ claudeDir: string;
37
+ codexDir: string;
38
+ }
39
+
40
+ export interface CreateCodeAgentTraceRefInput {
41
+ target: CodeAgentTraceTarget;
42
+ sessionKey: string;
43
+ nativeSessionId?: string | null;
44
+ nativeSessionIdHash?: string | null;
45
+ projectKey?: string | null;
46
+ runId?: string | null;
47
+ roleId?: string | null;
48
+ cwd?: string | null;
49
+ cwdHash?: string | null;
50
+ discoveredAt?: Date | string;
51
+ source: CodeAgentTraceRefSource;
52
+ tracePath?: string | null;
53
+ tracePathExistsAtDiscovery?: boolean | null;
54
+ tracePathTrusted?: boolean;
55
+ notes?: string[];
56
+ }
57
+
58
+ export interface CodeAgentTraceRefRecord {
59
+ ref: CodeAgentTraceRefV1;
60
+ path: string;
61
+ }
62
+
63
+ export interface CodeAgentTraceRefListInput {
64
+ homeDir: string;
65
+ target?: CodeAgentTraceTarget;
66
+ projectKey?: string;
67
+ runId?: string;
68
+ roleId?: string;
69
+ }
70
+
71
+ export interface RecordCodeAgentTraceRefFromHookInput {
72
+ homeDir: string;
73
+ target: CodeAgentTraceTarget;
74
+ payload: Record<string, unknown>;
75
+ environment?: Record<string, string | undefined>;
76
+ now?: Date | string;
77
+ }
78
+
79
+ const CODE_AGENT_TRACE_TARGETS = ["claude", "codex"] as const;
80
+ const CODE_AGENT_TRACE_REF_SOURCES = [
81
+ "hook-payload",
82
+ "environment",
83
+ "adapter-discovery",
84
+ "manual",
85
+ ] as const;
86
+ const SENSITIVE_IDENTIFIER_PATTERN =
87
+ /https?:\/\/\S+|(^|[^a-z0-9])(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|stdout|stderr|transcript|formattedresponse|additionalcontext)([^a-z0-9]|$)|raw[\s_-]?(payload|prompt|output|source)|source[\s_-]?dump/i;
88
+ const SENSITIVE_PATH_PATTERN =
89
+ /(^|[/\\._-])(secret|token|password|passwd|api[_-]?key|apikey|credential|credentials)([/\\._-]|$)/i;
90
+
91
+ export function resolveCodeAgentTraceRefPaths(homeDir: string): CodeAgentTraceRefPaths {
92
+ const rootDir = join(resolveEvoDevPaths(homeDir).stateDir, "code-agent-traces");
93
+ return {
94
+ rootDir,
95
+ claudeDir: join(rootDir, "claude"),
96
+ codexDir: join(rootDir, "codex"),
97
+ };
98
+ }
99
+
100
+ export function createCodeAgentTraceRef(input: CreateCodeAgentTraceRefInput): CodeAgentTraceRefV1 {
101
+ const sessionKey = sanitizePersistentIdentifier(input.sessionKey, "session");
102
+ const nativeSessionId = input.nativeSessionId ?? null;
103
+ const nativeSessionIdHash =
104
+ input.nativeSessionIdHash === undefined
105
+ ? hashOptionalIdentifier(nativeSessionId)
106
+ : sanitizeHashMetadata(input.nativeSessionIdHash);
107
+ const cwdHash =
108
+ input.cwdHash === undefined
109
+ ? hashOptionalIdentifier(input.cwd ?? null)
110
+ : sanitizeHashMetadata(input.cwdHash);
111
+ const notes = [...(input.notes ?? [])];
112
+ const normalizedTracePath = normalizeTracePathForStorage(input.tracePath ?? null, notes, {
113
+ nativeSessionId,
114
+ nativeSessionIdHash,
115
+ });
116
+
117
+ return {
118
+ version: 1,
119
+ id: `trace-ref-${input.target}-${sessionKey}`,
120
+ target: input.target,
121
+ sessionKey,
122
+ nativeSessionIdHash,
123
+ projectKey:
124
+ input.projectKey === undefined || input.projectKey === null
125
+ ? null
126
+ : sanitizePersistentIdentifier(input.projectKey, "project"),
127
+ runId:
128
+ input.runId === undefined || input.runId === null
129
+ ? null
130
+ : sanitizePersistentIdentifier(input.runId, "run"),
131
+ roleId:
132
+ input.roleId === undefined || input.roleId === null
133
+ ? null
134
+ : sanitizePersistentIdentifier(input.roleId, "role"),
135
+ cwdHash,
136
+ discoveredAt: normalizeTimestamp(input.discoveredAt),
137
+ source: input.source,
138
+ tracePath: normalizedTracePath,
139
+ tracePathExistsAtDiscovery:
140
+ normalizedTracePath === null ? null : (input.tracePathExistsAtDiscovery ?? null),
141
+ tracePathTrusted: normalizedTracePath === null ? false : (input.tracePathTrusted ?? false),
142
+ contentStoredByEvoDev: false,
143
+ contentReadableByDefault: false,
144
+ notes: sanitizeNotes(notes),
145
+ };
146
+ }
147
+
148
+ export function parseCodeAgentTraceRef(value: unknown): CodeAgentTraceRefV1 {
149
+ if (!isRecord(value)) throw new Error("Code Agent trace ref must be an object.");
150
+ if (value.version !== 1) throw new Error("Code Agent trace ref version must be 1.");
151
+ assertString("id", value.id);
152
+ assertEnum("target", value.target, CODE_AGENT_TRACE_TARGETS);
153
+ assertString("sessionKey", value.sessionKey);
154
+ assertNullableString("nativeSessionIdHash", value.nativeSessionIdHash);
155
+ assertNullableString("projectKey", value.projectKey);
156
+ assertNullableString("runId", value.runId);
157
+ assertNullableString("roleId", value.roleId);
158
+ assertNullableString("cwdHash", value.cwdHash);
159
+ assertString("discoveredAt", value.discoveredAt);
160
+ assertEnum("source", value.source, CODE_AGENT_TRACE_REF_SOURCES);
161
+ assertNullableString("tracePath", value.tracePath);
162
+ if (
163
+ value.tracePathExistsAtDiscovery !== null &&
164
+ typeof value.tracePathExistsAtDiscovery !== "boolean"
165
+ ) {
166
+ throw new Error("tracePathExistsAtDiscovery must be boolean or null.");
167
+ }
168
+ if (typeof value.tracePathTrusted !== "boolean") {
169
+ throw new Error("tracePathTrusted must be boolean.");
170
+ }
171
+ if (value.contentStoredByEvoDev !== false) {
172
+ throw new Error("contentStoredByEvoDev must be false.");
173
+ }
174
+ if (value.contentReadableByDefault !== false) {
175
+ throw new Error("contentReadableByDefault must be false.");
176
+ }
177
+ if (!Array.isArray(value.notes) || !value.notes.every((note) => typeof note === "string")) {
178
+ throw new Error("notes must be a string array.");
179
+ }
180
+
181
+ return value as unknown as CodeAgentTraceRefV1;
182
+ }
183
+
184
+ export async function writeCodeAgentTraceRef(input: {
185
+ homeDir: string;
186
+ ref: CodeAgentTraceRefV1;
187
+ }): Promise<CodeAgentTraceRefRecord> {
188
+ const ref = sanitizeCodeAgentTraceRefForWrite(parseCodeAgentTraceRef(input.ref));
189
+ const path = resolveCodeAgentTraceRefPath({
190
+ homeDir: input.homeDir,
191
+ target: ref.target,
192
+ sessionKey: ref.sessionKey,
193
+ });
194
+ await mkdir(dirname(path), { recursive: true });
195
+ await writeFile(path, `${JSON.stringify(ref, null, 2)}\n`, "utf8");
196
+ return { ref, path };
197
+ }
198
+
199
+ export async function readCodeAgentTraceRef(input: {
200
+ homeDir: string;
201
+ id: string;
202
+ }): Promise<CodeAgentTraceRefRecord> {
203
+ const targetAndSession = parseTraceRefId(input.id);
204
+ const path = resolveCodeAgentTraceRefPath({
205
+ homeDir: input.homeDir,
206
+ target: targetAndSession.target,
207
+ sessionKey: targetAndSession.sessionKey,
208
+ });
209
+ const ref = sanitizeCodeAgentTraceRefForWrite(
210
+ parseCodeAgentTraceRef(JSON.parse(await readFile(path, "utf8"))),
211
+ );
212
+ if (ref.id !== input.id) throw new Error(`Code Agent trace ref id mismatch: ${input.id}`);
213
+ return { ref, path };
214
+ }
215
+
216
+ export async function listCodeAgentTraceRefs(
217
+ input: CodeAgentTraceRefListInput,
218
+ ): Promise<CodeAgentTraceRefRecord[]> {
219
+ const targets = input.target === undefined ? [...CODE_AGENT_TRACE_TARGETS] : [input.target];
220
+ const records: CodeAgentTraceRefRecord[] = [];
221
+ for (const target of targets) {
222
+ const dir = resolveCodeAgentTraceRefTargetDir(input.homeDir, target);
223
+ const entries = await readDirectoryEntries(dir);
224
+ for (const entry of entries) {
225
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
226
+ const path = join(dir, entry.name);
227
+ const ref = sanitizeCodeAgentTraceRefForWrite(
228
+ parseCodeAgentTraceRef(JSON.parse(await readFile(path, "utf8"))),
229
+ );
230
+ if (input.projectKey !== undefined && ref.projectKey !== input.projectKey) continue;
231
+ if (input.runId !== undefined && ref.runId !== input.runId) continue;
232
+ if (input.roleId !== undefined && ref.roleId !== input.roleId) continue;
233
+ records.push({ ref, path });
234
+ }
235
+ }
236
+ return records.sort((left, right) => left.ref.id.localeCompare(right.ref.id));
237
+ }
238
+
239
+ export async function recordCodeAgentTraceRefFromHook(
240
+ input: RecordCodeAgentTraceRefFromHookInput,
241
+ ): Promise<CodeAgentTraceRefRecord | null> {
242
+ const environment = input.environment ?? {};
243
+ const sessionIdFromPayload = optionalString(input.payload.session_id ?? input.payload.sessionId);
244
+ const sessionIdFromEnvironment = optionalString(environment.EVODEV_CODE_AGENT_SESSION_ID);
245
+ const tracePathFromPayload = readPayloadTracePath(input.payload);
246
+ const tracePathFromEnvironment = optionalString(environment.EVODEV_CODE_AGENT_TRACE_PATH);
247
+ const nativeSessionId = sessionIdFromPayload ?? sessionIdFromEnvironment;
248
+ const tracePathInput = tracePathFromPayload ?? tracePathFromEnvironment;
249
+ if (nativeSessionId === null && tracePathInput === null) return null;
250
+
251
+ const notes: string[] = [];
252
+ const normalizedPath = normalizeTracePathForInspection(tracePathInput, notes);
253
+ const pathStatus =
254
+ normalizedPath === null
255
+ ? { exists: null, trusted: false, notes: [] }
256
+ : await inspectTracePath({
257
+ homeDir: input.homeDir,
258
+ target: input.target,
259
+ tracePath: normalizedPath,
260
+ });
261
+ notes.push(...pathStatus.notes);
262
+ const team = resolveTraceTeamContext({
263
+ homeDir: input.homeDir,
264
+ payload: input.payload,
265
+ environment,
266
+ });
267
+ const source =
268
+ sessionIdFromPayload !== null || tracePathFromPayload !== null ? "hook-payload" : "environment";
269
+
270
+ const ref = createCodeAgentTraceRef({
271
+ target: input.target,
272
+ sessionKey: resolveTraceSessionKey(input.payload),
273
+ nativeSessionId,
274
+ projectKey: team?.projectKey ?? null,
275
+ runId: team?.runId ?? null,
276
+ roleId: team?.roleId ?? null,
277
+ cwd: optionalString(input.payload.cwd),
278
+ discoveredAt: input.now,
279
+ source,
280
+ tracePath: normalizedPath,
281
+ tracePathExistsAtDiscovery: pathStatus.exists,
282
+ tracePathTrusted: pathStatus.trusted,
283
+ notes,
284
+ });
285
+ return await writeCodeAgentTraceRef({ homeDir: input.homeDir, ref });
286
+ }
287
+
288
+ function sanitizeCodeAgentTraceRefForWrite(ref: CodeAgentTraceRefV1): CodeAgentTraceRefV1 {
289
+ return createCodeAgentTraceRef({
290
+ target: ref.target,
291
+ sessionKey: ref.sessionKey,
292
+ nativeSessionIdHash: sanitizeHashMetadata(ref.nativeSessionIdHash),
293
+ projectKey: ref.projectKey,
294
+ runId: ref.runId,
295
+ roleId: ref.roleId,
296
+ cwdHash: sanitizeHashMetadata(ref.cwdHash),
297
+ discoveredAt: ref.discoveredAt,
298
+ source: ref.source,
299
+ tracePath: ref.tracePath,
300
+ tracePathExistsAtDiscovery: ref.tracePathExistsAtDiscovery,
301
+ tracePathTrusted: ref.tracePathTrusted,
302
+ notes: ref.notes,
303
+ });
304
+ }
305
+
306
+ function resolveCodeAgentTraceRefPath(input: {
307
+ homeDir: string;
308
+ target: CodeAgentTraceTarget;
309
+ sessionKey: string;
310
+ }): string {
311
+ return join(
312
+ resolveCodeAgentTraceRefTargetDir(input.homeDir, input.target),
313
+ `${sanitizePersistentIdentifier(input.sessionKey, "session")}.json`,
314
+ );
315
+ }
316
+
317
+ function resolveCodeAgentTraceRefTargetDir(homeDir: string, target: CodeAgentTraceTarget): string {
318
+ const paths = resolveCodeAgentTraceRefPaths(homeDir);
319
+ return target === "claude" ? paths.claudeDir : paths.codexDir;
320
+ }
321
+
322
+ function parseTraceRefId(id: string): { target: CodeAgentTraceTarget; sessionKey: string } {
323
+ const match = id.match(/^trace-ref-(claude|codex)-(.+)$/);
324
+ if (match === null) throw new Error(`Invalid Code Agent trace ref id: ${id}`);
325
+ return {
326
+ target: match[1] as CodeAgentTraceTarget,
327
+ sessionKey: sanitizePersistentIdentifier(match[2] ?? "", "session"),
328
+ };
329
+ }
330
+
331
+ async function readDirectoryEntries(dir: string) {
332
+ try {
333
+ return await readdir(dir, { withFileTypes: true });
334
+ } catch (error) {
335
+ if (isNotFoundError(error)) return [];
336
+ throw error;
337
+ }
338
+ }
339
+
340
+ function readPayloadTracePath(payload: Record<string, unknown>): string | null {
341
+ return (
342
+ optionalString(payload.transcript_path) ??
343
+ optionalString(payload.transcriptPath) ??
344
+ optionalString(payload.trace_path) ??
345
+ optionalString(payload.tracePath) ??
346
+ optionalString(payload.session_path) ??
347
+ optionalString(payload.sessionPath)
348
+ );
349
+ }
350
+
351
+ function normalizeTracePathForStorage(
352
+ value: string | null,
353
+ notes: string[],
354
+ redaction: { nativeSessionId: string | null; nativeSessionIdHash: string | null },
355
+ ): string | null {
356
+ const normalized = normalizeTracePathForInspection(value, notes);
357
+ return normalized === null ? null : redactTracePathForStorage(normalized, notes, redaction);
358
+ }
359
+
360
+ function normalizeTracePathForInspection(value: string | null, notes: string[]): string | null {
361
+ if (value === null || value.trim() === "") return null;
362
+ const trimmed = value.trim();
363
+ if (/[\0\r\n]/.test(trimmed)) {
364
+ notes.push("trace path omitted: unsafe control characters");
365
+ return null;
366
+ }
367
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
368
+ notes.push("trace path omitted: URLs are not stored");
369
+ return null;
370
+ }
371
+ if (!isAbsolute(trimmed)) {
372
+ notes.push("trace path omitted: relative paths are not trusted");
373
+ return null;
374
+ }
375
+ if (hasRawTraversalSegment(trimmed)) {
376
+ notes.push("trace path omitted: traversal segments are not trusted");
377
+ return null;
378
+ }
379
+ if (SENSITIVE_PATH_PATTERN.test(trimmed)) {
380
+ notes.push("trace path omitted: sensitive-looking path segment");
381
+ return null;
382
+ }
383
+ return normalize(trimmed);
384
+ }
385
+
386
+ function redactTracePathForStorage(
387
+ path: string,
388
+ notes: string[],
389
+ redaction: { nativeSessionId: string | null; nativeSessionIdHash: string | null },
390
+ ): string {
391
+ if (redaction.nativeSessionId === null || redaction.nativeSessionId.trim() === "") return path;
392
+ if (!path.includes(redaction.nativeSessionId)) return path;
393
+ notes.push("trace-path-redacted-native-session-id");
394
+ return path
395
+ .split(redaction.nativeSessionId)
396
+ .join(redaction.nativeSessionIdHash ?? "native-session-id-redacted");
397
+ }
398
+
399
+ async function inspectTracePath(input: {
400
+ homeDir: string;
401
+ target: CodeAgentTraceTarget;
402
+ tracePath: string;
403
+ }): Promise<{ exists: boolean | null; trusted: boolean; notes: string[] }> {
404
+ const notes: string[] = [];
405
+ let exists: boolean | null = null;
406
+ try {
407
+ await stat(input.tracePath);
408
+ exists = true;
409
+ } catch (error) {
410
+ if (isNotFoundError(error)) {
411
+ exists = false;
412
+ notes.push("trace path was not present at discovery");
413
+ } else {
414
+ exists = null;
415
+ notes.push("trace path existence could not be checked");
416
+ }
417
+ }
418
+
419
+ const trusted = exists === true && isDescendant(resolveTargetUserDir(input), input.tracePath);
420
+ if (exists === true && !trusted) {
421
+ notes.push("trace path exists outside the target user-level directory");
422
+ }
423
+ return { exists, trusted, notes };
424
+ }
425
+
426
+ function resolveTargetUserDir(input: { homeDir: string; target: CodeAgentTraceTarget }): string {
427
+ return normalize(
428
+ join(stripTrailingSlash(input.homeDir), input.target === "claude" ? ".claude" : ".codex"),
429
+ );
430
+ }
431
+
432
+ function isDescendant(parent: string, child: string): boolean {
433
+ const relation = relative(normalize(parent), normalize(child));
434
+ return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
435
+ }
436
+
437
+ function hasRawTraversalSegment(path: string): boolean {
438
+ return path.split(/[/\\]+/).some((part) => part === "..");
439
+ }
440
+
441
+ function hashOptionalIdentifier(value: string | null): string | null {
442
+ if (value === null || value.trim() === "") return null;
443
+ return `sha256-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
444
+ }
445
+
446
+ function sanitizeHashMetadata(value: string | null): string | null {
447
+ if (value === null || value.trim() === "") return null;
448
+ if (/^sha256-[a-f0-9]{16}$/i.test(value)) return value;
449
+ return hashOptionalIdentifier(value);
450
+ }
451
+
452
+ function sanitizePersistentIdentifier(value: string, prefix: string): string {
453
+ const pathSafe = value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120) || `${prefix}-local`;
454
+ if (!SENSITIVE_IDENTIFIER_PATTERN.test(value) && !SENSITIVE_IDENTIFIER_PATTERN.test(pathSafe)) {
455
+ return pathSafe;
456
+ }
457
+ const hash = createHash("sha256").update(value).digest("hex").slice(0, 16);
458
+ return `${prefix}-${hash}`;
459
+ }
460
+
461
+ function sanitizeNotes(notes: string[]): string[] {
462
+ return notes
463
+ .filter((note) => note.trim() !== "")
464
+ .slice(0, 20)
465
+ .map((note) => sanitizeNote(note));
466
+ }
467
+
468
+ function sanitizeNote(note: string): string {
469
+ const truncated = note.trim().slice(0, 240);
470
+ if (SENSITIVE_IDENTIFIER_PATTERN.test(truncated)) return "trace ref note redacted";
471
+ return truncated;
472
+ }
473
+
474
+ function normalizeTimestamp(value?: Date | string): string {
475
+ if (value instanceof Date) return value.toISOString();
476
+ if (typeof value === "string" && value.trim() !== "") return new Date(value).toISOString();
477
+ return new Date().toISOString();
478
+ }
479
+
480
+ function optionalString(value: unknown): string | null {
481
+ return typeof value === "string" && value.trim() !== "" ? value : null;
482
+ }
483
+
484
+ function assertString(label: string, value: unknown): asserts value is string {
485
+ if (typeof value !== "string" || value.trim() === "") {
486
+ throw new Error(`${label} must be a non-empty string.`);
487
+ }
488
+ }
489
+
490
+ function assertNullableString(label: string, value: unknown): asserts value is string | null {
491
+ if (value !== null && typeof value !== "string") {
492
+ throw new Error(`${label} must be a string or null.`);
493
+ }
494
+ }
495
+
496
+ function assertEnum<T extends readonly string[]>(
497
+ label: string,
498
+ value: unknown,
499
+ allowed: T,
500
+ ): asserts value is T[number] {
501
+ if (typeof value !== "string" || !allowed.includes(value)) {
502
+ throw new Error(`${label} is invalid.`);
503
+ }
504
+ }
505
+
506
+ function stripTrailingSlash(path: string): string {
507
+ if (path === "/") return path;
508
+ return path.replace(/[/\\]+$/, "");
509
+ }
510
+
511
+ function isNotFoundError(error: unknown): boolean {
512
+ return (
513
+ error instanceof Error &&
514
+ (("code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") ||
515
+ error.message.includes("ENOENT"))
516
+ );
517
+ }
518
+
519
+ function isRecord(value: unknown): value is Record<string, unknown> {
520
+ return typeof value === "object" && value !== null && !Array.isArray(value);
521
+ }
@@ -8,13 +8,16 @@ export {
8
8
  } from "./registry.ts";
9
9
  export {
10
10
  type EvoDevSettings,
11
+ type MemorySettings,
11
12
  type PluginSettings,
12
13
  type SettingsInput,
13
14
  type TeamRuntimeSettings,
15
+ createDefaultMemorySettings,
14
16
  createDefaultTeamRuntimeSettings,
15
17
  createDefaultSettings,
16
18
  mergeSettings,
17
19
  parseSettings,
20
+ readRuntimeInjectionSettings,
18
21
  } from "./settings.ts";
19
22
  export {
20
23
  type InstallState,
@@ -29,7 +29,7 @@ export function resolveEvoDevPaths(homeDir: string = getHomeDir()): EvoDevPaths
29
29
  const evosDir = `${rootDir}/evos`;
30
30
  const roleAgentsDir = `${rootDir}/agents/roles`;
31
31
  const teamsDir = `${rootDir}/teams`;
32
- const runsDir = `${rootDir}/runs`;
32
+ const runsDir = `${teamsDir}/runs`;
33
33
 
34
34
  return {
35
35
  homeDir: normalizedHome,