@evo-dev/core 0.0.1-alpha → 0.0.1-alpha.1
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/agents/review/code-reviewer/examples.md +1 -1
- package/assets/agents/review/code-reviewer/prompt.md +1 -1
- package/assets/agents/review/code-reviewer/verification.md +1 -1
- package/assets/skills/coding/knowledge-distillation/SKILL.md +248 -0
- package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +122 -0
- package/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
- package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
- package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
- package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
- package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
- package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
- package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
- package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
- package/dist/config/index.js +242 -36
- package/dist/index.js +5045 -934
- package/dist/plugins/index.js +32 -32
- package/package.json +1 -1
- package/src/agents/index.ts +28 -49
- package/src/config/index.ts +2 -0
- package/src/config/paths.ts +30 -0
- package/src/config/settings.ts +52 -0
- package/src/config/store.ts +150 -0
- package/src/daemon/index.ts +376 -3
- package/src/evolution/index.ts +2356 -0
- package/src/hooks/index.ts +255 -238
- package/src/index.ts +4 -0
- package/src/pack/index.ts +13 -13
- package/src/plugins/capabilities.ts +40 -42
- package/src/plugins/index.ts +0 -1
- package/src/plugins/types.ts +4 -0
- package/src/protected-zones/index.ts +29 -11
- package/src/runtime-logs/index.ts +324 -0
- package/src/sync/orchestrator.ts +6 -0
- package/src/task/index.ts +3 -3
- package/src/team/index.ts +2398 -0
- package/src/team/mcp.ts +401 -0
- package/src/workflow/index.ts +6 -6
package/src/config/store.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
export interface CoreConfigStore {
|
|
23
23
|
readonly paths: EvoDevPaths;
|
|
24
24
|
ensureBaseDirs(): Promise<void>;
|
|
25
|
+
ensureKnowledgeBase(): Promise<void>;
|
|
25
26
|
readSettings(): Promise<EvoDevSettings>;
|
|
26
27
|
writeSettings(settings: EvoDevSettings): Promise<void>;
|
|
27
28
|
mergeAndWriteSettings(input: SettingsInput): Promise<EvoDevSettings>;
|
|
@@ -40,6 +41,15 @@ export function createCoreConfigStore(homeDir?: string): CoreConfigStore {
|
|
|
40
41
|
paths,
|
|
41
42
|
async ensureBaseDirs() {
|
|
42
43
|
await mkdir(paths.stateDir, { recursive: true });
|
|
44
|
+
await mkdir(paths.logsDir, { recursive: true });
|
|
45
|
+
await mkdir(paths.knowledgeDir, { recursive: true });
|
|
46
|
+
await mkdir(paths.evosCasesDir, { recursive: true });
|
|
47
|
+
await mkdir(paths.roleAgentsDir, { recursive: true });
|
|
48
|
+
await mkdir(paths.teamsDir, { recursive: true });
|
|
49
|
+
await mkdir(paths.runsDir, { recursive: true });
|
|
50
|
+
},
|
|
51
|
+
async ensureKnowledgeBase() {
|
|
52
|
+
await ensureKnowledgeBaseFiles(paths);
|
|
43
53
|
},
|
|
44
54
|
async readSettings() {
|
|
45
55
|
return readJsonFile(paths.settingsPath, parseSettings);
|
|
@@ -81,6 +91,7 @@ export function createCoreConfigStore(homeDir?: string): CoreConfigStore {
|
|
|
81
91
|
export async function initializeCoreConfig(homeDir?: string): Promise<CoreConfigStore> {
|
|
82
92
|
const store = createCoreConfigStore(homeDir);
|
|
83
93
|
await store.ensureBaseDirs();
|
|
94
|
+
await store.ensureKnowledgeBase();
|
|
84
95
|
await writeIfMissing(store.paths.settingsPath, createDefaultSettings());
|
|
85
96
|
await writeIfMissing(store.paths.registryPath, createDefaultRegistry());
|
|
86
97
|
await writeIfMissing(store.paths.installStatePath, createDefaultInstallState());
|
|
@@ -88,6 +99,87 @@ export async function initializeCoreConfig(homeDir?: string): Promise<CoreConfig
|
|
|
88
99
|
return store;
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
export async function ensureKnowledgeBaseFiles(paths: EvoDevPaths): Promise<void> {
|
|
103
|
+
await mkdir(paths.knowledgeDir, { recursive: true });
|
|
104
|
+
await mkdir(paths.evosCasesDir, { recursive: true });
|
|
105
|
+
await writeTextIfMissing(
|
|
106
|
+
`${paths.knowledgeDir}/README.md`,
|
|
107
|
+
[
|
|
108
|
+
"# EvoDev Knowledge",
|
|
109
|
+
"",
|
|
110
|
+
"Local-private knowledge base for user-accepted facts, decisions, architecture notes, and reusable domain context.",
|
|
111
|
+
"",
|
|
112
|
+
"EvoDev must not populate this directory from source code, prompts, command output, logs, or transcripts without an explicit consent flow.",
|
|
113
|
+
"",
|
|
114
|
+
].join("\n"),
|
|
115
|
+
);
|
|
116
|
+
await writeIndexIfMissingOrMigrate(paths.knowledgeIndexPath, "knowledge-index", {
|
|
117
|
+
version: 1,
|
|
118
|
+
kind: "knowledge-index",
|
|
119
|
+
roleTags: [],
|
|
120
|
+
entries: [],
|
|
121
|
+
});
|
|
122
|
+
await writeTextIfMissing(
|
|
123
|
+
`${paths.evosDir}/README.md`,
|
|
124
|
+
[
|
|
125
|
+
"# EvoDev Evos",
|
|
126
|
+
"",
|
|
127
|
+
"Local-private evolution case library for reviewed improvement cases and reusable process changes.",
|
|
128
|
+
"",
|
|
129
|
+
"Cases start empty. Future automation may propose candidates, but accepted evos require explicit review before they can influence workflows or routing.",
|
|
130
|
+
"",
|
|
131
|
+
].join("\n"),
|
|
132
|
+
);
|
|
133
|
+
await writeTextIfMissing(
|
|
134
|
+
`${paths.evosCasesDir}/README.md`,
|
|
135
|
+
[
|
|
136
|
+
"# Evolution Cases",
|
|
137
|
+
"",
|
|
138
|
+
"Store one reviewed evolution case per file. Do not store raw prompts, source dumps, secrets, transcripts, or raw command output here.",
|
|
139
|
+
"",
|
|
140
|
+
].join("\n"),
|
|
141
|
+
);
|
|
142
|
+
await writeIndexIfMissingOrMigrate(paths.evosIndexPath, "evos-index", {
|
|
143
|
+
version: 1,
|
|
144
|
+
kind: "evos-index",
|
|
145
|
+
roleTags: [],
|
|
146
|
+
cases: [],
|
|
147
|
+
});
|
|
148
|
+
await writeTextIfMissing(
|
|
149
|
+
`${paths.roleAgentsDir}/README.md`,
|
|
150
|
+
[
|
|
151
|
+
"# Role Agents",
|
|
152
|
+
"",
|
|
153
|
+
"Local-private role agent registry for EvoDev-managed agent roles and user-reviewed role extensions.",
|
|
154
|
+
"",
|
|
155
|
+
"Repository-specific role agents should be proposed first and written into a user repository only after explicit project opt-in.",
|
|
156
|
+
"",
|
|
157
|
+
].join("\n"),
|
|
158
|
+
);
|
|
159
|
+
await writeIndexIfMissingOrMigrate(paths.roleAgentsIndexPath, "role-agent-index", {
|
|
160
|
+
version: 1,
|
|
161
|
+
kind: "role-agent-index",
|
|
162
|
+
roles: [],
|
|
163
|
+
projectExtensions: [],
|
|
164
|
+
});
|
|
165
|
+
await writeTextIfMissing(
|
|
166
|
+
`${paths.teamsDir}/README.md`,
|
|
167
|
+
[
|
|
168
|
+
"# Agent Teams",
|
|
169
|
+
"",
|
|
170
|
+
"Local-private EvoHub team registry for reviewed role-agent team definitions.",
|
|
171
|
+
"",
|
|
172
|
+
"Teams may reference role agents and role-tagged knowledge, but they must not contain raw source, prompts, transcripts, secrets, or raw command output.",
|
|
173
|
+
"",
|
|
174
|
+
].join("\n"),
|
|
175
|
+
);
|
|
176
|
+
await writeIndexIfMissingOrMigrate(paths.teamsIndexPath, "agent-team-index", {
|
|
177
|
+
version: 1,
|
|
178
|
+
kind: "agent-team-index",
|
|
179
|
+
teams: [],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
91
183
|
async function readJsonFile<T>(filePath: string, parse: (value: unknown) => T): Promise<T> {
|
|
92
184
|
let raw: string;
|
|
93
185
|
|
|
@@ -148,6 +240,60 @@ async function writeIfMissing(filePath: string, value: unknown): Promise<void> {
|
|
|
148
240
|
}
|
|
149
241
|
}
|
|
150
242
|
|
|
243
|
+
async function writeIndexIfMissingOrMigrate(
|
|
244
|
+
filePath: string,
|
|
245
|
+
kind: string,
|
|
246
|
+
defaults: Record<string, unknown>,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
let raw: string;
|
|
249
|
+
try {
|
|
250
|
+
raw = await readFile(filePath, "utf8");
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
253
|
+
await writeJsonFile(filePath, defaults);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
throw new EvoDevConfigError(
|
|
258
|
+
`Cannot inspect config file (${describeFileError(error)})`,
|
|
259
|
+
filePath,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let existing: unknown;
|
|
264
|
+
try {
|
|
265
|
+
existing = JSON.parse(raw);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new EvoDevConfigError(
|
|
268
|
+
`Invalid bootstrap index JSON (${describeFileError(error)})`,
|
|
269
|
+
filePath,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!isRecord(existing) || existing.kind !== kind) return;
|
|
274
|
+
|
|
275
|
+
const migrated = { ...defaults, ...existing };
|
|
276
|
+
if (Object.keys(defaults).every((key) => key in existing)) return;
|
|
277
|
+
await writeJsonFile(filePath, migrated);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function writeTextIfMissing(filePath: string, value: string): Promise<void> {
|
|
281
|
+
try {
|
|
282
|
+
await readFile(filePath, "utf8");
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
285
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
286
|
+
await writeFile(filePath, value, "utf8");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
throw new EvoDevConfigError(
|
|
291
|
+
`Cannot inspect config file (${describeFileError(error)})`,
|
|
292
|
+
filePath,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
151
297
|
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
152
298
|
await mkdir(dirname(filePath), { recursive: true });
|
|
153
299
|
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
@@ -164,3 +310,7 @@ function describeFileError(error: unknown): string {
|
|
|
164
310
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
165
311
|
return error instanceof Error && "code" in error;
|
|
166
312
|
}
|
|
313
|
+
|
|
314
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
315
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
316
|
+
}
|
package/src/daemon/index.ts
CHANGED
|
@@ -2,7 +2,17 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { type IncomingMessage, createServer } from "node:http";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { listEvolutionTriggers, processEvolutionTriggers } from "../evolution/index.ts";
|
|
5
6
|
import { listObservabilityEvents } from "../observability/index.ts";
|
|
7
|
+
import {
|
|
8
|
+
type TeamRuntimeAdapter,
|
|
9
|
+
listTeamRuns,
|
|
10
|
+
reconcileTeamRun,
|
|
11
|
+
resumeTeamRun,
|
|
12
|
+
sendTeamMessage,
|
|
13
|
+
spawnTeamRole,
|
|
14
|
+
stopTeamRole,
|
|
15
|
+
} from "../team/index.ts";
|
|
6
16
|
|
|
7
17
|
export interface DaemonPaths {
|
|
8
18
|
rootDir: string;
|
|
@@ -43,6 +53,15 @@ export interface DaemonRequestInput {
|
|
|
43
53
|
token?: string | null;
|
|
44
54
|
homeDir: string;
|
|
45
55
|
origin?: string | null;
|
|
56
|
+
body?: unknown;
|
|
57
|
+
runtimeAdapter?: TeamRuntimeAdapter;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface DaemonTeamStatusSummary {
|
|
61
|
+
run: unknown;
|
|
62
|
+
agents: unknown[];
|
|
63
|
+
stoppedAgents: string[];
|
|
64
|
+
notifications: unknown[];
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
const DEFAULT_PORT = 37645;
|
|
@@ -163,6 +182,39 @@ export async function handleDaemonRequest(
|
|
|
163
182
|
const removed = await cleanupDaemonState(input.homeDir, input.token ?? "");
|
|
164
183
|
return ok({ stopped: true, removed }, warnings);
|
|
165
184
|
}
|
|
185
|
+
if (input.path === "/teams/send" && input.method === "POST") {
|
|
186
|
+
return dashboardMutation(
|
|
187
|
+
await sendDashboardTeamMessage(input.homeDir, input.body, input.runtimeAdapter),
|
|
188
|
+
warnings,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (input.path === "/teams/spawn" && input.method === "POST") {
|
|
192
|
+
return dashboardMutation(
|
|
193
|
+
await spawnDashboardTeamRole(input.homeDir, input.body, input.runtimeAdapter),
|
|
194
|
+
warnings,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (input.path === "/teams/stop-role" && input.method === "POST") {
|
|
198
|
+
return dashboardMutation(
|
|
199
|
+
await stopDashboardTeamRole(input.homeDir, input.body, input.runtimeAdapter),
|
|
200
|
+
warnings,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (input.path === "/teams/resume" && input.method === "POST") {
|
|
204
|
+
return dashboardMutation(
|
|
205
|
+
await resumeDashboardTeamRun(input.homeDir, input.body, input.runtimeAdapter),
|
|
206
|
+
warnings,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (input.path === "/teams/reconcile" && input.method === "POST") {
|
|
210
|
+
return ok(await collectTeamStatus(input.homeDir, warnings, input.runtimeAdapter), warnings);
|
|
211
|
+
}
|
|
212
|
+
if (input.path === "/evolution/process" && input.method === "POST") {
|
|
213
|
+
return dashboardMutation(
|
|
214
|
+
await processDashboardEvolutionTriggers(input.homeDir, input.body),
|
|
215
|
+
warnings,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
166
218
|
if (input.method !== "GET") return notFound(warnings);
|
|
167
219
|
|
|
168
220
|
if (input.path === "/tasks")
|
|
@@ -181,8 +233,16 @@ export async function handleDaemonRequest(
|
|
|
181
233
|
await collectDirectorySummaries(join(input.homeDir, ".evodev", "PACKS"), warnings),
|
|
182
234
|
warnings,
|
|
183
235
|
);
|
|
184
|
-
if (input.path === "/runs") return ok(
|
|
185
|
-
if (input.path === "/
|
|
236
|
+
if (input.path === "/runs") return ok(await collectTeamRuns(input.homeDir, warnings), warnings);
|
|
237
|
+
if (input.path === "/evolution/triggers")
|
|
238
|
+
return ok(await collectEvolutionTriggers(input.homeDir, warnings), warnings);
|
|
239
|
+
if (input.path === "/agents")
|
|
240
|
+
return ok(
|
|
241
|
+
(await collectTeamStatus(input.homeDir, warnings, input.runtimeAdapter)).agents,
|
|
242
|
+
warnings,
|
|
243
|
+
);
|
|
244
|
+
if (input.path === "/teams/status")
|
|
245
|
+
return ok(await collectTeamStatus(input.homeDir, warnings, input.runtimeAdapter), warnings);
|
|
186
246
|
|
|
187
247
|
return notFound(warnings);
|
|
188
248
|
}
|
|
@@ -211,6 +271,7 @@ export async function runDaemonForeground(input: {
|
|
|
211
271
|
url.searchParams.get("token"),
|
|
212
272
|
homeDir: input.homeDir,
|
|
213
273
|
origin: readHeader(request, "origin"),
|
|
274
|
+
body: await readJsonBody(request),
|
|
214
275
|
});
|
|
215
276
|
|
|
216
277
|
response.writeHead(result.status, { "content-type": "application/json" });
|
|
@@ -248,8 +309,22 @@ export async function runDaemonForeground(input: {
|
|
|
248
309
|
input.write?.(
|
|
249
310
|
`Daemon listening on ${state.lock.host}:${state.lock.port}; token path: ${state.paths.tokenPath}`,
|
|
250
311
|
);
|
|
312
|
+
const reconcileInterval = setInterval(() => {
|
|
313
|
+
void reconcileTeamRun({ homeDir: input.homeDir }).catch(() => undefined);
|
|
314
|
+
}, 2_000);
|
|
315
|
+
reconcileInterval.unref();
|
|
316
|
+
const evolutionInterval = setInterval(() => {
|
|
317
|
+
void processEvolutionTriggers({ homeDir: input.homeDir, limit: 20 })
|
|
318
|
+
.then(() => clearDaemonEvolutionProcessError(input.homeDir))
|
|
319
|
+
.catch((error) => recordDaemonEvolutionProcessError(input.homeDir, error));
|
|
320
|
+
}, 5_000);
|
|
321
|
+
evolutionInterval.unref();
|
|
251
322
|
await new Promise<void>((resolve, reject) => {
|
|
252
|
-
server.once("close",
|
|
323
|
+
server.once("close", () => {
|
|
324
|
+
clearInterval(reconcileInterval);
|
|
325
|
+
clearInterval(evolutionInterval);
|
|
326
|
+
resolve();
|
|
327
|
+
});
|
|
253
328
|
server.once("error", reject);
|
|
254
329
|
});
|
|
255
330
|
}
|
|
@@ -264,6 +339,22 @@ function createDaemonToken(): string {
|
|
|
264
339
|
return randomBytes(32).toString("hex");
|
|
265
340
|
}
|
|
266
341
|
|
|
342
|
+
async function readJsonBody(request: IncomingMessage): Promise<unknown> {
|
|
343
|
+
if (request.method === "GET" || request.method === "HEAD") return undefined;
|
|
344
|
+
const chunks: Buffer[] = [];
|
|
345
|
+
let totalBytes = 0;
|
|
346
|
+
for await (const chunk of request) {
|
|
347
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
348
|
+
totalBytes += buffer.length;
|
|
349
|
+
if (totalBytes > 64 * 1024) throw new Error("Request body too large.");
|
|
350
|
+
chunks.push(buffer);
|
|
351
|
+
}
|
|
352
|
+
if (chunks.length === 0) return undefined;
|
|
353
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
354
|
+
if (raw === "") return undefined;
|
|
355
|
+
return JSON.parse(raw);
|
|
356
|
+
}
|
|
357
|
+
|
|
267
358
|
async function authorize(homeDir: string, token: string | null): Promise<{ ok: boolean }> {
|
|
268
359
|
const expected = await readDaemonToken(homeDir);
|
|
269
360
|
return { ok: expected !== null && token !== null && token === expected };
|
|
@@ -352,6 +443,203 @@ async function collectLearningCandidateSummaries(
|
|
|
352
443
|
});
|
|
353
444
|
}
|
|
354
445
|
|
|
446
|
+
async function collectEvolutionTriggers(homeDir: string, warnings: string[]): Promise<unknown[]> {
|
|
447
|
+
const lastError = await readDaemonEvolutionProcessError(homeDir).catch(() => null);
|
|
448
|
+
if (lastError !== null) warnings.push(`Evolution processor last error: ${lastError}`);
|
|
449
|
+
try {
|
|
450
|
+
return (await listEvolutionTriggers({ homeDir })).map((trigger) =>
|
|
451
|
+
sanitizeMetadata({
|
|
452
|
+
id: trigger.id,
|
|
453
|
+
projectKey: trigger.projectKey,
|
|
454
|
+
runId: trigger.runId,
|
|
455
|
+
roleId: trigger.roleId,
|
|
456
|
+
taskId: trigger.taskId,
|
|
457
|
+
eventType: trigger.eventType,
|
|
458
|
+
triggerStrength: trigger.triggerStrength,
|
|
459
|
+
triggerReason: trigger.triggerReason,
|
|
460
|
+
status: trigger.status,
|
|
461
|
+
attempts: trigger.attempts,
|
|
462
|
+
createdAt: trigger.createdAt,
|
|
463
|
+
updatedAt: trigger.updatedAt,
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
warnings.push(`Evolution triggers unavailable: ${describeError(error)}`);
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function processDashboardEvolutionTriggers(homeDir: string, body: unknown): Promise<unknown> {
|
|
473
|
+
if (body === undefined) {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
error: "evolution process requires JSON body with dryRun=true or confirm=true.",
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const input = expectRequestBody(body);
|
|
480
|
+
const dryRun = optionalBodyBoolean(input.dryRun) === true;
|
|
481
|
+
const confirm = optionalBodyBoolean(input.confirm) === true;
|
|
482
|
+
if (!dryRun && !confirm) {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
error: "evolution process requires dryRun=true or confirm=true.",
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return processEvolutionTriggers({
|
|
489
|
+
homeDir,
|
|
490
|
+
projectKey: optionalBodyString(input.projectKey),
|
|
491
|
+
runId: optionalBodyString(input.runId),
|
|
492
|
+
limit: optionalBodyNumber(input.limit),
|
|
493
|
+
dryRun,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function collectTeamRuns(homeDir: string, warnings: string[]): Promise<unknown[]> {
|
|
498
|
+
try {
|
|
499
|
+
return (await listTeamRuns({ homeDir })).map((run) =>
|
|
500
|
+
sanitizeMetadata({
|
|
501
|
+
runId: run.runId,
|
|
502
|
+
status: run.status,
|
|
503
|
+
repoRoot: run.repoRoot,
|
|
504
|
+
mainAgentId: run.mainAgentId,
|
|
505
|
+
session: run.tmux.session,
|
|
506
|
+
roleCount: Object.keys(run.roles).length,
|
|
507
|
+
createdAt: run.createdAt,
|
|
508
|
+
updatedAt: run.updatedAt,
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
warnings.push(`Team runs unavailable: ${describeError(error)}`);
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function collectTeamStatus(
|
|
518
|
+
homeDir: string,
|
|
519
|
+
warnings: string[],
|
|
520
|
+
runtimeAdapter?: TeamRuntimeAdapter,
|
|
521
|
+
): Promise<DaemonTeamStatusSummary> {
|
|
522
|
+
try {
|
|
523
|
+
const status = await reconcileTeamRun({ homeDir, runtimeAdapter });
|
|
524
|
+
return sanitizeMetadata({
|
|
525
|
+
run:
|
|
526
|
+
status.run === null
|
|
527
|
+
? null
|
|
528
|
+
: {
|
|
529
|
+
runId: status.run.runId,
|
|
530
|
+
status: status.run.status,
|
|
531
|
+
repoRoot: status.run.repoRoot,
|
|
532
|
+
session: status.run.tmux.session,
|
|
533
|
+
updatedAt: status.run.updatedAt,
|
|
534
|
+
},
|
|
535
|
+
agents: status.agents.map((agent) => ({
|
|
536
|
+
roleId: agent.roleId,
|
|
537
|
+
roleName: agent.roleName,
|
|
538
|
+
runId: agent.runId,
|
|
539
|
+
runtime: agent.runtime,
|
|
540
|
+
model: agent.model,
|
|
541
|
+
thinkingLevel: agent.thinkingLevel,
|
|
542
|
+
status: agent.status,
|
|
543
|
+
paneId: agent.tmux.paneId,
|
|
544
|
+
canReceiveMessages:
|
|
545
|
+
agent.status === "running" ||
|
|
546
|
+
agent.status === "recovering" ||
|
|
547
|
+
agent.status === "recreated",
|
|
548
|
+
nativeSessionRecorded: agent.nativeSession.sessionId !== null,
|
|
549
|
+
updatedAt: agent.updatedAt,
|
|
550
|
+
})),
|
|
551
|
+
stoppedAgents: status.stoppedAgents.map((agent) => agent.roleId),
|
|
552
|
+
notifications: status.notifications.map((notification) => ({
|
|
553
|
+
ok: notification.ok,
|
|
554
|
+
error: notification.error ?? null,
|
|
555
|
+
deliveredTo: notification.deliveredTo ?? null,
|
|
556
|
+
})),
|
|
557
|
+
}) as DaemonTeamStatusSummary;
|
|
558
|
+
} catch (error) {
|
|
559
|
+
warnings.push(`Team status unavailable: ${describeError(error)}`);
|
|
560
|
+
return { run: null, agents: [], stoppedAgents: [], notifications: [] };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function sendDashboardTeamMessage(
|
|
565
|
+
homeDir: string,
|
|
566
|
+
body: unknown,
|
|
567
|
+
runtimeAdapter?: TeamRuntimeAdapter,
|
|
568
|
+
): Promise<unknown> {
|
|
569
|
+
const input = expectRequestBody(body);
|
|
570
|
+
return sendTeamMessage({
|
|
571
|
+
homeDir,
|
|
572
|
+
runId: optionalBodyString(input.runId),
|
|
573
|
+
fromRoleId: "user",
|
|
574
|
+
toRoleId: expectBodyString(input, "toRoleId"),
|
|
575
|
+
message: expectBodyString(input, "message"),
|
|
576
|
+
runtimeAdapter,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function spawnDashboardTeamRole(
|
|
581
|
+
homeDir: string,
|
|
582
|
+
body: unknown,
|
|
583
|
+
runtimeAdapter?: TeamRuntimeAdapter,
|
|
584
|
+
): Promise<unknown> {
|
|
585
|
+
const input = expectRequestBody(body);
|
|
586
|
+
const roleId = expectBodyString(input, "roleId");
|
|
587
|
+
const status = await reconcileTeamRun({
|
|
588
|
+
homeDir,
|
|
589
|
+
runId: optionalBodyString(input.runId),
|
|
590
|
+
runtimeAdapter,
|
|
591
|
+
notifyMain: false,
|
|
592
|
+
});
|
|
593
|
+
if (status.run === null) return { ok: false, error: "team-run-not-found" };
|
|
594
|
+
if (status.run.status !== "running") return { ok: false, error: "team-run-not-running" };
|
|
595
|
+
const result = await spawnTeamRole({
|
|
596
|
+
homeDir,
|
|
597
|
+
repoRoot: status.run.repoRoot,
|
|
598
|
+
runId: status.run.runId,
|
|
599
|
+
roleId,
|
|
600
|
+
runtimeAdapter,
|
|
601
|
+
});
|
|
602
|
+
return { ok: true, created: result.created, agent: result.agent };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function stopDashboardTeamRole(
|
|
606
|
+
homeDir: string,
|
|
607
|
+
body: unknown,
|
|
608
|
+
runtimeAdapter?: TeamRuntimeAdapter,
|
|
609
|
+
): Promise<unknown> {
|
|
610
|
+
const input = expectRequestBody(body);
|
|
611
|
+
return stopTeamRole({
|
|
612
|
+
homeDir,
|
|
613
|
+
runId: optionalBodyString(input.runId),
|
|
614
|
+
roleId: expectBodyString(input, "roleId"),
|
|
615
|
+
runtimeAdapter,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function resumeDashboardTeamRun(
|
|
620
|
+
homeDir: string,
|
|
621
|
+
body: unknown,
|
|
622
|
+
runtimeAdapter?: TeamRuntimeAdapter,
|
|
623
|
+
): Promise<unknown> {
|
|
624
|
+
const input = expectRequestBody(body);
|
|
625
|
+
const decision = optionalBodyString(input.onSessionMissing);
|
|
626
|
+
if (decision !== undefined && !["ask", "fail", "recreate", "resume-latest"].includes(decision)) {
|
|
627
|
+
return { ok: false, error: "invalid-on-session-missing" };
|
|
628
|
+
}
|
|
629
|
+
const result = await resumeTeamRun({
|
|
630
|
+
homeDir,
|
|
631
|
+
runId: optionalBodyString(input.runId),
|
|
632
|
+
runtimeAdapter,
|
|
633
|
+
missingSessionDecision: decision as "ask" | "fail" | "recreate" | "resume-latest" | undefined,
|
|
634
|
+
});
|
|
635
|
+
return {
|
|
636
|
+
ok: result.decisionRequired.length === 0,
|
|
637
|
+
run: result.run,
|
|
638
|
+
outcomes: result.outcomes,
|
|
639
|
+
decisionRequired: result.decisionRequired,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
355
643
|
async function collectDirectorySummaries(root: string, warnings: string[]): Promise<unknown[]> {
|
|
356
644
|
if (!(await pathExists(root))) {
|
|
357
645
|
warnings.push(`Store not found: ${root}`);
|
|
@@ -379,6 +667,91 @@ function sanitizeMetadata(value: unknown): unknown {
|
|
|
379
667
|
return output;
|
|
380
668
|
}
|
|
381
669
|
|
|
670
|
+
function dashboardMutation(
|
|
671
|
+
data: unknown,
|
|
672
|
+
warnings: string[],
|
|
673
|
+
): { status: number; body: DaemonResponseBody } {
|
|
674
|
+
const okResult = !(
|
|
675
|
+
typeof data === "object" &&
|
|
676
|
+
data !== null &&
|
|
677
|
+
"ok" in data &&
|
|
678
|
+
(data as { ok?: unknown }).ok === false
|
|
679
|
+
);
|
|
680
|
+
return {
|
|
681
|
+
status: okResult ? 200 : 400,
|
|
682
|
+
body: { ok: okResult, data: sanitizeMetadata(data), warnings },
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function expectRequestBody(value: unknown): Record<string, unknown> {
|
|
687
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
688
|
+
throw new Error("Expected JSON object request body.");
|
|
689
|
+
}
|
|
690
|
+
return value as Record<string, unknown>;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function expectBodyString(input: Record<string, unknown>, key: string): string {
|
|
694
|
+
const value = input[key];
|
|
695
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
696
|
+
throw new Error(`Expected non-empty string body.${key}.`);
|
|
697
|
+
}
|
|
698
|
+
return value;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function optionalBodyString(value: unknown): string | undefined {
|
|
702
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function optionalBodyNumber(value: unknown): number | undefined {
|
|
706
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function optionalBodyBoolean(value: unknown): boolean | undefined {
|
|
710
|
+
return typeof value === "boolean" ? value : undefined;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function resolveDaemonEvolutionProcessErrorPath(homeDir: string): string {
|
|
714
|
+
return join(resolveDaemonPaths(homeDir).rootDir, "evolution-process-error.json");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function recordDaemonEvolutionProcessError(homeDir: string, error: unknown): Promise<void> {
|
|
718
|
+
try {
|
|
719
|
+
const path = resolveDaemonEvolutionProcessErrorPath(homeDir);
|
|
720
|
+
await mkdir(dirname(path), { recursive: true });
|
|
721
|
+
await writeFile(
|
|
722
|
+
path,
|
|
723
|
+
`${JSON.stringify(
|
|
724
|
+
{
|
|
725
|
+
schemaVersion: 1,
|
|
726
|
+
kind: "daemon-evolution-process-error",
|
|
727
|
+
updatedAt: new Date().toISOString(),
|
|
728
|
+
summary: sanitizeMetadata(describeError(error)),
|
|
729
|
+
},
|
|
730
|
+
null,
|
|
731
|
+
2,
|
|
732
|
+
)}\n`,
|
|
733
|
+
{ encoding: "utf8", flag: "w", mode: 0o600 },
|
|
734
|
+
);
|
|
735
|
+
} catch {
|
|
736
|
+
// Daemon background diagnostics must not affect foreground request handling.
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function clearDaemonEvolutionProcessError(homeDir: string): Promise<void> {
|
|
741
|
+
await rm(resolveDaemonEvolutionProcessErrorPath(homeDir), { force: true }).catch(() => undefined);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function readDaemonEvolutionProcessError(homeDir: string): Promise<string | null> {
|
|
745
|
+
const path = resolveDaemonEvolutionProcessErrorPath(homeDir);
|
|
746
|
+
if (!(await pathExists(path))) return null;
|
|
747
|
+
const value = JSON.parse(await readFile(path, "utf8")) as { summary?: unknown };
|
|
748
|
+
return typeof value.summary === "string" && value.summary.length > 0 ? value.summary : null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function describeError(error: unknown): string {
|
|
752
|
+
return error instanceof Error ? error.message : String(error);
|
|
753
|
+
}
|
|
754
|
+
|
|
382
755
|
async function collectNamedFiles(root: string, name: string): Promise<string[]> {
|
|
383
756
|
const entries = await readdir(root, { withFileTypes: true });
|
|
384
757
|
const files: string[] = [];
|