@evo-dev/core 0.0.1-alpha.2 → 0.0.1-alpha.4
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/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 +181 -125
- package/dist/index.js +10971 -9455
- package/package.json +1 -1
- package/src/agents/index.ts +1 -1
- package/src/code-agent-traces/index.ts +3 -10
- package/src/config/settings.ts +14 -0
- package/src/config/store.ts +1 -1
- package/src/daemon/index.ts +1 -1
- package/src/evolution/candidates/index.ts +235 -0
- package/src/evolution/control/index.ts +19 -0
- package/src/evolution/evidence/analysis.ts +529 -0
- package/src/evolution/evidence/index.ts +3 -0
- package/src/evolution/evidence/session-memory/constants.ts +12 -0
- package/src/evolution/evidence/session-memory/index.ts +4 -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 +192 -0
- package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
- package/src/evolution/evidence/session-memory/storage.ts +145 -0
- package/src/evolution/evidence/session-memory/types.ts +207 -0
- package/src/evolution/evidence/session-memory/updater.ts +184 -0
- package/src/evolution/formatters.ts +169 -0
- package/src/evolution/index.ts +12 -2827
- package/src/{knowledge → evolution/knowledge}/index.ts +7 -7
- package/src/evolution/paths.ts +41 -0
- package/src/evolution/processor/distillation.ts +436 -0
- package/src/evolution/processor/index.ts +3 -0
- package/src/evolution/processor/process.ts +306 -0
- package/src/evolution/schema.ts +522 -0
- package/src/evolution/shared.ts +677 -0
- package/src/evolution/triggers/classification.ts +102 -0
- package/src/evolution/triggers/index.ts +295 -0
- package/src/hooks/index.ts +56 -9
- package/src/index.ts +10 -2
- package/src/runtime-logs/index.ts +4 -12
- package/src/team/index.ts +576 -3
- 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/{learning → evolution/review}/index.ts +0 -0
package/src/team/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { appendFile, cp, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { resolveEvoDevPaths } from "../config/paths.ts";
|
|
5
5
|
import {
|
|
6
6
|
createDefaultSettings,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
formatScopedKnowledgePromptBlock,
|
|
13
13
|
hasContextInjectionReceipt,
|
|
14
14
|
writeContextInjectionReceipt,
|
|
15
|
-
} from "../knowledge/index.ts";
|
|
15
|
+
} from "../evolution/knowledge/index.ts";
|
|
16
16
|
import { resolveProjectLogKey } from "../runtime-logs/index.ts";
|
|
17
17
|
import { renderTeamRoleStartupPrompt } from "./prompts.ts";
|
|
18
18
|
|
|
@@ -161,7 +161,7 @@ export interface TeamRoleDefinition {
|
|
|
161
161
|
prompt: string;
|
|
162
162
|
permissions: TeamRolePermissions;
|
|
163
163
|
teamPolicy: TeamRolePolicy;
|
|
164
|
-
source: "builtin" | "global";
|
|
164
|
+
source: "builtin" | "global" | "overlay";
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
export interface ResolvedTeamRole extends TeamRoleDefinition {
|
|
@@ -178,6 +178,59 @@ export interface TeamNativeAgentBinding {
|
|
|
178
178
|
updatedAt: string;
|
|
179
179
|
}
|
|
180
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
|
+
|
|
181
234
|
export interface TeamRoleBindingRecord {
|
|
182
235
|
version: 1;
|
|
183
236
|
roleId: string;
|
|
@@ -654,6 +707,22 @@ const BUILT_IN_ROLE_PROMPTS: Record<
|
|
|
654
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.",
|
|
655
708
|
},
|
|
656
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
|
+
};
|
|
657
726
|
|
|
658
727
|
export function createTeamRunStore(homeDir?: string): TeamRunStore {
|
|
659
728
|
const paths = resolveTeamRunPaths(homeDir);
|
|
@@ -1908,6 +1977,216 @@ export async function listTeamAgents(input: TeamStatusInput = {}): Promise<TeamA
|
|
|
1908
1977
|
}));
|
|
1909
1978
|
}
|
|
1910
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
|
+
|
|
1911
2190
|
export async function resolveTeamRole(input: {
|
|
1912
2191
|
homeDir?: string;
|
|
1913
2192
|
repoRoot: string;
|
|
@@ -1920,6 +2199,37 @@ export async function resolveTeamRole(input: {
|
|
|
1920
2199
|
}): Promise<ResolvedTeamRole> {
|
|
1921
2200
|
assertSafeId(input.roleId, "roleId");
|
|
1922
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
|
+
|
|
1923
2233
|
const globalRolePath = join(
|
|
1924
2234
|
resolveEvoDevPaths(input.homeDir).roleAgentsDir,
|
|
1925
2235
|
`${input.roleId}.json`,
|
|
@@ -1942,12 +2252,24 @@ export async function resolveTeamRole(input: {
|
|
|
1942
2252
|
defaultThinkingLevel: settings.teamRuntime.defaultThinkingLevel,
|
|
1943
2253
|
recordTranscript: settings.teamRuntime.recordTranscript,
|
|
1944
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;
|
|
1945
2266
|
|
|
1946
2267
|
return {
|
|
1947
2268
|
...parsed,
|
|
1948
2269
|
runtime: input.overrides?.runtime ?? nativeAgent?.target ?? parsed.runtime,
|
|
1949
2270
|
model: input.overrides?.model ?? parsed.model,
|
|
1950
2271
|
thinkingLevel: input.overrides?.thinkingLevel ?? parsed.thinkingLevel,
|
|
2272
|
+
prompt,
|
|
1951
2273
|
sourcePath,
|
|
1952
2274
|
nativeAgent,
|
|
1953
2275
|
};
|
|
@@ -2614,6 +2936,239 @@ function createBuiltInRole(roleId: string, runtime: TeamAgentRuntime): unknown {
|
|
|
2614
2936
|
};
|
|
2615
2937
|
}
|
|
2616
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
|
+
|
|
2617
3172
|
function parseRolePermissions(value: unknown, main: boolean): TeamRolePermissions {
|
|
2618
3173
|
const input = isRecord(value) ? value : {};
|
|
2619
3174
|
return {
|
|
@@ -2742,6 +3297,12 @@ function parseRuntime(value: unknown, fallback: TeamAgentRuntime): TeamAgentRunt
|
|
|
2742
3297
|
throw new Error("Role runtime must be codex or claude.");
|
|
2743
3298
|
}
|
|
2744
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
|
+
|
|
2745
3306
|
function parseWriteMode(value: unknown, fallback: TeamWriteMode): TeamWriteMode {
|
|
2746
3307
|
if (value === undefined || value === null) return fallback;
|
|
2747
3308
|
if (["read-only", "repo-write", "worktree-write", "disabled"].includes(String(value))) {
|
|
@@ -2750,6 +3311,18 @@ function parseWriteMode(value: unknown, fallback: TeamWriteMode): TeamWriteMode
|
|
|
2750
3311
|
throw new Error("Role writeMode is invalid.");
|
|
2751
3312
|
}
|
|
2752
3313
|
|
|
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
|
+
|
|
2753
3326
|
export function isActiveTeamAgentStatus(status: TeamAgentStatus): boolean {
|
|
2754
3327
|
return (
|
|
2755
3328
|
status === "starting" ||
|
|
@@ -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
|
+
}
|