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