@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/assets/skills/coding/knowledge-distillation/SKILL.md +112 -111
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
- package/dist/config/index.js +757 -34
- package/dist/index.js +6862 -1535
- package/package.json +5 -1
- package/src/agents/index.ts +56 -0
- package/src/code-agent-traces/index.ts +521 -0
- package/src/config/index.ts +3 -0
- package/src/config/paths.ts +1 -1
- package/src/config/settings.ts +78 -0
- package/src/config/store.ts +2 -0
- package/src/daemon/index.ts +98 -9
- package/src/evolution/index.ts +494 -23
- package/src/hooks/index.ts +315 -36
- package/src/index.ts +2 -0
- package/src/knowledge/index.ts +4784 -0
- package/src/runtime-logs/index.ts +490 -16
- package/src/team/index.ts +847 -176
- package/src/team/mcp.ts +9 -5
- package/src/team/prompts.ts +141 -0
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1272
|
+
if (target.status === "failed" || target.status === "unknown") {
|
|
860
1273
|
return {
|
|
861
1274
|
ok: false,
|
|
862
|
-
error: "target-role-
|
|
863
|
-
message: `Role ${input.toRoleId} is not
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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:
|
|
1187
|
-
agents:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1655
|
-
await this.runTmux(["
|
|
1656
|
-
|
|
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
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
}
|