@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.
Files changed (38) hide show
  1. package/assets/agents/review/code-reviewer/examples.md +1 -1
  2. package/assets/agents/review/code-reviewer/prompt.md +1 -1
  3. package/assets/agents/review/code-reviewer/verification.md +1 -1
  4. package/assets/skills/coding/knowledge-distillation/SKILL.md +248 -0
  5. package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
  6. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +122 -0
  7. package/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  8. package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  9. package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  10. package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  11. package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  12. package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  13. package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  14. package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  15. package/dist/config/index.js +242 -36
  16. package/dist/index.js +5045 -934
  17. package/dist/plugins/index.js +32 -32
  18. package/package.json +1 -1
  19. package/src/agents/index.ts +28 -49
  20. package/src/config/index.ts +2 -0
  21. package/src/config/paths.ts +30 -0
  22. package/src/config/settings.ts +52 -0
  23. package/src/config/store.ts +150 -0
  24. package/src/daemon/index.ts +376 -3
  25. package/src/evolution/index.ts +2356 -0
  26. package/src/hooks/index.ts +255 -238
  27. package/src/index.ts +4 -0
  28. package/src/pack/index.ts +13 -13
  29. package/src/plugins/capabilities.ts +40 -42
  30. package/src/plugins/index.ts +0 -1
  31. package/src/plugins/types.ts +4 -0
  32. package/src/protected-zones/index.ts +29 -11
  33. package/src/runtime-logs/index.ts +324 -0
  34. package/src/sync/orchestrator.ts +6 -0
  35. package/src/task/index.ts +3 -3
  36. package/src/team/index.ts +2398 -0
  37. package/src/team/mcp.ts +401 -0
  38. package/src/workflow/index.ts +6 -6
@@ -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
+ }
@@ -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([], warnings);
185
- if (input.path === "/agents") return ok([], warnings);
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", resolve);
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[] = [];