@evo-dev/core 0.0.1-alpha → 0.0.1-alpha.10

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 (85) 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 +251 -0
  5. package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
  6. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
  7. package/assets/team/agents/code-reviewer.md +48 -0
  8. package/assets/team/agents/docs-maintainer.md +51 -0
  9. package/assets/team/agents/implementation-engineer.md +51 -0
  10. package/assets/team/agents/product-scope-analyst.md +58 -0
  11. package/assets/team/agents/release-engineer.md +55 -0
  12. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  13. package/assets/team/agents/solution-architect.md +51 -0
  14. package/assets/team/agents/verification-engineer.md +51 -0
  15. package/assets/team/team.md +102 -0
  16. package/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  17. package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  18. package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  19. package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  20. package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  21. package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  22. package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  23. package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  24. package/dist/config/index.js +1115 -81
  25. package/dist/index.js +13796 -2196
  26. package/dist/plugins/index.js +32 -32
  27. package/package.json +5 -1
  28. package/src/agents/index.ts +63 -292
  29. package/src/code-agent-traces/index.ts +520 -0
  30. package/src/config/index.ts +7 -0
  31. package/src/config/paths.ts +30 -0
  32. package/src/config/settings.ts +201 -0
  33. package/src/config/store.ts +152 -0
  34. package/src/daemon/index.ts +462 -40
  35. package/src/evolution/candidates/index.ts +564 -0
  36. package/src/evolution/control/index.ts +20 -0
  37. package/src/evolution/evidence/analysis.ts +533 -0
  38. package/src/evolution/evidence/index.ts +3 -0
  39. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  40. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  41. package/src/evolution/evidence/session-memory/index.ts +7 -0
  42. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  43. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  44. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  45. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  46. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  47. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  48. package/src/evolution/evidence/session-memory/types.ts +221 -0
  49. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  50. package/src/evolution/formatters.ts +169 -0
  51. package/src/evolution/index.ts +16 -0
  52. package/src/evolution/knowledge/index.ts +5427 -0
  53. package/src/evolution/paths.ts +44 -0
  54. package/src/evolution/processor/distillation.ts +518 -0
  55. package/src/evolution/processor/index.ts +3 -0
  56. package/src/evolution/processor/process.ts +528 -0
  57. package/src/{learning → evolution/review}/index.ts +10 -14
  58. package/src/evolution/schema.ts +568 -0
  59. package/src/evolution/shared.ts +758 -0
  60. package/src/evolution/triggers/classification.ts +102 -0
  61. package/src/evolution/triggers/index.ts +295 -0
  62. package/src/hooks/index.ts +652 -376
  63. package/src/index.ts +16 -3
  64. package/src/pack/index.ts +13 -13
  65. package/src/plugins/capabilities.ts +40 -42
  66. package/src/plugins/index.ts +0 -1
  67. package/src/plugins/types.ts +4 -0
  68. package/src/projects/index.ts +453 -0
  69. package/src/protected-zones/index.ts +29 -11
  70. package/src/runtime-logs/index.ts +790 -0
  71. package/src/sync/orchestrator.ts +6 -0
  72. package/src/team/index.ts +3642 -0
  73. package/src/team/mcp.ts +405 -0
  74. package/src/team/prompts.ts +141 -0
  75. package/src/utils/errors.ts +13 -0
  76. package/src/utils/fs.ts +40 -0
  77. package/src/utils/hash.ts +9 -0
  78. package/src/utils/ids.ts +12 -0
  79. package/src/utils/index.ts +7 -0
  80. package/src/utils/parsing.ts +11 -0
  81. package/src/utils/text.ts +18 -0
  82. package/src/utils/time.ts +5 -0
  83. package/src/workflow/index.ts +6 -24
  84. package/src/project/index.ts +0 -507
  85. package/src/task/index.ts +0 -840
@@ -2,7 +2,24 @@ 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/control/index.ts";
5
6
  import { listObservabilityEvents } from "../observability/index.ts";
7
+ import {
8
+ type TeamRuntimeAdapter,
9
+ isActiveTeamAgentStatus,
10
+ isIdleTeamAgentStatus,
11
+ isMidTurnTeamAgentStatus,
12
+ listTeamRuns,
13
+ markTeamMessagesDelivered,
14
+ readPendingTeamMessagesForRole,
15
+ reconcileTeamRun,
16
+ resumeTeamRun,
17
+ schedulePendingTeamMessageDelivery,
18
+ sendTeamMessage,
19
+ spawnTeamRole,
20
+ stopTeamRole,
21
+ updateTeamAgentHookState,
22
+ } from "../team/index.ts";
6
23
 
7
24
  export interface DaemonPaths {
8
25
  rootDir: string;
@@ -43,6 +60,15 @@ export interface DaemonRequestInput {
43
60
  token?: string | null;
44
61
  homeDir: string;
45
62
  origin?: string | null;
63
+ body?: unknown;
64
+ runtimeAdapter?: TeamRuntimeAdapter;
65
+ }
66
+
67
+ interface DaemonTeamStatusSummary {
68
+ run: unknown;
69
+ agents: unknown[];
70
+ stoppedAgents: string[];
71
+ notifications: unknown[];
46
72
  }
47
73
 
48
74
  const DEFAULT_PORT = 37645;
@@ -163,10 +189,68 @@ export async function handleDaemonRequest(
163
189
  const removed = await cleanupDaemonState(input.homeDir, input.token ?? "");
164
190
  return ok({ stopped: true, removed }, warnings);
165
191
  }
192
+ if (
193
+ (input.path === "/team/message/enqueue" || input.path === "/teams/send") &&
194
+ input.method === "POST"
195
+ ) {
196
+ return dashboardMutation(
197
+ await sendDashboardTeamMessage(input.homeDir, input.body, input.runtimeAdapter),
198
+ warnings,
199
+ );
200
+ }
201
+ if (input.path === "/team/agent/state" && input.method === "POST") {
202
+ return dashboardMutation(
203
+ await updateDashboardTeamAgentState(input.homeDir, input.body),
204
+ warnings,
205
+ );
206
+ }
207
+ if (input.path === "/team/message/claim" && input.method === "POST") {
208
+ return ok(await claimDashboardTeamMessages(input.homeDir, input.body), warnings);
209
+ }
210
+ if (input.path === "/team/message/delivered" && input.method === "POST") {
211
+ return dashboardMutation(
212
+ await markDashboardTeamMessagesDelivered(input.homeDir, input.body),
213
+ warnings,
214
+ );
215
+ }
216
+ if (
217
+ (input.path === "/team/reconcile" || input.path === "/teams/reconcile") &&
218
+ input.method === "POST"
219
+ ) {
220
+ await schedulePendingTeamMessageDelivery({
221
+ homeDir: input.homeDir,
222
+ runtimeAdapter: input.runtimeAdapter,
223
+ }).catch((error) =>
224
+ warnings.push(`Team delivery scheduling unavailable: ${describeError(error)}`),
225
+ );
226
+ return ok(await collectTeamStatus(input.homeDir, warnings, input.runtimeAdapter), warnings);
227
+ }
228
+ if (input.path === "/teams/spawn" && input.method === "POST") {
229
+ return dashboardMutation(
230
+ await spawnDashboardTeamRole(input.homeDir, input.body, input.runtimeAdapter),
231
+ warnings,
232
+ );
233
+ }
234
+ if (input.path === "/teams/stop-role" && input.method === "POST") {
235
+ return dashboardMutation(
236
+ await stopDashboardTeamRole(input.homeDir, input.body, input.runtimeAdapter),
237
+ warnings,
238
+ );
239
+ }
240
+ if (input.path === "/teams/resume" && input.method === "POST") {
241
+ return dashboardMutation(
242
+ await resumeDashboardTeamRun(input.homeDir, input.body, input.runtimeAdapter),
243
+ warnings,
244
+ );
245
+ }
246
+ if (input.path === "/evolution/process" && input.method === "POST") {
247
+ return dashboardMutation(
248
+ await processDashboardEvolutionTriggers(input.homeDir, input.body),
249
+ warnings,
250
+ );
251
+ }
166
252
  if (input.method !== "GET") return notFound(warnings);
167
253
 
168
- if (input.path === "/tasks")
169
- return ok(await collectTaskSummaries(input.homeDir, warnings), warnings);
170
254
  if (input.path === "/observability/events")
171
255
  return ok(await collectObservabilitySummaries(input.homeDir, warnings), warnings);
172
256
  if (input.path === "/memory/candidates")
@@ -181,8 +265,16 @@ export async function handleDaemonRequest(
181
265
  await collectDirectorySummaries(join(input.homeDir, ".evodev", "PACKS"), warnings),
182
266
  warnings,
183
267
  );
184
- if (input.path === "/runs") return ok([], warnings);
185
- if (input.path === "/agents") return ok([], warnings);
268
+ if (input.path === "/runs") return ok(await collectTeamRuns(input.homeDir, warnings), warnings);
269
+ if (input.path === "/evolution/triggers")
270
+ return ok(await collectEvolutionTriggers(input.homeDir, warnings), warnings);
271
+ if (input.path === "/agents")
272
+ return ok(
273
+ (await collectTeamStatus(input.homeDir, warnings, input.runtimeAdapter)).agents,
274
+ warnings,
275
+ );
276
+ if (input.path === "/teams/status")
277
+ return ok(await collectTeamStatus(input.homeDir, warnings, input.runtimeAdapter), warnings);
186
278
 
187
279
  return notFound(warnings);
188
280
  }
@@ -211,6 +303,7 @@ export async function runDaemonForeground(input: {
211
303
  url.searchParams.get("token"),
212
304
  homeDir: input.homeDir,
213
305
  origin: readHeader(request, "origin"),
306
+ body: await readJsonBody(request),
214
307
  });
215
308
 
216
309
  response.writeHead(result.status, { "content-type": "application/json" });
@@ -248,8 +341,22 @@ export async function runDaemonForeground(input: {
248
341
  input.write?.(
249
342
  `Daemon listening on ${state.lock.host}:${state.lock.port}; token path: ${state.paths.tokenPath}`,
250
343
  );
344
+ const reconcileInterval = setInterval(() => {
345
+ void reconcileTeamRun({ homeDir: input.homeDir }).catch(() => undefined);
346
+ }, 2_000);
347
+ reconcileInterval.unref();
348
+ const evolutionInterval = setInterval(() => {
349
+ void processEvolutionTriggers({ homeDir: input.homeDir, limit: 20 })
350
+ .then(() => clearDaemonEvolutionProcessError(input.homeDir))
351
+ .catch((error) => recordDaemonEvolutionProcessError(input.homeDir, error));
352
+ }, 5_000);
353
+ evolutionInterval.unref();
251
354
  await new Promise<void>((resolve, reject) => {
252
- server.once("close", resolve);
355
+ server.once("close", () => {
356
+ clearInterval(reconcileInterval);
357
+ clearInterval(evolutionInterval);
358
+ resolve();
359
+ });
253
360
  server.once("error", reject);
254
361
  });
255
362
  }
@@ -264,6 +371,22 @@ function createDaemonToken(): string {
264
371
  return randomBytes(32).toString("hex");
265
372
  }
266
373
 
374
+ async function readJsonBody(request: IncomingMessage): Promise<unknown> {
375
+ if (request.method === "GET" || request.method === "HEAD") return undefined;
376
+ const chunks: Buffer[] = [];
377
+ let totalBytes = 0;
378
+ for await (const chunk of request) {
379
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
380
+ totalBytes += buffer.length;
381
+ if (totalBytes > 64 * 1024) throw new Error("Request body too large.");
382
+ chunks.push(buffer);
383
+ }
384
+ if (chunks.length === 0) return undefined;
385
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
386
+ if (raw === "") return undefined;
387
+ return JSON.parse(raw);
388
+ }
389
+
267
390
  async function authorize(homeDir: string, token: string | null): Promise<{ ok: boolean }> {
268
391
  const expected = await readDaemonToken(homeDir);
269
392
  return { ok: expected !== null && token !== null && token === expected };
@@ -282,33 +405,6 @@ function isAllowedLocalOrigin(origin: string | null): boolean {
282
405
  }
283
406
  }
284
407
 
285
- async function collectTaskSummaries(homeDir: string, warnings: string[]): Promise<unknown[]> {
286
- const root = join(homeDir, ".evodev", "STATE", "tasks");
287
- if (!(await pathExists(root))) {
288
- warnings.push("Task store not found; returning empty tasks.");
289
- return [];
290
- }
291
- const contracts = await collectNamedFiles(root, "contract.json");
292
- const summaries: unknown[] = [];
293
- for (const file of contracts) {
294
- try {
295
- const contract = JSON.parse(await readFile(file, "utf8"));
296
- summaries.push(
297
- sanitizeMetadata({
298
- taskId: contract.taskId,
299
- status: contract.status,
300
- mode: contract.route?.mode ?? null,
301
- workflowId: contract.route?.workflowId ?? null,
302
- verificationStatus: contract.verification?.status ?? null,
303
- }),
304
- );
305
- } catch {
306
- warnings.push(`Skipped unreadable task contract: ${file}`);
307
- }
308
- }
309
- return summaries;
310
- }
311
-
312
408
  async function collectObservabilitySummaries(
313
409
  homeDir: string,
314
410
  warnings: string[],
@@ -352,6 +448,258 @@ async function collectLearningCandidateSummaries(
352
448
  });
353
449
  }
354
450
 
451
+ async function collectEvolutionTriggers(homeDir: string, warnings: string[]): Promise<unknown[]> {
452
+ const lastError = await readDaemonEvolutionProcessError(homeDir).catch(() => null);
453
+ if (lastError !== null) warnings.push(`Evolution processor last error: ${lastError}`);
454
+ try {
455
+ return (await listEvolutionTriggers({ homeDir })).map((trigger) =>
456
+ sanitizeMetadata({
457
+ id: trigger.id,
458
+ projectKey: trigger.projectKey,
459
+ runId: trigger.runId,
460
+ roleId: trigger.roleId,
461
+ taskId: trigger.taskId,
462
+ eventType: trigger.eventType,
463
+ triggerStrength: trigger.triggerStrength,
464
+ triggerReason: trigger.triggerReason,
465
+ status: trigger.status,
466
+ attempts: trigger.attempts,
467
+ createdAt: trigger.createdAt,
468
+ updatedAt: trigger.updatedAt,
469
+ }),
470
+ );
471
+ } catch (error) {
472
+ warnings.push(`Evolution triggers unavailable: ${describeError(error)}`);
473
+ return [];
474
+ }
475
+ }
476
+
477
+ async function processDashboardEvolutionTriggers(homeDir: string, body: unknown): Promise<unknown> {
478
+ if (body === undefined) {
479
+ return {
480
+ ok: false,
481
+ error: "evolution process requires JSON body with dryRun=true or confirm=true.",
482
+ };
483
+ }
484
+ const input = expectRequestBody(body);
485
+ const dryRun = optionalBodyBoolean(input.dryRun) === true;
486
+ const confirm = optionalBodyBoolean(input.confirm) === true;
487
+ if (!dryRun && !confirm) {
488
+ return {
489
+ ok: false,
490
+ error: "evolution process requires dryRun=true or confirm=true.",
491
+ };
492
+ }
493
+ return processEvolutionTriggers({
494
+ homeDir,
495
+ projectKey: optionalBodyString(input.projectKey),
496
+ runId: optionalBodyString(input.runId),
497
+ limit: optionalBodyNumber(input.limit),
498
+ dryRun,
499
+ });
500
+ }
501
+
502
+ async function collectTeamRuns(homeDir: string, warnings: string[]): Promise<unknown[]> {
503
+ try {
504
+ return (await listTeamRuns({ homeDir })).map((run) =>
505
+ sanitizeMetadata({
506
+ runId: run.runId,
507
+ status: run.status,
508
+ repoRoot: run.repoRoot,
509
+ mainAgentId: run.mainAgentId,
510
+ session: run.tmux.session,
511
+ roleCount: Object.keys(run.roles).length,
512
+ createdAt: run.createdAt,
513
+ updatedAt: run.updatedAt,
514
+ }),
515
+ );
516
+ } catch (error) {
517
+ warnings.push(`Team runs unavailable: ${describeError(error)}`);
518
+ return [];
519
+ }
520
+ }
521
+
522
+ async function collectTeamStatus(
523
+ homeDir: string,
524
+ warnings: string[],
525
+ runtimeAdapter?: TeamRuntimeAdapter,
526
+ ): Promise<DaemonTeamStatusSummary> {
527
+ try {
528
+ const status = await reconcileTeamRun({ homeDir, runtimeAdapter });
529
+ return sanitizeMetadata({
530
+ run:
531
+ status.run === null
532
+ ? null
533
+ : {
534
+ runId: status.run.runId,
535
+ status: status.run.status,
536
+ repoRoot: status.run.repoRoot,
537
+ session: status.run.tmux.session,
538
+ updatedAt: status.run.updatedAt,
539
+ },
540
+ agents: status.agents.map((agent) => ({
541
+ roleId: agent.roleId,
542
+ roleName: agent.roleName,
543
+ runId: agent.runId,
544
+ runtime: agent.runtime,
545
+ model: agent.model,
546
+ thinkingLevel: agent.thinkingLevel,
547
+ status: agent.status,
548
+ paneId: agent.tmux.paneId,
549
+ canReceiveMessages: isActiveTeamAgentStatus(agent.status),
550
+ isIdle: isIdleTeamAgentStatus(agent.status),
551
+ isMidTurn: isMidTurnTeamAgentStatus(agent.status),
552
+ nativeSessionRecorded: agent.nativeSession.sessionId !== null,
553
+ updatedAt: agent.updatedAt,
554
+ })),
555
+ stoppedAgents: status.stoppedAgents.map((agent) => agent.roleId),
556
+ notifications: status.notifications.map((notification) => ({
557
+ ok: notification.ok,
558
+ error: notification.error ?? null,
559
+ delivery: notification.delivery ?? null,
560
+ queuedFor: notification.queuedFor ?? null,
561
+ })),
562
+ }) as DaemonTeamStatusSummary;
563
+ } catch (error) {
564
+ warnings.push(`Team status unavailable: ${describeError(error)}`);
565
+ return { run: null, agents: [], stoppedAgents: [], notifications: [] };
566
+ }
567
+ }
568
+
569
+ async function sendDashboardTeamMessage(
570
+ homeDir: string,
571
+ body: unknown,
572
+ runtimeAdapter?: TeamRuntimeAdapter,
573
+ ): Promise<unknown> {
574
+ const input = expectRequestBody(body);
575
+ return sendTeamMessage({
576
+ homeDir,
577
+ runId: optionalBodyString(input.runId),
578
+ fromRoleId: "user",
579
+ toRoleId: expectBodyString(input, "toRoleId"),
580
+ message: expectBodyString(input, "message"),
581
+ runtimeAdapter,
582
+ });
583
+ }
584
+
585
+ async function updateDashboardTeamAgentState(homeDir: string, body: unknown): Promise<unknown> {
586
+ const input = expectRequestBody(body);
587
+ const result = await updateTeamAgentHookState({
588
+ homeDir,
589
+ runId: expectBodyString(input, "runId"),
590
+ roleId: expectBodyString(input, "roleId"),
591
+ hookEvent: expectBodyString(input, "hookEvent"),
592
+ });
593
+ return {
594
+ ok: true,
595
+ roleId: result.agent.roleId,
596
+ status: result.agent.status,
597
+ statusPath: result.statusPath,
598
+ };
599
+ }
600
+
601
+ async function claimDashboardTeamMessages(homeDir: string, body: unknown): Promise<unknown> {
602
+ const input = expectRequestBody(body);
603
+ const runId = expectBodyString(input, "runId");
604
+ const roleId = expectBodyString(input, "roleId");
605
+ const messages = await readPendingTeamMessagesForRole({
606
+ homeDir,
607
+ runId,
608
+ roleId,
609
+ limit: optionalBodyNumber(input.limit),
610
+ });
611
+ return {
612
+ runId,
613
+ roleId,
614
+ messages,
615
+ };
616
+ }
617
+
618
+ async function markDashboardTeamMessagesDelivered(
619
+ homeDir: string,
620
+ body: unknown,
621
+ ): Promise<unknown> {
622
+ const input = expectRequestBody(body);
623
+ const messageIdsValue = input.messageIds;
624
+ if (!Array.isArray(messageIdsValue)) throw new Error("Expected body.messageIds array.");
625
+ const messageIds = messageIdsValue.map((value) => {
626
+ if (typeof value !== "string" || value.length === 0) {
627
+ throw new Error("Expected body.messageIds to contain non-empty strings.");
628
+ }
629
+ return value;
630
+ });
631
+ await markTeamMessagesDelivered({
632
+ homeDir,
633
+ runId: expectBodyString(input, "runId"),
634
+ roleId: expectBodyString(input, "roleId"),
635
+ messageIds,
636
+ });
637
+ return { ok: true, delivered: messageIds };
638
+ }
639
+
640
+ async function spawnDashboardTeamRole(
641
+ homeDir: string,
642
+ body: unknown,
643
+ runtimeAdapter?: TeamRuntimeAdapter,
644
+ ): Promise<unknown> {
645
+ const input = expectRequestBody(body);
646
+ const roleId = expectBodyString(input, "roleId");
647
+ const status = await reconcileTeamRun({
648
+ homeDir,
649
+ runId: optionalBodyString(input.runId),
650
+ runtimeAdapter,
651
+ notifyMain: false,
652
+ });
653
+ if (status.run === null) return { ok: false, error: "team-run-not-found" };
654
+ if (status.run.status !== "running") return { ok: false, error: "team-run-not-running" };
655
+ const result = await spawnTeamRole({
656
+ homeDir,
657
+ repoRoot: status.run.repoRoot,
658
+ runId: status.run.runId,
659
+ roleId,
660
+ runtimeAdapter,
661
+ });
662
+ return { ok: true, created: result.created, agent: result.agent };
663
+ }
664
+
665
+ async function stopDashboardTeamRole(
666
+ homeDir: string,
667
+ body: unknown,
668
+ runtimeAdapter?: TeamRuntimeAdapter,
669
+ ): Promise<unknown> {
670
+ const input = expectRequestBody(body);
671
+ return stopTeamRole({
672
+ homeDir,
673
+ runId: optionalBodyString(input.runId),
674
+ roleId: expectBodyString(input, "roleId"),
675
+ runtimeAdapter,
676
+ });
677
+ }
678
+
679
+ async function resumeDashboardTeamRun(
680
+ homeDir: string,
681
+ body: unknown,
682
+ runtimeAdapter?: TeamRuntimeAdapter,
683
+ ): Promise<unknown> {
684
+ const input = expectRequestBody(body);
685
+ const decision = optionalBodyString(input.onSessionMissing);
686
+ if (decision !== undefined && !["ask", "fail", "recreate", "resume-latest"].includes(decision)) {
687
+ return { ok: false, error: "invalid-on-session-missing" };
688
+ }
689
+ const result = await resumeTeamRun({
690
+ homeDir,
691
+ runId: optionalBodyString(input.runId),
692
+ runtimeAdapter,
693
+ missingSessionDecision: decision as "ask" | "fail" | "recreate" | "resume-latest" | undefined,
694
+ });
695
+ return {
696
+ ok: result.decisionRequired.length === 0,
697
+ run: result.run,
698
+ outcomes: result.outcomes,
699
+ decisionRequired: result.decisionRequired,
700
+ };
701
+ }
702
+
355
703
  async function collectDirectorySummaries(root: string, warnings: string[]): Promise<unknown[]> {
356
704
  if (!(await pathExists(root))) {
357
705
  warnings.push(`Store not found: ${root}`);
@@ -379,15 +727,89 @@ function sanitizeMetadata(value: unknown): unknown {
379
727
  return output;
380
728
  }
381
729
 
382
- async function collectNamedFiles(root: string, name: string): Promise<string[]> {
383
- const entries = await readdir(root, { withFileTypes: true });
384
- const files: string[] = [];
385
- for (const entry of entries) {
386
- const path = join(root, entry.name);
387
- if (entry.isDirectory()) files.push(...(await collectNamedFiles(path, name)));
388
- else if (entry.isFile() && entry.name === name) files.push(path);
730
+ function dashboardMutation(
731
+ data: unknown,
732
+ warnings: string[],
733
+ ): { status: number; body: DaemonResponseBody } {
734
+ const okResult = !(
735
+ typeof data === "object" &&
736
+ data !== null &&
737
+ "ok" in data &&
738
+ (data as { ok?: unknown }).ok === false
739
+ );
740
+ return {
741
+ status: okResult ? 200 : 400,
742
+ body: { ok: okResult, data: sanitizeMetadata(data), warnings },
743
+ };
744
+ }
745
+
746
+ function expectRequestBody(value: unknown): Record<string, unknown> {
747
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
748
+ throw new Error("Expected JSON object request body.");
389
749
  }
390
- return files;
750
+ return value as Record<string, unknown>;
751
+ }
752
+
753
+ function expectBodyString(input: Record<string, unknown>, key: string): string {
754
+ const value = input[key];
755
+ if (typeof value !== "string" || value.length === 0) {
756
+ throw new Error(`Expected non-empty string body.${key}.`);
757
+ }
758
+ return value;
759
+ }
760
+
761
+ function optionalBodyString(value: unknown): string | undefined {
762
+ return typeof value === "string" && value.length > 0 ? value : undefined;
763
+ }
764
+
765
+ function optionalBodyNumber(value: unknown): number | undefined {
766
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
767
+ }
768
+
769
+ function optionalBodyBoolean(value: unknown): boolean | undefined {
770
+ return typeof value === "boolean" ? value : undefined;
771
+ }
772
+
773
+ function resolveDaemonEvolutionProcessErrorPath(homeDir: string): string {
774
+ return join(resolveDaemonPaths(homeDir).rootDir, "evolution-process-error.json");
775
+ }
776
+
777
+ async function recordDaemonEvolutionProcessError(homeDir: string, error: unknown): Promise<void> {
778
+ try {
779
+ const path = resolveDaemonEvolutionProcessErrorPath(homeDir);
780
+ await mkdir(dirname(path), { recursive: true });
781
+ await writeFile(
782
+ path,
783
+ `${JSON.stringify(
784
+ {
785
+ schemaVersion: 1,
786
+ kind: "daemon-evolution-process-error",
787
+ updatedAt: new Date().toISOString(),
788
+ summary: sanitizeMetadata(describeError(error)),
789
+ },
790
+ null,
791
+ 2,
792
+ )}\n`,
793
+ { encoding: "utf8", flag: "w", mode: 0o600 },
794
+ );
795
+ } catch {
796
+ // Daemon background diagnostics must not affect foreground request handling.
797
+ }
798
+ }
799
+
800
+ async function clearDaemonEvolutionProcessError(homeDir: string): Promise<void> {
801
+ await rm(resolveDaemonEvolutionProcessErrorPath(homeDir), { force: true }).catch(() => undefined);
802
+ }
803
+
804
+ async function readDaemonEvolutionProcessError(homeDir: string): Promise<string | null> {
805
+ const path = resolveDaemonEvolutionProcessErrorPath(homeDir);
806
+ if (!(await pathExists(path))) return null;
807
+ const value = JSON.parse(await readFile(path, "utf8")) as { summary?: unknown };
808
+ return typeof value.summary === "string" && value.summary.length > 0 ? value.summary : null;
809
+ }
810
+
811
+ function describeError(error: unknown): string {
812
+ return error instanceof Error ? error.message : String(error);
391
813
  }
392
814
 
393
815
  function ok(data: unknown, warnings: string[]): { status: number; body: DaemonResponseBody } {