@evo-dev/core 0.0.1-alpha.1 → 0.0.1-alpha.11

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 (66) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
  2. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
  3. package/assets/team/agents/code-reviewer.md +48 -0
  4. package/assets/team/agents/docs-maintainer.md +51 -0
  5. package/assets/team/agents/implementation-engineer.md +51 -0
  6. package/assets/team/agents/product-scope-analyst.md +58 -0
  7. package/assets/team/agents/release-engineer.md +55 -0
  8. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  9. package/assets/team/agents/solution-architect.md +51 -0
  10. package/assets/team/agents/verification-engineer.md +51 -0
  11. package/assets/team/team.md +102 -0
  12. package/dist/config/index.js +925 -97
  13. package/dist/index.js +13107 -5618
  14. package/package.json +5 -1
  15. package/src/agents/index.ts +56 -264
  16. package/src/code-agent-traces/index.ts +520 -0
  17. package/src/config/index.ts +5 -0
  18. package/src/config/paths.ts +1 -1
  19. package/src/config/settings.ts +149 -0
  20. package/src/config/store.ts +2 -0
  21. package/src/daemon/index.ts +99 -50
  22. package/src/evolution/candidates/index.ts +564 -0
  23. package/src/evolution/control/index.ts +20 -0
  24. package/src/evolution/evidence/analysis.ts +533 -0
  25. package/src/evolution/evidence/index.ts +3 -0
  26. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +7 -0
  29. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  30. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  31. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  32. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  33. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  34. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  35. package/src/evolution/evidence/session-memory/types.ts +221 -0
  36. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  37. package/src/evolution/formatters.ts +169 -0
  38. package/src/evolution/index.ts +16 -2356
  39. package/src/evolution/knowledge/index.ts +5427 -0
  40. package/src/evolution/paths.ts +44 -0
  41. package/src/evolution/processor/distillation.ts +518 -0
  42. package/src/evolution/processor/index.ts +3 -0
  43. package/src/evolution/processor/process.ts +528 -0
  44. package/src/{learning → evolution/review}/index.ts +10 -14
  45. package/src/evolution/schema.ts +568 -0
  46. package/src/evolution/shared.ts +758 -0
  47. package/src/evolution/triggers/classification.ts +102 -0
  48. package/src/evolution/triggers/index.ts +295 -0
  49. package/src/hooks/index.ts +438 -179
  50. package/src/index.ts +12 -3
  51. package/src/projects/index.ts +453 -0
  52. package/src/runtime-logs/index.ts +490 -24
  53. package/src/team/index.ts +1429 -185
  54. package/src/team/mcp.ts +9 -5
  55. package/src/team/prompts.ts +141 -0
  56. package/src/utils/errors.ts +13 -0
  57. package/src/utils/fs.ts +40 -0
  58. package/src/utils/hash.ts +9 -0
  59. package/src/utils/ids.ts +12 -0
  60. package/src/utils/index.ts +7 -0
  61. package/src/utils/parsing.ts +11 -0
  62. package/src/utils/text.ts +18 -0
  63. package/src/utils/time.ts +5 -0
  64. package/src/workflow/index.ts +3 -21
  65. package/src/project/index.ts +0 -507
  66. package/src/task/index.ts +0 -840
package/src/team/index.ts CHANGED
@@ -1,15 +1,39 @@
1
1
  import { spawn } from "node:child_process";
2
- import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
2
+ import { appendFile, cp, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { resolveEvoDevPaths } from "../config/paths.ts";
5
- import { createDefaultSettings, parseSettings } from "../config/settings.ts";
5
+ import {
6
+ createDefaultSettings,
7
+ parseSettings,
8
+ readRuntimeInjectionSettings,
9
+ } from "../config/settings.ts";
10
+ import {
11
+ createScopedKnowledgeContextPack,
12
+ formatScopedKnowledgePromptBlock,
13
+ hasContextInjectionReceipt,
14
+ writeContextInjectionReceipt,
15
+ } from "../evolution/knowledge/index.ts";
6
16
  import { resolveProjectLogKey } from "../runtime-logs/index.ts";
17
+ import { renderTeamRoleStartupPrompt } from "./prompts.ts";
7
18
 
8
19
  export type TeamAgentRuntime = "codex" | "claude";
9
- export type TeamAgentStatus = "running" | "recovering" | "recreated" | "stopped" | "failed";
20
+ export type TeamAgentStatus =
21
+ | "starting"
22
+ | "running"
23
+ | "busy"
24
+ | "idle"
25
+ | "waiting-input"
26
+ | "recovering"
27
+ | "recreated"
28
+ | "stopped"
29
+ | "exited"
30
+ | "needs-user-attention"
31
+ | "failed"
32
+ | "unknown";
10
33
  export type TeamRunStatus = "running" | "stopped" | "failed";
11
34
  export type TeamWriteMode = "read-only" | "repo-write" | "worktree-write" | "disabled";
12
35
  export type TeamMessageType = "request" | "result" | "issue" | "notice";
36
+ export type TeamMessageDeliveryState = "pending" | "claimed" | "wakeup-sent" | "failed";
13
37
  export type TeamRuntimeAgentRecoveryMode =
14
38
  | { type: "resume"; sessionId: string }
15
39
  | { type: "resume-latest" }
@@ -32,6 +56,7 @@ export interface TeamRuntimeCommandRunner {
32
56
 
33
57
  export interface TmuxRuntimeAdapterOptions {
34
58
  runtimeCommands?: Partial<Record<TeamAgentRuntime, string>>;
59
+ environment?: Record<string, string | undefined>;
35
60
  }
36
61
 
37
62
  export interface TeamRuntimeAdapter {
@@ -107,6 +132,12 @@ export interface TeamRuntimeAttachInput {
107
132
  paneId?: string;
108
133
  }
109
134
 
135
+ export interface TeamRoleRuntimeContextInput {
136
+ homeDir?: string;
137
+ runId: string;
138
+ roleId: string;
139
+ }
140
+
110
141
  export interface TeamRolePermissions {
111
142
  writeMode: TeamWriteMode;
112
143
  canUseTeamsMcp: boolean;
@@ -130,7 +161,7 @@ export interface TeamRoleDefinition {
130
161
  prompt: string;
131
162
  permissions: TeamRolePermissions;
132
163
  teamPolicy: TeamRolePolicy;
133
- source: "builtin" | "global";
164
+ source: "builtin" | "global" | "overlay";
134
165
  }
135
166
 
136
167
  export interface ResolvedTeamRole extends TeamRoleDefinition {
@@ -147,6 +178,59 @@ export interface TeamNativeAgentBinding {
147
178
  updatedAt: string;
148
179
  }
149
180
 
181
+ export type TeamOverlaySource = "repo" | "global" | "builtin";
182
+
183
+ export interface TeamDefinition {
184
+ version: 1;
185
+ name: string;
186
+ description: string;
187
+ agents: Record<string, string>;
188
+ body: string;
189
+ }
190
+
191
+ export interface TeamOverlayResolution {
192
+ source: TeamOverlaySource;
193
+ teamPath: string | null;
194
+ definition: TeamDefinition;
195
+ }
196
+
197
+ export interface TeamAgentReference {
198
+ roleId: string;
199
+ reference: string;
200
+ sourcePath: string;
201
+ scope: "repo" | "global";
202
+ }
203
+
204
+ export interface TeamOverlayAgentSummary {
205
+ roleId: string;
206
+ name: string;
207
+ description: string;
208
+ sourcePath: string;
209
+ }
210
+
211
+ export interface TeamAgentDefinition {
212
+ roleId: string;
213
+ name: string;
214
+ description: string;
215
+ runtime: TeamAgentRuntime | null;
216
+ model: string | null;
217
+ thinkingLevel: string | null;
218
+ writeMode: TeamWriteMode | null;
219
+ skills: string[];
220
+ sourcePath: string;
221
+ markdown: string;
222
+ }
223
+
224
+ export interface DefaultTeamOverlayFileResult {
225
+ sourcePath: string;
226
+ targetPath: string;
227
+ written: boolean;
228
+ }
229
+
230
+ export interface EnsureDefaultTeamOverlayResult {
231
+ files: DefaultTeamOverlayFileResult[];
232
+ }
233
+
150
234
  export interface TeamRoleBindingRecord {
151
235
  version: 1;
152
236
  roleId: string;
@@ -219,6 +303,18 @@ export interface TeamMessageRecord {
219
303
  createdAt: string;
220
304
  }
221
305
 
306
+ export interface TeamPendingMessageRecord extends TeamMessageRecord {
307
+ deliveryState: TeamMessageDeliveryState;
308
+ attemptCount: number;
309
+ lastAttemptAt: string | null;
310
+ }
311
+
312
+ export interface TeamMessageListFile {
313
+ version: 1;
314
+ updatedAt: string | null;
315
+ messages: TeamPendingMessageRecord[];
316
+ }
317
+
222
318
  export interface TeamEventRecord {
223
319
  version: 1;
224
320
  eventId: string;
@@ -230,7 +326,9 @@ export interface TeamEventRecord {
230
326
  | "TeamMessageAccepted"
231
327
  | "TeamMessageDelivered"
232
328
  | "TeamMessageDeliveryFailed"
329
+ | "TeamMessageWakeupSent"
233
330
  | "AgentNativeSessionRecorded"
331
+ | "AgentHookStateUpdated"
234
332
  | "AgentRecoveryDecisionRequired"
235
333
  | "AgentRecovered"
236
334
  | "AgentRecreated"
@@ -242,6 +340,29 @@ export interface TeamEventRecord {
242
340
  createdAt: string;
243
341
  }
244
342
 
343
+ export interface TeamRunStatusSnapshot {
344
+ version: 1;
345
+ runId: string;
346
+ repoRoot: string;
347
+ runStatus: TeamRunStatus;
348
+ tmux: {
349
+ session: string;
350
+ };
351
+ agents: Array<{
352
+ roleId: string;
353
+ agentId: string;
354
+ roleName: string;
355
+ runtime: TeamAgentRuntime;
356
+ status: TeamAgentStatus;
357
+ paneId: string;
358
+ window: string;
359
+ nativeSessionId: string | null;
360
+ updatedAt: string;
361
+ }>;
362
+ lastEvent: string | null;
363
+ updatedAt: string;
364
+ }
365
+
245
366
  export interface StartTeamRunInput {
246
367
  homeDir?: string;
247
368
  repoRoot: string;
@@ -432,7 +553,8 @@ export async function unsetTeamRoleBinding(input: {
432
553
  export interface TeamMessageSendResult {
433
554
  ok: boolean;
434
555
  messageId?: string;
435
- deliveredTo?: string;
556
+ delivery?: "queued";
557
+ queuedFor?: string;
436
558
  cc?: string[];
437
559
  error?: string;
438
560
  message?: string;
@@ -444,6 +566,29 @@ export interface TeamMessageBrokerOptions {
444
566
  }
445
567
 
446
568
  export type TeamMessageBrokerSendInput = Omit<SendTeamMessageInput, "homeDir" | "runtimeAdapter">;
569
+ export interface ReadPendingTeamMessagesInput {
570
+ homeDir?: string;
571
+ runId: string;
572
+ roleId: string;
573
+ limit?: number;
574
+ }
575
+
576
+ export interface MarkTeamMessagesDeliveredInput {
577
+ homeDir?: string;
578
+ runId: string;
579
+ roleId: string;
580
+ messageIds: string[];
581
+ now?: Date;
582
+ }
583
+
584
+ export interface UpdateTeamAgentHookStateInput {
585
+ homeDir?: string;
586
+ runId: string;
587
+ roleId: string;
588
+ hookEvent: string;
589
+ now?: Date;
590
+ }
591
+
447
592
  export interface TeamMessageBrokerSpawnInput {
448
593
  runId?: string;
449
594
  fromRoleId?: string;
@@ -488,12 +633,21 @@ export interface TeamRunResumeResult extends TeamStatusResult {
488
633
  notifications: TeamMessageSendResult[];
489
634
  }
490
635
 
636
+ export interface TeamMessageDeliveryScheduleResult {
637
+ wokenRoleIds: string[];
638
+ recoveredRoleIds: string[];
639
+ needsUserAttentionRoleIds: string[];
640
+ warnings: string[];
641
+ }
642
+
491
643
  export interface TeamAgentSummary {
492
644
  roleId: string;
493
645
  roleName: string;
494
646
  status: TeamAgentStatus;
495
647
  runtime: TeamAgentRuntime;
496
648
  canReceiveMessages: boolean;
649
+ isIdle: boolean;
650
+ isMidTurn: boolean;
497
651
  }
498
652
 
499
653
  export interface TeamRunStore {
@@ -506,11 +660,24 @@ export interface TeamRunStore {
506
660
  writeAgent(agent: TeamAgentRecord): Promise<void>;
507
661
  readAgent(runId: string, roleId: string): Promise<TeamAgentRecord>;
508
662
  readAgents(runId: string): Promise<TeamAgentRecord[]>;
663
+ readMessages(runId: string): Promise<TeamMessageRecord[]>;
664
+ readEvents(runId: string): Promise<TeamEventRecord[]>;
665
+ readMessageList(runId: string): Promise<TeamPendingMessageRecord[]>;
666
+ writeMessageList(
667
+ runId: string,
668
+ messages: TeamPendingMessageRecord[],
669
+ updatedAt: string,
670
+ ): Promise<void>;
671
+ writeStatus(runId: string, snapshot: TeamRunStatusSnapshot): Promise<void>;
509
672
  appendMessage(runId: string, message: TeamMessageRecord): Promise<void>;
510
673
  appendEvent(runId: string, event: TeamEventRecord): Promise<void>;
511
674
  }
512
675
 
513
676
  const SAFE_ROLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
677
+ export const TEAM_INTERNAL_WAKE_SIGNAL = [
678
+ "[EvoDev internal wake signal]",
679
+ "No user request is included in this message. Continue only from EvoDev team inbox messages injected by hooks.",
680
+ ].join("\n");
514
681
  const BUILT_IN_ROLE_PROMPTS: Record<
515
682
  string,
516
683
  { roleName: string; description: string; prompt: string }
@@ -519,27 +686,43 @@ const BUILT_IN_ROLE_PROMPTS: Record<
519
686
  roleName: "Main Conductor",
520
687
  description: "Coordinates the EvoDev team run and owns final synthesis.",
521
688
  prompt:
522
- "You are the main conductor for this EvoDev team run. Coordinate role agents, keep decisions explicit, and synthesize final outcomes.",
689
+ "You are the main conductor for this EvoDev team run. Decide whether the user request actually needs team execution. If it does, plan required role agents, role assignments, dependencies, and runnable batches before spawning roles. Coordinate role agents, keep decisions explicit, and synthesize final outcomes. Once work is delegated to role agents, do not perform implementation, testing, or review details yourself.",
523
690
  },
524
691
  reviewer: {
525
692
  roleName: "Code Reviewer",
526
693
  description: "Reviews implementation quality, risks, and regressions.",
527
694
  prompt:
528
- "You are the code reviewer for this EvoDev team run. Review changes and report concrete findings.",
695
+ "You are the code reviewer for this EvoDev team run. Review assigned changes and report concrete findings to the role that can act on them; notify main only when coordination or final synthesis is needed.",
529
696
  },
530
697
  tester: {
531
698
  roleName: "Test Engineer",
532
699
  description: "Verifies behavior and identifies test gaps.",
533
700
  prompt:
534
- "You are the test engineer for this EvoDev team run. Run or recommend focused verification and report gaps.",
701
+ "You are the test engineer for this EvoDev team run. Run or recommend focused verification and report gaps to the implementing role when they need action; notify main only when coordination or final synthesis is needed.",
535
702
  },
536
703
  executor: {
537
704
  roleName: "Implementation Executor",
538
705
  description: "Implements scoped changes assigned by main.",
539
706
  prompt:
540
- "You are the implementation executor for this EvoDev team run. Keep changes scoped and report verification evidence.",
707
+ "You are the implementation executor for this EvoDev team run. Keep changes scoped, avoid taking conductor decisions, and report changed files plus verification evidence.",
541
708
  },
542
709
  };
710
+ const BUILT_IN_TEAM_DEFINITION: TeamDefinition = {
711
+ version: 1,
712
+ name: "builtin-minimal-team",
713
+ description: "Built-in minimal EvoDev team fallback.",
714
+ agents: {
715
+ executor: "builtin:executor",
716
+ reviewer: "builtin:reviewer",
717
+ tester: "builtin:tester",
718
+ },
719
+ body: [
720
+ "# Built-in Minimal Team",
721
+ "",
722
+ "Use role agents only when delegation improves correctness, coverage, safety, or latency.",
723
+ "Spawn roles on demand and send self-contained assignments through Teams MCP.",
724
+ ].join("\n"),
725
+ };
543
726
 
544
727
  export function createTeamRunStore(homeDir?: string): TeamRunStore {
545
728
  const paths = resolveTeamRunPaths(homeDir);
@@ -547,6 +730,7 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
547
730
  return {
548
731
  paths,
549
732
  async createRunDirs(runId) {
733
+ await migrateLegacyRunDirIfNeeded(paths, runId);
550
734
  await mkdir(paths.runDir(runId), { recursive: true });
551
735
  await mkdir(paths.agentsDir(runId), { recursive: true });
552
736
  },
@@ -555,6 +739,7 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
555
739
  await writeJson(paths.runPath(run.runId), parseTeamRunRecord(run));
556
740
  },
557
741
  async readRun(runId) {
742
+ await migrateLegacyRunDirIfNeeded(paths, runId);
558
743
  return parseTeamRunRecord(JSON.parse(await readFile(paths.runPath(runId), "utf8")));
559
744
  },
560
745
  async writeLatestRunId(runId) {
@@ -567,7 +752,17 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
567
752
  if (latest?.version === 1 && typeof latest.runId === "string") return latest.runId;
568
753
  return null;
569
754
  } catch {
570
- return null;
755
+ try {
756
+ const latest = JSON.parse(await readFile(paths.legacyLatestRunPath, "utf8"));
757
+ if (latest?.version === 1 && typeof latest.runId === "string") {
758
+ await migrateLegacyRunDirIfNeeded(paths, latest.runId);
759
+ await this.writeLatestRunId(latest.runId);
760
+ return latest.runId;
761
+ }
762
+ return null;
763
+ } catch {
764
+ return null;
765
+ }
571
766
  }
572
767
  },
573
768
  async writeAgent(agent) {
@@ -575,11 +770,13 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
575
770
  await writeJson(paths.agentPath(agent.runId, agent.roleId), parseTeamAgentRecord(agent));
576
771
  },
577
772
  async readAgent(runId, roleId) {
773
+ await migrateLegacyRunDirIfNeeded(paths, runId);
578
774
  return parseTeamAgentRecord(
579
775
  JSON.parse(await readFile(paths.agentPath(runId, roleId), "utf8")),
580
776
  );
581
777
  },
582
778
  async readAgents(runId) {
779
+ await migrateLegacyRunDirIfNeeded(paths, runId);
583
780
  try {
584
781
  const entries = await readdir(paths.agentsDir(runId));
585
782
  const agents = await Promise.all(
@@ -596,9 +793,51 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
596
793
  return [];
597
794
  }
598
795
  },
796
+ async readMessages(runId) {
797
+ await migrateLegacyRunDirIfNeeded(paths, runId);
798
+ return readJsonLines(paths.messagesPath(runId), parseTeamMessageRecord);
799
+ },
800
+ async readEvents(runId) {
801
+ await migrateLegacyRunDirIfNeeded(paths, runId);
802
+ return readJsonLines(paths.eventsPath(runId), parseTeamEventRecord);
803
+ },
804
+ async readMessageList(runId) {
805
+ await migrateLegacyRunDirIfNeeded(paths, runId);
806
+ try {
807
+ const file = parseTeamMessageListFile(
808
+ JSON.parse(await readFile(paths.messageListPath(runId), "utf8")),
809
+ );
810
+ return file.messages;
811
+ } catch (error) {
812
+ if (isNotFoundError(error)) return [];
813
+ throw error;
814
+ }
815
+ },
816
+ async writeMessageList(runId, messages, updatedAt) {
817
+ await this.createRunDirs(runId);
818
+ await writeJson(paths.messageListPath(runId), {
819
+ version: 1,
820
+ updatedAt,
821
+ messages: messages.map(parseTeamPendingMessageRecord),
822
+ });
823
+ },
824
+ async writeStatus(runId, snapshot) {
825
+ await this.createRunDirs(runId);
826
+ await writeJson(paths.statusPath(runId), snapshot);
827
+ },
599
828
  async appendMessage(runId, message) {
600
829
  await this.createRunDirs(runId);
601
- await appendJsonLine(paths.messagesPath(runId), parseTeamMessageRecord(message));
830
+ const parsed = parseTeamMessageRecord(message);
831
+ await appendJsonLine(paths.messagesPath(runId), parsed);
832
+ const pending = await this.readMessageList(runId);
833
+ await this.writeMessageList(
834
+ runId,
835
+ [
836
+ ...pending.filter((item) => item.messageId !== parsed.messageId),
837
+ createPendingMessageRecord(parsed),
838
+ ],
839
+ parsed.createdAt,
840
+ );
602
841
  },
603
842
  async appendEvent(runId, event) {
604
843
  await this.createRunDirs(runId);
@@ -609,23 +848,84 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
609
848
 
610
849
  export function resolveTeamRunPaths(homeDir?: string) {
611
850
  const paths = resolveEvoDevPaths(homeDir);
851
+ const legacyRunsDir = join(paths.rootDir, "runs");
612
852
  return {
613
853
  rootDir: paths.rootDir,
614
854
  roleAgentsDir: paths.roleAgentsDir,
615
855
  teamsDir: paths.teamsDir,
616
856
  runsDir: paths.runsDir,
857
+ legacyRunsDir,
617
858
  latestRunPath: paths.latestRunPath,
859
+ legacyLatestRunPath: join(legacyRunsDir, "latest.json"),
618
860
  runDir: (runId: string) => join(paths.runsDir, runId),
861
+ legacyRunDir: (runId: string) => join(legacyRunsDir, runId),
619
862
  runPath: (runId: string) => join(paths.runsDir, runId, "run.json"),
863
+ legacyRunPath: (runId: string) => join(legacyRunsDir, runId, "run.json"),
620
864
  tmuxPath: (runId: string) => join(paths.runsDir, runId, "tmux.json"),
865
+ legacyTmuxPath: (runId: string) => join(legacyRunsDir, runId, "tmux.json"),
621
866
  agentsDir: (runId: string) => join(paths.runsDir, runId, "agents"),
867
+ legacyAgentsDir: (runId: string) => join(legacyRunsDir, runId, "agents"),
622
868
  agentPath: (runId: string, roleId: string) =>
623
869
  join(paths.runsDir, runId, "agents", `${roleId}.json`),
870
+ legacyAgentPath: (runId: string, roleId: string) =>
871
+ join(legacyRunsDir, runId, "agents", `${roleId}.json`),
624
872
  messagesPath: (runId: string) => join(paths.runsDir, runId, "messages.jsonl"),
873
+ legacyMessagesPath: (runId: string) => join(legacyRunsDir, runId, "messages.jsonl"),
625
874
  eventsPath: (runId: string) => join(paths.runsDir, runId, "events.jsonl"),
875
+ legacyEventsPath: (runId: string) => join(legacyRunsDir, runId, "events.jsonl"),
876
+ statusPath: (runId: string) => join(paths.runsDir, runId, "status.json"),
877
+ messageListPath: (runId: string) => join(paths.runsDir, runId, "message-list.json"),
626
878
  };
627
879
  }
628
880
 
881
+ async function migrateLegacyRunDirIfNeeded(
882
+ paths: ReturnType<typeof resolveTeamRunPaths>,
883
+ runId: string,
884
+ ): Promise<void> {
885
+ const nextDir = paths.runDir(runId);
886
+ if (await pathExists(nextDir)) return;
887
+ const legacyDir = paths.legacyRunDir(runId);
888
+ if (!(await pathExists(legacyDir))) return;
889
+ await mkdir(dirname(nextDir), { recursive: true });
890
+ await cp(legacyDir, nextDir, { recursive: true, errorOnExist: false, force: false });
891
+ }
892
+
893
+ async function writeTeamStatusSnapshot(input: {
894
+ store: TeamRunStore;
895
+ runId: string;
896
+ now: string;
897
+ lastEvent?: string | null;
898
+ }): Promise<TeamRunStatusSnapshot | null> {
899
+ try {
900
+ const run = await input.store.readRun(input.runId);
901
+ const agents = await input.store.readAgents(run.runId);
902
+ const snapshot: TeamRunStatusSnapshot = {
903
+ version: 1,
904
+ runId: run.runId,
905
+ repoRoot: run.repoRoot,
906
+ runStatus: run.status,
907
+ tmux: { session: run.tmux.session },
908
+ agents: agents.map((agent) => ({
909
+ roleId: agent.roleId,
910
+ agentId: agent.agentId,
911
+ roleName: agent.roleName,
912
+ runtime: agent.runtime,
913
+ status: agent.status,
914
+ paneId: agent.tmux.paneId,
915
+ window: agent.tmux.window,
916
+ nativeSessionId: agent.nativeSession.sessionId,
917
+ updatedAt: agent.updatedAt,
918
+ })),
919
+ lastEvent: input.lastEvent ?? null,
920
+ updatedAt: input.now,
921
+ };
922
+ await input.store.writeStatus(run.runId, snapshot);
923
+ return snapshot;
924
+ } catch {
925
+ return null;
926
+ }
927
+ }
928
+
629
929
  export async function startTeamRun(input: StartTeamRunInput): Promise<TeamRunStartResult> {
630
930
  const store = createTeamRunStore(input.homeDir);
631
931
  const now = input.now ?? new Date();
@@ -639,11 +939,13 @@ export async function startTeamRun(input: StartTeamRunInput): Promise<TeamRunSta
639
939
  overrides: input.mainOverrides,
640
940
  });
641
941
  const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
642
- const startupPrompt = createAgentStartupPrompt({
942
+ const startupPrompt = await createAgentStartupPrompt({
943
+ homeDir: input.homeDir,
643
944
  runId,
644
945
  repoRoot: input.repoRoot,
645
946
  role,
646
947
  roster: [],
948
+ now: createdAt,
647
949
  });
648
950
  const handle = await runtimeAdapter.createRun({
649
951
  runId,
@@ -688,6 +990,12 @@ export async function startTeamRun(input: StartTeamRunInput): Promise<TeamRunSta
688
990
  agentId: agent.agentId,
689
991
  }),
690
992
  );
993
+ await writeTeamStatusSnapshot({
994
+ store,
995
+ runId,
996
+ now: createdAt,
997
+ lastEvent: "TeamRunStarted",
998
+ });
691
999
 
692
1000
  return { run, mainAgent: agent };
693
1001
  }
@@ -713,11 +1021,13 @@ export async function spawnTeamRole(input: SpawnTeamRoleInput): Promise<TeamRole
713
1021
  });
714
1022
  const agents = await store.readAgents(run.runId);
715
1023
  const mainAgent = agents.find((agent) => agent.roleId === "main");
716
- const startupPrompt = createAgentStartupPrompt({
1024
+ const startupPrompt = await createAgentStartupPrompt({
1025
+ homeDir: input.homeDir,
717
1026
  runId: run.runId,
718
1027
  repoRoot: run.repoRoot,
719
1028
  role,
720
1029
  roster: agents,
1030
+ now,
721
1031
  });
722
1032
  const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
723
1033
  const handle = await runtimeAdapter.spawnAgent({
@@ -744,6 +1054,12 @@ export async function spawnTeamRole(input: SpawnTeamRoleInput): Promise<TeamRole
744
1054
  agentId: agent.agentId,
745
1055
  }),
746
1056
  );
1057
+ await writeTeamStatusSnapshot({
1058
+ store,
1059
+ runId: run.runId,
1060
+ now,
1061
+ lastEvent: "AgentSpawned",
1062
+ });
747
1063
 
748
1064
  return { run: updatedRun, agent, created: true };
749
1065
  }
@@ -802,6 +1118,12 @@ export async function stopTeamRole(input: StopTeamRoleInput): Promise<TeamRoleSt
802
1118
  agentId,
803
1119
  }),
804
1120
  );
1121
+ await writeTeamStatusSnapshot({
1122
+ store,
1123
+ runId: run.runId,
1124
+ now,
1125
+ lastEvent: "AgentStopped",
1126
+ });
805
1127
 
806
1128
  return { ok: true, run: updatedRun, agent: stoppedAgent, stopped: true };
807
1129
  }
@@ -833,6 +1155,166 @@ export async function sendTeamMessage(input: SendTeamMessageInput): Promise<Team
833
1155
  }).send(input);
834
1156
  }
835
1157
 
1158
+ export async function readPendingTeamMessagesForRole(
1159
+ input: ReadPendingTeamMessagesInput,
1160
+ ): Promise<TeamMessageRecord[]> {
1161
+ const store = createTeamRunStore(input.homeDir);
1162
+ const messages = await store.readMessageList(input.runId);
1163
+ const pending = messages.filter(
1164
+ (message) => message.toRoleId === input.roleId && message.deliveryState !== "failed",
1165
+ );
1166
+ return pending.slice(0, input.limit ?? 5);
1167
+ }
1168
+
1169
+ export async function markTeamMessagesDelivered(
1170
+ input: MarkTeamMessagesDeliveredInput,
1171
+ ): Promise<void> {
1172
+ if (input.messageIds.length === 0) return;
1173
+ const store = createTeamRunStore(input.homeDir);
1174
+ const now = (input.now ?? new Date()).toISOString();
1175
+ const delivered = new Set(input.messageIds);
1176
+ const pending = await store.readMessageList(input.runId);
1177
+ await store.writeMessageList(
1178
+ input.runId,
1179
+ pending.filter((message) => !delivered.has(message.messageId)),
1180
+ now,
1181
+ );
1182
+ for (const messageId of input.messageIds) {
1183
+ assertSafeId(messageId, "messageId");
1184
+ await store.appendEvent(
1185
+ input.runId,
1186
+ createTeamEvent(input.runId, "TeamMessageDelivered", `Delivered message ${messageId}.`, now, {
1187
+ roleId: input.roleId,
1188
+ messageId,
1189
+ }),
1190
+ );
1191
+ }
1192
+ }
1193
+
1194
+ export async function schedulePendingTeamMessageDelivery(input: {
1195
+ homeDir?: string;
1196
+ runId?: string;
1197
+ roleId?: string;
1198
+ now?: Date;
1199
+ runtimeAdapter?: TeamRuntimeAdapter;
1200
+ }): Promise<TeamMessageDeliveryScheduleResult> {
1201
+ const store = createTeamRunStore(input.homeDir);
1202
+ const runId = await resolveRequestedRunId(store, input.runId);
1203
+ const run = await store.readRun(runId);
1204
+ const pending = await store.readMessageList(run.runId);
1205
+ const targetRoleIds = new Set(
1206
+ pending
1207
+ .filter((message) => input.roleId === undefined || message.toRoleId === input.roleId)
1208
+ .map((message) => message.toRoleId),
1209
+ );
1210
+ const result: TeamMessageDeliveryScheduleResult = {
1211
+ wokenRoleIds: [],
1212
+ recoveredRoleIds: [],
1213
+ needsUserAttentionRoleIds: [],
1214
+ warnings: [],
1215
+ };
1216
+ if (targetRoleIds.size === 0) return result;
1217
+
1218
+ const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
1219
+ const livePaneIds = await readLivePaneIds(runtimeAdapter, run.tmux.session);
1220
+ const now = (input.now ?? new Date()).toISOString();
1221
+
1222
+ for (const roleId of targetRoleIds) {
1223
+ const agent = await store.readAgent(run.runId, roleId).catch(() => null);
1224
+ if (agent === null) continue;
1225
+
1226
+ if (
1227
+ agent.roleId === "main" &&
1228
+ (run.status !== "running" || !isActiveTeamAgentStatus(agent.status))
1229
+ ) {
1230
+ const updated: TeamAgentRecord = {
1231
+ ...agent,
1232
+ status: "needs-user-attention",
1233
+ updatedAt: now,
1234
+ };
1235
+ await store.writeAgent(updated);
1236
+ await writeTeamStatusSnapshot({
1237
+ store,
1238
+ runId: run.runId,
1239
+ now,
1240
+ lastEvent: "AgentNeedsUserAttention",
1241
+ });
1242
+ result.needsUserAttentionRoleIds.push(roleId);
1243
+ continue;
1244
+ }
1245
+
1246
+ if (run.status !== "running") continue;
1247
+
1248
+ if (!isActiveTeamAgentStatus(agent.status)) {
1249
+ const resumed = await resumeTeamRun({
1250
+ homeDir: input.homeDir,
1251
+ runId: run.runId,
1252
+ now: new Date(now),
1253
+ runtimeAdapter,
1254
+ missingSessionDecision: "recreate",
1255
+ notifyMain: false,
1256
+ });
1257
+ const recovered = resumed.outcomes.find(
1258
+ (outcome) =>
1259
+ outcome.roleId === roleId &&
1260
+ (outcome.outcome === "resumed" || outcome.outcome === "recreated"),
1261
+ );
1262
+ if (recovered !== undefined) result.recoveredRoleIds.push(roleId);
1263
+ else result.warnings.push(`Role ${roleId} could not be automatically recovered.`);
1264
+ continue;
1265
+ }
1266
+
1267
+ if (isIdleTeamAgentStatus(agent.status) && livePaneIds.has(agent.tmux.paneId)) {
1268
+ await runtimeAdapter.sendInput({
1269
+ session: agent.tmux.session,
1270
+ paneId: agent.tmux.paneId,
1271
+ text: TEAM_INTERNAL_WAKE_SIGNAL,
1272
+ });
1273
+ await markPendingMessagesWakeupSent({
1274
+ store,
1275
+ runId: run.runId,
1276
+ roleId,
1277
+ now,
1278
+ });
1279
+ result.wokenRoleIds.push(roleId);
1280
+ }
1281
+ }
1282
+
1283
+ return result;
1284
+ }
1285
+
1286
+ async function markPendingMessagesWakeupSent(input: {
1287
+ store: TeamRunStore;
1288
+ runId: string;
1289
+ roleId: string;
1290
+ now: string;
1291
+ }): Promise<void> {
1292
+ const pending = await input.store.readMessageList(input.runId);
1293
+ const updated = pending.map((message) =>
1294
+ message.toRoleId === input.roleId
1295
+ ? {
1296
+ ...message,
1297
+ deliveryState: "wakeup-sent" as const,
1298
+ attemptCount: message.attemptCount + 1,
1299
+ lastAttemptAt: input.now,
1300
+ }
1301
+ : message,
1302
+ );
1303
+ await input.store.writeMessageList(input.runId, updated, input.now);
1304
+ for (const message of updated.filter((item) => item.toRoleId === input.roleId)) {
1305
+ await input.store.appendEvent(
1306
+ input.runId,
1307
+ createTeamEvent(
1308
+ input.runId,
1309
+ "TeamMessageWakeupSent",
1310
+ `Sent wake signal for message ${message.messageId}.`,
1311
+ input.now,
1312
+ { roleId: input.roleId, messageId: message.messageId },
1313
+ ),
1314
+ );
1315
+ }
1316
+ }
1317
+
836
1318
  async function sendTeamMessageWithBrokerContext(
837
1319
  options: TeamMessageBrokerOptions,
838
1320
  input: TeamMessageBrokerSendInput,
@@ -856,11 +1338,11 @@ async function sendTeamMessageWithBrokerContext(
856
1338
  };
857
1339
  }
858
1340
  const target = await store.readAgent(run.runId, input.toRoleId);
859
- if (!isActiveTeamAgentStatus(target.status)) {
1341
+ if (target.status === "failed" || target.status === "unknown") {
860
1342
  return {
861
1343
  ok: false,
862
- error: "target-role-not-running",
863
- message: `Role ${input.toRoleId} is not running in run ${run.runId}.`,
1344
+ error: "target-role-unavailable",
1345
+ message: `Role ${input.toRoleId} is not available in run ${run.runId}.`,
864
1346
  };
865
1347
  }
866
1348
 
@@ -896,58 +1378,21 @@ async function sendTeamMessageWithBrokerContext(
896
1378
  },
897
1379
  ),
898
1380
  );
1381
+ await schedulePendingTeamMessageDelivery({
1382
+ homeDir: options.homeDir,
1383
+ runId: run.runId,
1384
+ roleId: input.toRoleId,
1385
+ now: new Date(now),
1386
+ runtimeAdapter: options.runtimeAdapter,
1387
+ });
899
1388
 
900
- const runtimeAdapter = options.runtimeAdapter ?? createTmuxRuntimeAdapter();
901
- try {
902
- await runtimeAdapter.sendInput({
903
- session: target.tmux.session,
904
- paneId: target.tmux.paneId,
905
- text: formatDeliveredTeamMessage(message, false),
906
- });
907
- for (const ccRoleId of ccRoleIds) {
908
- const cc = await store.readAgent(run.runId, ccRoleId);
909
- if (isActiveTeamAgentStatus(cc.status)) {
910
- await runtimeAdapter.sendInput({
911
- session: cc.tmux.session,
912
- paneId: cc.tmux.paneId,
913
- text: formatDeliveredTeamMessage(message, true),
914
- });
915
- }
916
- }
917
- await store.appendEvent(
918
- run.runId,
919
- createTeamEvent(
920
- run.runId,
921
- "TeamMessageDelivered",
922
- `Delivered message ${message.messageId} to ${input.toRoleId}.`,
923
- now,
924
- { roleId: input.toRoleId, agentId: targetAgentId, messageId: message.messageId },
925
- ),
926
- );
927
- return {
928
- ok: true,
929
- messageId: message.messageId,
930
- deliveredTo: input.toRoleId,
931
- cc: ccRoleIds,
932
- };
933
- } catch (error) {
934
- await store.appendEvent(
935
- run.runId,
936
- createTeamEvent(
937
- run.runId,
938
- "TeamMessageDeliveryFailed",
939
- `Failed to deliver message ${message.messageId}: ${describeError(error)}`,
940
- now,
941
- { roleId: input.toRoleId, agentId: targetAgentId, messageId: message.messageId },
942
- ),
943
- );
944
- return {
945
- ok: false,
946
- messageId: message.messageId,
947
- error: "delivery-failed",
948
- message: describeError(error),
949
- };
950
- }
1389
+ return {
1390
+ ok: true,
1391
+ messageId: message.messageId,
1392
+ delivery: "queued",
1393
+ queuedFor: input.toRoleId,
1394
+ cc: ccRoleIds,
1395
+ };
951
1396
  }
952
1397
 
953
1398
  async function spawnTeamRoleWithBrokerContext(
@@ -1084,6 +1529,12 @@ export async function stopTeamRun(input: StopTeamRunInput): Promise<TeamStatusRe
1084
1529
  run.runId,
1085
1530
  createTeamEvent(run.runId, "TeamRunStopped", `Stopped team run ${run.runId}.`, now),
1086
1531
  );
1532
+ await writeTeamStatusSnapshot({
1533
+ store,
1534
+ runId: run.runId,
1535
+ now,
1536
+ lastEvent: "TeamRunStopped",
1537
+ });
1087
1538
  return { run: stoppedRun, agents: stoppedAgents };
1088
1539
  }
1089
1540
 
@@ -1097,12 +1548,13 @@ export async function getTeamStatus(input: TeamStatusInput = {}): Promise<TeamSt
1097
1548
 
1098
1549
  export async function listTeamRuns(input: ListTeamRunsInput = {}): Promise<TeamRunRecord[]> {
1099
1550
  const store = createTeamRunStore(input.homeDir);
1100
- let entries: string[];
1101
- try {
1102
- entries = await readdir(store.paths.runsDir);
1103
- } catch (error) {
1104
- if (isNotFoundError(error)) return [];
1105
- throw error;
1551
+ const entries = new Set<string>();
1552
+ for (const runsDir of [store.paths.runsDir, store.paths.legacyRunsDir]) {
1553
+ try {
1554
+ for (const entry of await readdir(runsDir)) entries.add(entry);
1555
+ } catch (error) {
1556
+ if (!isNotFoundError(error)) throw error;
1557
+ }
1106
1558
  }
1107
1559
  const runs: TeamRunRecord[] = [];
1108
1560
  for (const entry of entries) {
@@ -1145,6 +1597,18 @@ export async function reconcileTeamRun(
1145
1597
  .map((agent) => ({ ...agent, status: "stopped" as const, updatedAt: now }));
1146
1598
 
1147
1599
  if (stoppedAgents.length === 0) {
1600
+ await schedulePendingTeamMessageDelivery({
1601
+ homeDir: input.homeDir,
1602
+ runId: run.runId,
1603
+ now: new Date(now),
1604
+ runtimeAdapter,
1605
+ });
1606
+ await writeTeamStatusSnapshot({
1607
+ store,
1608
+ runId: run.runId,
1609
+ now,
1610
+ lastEvent: "TeamReconciled",
1611
+ });
1148
1612
  return { run, agents, stoppedAgents: [], notifications: [], runtimeAvailable: true };
1149
1613
  }
1150
1614
 
@@ -1181,10 +1645,24 @@ export async function reconcileTeamRun(
1181
1645
  livePaneIds,
1182
1646
  runtimeAdapter,
1183
1647
  });
1648
+ await schedulePendingTeamMessageDelivery({
1649
+ homeDir: input.homeDir,
1650
+ runId: run.runId,
1651
+ now: new Date(now),
1652
+ runtimeAdapter,
1653
+ });
1654
+ await writeTeamStatusSnapshot({
1655
+ store,
1656
+ runId: run.runId,
1657
+ now,
1658
+ lastEvent: "TeamReconciled",
1659
+ });
1660
+ const finalRun = await store.readRun(run.runId);
1661
+ const finalAgents = await store.readAgents(run.runId);
1184
1662
 
1185
1663
  return {
1186
- run: updatedRun,
1187
- agents: updatedAgents,
1664
+ run: finalRun,
1665
+ agents: finalAgents,
1188
1666
  stoppedAgents,
1189
1667
  notifications,
1190
1668
  runtimeAvailable: livePaneIds.size > 0,
@@ -1222,22 +1700,89 @@ export async function recordTeamAgentNativeSession(
1222
1700
  },
1223
1701
  ),
1224
1702
  );
1703
+ await writeTeamStatusSnapshot({
1704
+ store,
1705
+ runId,
1706
+ now,
1707
+ lastEvent: "AgentNativeSessionRecorded",
1708
+ });
1225
1709
 
1226
1710
  return updatedAgent;
1227
1711
  }
1228
1712
 
1229
- export async function resumeTeamRun(input: ResumeTeamRunInput = {}): Promise<TeamRunResumeResult> {
1713
+ export async function updateTeamAgentHookState(
1714
+ input: UpdateTeamAgentHookStateInput,
1715
+ ): Promise<{ agent: TeamAgentRecord; statusPath: string; agentPath: string }> {
1230
1716
  const store = createTeamRunStore(input.homeDir);
1231
1717
  const runId = await resolveRequestedRunId(store, input.runId);
1232
- const run = await store.readRun(runId);
1233
- const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
1718
+ const agent = await store.readAgent(runId, input.roleId);
1234
1719
  const now = (input.now ?? new Date()).toISOString();
1235
- const decision = input.missingSessionDecision ?? "ask";
1236
- const originalAgents = sortAgentsForRecovery(await store.readAgents(run.runId));
1237
- const livePaneIds = await readLivePaneIds(runtimeAdapter, run.tmux.session);
1238
- const outcomes: TeamAgentRecoveryResult[] = [];
1239
- const updatedAgents: TeamAgentRecord[] = [];
1240
- let updatedRun: TeamRunRecord = { ...run, status: "running", updatedAt: now };
1720
+ const nextStatus = teamAgentStatusForHookEvent(input.hookEvent, agent.status);
1721
+ const updatedAgent: TeamAgentRecord =
1722
+ nextStatus === agent.status
1723
+ ? { ...agent, updatedAt: now }
1724
+ : {
1725
+ ...agent,
1726
+ status: nextStatus,
1727
+ updatedAt: now,
1728
+ };
1729
+ await store.writeAgent(updatedAgent);
1730
+ await store.appendEvent(
1731
+ runId,
1732
+ createTeamEvent(
1733
+ runId,
1734
+ "AgentHookStateUpdated",
1735
+ `Updated ${input.roleId} hook state after ${input.hookEvent}.`,
1736
+ now,
1737
+ { roleId: input.roleId, agentId: updatedAgent.agentId },
1738
+ ),
1739
+ );
1740
+ await writeTeamStatusSnapshot({
1741
+ store,
1742
+ runId,
1743
+ now,
1744
+ lastEvent: input.hookEvent,
1745
+ });
1746
+ return {
1747
+ agent: updatedAgent,
1748
+ statusPath: store.paths.statusPath(runId),
1749
+ agentPath: store.paths.agentPath(runId, input.roleId),
1750
+ };
1751
+ }
1752
+
1753
+ export async function createTeamRoleRuntimeContext(
1754
+ input: TeamRoleRuntimeContextInput,
1755
+ ): Promise<string> {
1756
+ const store = createTeamRunStore(input.homeDir);
1757
+ const run = await store.readRun(input.runId);
1758
+ const agent = await store.readAgent(run.runId, input.roleId);
1759
+ const agents = await store.readAgents(run.runId);
1760
+ const role = await resolveRoleForAgentRecovery({
1761
+ homeDir: input.homeDir,
1762
+ repoRoot: run.repoRoot,
1763
+ agent,
1764
+ });
1765
+ return createAgentStartupPrompt({
1766
+ homeDir: input.homeDir,
1767
+ runId: run.runId,
1768
+ repoRoot: run.repoRoot,
1769
+ role,
1770
+ roster: agents,
1771
+ });
1772
+ }
1773
+
1774
+ export async function resumeTeamRun(input: ResumeTeamRunInput = {}): Promise<TeamRunResumeResult> {
1775
+ const store = createTeamRunStore(input.homeDir);
1776
+ const runId = await resolveRequestedRunId(store, input.runId);
1777
+ const run = await store.readRun(runId);
1778
+ const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
1779
+ const now = (input.now ?? new Date()).toISOString();
1780
+ const decision = input.missingSessionDecision ?? "ask";
1781
+ const originalAgents = sortAgentsForRecovery(await store.readAgents(run.runId));
1782
+ const livePaneIds = await readLivePaneIds(runtimeAdapter, run.tmux.session);
1783
+ const outcomes: TeamAgentRecoveryResult[] = [];
1784
+ const updatedAgents: TeamAgentRecord[] = [];
1785
+ let updatedRun: TeamRunRecord = { ...run, status: "running", updatedAt: now };
1241
1786
 
1242
1787
  for (const agent of originalAgents) {
1243
1788
  if (isActiveTeamAgentStatus(agent.status) && livePaneIds.has(agent.tmux.paneId)) {
@@ -1262,11 +1807,13 @@ export async function resumeTeamRun(input: ResumeTeamRunInput = {}): Promise<Tea
1262
1807
  ...updatedAgents,
1263
1808
  ...originalAgents.filter((item) => item.roleId !== agent.roleId),
1264
1809
  ];
1265
- const startupPrompt = createAgentStartupPrompt({
1810
+ const startupPrompt = await createAgentStartupPrompt({
1811
+ homeDir: input.homeDir,
1266
1812
  runId: run.runId,
1267
1813
  repoRoot: run.repoRoot,
1268
1814
  role,
1269
1815
  roster,
1816
+ now,
1270
1817
  });
1271
1818
  const recoveryMode = resolveRecoveryMode(agent, decision);
1272
1819
 
@@ -1376,6 +1923,12 @@ export async function resumeTeamRun(input: ResumeTeamRunInput = {}): Promise<Tea
1376
1923
  updatedAt: now,
1377
1924
  };
1378
1925
  await store.writeRun(updatedRun);
1926
+ await writeTeamStatusSnapshot({
1927
+ store,
1928
+ runId: run.runId,
1929
+ now,
1930
+ lastEvent: "TeamRunResumed",
1931
+ });
1379
1932
 
1380
1933
  const decisionRequired = outcomes.filter((outcome) => outcome.outcome === "needs-decision");
1381
1934
  const notifications =
@@ -1419,9 +1972,221 @@ export async function listTeamAgents(input: TeamStatusInput = {}): Promise<TeamA
1419
1972
  status: agent.status,
1420
1973
  runtime: agent.runtime,
1421
1974
  canReceiveMessages: isActiveTeamAgentStatus(agent.status),
1975
+ isIdle: isIdleTeamAgentStatus(agent.status),
1976
+ isMidTurn: isMidTurnTeamAgentStatus(agent.status),
1422
1977
  }));
1423
1978
  }
1424
1979
 
1980
+ export async function resolveTeamOverlay(input: {
1981
+ homeDir?: string;
1982
+ repoRoot: string;
1983
+ }): Promise<TeamOverlayResolution> {
1984
+ const repoTeamPath = join(input.repoRoot, ".evodev", "team", "team.md");
1985
+ if (await pathExists(repoTeamPath)) {
1986
+ return {
1987
+ source: "repo",
1988
+ teamPath: repoTeamPath,
1989
+ definition: parseTeamDefinitionMarkdown(await readFile(repoTeamPath, "utf8")),
1990
+ };
1991
+ }
1992
+
1993
+ const globalTeamPath = resolveGlobalTeamMarkdownPath(input.homeDir);
1994
+ if (await pathExists(globalTeamPath)) {
1995
+ return {
1996
+ source: "global",
1997
+ teamPath: globalTeamPath,
1998
+ definition: parseTeamDefinitionMarkdown(await readFile(globalTeamPath, "utf8")),
1999
+ };
2000
+ }
2001
+
2002
+ return {
2003
+ source: "builtin",
2004
+ teamPath: null,
2005
+ definition: BUILT_IN_TEAM_DEFINITION,
2006
+ };
2007
+ }
2008
+
2009
+ export async function ensureDefaultTeamOverlay(input: {
2010
+ homeDir?: string;
2011
+ assetsRootDir: string;
2012
+ }): Promise<EnsureDefaultTeamOverlayResult> {
2013
+ const assets = await listDefaultTeamOverlayAssets(input.assetsRootDir);
2014
+ const paths = resolveEvoDevPaths(input.homeDir);
2015
+ const files: DefaultTeamOverlayFileResult[] = [];
2016
+
2017
+ for (const asset of assets) {
2018
+ const targetPath =
2019
+ asset.kind === "team"
2020
+ ? join(paths.rootDir, "team", "team.md")
2021
+ : join(paths.rootDir, "team", "agents", asset.name);
2022
+ const content = await readFile(asset.sourcePath, "utf8");
2023
+ files.push({
2024
+ sourcePath: asset.sourcePath,
2025
+ targetPath,
2026
+ written: await writeTextFileIfMissing(targetPath, content),
2027
+ });
2028
+ }
2029
+
2030
+ return { files };
2031
+ }
2032
+
2033
+ export function parseTeamDefinitionMarkdown(content: string): TeamDefinition {
2034
+ const markdown = parseMarkdownWithFrontmatter(content);
2035
+ const frontmatter = markdown.frontmatter;
2036
+ const version = frontmatter.version;
2037
+ if (version !== 1) throw new Error("team.md frontmatter version must be 1.");
2038
+ const agents = parseTeamDefinitionAgents(frontmatter.agents);
2039
+
2040
+ return {
2041
+ version: 1,
2042
+ name: optionalString(frontmatter.name) ?? "evodev-team",
2043
+ description: optionalString(frontmatter.description) ?? "EvoDev team overlay.",
2044
+ agents,
2045
+ body: markdown.body.trim(),
2046
+ };
2047
+ }
2048
+
2049
+ export function resolveTeamAgentReference(input: {
2050
+ homeDir?: string;
2051
+ repoRoot: string;
2052
+ roleId: string;
2053
+ reference: string;
2054
+ }): TeamAgentReference {
2055
+ assertSafeId(input.roleId, "roleId");
2056
+ const reference = input.reference.trim();
2057
+ if (reference.startsWith("global:")) {
2058
+ const name = reference.slice("global:".length);
2059
+ assertSafeId(name, "global agent name");
2060
+ return {
2061
+ roleId: input.roleId,
2062
+ reference,
2063
+ sourcePath: join(resolveEvoDevPaths(input.homeDir).rootDir, "team", "agents", `${name}.md`),
2064
+ scope: "global",
2065
+ };
2066
+ }
2067
+
2068
+ if (reference.includes(":")) {
2069
+ throw new Error(`Unsupported team agent reference for ${input.roleId}: ${reference}`);
2070
+ }
2071
+ if (isAbsolute(reference)) {
2072
+ throw new Error(`Team agent path for ${input.roleId} must be repo-relative.`);
2073
+ }
2074
+ const sourcePath = resolve(input.repoRoot, reference);
2075
+ const relativePath = relative(input.repoRoot, sourcePath);
2076
+ if (
2077
+ relativePath === "" ||
2078
+ relativePath.startsWith("..") ||
2079
+ isAbsolute(relativePath) ||
2080
+ extname(sourcePath) !== ".md"
2081
+ ) {
2082
+ throw new Error(`Team agent path for ${input.roleId} must be a repo-local Markdown file.`);
2083
+ }
2084
+
2085
+ return {
2086
+ roleId: input.roleId,
2087
+ reference,
2088
+ sourcePath,
2089
+ scope: "repo",
2090
+ };
2091
+ }
2092
+
2093
+ export async function readTeamAgentSummary(input: {
2094
+ homeDir?: string;
2095
+ repoRoot: string;
2096
+ roleId: string;
2097
+ reference: string;
2098
+ }): Promise<TeamOverlayAgentSummary> {
2099
+ const reference = resolveTeamAgentReference(input);
2100
+ const markdown = await readTeamAgentMarkdown(reference);
2101
+ const parsed = parseMarkdownWithFrontmatter(markdown);
2102
+ return {
2103
+ roleId: input.roleId,
2104
+ name: optionalString(parsed.frontmatter.name) ?? defaultRoleName(input.roleId),
2105
+ description:
2106
+ optionalString(parsed.frontmatter.description) ?? `EvoDev ${input.roleId} role agent.`,
2107
+ sourcePath: reference.sourcePath,
2108
+ };
2109
+ }
2110
+
2111
+ export async function readTeamAgentDefinition(input: {
2112
+ homeDir?: string;
2113
+ repoRoot: string;
2114
+ roleId: string;
2115
+ reference: string;
2116
+ }): Promise<TeamAgentDefinition> {
2117
+ const reference = resolveTeamAgentReference(input);
2118
+ const markdown = await readTeamAgentMarkdown(reference);
2119
+ const parsed = parseMarkdownWithFrontmatter(markdown);
2120
+ const evodev = isRecord(parsed.frontmatter.evodev) ? parsed.frontmatter.evodev : {};
2121
+ return {
2122
+ roleId: input.roleId,
2123
+ name: optionalString(parsed.frontmatter.name) ?? defaultRoleName(input.roleId),
2124
+ description:
2125
+ optionalString(parsed.frontmatter.description) ?? `EvoDev ${input.roleId} role agent.`,
2126
+ runtime: optionalRuntime(evodev.runtime),
2127
+ model: optionalNullableString(parsed.frontmatter.model) ?? null,
2128
+ thinkingLevel:
2129
+ optionalNullableString(evodev.thinking) ??
2130
+ optionalNullableString(evodev.thinkingLevel) ??
2131
+ null,
2132
+ writeMode: optionalWriteMode(evodev.writeMode),
2133
+ skills: parseStringList(evodev.skills),
2134
+ sourcePath: reference.sourcePath,
2135
+ markdown,
2136
+ };
2137
+ }
2138
+
2139
+ export async function renderMainTeamOverlayContext(input: {
2140
+ homeDir?: string;
2141
+ repoRoot: string;
2142
+ overlay?: TeamOverlayResolution;
2143
+ }): Promise<string> {
2144
+ const overlay = input.overlay ?? (await resolveTeamOverlay(input));
2145
+ const summaries = await readTeamOverlayAgentSummaries({
2146
+ homeDir: input.homeDir,
2147
+ repoRoot: input.repoRoot,
2148
+ overlay,
2149
+ });
2150
+ const roleLines =
2151
+ summaries.length === 0
2152
+ ? ["- none declared"]
2153
+ : summaries.map((summary) => `- ${summary.roleId}: ${summary.name} - ${summary.description}`);
2154
+ const source =
2155
+ overlay.teamPath === null
2156
+ ? `${overlay.source} fallback`
2157
+ : `${overlay.source}: ${overlay.teamPath}`;
2158
+
2159
+ return [
2160
+ "EvoDev team overlay:",
2161
+ `Team: ${overlay.definition.name}`,
2162
+ `Description: ${overlay.definition.description}`,
2163
+ `Source: ${source}`,
2164
+ "",
2165
+ "Team strategy:",
2166
+ overlay.definition.body || "(none)",
2167
+ "",
2168
+ "Declared role agents:",
2169
+ ...roleLines,
2170
+ "",
2171
+ "Overlay rules:",
2172
+ "- Declared role agents are spawned only on demand.",
2173
+ "- Delegate by role id; role agent Markdown is loaded only inside the spawned role.",
2174
+ ].join("\n");
2175
+ }
2176
+
2177
+ export function renderRoleAgentDefinitionContext(input: {
2178
+ definition: TeamAgentDefinition;
2179
+ }): string {
2180
+ return [
2181
+ "EvoDev role agent Markdown definition:",
2182
+ `Source path: ${input.definition.sourcePath}`,
2183
+ "Use this Markdown as the role-specific operating definition for this role only.",
2184
+ "<evodev-agent-markdown>",
2185
+ input.definition.markdown.trimEnd(),
2186
+ "</evodev-agent-markdown>",
2187
+ ].join("\n");
2188
+ }
2189
+
1425
2190
  export async function resolveTeamRole(input: {
1426
2191
  homeDir?: string;
1427
2192
  repoRoot: string;
@@ -1434,6 +2199,37 @@ export async function resolveTeamRole(input: {
1434
2199
  }): Promise<ResolvedTeamRole> {
1435
2200
  assertSafeId(input.roleId, "roleId");
1436
2201
  const settings = await readSettingsOrDefault(input.homeDir);
2202
+ const overlay = await resolveTeamOverlay({ homeDir: input.homeDir, repoRoot: input.repoRoot });
2203
+ if (overlay.source !== "builtin" && input.roleId !== "main") {
2204
+ const reference = overlay.definition.agents[input.roleId];
2205
+ if (reference === undefined) {
2206
+ throw new Error(
2207
+ `Role ${input.roleId} is not declared in team overlay ${overlay.teamPath ?? overlay.source}.`,
2208
+ );
2209
+ }
2210
+ const agent = await readTeamAgentDefinition({
2211
+ homeDir: input.homeDir,
2212
+ repoRoot: input.repoRoot,
2213
+ roleId: input.roleId,
2214
+ reference,
2215
+ });
2216
+ return {
2217
+ version: 1,
2218
+ roleId: input.roleId,
2219
+ roleName: agent.name,
2220
+ description: agent.description,
2221
+ runtime: agent.runtime ?? settings.teamRuntime.defaultRuntime,
2222
+ model: agent.model ?? settings.teamRuntime.defaultModel,
2223
+ thinkingLevel: agent.thinkingLevel ?? settings.teamRuntime.defaultThinkingLevel,
2224
+ prompt: renderRoleAgentDefinitionContext({ definition: agent }),
2225
+ permissions: parseRolePermissions({ writeMode: agent.writeMode ?? undefined }, false),
2226
+ teamPolicy: parseRolePolicy(undefined, settings.teamRuntime.recordTranscript),
2227
+ source: "overlay",
2228
+ sourcePath: agent.sourcePath,
2229
+ nativeAgent: null,
2230
+ };
2231
+ }
2232
+
1437
2233
  const globalRolePath = join(
1438
2234
  resolveEvoDevPaths(input.homeDir).roleAgentsDir,
1439
2235
  `${input.roleId}.json`,
@@ -1456,12 +2252,24 @@ export async function resolveTeamRole(input: {
1456
2252
  defaultThinkingLevel: settings.teamRuntime.defaultThinkingLevel,
1457
2253
  recordTranscript: settings.teamRuntime.recordTranscript,
1458
2254
  });
2255
+ const prompt =
2256
+ input.roleId === "main"
2257
+ ? appendPromptBlock(
2258
+ parsed.prompt,
2259
+ await renderMainTeamOverlayContext({
2260
+ homeDir: input.homeDir,
2261
+ repoRoot: input.repoRoot,
2262
+ overlay,
2263
+ }),
2264
+ )
2265
+ : parsed.prompt;
1459
2266
 
1460
2267
  return {
1461
2268
  ...parsed,
1462
2269
  runtime: input.overrides?.runtime ?? nativeAgent?.target ?? parsed.runtime,
1463
2270
  model: input.overrides?.model ?? parsed.model,
1464
2271
  thinkingLevel: input.overrides?.thinkingLevel ?? parsed.thinkingLevel,
2272
+ prompt,
1465
2273
  sourcePath,
1466
2274
  nativeAgent,
1467
2275
  };
@@ -1507,6 +2315,8 @@ export function createTmuxRuntimeAdapter(
1507
2315
  }
1508
2316
 
1509
2317
  export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
2318
+ private messageBufferCounter = 0;
2319
+
1510
2320
  constructor(
1511
2321
  private readonly runner: TeamRuntimeCommandRunner,
1512
2322
  private readonly options: TmuxRuntimeAdapterOptions = {},
@@ -1517,6 +2327,7 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1517
2327
  runId: input.runId,
1518
2328
  repoRoot: input.repoRoot,
1519
2329
  runtimeCommands: this.options.runtimeCommands,
2330
+ environment: this.options.environment,
1520
2331
  });
1521
2332
  await this.runTmux([
1522
2333
  "new-session",
@@ -1536,7 +2347,10 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1536
2347
  `${input.sessionName}:main.0`,
1537
2348
  "#{pane_id}",
1538
2349
  ]);
1539
- return { session: input.sessionName, window: "main", paneId: pane.stdout.trim() };
2350
+ const paneId = pane.stdout.trim();
2351
+ await this.configureTeamWindow(`${input.sessionName}:main`);
2352
+ await this.setPaneTitle(paneId, input.role);
2353
+ return { session: input.sessionName, window: "main", paneId };
1540
2354
  }
1541
2355
 
1542
2356
  async spawnAgent(input: TeamRuntimeSpawnAgentInput): Promise<TeamRuntimeAgentHandle> {
@@ -1545,9 +2359,10 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1545
2359
  runId: input.runId,
1546
2360
  repoRoot: input.repoRoot,
1547
2361
  runtimeCommands: this.options.runtimeCommands,
2362
+ environment: this.options.environment,
1548
2363
  });
1549
2364
  if (input.targetPaneId !== undefined) {
1550
- const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command);
2365
+ const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command, input.role);
1551
2366
  return { session: input.sessionName, window: "main", paneId: pane };
1552
2367
  }
1553
2368
 
@@ -1565,7 +2380,10 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1565
2380
  input.repoRoot,
1566
2381
  command,
1567
2382
  ]);
1568
- return { session: input.sessionName, window, paneId: pane.stdout.trim() };
2383
+ const paneId = pane.stdout.trim();
2384
+ await this.configureTeamWindow(`${input.sessionName}:${window}`);
2385
+ await this.setPaneTitle(paneId, input.role);
2386
+ return { session: input.sessionName, window, paneId };
1569
2387
  }
1570
2388
 
1571
2389
  async recoverAgent(input: TeamRuntimeRecoverAgentInput): Promise<TeamRuntimeAgentHandle> {
@@ -1576,14 +2394,16 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1576
2394
  runId: input.runId,
1577
2395
  repoRoot: input.repoRoot,
1578
2396
  runtimeCommands: this.options.runtimeCommands,
2397
+ environment: this.options.environment,
1579
2398
  })
1580
2399
  : buildAgentResumeShellCommand(input.role, input.startupPrompt, {
1581
2400
  runId: input.runId,
1582
2401
  repoRoot: input.repoRoot,
1583
2402
  mode: input.mode,
1584
2403
  runtimeCommands: this.options.runtimeCommands,
2404
+ environment: this.options.environment,
1585
2405
  });
1586
- const session = await this.runner.run("tmux", ["has-session", "-t", input.sessionName]);
2406
+ const session = await this.runner.run("tmux", ["-u", "has-session", "-t", input.sessionName]);
1587
2407
  if (session.exitCode !== 0) {
1588
2408
  await this.runTmux([
1589
2409
  "new-session",
@@ -1603,11 +2423,14 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1603
2423
  `${input.sessionName}:${window}.0`,
1604
2424
  "#{pane_id}",
1605
2425
  ]);
1606
- return recoveredHandle(input, window, pane.stdout.trim());
2426
+ const paneId = pane.stdout.trim();
2427
+ await this.configureTeamWindow(`${input.sessionName}:${window}`);
2428
+ await this.setPaneTitle(paneId, input.role);
2429
+ return recoveredHandle(input, window, paneId);
1607
2430
  }
1608
2431
 
1609
2432
  if (input.targetPaneId !== undefined && input.role.roleId !== "main") {
1610
- const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command);
2433
+ const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command, input.role);
1611
2434
  return recoveredHandle(input, "main", pane);
1612
2435
  }
1613
2436
 
@@ -1625,13 +2448,17 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1625
2448
  input.repoRoot,
1626
2449
  command,
1627
2450
  ]);
1628
- return recoveredHandle(input, window, pane.stdout.trim());
2451
+ const paneId = pane.stdout.trim();
2452
+ await this.configureTeamWindow(`${input.sessionName}:${window}`);
2453
+ await this.setPaneTitle(paneId, input.role);
2454
+ return recoveredHandle(input, window, paneId);
1629
2455
  }
1630
2456
 
1631
2457
  private async splitPane(
1632
2458
  targetPaneId: string,
1633
2459
  repoRoot: string,
1634
2460
  command: string,
2461
+ role: ResolvedTeamRole,
1635
2462
  ): Promise<string> {
1636
2463
  const pane = await this.runTmux([
1637
2464
  "split-window",
@@ -1646,18 +2473,43 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1646
2473
  repoRoot,
1647
2474
  command,
1648
2475
  ]);
1649
- await this.runTmux(["select-layout", "-t", targetPaneId, "tiled"]);
1650
- return pane.stdout.trim();
2476
+ const paneId = pane.stdout.trim();
2477
+ await this.configureTeamWindow(targetPaneId);
2478
+ await this.setPaneTitle(paneId, role);
2479
+ await this.runTmux(["select-layout", "-t", targetPaneId, "main-vertical"]);
2480
+ return paneId;
2481
+ }
2482
+
2483
+ private async configureTeamWindow(target: string): Promise<void> {
2484
+ await this.runTmux(["set-option", "-w", "-t", target, "pane-border-status", "top"]);
2485
+ await this.runTmux([
2486
+ "set-option",
2487
+ "-w",
2488
+ "-t",
2489
+ target,
2490
+ "pane-border-format",
2491
+ "[#{pane_index}] #{pane_title}",
2492
+ ]);
2493
+ }
2494
+
2495
+ private async setPaneTitle(paneId: string, role: ResolvedTeamRole): Promise<void> {
2496
+ await this.runTmux(["select-pane", "-t", paneId, "-T", formatTeamPaneTitle(role)]);
1651
2497
  }
1652
2498
 
1653
2499
  async sendInput(input: TeamRuntimeSendInputInput): Promise<void> {
1654
- await this.runTmux(["set-buffer", "-b", "evodev-message", input.text]);
1655
- await this.runTmux(["paste-buffer", "-d", "-b", "evodev-message", "-t", input.paneId]);
1656
- await this.runTmux(["send-keys", "-t", input.paneId, "Enter"]);
2500
+ const bufferName = `evodev-message-${Date.now()}-${this.messageBufferCounter++}`;
2501
+ await this.runTmux(["set-buffer", "-b", bufferName, input.text]);
2502
+ try {
2503
+ await this.runTmux(["paste-buffer", "-d", "-p", "-r", "-b", bufferName, "-t", input.paneId]);
2504
+ } catch (error) {
2505
+ await this.runner.run("tmux", ["-u", "delete-buffer", "-b", bufferName]);
2506
+ throw error;
2507
+ }
2508
+ await this.runTmux(["send-keys", "-t", input.paneId, "C-m"]);
1657
2509
  }
1658
2510
 
1659
2511
  async listPanes(input: TeamRuntimeListPanesInput): Promise<TeamRuntimePaneInfo[]> {
1660
- const panes = await this.runTmux(["list-panes", "-t", input.session, "-F", "#{pane_id}"]);
2512
+ const panes = await this.runTmux(["list-panes", "-s", "-t", input.session, "-F", "#{pane_id}"]);
1661
2513
  return panes.stdout
1662
2514
  .split("\n")
1663
2515
  .map((paneId) => paneId.trim())
@@ -1675,15 +2527,15 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1675
2527
 
1676
2528
  formatAttachCommand(input: TeamRuntimeAttachInput): string {
1677
2529
  return input.paneId === undefined
1678
- ? `tmux attach-session -t ${input.session}`
1679
- : `tmux attach-session -t ${input.session} \\; select-pane -t ${input.paneId}`;
2530
+ ? `tmux -u attach-session -t ${input.session}`
2531
+ : `tmux -u attach-session -t ${input.session} \\; select-pane -t ${input.paneId}`;
1680
2532
  }
1681
2533
 
1682
2534
  private async runTmux(
1683
2535
  args: string[],
1684
2536
  options?: { input?: string },
1685
2537
  ): Promise<TeamRuntimeCommandResult> {
1686
- const result = await this.runner.run("tmux", args, options);
2538
+ const result = await this.runner.run("tmux", ["-u", ...args], options);
1687
2539
  if (result.exitCode !== 0) {
1688
2540
  throw new Error(`tmux ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
1689
2541
  }
@@ -1854,84 +2706,50 @@ async function resolveRoleForAgentRecovery(input: {
1854
2706
  }
1855
2707
 
1856
2708
  function createAgentStartupPrompt(input: {
2709
+ homeDir?: string;
1857
2710
  runId: string;
1858
2711
  repoRoot: string;
1859
2712
  role: ResolvedTeamRole;
1860
2713
  roster: TeamAgentRecord[];
1861
- }): string {
1862
- const roster =
1863
- input.roster.length === 0
1864
- ? "- main: starting"
1865
- : input.roster
1866
- .map((agent) => `- ${agent.roleId}: ${agent.status} (${agent.roleName})`)
1867
- .join("\n");
1868
- return [
1869
- "You are an EvoDev managed role agent.",
1870
- `Team run: ${input.runId}`,
1871
- `Repository: ${input.repoRoot}`,
1872
- `Role id: ${input.role.roleId}`,
1873
- `Role name: ${input.role.roleName}`,
1874
- `Runtime: ${input.role.runtime}`,
1875
- input.role.nativeAgent === null
1876
- ? "Native Code Agent binding: none"
1877
- : `Native Code Agent binding: ${input.role.nativeAgent.target}/${input.role.nativeAgent.agentName} (${input.role.nativeAgent.scope})`,
1878
- `Model: ${input.role.model ?? "default"}`,
1879
- `Thinking level: ${input.role.thinkingLevel ?? "default"}`,
1880
- `Write mode: ${input.role.permissions.writeMode}`,
1881
- `Transcript recording: ${input.role.teamPolicy.recordTranscript ? "enabled" : "disabled"}`,
1882
- "",
1883
- "Current known agents:",
1884
- roster,
1885
- "",
1886
- "EvoDev team runtime control contract:",
1887
- "This run is already inside the EvoDev-managed team runtime.",
1888
- "For EvoDev role-agent lifecycle, use the current EvoDev run control plane.",
1889
- "Unprefixed user requests such as 'start the team', 'execute team', 'create agents', or 'spawn roles' mean: use the current EvoDev run control plane.",
1890
- "Do not answer those requests with only a role plan when a role agent should be created.",
1891
- "",
1892
- "Use Teams MCP as the primary control plane:",
1893
- "- list_agents: discover current EvoDev role agents.",
1894
- "- spawn_role: create or reuse exactly one EvoDev role agent.",
1895
- "- send_message: communicate through the EvoDev broker.",
1896
- "- stop_role: stop a role agent when allowed.",
1897
- "If Teams MCP is unavailable, fall back to the EvoDev CLI from this repository:",
1898
- "- evodev team spawn --role <roleId>",
1899
- "- evodev team send --to <roleId> --message <text>",
1900
- "- evodev team status",
1901
- "Teams MCP defaults are inherited from the environment:",
1902
- `EVODEV_TEAM_RUN_ID=${input.runId}`,
1903
- `EVODEV_TEAM_ROLE_ID=${input.role.roleId}`,
1904
- "When calling Teams MCP tools, let the MCP server-bound environment identify this run and role.",
1905
- "Do not operate tmux directly for role lifecycle; let EvoDev create and track panes.",
1906
- "Any server-bound role may request role lifecycle changes; EvoDev records and tracks panes but does not use role policy to stop execution.",
1907
- input.role.roleId === "main"
1908
- ? "As main, when the user approves team execution, pick the needed role ids, call spawn_role or evodev team spawn for each role, verify with list_agents/status, then send role-specific assignments."
1909
- : "As a non-main role, prefer coordinating with main through send_message, but lifecycle tools remain advisory and non-gating.",
1910
- input.role.nativeAgent === null
1911
- ? "No native Code Agent agent is bound to this role."
1912
- : `Use the bound native Code Agent agent name '${input.role.nativeAgent.agentName}' as role context when the runtime supports named agents; EvoDev does not load or transform the native agent file.`,
1913
- "Cross-agent messages are copied to main.",
1914
- "",
1915
- input.role.prompt,
1916
- ].join("\n");
2714
+ now?: string;
2715
+ }): Promise<string> {
2716
+ return createScopedTeamStartupContext(input).then((scopedContext) =>
2717
+ renderTeamRoleStartupPrompt({ ...input, scopedContext }),
2718
+ );
1917
2719
  }
1918
2720
 
1919
- function formatDeliveredTeamMessage(message: TeamMessageRecord, cc: boolean): string {
1920
- const header = cc ? "[EvoDev team cc]" : "[EvoDev team message]";
1921
- const footer = cc ? "[/EvoDev team cc]" : "[/EvoDev team message]";
1922
- return [
1923
- header,
1924
- `from: ${message.fromRoleId}`,
1925
- `to: ${message.toRoleId}`,
1926
- `type: ${message.type}`,
1927
- `messageId: ${message.messageId}`,
1928
- cc ? "" : `cc: ${message.ccRoleIds.join(",") || "none"}`,
1929
- "",
1930
- message.body,
1931
- footer,
1932
- ]
1933
- .filter((line) => line !== "")
1934
- .join("\n");
2721
+ async function createScopedTeamStartupContext(input: {
2722
+ homeDir?: string;
2723
+ runId: string;
2724
+ repoRoot: string;
2725
+ role: ResolvedTeamRole;
2726
+ now?: string;
2727
+ }): Promise<string | null> {
2728
+ if (input.role.roleId === "main") return null;
2729
+ const homeDir = resolveEvoDevPaths(input.homeDir).homeDir;
2730
+ const settings = await readRuntimeInjectionSettings(homeDir);
2731
+ if (!settings.runtimeInjection) return null;
2732
+
2733
+ const pack = await createScopedKnowledgeContextPack({
2734
+ homeDir,
2735
+ projectKey: resolveProjectLogKey(homeDir, input.repoRoot),
2736
+ roleId: input.role.roleId,
2737
+ });
2738
+ if (pack === null) return null;
2739
+
2740
+ const sessionKey = `team-${input.runId}-${input.role.roleId}`;
2741
+ if (await hasContextInjectionReceipt({ homeDir, sessionKey, contextPackId: pack.id })) {
2742
+ return null;
2743
+ }
2744
+ await writeContextInjectionReceipt({
2745
+ homeDir,
2746
+ sessionKey,
2747
+ pack,
2748
+ trigger: "team-startup",
2749
+ hookEventId: null,
2750
+ injectedAt: input.now,
2751
+ });
2752
+ return formatScopedKnowledgePromptBlock(pack);
1935
2753
  }
1936
2754
 
1937
2755
  function buildAgentShellCommand(
@@ -1941,17 +2759,18 @@ function buildAgentShellCommand(
1941
2759
  runId: string;
1942
2760
  repoRoot: string;
1943
2761
  runtimeCommands?: TmuxRuntimeAdapterOptions["runtimeCommands"];
2762
+ environment?: TmuxRuntimeAdapterOptions["environment"];
1944
2763
  },
1945
2764
  ): string {
1946
2765
  const args =
1947
2766
  role.runtime === "codex"
1948
2767
  ? buildCodexArgs(role, startupPrompt)
1949
2768
  : buildClaudeArgs(role, startupPrompt);
1950
- const env = {
2769
+ const env = createAgentLaunchEnvironment(context.environment, {
1951
2770
  EVODEV_TEAM_RUN_ID: context.runId,
1952
2771
  EVODEV_TEAM_ROLE_ID: role.roleId,
1953
2772
  EVODEV_TEAM_REPO_ROOT: context.repoRoot,
1954
- };
2773
+ });
1955
2774
  return buildAgentCommand(resolveRuntimeCommand(role.runtime, context.runtimeCommands), args, env);
1956
2775
  }
1957
2776
 
@@ -1963,21 +2782,48 @@ function buildAgentResumeShellCommand(
1963
2782
  repoRoot: string;
1964
2783
  mode: Exclude<TeamRuntimeAgentRecoveryMode, { type: "fresh" }>;
1965
2784
  runtimeCommands?: TmuxRuntimeAdapterOptions["runtimeCommands"];
2785
+ environment?: TmuxRuntimeAdapterOptions["environment"];
1966
2786
  },
1967
2787
  ): string {
1968
2788
  const args =
1969
2789
  role.runtime === "codex"
1970
2790
  ? buildCodexResumeArgs(role, startupPrompt, context.mode)
1971
2791
  : buildClaudeResumeArgs(role, context.mode);
1972
- const env = {
2792
+ const env = createAgentLaunchEnvironment(context.environment, {
1973
2793
  EVODEV_TEAM_RUN_ID: context.runId,
1974
2794
  EVODEV_TEAM_ROLE_ID: role.roleId,
1975
2795
  EVODEV_TEAM_REPO_ROOT: context.repoRoot,
1976
2796
  EVODEV_TEAM_RECOVERY: "1",
1977
- };
2797
+ });
1978
2798
  return buildAgentCommand(resolveRuntimeCommand(role.runtime, context.runtimeCommands), args, env);
1979
2799
  }
1980
2800
 
2801
+ function createAgentLaunchEnvironment(
2802
+ source: Record<string, string | undefined> | undefined,
2803
+ evodev: Record<string, string>,
2804
+ ): Record<string, string> {
2805
+ return {
2806
+ ...selectLocaleEnvironment(source ?? process.env),
2807
+ ...evodev,
2808
+ };
2809
+ }
2810
+
2811
+ function selectLocaleEnvironment(
2812
+ source: Record<string, string | undefined>,
2813
+ ): Record<string, string> {
2814
+ const selected: Record<string, string> = {};
2815
+ for (const key of Object.keys(source).sort()) {
2816
+ const value = source[key];
2817
+ if (value === undefined || !isLocaleEnvironmentKey(key)) continue;
2818
+ selected[key] = value;
2819
+ }
2820
+ return selected;
2821
+ }
2822
+
2823
+ function isLocaleEnvironmentKey(key: string): boolean {
2824
+ return key === "LANG" || key === "LANGUAGE" || key === "LC_ALL" || /^LC_[A-Z0-9_]+$/.test(key);
2825
+ }
2826
+
1981
2827
  function buildAgentCommand(
1982
2828
  runtimeCommand: string,
1983
2829
  args: string[],
@@ -1998,7 +2844,7 @@ function resolveRuntimeCommand(
1998
2844
  function buildCodexArgs(role: ResolvedTeamRole, startupPrompt: string): string[] {
1999
2845
  const args = ["--no-alt-screen"];
2000
2846
  if (role.model !== null) args.push("--model", role.model);
2001
- args.push(startupPrompt);
2847
+ if (shouldPassVisibleStartupPrompt(role)) args.push(startupPrompt);
2002
2848
  return args;
2003
2849
  }
2004
2850
 
@@ -2015,7 +2861,7 @@ function buildCodexResumeArgs(
2015
2861
  } else {
2016
2862
  args.push("--last");
2017
2863
  }
2018
- args.push(startupPrompt);
2864
+ if (shouldPassVisibleStartupPrompt(role)) args.push(startupPrompt);
2019
2865
  return args;
2020
2866
  }
2021
2867
 
@@ -2023,10 +2869,14 @@ function buildClaudeArgs(role: ResolvedTeamRole, startupPrompt: string): string[
2023
2869
  const args = [];
2024
2870
  if (role.model !== null) args.push("--model", role.model);
2025
2871
  if (role.thinkingLevel !== null) args.push("--effort", role.thinkingLevel);
2026
- args.push(startupPrompt);
2872
+ if (shouldPassVisibleStartupPrompt(role)) args.push(startupPrompt);
2027
2873
  return args;
2028
2874
  }
2029
2875
 
2876
+ function shouldPassVisibleStartupPrompt(role: ResolvedTeamRole): boolean {
2877
+ return role.roleId !== "main";
2878
+ }
2879
+
2030
2880
  function buildClaudeResumeArgs(
2031
2881
  role: ResolvedTeamRole,
2032
2882
  mode: Exclude<TeamRuntimeAgentRecoveryMode, { type: "fresh" }>,
@@ -2086,10 +2936,243 @@ function createBuiltInRole(roleId: string, runtime: TeamAgentRuntime): unknown {
2086
2936
  };
2087
2937
  }
2088
2938
 
2939
+ async function readTeamOverlayAgentSummaries(input: {
2940
+ homeDir?: string;
2941
+ repoRoot: string;
2942
+ overlay: TeamOverlayResolution;
2943
+ }): Promise<TeamOverlayAgentSummary[]> {
2944
+ const entries = Object.entries(input.overlay.definition.agents);
2945
+ if (input.overlay.source === "builtin") {
2946
+ return entries.map(([roleId]) => {
2947
+ const builtin = BUILT_IN_ROLE_PROMPTS[roleId];
2948
+ return {
2949
+ roleId,
2950
+ name: builtin?.roleName ?? defaultRoleName(roleId),
2951
+ description: builtin?.description ?? `EvoDev ${roleId} role agent.`,
2952
+ sourcePath: "builtin",
2953
+ };
2954
+ });
2955
+ }
2956
+
2957
+ return Promise.all(
2958
+ entries.map(([roleId, reference]) =>
2959
+ readTeamAgentSummary({
2960
+ homeDir: input.homeDir,
2961
+ repoRoot: input.repoRoot,
2962
+ roleId,
2963
+ reference,
2964
+ }),
2965
+ ),
2966
+ );
2967
+ }
2968
+
2969
+ async function listDefaultTeamOverlayAssets(
2970
+ assetsRootDir: string,
2971
+ ): Promise<
2972
+ Array<
2973
+ | { kind: "team"; sourcePath: string; name: "team.md" }
2974
+ | { kind: "agent"; sourcePath: string; name: string }
2975
+ >
2976
+ > {
2977
+ const teamPath = join(assetsRootDir, "team", "team.md");
2978
+ const agentsDir = join(assetsRootDir, "team", "agents");
2979
+ await assertReadableFile(teamPath, "Default team asset");
2980
+
2981
+ let entries: Array<{ name: string; isFile(): boolean }>;
2982
+ try {
2983
+ entries = (await readdir(agentsDir, { withFileTypes: true })) as Array<{
2984
+ name: string;
2985
+ isFile(): boolean;
2986
+ }>;
2987
+ } catch (error) {
2988
+ throw new Error(
2989
+ `Cannot read default team agents directory ${agentsDir}: ${describeError(error)}`,
2990
+ );
2991
+ }
2992
+
2993
+ const agentFiles = entries
2994
+ .filter((entry) => entry.isFile() && extname(entry.name) === ".md")
2995
+ .map((entry) => entry.name)
2996
+ .sort();
2997
+ if (agentFiles.length === 0) {
2998
+ throw new Error(`Default team agents directory has no Markdown files: ${agentsDir}`);
2999
+ }
3000
+
3001
+ return [
3002
+ { kind: "team", sourcePath: teamPath, name: "team.md" },
3003
+ ...agentFiles.map((name) => ({
3004
+ kind: "agent" as const,
3005
+ sourcePath: join(agentsDir, name),
3006
+ name,
3007
+ })),
3008
+ ];
3009
+ }
3010
+
3011
+ async function assertReadableFile(path: string, label: string): Promise<void> {
3012
+ try {
3013
+ const info = await stat(path);
3014
+ if (!info.isFile()) throw new Error("not a file");
3015
+ } catch (error) {
3016
+ throw new Error(`${label} is not readable at ${path}: ${describeError(error)}`);
3017
+ }
3018
+ }
3019
+
3020
+ async function writeTextFileIfMissing(path: string, content: string): Promise<boolean> {
3021
+ try {
3022
+ await readFile(path, "utf8");
3023
+ return false;
3024
+ } catch (error) {
3025
+ if (!isNotFoundError(error)) {
3026
+ throw new Error(`Cannot inspect ${path}: ${describeError(error)}`);
3027
+ }
3028
+ }
3029
+
3030
+ await mkdir(dirname(path), { recursive: true });
3031
+ await writeFile(path, content.endsWith("\n") ? content : `${content}\n`, "utf8");
3032
+ return true;
3033
+ }
3034
+
3035
+ function parseTeamDefinitionAgents(value: unknown): Record<string, string> {
3036
+ if (value === undefined) return {};
3037
+ if (!isRecord(value)) throw new Error("team.md agents must be a role-id map.");
3038
+ const agents: Record<string, string> = {};
3039
+ for (const [roleId, reference] of Object.entries(value)) {
3040
+ assertSafeId(roleId, "team.md agents roleId");
3041
+ if (typeof reference !== "string" || reference.trim() === "") {
3042
+ throw new Error(`team.md agent reference for ${roleId} must be a non-empty string.`);
3043
+ }
3044
+ agents[roleId] = reference.trim();
3045
+ }
3046
+ return agents;
3047
+ }
3048
+
3049
+ async function readTeamAgentMarkdown(reference: TeamAgentReference): Promise<string> {
3050
+ if (extname(reference.sourcePath) !== ".md") {
3051
+ throw new Error(`Team agent file for ${reference.roleId} must be Markdown.`);
3052
+ }
3053
+ try {
3054
+ return await readFile(reference.sourcePath, "utf8");
3055
+ } catch (error) {
3056
+ if (isNotFoundError(error)) {
3057
+ throw new Error(`Team agent file not found for ${reference.roleId}: ${reference.sourcePath}`);
3058
+ }
3059
+ throw new Error(
3060
+ `Cannot read team agent file for ${reference.roleId}: ${reference.sourcePath}: ${describeError(
3061
+ error,
3062
+ )}`,
3063
+ );
3064
+ }
3065
+ }
3066
+
3067
+ function parseMarkdownWithFrontmatter(content: string): {
3068
+ frontmatter: Record<string, unknown>;
3069
+ body: string;
3070
+ } {
3071
+ const text = content.startsWith("\uFEFF") ? content.slice(1) : content;
3072
+ const lines = text.split(/\r?\n/);
3073
+ if (lines[0]?.trim() !== "---") return { frontmatter: {}, body: text };
3074
+ const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
3075
+ if (end < 0) throw new Error("Markdown frontmatter is not closed.");
3076
+ return {
3077
+ frontmatter: parseSimpleYaml(lines.slice(1, end).join("\n")),
3078
+ body: lines.slice(end + 1).join("\n"),
3079
+ };
3080
+ }
3081
+
3082
+ function parseSimpleYaml(content: string): Record<string, unknown> {
3083
+ const root: Record<string, unknown> = {};
3084
+ const stack: Array<{ indent: number; value: Record<string, unknown> | unknown[] }> = [
3085
+ { indent: -1, value: root },
3086
+ ];
3087
+ const lines = content.split(/\r?\n/);
3088
+
3089
+ for (let index = 0; index < lines.length; index += 1) {
3090
+ const rawLine = lines[index] ?? "";
3091
+ if (rawLine.trim() === "" || rawLine.trimStart().startsWith("#")) continue;
3092
+ const indent = rawLine.match(/^ */)?.[0].length ?? 0;
3093
+ const trimmed = rawLine.trim();
3094
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
3095
+ const parent = stack[stack.length - 1].value;
3096
+
3097
+ if (trimmed.startsWith("- ")) {
3098
+ if (!Array.isArray(parent)) throw new Error("Invalid YAML list item placement.");
3099
+ parent.push(parseYamlScalar(trimmed.slice(2).trim()));
3100
+ continue;
3101
+ }
3102
+
3103
+ const separator = trimmed.indexOf(":");
3104
+ if (separator <= 0) throw new Error(`Invalid YAML line: ${trimmed}`);
3105
+ const key = trimmed.slice(0, separator).trim();
3106
+ const rawValue = trimmed.slice(separator + 1).trim();
3107
+ if (!isRecord(parent)) throw new Error(`Invalid YAML parent for key ${key}.`);
3108
+
3109
+ if (rawValue === "") {
3110
+ const next = findNextYamlContentLine(lines, index + 1);
3111
+ const value: Record<string, unknown> | unknown[] =
3112
+ next !== null && next.indent > indent && next.trimmed.startsWith("- ") ? [] : {};
3113
+ parent[key] = value;
3114
+ stack.push({ indent, value });
3115
+ continue;
3116
+ }
3117
+
3118
+ parent[key] = parseYamlScalar(rawValue);
3119
+ }
3120
+
3121
+ return root;
3122
+ }
3123
+
3124
+ function findNextYamlContentLine(
3125
+ lines: string[],
3126
+ start: number,
3127
+ ): { indent: number; trimmed: string } | null {
3128
+ for (let index = start; index < lines.length; index += 1) {
3129
+ const line = lines[index] ?? "";
3130
+ if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
3131
+ return {
3132
+ indent: line.match(/^ */)?.[0].length ?? 0,
3133
+ trimmed: line.trim(),
3134
+ };
3135
+ }
3136
+ return null;
3137
+ }
3138
+
3139
+ function parseYamlScalar(value: string): unknown {
3140
+ if (value === "") return "";
3141
+ if (value === "true") return true;
3142
+ if (value === "false") return false;
3143
+ if (value === "null" || value === "~") return null;
3144
+ if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
3145
+ if (value.startsWith("[") && value.endsWith("]")) {
3146
+ const inner = value.slice(1, -1).trim();
3147
+ if (inner === "") return [];
3148
+ return inner.split(",").map((item) => parseYamlScalar(item.trim()));
3149
+ }
3150
+ if (value.startsWith('"') && value.endsWith('"')) {
3151
+ try {
3152
+ return JSON.parse(value);
3153
+ } catch {
3154
+ return value.slice(1, -1);
3155
+ }
3156
+ }
3157
+ if (value.startsWith("'") && value.endsWith("'")) {
3158
+ return value.slice(1, -1).replace(/''/g, "'");
3159
+ }
3160
+ return value;
3161
+ }
3162
+
3163
+ function appendPromptBlock(prompt: string, block: string): string {
3164
+ if (block.trim() === "") return prompt;
3165
+ return `${prompt.trimEnd()}\n\n${block.trim()}`;
3166
+ }
3167
+
3168
+ function resolveGlobalTeamMarkdownPath(homeDir?: string): string {
3169
+ return join(resolveEvoDevPaths(homeDir).rootDir, "team", "team.md");
3170
+ }
3171
+
2089
3172
  function parseRolePermissions(value: unknown, main: boolean): TeamRolePermissions {
2090
3173
  const input = isRecord(value) ? value : {};
2091
3174
  return {
2092
- writeMode: parseWriteMode(input.writeMode, main ? "repo-write" : "read-only"),
3175
+ writeMode: parseWriteMode(input.writeMode, "repo-write"),
2093
3176
  canUseTeamsMcp: optionalBoolean(input.canUseTeamsMcp) ?? true,
2094
3177
  canSpawnAgents: optionalBoolean(input.canSpawnAgents) ?? main,
2095
3178
  canStopAgents: optionalBoolean(input.canStopAgents) ?? main,
@@ -2119,7 +3202,22 @@ function parseTeamAgentRecord(value: unknown): TeamAgentRecord {
2119
3202
  const agent = value as unknown as TeamAgentRecord;
2120
3203
  assertSafeId(agent.agentId, "agentId");
2121
3204
  assertSafeId(agent.roleId, "roleId");
2122
- if (!["running", "recovering", "recreated", "stopped", "failed"].includes(agent.status)) {
3205
+ if (
3206
+ ![
3207
+ "starting",
3208
+ "running",
3209
+ "busy",
3210
+ "idle",
3211
+ "waiting-input",
3212
+ "recovering",
3213
+ "recreated",
3214
+ "stopped",
3215
+ "exited",
3216
+ "needs-user-attention",
3217
+ "failed",
3218
+ "unknown",
3219
+ ].includes(agent.status)
3220
+ ) {
2123
3221
  throw new Error("Invalid agent status.");
2124
3222
  }
2125
3223
  const nativeSessionValue = isRecord(value.nativeSession) ? value.nativeSession : {};
@@ -2142,6 +3240,48 @@ function parseTeamMessageRecord(value: unknown): TeamMessageRecord {
2142
3240
  return message;
2143
3241
  }
2144
3242
 
3243
+ function createPendingMessageRecord(message: TeamMessageRecord): TeamPendingMessageRecord {
3244
+ return {
3245
+ ...message,
3246
+ deliveryState: "pending",
3247
+ attemptCount: 0,
3248
+ lastAttemptAt: null,
3249
+ };
3250
+ }
3251
+
3252
+ function parseTeamPendingMessageRecord(value: unknown): TeamPendingMessageRecord {
3253
+ const message = parseTeamMessageRecord(value);
3254
+ const input = value as Partial<TeamPendingMessageRecord>;
3255
+ const deliveryState =
3256
+ input.deliveryState === "pending" ||
3257
+ input.deliveryState === "claimed" ||
3258
+ input.deliveryState === "wakeup-sent" ||
3259
+ input.deliveryState === "failed"
3260
+ ? input.deliveryState
3261
+ : "pending";
3262
+ return {
3263
+ ...message,
3264
+ deliveryState,
3265
+ attemptCount:
3266
+ typeof input.attemptCount === "number" && Number.isInteger(input.attemptCount)
3267
+ ? input.attemptCount
3268
+ : 0,
3269
+ lastAttemptAt: optionalString(input.lastAttemptAt) ?? null,
3270
+ };
3271
+ }
3272
+
3273
+ function parseTeamMessageListFile(value: unknown): TeamMessageListFile {
3274
+ if (!isRecord(value) || value.version !== 1) throw new Error("Invalid team message list.");
3275
+ const messages = Array.isArray(value.messages)
3276
+ ? value.messages.map(parseTeamPendingMessageRecord)
3277
+ : [];
3278
+ return {
3279
+ version: 1,
3280
+ updatedAt: optionalString(value.updatedAt) ?? null,
3281
+ messages,
3282
+ };
3283
+ }
3284
+
2145
3285
  function parseTeamEventRecord(value: unknown): TeamEventRecord {
2146
3286
  if (!isRecord(value) || value.version !== 1) throw new Error("Invalid team event record.");
2147
3287
  const event = value as unknown as TeamEventRecord;
@@ -2157,6 +3297,12 @@ function parseRuntime(value: unknown, fallback: TeamAgentRuntime): TeamAgentRunt
2157
3297
  throw new Error("Role runtime must be codex or claude.");
2158
3298
  }
2159
3299
 
3300
+ function optionalRuntime(value: unknown): TeamAgentRuntime | null {
3301
+ if (value === undefined || value === null) return null;
3302
+ if (value === "codex" || value === "claude") return value;
3303
+ throw new Error("Role evodev.runtime must be codex or claude.");
3304
+ }
3305
+
2160
3306
  function parseWriteMode(value: unknown, fallback: TeamWriteMode): TeamWriteMode {
2161
3307
  if (value === undefined || value === null) return fallback;
2162
3308
  if (["read-only", "repo-write", "worktree-write", "disabled"].includes(String(value))) {
@@ -2165,8 +3311,55 @@ function parseWriteMode(value: unknown, fallback: TeamWriteMode): TeamWriteMode
2165
3311
  throw new Error("Role writeMode is invalid.");
2166
3312
  }
2167
3313
 
2168
- function isActiveTeamAgentStatus(status: TeamAgentStatus): boolean {
2169
- return status === "running" || status === "recovering" || status === "recreated";
3314
+ function optionalWriteMode(value: unknown): TeamWriteMode | null {
3315
+ if (value === undefined || value === null) return null;
3316
+ return parseWriteMode(value, "repo-write");
3317
+ }
3318
+
3319
+ function parseStringList(value: unknown): string[] {
3320
+ if (value === undefined || value === null) return [];
3321
+ if (typeof value === "string" && value.trim() !== "") return [value.trim()];
3322
+ if (!Array.isArray(value)) return [];
3323
+ return value.filter((item): item is string => typeof item === "string" && item.trim() !== "");
3324
+ }
3325
+
3326
+ export function isActiveTeamAgentStatus(status: TeamAgentStatus): boolean {
3327
+ return (
3328
+ status === "starting" ||
3329
+ status === "running" ||
3330
+ status === "busy" ||
3331
+ status === "idle" ||
3332
+ status === "waiting-input" ||
3333
+ status === "recovering" ||
3334
+ status === "recreated"
3335
+ );
3336
+ }
3337
+
3338
+ export function isIdleTeamAgentStatus(status: TeamAgentStatus): boolean {
3339
+ return status === "idle" || status === "waiting-input";
3340
+ }
3341
+
3342
+ export function isMidTurnTeamAgentStatus(status: TeamAgentStatus): boolean {
3343
+ return status === "busy";
3344
+ }
3345
+
3346
+ function teamAgentStatusForHookEvent(
3347
+ hookEvent: string,
3348
+ fallback: TeamAgentStatus,
3349
+ ): TeamAgentStatus {
3350
+ if (hookEvent === "PreToolUse") return "busy";
3351
+ if (
3352
+ hookEvent === "Stop" ||
3353
+ hookEvent === "TeammateIdle" ||
3354
+ hookEvent === "SubagentStop" ||
3355
+ hookEvent === "TaskCompleted"
3356
+ ) {
3357
+ return "idle";
3358
+ }
3359
+ if (hookEvent === "SessionStart" || hookEvent === "UserPromptSubmit") return "running";
3360
+ if (hookEvent === "PostToolUse" || hookEvent === "PostToolUseFailure") return "running";
3361
+ if (hookEvent === "SessionEnd") return "waiting-input";
3362
+ return fallback;
2170
3363
  }
2171
3364
 
2172
3365
  function createRunId(repoRoot: string, now: Date): string {
@@ -2215,6 +3408,34 @@ function sanitizeWindowName(value: string): string {
2215
3408
  return safeSlug(value).slice(0, 30) || "agent";
2216
3409
  }
2217
3410
 
3411
+ function formatTeamPaneTitle(role: ResolvedTeamRole): string {
3412
+ const label = role.roleName === role.roleId ? role.roleId : `${role.roleName} [${role.roleId}]`;
3413
+ const title = normalizeTmuxPaneTitle(label);
3414
+ return title.slice(0, 80) || role.roleId;
3415
+ }
3416
+
3417
+ function normalizeTmuxPaneTitle(value: string): string {
3418
+ let normalized = "";
3419
+ let pendingSpace = false;
3420
+
3421
+ for (const char of value) {
3422
+ const code = char.charCodeAt(0);
3423
+ const isControl = code < 32 || code === 127;
3424
+ if (isControl || char.trim() === "") {
3425
+ pendingSpace = normalized.length > 0;
3426
+ continue;
3427
+ }
3428
+
3429
+ if (pendingSpace) {
3430
+ normalized += " ";
3431
+ pendingSpace = false;
3432
+ }
3433
+ normalized += char;
3434
+ }
3435
+
3436
+ return normalized;
3437
+ }
3438
+
2218
3439
  function defaultRoleName(roleId: string): string {
2219
3440
  return roleId
2220
3441
  .split(/[-_.]/)
@@ -2352,6 +3573,16 @@ function isNotFoundError(error: unknown): boolean {
2352
3573
  );
2353
3574
  }
2354
3575
 
3576
+ async function pathExists(path: string): Promise<boolean> {
3577
+ try {
3578
+ await stat(path);
3579
+ return true;
3580
+ } catch (error) {
3581
+ if (isNotFoundError(error)) return false;
3582
+ throw error;
3583
+ }
3584
+ }
3585
+
2355
3586
  async function writeJson(path: string, value: unknown): Promise<void> {
2356
3587
  await mkdir(dirname(path), { recursive: true });
2357
3588
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
@@ -2362,6 +3593,19 @@ async function appendJsonLine(path: string, value: unknown): Promise<void> {
2362
3593
  await appendFile(path, `${JSON.stringify(value)}\n`, "utf8");
2363
3594
  }
2364
3595
 
3596
+ async function readJsonLines<T>(path: string, parse: (value: unknown) => T): Promise<T[]> {
3597
+ try {
3598
+ const text = await readFile(path, "utf8");
3599
+ return text
3600
+ .split("\n")
3601
+ .filter((line) => line.trim() !== "")
3602
+ .map((line) => parse(JSON.parse(line)));
3603
+ } catch (error) {
3604
+ if (isNotFoundError(error)) return [];
3605
+ throw error;
3606
+ }
3607
+ }
3608
+
2365
3609
  function shellQuote(value: string): string {
2366
3610
  return `'${value.replace(/'/g, `'\\''`)}'`;
2367
3611
  }