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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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";
2
+ import { appendFile, cp, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join } 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 "../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;
@@ -219,6 +250,18 @@ export interface TeamMessageRecord {
219
250
  createdAt: string;
220
251
  }
221
252
 
253
+ export interface TeamPendingMessageRecord extends TeamMessageRecord {
254
+ deliveryState: TeamMessageDeliveryState;
255
+ attemptCount: number;
256
+ lastAttemptAt: string | null;
257
+ }
258
+
259
+ export interface TeamMessageListFile {
260
+ version: 1;
261
+ updatedAt: string | null;
262
+ messages: TeamPendingMessageRecord[];
263
+ }
264
+
222
265
  export interface TeamEventRecord {
223
266
  version: 1;
224
267
  eventId: string;
@@ -230,7 +273,9 @@ export interface TeamEventRecord {
230
273
  | "TeamMessageAccepted"
231
274
  | "TeamMessageDelivered"
232
275
  | "TeamMessageDeliveryFailed"
276
+ | "TeamMessageWakeupSent"
233
277
  | "AgentNativeSessionRecorded"
278
+ | "AgentHookStateUpdated"
234
279
  | "AgentRecoveryDecisionRequired"
235
280
  | "AgentRecovered"
236
281
  | "AgentRecreated"
@@ -242,6 +287,29 @@ export interface TeamEventRecord {
242
287
  createdAt: string;
243
288
  }
244
289
 
290
+ export interface TeamRunStatusSnapshot {
291
+ version: 1;
292
+ runId: string;
293
+ repoRoot: string;
294
+ runStatus: TeamRunStatus;
295
+ tmux: {
296
+ session: string;
297
+ };
298
+ agents: Array<{
299
+ roleId: string;
300
+ agentId: string;
301
+ roleName: string;
302
+ runtime: TeamAgentRuntime;
303
+ status: TeamAgentStatus;
304
+ paneId: string;
305
+ window: string;
306
+ nativeSessionId: string | null;
307
+ updatedAt: string;
308
+ }>;
309
+ lastEvent: string | null;
310
+ updatedAt: string;
311
+ }
312
+
245
313
  export interface StartTeamRunInput {
246
314
  homeDir?: string;
247
315
  repoRoot: string;
@@ -432,7 +500,8 @@ export async function unsetTeamRoleBinding(input: {
432
500
  export interface TeamMessageSendResult {
433
501
  ok: boolean;
434
502
  messageId?: string;
435
- deliveredTo?: string;
503
+ delivery?: "queued";
504
+ queuedFor?: string;
436
505
  cc?: string[];
437
506
  error?: string;
438
507
  message?: string;
@@ -444,6 +513,29 @@ export interface TeamMessageBrokerOptions {
444
513
  }
445
514
 
446
515
  export type TeamMessageBrokerSendInput = Omit<SendTeamMessageInput, "homeDir" | "runtimeAdapter">;
516
+ export interface ReadPendingTeamMessagesInput {
517
+ homeDir?: string;
518
+ runId: string;
519
+ roleId: string;
520
+ limit?: number;
521
+ }
522
+
523
+ export interface MarkTeamMessagesDeliveredInput {
524
+ homeDir?: string;
525
+ runId: string;
526
+ roleId: string;
527
+ messageIds: string[];
528
+ now?: Date;
529
+ }
530
+
531
+ export interface UpdateTeamAgentHookStateInput {
532
+ homeDir?: string;
533
+ runId: string;
534
+ roleId: string;
535
+ hookEvent: string;
536
+ now?: Date;
537
+ }
538
+
447
539
  export interface TeamMessageBrokerSpawnInput {
448
540
  runId?: string;
449
541
  fromRoleId?: string;
@@ -488,12 +580,21 @@ export interface TeamRunResumeResult extends TeamStatusResult {
488
580
  notifications: TeamMessageSendResult[];
489
581
  }
490
582
 
583
+ export interface TeamMessageDeliveryScheduleResult {
584
+ wokenRoleIds: string[];
585
+ recoveredRoleIds: string[];
586
+ needsUserAttentionRoleIds: string[];
587
+ warnings: string[];
588
+ }
589
+
491
590
  export interface TeamAgentSummary {
492
591
  roleId: string;
493
592
  roleName: string;
494
593
  status: TeamAgentStatus;
495
594
  runtime: TeamAgentRuntime;
496
595
  canReceiveMessages: boolean;
596
+ isIdle: boolean;
597
+ isMidTurn: boolean;
497
598
  }
498
599
 
499
600
  export interface TeamRunStore {
@@ -506,11 +607,24 @@ export interface TeamRunStore {
506
607
  writeAgent(agent: TeamAgentRecord): Promise<void>;
507
608
  readAgent(runId: string, roleId: string): Promise<TeamAgentRecord>;
508
609
  readAgents(runId: string): Promise<TeamAgentRecord[]>;
610
+ readMessages(runId: string): Promise<TeamMessageRecord[]>;
611
+ readEvents(runId: string): Promise<TeamEventRecord[]>;
612
+ readMessageList(runId: string): Promise<TeamPendingMessageRecord[]>;
613
+ writeMessageList(
614
+ runId: string,
615
+ messages: TeamPendingMessageRecord[],
616
+ updatedAt: string,
617
+ ): Promise<void>;
618
+ writeStatus(runId: string, snapshot: TeamRunStatusSnapshot): Promise<void>;
509
619
  appendMessage(runId: string, message: TeamMessageRecord): Promise<void>;
510
620
  appendEvent(runId: string, event: TeamEventRecord): Promise<void>;
511
621
  }
512
622
 
513
623
  const SAFE_ROLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
624
+ export const TEAM_INTERNAL_WAKE_SIGNAL = [
625
+ "[EvoDev internal wake signal]",
626
+ "No user request is included in this message. Continue only from EvoDev team inbox messages injected by hooks.",
627
+ ].join("\n");
514
628
  const BUILT_IN_ROLE_PROMPTS: Record<
515
629
  string,
516
630
  { roleName: string; description: string; prompt: string }
@@ -519,25 +633,25 @@ const BUILT_IN_ROLE_PROMPTS: Record<
519
633
  roleName: "Main Conductor",
520
634
  description: "Coordinates the EvoDev team run and owns final synthesis.",
521
635
  prompt:
522
- "You are the main conductor for this EvoDev team run. Coordinate role agents, keep decisions explicit, and synthesize final outcomes.",
636
+ "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
637
  },
524
638
  reviewer: {
525
639
  roleName: "Code Reviewer",
526
640
  description: "Reviews implementation quality, risks, and regressions.",
527
641
  prompt:
528
- "You are the code reviewer for this EvoDev team run. Review changes and report concrete findings.",
642
+ "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
643
  },
530
644
  tester: {
531
645
  roleName: "Test Engineer",
532
646
  description: "Verifies behavior and identifies test gaps.",
533
647
  prompt:
534
- "You are the test engineer for this EvoDev team run. Run or recommend focused verification and report gaps.",
648
+ "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
649
  },
536
650
  executor: {
537
651
  roleName: "Implementation Executor",
538
652
  description: "Implements scoped changes assigned by main.",
539
653
  prompt:
540
- "You are the implementation executor for this EvoDev team run. Keep changes scoped and report verification evidence.",
654
+ "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
655
  },
542
656
  };
543
657
 
@@ -547,6 +661,7 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
547
661
  return {
548
662
  paths,
549
663
  async createRunDirs(runId) {
664
+ await migrateLegacyRunDirIfNeeded(paths, runId);
550
665
  await mkdir(paths.runDir(runId), { recursive: true });
551
666
  await mkdir(paths.agentsDir(runId), { recursive: true });
552
667
  },
@@ -555,6 +670,7 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
555
670
  await writeJson(paths.runPath(run.runId), parseTeamRunRecord(run));
556
671
  },
557
672
  async readRun(runId) {
673
+ await migrateLegacyRunDirIfNeeded(paths, runId);
558
674
  return parseTeamRunRecord(JSON.parse(await readFile(paths.runPath(runId), "utf8")));
559
675
  },
560
676
  async writeLatestRunId(runId) {
@@ -567,7 +683,17 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
567
683
  if (latest?.version === 1 && typeof latest.runId === "string") return latest.runId;
568
684
  return null;
569
685
  } catch {
570
- return null;
686
+ try {
687
+ const latest = JSON.parse(await readFile(paths.legacyLatestRunPath, "utf8"));
688
+ if (latest?.version === 1 && typeof latest.runId === "string") {
689
+ await migrateLegacyRunDirIfNeeded(paths, latest.runId);
690
+ await this.writeLatestRunId(latest.runId);
691
+ return latest.runId;
692
+ }
693
+ return null;
694
+ } catch {
695
+ return null;
696
+ }
571
697
  }
572
698
  },
573
699
  async writeAgent(agent) {
@@ -575,11 +701,13 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
575
701
  await writeJson(paths.agentPath(agent.runId, agent.roleId), parseTeamAgentRecord(agent));
576
702
  },
577
703
  async readAgent(runId, roleId) {
704
+ await migrateLegacyRunDirIfNeeded(paths, runId);
578
705
  return parseTeamAgentRecord(
579
706
  JSON.parse(await readFile(paths.agentPath(runId, roleId), "utf8")),
580
707
  );
581
708
  },
582
709
  async readAgents(runId) {
710
+ await migrateLegacyRunDirIfNeeded(paths, runId);
583
711
  try {
584
712
  const entries = await readdir(paths.agentsDir(runId));
585
713
  const agents = await Promise.all(
@@ -596,9 +724,51 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
596
724
  return [];
597
725
  }
598
726
  },
727
+ async readMessages(runId) {
728
+ await migrateLegacyRunDirIfNeeded(paths, runId);
729
+ return readJsonLines(paths.messagesPath(runId), parseTeamMessageRecord);
730
+ },
731
+ async readEvents(runId) {
732
+ await migrateLegacyRunDirIfNeeded(paths, runId);
733
+ return readJsonLines(paths.eventsPath(runId), parseTeamEventRecord);
734
+ },
735
+ async readMessageList(runId) {
736
+ await migrateLegacyRunDirIfNeeded(paths, runId);
737
+ try {
738
+ const file = parseTeamMessageListFile(
739
+ JSON.parse(await readFile(paths.messageListPath(runId), "utf8")),
740
+ );
741
+ return file.messages;
742
+ } catch (error) {
743
+ if (isNotFoundError(error)) return [];
744
+ throw error;
745
+ }
746
+ },
747
+ async writeMessageList(runId, messages, updatedAt) {
748
+ await this.createRunDirs(runId);
749
+ await writeJson(paths.messageListPath(runId), {
750
+ version: 1,
751
+ updatedAt,
752
+ messages: messages.map(parseTeamPendingMessageRecord),
753
+ });
754
+ },
755
+ async writeStatus(runId, snapshot) {
756
+ await this.createRunDirs(runId);
757
+ await writeJson(paths.statusPath(runId), snapshot);
758
+ },
599
759
  async appendMessage(runId, message) {
600
760
  await this.createRunDirs(runId);
601
- await appendJsonLine(paths.messagesPath(runId), parseTeamMessageRecord(message));
761
+ const parsed = parseTeamMessageRecord(message);
762
+ await appendJsonLine(paths.messagesPath(runId), parsed);
763
+ const pending = await this.readMessageList(runId);
764
+ await this.writeMessageList(
765
+ runId,
766
+ [
767
+ ...pending.filter((item) => item.messageId !== parsed.messageId),
768
+ createPendingMessageRecord(parsed),
769
+ ],
770
+ parsed.createdAt,
771
+ );
602
772
  },
603
773
  async appendEvent(runId, event) {
604
774
  await this.createRunDirs(runId);
@@ -609,23 +779,84 @@ export function createTeamRunStore(homeDir?: string): TeamRunStore {
609
779
 
610
780
  export function resolveTeamRunPaths(homeDir?: string) {
611
781
  const paths = resolveEvoDevPaths(homeDir);
782
+ const legacyRunsDir = join(paths.rootDir, "runs");
612
783
  return {
613
784
  rootDir: paths.rootDir,
614
785
  roleAgentsDir: paths.roleAgentsDir,
615
786
  teamsDir: paths.teamsDir,
616
787
  runsDir: paths.runsDir,
788
+ legacyRunsDir,
617
789
  latestRunPath: paths.latestRunPath,
790
+ legacyLatestRunPath: join(legacyRunsDir, "latest.json"),
618
791
  runDir: (runId: string) => join(paths.runsDir, runId),
792
+ legacyRunDir: (runId: string) => join(legacyRunsDir, runId),
619
793
  runPath: (runId: string) => join(paths.runsDir, runId, "run.json"),
794
+ legacyRunPath: (runId: string) => join(legacyRunsDir, runId, "run.json"),
620
795
  tmuxPath: (runId: string) => join(paths.runsDir, runId, "tmux.json"),
796
+ legacyTmuxPath: (runId: string) => join(legacyRunsDir, runId, "tmux.json"),
621
797
  agentsDir: (runId: string) => join(paths.runsDir, runId, "agents"),
798
+ legacyAgentsDir: (runId: string) => join(legacyRunsDir, runId, "agents"),
622
799
  agentPath: (runId: string, roleId: string) =>
623
800
  join(paths.runsDir, runId, "agents", `${roleId}.json`),
801
+ legacyAgentPath: (runId: string, roleId: string) =>
802
+ join(legacyRunsDir, runId, "agents", `${roleId}.json`),
624
803
  messagesPath: (runId: string) => join(paths.runsDir, runId, "messages.jsonl"),
804
+ legacyMessagesPath: (runId: string) => join(legacyRunsDir, runId, "messages.jsonl"),
625
805
  eventsPath: (runId: string) => join(paths.runsDir, runId, "events.jsonl"),
806
+ legacyEventsPath: (runId: string) => join(legacyRunsDir, runId, "events.jsonl"),
807
+ statusPath: (runId: string) => join(paths.runsDir, runId, "status.json"),
808
+ messageListPath: (runId: string) => join(paths.runsDir, runId, "message-list.json"),
626
809
  };
627
810
  }
628
811
 
812
+ async function migrateLegacyRunDirIfNeeded(
813
+ paths: ReturnType<typeof resolveTeamRunPaths>,
814
+ runId: string,
815
+ ): Promise<void> {
816
+ const nextDir = paths.runDir(runId);
817
+ if (await pathExists(nextDir)) return;
818
+ const legacyDir = paths.legacyRunDir(runId);
819
+ if (!(await pathExists(legacyDir))) return;
820
+ await mkdir(dirname(nextDir), { recursive: true });
821
+ await cp(legacyDir, nextDir, { recursive: true, errorOnExist: false, force: false });
822
+ }
823
+
824
+ async function writeTeamStatusSnapshot(input: {
825
+ store: TeamRunStore;
826
+ runId: string;
827
+ now: string;
828
+ lastEvent?: string | null;
829
+ }): Promise<TeamRunStatusSnapshot | null> {
830
+ try {
831
+ const run = await input.store.readRun(input.runId);
832
+ const agents = await input.store.readAgents(run.runId);
833
+ const snapshot: TeamRunStatusSnapshot = {
834
+ version: 1,
835
+ runId: run.runId,
836
+ repoRoot: run.repoRoot,
837
+ runStatus: run.status,
838
+ tmux: { session: run.tmux.session },
839
+ agents: agents.map((agent) => ({
840
+ roleId: agent.roleId,
841
+ agentId: agent.agentId,
842
+ roleName: agent.roleName,
843
+ runtime: agent.runtime,
844
+ status: agent.status,
845
+ paneId: agent.tmux.paneId,
846
+ window: agent.tmux.window,
847
+ nativeSessionId: agent.nativeSession.sessionId,
848
+ updatedAt: agent.updatedAt,
849
+ })),
850
+ lastEvent: input.lastEvent ?? null,
851
+ updatedAt: input.now,
852
+ };
853
+ await input.store.writeStatus(run.runId, snapshot);
854
+ return snapshot;
855
+ } catch {
856
+ return null;
857
+ }
858
+ }
859
+
629
860
  export async function startTeamRun(input: StartTeamRunInput): Promise<TeamRunStartResult> {
630
861
  const store = createTeamRunStore(input.homeDir);
631
862
  const now = input.now ?? new Date();
@@ -639,11 +870,13 @@ export async function startTeamRun(input: StartTeamRunInput): Promise<TeamRunSta
639
870
  overrides: input.mainOverrides,
640
871
  });
641
872
  const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
642
- const startupPrompt = createAgentStartupPrompt({
873
+ const startupPrompt = await createAgentStartupPrompt({
874
+ homeDir: input.homeDir,
643
875
  runId,
644
876
  repoRoot: input.repoRoot,
645
877
  role,
646
878
  roster: [],
879
+ now: createdAt,
647
880
  });
648
881
  const handle = await runtimeAdapter.createRun({
649
882
  runId,
@@ -688,6 +921,12 @@ export async function startTeamRun(input: StartTeamRunInput): Promise<TeamRunSta
688
921
  agentId: agent.agentId,
689
922
  }),
690
923
  );
924
+ await writeTeamStatusSnapshot({
925
+ store,
926
+ runId,
927
+ now: createdAt,
928
+ lastEvent: "TeamRunStarted",
929
+ });
691
930
 
692
931
  return { run, mainAgent: agent };
693
932
  }
@@ -713,11 +952,13 @@ export async function spawnTeamRole(input: SpawnTeamRoleInput): Promise<TeamRole
713
952
  });
714
953
  const agents = await store.readAgents(run.runId);
715
954
  const mainAgent = agents.find((agent) => agent.roleId === "main");
716
- const startupPrompt = createAgentStartupPrompt({
955
+ const startupPrompt = await createAgentStartupPrompt({
956
+ homeDir: input.homeDir,
717
957
  runId: run.runId,
718
958
  repoRoot: run.repoRoot,
719
959
  role,
720
960
  roster: agents,
961
+ now,
721
962
  });
722
963
  const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
723
964
  const handle = await runtimeAdapter.spawnAgent({
@@ -744,6 +985,12 @@ export async function spawnTeamRole(input: SpawnTeamRoleInput): Promise<TeamRole
744
985
  agentId: agent.agentId,
745
986
  }),
746
987
  );
988
+ await writeTeamStatusSnapshot({
989
+ store,
990
+ runId: run.runId,
991
+ now,
992
+ lastEvent: "AgentSpawned",
993
+ });
747
994
 
748
995
  return { run: updatedRun, agent, created: true };
749
996
  }
@@ -802,6 +1049,12 @@ export async function stopTeamRole(input: StopTeamRoleInput): Promise<TeamRoleSt
802
1049
  agentId,
803
1050
  }),
804
1051
  );
1052
+ await writeTeamStatusSnapshot({
1053
+ store,
1054
+ runId: run.runId,
1055
+ now,
1056
+ lastEvent: "AgentStopped",
1057
+ });
805
1058
 
806
1059
  return { ok: true, run: updatedRun, agent: stoppedAgent, stopped: true };
807
1060
  }
@@ -833,6 +1086,166 @@ export async function sendTeamMessage(input: SendTeamMessageInput): Promise<Team
833
1086
  }).send(input);
834
1087
  }
835
1088
 
1089
+ export async function readPendingTeamMessagesForRole(
1090
+ input: ReadPendingTeamMessagesInput,
1091
+ ): Promise<TeamMessageRecord[]> {
1092
+ const store = createTeamRunStore(input.homeDir);
1093
+ const messages = await store.readMessageList(input.runId);
1094
+ const pending = messages.filter(
1095
+ (message) => message.toRoleId === input.roleId && message.deliveryState !== "failed",
1096
+ );
1097
+ return pending.slice(0, input.limit ?? 5);
1098
+ }
1099
+
1100
+ export async function markTeamMessagesDelivered(
1101
+ input: MarkTeamMessagesDeliveredInput,
1102
+ ): Promise<void> {
1103
+ if (input.messageIds.length === 0) return;
1104
+ const store = createTeamRunStore(input.homeDir);
1105
+ const now = (input.now ?? new Date()).toISOString();
1106
+ const delivered = new Set(input.messageIds);
1107
+ const pending = await store.readMessageList(input.runId);
1108
+ await store.writeMessageList(
1109
+ input.runId,
1110
+ pending.filter((message) => !delivered.has(message.messageId)),
1111
+ now,
1112
+ );
1113
+ for (const messageId of input.messageIds) {
1114
+ assertSafeId(messageId, "messageId");
1115
+ await store.appendEvent(
1116
+ input.runId,
1117
+ createTeamEvent(input.runId, "TeamMessageDelivered", `Delivered message ${messageId}.`, now, {
1118
+ roleId: input.roleId,
1119
+ messageId,
1120
+ }),
1121
+ );
1122
+ }
1123
+ }
1124
+
1125
+ export async function schedulePendingTeamMessageDelivery(input: {
1126
+ homeDir?: string;
1127
+ runId?: string;
1128
+ roleId?: string;
1129
+ now?: Date;
1130
+ runtimeAdapter?: TeamRuntimeAdapter;
1131
+ }): Promise<TeamMessageDeliveryScheduleResult> {
1132
+ const store = createTeamRunStore(input.homeDir);
1133
+ const runId = await resolveRequestedRunId(store, input.runId);
1134
+ const run = await store.readRun(runId);
1135
+ const pending = await store.readMessageList(run.runId);
1136
+ const targetRoleIds = new Set(
1137
+ pending
1138
+ .filter((message) => input.roleId === undefined || message.toRoleId === input.roleId)
1139
+ .map((message) => message.toRoleId),
1140
+ );
1141
+ const result: TeamMessageDeliveryScheduleResult = {
1142
+ wokenRoleIds: [],
1143
+ recoveredRoleIds: [],
1144
+ needsUserAttentionRoleIds: [],
1145
+ warnings: [],
1146
+ };
1147
+ if (targetRoleIds.size === 0) return result;
1148
+
1149
+ const runtimeAdapter = input.runtimeAdapter ?? createTmuxRuntimeAdapter();
1150
+ const livePaneIds = await readLivePaneIds(runtimeAdapter, run.tmux.session);
1151
+ const now = (input.now ?? new Date()).toISOString();
1152
+
1153
+ for (const roleId of targetRoleIds) {
1154
+ const agent = await store.readAgent(run.runId, roleId).catch(() => null);
1155
+ if (agent === null) continue;
1156
+
1157
+ if (
1158
+ agent.roleId === "main" &&
1159
+ (run.status !== "running" || !isActiveTeamAgentStatus(agent.status))
1160
+ ) {
1161
+ const updated: TeamAgentRecord = {
1162
+ ...agent,
1163
+ status: "needs-user-attention",
1164
+ updatedAt: now,
1165
+ };
1166
+ await store.writeAgent(updated);
1167
+ await writeTeamStatusSnapshot({
1168
+ store,
1169
+ runId: run.runId,
1170
+ now,
1171
+ lastEvent: "AgentNeedsUserAttention",
1172
+ });
1173
+ result.needsUserAttentionRoleIds.push(roleId);
1174
+ continue;
1175
+ }
1176
+
1177
+ if (run.status !== "running") continue;
1178
+
1179
+ if (!isActiveTeamAgentStatus(agent.status)) {
1180
+ const resumed = await resumeTeamRun({
1181
+ homeDir: input.homeDir,
1182
+ runId: run.runId,
1183
+ now: new Date(now),
1184
+ runtimeAdapter,
1185
+ missingSessionDecision: "recreate",
1186
+ notifyMain: false,
1187
+ });
1188
+ const recovered = resumed.outcomes.find(
1189
+ (outcome) =>
1190
+ outcome.roleId === roleId &&
1191
+ (outcome.outcome === "resumed" || outcome.outcome === "recreated"),
1192
+ );
1193
+ if (recovered !== undefined) result.recoveredRoleIds.push(roleId);
1194
+ else result.warnings.push(`Role ${roleId} could not be automatically recovered.`);
1195
+ continue;
1196
+ }
1197
+
1198
+ if (isIdleTeamAgentStatus(agent.status) && livePaneIds.has(agent.tmux.paneId)) {
1199
+ await runtimeAdapter.sendInput({
1200
+ session: agent.tmux.session,
1201
+ paneId: agent.tmux.paneId,
1202
+ text: TEAM_INTERNAL_WAKE_SIGNAL,
1203
+ });
1204
+ await markPendingMessagesWakeupSent({
1205
+ store,
1206
+ runId: run.runId,
1207
+ roleId,
1208
+ now,
1209
+ });
1210
+ result.wokenRoleIds.push(roleId);
1211
+ }
1212
+ }
1213
+
1214
+ return result;
1215
+ }
1216
+
1217
+ async function markPendingMessagesWakeupSent(input: {
1218
+ store: TeamRunStore;
1219
+ runId: string;
1220
+ roleId: string;
1221
+ now: string;
1222
+ }): Promise<void> {
1223
+ const pending = await input.store.readMessageList(input.runId);
1224
+ const updated = pending.map((message) =>
1225
+ message.toRoleId === input.roleId
1226
+ ? {
1227
+ ...message,
1228
+ deliveryState: "wakeup-sent" as const,
1229
+ attemptCount: message.attemptCount + 1,
1230
+ lastAttemptAt: input.now,
1231
+ }
1232
+ : message,
1233
+ );
1234
+ await input.store.writeMessageList(input.runId, updated, input.now);
1235
+ for (const message of updated.filter((item) => item.toRoleId === input.roleId)) {
1236
+ await input.store.appendEvent(
1237
+ input.runId,
1238
+ createTeamEvent(
1239
+ input.runId,
1240
+ "TeamMessageWakeupSent",
1241
+ `Sent wake signal for message ${message.messageId}.`,
1242
+ input.now,
1243
+ { roleId: input.roleId, messageId: message.messageId },
1244
+ ),
1245
+ );
1246
+ }
1247
+ }
1248
+
836
1249
  async function sendTeamMessageWithBrokerContext(
837
1250
  options: TeamMessageBrokerOptions,
838
1251
  input: TeamMessageBrokerSendInput,
@@ -856,11 +1269,11 @@ async function sendTeamMessageWithBrokerContext(
856
1269
  };
857
1270
  }
858
1271
  const target = await store.readAgent(run.runId, input.toRoleId);
859
- if (!isActiveTeamAgentStatus(target.status)) {
1272
+ if (target.status === "failed" || target.status === "unknown") {
860
1273
  return {
861
1274
  ok: false,
862
- error: "target-role-not-running",
863
- message: `Role ${input.toRoleId} is not running in run ${run.runId}.`,
1275
+ error: "target-role-unavailable",
1276
+ message: `Role ${input.toRoleId} is not available in run ${run.runId}.`,
864
1277
  };
865
1278
  }
866
1279
 
@@ -896,58 +1309,21 @@ async function sendTeamMessageWithBrokerContext(
896
1309
  },
897
1310
  ),
898
1311
  );
1312
+ await schedulePendingTeamMessageDelivery({
1313
+ homeDir: options.homeDir,
1314
+ runId: run.runId,
1315
+ roleId: input.toRoleId,
1316
+ now: new Date(now),
1317
+ runtimeAdapter: options.runtimeAdapter,
1318
+ });
899
1319
 
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
- }
1320
+ return {
1321
+ ok: true,
1322
+ messageId: message.messageId,
1323
+ delivery: "queued",
1324
+ queuedFor: input.toRoleId,
1325
+ cc: ccRoleIds,
1326
+ };
951
1327
  }
952
1328
 
953
1329
  async function spawnTeamRoleWithBrokerContext(
@@ -1084,6 +1460,12 @@ export async function stopTeamRun(input: StopTeamRunInput): Promise<TeamStatusRe
1084
1460
  run.runId,
1085
1461
  createTeamEvent(run.runId, "TeamRunStopped", `Stopped team run ${run.runId}.`, now),
1086
1462
  );
1463
+ await writeTeamStatusSnapshot({
1464
+ store,
1465
+ runId: run.runId,
1466
+ now,
1467
+ lastEvent: "TeamRunStopped",
1468
+ });
1087
1469
  return { run: stoppedRun, agents: stoppedAgents };
1088
1470
  }
1089
1471
 
@@ -1097,12 +1479,13 @@ export async function getTeamStatus(input: TeamStatusInput = {}): Promise<TeamSt
1097
1479
 
1098
1480
  export async function listTeamRuns(input: ListTeamRunsInput = {}): Promise<TeamRunRecord[]> {
1099
1481
  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;
1482
+ const entries = new Set<string>();
1483
+ for (const runsDir of [store.paths.runsDir, store.paths.legacyRunsDir]) {
1484
+ try {
1485
+ for (const entry of await readdir(runsDir)) entries.add(entry);
1486
+ } catch (error) {
1487
+ if (!isNotFoundError(error)) throw error;
1488
+ }
1106
1489
  }
1107
1490
  const runs: TeamRunRecord[] = [];
1108
1491
  for (const entry of entries) {
@@ -1145,6 +1528,18 @@ export async function reconcileTeamRun(
1145
1528
  .map((agent) => ({ ...agent, status: "stopped" as const, updatedAt: now }));
1146
1529
 
1147
1530
  if (stoppedAgents.length === 0) {
1531
+ await schedulePendingTeamMessageDelivery({
1532
+ homeDir: input.homeDir,
1533
+ runId: run.runId,
1534
+ now: new Date(now),
1535
+ runtimeAdapter,
1536
+ });
1537
+ await writeTeamStatusSnapshot({
1538
+ store,
1539
+ runId: run.runId,
1540
+ now,
1541
+ lastEvent: "TeamReconciled",
1542
+ });
1148
1543
  return { run, agents, stoppedAgents: [], notifications: [], runtimeAvailable: true };
1149
1544
  }
1150
1545
 
@@ -1181,10 +1576,24 @@ export async function reconcileTeamRun(
1181
1576
  livePaneIds,
1182
1577
  runtimeAdapter,
1183
1578
  });
1579
+ await schedulePendingTeamMessageDelivery({
1580
+ homeDir: input.homeDir,
1581
+ runId: run.runId,
1582
+ now: new Date(now),
1583
+ runtimeAdapter,
1584
+ });
1585
+ await writeTeamStatusSnapshot({
1586
+ store,
1587
+ runId: run.runId,
1588
+ now,
1589
+ lastEvent: "TeamReconciled",
1590
+ });
1591
+ const finalRun = await store.readRun(run.runId);
1592
+ const finalAgents = await store.readAgents(run.runId);
1184
1593
 
1185
1594
  return {
1186
- run: updatedRun,
1187
- agents: updatedAgents,
1595
+ run: finalRun,
1596
+ agents: finalAgents,
1188
1597
  stoppedAgents,
1189
1598
  notifications,
1190
1599
  runtimeAvailable: livePaneIds.size > 0,
@@ -1222,10 +1631,77 @@ export async function recordTeamAgentNativeSession(
1222
1631
  },
1223
1632
  ),
1224
1633
  );
1634
+ await writeTeamStatusSnapshot({
1635
+ store,
1636
+ runId,
1637
+ now,
1638
+ lastEvent: "AgentNativeSessionRecorded",
1639
+ });
1225
1640
 
1226
1641
  return updatedAgent;
1227
1642
  }
1228
1643
 
1644
+ export async function updateTeamAgentHookState(
1645
+ input: UpdateTeamAgentHookStateInput,
1646
+ ): Promise<{ agent: TeamAgentRecord; statusPath: string; agentPath: string }> {
1647
+ const store = createTeamRunStore(input.homeDir);
1648
+ const runId = await resolveRequestedRunId(store, input.runId);
1649
+ const agent = await store.readAgent(runId, input.roleId);
1650
+ const now = (input.now ?? new Date()).toISOString();
1651
+ const nextStatus = teamAgentStatusForHookEvent(input.hookEvent, agent.status);
1652
+ const updatedAgent: TeamAgentRecord =
1653
+ nextStatus === agent.status
1654
+ ? { ...agent, updatedAt: now }
1655
+ : {
1656
+ ...agent,
1657
+ status: nextStatus,
1658
+ updatedAt: now,
1659
+ };
1660
+ await store.writeAgent(updatedAgent);
1661
+ await store.appendEvent(
1662
+ runId,
1663
+ createTeamEvent(
1664
+ runId,
1665
+ "AgentHookStateUpdated",
1666
+ `Updated ${input.roleId} hook state after ${input.hookEvent}.`,
1667
+ now,
1668
+ { roleId: input.roleId, agentId: updatedAgent.agentId },
1669
+ ),
1670
+ );
1671
+ await writeTeamStatusSnapshot({
1672
+ store,
1673
+ runId,
1674
+ now,
1675
+ lastEvent: input.hookEvent,
1676
+ });
1677
+ return {
1678
+ agent: updatedAgent,
1679
+ statusPath: store.paths.statusPath(runId),
1680
+ agentPath: store.paths.agentPath(runId, input.roleId),
1681
+ };
1682
+ }
1683
+
1684
+ export async function createTeamRoleRuntimeContext(
1685
+ input: TeamRoleRuntimeContextInput,
1686
+ ): Promise<string> {
1687
+ const store = createTeamRunStore(input.homeDir);
1688
+ const run = await store.readRun(input.runId);
1689
+ const agent = await store.readAgent(run.runId, input.roleId);
1690
+ const agents = await store.readAgents(run.runId);
1691
+ const role = await resolveRoleForAgentRecovery({
1692
+ homeDir: input.homeDir,
1693
+ repoRoot: run.repoRoot,
1694
+ agent,
1695
+ });
1696
+ return createAgentStartupPrompt({
1697
+ homeDir: input.homeDir,
1698
+ runId: run.runId,
1699
+ repoRoot: run.repoRoot,
1700
+ role,
1701
+ roster: agents,
1702
+ });
1703
+ }
1704
+
1229
1705
  export async function resumeTeamRun(input: ResumeTeamRunInput = {}): Promise<TeamRunResumeResult> {
1230
1706
  const store = createTeamRunStore(input.homeDir);
1231
1707
  const runId = await resolveRequestedRunId(store, input.runId);
@@ -1262,11 +1738,13 @@ export async function resumeTeamRun(input: ResumeTeamRunInput = {}): Promise<Tea
1262
1738
  ...updatedAgents,
1263
1739
  ...originalAgents.filter((item) => item.roleId !== agent.roleId),
1264
1740
  ];
1265
- const startupPrompt = createAgentStartupPrompt({
1741
+ const startupPrompt = await createAgentStartupPrompt({
1742
+ homeDir: input.homeDir,
1266
1743
  runId: run.runId,
1267
1744
  repoRoot: run.repoRoot,
1268
1745
  role,
1269
1746
  roster,
1747
+ now,
1270
1748
  });
1271
1749
  const recoveryMode = resolveRecoveryMode(agent, decision);
1272
1750
 
@@ -1376,6 +1854,12 @@ export async function resumeTeamRun(input: ResumeTeamRunInput = {}): Promise<Tea
1376
1854
  updatedAt: now,
1377
1855
  };
1378
1856
  await store.writeRun(updatedRun);
1857
+ await writeTeamStatusSnapshot({
1858
+ store,
1859
+ runId: run.runId,
1860
+ now,
1861
+ lastEvent: "TeamRunResumed",
1862
+ });
1379
1863
 
1380
1864
  const decisionRequired = outcomes.filter((outcome) => outcome.outcome === "needs-decision");
1381
1865
  const notifications =
@@ -1419,6 +1903,8 @@ export async function listTeamAgents(input: TeamStatusInput = {}): Promise<TeamA
1419
1903
  status: agent.status,
1420
1904
  runtime: agent.runtime,
1421
1905
  canReceiveMessages: isActiveTeamAgentStatus(agent.status),
1906
+ isIdle: isIdleTeamAgentStatus(agent.status),
1907
+ isMidTurn: isMidTurnTeamAgentStatus(agent.status),
1422
1908
  }));
1423
1909
  }
1424
1910
 
@@ -1507,6 +1993,8 @@ export function createTmuxRuntimeAdapter(
1507
1993
  }
1508
1994
 
1509
1995
  export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1996
+ private messageBufferCounter = 0;
1997
+
1510
1998
  constructor(
1511
1999
  private readonly runner: TeamRuntimeCommandRunner,
1512
2000
  private readonly options: TmuxRuntimeAdapterOptions = {},
@@ -1517,6 +2005,7 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1517
2005
  runId: input.runId,
1518
2006
  repoRoot: input.repoRoot,
1519
2007
  runtimeCommands: this.options.runtimeCommands,
2008
+ environment: this.options.environment,
1520
2009
  });
1521
2010
  await this.runTmux([
1522
2011
  "new-session",
@@ -1536,7 +2025,10 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1536
2025
  `${input.sessionName}:main.0`,
1537
2026
  "#{pane_id}",
1538
2027
  ]);
1539
- return { session: input.sessionName, window: "main", paneId: pane.stdout.trim() };
2028
+ const paneId = pane.stdout.trim();
2029
+ await this.configureTeamWindow(`${input.sessionName}:main`);
2030
+ await this.setPaneTitle(paneId, input.role);
2031
+ return { session: input.sessionName, window: "main", paneId };
1540
2032
  }
1541
2033
 
1542
2034
  async spawnAgent(input: TeamRuntimeSpawnAgentInput): Promise<TeamRuntimeAgentHandle> {
@@ -1545,9 +2037,10 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1545
2037
  runId: input.runId,
1546
2038
  repoRoot: input.repoRoot,
1547
2039
  runtimeCommands: this.options.runtimeCommands,
2040
+ environment: this.options.environment,
1548
2041
  });
1549
2042
  if (input.targetPaneId !== undefined) {
1550
- const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command);
2043
+ const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command, input.role);
1551
2044
  return { session: input.sessionName, window: "main", paneId: pane };
1552
2045
  }
1553
2046
 
@@ -1565,7 +2058,10 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1565
2058
  input.repoRoot,
1566
2059
  command,
1567
2060
  ]);
1568
- return { session: input.sessionName, window, paneId: pane.stdout.trim() };
2061
+ const paneId = pane.stdout.trim();
2062
+ await this.configureTeamWindow(`${input.sessionName}:${window}`);
2063
+ await this.setPaneTitle(paneId, input.role);
2064
+ return { session: input.sessionName, window, paneId };
1569
2065
  }
1570
2066
 
1571
2067
  async recoverAgent(input: TeamRuntimeRecoverAgentInput): Promise<TeamRuntimeAgentHandle> {
@@ -1576,14 +2072,16 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1576
2072
  runId: input.runId,
1577
2073
  repoRoot: input.repoRoot,
1578
2074
  runtimeCommands: this.options.runtimeCommands,
2075
+ environment: this.options.environment,
1579
2076
  })
1580
2077
  : buildAgentResumeShellCommand(input.role, input.startupPrompt, {
1581
2078
  runId: input.runId,
1582
2079
  repoRoot: input.repoRoot,
1583
2080
  mode: input.mode,
1584
2081
  runtimeCommands: this.options.runtimeCommands,
2082
+ environment: this.options.environment,
1585
2083
  });
1586
- const session = await this.runner.run("tmux", ["has-session", "-t", input.sessionName]);
2084
+ const session = await this.runner.run("tmux", ["-u", "has-session", "-t", input.sessionName]);
1587
2085
  if (session.exitCode !== 0) {
1588
2086
  await this.runTmux([
1589
2087
  "new-session",
@@ -1603,11 +2101,14 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1603
2101
  `${input.sessionName}:${window}.0`,
1604
2102
  "#{pane_id}",
1605
2103
  ]);
1606
- return recoveredHandle(input, window, pane.stdout.trim());
2104
+ const paneId = pane.stdout.trim();
2105
+ await this.configureTeamWindow(`${input.sessionName}:${window}`);
2106
+ await this.setPaneTitle(paneId, input.role);
2107
+ return recoveredHandle(input, window, paneId);
1607
2108
  }
1608
2109
 
1609
2110
  if (input.targetPaneId !== undefined && input.role.roleId !== "main") {
1610
- const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command);
2111
+ const pane = await this.splitPane(input.targetPaneId, input.repoRoot, command, input.role);
1611
2112
  return recoveredHandle(input, "main", pane);
1612
2113
  }
1613
2114
 
@@ -1625,13 +2126,17 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1625
2126
  input.repoRoot,
1626
2127
  command,
1627
2128
  ]);
1628
- return recoveredHandle(input, window, pane.stdout.trim());
2129
+ const paneId = pane.stdout.trim();
2130
+ await this.configureTeamWindow(`${input.sessionName}:${window}`);
2131
+ await this.setPaneTitle(paneId, input.role);
2132
+ return recoveredHandle(input, window, paneId);
1629
2133
  }
1630
2134
 
1631
2135
  private async splitPane(
1632
2136
  targetPaneId: string,
1633
2137
  repoRoot: string,
1634
2138
  command: string,
2139
+ role: ResolvedTeamRole,
1635
2140
  ): Promise<string> {
1636
2141
  const pane = await this.runTmux([
1637
2142
  "split-window",
@@ -1646,18 +2151,43 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1646
2151
  repoRoot,
1647
2152
  command,
1648
2153
  ]);
1649
- await this.runTmux(["select-layout", "-t", targetPaneId, "tiled"]);
1650
- return pane.stdout.trim();
2154
+ const paneId = pane.stdout.trim();
2155
+ await this.configureTeamWindow(targetPaneId);
2156
+ await this.setPaneTitle(paneId, role);
2157
+ await this.runTmux(["select-layout", "-t", targetPaneId, "main-vertical"]);
2158
+ return paneId;
2159
+ }
2160
+
2161
+ private async configureTeamWindow(target: string): Promise<void> {
2162
+ await this.runTmux(["set-option", "-w", "-t", target, "pane-border-status", "top"]);
2163
+ await this.runTmux([
2164
+ "set-option",
2165
+ "-w",
2166
+ "-t",
2167
+ target,
2168
+ "pane-border-format",
2169
+ "[#{pane_index}] #{pane_title}",
2170
+ ]);
2171
+ }
2172
+
2173
+ private async setPaneTitle(paneId: string, role: ResolvedTeamRole): Promise<void> {
2174
+ await this.runTmux(["select-pane", "-t", paneId, "-T", formatTeamPaneTitle(role)]);
1651
2175
  }
1652
2176
 
1653
2177
  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"]);
2178
+ const bufferName = `evodev-message-${Date.now()}-${this.messageBufferCounter++}`;
2179
+ await this.runTmux(["set-buffer", "-b", bufferName, input.text]);
2180
+ try {
2181
+ await this.runTmux(["paste-buffer", "-d", "-p", "-r", "-b", bufferName, "-t", input.paneId]);
2182
+ } catch (error) {
2183
+ await this.runner.run("tmux", ["-u", "delete-buffer", "-b", bufferName]);
2184
+ throw error;
2185
+ }
2186
+ await this.runTmux(["send-keys", "-t", input.paneId, "C-m"]);
1657
2187
  }
1658
2188
 
1659
2189
  async listPanes(input: TeamRuntimeListPanesInput): Promise<TeamRuntimePaneInfo[]> {
1660
- const panes = await this.runTmux(["list-panes", "-t", input.session, "-F", "#{pane_id}"]);
2190
+ const panes = await this.runTmux(["list-panes", "-s", "-t", input.session, "-F", "#{pane_id}"]);
1661
2191
  return panes.stdout
1662
2192
  .split("\n")
1663
2193
  .map((paneId) => paneId.trim())
@@ -1675,15 +2205,15 @@ export class TmuxRuntimeAdapter implements TeamRuntimeAdapter {
1675
2205
 
1676
2206
  formatAttachCommand(input: TeamRuntimeAttachInput): string {
1677
2207
  return input.paneId === undefined
1678
- ? `tmux attach-session -t ${input.session}`
1679
- : `tmux attach-session -t ${input.session} \\; select-pane -t ${input.paneId}`;
2208
+ ? `tmux -u attach-session -t ${input.session}`
2209
+ : `tmux -u attach-session -t ${input.session} \\; select-pane -t ${input.paneId}`;
1680
2210
  }
1681
2211
 
1682
2212
  private async runTmux(
1683
2213
  args: string[],
1684
2214
  options?: { input?: string },
1685
2215
  ): Promise<TeamRuntimeCommandResult> {
1686
- const result = await this.runner.run("tmux", args, options);
2216
+ const result = await this.runner.run("tmux", ["-u", ...args], options);
1687
2217
  if (result.exitCode !== 0) {
1688
2218
  throw new Error(`tmux ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
1689
2219
  }
@@ -1854,84 +2384,50 @@ async function resolveRoleForAgentRecovery(input: {
1854
2384
  }
1855
2385
 
1856
2386
  function createAgentStartupPrompt(input: {
2387
+ homeDir?: string;
1857
2388
  runId: string;
1858
2389
  repoRoot: string;
1859
2390
  role: ResolvedTeamRole;
1860
2391
  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");
1917
- }
1918
-
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");
2392
+ now?: string;
2393
+ }): Promise<string> {
2394
+ return createScopedTeamStartupContext(input).then((scopedContext) =>
2395
+ renderTeamRoleStartupPrompt({ ...input, scopedContext }),
2396
+ );
2397
+ }
2398
+
2399
+ async function createScopedTeamStartupContext(input: {
2400
+ homeDir?: string;
2401
+ runId: string;
2402
+ repoRoot: string;
2403
+ role: ResolvedTeamRole;
2404
+ now?: string;
2405
+ }): Promise<string | null> {
2406
+ if (input.role.roleId === "main") return null;
2407
+ const homeDir = resolveEvoDevPaths(input.homeDir).homeDir;
2408
+ const settings = await readRuntimeInjectionSettings(homeDir);
2409
+ if (!settings.runtimeInjection) return null;
2410
+
2411
+ const pack = await createScopedKnowledgeContextPack({
2412
+ homeDir,
2413
+ projectKey: resolveProjectLogKey(homeDir, input.repoRoot),
2414
+ roleId: input.role.roleId,
2415
+ });
2416
+ if (pack === null) return null;
2417
+
2418
+ const sessionKey = `team-${input.runId}-${input.role.roleId}`;
2419
+ if (await hasContextInjectionReceipt({ homeDir, sessionKey, contextPackId: pack.id })) {
2420
+ return null;
2421
+ }
2422
+ await writeContextInjectionReceipt({
2423
+ homeDir,
2424
+ sessionKey,
2425
+ pack,
2426
+ trigger: "team-startup",
2427
+ hookEventId: null,
2428
+ injectedAt: input.now,
2429
+ });
2430
+ return formatScopedKnowledgePromptBlock(pack);
1935
2431
  }
1936
2432
 
1937
2433
  function buildAgentShellCommand(
@@ -1941,17 +2437,18 @@ function buildAgentShellCommand(
1941
2437
  runId: string;
1942
2438
  repoRoot: string;
1943
2439
  runtimeCommands?: TmuxRuntimeAdapterOptions["runtimeCommands"];
2440
+ environment?: TmuxRuntimeAdapterOptions["environment"];
1944
2441
  },
1945
2442
  ): string {
1946
2443
  const args =
1947
2444
  role.runtime === "codex"
1948
2445
  ? buildCodexArgs(role, startupPrompt)
1949
2446
  : buildClaudeArgs(role, startupPrompt);
1950
- const env = {
2447
+ const env = createAgentLaunchEnvironment(context.environment, {
1951
2448
  EVODEV_TEAM_RUN_ID: context.runId,
1952
2449
  EVODEV_TEAM_ROLE_ID: role.roleId,
1953
2450
  EVODEV_TEAM_REPO_ROOT: context.repoRoot,
1954
- };
2451
+ });
1955
2452
  return buildAgentCommand(resolveRuntimeCommand(role.runtime, context.runtimeCommands), args, env);
1956
2453
  }
1957
2454
 
@@ -1963,21 +2460,48 @@ function buildAgentResumeShellCommand(
1963
2460
  repoRoot: string;
1964
2461
  mode: Exclude<TeamRuntimeAgentRecoveryMode, { type: "fresh" }>;
1965
2462
  runtimeCommands?: TmuxRuntimeAdapterOptions["runtimeCommands"];
2463
+ environment?: TmuxRuntimeAdapterOptions["environment"];
1966
2464
  },
1967
2465
  ): string {
1968
2466
  const args =
1969
2467
  role.runtime === "codex"
1970
2468
  ? buildCodexResumeArgs(role, startupPrompt, context.mode)
1971
2469
  : buildClaudeResumeArgs(role, context.mode);
1972
- const env = {
2470
+ const env = createAgentLaunchEnvironment(context.environment, {
1973
2471
  EVODEV_TEAM_RUN_ID: context.runId,
1974
2472
  EVODEV_TEAM_ROLE_ID: role.roleId,
1975
2473
  EVODEV_TEAM_REPO_ROOT: context.repoRoot,
1976
2474
  EVODEV_TEAM_RECOVERY: "1",
1977
- };
2475
+ });
1978
2476
  return buildAgentCommand(resolveRuntimeCommand(role.runtime, context.runtimeCommands), args, env);
1979
2477
  }
1980
2478
 
2479
+ function createAgentLaunchEnvironment(
2480
+ source: Record<string, string | undefined> | undefined,
2481
+ evodev: Record<string, string>,
2482
+ ): Record<string, string> {
2483
+ return {
2484
+ ...selectLocaleEnvironment(source ?? process.env),
2485
+ ...evodev,
2486
+ };
2487
+ }
2488
+
2489
+ function selectLocaleEnvironment(
2490
+ source: Record<string, string | undefined>,
2491
+ ): Record<string, string> {
2492
+ const selected: Record<string, string> = {};
2493
+ for (const key of Object.keys(source).sort()) {
2494
+ const value = source[key];
2495
+ if (value === undefined || !isLocaleEnvironmentKey(key)) continue;
2496
+ selected[key] = value;
2497
+ }
2498
+ return selected;
2499
+ }
2500
+
2501
+ function isLocaleEnvironmentKey(key: string): boolean {
2502
+ return key === "LANG" || key === "LANGUAGE" || key === "LC_ALL" || /^LC_[A-Z0-9_]+$/.test(key);
2503
+ }
2504
+
1981
2505
  function buildAgentCommand(
1982
2506
  runtimeCommand: string,
1983
2507
  args: string[],
@@ -1998,7 +2522,7 @@ function resolveRuntimeCommand(
1998
2522
  function buildCodexArgs(role: ResolvedTeamRole, startupPrompt: string): string[] {
1999
2523
  const args = ["--no-alt-screen"];
2000
2524
  if (role.model !== null) args.push("--model", role.model);
2001
- args.push(startupPrompt);
2525
+ if (shouldPassVisibleStartupPrompt(role)) args.push(startupPrompt);
2002
2526
  return args;
2003
2527
  }
2004
2528
 
@@ -2015,7 +2539,7 @@ function buildCodexResumeArgs(
2015
2539
  } else {
2016
2540
  args.push("--last");
2017
2541
  }
2018
- args.push(startupPrompt);
2542
+ if (shouldPassVisibleStartupPrompt(role)) args.push(startupPrompt);
2019
2543
  return args;
2020
2544
  }
2021
2545
 
@@ -2023,10 +2547,14 @@ function buildClaudeArgs(role: ResolvedTeamRole, startupPrompt: string): string[
2023
2547
  const args = [];
2024
2548
  if (role.model !== null) args.push("--model", role.model);
2025
2549
  if (role.thinkingLevel !== null) args.push("--effort", role.thinkingLevel);
2026
- args.push(startupPrompt);
2550
+ if (shouldPassVisibleStartupPrompt(role)) args.push(startupPrompt);
2027
2551
  return args;
2028
2552
  }
2029
2553
 
2554
+ function shouldPassVisibleStartupPrompt(role: ResolvedTeamRole): boolean {
2555
+ return role.roleId !== "main";
2556
+ }
2557
+
2030
2558
  function buildClaudeResumeArgs(
2031
2559
  role: ResolvedTeamRole,
2032
2560
  mode: Exclude<TeamRuntimeAgentRecoveryMode, { type: "fresh" }>,
@@ -2089,7 +2617,7 @@ function createBuiltInRole(roleId: string, runtime: TeamAgentRuntime): unknown {
2089
2617
  function parseRolePermissions(value: unknown, main: boolean): TeamRolePermissions {
2090
2618
  const input = isRecord(value) ? value : {};
2091
2619
  return {
2092
- writeMode: parseWriteMode(input.writeMode, main ? "repo-write" : "read-only"),
2620
+ writeMode: parseWriteMode(input.writeMode, "repo-write"),
2093
2621
  canUseTeamsMcp: optionalBoolean(input.canUseTeamsMcp) ?? true,
2094
2622
  canSpawnAgents: optionalBoolean(input.canSpawnAgents) ?? main,
2095
2623
  canStopAgents: optionalBoolean(input.canStopAgents) ?? main,
@@ -2119,7 +2647,22 @@ function parseTeamAgentRecord(value: unknown): TeamAgentRecord {
2119
2647
  const agent = value as unknown as TeamAgentRecord;
2120
2648
  assertSafeId(agent.agentId, "agentId");
2121
2649
  assertSafeId(agent.roleId, "roleId");
2122
- if (!["running", "recovering", "recreated", "stopped", "failed"].includes(agent.status)) {
2650
+ if (
2651
+ ![
2652
+ "starting",
2653
+ "running",
2654
+ "busy",
2655
+ "idle",
2656
+ "waiting-input",
2657
+ "recovering",
2658
+ "recreated",
2659
+ "stopped",
2660
+ "exited",
2661
+ "needs-user-attention",
2662
+ "failed",
2663
+ "unknown",
2664
+ ].includes(agent.status)
2665
+ ) {
2123
2666
  throw new Error("Invalid agent status.");
2124
2667
  }
2125
2668
  const nativeSessionValue = isRecord(value.nativeSession) ? value.nativeSession : {};
@@ -2142,6 +2685,48 @@ function parseTeamMessageRecord(value: unknown): TeamMessageRecord {
2142
2685
  return message;
2143
2686
  }
2144
2687
 
2688
+ function createPendingMessageRecord(message: TeamMessageRecord): TeamPendingMessageRecord {
2689
+ return {
2690
+ ...message,
2691
+ deliveryState: "pending",
2692
+ attemptCount: 0,
2693
+ lastAttemptAt: null,
2694
+ };
2695
+ }
2696
+
2697
+ function parseTeamPendingMessageRecord(value: unknown): TeamPendingMessageRecord {
2698
+ const message = parseTeamMessageRecord(value);
2699
+ const input = value as Partial<TeamPendingMessageRecord>;
2700
+ const deliveryState =
2701
+ input.deliveryState === "pending" ||
2702
+ input.deliveryState === "claimed" ||
2703
+ input.deliveryState === "wakeup-sent" ||
2704
+ input.deliveryState === "failed"
2705
+ ? input.deliveryState
2706
+ : "pending";
2707
+ return {
2708
+ ...message,
2709
+ deliveryState,
2710
+ attemptCount:
2711
+ typeof input.attemptCount === "number" && Number.isInteger(input.attemptCount)
2712
+ ? input.attemptCount
2713
+ : 0,
2714
+ lastAttemptAt: optionalString(input.lastAttemptAt) ?? null,
2715
+ };
2716
+ }
2717
+
2718
+ function parseTeamMessageListFile(value: unknown): TeamMessageListFile {
2719
+ if (!isRecord(value) || value.version !== 1) throw new Error("Invalid team message list.");
2720
+ const messages = Array.isArray(value.messages)
2721
+ ? value.messages.map(parseTeamPendingMessageRecord)
2722
+ : [];
2723
+ return {
2724
+ version: 1,
2725
+ updatedAt: optionalString(value.updatedAt) ?? null,
2726
+ messages,
2727
+ };
2728
+ }
2729
+
2145
2730
  function parseTeamEventRecord(value: unknown): TeamEventRecord {
2146
2731
  if (!isRecord(value) || value.version !== 1) throw new Error("Invalid team event record.");
2147
2732
  const event = value as unknown as TeamEventRecord;
@@ -2165,8 +2750,43 @@ function parseWriteMode(value: unknown, fallback: TeamWriteMode): TeamWriteMode
2165
2750
  throw new Error("Role writeMode is invalid.");
2166
2751
  }
2167
2752
 
2168
- function isActiveTeamAgentStatus(status: TeamAgentStatus): boolean {
2169
- return status === "running" || status === "recovering" || status === "recreated";
2753
+ export function isActiveTeamAgentStatus(status: TeamAgentStatus): boolean {
2754
+ return (
2755
+ status === "starting" ||
2756
+ status === "running" ||
2757
+ status === "busy" ||
2758
+ status === "idle" ||
2759
+ status === "waiting-input" ||
2760
+ status === "recovering" ||
2761
+ status === "recreated"
2762
+ );
2763
+ }
2764
+
2765
+ export function isIdleTeamAgentStatus(status: TeamAgentStatus): boolean {
2766
+ return status === "idle" || status === "waiting-input";
2767
+ }
2768
+
2769
+ export function isMidTurnTeamAgentStatus(status: TeamAgentStatus): boolean {
2770
+ return status === "busy";
2771
+ }
2772
+
2773
+ function teamAgentStatusForHookEvent(
2774
+ hookEvent: string,
2775
+ fallback: TeamAgentStatus,
2776
+ ): TeamAgentStatus {
2777
+ if (hookEvent === "PreToolUse") return "busy";
2778
+ if (
2779
+ hookEvent === "Stop" ||
2780
+ hookEvent === "TeammateIdle" ||
2781
+ hookEvent === "SubagentStop" ||
2782
+ hookEvent === "TaskCompleted"
2783
+ ) {
2784
+ return "idle";
2785
+ }
2786
+ if (hookEvent === "SessionStart" || hookEvent === "UserPromptSubmit") return "running";
2787
+ if (hookEvent === "PostToolUse" || hookEvent === "PostToolUseFailure") return "running";
2788
+ if (hookEvent === "SessionEnd") return "waiting-input";
2789
+ return fallback;
2170
2790
  }
2171
2791
 
2172
2792
  function createRunId(repoRoot: string, now: Date): string {
@@ -2215,6 +2835,34 @@ function sanitizeWindowName(value: string): string {
2215
2835
  return safeSlug(value).slice(0, 30) || "agent";
2216
2836
  }
2217
2837
 
2838
+ function formatTeamPaneTitle(role: ResolvedTeamRole): string {
2839
+ const label = role.roleName === role.roleId ? role.roleId : `${role.roleName} [${role.roleId}]`;
2840
+ const title = normalizeTmuxPaneTitle(label);
2841
+ return title.slice(0, 80) || role.roleId;
2842
+ }
2843
+
2844
+ function normalizeTmuxPaneTitle(value: string): string {
2845
+ let normalized = "";
2846
+ let pendingSpace = false;
2847
+
2848
+ for (const char of value) {
2849
+ const code = char.charCodeAt(0);
2850
+ const isControl = code < 32 || code === 127;
2851
+ if (isControl || char.trim() === "") {
2852
+ pendingSpace = normalized.length > 0;
2853
+ continue;
2854
+ }
2855
+
2856
+ if (pendingSpace) {
2857
+ normalized += " ";
2858
+ pendingSpace = false;
2859
+ }
2860
+ normalized += char;
2861
+ }
2862
+
2863
+ return normalized;
2864
+ }
2865
+
2218
2866
  function defaultRoleName(roleId: string): string {
2219
2867
  return roleId
2220
2868
  .split(/[-_.]/)
@@ -2352,6 +3000,16 @@ function isNotFoundError(error: unknown): boolean {
2352
3000
  );
2353
3001
  }
2354
3002
 
3003
+ async function pathExists(path: string): Promise<boolean> {
3004
+ try {
3005
+ await stat(path);
3006
+ return true;
3007
+ } catch (error) {
3008
+ if (isNotFoundError(error)) return false;
3009
+ throw error;
3010
+ }
3011
+ }
3012
+
2355
3013
  async function writeJson(path: string, value: unknown): Promise<void> {
2356
3014
  await mkdir(dirname(path), { recursive: true });
2357
3015
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
@@ -2362,6 +3020,19 @@ async function appendJsonLine(path: string, value: unknown): Promise<void> {
2362
3020
  await appendFile(path, `${JSON.stringify(value)}\n`, "utf8");
2363
3021
  }
2364
3022
 
3023
+ async function readJsonLines<T>(path: string, parse: (value: unknown) => T): Promise<T[]> {
3024
+ try {
3025
+ const text = await readFile(path, "utf8");
3026
+ return text
3027
+ .split("\n")
3028
+ .filter((line) => line.trim() !== "")
3029
+ .map((line) => parse(JSON.parse(line)));
3030
+ } catch (error) {
3031
+ if (isNotFoundError(error)) return [];
3032
+ throw error;
3033
+ }
3034
+ }
3035
+
2365
3036
  function shellQuote(value: string): string {
2366
3037
  return `'${value.replace(/'/g, `'\\''`)}'`;
2367
3038
  }