@evo-dev/core 0.0.1-alpha.4 → 0.0.1-alpha.6
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/dist/config/index.js +3 -39
- package/dist/index.js +1385 -2017
- package/package.json +1 -1
- package/src/agents/index.ts +0 -264
- package/src/code-agent-traces/index.ts +8 -2
- package/src/daemon/index.ts +0 -40
- package/src/evolution/candidates/index.ts +367 -38
- package/src/evolution/evidence/session-memory/index.ts +1 -0
- package/src/evolution/evidence/session-memory/segment.ts +2 -2
- package/src/evolution/evidence/session-memory/storage.ts +173 -2
- package/src/evolution/evidence/session-memory/types.ts +3 -3
- package/src/evolution/evidence/session-memory/updater.ts +8 -1
- package/src/evolution/index.ts +4 -0
- package/src/evolution/knowledge/index.ts +9 -74
- package/src/evolution/paths.ts +3 -0
- package/src/evolution/processor/distillation.ts +42 -33
- package/src/evolution/processor/process.ts +3 -74
- package/src/evolution/review/index.ts +3 -9
- package/src/evolution/schema.ts +23 -2
- package/src/evolution/shared.ts +62 -2
- package/src/hooks/index.ts +91 -179
- package/src/index.ts +1 -2
- package/src/projects/index.ts +453 -0
- package/src/workflow/index.ts +3 -21
- package/src/project/index.ts +0 -507
- package/src/task/index.ts +0 -840
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, parse } from "node:path";
|
|
3
|
+
import { resolveEvoDevPaths } from "../config/paths.ts";
|
|
4
|
+
import { resolveProjectLogKey } from "../runtime-logs/index.ts";
|
|
5
|
+
import {
|
|
6
|
+
isFileExistsError,
|
|
7
|
+
isNotFoundError,
|
|
8
|
+
normalizeTimestamp,
|
|
9
|
+
sanitizeStorageId,
|
|
10
|
+
sha256Short,
|
|
11
|
+
writeJsonFile,
|
|
12
|
+
} from "../utils/index.ts";
|
|
13
|
+
|
|
14
|
+
export type ProjectWorkspaceKind = "git" | "directory";
|
|
15
|
+
export type ProjectDiscoveryTarget = "claude" | "codex";
|
|
16
|
+
|
|
17
|
+
export interface ResolvedProjectWorkspace {
|
|
18
|
+
projectKey: string;
|
|
19
|
+
displayName: string;
|
|
20
|
+
workspaceRoot: string;
|
|
21
|
+
workspaceKind: ProjectWorkspaceKind;
|
|
22
|
+
cwd: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DiscoveredProjectRecordV1 {
|
|
26
|
+
schemaVersion: 1;
|
|
27
|
+
kind: "discovered-project";
|
|
28
|
+
projectKey: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
workspaceRoot: string;
|
|
31
|
+
lastCwd: string;
|
|
32
|
+
workspaceKind: ProjectWorkspaceKind;
|
|
33
|
+
sourceTargets: ProjectDiscoveryTarget[];
|
|
34
|
+
lastSessionKey: string | null;
|
|
35
|
+
firstSeenAt: string;
|
|
36
|
+
lastSeenAt: string;
|
|
37
|
+
localOnly: true;
|
|
38
|
+
sourceContentStored: false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RegisteredProjectRecordV1 {
|
|
42
|
+
schemaVersion: 1;
|
|
43
|
+
kind: "registered-project";
|
|
44
|
+
projectKey: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
workspaceRoot: string;
|
|
47
|
+
workspaceKind: ProjectWorkspaceKind;
|
|
48
|
+
registeredAt: string;
|
|
49
|
+
lastSeenAt: string;
|
|
50
|
+
localOnly: true;
|
|
51
|
+
sourceContentStored: false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ProjectRegistrationError extends Error {
|
|
55
|
+
readonly code: "invalid" | "not-found" | "conflict";
|
|
56
|
+
|
|
57
|
+
constructor(code: "invalid" | "not-found" | "conflict", message: string) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "ProjectRegistrationError";
|
|
60
|
+
this.code = code;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveProjectRegistryPaths(homeDir: string): {
|
|
65
|
+
discoveredDir: string;
|
|
66
|
+
registeredDir: string;
|
|
67
|
+
discoveredPath: (projectKey: string) => string;
|
|
68
|
+
registeredPath: (projectKey: string) => string;
|
|
69
|
+
} {
|
|
70
|
+
const paths = resolveEvoDevPaths(homeDir);
|
|
71
|
+
const discoveredDir = join(paths.stateDir, "projects", "discovered");
|
|
72
|
+
const registeredDir = join(paths.rootDir, "projects");
|
|
73
|
+
return {
|
|
74
|
+
discoveredDir,
|
|
75
|
+
registeredDir,
|
|
76
|
+
discoveredPath: (projectKey) => join(discoveredDir, `${projectKey}.json`),
|
|
77
|
+
registeredPath: (projectKey) => join(registeredDir, projectKey, "workspace.json"),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function resolveProjectWorkspaceFromCwd(input: {
|
|
82
|
+
homeDir: string;
|
|
83
|
+
cwd: string;
|
|
84
|
+
}): Promise<ResolvedProjectWorkspace | null> {
|
|
85
|
+
if (!isAbsolute(input.cwd)) return null;
|
|
86
|
+
|
|
87
|
+
let cwd: string;
|
|
88
|
+
try {
|
|
89
|
+
const info = await stat(input.cwd);
|
|
90
|
+
if (!info.isDirectory()) return null;
|
|
91
|
+
cwd = await realpath(input.cwd);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (isNotFoundError(error)) return null;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const gitRoot = await findNearestGitRoot(cwd);
|
|
98
|
+
const workspaceRoot = gitRoot ?? cwd;
|
|
99
|
+
return {
|
|
100
|
+
projectKey: sanitizeStorageId(resolveProjectLogKey(input.homeDir, workspaceRoot), "project"),
|
|
101
|
+
displayName: basename(workspaceRoot) || parse(workspaceRoot).root,
|
|
102
|
+
workspaceRoot,
|
|
103
|
+
workspaceKind: gitRoot === null ? "directory" : "git",
|
|
104
|
+
cwd,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function recordDiscoveredProject(input: {
|
|
109
|
+
homeDir: string;
|
|
110
|
+
cwd: string;
|
|
111
|
+
target: ProjectDiscoveryTarget;
|
|
112
|
+
sessionKey?: string | null;
|
|
113
|
+
now?: Date | string;
|
|
114
|
+
}): Promise<{ record: DiscoveredProjectRecordV1; path: string } | null> {
|
|
115
|
+
const workspace = await resolveProjectWorkspaceFromCwd(input);
|
|
116
|
+
if (workspace === null) return null;
|
|
117
|
+
|
|
118
|
+
const now = normalizeTimestamp(input.now);
|
|
119
|
+
const paths = resolveProjectRegistryPaths(input.homeDir);
|
|
120
|
+
const path = paths.discoveredPath(workspace.projectKey);
|
|
121
|
+
const existing = await readDiscoveredProject(path);
|
|
122
|
+
const record: DiscoveredProjectRecordV1 = {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
kind: "discovered-project",
|
|
125
|
+
projectKey: workspace.projectKey,
|
|
126
|
+
displayName: workspace.displayName,
|
|
127
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
128
|
+
lastCwd: workspace.cwd,
|
|
129
|
+
workspaceKind: workspace.workspaceKind,
|
|
130
|
+
sourceTargets: mergeTargets(existing?.sourceTargets ?? [], [input.target]),
|
|
131
|
+
lastSessionKey: normalizeSessionKey(input.sessionKey ?? existing?.lastSessionKey ?? null),
|
|
132
|
+
firstSeenAt: existing?.firstSeenAt ?? now,
|
|
133
|
+
lastSeenAt: now,
|
|
134
|
+
localOnly: true,
|
|
135
|
+
sourceContentStored: false,
|
|
136
|
+
};
|
|
137
|
+
await writeJsonFile(path, record);
|
|
138
|
+
return { record, path };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function listDiscoveredProjects(input: {
|
|
142
|
+
homeDir: string;
|
|
143
|
+
}): Promise<DiscoveredProjectRecordV1[]> {
|
|
144
|
+
const paths = resolveProjectRegistryPaths(input.homeDir);
|
|
145
|
+
const records = new Map<string, DiscoveredProjectRecordV1>();
|
|
146
|
+
|
|
147
|
+
for (const path of await listJsonFilePaths(paths.discoveredDir)) {
|
|
148
|
+
const record = await readDiscoveredProject(path);
|
|
149
|
+
if (record !== null) mergeDiscoveredRecord(records, record);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const binding of await listLegacyHookBindings(input.homeDir)) {
|
|
153
|
+
if (binding.cwd === null) continue;
|
|
154
|
+
const workspace = await resolveProjectWorkspaceFromCwd({
|
|
155
|
+
homeDir: input.homeDir,
|
|
156
|
+
cwd: binding.cwd,
|
|
157
|
+
});
|
|
158
|
+
if (workspace === null) continue;
|
|
159
|
+
mergeDiscoveredRecord(records, {
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
kind: "discovered-project",
|
|
162
|
+
projectKey: workspace.projectKey,
|
|
163
|
+
displayName: workspace.displayName,
|
|
164
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
165
|
+
lastCwd: workspace.cwd,
|
|
166
|
+
workspaceKind: workspace.workspaceKind,
|
|
167
|
+
sourceTargets: [binding.target],
|
|
168
|
+
lastSessionKey: normalizeSessionKey(binding.sessionKey),
|
|
169
|
+
firstSeenAt: binding.updatedAt,
|
|
170
|
+
lastSeenAt: binding.updatedAt,
|
|
171
|
+
localOnly: true,
|
|
172
|
+
sourceContentStored: false,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...records.values()].sort(
|
|
177
|
+
(left, right) =>
|
|
178
|
+
right.lastSeenAt.localeCompare(left.lastSeenAt) ||
|
|
179
|
+
left.projectKey.localeCompare(right.projectKey),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function listRegisteredProjects(input: {
|
|
184
|
+
homeDir: string;
|
|
185
|
+
}): Promise<RegisteredProjectRecordV1[]> {
|
|
186
|
+
const { registeredDir } = resolveProjectRegistryPaths(input.homeDir);
|
|
187
|
+
const records: RegisteredProjectRecordV1[] = [];
|
|
188
|
+
for (const projectKey of await listDirectoryNames(registeredDir)) {
|
|
189
|
+
const record = await readRegisteredProject(join(registeredDir, projectKey, "workspace.json"));
|
|
190
|
+
if (record !== null && record.projectKey === projectKey) records.push(record);
|
|
191
|
+
}
|
|
192
|
+
return records.sort(
|
|
193
|
+
(left, right) =>
|
|
194
|
+
right.lastSeenAt.localeCompare(left.lastSeenAt) ||
|
|
195
|
+
left.projectKey.localeCompare(right.projectKey),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function registerDiscoveredProject(input: {
|
|
200
|
+
homeDir: string;
|
|
201
|
+
projectKey: string;
|
|
202
|
+
now?: Date | string;
|
|
203
|
+
}): Promise<{ record: RegisteredProjectRecordV1; path: string; changed: boolean }> {
|
|
204
|
+
assertProjectKey(input.projectKey);
|
|
205
|
+
const paths = resolveProjectRegistryPaths(input.homeDir);
|
|
206
|
+
const path = paths.registeredPath(input.projectKey);
|
|
207
|
+
const existing = await readRegisteredProject(path);
|
|
208
|
+
if (existing !== null) return { record: existing, path, changed: false };
|
|
209
|
+
|
|
210
|
+
const discovered = (await listDiscoveredProjects({ homeDir: input.homeDir })).find(
|
|
211
|
+
(record) => record.projectKey === input.projectKey,
|
|
212
|
+
);
|
|
213
|
+
if (discovered === undefined) {
|
|
214
|
+
throw new ProjectRegistrationError(
|
|
215
|
+
"not-found",
|
|
216
|
+
"The project must be discovered by a local Code Agent session before it can be added.",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const record: RegisteredProjectRecordV1 = {
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
kind: "registered-project",
|
|
223
|
+
projectKey: discovered.projectKey,
|
|
224
|
+
displayName: discovered.displayName,
|
|
225
|
+
workspaceRoot: discovered.workspaceRoot,
|
|
226
|
+
workspaceKind: discovered.workspaceKind,
|
|
227
|
+
registeredAt: normalizeTimestamp(input.now),
|
|
228
|
+
lastSeenAt: discovered.lastSeenAt,
|
|
229
|
+
localOnly: true,
|
|
230
|
+
sourceContentStored: false,
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
await writeJsonFile(path, record, { overwrite: false });
|
|
234
|
+
return { record, path, changed: true };
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (!isFileExistsError(error)) throw error;
|
|
237
|
+
const concurrent = await readRegisteredProject(path);
|
|
238
|
+
if (concurrent !== null) return { record: concurrent, path, changed: false };
|
|
239
|
+
throw new ProjectRegistrationError("conflict", "The project registration already exists.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function findNearestGitRoot(cwd: string): Promise<string | null> {
|
|
244
|
+
let current = cwd;
|
|
245
|
+
for (;;) {
|
|
246
|
+
try {
|
|
247
|
+
const marker = await lstat(join(current, ".git"));
|
|
248
|
+
if (marker.isDirectory() || marker.isFile()) return current;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (!isNotFoundError(error)) throw error;
|
|
251
|
+
}
|
|
252
|
+
const parent = dirname(current);
|
|
253
|
+
if (parent === current) return null;
|
|
254
|
+
current = parent;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function readDiscoveredProject(path: string): Promise<DiscoveredProjectRecordV1 | null> {
|
|
259
|
+
try {
|
|
260
|
+
return parseDiscoveredProject(JSON.parse(await readFile(path, "utf8")) as unknown);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (isNotFoundError(error) || error instanceof ProjectRegistrationError) return null;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function readRegisteredProject(path: string): Promise<RegisteredProjectRecordV1 | null> {
|
|
268
|
+
try {
|
|
269
|
+
return parseRegisteredProject(JSON.parse(await readFile(path, "utf8")) as unknown);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (isNotFoundError(error) || error instanceof ProjectRegistrationError) return null;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseDiscoveredProject(value: unknown): DiscoveredProjectRecordV1 {
|
|
277
|
+
const record = requireRecord(value);
|
|
278
|
+
if (
|
|
279
|
+
record.schemaVersion !== 1 ||
|
|
280
|
+
record.kind !== "discovered-project" ||
|
|
281
|
+
!isProjectKey(record.projectKey) ||
|
|
282
|
+
!isNonEmptyString(record.displayName) ||
|
|
283
|
+
!isAbsoluteString(record.workspaceRoot) ||
|
|
284
|
+
!isAbsoluteString(record.lastCwd) ||
|
|
285
|
+
!isWorkspaceKind(record.workspaceKind) ||
|
|
286
|
+
!isDiscoveryTargetArray(record.sourceTargets) ||
|
|
287
|
+
!(record.lastSessionKey === null || isNonEmptyString(record.lastSessionKey)) ||
|
|
288
|
+
!isNonEmptyString(record.firstSeenAt) ||
|
|
289
|
+
!isNonEmptyString(record.lastSeenAt) ||
|
|
290
|
+
record.localOnly !== true ||
|
|
291
|
+
record.sourceContentStored !== false
|
|
292
|
+
) {
|
|
293
|
+
throw new ProjectRegistrationError("invalid", "Discovered project record is invalid.");
|
|
294
|
+
}
|
|
295
|
+
return record as unknown as DiscoveredProjectRecordV1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseRegisteredProject(value: unknown): RegisteredProjectRecordV1 {
|
|
299
|
+
const record = requireRecord(value);
|
|
300
|
+
if (
|
|
301
|
+
record.schemaVersion !== 1 ||
|
|
302
|
+
record.kind !== "registered-project" ||
|
|
303
|
+
!isProjectKey(record.projectKey) ||
|
|
304
|
+
!isNonEmptyString(record.displayName) ||
|
|
305
|
+
!isAbsoluteString(record.workspaceRoot) ||
|
|
306
|
+
!isWorkspaceKind(record.workspaceKind) ||
|
|
307
|
+
!isNonEmptyString(record.registeredAt) ||
|
|
308
|
+
!isNonEmptyString(record.lastSeenAt) ||
|
|
309
|
+
record.localOnly !== true ||
|
|
310
|
+
record.sourceContentStored !== false
|
|
311
|
+
) {
|
|
312
|
+
throw new ProjectRegistrationError("invalid", "Registered project record is invalid.");
|
|
313
|
+
}
|
|
314
|
+
return record as unknown as RegisteredProjectRecordV1;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function mergeDiscoveredRecord(
|
|
318
|
+
records: Map<string, DiscoveredProjectRecordV1>,
|
|
319
|
+
next: DiscoveredProjectRecordV1,
|
|
320
|
+
): void {
|
|
321
|
+
const previous = records.get(next.projectKey);
|
|
322
|
+
if (previous === undefined) {
|
|
323
|
+
records.set(next.projectKey, next);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const newer = next.lastSeenAt >= previous.lastSeenAt ? next : previous;
|
|
327
|
+
records.set(next.projectKey, {
|
|
328
|
+
...newer,
|
|
329
|
+
sourceTargets: mergeTargets(previous.sourceTargets, next.sourceTargets),
|
|
330
|
+
firstSeenAt: previous.firstSeenAt <= next.firstSeenAt ? previous.firstSeenAt : next.firstSeenAt,
|
|
331
|
+
lastSeenAt: previous.lastSeenAt >= next.lastSeenAt ? previous.lastSeenAt : next.lastSeenAt,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function mergeTargets(
|
|
336
|
+
left: ProjectDiscoveryTarget[],
|
|
337
|
+
right: ProjectDiscoveryTarget[],
|
|
338
|
+
): ProjectDiscoveryTarget[] {
|
|
339
|
+
return [...new Set([...left, ...right])].sort();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function normalizeSessionKey(value: string | null | undefined): string | null {
|
|
343
|
+
if (value === null || value === undefined) return null;
|
|
344
|
+
return /^session-[a-f0-9]{16}$/u.test(value) ? value : `session-${sha256Short(value)}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
interface LegacyHookBinding {
|
|
348
|
+
target: ProjectDiscoveryTarget;
|
|
349
|
+
sessionKey: string;
|
|
350
|
+
cwd: string | null;
|
|
351
|
+
updatedAt: string;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function listLegacyHookBindings(homeDir: string): Promise<LegacyHookBinding[]> {
|
|
355
|
+
const roots = [
|
|
356
|
+
join(homeDir, ".evodev", "state", "hooks", "sessions"),
|
|
357
|
+
join(homeDir, ".evodev", "STATE", "hooks", "sessions"),
|
|
358
|
+
];
|
|
359
|
+
const bindings: LegacyHookBinding[] = [];
|
|
360
|
+
for (const root of roots) {
|
|
361
|
+
for (const sessionDir of await listDirectoryNames(root)) {
|
|
362
|
+
try {
|
|
363
|
+
const value = JSON.parse(
|
|
364
|
+
await readFile(join(root, sessionDir, "binding.json"), "utf8"),
|
|
365
|
+
) as Record<string, unknown> | null;
|
|
366
|
+
if (
|
|
367
|
+
value === null ||
|
|
368
|
+
(value.target !== "claude" && value.target !== "codex") ||
|
|
369
|
+
!isNonEmptyString(value.sessionKey) ||
|
|
370
|
+
!(value.cwd === null || isAbsoluteString(value.cwd)) ||
|
|
371
|
+
!isNonEmptyString(value.updatedAt)
|
|
372
|
+
) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
bindings.push({
|
|
376
|
+
target: value.target,
|
|
377
|
+
sessionKey: value.sessionKey,
|
|
378
|
+
cwd: value.cwd,
|
|
379
|
+
updatedAt: value.updatedAt,
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (!isNotFoundError(error) && !(error instanceof SyntaxError)) throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return bindings;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function listJsonFilePaths(path: string): Promise<string[]> {
|
|
390
|
+
try {
|
|
391
|
+
return (await readdir(path, { withFileTypes: true }))
|
|
392
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
393
|
+
.map((entry) => join(path, entry.name))
|
|
394
|
+
.sort();
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (isNotFoundError(error)) return [];
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function listDirectoryNames(path: string): Promise<string[]> {
|
|
402
|
+
try {
|
|
403
|
+
return (await readdir(path, { withFileTypes: true }))
|
|
404
|
+
.filter((entry) => entry.isDirectory())
|
|
405
|
+
.map((entry) => entry.name)
|
|
406
|
+
.sort();
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (isNotFoundError(error)) return [];
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function assertProjectKey(value: string): void {
|
|
414
|
+
if (!isProjectKey(value)) {
|
|
415
|
+
throw new ProjectRegistrationError("invalid", "projectKey is invalid.");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isProjectKey(value: unknown): value is string {
|
|
420
|
+
return (
|
|
421
|
+
typeof value === "string" &&
|
|
422
|
+
value !== "." &&
|
|
423
|
+
value !== ".." &&
|
|
424
|
+
/^[A-Za-z0-9._-]{1,120}$/.test(value)
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
429
|
+
return typeof value === "string" && value.trim() !== "";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isAbsoluteString(value: unknown): value is string {
|
|
433
|
+
return isNonEmptyString(value) && isAbsolute(value);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isWorkspaceKind(value: unknown): value is ProjectWorkspaceKind {
|
|
437
|
+
return value === "git" || value === "directory";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isDiscoveryTargetArray(value: unknown): value is ProjectDiscoveryTarget[] {
|
|
441
|
+
return (
|
|
442
|
+
Array.isArray(value) &&
|
|
443
|
+
value.length > 0 &&
|
|
444
|
+
value.every((item) => item === "claude" || item === "codex")
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function requireRecord(value: unknown): Record<string, unknown> {
|
|
449
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
450
|
+
throw new ProjectRegistrationError("invalid", "Project record must be an object.");
|
|
451
|
+
}
|
|
452
|
+
return value as Record<string, unknown>;
|
|
453
|
+
}
|
package/src/workflow/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readFile, readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type { TaskContract } from "../task/index.ts";
|
|
4
3
|
|
|
5
4
|
export type WorkflowMode = "minimal" | "standard" | "rigorous";
|
|
6
5
|
|
|
@@ -36,8 +35,6 @@ export interface WorkflowStep {
|
|
|
36
35
|
|
|
37
36
|
export interface WorkflowPlan {
|
|
38
37
|
workflow: WorkflowManifest;
|
|
39
|
-
taskId?: string;
|
|
40
|
-
mode: WorkflowMode | null;
|
|
41
38
|
steps: Array<WorkflowStep & { plannedOnly: true }>;
|
|
42
39
|
requiredEvidence: string[];
|
|
43
40
|
warnings: string[];
|
|
@@ -78,26 +75,13 @@ export function parseWorkflowManifest(value: unknown): WorkflowManifest {
|
|
|
78
75
|
return manifest;
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
export function planWorkflow(input: {
|
|
82
|
-
workflow: WorkflowManifest;
|
|
83
|
-
contract?: TaskContract;
|
|
84
|
-
}): WorkflowPlan {
|
|
85
|
-
const mode = input.contract?.route.mode ?? null;
|
|
86
|
-
const warnings: string[] = [];
|
|
87
|
-
const advisories: string[] = [];
|
|
88
|
-
|
|
89
|
-
if (mode !== null && !input.workflow.modes.includes(mode)) {
|
|
90
|
-
advisories.push(`Workflow ${input.workflow.id} does not list task mode ${mode}.`);
|
|
91
|
-
}
|
|
92
|
-
|
|
78
|
+
export function planWorkflow(input: { workflow: WorkflowManifest }): WorkflowPlan {
|
|
93
79
|
return {
|
|
94
80
|
workflow: input.workflow,
|
|
95
|
-
taskId: input.contract?.taskId,
|
|
96
|
-
mode,
|
|
97
81
|
steps: input.workflow.steps.map((step) => ({ ...step, plannedOnly: true })),
|
|
98
82
|
requiredEvidence: input.workflow.requiredEvidence,
|
|
99
|
-
warnings,
|
|
100
|
-
advisories,
|
|
83
|
+
warnings: [],
|
|
84
|
+
advisories: [],
|
|
101
85
|
};
|
|
102
86
|
}
|
|
103
87
|
|
|
@@ -116,8 +100,6 @@ export function formatWorkflowPlan(plan: WorkflowPlan): string {
|
|
|
116
100
|
"EvoDev workflow dry-run",
|
|
117
101
|
"",
|
|
118
102
|
`Workflow: ${plan.workflow.id}`,
|
|
119
|
-
`Task: ${plan.taskId ?? "none"}`,
|
|
120
|
-
`Mode: ${plan.mode ?? "not routed"}`,
|
|
121
103
|
"",
|
|
122
104
|
"Steps:",
|
|
123
105
|
...plan.steps.map(
|