@evo-dev/core 0.0.1-alpha → 0.0.1-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/review/code-reviewer/examples.md +1 -1
- package/assets/agents/review/code-reviewer/prompt.md +1 -1
- package/assets/agents/review/code-reviewer/verification.md +1 -1
- package/assets/skills/coding/knowledge-distillation/SKILL.md +251 -0
- package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
- 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/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
- package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
- package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
- package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
- package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
- package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
- package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
- package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
- package/dist/config/index.js +1115 -81
- package/dist/index.js +13796 -2196
- package/dist/plugins/index.js +32 -32
- package/package.json +5 -1
- package/src/agents/index.ts +63 -292
- package/src/code-agent-traces/index.ts +520 -0
- package/src/config/index.ts +7 -0
- package/src/config/paths.ts +30 -0
- package/src/config/settings.ts +201 -0
- package/src/config/store.ts +152 -0
- package/src/daemon/index.ts +462 -40
- 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 -0
- 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 +652 -376
- package/src/index.ts +16 -3
- package/src/pack/index.ts +13 -13
- package/src/plugins/capabilities.ts +40 -42
- package/src/plugins/index.ts +0 -1
- package/src/plugins/types.ts +4 -0
- package/src/projects/index.ts +453 -0
- package/src/protected-zones/index.ts +29 -11
- package/src/runtime-logs/index.ts +790 -0
- package/src/sync/orchestrator.ts +6 -0
- package/src/team/index.ts +3642 -0
- package/src/team/mcp.ts +405 -0
- 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 +6 -24
- package/src/project/index.ts +0 -507
- package/src/task/index.ts +0 -840
package/src/team/mcp.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import type { Readable, Writable } from "node:stream";
|
|
3
|
+
import { TeamMessageBroker, type TeamMessageBrokerOptions, type TeamMessageType } from "./index.ts";
|
|
4
|
+
|
|
5
|
+
export interface TeamsMcpServerOptions extends TeamMessageBrokerOptions {
|
|
6
|
+
defaultRunId?: string;
|
|
7
|
+
defaultFromRoleId?: string;
|
|
8
|
+
now?: Date;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type JsonRpcId = string | number;
|
|
12
|
+
|
|
13
|
+
export interface JsonRpcResponse {
|
|
14
|
+
jsonrpc: "2.0";
|
|
15
|
+
id: JsonRpcId | null;
|
|
16
|
+
result?: Record<string, unknown>;
|
|
17
|
+
error?: {
|
|
18
|
+
code: number;
|
|
19
|
+
message: string;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface McpToolDefinition {
|
|
25
|
+
name: string;
|
|
26
|
+
title: string;
|
|
27
|
+
description: string;
|
|
28
|
+
inputSchema: Record<string, unknown>;
|
|
29
|
+
outputSchema?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PROTOCOL_VERSION = "2025-11-25";
|
|
33
|
+
const SUPPORTED_PROTOCOL_VERSIONS = new Set([PROTOCOL_VERSION, "2025-06-18"]);
|
|
34
|
+
const MESSAGE_TYPES = new Set<TeamMessageType>(["request", "result", "issue", "notice"]);
|
|
35
|
+
|
|
36
|
+
export function getTeamsMcpTools(): McpToolDefinition[] {
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
name: "list_agents",
|
|
40
|
+
title: "List EvoDev Team Agents",
|
|
41
|
+
description:
|
|
42
|
+
"List role agents in the current EvoDev team run for one-time roster discovery. Do not use this as a progress polling loop.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
properties: {
|
|
47
|
+
runId: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Optional EvoDev team run id. Defaults to the latest run.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
outputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
properties: {
|
|
57
|
+
agents: { type: "array" },
|
|
58
|
+
},
|
|
59
|
+
required: ["agents"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "send_message",
|
|
64
|
+
title: "Send EvoDev Team Message",
|
|
65
|
+
description:
|
|
66
|
+
"Queue a structured message to another EvoDev role agent. Delivery happens through hook safe points; callers should not wait or poll for immediate progress.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
properties: {
|
|
71
|
+
runId: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Optional EvoDev team run id. Defaults to the latest run.",
|
|
74
|
+
},
|
|
75
|
+
toRoleId: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Target role id.",
|
|
78
|
+
},
|
|
79
|
+
message: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Message body to deliver.",
|
|
82
|
+
},
|
|
83
|
+
type: {
|
|
84
|
+
type: "string",
|
|
85
|
+
enum: ["request", "result", "issue", "notice"],
|
|
86
|
+
description: "Message type.",
|
|
87
|
+
},
|
|
88
|
+
fromRoleId: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Optional sender role id for audit context.",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: ["toRoleId", "message"],
|
|
94
|
+
},
|
|
95
|
+
outputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
additionalProperties: true,
|
|
98
|
+
properties: {
|
|
99
|
+
ok: { type: "boolean" },
|
|
100
|
+
messageId: { type: "string" },
|
|
101
|
+
delivery: { type: "string", enum: ["queued"] },
|
|
102
|
+
queuedFor: { type: "string" },
|
|
103
|
+
cc: { type: "array" },
|
|
104
|
+
},
|
|
105
|
+
required: ["ok"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "spawn_role",
|
|
110
|
+
title: "Spawn EvoDev Role Agent",
|
|
111
|
+
description:
|
|
112
|
+
"Create or reuse one EvoDev role agent. For main, spawn needed roles, send their task messages, then finish the current turn unless new input or hook-delivered messages require coordination.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
properties: {
|
|
117
|
+
runId: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Optional EvoDev team run id. Defaults to the current team run.",
|
|
120
|
+
},
|
|
121
|
+
roleId: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Role id to create or reuse.",
|
|
124
|
+
},
|
|
125
|
+
reason: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "Reason for lifecycle change.",
|
|
128
|
+
},
|
|
129
|
+
fromRoleId: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Optional sender role id for audit context.",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ["roleId"],
|
|
135
|
+
},
|
|
136
|
+
outputSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
additionalProperties: true,
|
|
139
|
+
properties: {
|
|
140
|
+
ok: { type: "boolean" },
|
|
141
|
+
created: { type: "boolean" },
|
|
142
|
+
agent: { type: "object" },
|
|
143
|
+
error: { type: "string" },
|
|
144
|
+
message: { type: "string" },
|
|
145
|
+
},
|
|
146
|
+
required: ["ok"],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "stop_role",
|
|
151
|
+
title: "Stop EvoDev Role Agent",
|
|
152
|
+
description: "Stop one EvoDev role agent.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: "object",
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
properties: {
|
|
157
|
+
runId: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Optional EvoDev team run id. Defaults to the current team run.",
|
|
160
|
+
},
|
|
161
|
+
roleId: {
|
|
162
|
+
type: "string",
|
|
163
|
+
description: "Role id to stop.",
|
|
164
|
+
},
|
|
165
|
+
reason: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: "Reason for lifecycle change.",
|
|
168
|
+
},
|
|
169
|
+
fromRoleId: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description: "Optional sender role id for audit context.",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
required: ["roleId"],
|
|
175
|
+
},
|
|
176
|
+
outputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
additionalProperties: true,
|
|
179
|
+
properties: {
|
|
180
|
+
ok: { type: "boolean" },
|
|
181
|
+
stopped: { type: "boolean" },
|
|
182
|
+
agent: { type: "object" },
|
|
183
|
+
error: { type: "string" },
|
|
184
|
+
message: { type: "string" },
|
|
185
|
+
},
|
|
186
|
+
required: ["ok"],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function handleTeamsMcpLine(
|
|
193
|
+
line: string,
|
|
194
|
+
options: TeamsMcpServerOptions = {},
|
|
195
|
+
): Promise<JsonRpcResponse | null> {
|
|
196
|
+
try {
|
|
197
|
+
return await handleTeamsMcpMessage(JSON.parse(line), options);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return jsonRpcError(null, -32700, "Parse error", describeError(error));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function handleTeamsMcpMessage(
|
|
204
|
+
message: unknown,
|
|
205
|
+
options: TeamsMcpServerOptions = {},
|
|
206
|
+
): Promise<JsonRpcResponse | null> {
|
|
207
|
+
const request = parseRequest(message);
|
|
208
|
+
if (request === null) return null;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
if (request.method === "initialize")
|
|
212
|
+
return jsonRpcResult(request.id, initializeResult(request));
|
|
213
|
+
if (request.method === "ping") return jsonRpcResult(request.id, {});
|
|
214
|
+
if (request.method === "tools/list") {
|
|
215
|
+
return jsonRpcResult(request.id, { tools: getTeamsMcpTools() });
|
|
216
|
+
}
|
|
217
|
+
if (request.method === "tools/call") {
|
|
218
|
+
return jsonRpcResult(request.id, await callTeamsTool(request.params, options));
|
|
219
|
+
}
|
|
220
|
+
return jsonRpcError(request.id, -32601, `Method not found: ${request.method}`);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return jsonRpcError(request.id, -32603, describeError(error));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function runTeamsMcpStdioServer(
|
|
227
|
+
options: TeamsMcpServerOptions & {
|
|
228
|
+
input?: Readable;
|
|
229
|
+
output?: Writable;
|
|
230
|
+
} = {},
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
const input = options.input ?? process.stdin;
|
|
233
|
+
const output = options.output ?? process.stdout;
|
|
234
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
235
|
+
|
|
236
|
+
for await (const line of lines) {
|
|
237
|
+
if (line.trim() === "") continue;
|
|
238
|
+
const response = await handleTeamsMcpLine(line, options);
|
|
239
|
+
if (response !== null) {
|
|
240
|
+
output.write(`${JSON.stringify(response)}\n`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function initializeResult(request: ParsedRequest): Record<string, unknown> {
|
|
246
|
+
const params = isRecord(request.params) ? request.params : {};
|
|
247
|
+
const requestedVersion =
|
|
248
|
+
typeof params.protocolVersion === "string" ? params.protocolVersion : PROTOCOL_VERSION;
|
|
249
|
+
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(requestedVersion)
|
|
250
|
+
? requestedVersion
|
|
251
|
+
: PROTOCOL_VERSION;
|
|
252
|
+
return {
|
|
253
|
+
protocolVersion,
|
|
254
|
+
capabilities: {
|
|
255
|
+
tools: {
|
|
256
|
+
listChanged: false,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
serverInfo: {
|
|
260
|
+
name: "evodev-teams",
|
|
261
|
+
version: "0.0.1-alpha",
|
|
262
|
+
},
|
|
263
|
+
instructions:
|
|
264
|
+
"Use list_agents only for one-time roster discovery, spawn_role to create or reuse role agents in the current EvoDev team run, send_message to queue brokered role messages, and stop_role to stop roles. send_message does not paste into live prompts; pending messages surface through hook safe points. Main should delegate runnable role tasks, then finish the current turn instead of sleeping, waiting, or polling list_agents/status for progress. Role lifecycle is tracked by EvoDev Teams MCP/CLI (`evodev team ...`), not direct tmux orchestration.",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function callTeamsTool(
|
|
269
|
+
params: unknown,
|
|
270
|
+
options: TeamsMcpServerOptions,
|
|
271
|
+
): Promise<Record<string, unknown>> {
|
|
272
|
+
const input = expectRecord(params, "tools/call params");
|
|
273
|
+
const name = expectString(input.name, "tools/call params.name");
|
|
274
|
+
const args = input.arguments === undefined ? {} : expectRecord(input.arguments, "arguments");
|
|
275
|
+
const broker = new TeamMessageBroker(options);
|
|
276
|
+
|
|
277
|
+
if (name === "list_agents") {
|
|
278
|
+
const agents = await broker.listAgents({
|
|
279
|
+
runId: optionalString(args.runId) ?? options.defaultRunId,
|
|
280
|
+
});
|
|
281
|
+
return toolResult({ agents });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (name === "send_message") {
|
|
285
|
+
const toRoleId = expectString(args.toRoleId, "arguments.toRoleId");
|
|
286
|
+
const message = expectString(args.message, "arguments.message");
|
|
287
|
+
const type = parseMessageType(args.type);
|
|
288
|
+
const fromRoleId = optionalString(args.fromRoleId) ?? options.defaultFromRoleId;
|
|
289
|
+
const result = await broker.send({
|
|
290
|
+
runId: optionalString(args.runId) ?? options.defaultRunId,
|
|
291
|
+
fromRoleId,
|
|
292
|
+
toRoleId,
|
|
293
|
+
message,
|
|
294
|
+
type,
|
|
295
|
+
now: options.now,
|
|
296
|
+
});
|
|
297
|
+
return result.ok ? toolResult({ ...result }) : toolResult({ ...result }, true);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (name === "spawn_role") {
|
|
301
|
+
const roleId = expectString(args.roleId, "arguments.roleId");
|
|
302
|
+
const fromRoleId = optionalString(args.fromRoleId) ?? options.defaultFromRoleId;
|
|
303
|
+
const result = await broker.spawnRole({
|
|
304
|
+
runId: optionalString(args.runId) ?? options.defaultRunId,
|
|
305
|
+
fromRoleId,
|
|
306
|
+
roleId,
|
|
307
|
+
reason: optionalString(args.reason),
|
|
308
|
+
now: options.now,
|
|
309
|
+
});
|
|
310
|
+
return result.ok ? toolResult({ ...result }) : toolResult({ ...result }, true);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (name === "stop_role") {
|
|
314
|
+
const roleId = expectString(args.roleId, "arguments.roleId");
|
|
315
|
+
const fromRoleId = optionalString(args.fromRoleId) ?? options.defaultFromRoleId;
|
|
316
|
+
const result = await broker.stopRole({
|
|
317
|
+
runId: optionalString(args.runId) ?? options.defaultRunId,
|
|
318
|
+
fromRoleId,
|
|
319
|
+
roleId,
|
|
320
|
+
reason: optionalString(args.reason),
|
|
321
|
+
now: options.now,
|
|
322
|
+
});
|
|
323
|
+
return result.ok ? toolResult({ ...result }) : toolResult({ ...result }, true);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
throw new Error(`Unknown Teams MCP tool: ${name}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function toolResult(
|
|
330
|
+
structuredContent: Record<string, unknown>,
|
|
331
|
+
isError = false,
|
|
332
|
+
): Record<string, unknown> {
|
|
333
|
+
return {
|
|
334
|
+
content: [
|
|
335
|
+
{
|
|
336
|
+
type: "text",
|
|
337
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
structuredContent,
|
|
341
|
+
isError,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface ParsedRequest {
|
|
346
|
+
id: JsonRpcId;
|
|
347
|
+
method: string;
|
|
348
|
+
params?: unknown;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function parseRequest(message: unknown): ParsedRequest | null {
|
|
352
|
+
if (!isRecord(message)) throw new Error("Invalid JSON-RPC message; expected object.");
|
|
353
|
+
if (message.jsonrpc !== "2.0") throw new Error("Invalid JSON-RPC version.");
|
|
354
|
+
if (message.id === undefined) return null;
|
|
355
|
+
if (typeof message.id !== "string" && typeof message.id !== "number") {
|
|
356
|
+
throw new Error("Invalid JSON-RPC id.");
|
|
357
|
+
}
|
|
358
|
+
const method = expectString(message.method, "method");
|
|
359
|
+
return { id: message.id, method, params: message.params };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function parseMessageType(value: unknown): TeamMessageType | undefined {
|
|
363
|
+
if (value === undefined) return undefined;
|
|
364
|
+
if (typeof value === "string" && MESSAGE_TYPES.has(value as TeamMessageType)) {
|
|
365
|
+
return value as TeamMessageType;
|
|
366
|
+
}
|
|
367
|
+
throw new Error("arguments.type must be request, result, issue, or notice.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function jsonRpcResult(id: JsonRpcId, result: Record<string, unknown>): JsonRpcResponse {
|
|
371
|
+
return { jsonrpc: "2.0", id, result };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function jsonRpcError(
|
|
375
|
+
id: JsonRpcId | null,
|
|
376
|
+
code: number,
|
|
377
|
+
message: string,
|
|
378
|
+
data?: unknown,
|
|
379
|
+
): JsonRpcResponse {
|
|
380
|
+
return { jsonrpc: "2.0", id, error: { code, message, data } };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function expectRecord(value: unknown, path: string): Record<string, unknown> {
|
|
384
|
+
if (!isRecord(value)) throw new Error(`Invalid ${path}; expected object.`);
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function expectString(value: unknown, path: string): string {
|
|
389
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
390
|
+
throw new Error(`Invalid ${path}; expected non-empty string.`);
|
|
391
|
+
}
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function optionalString(value: unknown): string | undefined {
|
|
396
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
400
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function describeError(error: unknown): string {
|
|
404
|
+
return error instanceof Error ? error.message : String(error);
|
|
405
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export interface TeamStartupPromptRole {
|
|
2
|
+
roleId: string;
|
|
3
|
+
roleName: string;
|
|
4
|
+
runtime: string;
|
|
5
|
+
model: string | null;
|
|
6
|
+
thinkingLevel: string | null;
|
|
7
|
+
prompt: string;
|
|
8
|
+
permissions: {
|
|
9
|
+
writeMode: string;
|
|
10
|
+
};
|
|
11
|
+
teamPolicy: {
|
|
12
|
+
recordTranscript: boolean;
|
|
13
|
+
};
|
|
14
|
+
nativeAgent: {
|
|
15
|
+
target: string;
|
|
16
|
+
agentName: string;
|
|
17
|
+
scope: string;
|
|
18
|
+
} | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TeamStartupPromptRosterAgent {
|
|
22
|
+
roleId: string;
|
|
23
|
+
status: string;
|
|
24
|
+
roleName: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RenderTeamRoleStartupPromptInput {
|
|
28
|
+
runId: string;
|
|
29
|
+
repoRoot: string;
|
|
30
|
+
role: TeamStartupPromptRole;
|
|
31
|
+
roster: TeamStartupPromptRosterAgent[];
|
|
32
|
+
scopedContext?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const TEAM_ROLE_STARTUP_PROMPT_TEMPLATE = [
|
|
36
|
+
"You are an EvoDev managed role agent.",
|
|
37
|
+
"Team run: {{runId}}",
|
|
38
|
+
"Repository: {{repoRoot}}",
|
|
39
|
+
"Role id: {{roleId}}",
|
|
40
|
+
"Role name: {{roleName}}",
|
|
41
|
+
"Runtime: {{runtime}}",
|
|
42
|
+
"Native Code Agent binding: {{nativeAgentBinding}}",
|
|
43
|
+
"Model: {{model}}",
|
|
44
|
+
"Thinking level: {{thinkingLevel}}",
|
|
45
|
+
"Write mode: {{writeMode}}",
|
|
46
|
+
"Transcript recording: {{transcriptRecording}}",
|
|
47
|
+
"",
|
|
48
|
+
"Current known agents:",
|
|
49
|
+
"{{roster}}",
|
|
50
|
+
"",
|
|
51
|
+
"EvoDev team runtime control contract:",
|
|
52
|
+
"This run is already inside the EvoDev-managed team runtime.",
|
|
53
|
+
"For EvoDev role-agent lifecycle, use the current EvoDev run control plane.",
|
|
54
|
+
"Unprefixed user requests such as 'start the team', 'execute team', 'create agents', or 'spawn roles' mean: use the current EvoDev run control plane.",
|
|
55
|
+
"Do not answer those requests with only a role plan when a role agent should be created.",
|
|
56
|
+
"",
|
|
57
|
+
"Use Teams MCP as the primary control plane:",
|
|
58
|
+
"- list_agents: discover current EvoDev role agents.",
|
|
59
|
+
"- spawn_role: create or reuse exactly one EvoDev role agent.",
|
|
60
|
+
"- send_message: communicate through the EvoDev broker.",
|
|
61
|
+
"- stop_role: stop a role agent when allowed.",
|
|
62
|
+
"If Teams MCP is unavailable, fall back to the EvoDev CLI from this repository:",
|
|
63
|
+
"- evodev team spawn --role <roleId>",
|
|
64
|
+
"- evodev team send --to <roleId> --message <text>",
|
|
65
|
+
"- evodev team status",
|
|
66
|
+
"Teams MCP defaults are inherited from the environment:",
|
|
67
|
+
"EVODEV_TEAM_RUN_ID={{runId}}",
|
|
68
|
+
"EVODEV_TEAM_ROLE_ID={{roleId}}",
|
|
69
|
+
"When calling Teams MCP tools, let the MCP server-bound environment identify this run and role.",
|
|
70
|
+
"Do not operate tmux directly for role lifecycle; let EvoDev create and track panes.",
|
|
71
|
+
"Any server-bound role may request role lifecycle changes; EvoDev records and tracks panes but does not use role policy to stop execution.",
|
|
72
|
+
"{{roleGuidance}}",
|
|
73
|
+
"{{nativeAgentInstruction}}",
|
|
74
|
+
"Team messages are durably queued and delivered at hook safe points, not pasted into a live prompt. cc values are audit context; they are not an instruction for main to immediately forward or act.",
|
|
75
|
+
"{{scopedContext}}",
|
|
76
|
+
"",
|
|
77
|
+
"{{rolePrompt}}",
|
|
78
|
+
].join("\n");
|
|
79
|
+
|
|
80
|
+
const MAIN_ROLE_GUIDANCE = [
|
|
81
|
+
"As main, you are the planner and delegator for complex work, not a standby worker.",
|
|
82
|
+
"On each user prompt or hook-delivered inbox message, decide whether to answer directly, ask for clarification, or use team execution.",
|
|
83
|
+
"Use team execution only when role separation improves correctness, coverage, safety, or latency.",
|
|
84
|
+
"When team execution is needed, create an upfront task plan with required role ids, role-specific assignments, dependencies, and runnable batches.",
|
|
85
|
+
"Spawn or reuse all roles needed for the first runnable batch and send each role a self-contained task message.",
|
|
86
|
+
"After all currently runnable tasks are delegated, finish your current turn immediately; do not call sleep, wait idly, poll list_agents/status, or keep the turn alive to watch progress.",
|
|
87
|
+
"Use list_agents only for one-time roster discovery when the current roster is genuinely unknown, never as a progress check.",
|
|
88
|
+
"Resume coordination only when the user sends new input or a role message is delivered by hooks; then decide whether to adjust tasks, send supplemental instructions, spawn dependent roles, synthesize completed results, or ask the user.",
|
|
89
|
+
"While delegated role work is pending, do not perform concrete implementation, testing, package research, or review work yourself.",
|
|
90
|
+
].join(" ");
|
|
91
|
+
|
|
92
|
+
const NON_MAIN_ROLE_GUIDANCE =
|
|
93
|
+
"As a non-main role, send results or issues to the role that needs to act next. Coordinate with main only when the decision affects scope, assignment, user-facing synthesis, or unresolved tradeoffs.";
|
|
94
|
+
|
|
95
|
+
const PLACEHOLDER_PATTERN = /\{\{([A-Za-z][A-Za-z0-9]*)\}\}/g;
|
|
96
|
+
|
|
97
|
+
export function renderTeamRoleStartupPrompt(input: RenderTeamRoleStartupPromptInput): string {
|
|
98
|
+
const values: Record<string, string> = {
|
|
99
|
+
runId: input.runId,
|
|
100
|
+
repoRoot: input.repoRoot,
|
|
101
|
+
roleId: input.role.roleId,
|
|
102
|
+
roleName: input.role.roleName,
|
|
103
|
+
runtime: input.role.runtime,
|
|
104
|
+
nativeAgentBinding: formatNativeAgentBinding(input.role),
|
|
105
|
+
model: input.role.model ?? "default",
|
|
106
|
+
thinkingLevel: input.role.thinkingLevel ?? "default",
|
|
107
|
+
writeMode: input.role.permissions.writeMode,
|
|
108
|
+
transcriptRecording: input.role.teamPolicy.recordTranscript ? "enabled" : "disabled",
|
|
109
|
+
roster: formatStartupPromptRoster(input.roster),
|
|
110
|
+
roleGuidance: input.role.roleId === "main" ? MAIN_ROLE_GUIDANCE : NON_MAIN_ROLE_GUIDANCE,
|
|
111
|
+
nativeAgentInstruction: formatNativeAgentInstruction(input.role),
|
|
112
|
+
scopedContext: input.scopedContext ?? "",
|
|
113
|
+
rolePrompt: input.role.prompt,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return renderPromptTemplate(TEAM_ROLE_STARTUP_PROMPT_TEMPLATE, values);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderPromptTemplate(template: string, values: Record<string, string>): string {
|
|
120
|
+
return template.replace(PLACEHOLDER_PATTERN, (_placeholder, key: string) => {
|
|
121
|
+
if (!Object.hasOwn(values, key)) {
|
|
122
|
+
throw new Error(`Missing team startup prompt template value: ${key}`);
|
|
123
|
+
}
|
|
124
|
+
return values[key];
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatStartupPromptRoster(roster: TeamStartupPromptRosterAgent[]): string {
|
|
129
|
+
if (roster.length === 0) return "- main: starting";
|
|
130
|
+
return roster.map((agent) => `- ${agent.roleId}: ${agent.status} (${agent.roleName})`).join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatNativeAgentBinding(role: TeamStartupPromptRole): string {
|
|
134
|
+
if (role.nativeAgent === null) return "none";
|
|
135
|
+
return `${role.nativeAgent.target}/${role.nativeAgent.agentName} (${role.nativeAgent.scope})`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatNativeAgentInstruction(role: TeamStartupPromptRole): string {
|
|
139
|
+
if (role.nativeAgent === null) return "No native Code Agent agent is bound to this role.";
|
|
140
|
+
return `Use the bound native Code Agent agent name '${role.nativeAgent.agentName}' as role context when the runtime supports named agents; EvoDev does not load or transform the native agent file.`;
|
|
141
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function isNotFoundError(error: unknown): boolean {
|
|
2
|
+
return (
|
|
3
|
+
error instanceof Error &&
|
|
4
|
+
(("code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") ||
|
|
5
|
+
error.message.includes("ENOENT"))
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isFileExistsError(error: unknown): boolean {
|
|
10
|
+
return (
|
|
11
|
+
error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EEXIST"
|
|
12
|
+
);
|
|
13
|
+
}
|
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { isNotFoundError } from "./errors.ts";
|
|
4
|
+
|
|
5
|
+
export async function pathExists(path: string): Promise<boolean> {
|
|
6
|
+
try {
|
|
7
|
+
await stat(path);
|
|
8
|
+
return true;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (isNotFoundError(error)) return false;
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function readJsonFile<T>(path: string, parse: (value: unknown) => T): Promise<T> {
|
|
16
|
+
return parse(JSON.parse(await readFile(path, "utf8")) as unknown);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readJsonFiles<T>(dir: string, parse: (value: unknown) => T): Promise<T[]> {
|
|
20
|
+
if (!(await pathExists(dir))) return [];
|
|
21
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
22
|
+
const values: T[] = [];
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
25
|
+
values.push(await readJsonFile(join(dir, entry.name), parse));
|
|
26
|
+
}
|
|
27
|
+
return values;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeJsonFile(
|
|
31
|
+
path: string,
|
|
32
|
+
value: unknown,
|
|
33
|
+
options: { overwrite?: boolean } = {},
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
await mkdir(dirname(path), { recursive: true });
|
|
36
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
flag: options.overwrite === false ? "wx" : "w",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function sha256Hex(value: string): string {
|
|
4
|
+
return createHash("sha256").update(value).digest("hex");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sha256Short(value: string, length = 16): string {
|
|
8
|
+
return sha256Hex(value).slice(0, length);
|
|
9
|
+
}
|
package/src/utils/ids.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { sha256Short } from "./hash.ts";
|
|
2
|
+
|
|
3
|
+
export function createStableId(prefix: string, parts: string[]): string {
|
|
4
|
+
return `${prefix}-${sha256Short(parts.join("\0"))}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sanitizeStorageId(value: string, fallbackPrefix: string): string {
|
|
8
|
+
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120);
|
|
9
|
+
return sanitized === "" || sanitized === "." || sanitized === ".."
|
|
10
|
+
? `${fallbackPrefix}-local`
|
|
11
|
+
: sanitized;
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function optionalString(value: unknown): string | null {
|
|
2
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function optionalBoolean(value: unknown, fallback: boolean): boolean {
|
|
6
|
+
return typeof value === "boolean" ? value : fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function positiveInteger(value: unknown, fallback: number): number {
|
|
10
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function sanitizeSummary(value: string): string {
|
|
2
|
+
const trimmed = value.replace(/\s+/g, " ").trim();
|
|
3
|
+
return trimmed.length === 0 ? "Session memory event observed." : trimmed.slice(0, 600);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function truncateUtf8Tail(
|
|
7
|
+
value: string,
|
|
8
|
+
maxBytes: number,
|
|
9
|
+
): { content: string; truncated: boolean } {
|
|
10
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) {
|
|
11
|
+
return { content: value, truncated: false };
|
|
12
|
+
}
|
|
13
|
+
let content = value.slice(Math.max(0, value.length - maxBytes));
|
|
14
|
+
while (Buffer.byteLength(content, "utf8") > maxBytes && content.length > 0) {
|
|
15
|
+
content = content.slice(1);
|
|
16
|
+
}
|
|
17
|
+
return { content, truncated: true };
|
|
18
|
+
}
|