@evo-dev/core 0.0.1-alpha.2 → 0.0.1-alpha.20

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.
Files changed (79) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +5 -3
  2. package/assets/team/agents/code-reviewer.md +48 -0
  3. package/assets/team/agents/docs-maintainer.md +51 -0
  4. package/assets/team/agents/implementation-engineer.md +51 -0
  5. package/assets/team/agents/product-scope-analyst.md +58 -0
  6. package/assets/team/agents/release-engineer.md +55 -0
  7. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  8. package/assets/team/agents/solution-architect.md +51 -0
  9. package/assets/team/agents/verification-engineer.md +51 -0
  10. package/assets/team/team.md +102 -0
  11. package/dist/assets/index.js +5 -5
  12. package/dist/config/index.js +793 -241
  13. package/dist/index.js +20840 -12908
  14. package/dist/plugins/index.js +13 -13
  15. package/package.json +1 -1
  16. package/src/agents/index.ts +1 -265
  17. package/src/code-agent-traces/index.ts +11 -12
  18. package/src/config/index.ts +2 -0
  19. package/src/config/settings.ts +116 -7
  20. package/src/config/store.ts +1 -1
  21. package/src/daemon/index.ts +1 -41
  22. package/src/evolution/candidates/index.ts +730 -0
  23. package/src/evolution/control/index.ts +20 -0
  24. package/src/evolution/evidence/analysis.ts +533 -0
  25. package/src/evolution/evidence/index.ts +3 -0
  26. package/src/evolution/evidence/session-memory/analysis.ts +287 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +9 -0
  29. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  30. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  31. package/src/evolution/evidence/session-memory/retention.ts +643 -0
  32. package/src/evolution/evidence/session-memory/segment.ts +216 -0
  33. package/src/evolution/evidence/session-memory/semantic-packet.ts +408 -0
  34. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  35. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  36. package/src/evolution/evidence/session-memory/storage.ts +744 -0
  37. package/src/evolution/evidence/session-memory/types.ts +296 -0
  38. package/src/evolution/evidence/session-memory/updater.ts +199 -0
  39. package/src/evolution/formatters.ts +169 -0
  40. package/src/evolution/imports/apply.ts +435 -0
  41. package/src/evolution/imports/diff.ts +472 -0
  42. package/src/evolution/imports/index.ts +7 -0
  43. package/src/evolution/imports/materialize.ts +640 -0
  44. package/src/evolution/imports/paths.ts +129 -0
  45. package/src/evolution/imports/stage.ts +414 -0
  46. package/src/evolution/imports/storage.ts +952 -0
  47. package/src/evolution/imports/types.ts +226 -0
  48. package/src/evolution/index.ts +19 -2827
  49. package/src/evolution/knowledge/change-store.ts +558 -0
  50. package/src/evolution/knowledge/changes.ts +459 -0
  51. package/src/evolution/knowledge/freshness.ts +69 -0
  52. package/src/{knowledge → evolution/knowledge}/index.ts +1532 -206
  53. package/src/evolution/knowledge/review.ts +446 -0
  54. package/src/evolution/knowledge/support.ts +135 -0
  55. package/src/evolution/paths.ts +44 -0
  56. package/src/evolution/processor/distillation.ts +518 -0
  57. package/src/evolution/processor/index.ts +3 -0
  58. package/src/evolution/processor/process.ts +594 -0
  59. package/src/{learning → evolution/review}/index.ts +10 -14
  60. package/src/evolution/schema.ts +639 -0
  61. package/src/evolution/shared.ts +1053 -0
  62. package/src/evolution/triggers/classification.ts +102 -0
  63. package/src/evolution/triggers/index.ts +295 -0
  64. package/src/hooks/index.ts +281 -197
  65. package/src/index.ts +15 -4
  66. package/src/projects/index.ts +934 -0
  67. package/src/runtime-logs/index.ts +100 -13
  68. package/src/team/index.ts +582 -3
  69. package/src/utils/errors.ts +13 -0
  70. package/src/utils/fs.ts +40 -0
  71. package/src/utils/hash.ts +9 -0
  72. package/src/utils/ids.ts +12 -0
  73. package/src/utils/index.ts +7 -0
  74. package/src/utils/parsing.ts +11 -0
  75. package/src/utils/text.ts +18 -0
  76. package/src/utils/time.ts +5 -0
  77. package/src/workflow/index.ts +3 -21
  78. package/src/project/index.ts +0 -507
  79. package/src/task/index.ts +0 -840
@@ -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
+ }
@@ -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,7 @@
1
+ export * from "./errors.ts";
2
+ export * from "./fs.ts";
3
+ export * from "./hash.ts";
4
+ export * from "./ids.ts";
5
+ export * from "./parsing.ts";
6
+ export * from "./text.ts";
7
+ export * from "./time.ts";
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export function normalizeTimestamp(value?: string | Date): string {
2
+ if (value instanceof Date) return value.toISOString();
3
+ if (typeof value === "string" && value.trim() !== "") return new Date(value).toISOString();
4
+ return new Date().toISOString();
5
+ }
@@ -1,6 +1,5 @@
1
1
  import { readFile, readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import type { TaskContract } from "../task/index.ts";
4
3
 
5
4
  export type WorkflowMode = "minimal" | "standard" | "rigorous";
6
5
 
@@ -36,8 +35,6 @@ export interface WorkflowStep {
36
35
 
37
36
  export interface WorkflowPlan {
38
37
  workflow: WorkflowManifest;
39
- taskId?: string;
40
- mode: WorkflowMode | null;
41
38
  steps: Array<WorkflowStep & { plannedOnly: true }>;
42
39
  requiredEvidence: string[];
43
40
  warnings: string[];
@@ -78,26 +75,13 @@ export function parseWorkflowManifest(value: unknown): WorkflowManifest {
78
75
  return manifest;
79
76
  }
80
77
 
81
- export function planWorkflow(input: {
82
- workflow: WorkflowManifest;
83
- contract?: TaskContract;
84
- }): WorkflowPlan {
85
- const mode = input.contract?.route.mode ?? null;
86
- const warnings: string[] = [];
87
- const advisories: string[] = [];
88
-
89
- if (mode !== null && !input.workflow.modes.includes(mode)) {
90
- advisories.push(`Workflow ${input.workflow.id} does not list task mode ${mode}.`);
91
- }
92
-
78
+ export function planWorkflow(input: { workflow: WorkflowManifest }): WorkflowPlan {
93
79
  return {
94
80
  workflow: input.workflow,
95
- taskId: input.contract?.taskId,
96
- mode,
97
81
  steps: input.workflow.steps.map((step) => ({ ...step, plannedOnly: true })),
98
82
  requiredEvidence: input.workflow.requiredEvidence,
99
- warnings,
100
- advisories,
83
+ warnings: [],
84
+ advisories: [],
101
85
  };
102
86
  }
103
87
 
@@ -116,8 +100,6 @@ export function formatWorkflowPlan(plan: WorkflowPlan): string {
116
100
  "EvoDev workflow dry-run",
117
101
  "",
118
102
  `Workflow: ${plan.workflow.id}`,
119
- `Task: ${plan.taskId ?? "none"}`,
120
- `Mode: ${plan.mode ?? "not routed"}`,
121
103
  "",
122
104
  "Steps:",
123
105
  ...plan.steps.map(
@@ -1,507 +0,0 @@
1
- import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
- import { basename, join, relative } from "node:path";
3
-
4
- export const PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS = [
5
- ".evodev/project.json",
6
- ".evodev/profile.md",
7
- ".evodev/index.json",
8
- ".evodev/commands.json",
9
- ".evodev/privacy.json",
10
- ".evodev/decisions/README.md",
11
- ] as const;
12
-
13
- export type ProjectContextAllowedRelativePath =
14
- (typeof PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS)[number];
15
-
16
- export type ProjectContextPlanAction = "create" | "error";
17
-
18
- export interface ProjectContextPlanEntry {
19
- relativePath: ProjectContextAllowedRelativePath;
20
- absolutePath: string;
21
- action: ProjectContextPlanAction;
22
- reason: string;
23
- }
24
-
25
- export interface ProjectContextPlan {
26
- projectDir: string;
27
- projectId: string;
28
- files: ProjectContextPlanEntry[];
29
- index: ProjectIndex;
30
- commands: ProjectCommands;
31
- privacy: ProjectPrivacy;
32
- warnings: string[];
33
- errors: string[];
34
- }
35
-
36
- export interface ProjectProfile {
37
- version: 1;
38
- projectId: string;
39
- displayName: string;
40
- root: {
41
- pathPolicy: "local-only";
42
- };
43
- privacy: {
44
- classification: "local-private";
45
- metadataOnly: true;
46
- sourceContentIncluded: false;
47
- rawCommandsIncluded: false;
48
- };
49
- }
50
-
51
- export interface ProjectIndex {
52
- version: 1;
53
- mode: "metadata-only";
54
- projectId: string;
55
- generatedAt: null;
56
- rootName: string;
57
- files: ProjectFileMetadata[];
58
- docs: {
59
- entrypoints: string[];
60
- };
61
- commands: {
62
- scripts: ProjectScriptSummary[];
63
- testCommandCandidates: string[];
64
- };
65
- exclusions: string[];
66
- sourceContentIncluded: false;
67
- }
68
-
69
- export interface ProjectFileMetadata {
70
- path: string;
71
- kind: "file" | "directory";
72
- sizeBytes?: number;
73
- }
74
-
75
- export interface ProjectCommands {
76
- version: 1;
77
- metadataOnly: true;
78
- rawCommandsIncluded: false;
79
- scripts: ProjectScriptSummary[];
80
- warnings: string[];
81
- }
82
-
83
- export interface ProjectScriptSummary {
84
- name: string;
85
- commandClass: ProjectCommandClass;
86
- summary: string;
87
- rawCommandStored: false;
88
- redacted: boolean;
89
- warning?: string;
90
- }
91
-
92
- export type ProjectCommandClass =
93
- | "build"
94
- | "format"
95
- | "install"
96
- | "lint"
97
- | "other"
98
- | "release"
99
- | "test"
100
- | "typecheck";
101
-
102
- export interface ProjectPrivacy {
103
- version: 1;
104
- classification: "local-private";
105
- metadataOnly: true;
106
- sourceContentIndex: false;
107
- promptHistoryIndex: false;
108
- shellHistoryIndex: false;
109
- rawCommandOutputIndex: false;
110
- externalUpload: false;
111
- excludedGlobs: string[];
112
- protectedPatterns: string[];
113
- }
114
-
115
- export interface WriteProjectContextResult {
116
- writtenFiles: ProjectContextAllowedRelativePath[];
117
- }
118
-
119
- const EXCLUDED_DIRECTORY_NAMES = new Set([
120
- ".git",
121
- ".claude",
122
- ".codex",
123
- "backups",
124
- "build",
125
- "coverage",
126
- "dist",
127
- "node_modules",
128
- ]);
129
-
130
- const EXCLUDED_FILE_PREFIXES = [".env"];
131
- const EXCLUDED_FILE_PARTS = [
132
- "api-key",
133
- "api_key",
134
- "apikey",
135
- "credential",
136
- "credentials",
137
- "internal",
138
- "internal-link",
139
- "password",
140
- "passwords",
141
- "passwd",
142
- "private",
143
- "private-url",
144
- "secret",
145
- "token",
146
- ];
147
- const EXCLUDED_FILE_EXTENSIONS = [".key", ".pem", ".p12", ".pfx"];
148
- const DOC_ENTRYPOINTS = new Set(["README.md"]);
149
- const DEFAULT_EXCLUDED_GLOBS = [
150
- ".git/**",
151
- "node_modules/**",
152
- "dist/**",
153
- "build/**",
154
- "coverage/**",
155
- ".env*",
156
- "**/*secret*",
157
- "**/*token*",
158
- "**/*password*",
159
- "**/*passwd*",
160
- "**/*api-key*",
161
- "**/*api_key*",
162
- "**/*apikey*",
163
- "**/*credential*",
164
- "**/*private*",
165
- "**/*internal*",
166
- "**/*.key",
167
- "**/*.pem",
168
- ".claude/**",
169
- ".codex/**",
170
- ".evodev/backups/**",
171
- ];
172
- const PROTECTED_PATTERNS = [
173
- "secret",
174
- "token",
175
- "password",
176
- "passwd",
177
- "api-key",
178
- "credential",
179
- "private",
180
- "private-url",
181
- "internal",
182
- "internal-link",
183
- ];
184
-
185
- export async function createProjectContextPlan(projectDir: string): Promise<ProjectContextPlan> {
186
- const projectDirStat = await stat(projectDir);
187
- if (!projectDirStat.isDirectory()) {
188
- throw new Error(`Project dir is not a directory: ${projectDir}`);
189
- }
190
-
191
- const projectId = createProjectId(projectDir);
192
- const privacy = createProjectPrivacy();
193
- const files = await collectProjectFileMetadata(projectDir);
194
- const commands = await collectProjectCommands(projectDir);
195
- const docs = files
196
- .filter((file) => file.kind === "file" && DOC_ENTRYPOINTS.has(basename(file.path)))
197
- .map((file) => file.path)
198
- .sort();
199
- const index: ProjectIndex = {
200
- version: 1,
201
- mode: "metadata-only",
202
- projectId,
203
- generatedAt: null,
204
- rootName: basename(projectDir),
205
- files,
206
- docs: { entrypoints: docs },
207
- commands: {
208
- scripts: commands.scripts,
209
- testCommandCandidates: commands.scripts
210
- .filter((script) => script.commandClass === "test")
211
- .map((script) => script.name),
212
- },
213
- exclusions: privacy.excludedGlobs,
214
- sourceContentIncluded: false,
215
- };
216
- const planFiles = await Promise.all(
217
- PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS.map(async (relativePath) => {
218
- const absolutePath = join(projectDir, relativePath);
219
- const exists = await pathExists(absolutePath);
220
- return {
221
- relativePath,
222
- absolutePath,
223
- action: exists ? "error" : "create",
224
- reason: exists
225
- ? "Target already exists; I1 does not overwrite."
226
- : "Allowed project context file.",
227
- } satisfies ProjectContextPlanEntry;
228
- }),
229
- );
230
- const errors = planFiles
231
- .filter((file) => file.action === "error")
232
- .map((file) => `${file.relativePath}: ${file.reason}`);
233
-
234
- return {
235
- projectDir,
236
- projectId,
237
- files: planFiles,
238
- index,
239
- commands,
240
- privacy,
241
- warnings: commands.warnings,
242
- errors,
243
- };
244
- }
245
-
246
- export async function writeProjectContext(
247
- plan: ProjectContextPlan,
248
- ): Promise<WriteProjectContextResult> {
249
- if (plan.errors.length > 0) {
250
- throw new Error(`Cannot write project context:\n${plan.errors.join("\n")}`);
251
- }
252
-
253
- const payloads = createProjectContextPayloads(plan);
254
- const writtenFiles: ProjectContextAllowedRelativePath[] = [];
255
-
256
- for (const relativePath of PROJECT_CONTEXT_ALLOWED_RELATIVE_PATHS) {
257
- const content = payloads[relativePath];
258
- const absolutePath = join(plan.projectDir, relativePath);
259
- await mkdir(join(absolutePath, ".."), { recursive: true });
260
- await writeFile(absolutePath, content, { encoding: "utf8", flag: "wx" });
261
- writtenFiles.push(relativePath);
262
- }
263
-
264
- return { writtenFiles };
265
- }
266
-
267
- export function formatProjectContextPlan(
268
- plan: ProjectContextPlan,
269
- mode: "dry-run" | "write",
270
- ): string {
271
- return [
272
- "EvoDev project init",
273
- "",
274
- `Mode: ${mode}`,
275
- `Project: ${plan.projectDir}`,
276
- `Project id: ${plan.projectId}`,
277
- "",
278
- "Plan:",
279
- ...plan.files.map((file) => ` - ${file.action}: ${file.relativePath} (${file.reason})`),
280
- "",
281
- "Metadata-only index summary:",
282
- ` - files: ${plan.index.files.filter((file) => file.kind === "file").length}`,
283
- ` - directories: ${plan.index.files.filter((file) => file.kind === "directory").length}`,
284
- ` - docs: ${plan.index.docs.entrypoints.length}`,
285
- ` - package scripts: ${plan.commands.scripts.length}`,
286
- ...plan.warnings.map((warning) => `Warning: ${warning}`),
287
- ...plan.errors.map((error) => `Error: ${error}`),
288
- ].join("\n");
289
- }
290
-
291
- function createProjectContextPayloads(
292
- plan: ProjectContextPlan,
293
- ): Record<ProjectContextAllowedRelativePath, string> {
294
- const profile: ProjectProfile = {
295
- version: 1,
296
- projectId: plan.projectId,
297
- displayName: basename(plan.projectDir),
298
- root: { pathPolicy: "local-only" },
299
- privacy: {
300
- classification: "local-private",
301
- metadataOnly: true,
302
- sourceContentIncluded: false,
303
- rawCommandsIncluded: false,
304
- },
305
- };
306
-
307
- return {
308
- ".evodev/project.json": `${JSON.stringify(profile, null, 2)}\n`,
309
- ".evodev/profile.md": createProfileMarkdown(plan),
310
- ".evodev/index.json": `${JSON.stringify(plan.index, null, 2)}\n`,
311
- ".evodev/commands.json": `${JSON.stringify(plan.commands, null, 2)}\n`,
312
- ".evodev/privacy.json": `${JSON.stringify(plan.privacy, null, 2)}\n`,
313
- ".evodev/decisions/README.md": "# Project Decisions\n\nRecord project decisions here.\n",
314
- };
315
- }
316
-
317
- function createProfileMarkdown(plan: ProjectContextPlan): string {
318
- return [
319
- `# ${basename(plan.projectDir)} Project Context`,
320
- "",
321
- "This project context was generated as metadata-only local state.",
322
- "",
323
- `- Project id: ${plan.projectId}`,
324
- "- Source content included: false",
325
- "- Raw package script commands included: false",
326
- "- External upload: false",
327
- "",
328
- ].join("\n");
329
- }
330
-
331
- async function collectProjectFileMetadata(projectDir: string): Promise<ProjectFileMetadata[]> {
332
- const files: ProjectFileMetadata[] = [];
333
-
334
- async function visit(dir: string): Promise<void> {
335
- const entries = await readdir(dir, { withFileTypes: true });
336
-
337
- for (const entry of entries) {
338
- const absolutePath = join(dir, entry.name);
339
- const relativePath = relative(projectDir, absolutePath).replaceAll("\\", "/");
340
- if (shouldExcludePath(relativePath, entry.isDirectory())) {
341
- continue;
342
- }
343
-
344
- if (entry.isDirectory()) {
345
- files.push({ path: relativePath, kind: "directory" });
346
- await visit(absolutePath);
347
- continue;
348
- }
349
-
350
- if (entry.isFile()) {
351
- const fileStat = await stat(absolutePath);
352
- files.push({ path: relativePath, kind: "file", sizeBytes: fileStat.size });
353
- }
354
- }
355
- }
356
-
357
- await visit(projectDir);
358
- return files.sort((left, right) => left.path.localeCompare(right.path));
359
- }
360
-
361
- async function collectProjectCommands(projectDir: string): Promise<ProjectCommands> {
362
- const packageJsonPath = join(projectDir, "package.json");
363
- const warnings: string[] = [];
364
-
365
- if (!(await pathExists(packageJsonPath))) {
366
- return { version: 1, metadataOnly: true, rawCommandsIncluded: false, scripts: [], warnings };
367
- }
368
-
369
- let parsed: unknown;
370
- try {
371
- parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
372
- } catch (error) {
373
- warnings.push(`package.json scripts skipped: ${describeError(error)}`);
374
- return { version: 1, metadataOnly: true, rawCommandsIncluded: false, scripts: [], warnings };
375
- }
376
-
377
- const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
378
- const summaries: ProjectScriptSummary[] = [];
379
-
380
- for (const [name, value] of Object.entries(scripts).sort(([left], [right]) =>
381
- left.localeCompare(right),
382
- )) {
383
- if (typeof value !== "string") {
384
- continue;
385
- }
386
-
387
- const commandClass = classifyScript(name, value);
388
- const sensitiveReason = detectSensitiveScript(`${name} ${value}`);
389
- if (sensitiveReason !== null) {
390
- const safeName = `redacted-${commandClass}-script`;
391
- const warning = `Protected ${commandClass} script name/command omitted because it matched protected pattern: ${sensitiveReason}.`;
392
- warnings.push(warning);
393
- summaries.push({
394
- name: safeName,
395
- commandClass,
396
- summary: `Protected ${commandClass} script name/command omitted.`,
397
- rawCommandStored: false,
398
- redacted: true,
399
- warning,
400
- });
401
- continue;
402
- }
403
-
404
- summaries.push({
405
- name,
406
- commandClass,
407
- summary: `Safe ${commandClass} script command summary only.`,
408
- rawCommandStored: false,
409
- redacted: false,
410
- });
411
- }
412
-
413
- return {
414
- version: 1,
415
- metadataOnly: true,
416
- rawCommandsIncluded: false,
417
- scripts: summaries,
418
- warnings,
419
- };
420
- }
421
-
422
- function classifyScript(name: string, command: string): ProjectCommandClass {
423
- const text = `${name} ${command}`.toLowerCase();
424
- if (text.includes("typecheck") || text.includes("tsc")) return "typecheck";
425
- if (text.includes("lint") || text.includes("biome") || text.includes("eslint")) return "lint";
426
- if (text.includes("test")) return "test";
427
- if (text.includes("build")) return "build";
428
- if (text.includes("format") || text.includes("prettier")) return "format";
429
- if (text.includes("install")) return "install";
430
- if (text.includes("release") || text.includes("publish") || text.includes("pack"))
431
- return "release";
432
- return "other";
433
- }
434
-
435
- function detectSensitiveScript(command: string): string | null {
436
- const lower = command.toLowerCase();
437
- if (
438
- /(^|[^a-z0-9])(secret|token|password|passwd|api[_-]?key|apikey|credential|credentials|private|internal|private-url|internal-link)([^a-z0-9]|$)/.test(
439
- lower,
440
- )
441
- )
442
- return "protected-name-or-command";
443
- if (/https?:\/\//i.test(command)) return "url";
444
- if (/\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b/i.test(command)) return "email-or-internal-id";
445
- return null;
446
- }
447
-
448
- function createProjectPrivacy(): ProjectPrivacy {
449
- return {
450
- version: 1,
451
- classification: "local-private",
452
- metadataOnly: true,
453
- sourceContentIndex: false,
454
- promptHistoryIndex: false,
455
- shellHistoryIndex: false,
456
- rawCommandOutputIndex: false,
457
- externalUpload: false,
458
- excludedGlobs: DEFAULT_EXCLUDED_GLOBS,
459
- protectedPatterns: PROTECTED_PATTERNS,
460
- };
461
- }
462
-
463
- function createProjectId(projectDir: string): string {
464
- return (
465
- basename(projectDir)
466
- .toLowerCase()
467
- .replace(/[^a-z0-9._-]+/g, "-")
468
- .replace(/^-+|-+$/g, "") || "project"
469
- );
470
- }
471
-
472
- function shouldExcludePath(relativePath: string, isDirectory: boolean): boolean {
473
- const segments = relativePath.split("/");
474
- const name = segments.at(-1) ?? relativePath;
475
- const lowerName = name.toLowerCase();
476
-
477
- if (isDirectory && EXCLUDED_DIRECTORY_NAMES.has(name)) return true;
478
- if (segments.includes(".evodev") && !relativePath.startsWith(".evodev/decisions")) return true;
479
- if (EXCLUDED_FILE_PREFIXES.some((prefix) => name.startsWith(prefix))) return true;
480
- if (EXCLUDED_FILE_PARTS.some((part) => lowerName.includes(part))) return true;
481
- if (EXCLUDED_FILE_EXTENSIONS.some((extension) => lowerName.endsWith(extension))) return true;
482
- return false;
483
- }
484
-
485
- async function pathExists(path: string): Promise<boolean> {
486
- try {
487
- await stat(path);
488
- return true;
489
- } catch (error) {
490
- if (
491
- error instanceof Error &&
492
- "code" in error &&
493
- (error as NodeJS.ErrnoException).code === "ENOENT"
494
- ) {
495
- return false;
496
- }
497
- throw error;
498
- }
499
- }
500
-
501
- function isRecord(value: unknown): value is Record<string, unknown> {
502
- return typeof value === "object" && value !== null && !Array.isArray(value);
503
- }
504
-
505
- function describeError(error: unknown): string {
506
- return error instanceof Error ? error.message : String(error);
507
- }