@evo-dev/core 0.0.1-alpha
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 +19 -0
- package/assets/agents/review/code-reviewer/manifest.json +10 -0
- package/assets/agents/review/code-reviewer/prompt.md +59 -0
- package/assets/agents/review/code-reviewer/verification.md +11 -0
- package/assets/skills/coding/engineering-discipline/SKILL.md +63 -0
- package/assets/skills/coding/engineering-discipline/anti-patterns.md +21 -0
- package/assets/skills/coding/engineering-discipline/examples.md +19 -0
- package/assets/skills/coding/engineering-discipline/manifest.json +10 -0
- package/assets/skills/coding/engineering-discipline/verification.md +11 -0
- package/assets/workflows/rd-bug-fix/WORKFLOW.json +45 -0
- package/assets/workflows/rd-code-review/WORKFLOW.json +45 -0
- package/assets/workflows/rd-docs-update/WORKFLOW.json +45 -0
- package/assets/workflows/rd-feature-implementation/WORKFLOW.json +45 -0
- package/assets/workflows/rd-refactor/WORKFLOW.json +45 -0
- package/assets/workflows/rd-release-readiness/WORKFLOW.json +49 -0
- package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +45 -0
- package/assets/workflows/rd-test-generation/WORKFLOW.json +45 -0
- package/dist/assets/index.js +209 -0
- package/dist/config/index.js +601 -0
- package/dist/index.js +4879 -0
- package/dist/plugins/index.js +265 -0
- package/package.json +30 -0
- package/src/.gitkeep +0 -0
- package/src/agents/index.ts +561 -0
- package/src/assets/errors.ts +21 -0
- package/src/assets/index.ts +18 -0
- package/src/assets/manifest.ts +109 -0
- package/src/assets/scanner.ts +189 -0
- package/src/config/errors.ts +21 -0
- package/src/config/index.ts +26 -0
- package/src/config/paths.ts +43 -0
- package/src/config/registry.ts +84 -0
- package/src/config/settings.ts +212 -0
- package/src/config/state.ts +130 -0
- package/src/config/store.ts +166 -0
- package/src/daemon/index.ts +414 -0
- package/src/hooks/index.ts +1023 -0
- package/src/index.ts +14 -0
- package/src/learning/index.ts +714 -0
- package/src/observability/index.ts +272 -0
- package/src/pack/index.ts +779 -0
- package/src/plugins/capabilities.ts +347 -0
- package/src/plugins/index.ts +41 -0
- package/src/plugins/registry.ts +60 -0
- package/src/plugins/types.ts +123 -0
- package/src/project/index.ts +507 -0
- package/src/protected-zones/index.ts +137 -0
- package/src/sync/index.ts +7 -0
- package/src/sync/orchestrator.ts +298 -0
- package/src/task/index.ts +840 -0
- package/src/workflow/index.ts +137 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import type { TaskContract, TaskExecutionMode } from "../task/index.ts";
|
|
3
|
+
|
|
4
|
+
export type AgentPersistence = "named" | "dynamic";
|
|
5
|
+
export type AgentLens =
|
|
6
|
+
| "review"
|
|
7
|
+
| "qa"
|
|
8
|
+
| "security"
|
|
9
|
+
| "release"
|
|
10
|
+
| "architecture"
|
|
11
|
+
| "migration"
|
|
12
|
+
| "test";
|
|
13
|
+
export type AgentOutputSchemaId =
|
|
14
|
+
| "review-findings-v1"
|
|
15
|
+
| "verification-summary-v1"
|
|
16
|
+
| "design-options-v1";
|
|
17
|
+
export type AgentFindingSeverity = "blocker" | "high" | "medium" | "low" | "info";
|
|
18
|
+
|
|
19
|
+
export interface AgentPermissions {
|
|
20
|
+
canReadMetadata: boolean;
|
|
21
|
+
canReadSourceContent: boolean;
|
|
22
|
+
canWriteFiles: boolean;
|
|
23
|
+
canRunCommands: boolean;
|
|
24
|
+
canUseNetwork: boolean;
|
|
25
|
+
canSpawnAgents: boolean;
|
|
26
|
+
canWriteMemory: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AgentProfile {
|
|
30
|
+
version: 1;
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
persistence: AgentPersistence;
|
|
34
|
+
description: string;
|
|
35
|
+
traits: {
|
|
36
|
+
expertise: string[];
|
|
37
|
+
stance: string[];
|
|
38
|
+
approach: string[];
|
|
39
|
+
domain: string[];
|
|
40
|
+
};
|
|
41
|
+
inputs: {
|
|
42
|
+
required: string[];
|
|
43
|
+
optional: string[];
|
|
44
|
+
forbidden: string[];
|
|
45
|
+
};
|
|
46
|
+
permissions: AgentPermissions;
|
|
47
|
+
output: {
|
|
48
|
+
schema: AgentOutputSchemaId;
|
|
49
|
+
requiredFields: string[];
|
|
50
|
+
};
|
|
51
|
+
privacy: {
|
|
52
|
+
classification: "local-private";
|
|
53
|
+
metadataOnly: true;
|
|
54
|
+
rawPromptStored: false;
|
|
55
|
+
sourceContentStored: false;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AgentPlannedInvocation {
|
|
60
|
+
profile: AgentProfile;
|
|
61
|
+
lens: AgentLens;
|
|
62
|
+
reason: string;
|
|
63
|
+
plannedOnly: true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AgentMergePlan {
|
|
67
|
+
strategy: "none" | "single-output" | "dedupe-preserve-conflicts-fail-closed";
|
|
68
|
+
outputSchema: AgentOutputSchemaId | null;
|
|
69
|
+
dedupeBy: string[];
|
|
70
|
+
conflictPolicy: string[];
|
|
71
|
+
failClosedCategories: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AgentComposeDryRunPlan {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
taskId: string;
|
|
77
|
+
mode: TaskExecutionMode | null;
|
|
78
|
+
workflowId: string | null;
|
|
79
|
+
agents: AgentPlannedInvocation[];
|
|
80
|
+
permissions: AgentPermissions;
|
|
81
|
+
mergePlan: AgentMergePlan;
|
|
82
|
+
warnings: string[];
|
|
83
|
+
blockers: string[];
|
|
84
|
+
rationale: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface AgentContextDryRunBundle {
|
|
88
|
+
profile: AgentProfile;
|
|
89
|
+
context: {
|
|
90
|
+
metadataOnly: true;
|
|
91
|
+
allowedSections: string[];
|
|
92
|
+
forbiddenSections: string[];
|
|
93
|
+
provenance: string[];
|
|
94
|
+
};
|
|
95
|
+
warnings: string[];
|
|
96
|
+
blockers: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ReviewFindingOutput {
|
|
100
|
+
summary: string;
|
|
101
|
+
findings: Array<{
|
|
102
|
+
severity: AgentFindingSeverity;
|
|
103
|
+
category: string;
|
|
104
|
+
path?: string;
|
|
105
|
+
line?: number;
|
|
106
|
+
title: string;
|
|
107
|
+
rationale: string;
|
|
108
|
+
recommendation: string;
|
|
109
|
+
evidenceRefs: string[];
|
|
110
|
+
confidence: "low" | "medium" | "high";
|
|
111
|
+
}>;
|
|
112
|
+
confidence: "low" | "medium" | "high";
|
|
113
|
+
evidenceRefs: string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const DEFAULT_AGENT_PERMISSIONS: AgentPermissions = {
|
|
117
|
+
canReadMetadata: true,
|
|
118
|
+
canReadSourceContent: false,
|
|
119
|
+
canWriteFiles: false,
|
|
120
|
+
canRunCommands: false,
|
|
121
|
+
canUseNetwork: false,
|
|
122
|
+
canSpawnAgents: false,
|
|
123
|
+
canWriteMemory: false,
|
|
124
|
+
};
|
|
125
|
+
const FORBIDDEN_INPUTS = ["rawPrompts", "secrets", "rawCommandLogs", "sourceCorpus"];
|
|
126
|
+
const SAFE_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
127
|
+
const PRIVACY_BLOCKER_PATTERN =
|
|
128
|
+
/raw prompt|rawprompt|source corpus|sourcecorpus|secret|\.env|raw command|raw log|internal url|private url|network|write files|run commands|spawn agents|write memory/i;
|
|
129
|
+
const LENS_TO_EXPERTISE: Record<AgentLens, string[]> = {
|
|
130
|
+
review: ["code-review"],
|
|
131
|
+
qa: ["qa", "verification"],
|
|
132
|
+
security: ["security", "privacy"],
|
|
133
|
+
release: ["release", "packaging"],
|
|
134
|
+
architecture: ["architecture"],
|
|
135
|
+
migration: ["migration", "impact-analysis"],
|
|
136
|
+
test: ["testing"],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export function createDefaultAgentPermissions(): AgentPermissions {
|
|
140
|
+
return { ...DEFAULT_AGENT_PERMISSIONS };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function applyStrictestAgentPermissions(
|
|
144
|
+
...permissions: Array<Partial<AgentPermissions> | undefined>
|
|
145
|
+
): AgentPermissions {
|
|
146
|
+
const merged = createDefaultAgentPermissions();
|
|
147
|
+
for (const candidate of permissions) {
|
|
148
|
+
if (candidate === undefined) continue;
|
|
149
|
+
for (const key of Object.keys(merged) as Array<keyof AgentPermissions>) {
|
|
150
|
+
merged[key] = merged[key] && candidate[key] === true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return merged;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function parseAgentProfile(value: unknown): AgentProfile {
|
|
157
|
+
if (!isRecord(value)) throw new Error("Agent profile must be an object.");
|
|
158
|
+
if (value.version !== 1) throw new Error("Agent profile version must be 1.");
|
|
159
|
+
const profile = value as unknown as AgentProfile;
|
|
160
|
+
assertString(profile.id, "profile.id");
|
|
161
|
+
assertString(profile.name, "profile.name");
|
|
162
|
+
if (profile.persistence !== "named" && profile.persistence !== "dynamic") {
|
|
163
|
+
throw new Error("Agent profile persistence must be named or dynamic.");
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
profile.privacy?.metadataOnly !== true ||
|
|
167
|
+
profile.privacy.rawPromptStored !== false ||
|
|
168
|
+
profile.privacy.sourceContentStored !== false
|
|
169
|
+
) {
|
|
170
|
+
throw new Error("Agent profile must be metadata-only and must not store raw prompts/source.");
|
|
171
|
+
}
|
|
172
|
+
const permissions = normalizeAgentPermissions(profile.permissions);
|
|
173
|
+
if (
|
|
174
|
+
permissions.canWriteFiles ||
|
|
175
|
+
permissions.canRunCommands ||
|
|
176
|
+
permissions.canUseNetwork ||
|
|
177
|
+
permissions.canSpawnAgents ||
|
|
178
|
+
permissions.canWriteMemory ||
|
|
179
|
+
permissions.canReadSourceContent
|
|
180
|
+
) {
|
|
181
|
+
throw new Error("Agent profile requests permissions outside I8 dry-run boundaries.");
|
|
182
|
+
}
|
|
183
|
+
if (!SAFE_AGENT_ID_PATTERN.test(profile.id)) throw new Error("Agent profile id is unsafe.");
|
|
184
|
+
assertStringArray(profile.traits?.expertise, "profile.traits.expertise");
|
|
185
|
+
assertStringArray(profile.traits?.stance, "profile.traits.stance");
|
|
186
|
+
assertStringArray(profile.traits?.approach, "profile.traits.approach");
|
|
187
|
+
assertStringArray(profile.traits?.domain, "profile.traits.domain");
|
|
188
|
+
assertStringArray(profile.inputs?.required, "profile.inputs.required");
|
|
189
|
+
assertStringArray(profile.inputs?.optional, "profile.inputs.optional");
|
|
190
|
+
assertStringArray(profile.inputs?.forbidden, "profile.inputs.forbidden");
|
|
191
|
+
if (!FORBIDDEN_INPUTS.every((item) => profile.inputs.forbidden.includes(item))) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
"Agent profile must forbid raw prompts, secrets, raw command logs, and source corpus.",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (!isKnownOutputSchema(profile.output?.schema))
|
|
197
|
+
throw new Error("Agent profile output schema is unsupported.");
|
|
198
|
+
assertStringArray(profile.output.requiredFields, "profile.output.requiredFields");
|
|
199
|
+
return { ...profile, permissions };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function readAgentProfile(path: string): Promise<AgentProfile> {
|
|
203
|
+
return parseAgentProfile(JSON.parse(await readFile(path, "utf8")));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function composeAgentDryRun(input: {
|
|
207
|
+
contract: TaskContract;
|
|
208
|
+
workflowId?: string;
|
|
209
|
+
lenses?: AgentLens[];
|
|
210
|
+
}): AgentComposeDryRunPlan {
|
|
211
|
+
const mode = input.contract.route.mode;
|
|
212
|
+
const workflowId = input.workflowId ?? input.contract.route.workflowId;
|
|
213
|
+
const warnings: string[] = [];
|
|
214
|
+
const blockers: string[] = [];
|
|
215
|
+
const permissions = createDefaultAgentPermissions();
|
|
216
|
+
const requestedLenses = dedupeLenses(input.lenses ?? []);
|
|
217
|
+
const privacyBlockers = collectPrivacyBoundaryBlockers(
|
|
218
|
+
input.contract,
|
|
219
|
+
requestedLenses,
|
|
220
|
+
workflowId,
|
|
221
|
+
);
|
|
222
|
+
blockers.push(...privacyBlockers);
|
|
223
|
+
|
|
224
|
+
if (mode === "minimal") {
|
|
225
|
+
return {
|
|
226
|
+
ok: blockers.length === 0,
|
|
227
|
+
taskId: input.contract.taskId,
|
|
228
|
+
mode,
|
|
229
|
+
workflowId,
|
|
230
|
+
agents: [],
|
|
231
|
+
permissions,
|
|
232
|
+
mergePlan: createMergePlan([]),
|
|
233
|
+
warnings,
|
|
234
|
+
blockers,
|
|
235
|
+
rationale: "Minimal mode does not force dynamic agents by default.",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const selectedLenses = selectLenses(
|
|
240
|
+
mode,
|
|
241
|
+
requestedLenses,
|
|
242
|
+
workflowId,
|
|
243
|
+
input.contract.route.requiredReview,
|
|
244
|
+
);
|
|
245
|
+
const agents = selectedLenses.map((lens) => ({
|
|
246
|
+
profile: createDynamicProfile(lens, input.contract, permissions),
|
|
247
|
+
lens,
|
|
248
|
+
reason: createLensReason(lens, mode, workflowId),
|
|
249
|
+
plannedOnly: true as const,
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
if (mode === "standard" && agents.length > 1) {
|
|
253
|
+
blockers.push("Standard mode allows at most one optional reviewer/triager in I8 dry-run.");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
ok: blockers.length === 0,
|
|
258
|
+
taskId: input.contract.taskId,
|
|
259
|
+
mode,
|
|
260
|
+
workflowId,
|
|
261
|
+
agents,
|
|
262
|
+
permissions,
|
|
263
|
+
mergePlan: createMergePlan(agents),
|
|
264
|
+
warnings,
|
|
265
|
+
blockers,
|
|
266
|
+
rationale:
|
|
267
|
+
agents.length === 0
|
|
268
|
+
? "No dynamic agents selected; deterministic verification may be sufficient."
|
|
269
|
+
: `Selected ${agents.length} planned-only agent(s) based on mode/workflow/lenses.`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function loadAgentContextDryRun(profile: AgentProfile): AgentContextDryRunBundle {
|
|
274
|
+
const parsed = parseAgentProfile(profile);
|
|
275
|
+
return {
|
|
276
|
+
profile: parsed,
|
|
277
|
+
context: {
|
|
278
|
+
metadataOnly: true,
|
|
279
|
+
allowedSections: [
|
|
280
|
+
"role",
|
|
281
|
+
"inputs",
|
|
282
|
+
"permissions",
|
|
283
|
+
"output-schema",
|
|
284
|
+
"privacy",
|
|
285
|
+
"failure-behavior",
|
|
286
|
+
],
|
|
287
|
+
forbiddenSections: [...FORBIDDEN_INPUTS, "rawSource", "rawEvidence", "memoryBodies"],
|
|
288
|
+
provenance: [`agent-profile:${parsed.id}`],
|
|
289
|
+
},
|
|
290
|
+
warnings: [],
|
|
291
|
+
blockers: [],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function validateAgentOutput(
|
|
296
|
+
schema: AgentOutputSchemaId,
|
|
297
|
+
output: unknown,
|
|
298
|
+
): { ok: boolean; errors: string[] } {
|
|
299
|
+
if (schema === "verification-summary-v1")
|
|
300
|
+
return validateRequiredObject(output, ["summary", "status", "evidenceRefs"]);
|
|
301
|
+
if (schema === "design-options-v1")
|
|
302
|
+
return validateRequiredObject(output, ["summary", "options", "recommendation", "evidenceRefs"]);
|
|
303
|
+
const errors: string[] = [];
|
|
304
|
+
if (!isRecord(output)) return { ok: false, errors: ["Output must be an object."] };
|
|
305
|
+
if (typeof output.summary !== "string") errors.push("summary is required.");
|
|
306
|
+
if (!Array.isArray(output.findings)) errors.push("findings array is required.");
|
|
307
|
+
if (
|
|
308
|
+
output.confidence !== "low" &&
|
|
309
|
+
output.confidence !== "medium" &&
|
|
310
|
+
output.confidence !== "high"
|
|
311
|
+
) {
|
|
312
|
+
errors.push("confidence must be low/medium/high.");
|
|
313
|
+
}
|
|
314
|
+
if (!Array.isArray(output.evidenceRefs)) errors.push("evidenceRefs array is required.");
|
|
315
|
+
return { ok: errors.length === 0, errors };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function mergeReviewFindings(outputs: ReviewFindingOutput[]): {
|
|
319
|
+
acceptedFindings: ReviewFindingOutput["findings"];
|
|
320
|
+
unresolvedConflicts: string[];
|
|
321
|
+
} {
|
|
322
|
+
const byKey = new Map<string, ReviewFindingOutput["findings"][number]>();
|
|
323
|
+
const conflicts: string[] = [];
|
|
324
|
+
for (const output of outputs) {
|
|
325
|
+
for (const finding of output.findings) {
|
|
326
|
+
const key = `${finding.category}:${finding.path ?? ""}:${finding.line ?? ""}:${finding.title}`;
|
|
327
|
+
const existing = byKey.get(key);
|
|
328
|
+
if (existing === undefined) {
|
|
329
|
+
byKey.set(key, finding);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (severityRank(finding.severity) > severityRank(existing.severity)) byKey.set(key, finding);
|
|
333
|
+
if (
|
|
334
|
+
finding.severity !== existing.severity ||
|
|
335
|
+
finding.recommendation !== existing.recommendation
|
|
336
|
+
) {
|
|
337
|
+
conflicts.push(key);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { acceptedFindings: [...byKey.values()], unresolvedConflicts: conflicts };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function formatAgentComposeDryRun(plan: AgentComposeDryRunPlan): string {
|
|
345
|
+
return [
|
|
346
|
+
"EvoDev agent compose dry-run",
|
|
347
|
+
"",
|
|
348
|
+
`Task: ${plan.taskId}`,
|
|
349
|
+
`Mode: ${plan.mode ?? "not routed"}`,
|
|
350
|
+
`Workflow: ${plan.workflowId ?? "none"}`,
|
|
351
|
+
`Rationale: ${plan.rationale}`,
|
|
352
|
+
"No-write/no-spawn: true",
|
|
353
|
+
"Privacy: local-private, metadata-only, raw prompts/source/secrets/logs forbidden",
|
|
354
|
+
"Agents:",
|
|
355
|
+
...(plan.agents.length === 0
|
|
356
|
+
? [" - none"]
|
|
357
|
+
: plan.agents.map(
|
|
358
|
+
(agent) =>
|
|
359
|
+
` - ${agent.profile.id} [${agent.lens}] traits=${agent.profile.traits.expertise.join(",")} schema=${agent.profile.output.schema} plannedOnly=${agent.plannedOnly}`,
|
|
360
|
+
)),
|
|
361
|
+
"Inputs:",
|
|
362
|
+
...(plan.agents.length === 0
|
|
363
|
+
? [" - none"]
|
|
364
|
+
: [
|
|
365
|
+
` - required: ${plan.agents[0].profile.inputs.required.join(",")}`,
|
|
366
|
+
` - optional: ${plan.agents[0].profile.inputs.optional.join(",")}`,
|
|
367
|
+
` - forbidden: ${plan.agents[0].profile.inputs.forbidden.join(",")}`,
|
|
368
|
+
]),
|
|
369
|
+
"Permissions:",
|
|
370
|
+
...Object.entries(plan.permissions).map(([key, value]) => ` - ${key}: ${value}`),
|
|
371
|
+
"Merge plan:",
|
|
372
|
+
` - strategy: ${plan.mergePlan.strategy}`,
|
|
373
|
+
` - outputSchema: ${plan.mergePlan.outputSchema ?? "none"}`,
|
|
374
|
+
"Warnings:",
|
|
375
|
+
...(plan.warnings.length === 0
|
|
376
|
+
? [" - none"]
|
|
377
|
+
: plan.warnings.map((warning) => ` - ${warning}`)),
|
|
378
|
+
"Blockers:",
|
|
379
|
+
...(plan.blockers.length === 0
|
|
380
|
+
? [" - none"]
|
|
381
|
+
: plan.blockers.map((blocker) => ` - ${blocker}`)),
|
|
382
|
+
].join("\n");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function formatAgentContextDryRun(bundle: AgentContextDryRunBundle): string {
|
|
386
|
+
return [
|
|
387
|
+
"EvoDev agent load-context dry-run",
|
|
388
|
+
"",
|
|
389
|
+
`Agent: ${bundle.profile.id}`,
|
|
390
|
+
`Persistence: ${bundle.profile.persistence}`,
|
|
391
|
+
"Allowed sections:",
|
|
392
|
+
...bundle.context.allowedSections.map((section) => ` - ${section}`),
|
|
393
|
+
"Forbidden sections:",
|
|
394
|
+
...bundle.context.forbiddenSections.map((section) => ` - ${section}`),
|
|
395
|
+
"Permissions:",
|
|
396
|
+
...Object.entries(bundle.profile.permissions).map(([key, value]) => ` - ${key}: ${value}`),
|
|
397
|
+
"Blockers:",
|
|
398
|
+
...(bundle.blockers.length === 0
|
|
399
|
+
? [" - none"]
|
|
400
|
+
: bundle.blockers.map((blocker) => ` - ${blocker}`)),
|
|
401
|
+
].join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function createDynamicProfile(
|
|
405
|
+
lens: AgentLens,
|
|
406
|
+
contract: TaskContract,
|
|
407
|
+
permissions: AgentPermissions,
|
|
408
|
+
): AgentProfile {
|
|
409
|
+
return {
|
|
410
|
+
version: 1,
|
|
411
|
+
id: `dynamic-${lens}-${contract.taskId}`.slice(0, 100),
|
|
412
|
+
name: `Dynamic ${lens} reviewer`,
|
|
413
|
+
persistence: "dynamic",
|
|
414
|
+
description: `Task-scoped ${lens} profile for metadata-only dry-run planning.`,
|
|
415
|
+
traits: {
|
|
416
|
+
expertise: LENS_TO_EXPERTISE[lens],
|
|
417
|
+
stance: ["skeptical-reviewer"],
|
|
418
|
+
approach: ["evidence-first", "fail-closed"],
|
|
419
|
+
domain: ["software-rd"],
|
|
420
|
+
},
|
|
421
|
+
inputs: {
|
|
422
|
+
required: ["taskContract", "scope", "evidenceSummary"],
|
|
423
|
+
optional: ["workflowPlan"],
|
|
424
|
+
forbidden: FORBIDDEN_INPUTS,
|
|
425
|
+
},
|
|
426
|
+
permissions,
|
|
427
|
+
output: {
|
|
428
|
+
schema: "review-findings-v1",
|
|
429
|
+
requiredFields: ["summary", "findings", "confidence", "evidenceRefs"],
|
|
430
|
+
},
|
|
431
|
+
privacy: {
|
|
432
|
+
classification: "local-private",
|
|
433
|
+
metadataOnly: true,
|
|
434
|
+
rawPromptStored: false,
|
|
435
|
+
sourceContentStored: false,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function selectLenses(
|
|
441
|
+
mode: TaskExecutionMode | null,
|
|
442
|
+
requested: AgentLens[],
|
|
443
|
+
workflowId: string | null,
|
|
444
|
+
requiredReview: string[],
|
|
445
|
+
): AgentLens[] {
|
|
446
|
+
if (requested.length > 0) return mode === "standard" ? requested.slice(0, 1) : requested;
|
|
447
|
+
if (mode === "standard") return requiredReview.length > 0 ? ["review"] : [];
|
|
448
|
+
if (mode === "rigorous") {
|
|
449
|
+
if (workflowId?.includes("security")) return ["security", "review"];
|
|
450
|
+
if (workflowId?.includes("release")) return ["release", "security"];
|
|
451
|
+
if (workflowId?.includes("migration")) return ["migration", "architecture"];
|
|
452
|
+
if (workflowId?.includes("architecture")) return ["architecture", "review"];
|
|
453
|
+
return requiredReview.length > 0 ? ["review", "qa"] : [];
|
|
454
|
+
}
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function createMergePlan(agents: AgentPlannedInvocation[]): AgentMergePlan {
|
|
459
|
+
if (agents.length === 0)
|
|
460
|
+
return {
|
|
461
|
+
strategy: "none",
|
|
462
|
+
outputSchema: null,
|
|
463
|
+
dedupeBy: [],
|
|
464
|
+
conflictPolicy: [],
|
|
465
|
+
failClosedCategories: [],
|
|
466
|
+
};
|
|
467
|
+
if (agents.length === 1)
|
|
468
|
+
return {
|
|
469
|
+
strategy: "single-output",
|
|
470
|
+
outputSchema: agents[0].profile.output.schema,
|
|
471
|
+
dedupeBy: [],
|
|
472
|
+
conflictPolicy: [],
|
|
473
|
+
failClosedCategories: [],
|
|
474
|
+
};
|
|
475
|
+
return {
|
|
476
|
+
strategy: "dedupe-preserve-conflicts-fail-closed",
|
|
477
|
+
outputSchema: "review-findings-v1",
|
|
478
|
+
dedupeBy: ["category", "path", "line", "title"],
|
|
479
|
+
conflictPolicy: ["preserve dissent", "keep higher severity unless evidence refutes"],
|
|
480
|
+
failClosedCategories: ["security", "privacy", "release"],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function createLensReason(
|
|
485
|
+
lens: AgentLens,
|
|
486
|
+
mode: TaskExecutionMode | null,
|
|
487
|
+
workflowId: string | null,
|
|
488
|
+
): string {
|
|
489
|
+
return `${lens} lens selected for ${mode ?? "unrouted"} mode${workflowId ? ` and workflow ${workflowId}` : ""}.`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function collectPrivacyBoundaryBlockers(
|
|
493
|
+
contract: TaskContract,
|
|
494
|
+
lenses: AgentLens[],
|
|
495
|
+
workflowId: string | null,
|
|
496
|
+
): string[] {
|
|
497
|
+
const text = [
|
|
498
|
+
contract.source.summary,
|
|
499
|
+
contract.currentState.summary,
|
|
500
|
+
contract.targetState.summary,
|
|
501
|
+
contract.route.rationale,
|
|
502
|
+
workflowId ?? "",
|
|
503
|
+
...contract.scope.allowedOperations,
|
|
504
|
+
...contract.scope.requiresUserConfirmation,
|
|
505
|
+
...lenses,
|
|
506
|
+
].join(" ");
|
|
507
|
+
return PRIVACY_BLOCKER_PATTERN.test(text)
|
|
508
|
+
? [
|
|
509
|
+
"Agent planning refuses privacy/risky context requiring raw data, writes, commands, network, spawning, or memory.",
|
|
510
|
+
]
|
|
511
|
+
: [];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function validateRequiredObject(
|
|
515
|
+
output: unknown,
|
|
516
|
+
fields: string[],
|
|
517
|
+
): { ok: boolean; errors: string[] } {
|
|
518
|
+
if (!isRecord(output)) return { ok: false, errors: ["Output must be an object."] };
|
|
519
|
+
const errors = fields
|
|
520
|
+
.filter((field) => output[field] === undefined)
|
|
521
|
+
.map((field) => `${field} is required.`);
|
|
522
|
+
return { ok: errors.length === 0, errors };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function normalizeAgentPermissions(value: AgentPermissions): AgentPermissions {
|
|
526
|
+
return { ...createDefaultAgentPermissions(), ...value };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isKnownOutputSchema(value: unknown): value is AgentOutputSchemaId {
|
|
530
|
+
return (
|
|
531
|
+
value === "review-findings-v1" ||
|
|
532
|
+
value === "verification-summary-v1" ||
|
|
533
|
+
value === "design-options-v1"
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function dedupeLenses(lenses: AgentLens[]): AgentLens[] {
|
|
538
|
+
return [...new Set(lenses)];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function severityRank(severity: AgentFindingSeverity): number {
|
|
542
|
+
return { info: 0, low: 1, medium: 2, high: 3, blocker: 4 }[severity];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function assertString(value: unknown, path: string): void {
|
|
546
|
+
if (typeof value !== "string" || value.length === 0)
|
|
547
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function assertStringArray(value: unknown, path: string): void {
|
|
551
|
+
if (
|
|
552
|
+
!Array.isArray(value) ||
|
|
553
|
+
value.some((item) => typeof item !== "string" || item.length === 0)
|
|
554
|
+
) {
|
|
555
|
+
throw new Error(`${path} must be an array of non-empty strings.`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
560
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
561
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class EvoDevAssetError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
message: string,
|
|
4
|
+
readonly filePath?: string,
|
|
5
|
+
) {
|
|
6
|
+
super(filePath ? `${message}: ${filePath}` : message);
|
|
7
|
+
this.name = "EvoDevAssetError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function describeType(value: unknown): string {
|
|
12
|
+
if (value === null) {
|
|
13
|
+
return "null";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return "array";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return typeof value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { EvoDevAssetError } from "./errors.ts";
|
|
2
|
+
export {
|
|
3
|
+
type AgentManifest,
|
|
4
|
+
type AssetManifest,
|
|
5
|
+
type AssetTarget,
|
|
6
|
+
type BaseAssetManifest,
|
|
7
|
+
type SkillManifest,
|
|
8
|
+
parseAgentManifest,
|
|
9
|
+
parseSkillManifest,
|
|
10
|
+
} from "./manifest.ts";
|
|
11
|
+
export {
|
|
12
|
+
type AssetScannerPaths,
|
|
13
|
+
type AssetScanResult,
|
|
14
|
+
type ScannedAsset,
|
|
15
|
+
scanAgentAssets,
|
|
16
|
+
scanAssets,
|
|
17
|
+
scanSkillAssets,
|
|
18
|
+
} from "./scanner.ts";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { EvoDevAssetError, describeType } from "./errors.ts";
|
|
2
|
+
|
|
3
|
+
export type AssetTarget = "claude" | "codex";
|
|
4
|
+
|
|
5
|
+
export interface BaseAssetManifest {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
category: string;
|
|
10
|
+
description: string;
|
|
11
|
+
targets: AssetTarget[];
|
|
12
|
+
entry: string;
|
|
13
|
+
license?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkillManifest extends BaseAssetManifest {
|
|
17
|
+
kind: "skill";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AgentManifest extends BaseAssetManifest {
|
|
21
|
+
kind: "agent";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AssetManifest = SkillManifest | AgentManifest;
|
|
25
|
+
|
|
26
|
+
export function parseSkillManifest(value: unknown): SkillManifest {
|
|
27
|
+
return {
|
|
28
|
+
...parseBaseManifest(value, "skill manifest"),
|
|
29
|
+
kind: "skill",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseAgentManifest(value: unknown): AgentManifest {
|
|
34
|
+
return {
|
|
35
|
+
...parseBaseManifest(value, "agent manifest"),
|
|
36
|
+
kind: "agent",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseBaseManifest(value: unknown, label: string): BaseAssetManifest {
|
|
41
|
+
const root = expectRecord(value, label);
|
|
42
|
+
const manifest: BaseAssetManifest = {
|
|
43
|
+
id: expectSlug(root.id, `${label}.id`),
|
|
44
|
+
name: expectNonEmptyString(root.name, `${label}.name`),
|
|
45
|
+
version: expectNonEmptyString(root.version, `${label}.version`),
|
|
46
|
+
category: expectSlug(root.category, `${label}.category`),
|
|
47
|
+
description: expectNonEmptyString(root.description, `${label}.description`),
|
|
48
|
+
targets: expectTargets(root.targets, `${label}.targets`),
|
|
49
|
+
entry: expectRelativePath(root.entry, `${label}.entry`),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (root.license !== undefined) {
|
|
53
|
+
manifest.license = expectNonEmptyString(root.license, `${label}.license`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return manifest;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function expectRecord(value: unknown, path: string): Record<string, unknown> {
|
|
60
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
61
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected object, got ${describeType(value)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return value as Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expectNonEmptyString(value: unknown, path: string): string {
|
|
68
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
69
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected non-empty string`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function expectSlug(value: unknown, path: string): string {
|
|
76
|
+
const text = expectNonEmptyString(value, path);
|
|
77
|
+
|
|
78
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(text)) {
|
|
79
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected lowercase kebab-case slug`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function expectTargets(value: unknown, path: string): AssetTarget[] {
|
|
86
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
87
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected non-empty target array`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const targets = value.map((target, index) => {
|
|
91
|
+
if (target !== "claude" && target !== "codex") {
|
|
92
|
+
throw new EvoDevAssetError(`Invalid ${path}[${index}]; expected claude or codex`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return target;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return [...new Set(targets)];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function expectRelativePath(value: unknown, path: string): string {
|
|
102
|
+
const text = expectNonEmptyString(value, path);
|
|
103
|
+
|
|
104
|
+
if (text.startsWith("/") || text.includes("..")) {
|
|
105
|
+
throw new EvoDevAssetError(`Invalid ${path}; expected safe relative path`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return text;
|
|
109
|
+
}
|