@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.
- package/assets/skills/coding/knowledge-distillation/SKILL.md +112 -111
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
- package/assets/team/agents/code-reviewer.md +48 -0
- package/assets/team/agents/docs-maintainer.md +51 -0
- package/assets/team/agents/implementation-engineer.md +51 -0
- package/assets/team/agents/product-scope-analyst.md +58 -0
- package/assets/team/agents/release-engineer.md +55 -0
- package/assets/team/agents/security-boundary-reviewer.md +50 -0
- package/assets/team/agents/solution-architect.md +51 -0
- package/assets/team/agents/verification-engineer.md +51 -0
- package/assets/team/team.md +102 -0
- package/dist/config/index.js +774 -34
- package/dist/index.js +7331 -1573
- package/package.json +5 -1
- package/src/agents/index.ts +56 -0
- package/src/code-agent-traces/index.ts +521 -0
- package/src/config/index.ts +3 -0
- package/src/config/paths.ts +1 -1
- package/src/config/settings.ts +78 -0
- package/src/config/store.ts +2 -0
- package/src/daemon/index.ts +98 -9
- package/src/evolution/index.ts +494 -23
- package/src/hooks/index.ts +315 -36
- package/src/index.ts +2 -0
- package/src/knowledge/index.ts +4784 -0
- package/src/runtime-logs/index.ts +490 -16
- package/src/team/index.ts +1429 -185
- package/src/team/mcp.ts +9 -5
- package/src/team/prompts.ts +141 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evo-dev/core",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.3",
|
|
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
|
},
|
package/src/agents/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -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,
|
package/src/config/paths.ts
CHANGED
|
@@ -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 = `${
|
|
32
|
+
const runsDir = `${teamsDir}/runs`;
|
|
33
33
|
|
|
34
34
|
return {
|
|
35
35
|
homeDir: normalizedHome,
|