@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.
Files changed (85) hide show
  1. package/assets/agents/review/code-reviewer/examples.md +1 -1
  2. package/assets/agents/review/code-reviewer/prompt.md +1 -1
  3. package/assets/agents/review/code-reviewer/verification.md +1 -1
  4. package/assets/skills/coding/knowledge-distillation/SKILL.md +251 -0
  5. package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
  6. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
  7. package/assets/team/agents/code-reviewer.md +48 -0
  8. package/assets/team/agents/docs-maintainer.md +51 -0
  9. package/assets/team/agents/implementation-engineer.md +51 -0
  10. package/assets/team/agents/product-scope-analyst.md +58 -0
  11. package/assets/team/agents/release-engineer.md +55 -0
  12. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  13. package/assets/team/agents/solution-architect.md +51 -0
  14. package/assets/team/agents/verification-engineer.md +51 -0
  15. package/assets/team/team.md +102 -0
  16. package/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  17. package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  18. package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  19. package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  20. package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  21. package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  22. package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  23. package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  24. package/dist/config/index.js +1115 -81
  25. package/dist/index.js +13796 -2196
  26. package/dist/plugins/index.js +32 -32
  27. package/package.json +5 -1
  28. package/src/agents/index.ts +63 -292
  29. package/src/code-agent-traces/index.ts +520 -0
  30. package/src/config/index.ts +7 -0
  31. package/src/config/paths.ts +30 -0
  32. package/src/config/settings.ts +201 -0
  33. package/src/config/store.ts +152 -0
  34. package/src/daemon/index.ts +462 -40
  35. package/src/evolution/candidates/index.ts +564 -0
  36. package/src/evolution/control/index.ts +20 -0
  37. package/src/evolution/evidence/analysis.ts +533 -0
  38. package/src/evolution/evidence/index.ts +3 -0
  39. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  40. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  41. package/src/evolution/evidence/session-memory/index.ts +7 -0
  42. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  43. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  44. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  45. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  46. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  47. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  48. package/src/evolution/evidence/session-memory/types.ts +221 -0
  49. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  50. package/src/evolution/formatters.ts +169 -0
  51. package/src/evolution/index.ts +16 -0
  52. package/src/evolution/knowledge/index.ts +5427 -0
  53. package/src/evolution/paths.ts +44 -0
  54. package/src/evolution/processor/distillation.ts +518 -0
  55. package/src/evolution/processor/index.ts +3 -0
  56. package/src/evolution/processor/process.ts +528 -0
  57. package/src/{learning → evolution/review}/index.ts +10 -14
  58. package/src/evolution/schema.ts +568 -0
  59. package/src/evolution/shared.ts +758 -0
  60. package/src/evolution/triggers/classification.ts +102 -0
  61. package/src/evolution/triggers/index.ts +295 -0
  62. package/src/hooks/index.ts +652 -376
  63. package/src/index.ts +16 -3
  64. package/src/pack/index.ts +13 -13
  65. package/src/plugins/capabilities.ts +40 -42
  66. package/src/plugins/index.ts +0 -1
  67. package/src/plugins/types.ts +4 -0
  68. package/src/projects/index.ts +453 -0
  69. package/src/protected-zones/index.ts +29 -11
  70. package/src/runtime-logs/index.ts +790 -0
  71. package/src/sync/orchestrator.ts +6 -0
  72. package/src/team/index.ts +3642 -0
  73. package/src/team/mcp.ts +405 -0
  74. package/src/team/prompts.ts +141 -0
  75. package/src/utils/errors.ts +13 -0
  76. package/src/utils/fs.ts +40 -0
  77. package/src/utils/hash.ts +9 -0
  78. package/src/utils/ids.ts +12 -0
  79. package/src/utils/index.ts +7 -0
  80. package/src/utils/parsing.ts +11 -0
  81. package/src/utils/text.ts +18 -0
  82. package/src/utils/time.ts +5 -0
  83. package/src/workflow/index.ts +6 -24
  84. package/src/project/index.ts +0 -507
  85. package/src/task/index.ts +0 -840
@@ -1,5 +1,12 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import {
3
+ type SessionMemoryPolicySnapshot,
4
+ createDefaultSessionMemoryPolicy,
5
+ parseSessionMemoryPolicy,
6
+ } from "../evolution/evidence/session-memory/index.ts";
1
7
  import { type HookSettings, createDefaultHookSettings, parseHookSettings } from "../hooks/index.ts";
2
8
  import { EvoDevConfigError, describeType } from "./errors.ts";
9
+ import { resolveEvoDevPaths } from "./paths.ts";
3
10
 
4
11
  export interface PluginSettings {
5
12
  enabled: boolean;
@@ -7,6 +14,22 @@ export interface PluginSettings {
7
14
  autoSyncAgents?: boolean;
8
15
  }
9
16
 
17
+ export interface MemorySettings {
18
+ autoAccept: boolean;
19
+ runtimeInjection: boolean;
20
+ staleReview: boolean;
21
+ lexicalIndex: boolean;
22
+ sessionMemory: SessionMemoryPolicySnapshot;
23
+ }
24
+
25
+ export interface EvolutionSettings {
26
+ automation: {
27
+ knowledge: boolean;
28
+ semanticKnowledge: boolean;
29
+ recommendations: boolean;
30
+ };
31
+ }
32
+
10
33
  export interface EvoDevSettings {
11
34
  version: 1;
12
35
  platform: {
@@ -28,6 +51,9 @@ export interface EvoDevSettings {
28
51
  lastRunAt: string | null;
29
52
  };
30
53
  hooks: HookSettings;
54
+ teamRuntime: TeamRuntimeSettings;
55
+ memory: MemorySettings;
56
+ evolution: EvolutionSettings;
31
57
  }
32
58
 
33
59
  export type SettingsInput = Partial<{
@@ -43,8 +69,21 @@ export type SettingsInput = Partial<{
43
69
  }>;
44
70
  doctor: Partial<EvoDevSettings["doctor"]>;
45
71
  hooks: unknown;
72
+ teamRuntime: Partial<TeamRuntimeSettings>;
73
+ memory: Partial<MemorySettings>;
74
+ evolution: Partial<{
75
+ automation: Partial<EvolutionSettings["automation"]>;
76
+ }>;
46
77
  }>;
47
78
 
79
+ export interface TeamRuntimeSettings {
80
+ defaultRuntime: "codex" | "claude";
81
+ defaultModel: string | null;
82
+ defaultThinkingLevel: string | null;
83
+ recordTranscript: boolean;
84
+ displayMode: "normal" | "development";
85
+ }
86
+
48
87
  export function createDefaultSettings(os: string = process.platform): EvoDevSettings {
49
88
  return {
50
89
  version: 1,
@@ -73,6 +112,39 @@ export function createDefaultSettings(os: string = process.platform): EvoDevSett
73
112
  lastRunAt: null,
74
113
  },
75
114
  hooks: createDefaultHookSettings(),
115
+ teamRuntime: createDefaultTeamRuntimeSettings(),
116
+ memory: createDefaultMemorySettings(),
117
+ evolution: createDefaultEvolutionSettings(),
118
+ };
119
+ }
120
+
121
+ export function createDefaultTeamRuntimeSettings(): TeamRuntimeSettings {
122
+ return {
123
+ defaultRuntime: "codex",
124
+ defaultModel: null,
125
+ defaultThinkingLevel: null,
126
+ recordTranscript: false,
127
+ displayMode: "normal",
128
+ };
129
+ }
130
+
131
+ export function createDefaultMemorySettings(): MemorySettings {
132
+ return {
133
+ autoAccept: true,
134
+ runtimeInjection: true,
135
+ staleReview: true,
136
+ lexicalIndex: true,
137
+ sessionMemory: createDefaultSessionMemoryPolicy(),
138
+ };
139
+ }
140
+
141
+ export function createDefaultEvolutionSettings(): EvolutionSettings {
142
+ return {
143
+ automation: {
144
+ knowledge: true,
145
+ semanticKnowledge: false,
146
+ recommendations: false,
147
+ },
76
148
  };
77
149
  }
78
150
 
@@ -112,11 +184,37 @@ export function mergeSettings(
112
184
  ...existing.doctor,
113
185
  },
114
186
  hooks: existing.hooks ?? defaults.hooks,
187
+ teamRuntime: {
188
+ ...defaults.teamRuntime,
189
+ ...existing.teamRuntime,
190
+ },
191
+ memory: {
192
+ ...defaults.memory,
193
+ ...existing.memory,
194
+ },
195
+ evolution: {
196
+ ...defaults.evolution,
197
+ ...existing.evolution,
198
+ automation: {
199
+ ...defaults.evolution.automation,
200
+ ...existing.evolution?.automation,
201
+ },
202
+ },
115
203
  };
116
204
 
117
205
  return parseSettings(merged);
118
206
  }
119
207
 
208
+ export async function readRuntimeInjectionSettings(homeDir?: string): Promise<MemorySettings> {
209
+ const paths = resolveEvoDevPaths(homeDir);
210
+ try {
211
+ return parseSettings(JSON.parse(await readFile(paths.settingsPath, "utf8"))).memory;
212
+ } catch (error) {
213
+ if (isNotFoundError(error)) return createDefaultMemorySettings();
214
+ throw error;
215
+ }
216
+ }
217
+
120
218
  export function parseSettings(value: unknown): EvoDevSettings {
121
219
  const root = expectRecord(value, "settings");
122
220
  const version = root.version;
@@ -157,11 +255,104 @@ export function parseSettings(value: unknown): EvoDevSettings {
157
255
  lastRunAt: expectNullableString(doctor.lastRunAt, "settings.doctor.lastRunAt"),
158
256
  },
159
257
  hooks: parseHookSettings(root.hooks),
258
+ teamRuntime: parseTeamRuntimeSettings(
259
+ root.teamRuntime ?? createDefaultTeamRuntimeSettings(),
260
+ "settings.teamRuntime",
261
+ ),
262
+ memory: parseMemorySettings(root.memory ?? createDefaultMemorySettings(), "settings.memory"),
263
+ evolution: parseEvolutionSettings(
264
+ root.evolution ?? createDefaultEvolutionSettings(),
265
+ "settings.evolution",
266
+ ),
160
267
  };
161
268
 
162
269
  return parsed;
163
270
  }
164
271
 
272
+ function parseEvolutionSettings(value: unknown, path: string): EvolutionSettings {
273
+ const input = expectRecord(value, path);
274
+ const defaults = createDefaultEvolutionSettings();
275
+ const automation = expectRecord(input.automation ?? defaults.automation, `${path}.automation`);
276
+ return {
277
+ automation: {
278
+ knowledge:
279
+ automation.knowledge === undefined
280
+ ? defaults.automation.knowledge
281
+ : expectBoolean(automation.knowledge, `${path}.automation.knowledge`),
282
+ semanticKnowledge:
283
+ automation.semanticKnowledge === undefined
284
+ ? defaults.automation.semanticKnowledge
285
+ : expectBoolean(automation.semanticKnowledge, `${path}.automation.semanticKnowledge`),
286
+ recommendations:
287
+ automation.recommendations === undefined
288
+ ? defaults.automation.recommendations
289
+ : expectBoolean(automation.recommendations, `${path}.automation.recommendations`),
290
+ },
291
+ };
292
+ }
293
+
294
+ function parseMemorySettings(value: unknown, path: string): MemorySettings {
295
+ const input = expectRecord(value, path);
296
+ const defaults = createDefaultMemorySettings();
297
+ return {
298
+ autoAccept:
299
+ input.autoAccept === undefined
300
+ ? defaults.autoAccept
301
+ : expectBoolean(input.autoAccept, `${path}.autoAccept`),
302
+ runtimeInjection:
303
+ input.runtimeInjection === undefined
304
+ ? defaults.runtimeInjection
305
+ : expectBoolean(input.runtimeInjection, `${path}.runtimeInjection`),
306
+ staleReview:
307
+ input.staleReview === undefined
308
+ ? defaults.staleReview
309
+ : expectBoolean(input.staleReview, `${path}.staleReview`),
310
+ lexicalIndex:
311
+ input.lexicalIndex === undefined
312
+ ? defaults.lexicalIndex
313
+ : expectBoolean(input.lexicalIndex, `${path}.lexicalIndex`),
314
+ sessionMemory: parseSessionMemoryPolicy(
315
+ isPlainRecord(input.sessionMemory) ? input.sessionMemory : defaults.sessionMemory,
316
+ ),
317
+ };
318
+ }
319
+
320
+ function parseTeamRuntimeSettings(value: unknown, path: string): TeamRuntimeSettings {
321
+ const input = expectRecord(value, path);
322
+ const defaults = createDefaultTeamRuntimeSettings();
323
+ const defaultRuntime = input.defaultRuntime ?? defaults.defaultRuntime;
324
+ if (defaultRuntime !== "codex" && defaultRuntime !== "claude") {
325
+ throw new EvoDevConfigError(`Invalid ${path}.defaultRuntime; expected codex or claude`);
326
+ }
327
+
328
+ return {
329
+ defaultRuntime,
330
+ defaultModel:
331
+ input.defaultModel === undefined
332
+ ? defaults.defaultModel
333
+ : expectNullableString(input.defaultModel, `${path}.defaultModel`),
334
+ defaultThinkingLevel:
335
+ input.defaultThinkingLevel === undefined
336
+ ? defaults.defaultThinkingLevel
337
+ : expectNullableString(input.defaultThinkingLevel, `${path}.defaultThinkingLevel`),
338
+ recordTranscript:
339
+ input.recordTranscript === undefined
340
+ ? defaults.recordTranscript
341
+ : expectBoolean(input.recordTranscript, `${path}.recordTranscript`),
342
+ displayMode: parseTeamRuntimeDisplayMode(input.displayMode, defaults.displayMode, path),
343
+ };
344
+ }
345
+
346
+ function parseTeamRuntimeDisplayMode(
347
+ value: unknown,
348
+ fallback: TeamRuntimeSettings["displayMode"],
349
+ path: string,
350
+ ): TeamRuntimeSettings["displayMode"] {
351
+ if (value === undefined) return fallback;
352
+ if (value === "normal" || value === "development") return value;
353
+ throw new EvoDevConfigError(`Invalid ${path}.displayMode; expected normal or development`);
354
+ }
355
+
165
356
  function parsePluginSettings(value: unknown, path: string): PluginSettings {
166
357
  const input = expectRecord(value, path);
167
358
  const parsed: PluginSettings = {
@@ -187,6 +378,16 @@ function expectRecord(value: unknown, path: string): Record<string, unknown> {
187
378
  return value as Record<string, unknown>;
188
379
  }
189
380
 
381
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
382
+ return typeof value === "object" && value !== null && !Array.isArray(value);
383
+ }
384
+
385
+ function isNotFoundError(error: unknown): boolean {
386
+ return (
387
+ error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
388
+ );
389
+ }
390
+
190
391
  function expectString(value: unknown, path: string): string {
191
392
  if (typeof value !== "string" || value.length === 0) {
192
393
  throw new EvoDevConfigError(`Invalid ${path}; expected non-empty string`);
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
+ import { ensureOkfKnowledgeBase } from "../evolution/knowledge/index.ts";
3
4
  import { EvoDevConfigError } from "./errors.ts";
4
5
  import { type EvoDevPaths, resolveEvoDevPaths } from "./paths.ts";
5
6
  import { type EvoDevRegistry, createDefaultRegistry, parseRegistry } from "./registry.ts";
@@ -22,6 +23,7 @@ import {
22
23
  export interface CoreConfigStore {
23
24
  readonly paths: EvoDevPaths;
24
25
  ensureBaseDirs(): Promise<void>;
26
+ ensureKnowledgeBase(): Promise<void>;
25
27
  readSettings(): Promise<EvoDevSettings>;
26
28
  writeSettings(settings: EvoDevSettings): Promise<void>;
27
29
  mergeAndWriteSettings(input: SettingsInput): Promise<EvoDevSettings>;
@@ -40,6 +42,15 @@ export function createCoreConfigStore(homeDir?: string): CoreConfigStore {
40
42
  paths,
41
43
  async ensureBaseDirs() {
42
44
  await mkdir(paths.stateDir, { recursive: true });
45
+ await mkdir(paths.logsDir, { recursive: true });
46
+ await mkdir(paths.knowledgeDir, { recursive: true });
47
+ await mkdir(paths.evosCasesDir, { recursive: true });
48
+ await mkdir(paths.roleAgentsDir, { recursive: true });
49
+ await mkdir(paths.teamsDir, { recursive: true });
50
+ await mkdir(paths.runsDir, { recursive: true });
51
+ },
52
+ async ensureKnowledgeBase() {
53
+ await ensureKnowledgeBaseFiles(paths);
43
54
  },
44
55
  async readSettings() {
45
56
  return readJsonFile(paths.settingsPath, parseSettings);
@@ -81,6 +92,7 @@ export function createCoreConfigStore(homeDir?: string): CoreConfigStore {
81
92
  export async function initializeCoreConfig(homeDir?: string): Promise<CoreConfigStore> {
82
93
  const store = createCoreConfigStore(homeDir);
83
94
  await store.ensureBaseDirs();
95
+ await store.ensureKnowledgeBase();
84
96
  await writeIfMissing(store.paths.settingsPath, createDefaultSettings());
85
97
  await writeIfMissing(store.paths.registryPath, createDefaultRegistry());
86
98
  await writeIfMissing(store.paths.installStatePath, createDefaultInstallState());
@@ -88,6 +100,88 @@ export async function initializeCoreConfig(homeDir?: string): Promise<CoreConfig
88
100
  return store;
89
101
  }
90
102
 
103
+ export async function ensureKnowledgeBaseFiles(paths: EvoDevPaths): Promise<void> {
104
+ await mkdir(paths.knowledgeDir, { recursive: true });
105
+ await mkdir(paths.evosCasesDir, { recursive: true });
106
+ await ensureOkfKnowledgeBase(paths.homeDir);
107
+ await writeTextIfMissing(
108
+ `${paths.knowledgeDir}/README.md`,
109
+ [
110
+ "# EvoDev Knowledge",
111
+ "",
112
+ "Local-private knowledge base for user-accepted facts, decisions, architecture notes, and reusable domain context.",
113
+ "",
114
+ "EvoDev must not populate this directory from source code, prompts, command output, logs, or transcripts without an explicit consent flow.",
115
+ "",
116
+ ].join("\n"),
117
+ );
118
+ await writeIndexIfMissingOrMigrate(paths.knowledgeIndexPath, "knowledge-index", {
119
+ version: 1,
120
+ kind: "knowledge-index",
121
+ roleTags: [],
122
+ entries: [],
123
+ });
124
+ await writeTextIfMissing(
125
+ `${paths.evosDir}/README.md`,
126
+ [
127
+ "# EvoDev Evos",
128
+ "",
129
+ "Local-private evolution case library for reviewed improvement cases and reusable process changes.",
130
+ "",
131
+ "Cases start empty. Future automation may propose candidates, but accepted evos require explicit review before they can influence workflows or routing.",
132
+ "",
133
+ ].join("\n"),
134
+ );
135
+ await writeTextIfMissing(
136
+ `${paths.evosCasesDir}/README.md`,
137
+ [
138
+ "# Evolution Cases",
139
+ "",
140
+ "Store one reviewed evolution case per file. Do not store raw prompts, source dumps, secrets, transcripts, or raw command output here.",
141
+ "",
142
+ ].join("\n"),
143
+ );
144
+ await writeIndexIfMissingOrMigrate(paths.evosIndexPath, "evos-index", {
145
+ version: 1,
146
+ kind: "evos-index",
147
+ roleTags: [],
148
+ cases: [],
149
+ });
150
+ await writeTextIfMissing(
151
+ `${paths.roleAgentsDir}/README.md`,
152
+ [
153
+ "# Role Agents",
154
+ "",
155
+ "Local-private role agent registry for EvoDev-managed agent roles and user-reviewed role extensions.",
156
+ "",
157
+ "Repository-specific role agents should be proposed first and written into a user repository only after explicit project opt-in.",
158
+ "",
159
+ ].join("\n"),
160
+ );
161
+ await writeIndexIfMissingOrMigrate(paths.roleAgentsIndexPath, "role-agent-index", {
162
+ version: 1,
163
+ kind: "role-agent-index",
164
+ roles: [],
165
+ projectExtensions: [],
166
+ });
167
+ await writeTextIfMissing(
168
+ `${paths.teamsDir}/README.md`,
169
+ [
170
+ "# Agent Teams",
171
+ "",
172
+ "Local-private EvoHub team registry for reviewed role-agent team definitions.",
173
+ "",
174
+ "Teams may reference role agents and role-tagged knowledge, but they must not contain raw source, prompts, transcripts, secrets, or raw command output.",
175
+ "",
176
+ ].join("\n"),
177
+ );
178
+ await writeIndexIfMissingOrMigrate(paths.teamsIndexPath, "agent-team-index", {
179
+ version: 1,
180
+ kind: "agent-team-index",
181
+ teams: [],
182
+ });
183
+ }
184
+
91
185
  async function readJsonFile<T>(filePath: string, parse: (value: unknown) => T): Promise<T> {
92
186
  let raw: string;
93
187
 
@@ -148,6 +242,60 @@ async function writeIfMissing(filePath: string, value: unknown): Promise<void> {
148
242
  }
149
243
  }
150
244
 
245
+ async function writeIndexIfMissingOrMigrate(
246
+ filePath: string,
247
+ kind: string,
248
+ defaults: Record<string, unknown>,
249
+ ): Promise<void> {
250
+ let raw: string;
251
+ try {
252
+ raw = await readFile(filePath, "utf8");
253
+ } catch (error) {
254
+ if (isNodeError(error) && error.code === "ENOENT") {
255
+ await writeJsonFile(filePath, defaults);
256
+ return;
257
+ }
258
+
259
+ throw new EvoDevConfigError(
260
+ `Cannot inspect config file (${describeFileError(error)})`,
261
+ filePath,
262
+ );
263
+ }
264
+
265
+ let existing: unknown;
266
+ try {
267
+ existing = JSON.parse(raw);
268
+ } catch (error) {
269
+ throw new EvoDevConfigError(
270
+ `Invalid bootstrap index JSON (${describeFileError(error)})`,
271
+ filePath,
272
+ );
273
+ }
274
+
275
+ if (!isRecord(existing) || existing.kind !== kind) return;
276
+
277
+ const migrated = { ...defaults, ...existing };
278
+ if (Object.keys(defaults).every((key) => key in existing)) return;
279
+ await writeJsonFile(filePath, migrated);
280
+ }
281
+
282
+ async function writeTextIfMissing(filePath: string, value: string): Promise<void> {
283
+ try {
284
+ await readFile(filePath, "utf8");
285
+ } catch (error) {
286
+ if (isNodeError(error) && error.code === "ENOENT") {
287
+ await mkdir(dirname(filePath), { recursive: true });
288
+ await writeFile(filePath, value, "utf8");
289
+ return;
290
+ }
291
+
292
+ throw new EvoDevConfigError(
293
+ `Cannot inspect config file (${describeFileError(error)})`,
294
+ filePath,
295
+ );
296
+ }
297
+ }
298
+
151
299
  async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
152
300
  await mkdir(dirname(filePath), { recursive: true });
153
301
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
@@ -164,3 +312,7 @@ function describeFileError(error: unknown): string {
164
312
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
165
313
  return error instanceof Error && "code" in error;
166
314
  }
315
+
316
+ function isRecord(value: unknown): value is Record<string, unknown> {
317
+ return typeof value === "object" && value !== null && !Array.isArray(value);
318
+ }