@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.
Files changed (51) hide show
  1. package/assets/agents/review/code-reviewer/examples.md +19 -0
  2. package/assets/agents/review/code-reviewer/manifest.json +10 -0
  3. package/assets/agents/review/code-reviewer/prompt.md +59 -0
  4. package/assets/agents/review/code-reviewer/verification.md +11 -0
  5. package/assets/skills/coding/engineering-discipline/SKILL.md +63 -0
  6. package/assets/skills/coding/engineering-discipline/anti-patterns.md +21 -0
  7. package/assets/skills/coding/engineering-discipline/examples.md +19 -0
  8. package/assets/skills/coding/engineering-discipline/manifest.json +10 -0
  9. package/assets/skills/coding/engineering-discipline/verification.md +11 -0
  10. package/assets/workflows/rd-bug-fix/WORKFLOW.json +45 -0
  11. package/assets/workflows/rd-code-review/WORKFLOW.json +45 -0
  12. package/assets/workflows/rd-docs-update/WORKFLOW.json +45 -0
  13. package/assets/workflows/rd-feature-implementation/WORKFLOW.json +45 -0
  14. package/assets/workflows/rd-refactor/WORKFLOW.json +45 -0
  15. package/assets/workflows/rd-release-readiness/WORKFLOW.json +49 -0
  16. package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +45 -0
  17. package/assets/workflows/rd-test-generation/WORKFLOW.json +45 -0
  18. package/dist/assets/index.js +209 -0
  19. package/dist/config/index.js +601 -0
  20. package/dist/index.js +4879 -0
  21. package/dist/plugins/index.js +265 -0
  22. package/package.json +30 -0
  23. package/src/.gitkeep +0 -0
  24. package/src/agents/index.ts +561 -0
  25. package/src/assets/errors.ts +21 -0
  26. package/src/assets/index.ts +18 -0
  27. package/src/assets/manifest.ts +109 -0
  28. package/src/assets/scanner.ts +189 -0
  29. package/src/config/errors.ts +21 -0
  30. package/src/config/index.ts +26 -0
  31. package/src/config/paths.ts +43 -0
  32. package/src/config/registry.ts +84 -0
  33. package/src/config/settings.ts +212 -0
  34. package/src/config/state.ts +130 -0
  35. package/src/config/store.ts +166 -0
  36. package/src/daemon/index.ts +414 -0
  37. package/src/hooks/index.ts +1023 -0
  38. package/src/index.ts +14 -0
  39. package/src/learning/index.ts +714 -0
  40. package/src/observability/index.ts +272 -0
  41. package/src/pack/index.ts +779 -0
  42. package/src/plugins/capabilities.ts +347 -0
  43. package/src/plugins/index.ts +41 -0
  44. package/src/plugins/registry.ts +60 -0
  45. package/src/plugins/types.ts +123 -0
  46. package/src/project/index.ts +507 -0
  47. package/src/protected-zones/index.ts +137 -0
  48. package/src/sync/index.ts +7 -0
  49. package/src/sync/orchestrator.ts +298 -0
  50. package/src/task/index.ts +840 -0
  51. package/src/workflow/index.ts +137 -0
@@ -0,0 +1,189 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import { EvoDevAssetError } from "./errors.ts";
4
+ import {
5
+ type AgentManifest,
6
+ type SkillManifest,
7
+ parseAgentManifest,
8
+ parseSkillManifest,
9
+ } from "./manifest.ts";
10
+
11
+ export interface AssetScannerPaths {
12
+ skillsDir: string;
13
+ agentsDir: string;
14
+ }
15
+
16
+ export interface ScannedAsset<TManifest extends SkillManifest | AgentManifest> {
17
+ manifest: TManifest;
18
+ manifestPath: string;
19
+ entryPath: string;
20
+ assetDir: string;
21
+ registryKey: string;
22
+ }
23
+
24
+ export interface AssetScanResult {
25
+ skills: ScannedAsset<SkillManifest>[];
26
+ agents: ScannedAsset<AgentManifest>[];
27
+ }
28
+
29
+ export async function scanAssets(paths: AssetScannerPaths): Promise<AssetScanResult> {
30
+ const [skills, agents] = await Promise.all([
31
+ scanSkillAssets(paths.skillsDir),
32
+ scanAgentAssets(paths.agentsDir),
33
+ ]);
34
+
35
+ return { skills, agents };
36
+ }
37
+
38
+ export async function scanSkillAssets(skillsDir: string): Promise<ScannedAsset<SkillManifest>[]> {
39
+ return scanAssetKind(skillsDir, parseSkillManifest);
40
+ }
41
+
42
+ export async function scanAgentAssets(agentsDir: string): Promise<ScannedAsset<AgentManifest>[]> {
43
+ return scanAssetKind(agentsDir, parseAgentManifest);
44
+ }
45
+
46
+ async function scanAssetKind<TManifest extends SkillManifest | AgentManifest>(
47
+ rootDir: string,
48
+ parseManifest: (value: unknown) => TManifest,
49
+ ): Promise<ScannedAsset<TManifest>[]> {
50
+ if (!(await pathExists(rootDir))) {
51
+ return [];
52
+ }
53
+
54
+ const manifestPaths = await findManifestFiles(rootDir);
55
+ const assets = await Promise.all(
56
+ manifestPaths.map((manifestPath) => readScannedAsset(rootDir, manifestPath, parseManifest)),
57
+ );
58
+
59
+ return assets.sort((left, right) => left.registryKey.localeCompare(right.registryKey));
60
+ }
61
+
62
+ async function findManifestFiles(rootDir: string): Promise<string[]> {
63
+ const entries = await readdir(rootDir, { withFileTypes: true });
64
+ const nested = await Promise.all(
65
+ entries.map(async (entry) => {
66
+ const entryPath = join(rootDir, entry.name);
67
+
68
+ if (entry.isDirectory()) {
69
+ return findManifestFiles(entryPath);
70
+ }
71
+
72
+ if (entry.isFile() && entry.name === "manifest.json") {
73
+ return [entryPath];
74
+ }
75
+
76
+ return [];
77
+ }),
78
+ );
79
+
80
+ return nested.flat();
81
+ }
82
+
83
+ async function readScannedAsset<TManifest extends SkillManifest | AgentManifest>(
84
+ rootDir: string,
85
+ manifestPath: string,
86
+ parseManifest: (value: unknown) => TManifest,
87
+ ): Promise<ScannedAsset<TManifest>> {
88
+ const json = await readManifestJson(manifestPath);
89
+ const manifest = parseManifestWithPath(json, manifestPath, parseManifest);
90
+ const assetDir = manifestPath.slice(0, -"/manifest.json".length);
91
+ const entryPath = join(assetDir, manifest.entry);
92
+
93
+ if (!(await isFile(entryPath))) {
94
+ throw new EvoDevAssetError(`Asset entry file not found for ${manifest.id}`, entryPath);
95
+ }
96
+
97
+ const registryKey = createRegistryKey(rootDir, assetDir, manifest);
98
+ return {
99
+ manifest,
100
+ manifestPath,
101
+ entryPath,
102
+ assetDir,
103
+ registryKey,
104
+ };
105
+ }
106
+
107
+ async function readManifestJson(manifestPath: string): Promise<unknown> {
108
+ let raw: string;
109
+
110
+ try {
111
+ raw = await readFile(manifestPath, "utf8");
112
+ } catch (error) {
113
+ throw new EvoDevAssetError(`Cannot read manifest (${describeFileError(error)})`, manifestPath);
114
+ }
115
+
116
+ try {
117
+ return JSON.parse(raw);
118
+ } catch (error) {
119
+ throw new EvoDevAssetError(`Invalid manifest JSON (${describeFileError(error)})`, manifestPath);
120
+ }
121
+ }
122
+
123
+ function parseManifestWithPath<TManifest extends SkillManifest | AgentManifest>(
124
+ json: unknown,
125
+ manifestPath: string,
126
+ parseManifest: (value: unknown) => TManifest,
127
+ ): TManifest {
128
+ try {
129
+ return parseManifest(json);
130
+ } catch (error) {
131
+ if (error instanceof EvoDevAssetError) {
132
+ throw new EvoDevAssetError(error.message, manifestPath);
133
+ }
134
+
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ function createRegistryKey(
140
+ rootDir: string,
141
+ assetDir: string,
142
+ manifest: SkillManifest | AgentManifest,
143
+ ): string {
144
+ const relativeDir = relative(rootDir, assetDir).replaceAll("\\", "/");
145
+
146
+ if (relativeDir === "" || relativeDir === manifest.id) {
147
+ return manifest.id;
148
+ }
149
+
150
+ return relativeDir;
151
+ }
152
+
153
+ async function pathExists(path: string): Promise<boolean> {
154
+ try {
155
+ await stat(path);
156
+ return true;
157
+ } catch (error) {
158
+ if (isNodeError(error) && error.code === "ENOENT") {
159
+ return false;
160
+ }
161
+
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ async function isFile(path: string): Promise<boolean> {
167
+ try {
168
+ const fileStat = await stat(path);
169
+ return fileStat.isFile();
170
+ } catch (error) {
171
+ if (isNodeError(error) && error.code === "ENOENT") {
172
+ return false;
173
+ }
174
+
175
+ throw error;
176
+ }
177
+ }
178
+
179
+ function describeFileError(error: unknown): string {
180
+ if (error instanceof Error) {
181
+ return error.message;
182
+ }
183
+
184
+ return String(error);
185
+ }
186
+
187
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
188
+ return error instanceof Error && "code" in error;
189
+ }
@@ -0,0 +1,21 @@
1
+ export class EvoDevConfigError extends Error {
2
+ constructor(
3
+ message: string,
4
+ readonly filePath?: string,
5
+ ) {
6
+ super(filePath ? `${message}: ${filePath}` : message);
7
+ this.name = "EvoDevConfigError";
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,26 @@
1
+ export { EvoDevConfigError } from "./errors.ts";
2
+ export { type EvoDevPaths, resolveEvoDevPaths } from "./paths.ts";
3
+ export {
4
+ type EvoDevRegistry,
5
+ type RegisteredAsset,
6
+ createDefaultRegistry,
7
+ parseRegistry,
8
+ } from "./registry.ts";
9
+ export {
10
+ type EvoDevSettings,
11
+ type PluginSettings,
12
+ type SettingsInput,
13
+ createDefaultSettings,
14
+ mergeSettings,
15
+ parseSettings,
16
+ } from "./settings.ts";
17
+ export {
18
+ type InstallState,
19
+ type PluginSyncState,
20
+ type SyncState,
21
+ createDefaultInstallState,
22
+ createDefaultSyncState,
23
+ parseInstallState,
24
+ parseSyncState,
25
+ } from "./state.ts";
26
+ export { type CoreConfigStore, createCoreConfigStore, initializeCoreConfig } from "./store.ts";
@@ -0,0 +1,43 @@
1
+ export interface EvoDevPaths {
2
+ homeDir: string;
3
+ rootDir: string;
4
+ settingsPath: string;
5
+ registryPath: string;
6
+ stateDir: string;
7
+ installStatePath: string;
8
+ syncStatePath: string;
9
+ }
10
+
11
+ export function resolveEvoDevPaths(homeDir: string = getHomeDir()): EvoDevPaths {
12
+ const normalizedHome = stripTrailingSlash(homeDir);
13
+ const rootDir = `${normalizedHome}/.evodev`;
14
+ const stateDir = `${rootDir}/state`;
15
+
16
+ return {
17
+ homeDir: normalizedHome,
18
+ rootDir,
19
+ settingsPath: `${rootDir}/settings.json`,
20
+ registryPath: `${rootDir}/registry.json`,
21
+ stateDir,
22
+ installStatePath: `${stateDir}/install.json`,
23
+ syncStatePath: `${stateDir}/sync.json`,
24
+ };
25
+ }
26
+
27
+ function getHomeDir(): string {
28
+ const home = process.env.HOME;
29
+
30
+ if (home === undefined || home.trim() === "") {
31
+ throw new Error("Cannot resolve home directory: HOME is not set");
32
+ }
33
+
34
+ return home;
35
+ }
36
+
37
+ function stripTrailingSlash(path: string): string {
38
+ if (path === "/") {
39
+ return path;
40
+ }
41
+
42
+ return path.replace(/\/+$/, "");
43
+ }
@@ -0,0 +1,84 @@
1
+ import { EvoDevConfigError, describeType } from "./errors.ts";
2
+
3
+ export interface RegisteredAsset {
4
+ version: string;
5
+ source: "builtin" | "user";
6
+ targets: string[];
7
+ }
8
+
9
+ export interface EvoDevRegistry {
10
+ version: 1;
11
+ skills: Record<string, RegisteredAsset>;
12
+ agents: Record<string, RegisteredAsset>;
13
+ }
14
+
15
+ export function createDefaultRegistry(): EvoDevRegistry {
16
+ return {
17
+ version: 1,
18
+ skills: {},
19
+ agents: {},
20
+ };
21
+ }
22
+
23
+ export function parseRegistry(value: unknown): EvoDevRegistry {
24
+ const root = expectRecord(value, "registry");
25
+
26
+ if (root.version !== 1) {
27
+ throw new EvoDevConfigError("Invalid registry.version; expected 1");
28
+ }
29
+
30
+ return {
31
+ version: 1,
32
+ skills: parseAssetMap(root.skills, "registry.skills"),
33
+ agents: parseAssetMap(root.agents, "registry.agents"),
34
+ };
35
+ }
36
+
37
+ function parseAssetMap(value: unknown, path: string): Record<string, RegisteredAsset> {
38
+ const input = expectRecord(value, path);
39
+ const output: Record<string, RegisteredAsset> = {};
40
+
41
+ for (const [assetId, asset] of Object.entries(input)) {
42
+ output[assetId] = parseRegisteredAsset(asset, `${path}.${assetId}`);
43
+ }
44
+
45
+ return output;
46
+ }
47
+
48
+ function parseRegisteredAsset(value: unknown, path: string): RegisteredAsset {
49
+ const input = expectRecord(value, path);
50
+ const source = input.source;
51
+
52
+ if (source !== "builtin" && source !== "user") {
53
+ throw new EvoDevConfigError(`Invalid ${path}.source; expected builtin or user`);
54
+ }
55
+
56
+ if (
57
+ !Array.isArray(input.targets) ||
58
+ !input.targets.every((target) => typeof target === "string")
59
+ ) {
60
+ throw new EvoDevConfigError(`Invalid ${path}.targets; expected string array`);
61
+ }
62
+
63
+ return {
64
+ version: expectString(input.version, `${path}.version`),
65
+ source,
66
+ targets: [...input.targets],
67
+ };
68
+ }
69
+
70
+ function expectRecord(value: unknown, path: string): Record<string, unknown> {
71
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
72
+ throw new EvoDevConfigError(`Invalid ${path}; expected object, got ${describeType(value)}`);
73
+ }
74
+
75
+ return value as Record<string, unknown>;
76
+ }
77
+
78
+ function expectString(value: unknown, path: string): string {
79
+ if (typeof value !== "string" || value.length === 0) {
80
+ throw new EvoDevConfigError(`Invalid ${path}; expected non-empty string`);
81
+ }
82
+
83
+ return value;
84
+ }
@@ -0,0 +1,212 @@
1
+ import { type HookSettings, createDefaultHookSettings, parseHookSettings } from "../hooks/index.ts";
2
+ import { EvoDevConfigError, describeType } from "./errors.ts";
3
+
4
+ export interface PluginSettings {
5
+ enabled: boolean;
6
+ autoSyncSkills?: boolean;
7
+ autoSyncAgents?: boolean;
8
+ }
9
+
10
+ export interface EvoDevSettings {
11
+ version: 1;
12
+ platform: {
13
+ os: string;
14
+ };
15
+ plugins: {
16
+ claude: PluginSettings;
17
+ codex: PluginSettings;
18
+ };
19
+ assets: {
20
+ skills: {
21
+ enabled: boolean;
22
+ };
23
+ agents: {
24
+ enabled: boolean;
25
+ };
26
+ };
27
+ doctor: {
28
+ lastRunAt: string | null;
29
+ };
30
+ hooks: HookSettings;
31
+ }
32
+
33
+ export type SettingsInput = Partial<{
34
+ version: number;
35
+ platform: Partial<EvoDevSettings["platform"]>;
36
+ plugins: Partial<{
37
+ claude: Partial<PluginSettings>;
38
+ codex: Partial<PluginSettings>;
39
+ }>;
40
+ assets: Partial<{
41
+ skills: Partial<EvoDevSettings["assets"]["skills"]>;
42
+ agents: Partial<EvoDevSettings["assets"]["agents"]>;
43
+ }>;
44
+ doctor: Partial<EvoDevSettings["doctor"]>;
45
+ hooks: unknown;
46
+ }>;
47
+
48
+ export function createDefaultSettings(os: string = process.platform): EvoDevSettings {
49
+ return {
50
+ version: 1,
51
+ platform: {
52
+ os,
53
+ },
54
+ plugins: {
55
+ claude: {
56
+ enabled: true,
57
+ autoSyncSkills: true,
58
+ autoSyncAgents: true,
59
+ },
60
+ codex: {
61
+ enabled: false,
62
+ },
63
+ },
64
+ assets: {
65
+ skills: {
66
+ enabled: true,
67
+ },
68
+ agents: {
69
+ enabled: true,
70
+ },
71
+ },
72
+ doctor: {
73
+ lastRunAt: null,
74
+ },
75
+ hooks: createDefaultHookSettings(),
76
+ };
77
+ }
78
+
79
+ export function mergeSettings(
80
+ existing: SettingsInput,
81
+ defaults: EvoDevSettings = createDefaultSettings(),
82
+ ): EvoDevSettings {
83
+ const merged = {
84
+ ...defaults,
85
+ ...existing,
86
+ platform: {
87
+ ...defaults.platform,
88
+ ...existing.platform,
89
+ },
90
+ plugins: {
91
+ claude: {
92
+ ...defaults.plugins.claude,
93
+ ...existing.plugins?.claude,
94
+ },
95
+ codex: {
96
+ ...defaults.plugins.codex,
97
+ ...existing.plugins?.codex,
98
+ },
99
+ },
100
+ assets: {
101
+ skills: {
102
+ ...defaults.assets.skills,
103
+ ...existing.assets?.skills,
104
+ },
105
+ agents: {
106
+ ...defaults.assets.agents,
107
+ ...existing.assets?.agents,
108
+ },
109
+ },
110
+ doctor: {
111
+ ...defaults.doctor,
112
+ ...existing.doctor,
113
+ },
114
+ hooks: existing.hooks ?? defaults.hooks,
115
+ };
116
+
117
+ return parseSettings(merged);
118
+ }
119
+
120
+ export function parseSettings(value: unknown): EvoDevSettings {
121
+ const root = expectRecord(value, "settings");
122
+ const version = root.version;
123
+
124
+ if (version !== 1) {
125
+ throw new EvoDevConfigError("Invalid settings.version; expected 1");
126
+ }
127
+
128
+ const platform = expectRecord(root.platform, "settings.platform");
129
+ const plugins = expectRecord(root.plugins, "settings.plugins");
130
+ const assets = expectRecord(root.assets, "settings.assets");
131
+ const doctor = expectRecord(root.doctor, "settings.doctor");
132
+
133
+ const parsed: EvoDevSettings = {
134
+ version: 1,
135
+ platform: {
136
+ os: expectString(platform.os, "settings.platform.os"),
137
+ },
138
+ plugins: {
139
+ claude: parsePluginSettings(plugins.claude, "settings.plugins.claude"),
140
+ codex: parsePluginSettings(plugins.codex, "settings.plugins.codex"),
141
+ },
142
+ assets: {
143
+ skills: {
144
+ enabled: expectBoolean(
145
+ expectRecord(assets.skills, "settings.assets.skills").enabled,
146
+ "settings.assets.skills.enabled",
147
+ ),
148
+ },
149
+ agents: {
150
+ enabled: expectBoolean(
151
+ expectRecord(assets.agents, "settings.assets.agents").enabled,
152
+ "settings.assets.agents.enabled",
153
+ ),
154
+ },
155
+ },
156
+ doctor: {
157
+ lastRunAt: expectNullableString(doctor.lastRunAt, "settings.doctor.lastRunAt"),
158
+ },
159
+ hooks: parseHookSettings(root.hooks),
160
+ };
161
+
162
+ return parsed;
163
+ }
164
+
165
+ function parsePluginSettings(value: unknown, path: string): PluginSettings {
166
+ const input = expectRecord(value, path);
167
+ const parsed: PluginSettings = {
168
+ enabled: expectBoolean(input.enabled, `${path}.enabled`),
169
+ };
170
+
171
+ if (input.autoSyncSkills !== undefined) {
172
+ parsed.autoSyncSkills = expectBoolean(input.autoSyncSkills, `${path}.autoSyncSkills`);
173
+ }
174
+
175
+ if (input.autoSyncAgents !== undefined) {
176
+ parsed.autoSyncAgents = expectBoolean(input.autoSyncAgents, `${path}.autoSyncAgents`);
177
+ }
178
+
179
+ return parsed;
180
+ }
181
+
182
+ function expectRecord(value: unknown, path: string): Record<string, unknown> {
183
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
184
+ throw new EvoDevConfigError(`Invalid ${path}; expected object, got ${describeType(value)}`);
185
+ }
186
+
187
+ return value as Record<string, unknown>;
188
+ }
189
+
190
+ function expectString(value: unknown, path: string): string {
191
+ if (typeof value !== "string" || value.length === 0) {
192
+ throw new EvoDevConfigError(`Invalid ${path}; expected non-empty string`);
193
+ }
194
+
195
+ return value;
196
+ }
197
+
198
+ function expectNullableString(value: unknown, path: string): string | null {
199
+ if (value === null || typeof value === "string") {
200
+ return value;
201
+ }
202
+
203
+ throw new EvoDevConfigError(`Invalid ${path}; expected string or null`);
204
+ }
205
+
206
+ function expectBoolean(value: unknown, path: string): boolean {
207
+ if (typeof value !== "boolean") {
208
+ throw new EvoDevConfigError(`Invalid ${path}; expected boolean`);
209
+ }
210
+
211
+ return value;
212
+ }
@@ -0,0 +1,130 @@
1
+ import { EvoDevConfigError, describeType } from "./errors.ts";
2
+
3
+ export interface InstallState {
4
+ version: 1;
5
+ initializedAt: string | null;
6
+ initializedBy: "evodev";
7
+ selectedPlugins: string[];
8
+ }
9
+
10
+ export interface PluginSyncState {
11
+ skills: number;
12
+ agents: number;
13
+ status: "success" | "partial" | "failed" | "never-run";
14
+ }
15
+
16
+ export interface SyncState {
17
+ version: 1;
18
+ lastSyncAt: string | null;
19
+ targets: Record<string, PluginSyncState>;
20
+ }
21
+
22
+ export function createDefaultInstallState(): InstallState {
23
+ return {
24
+ version: 1,
25
+ initializedAt: null,
26
+ initializedBy: "evodev",
27
+ selectedPlugins: [],
28
+ };
29
+ }
30
+
31
+ export function createDefaultSyncState(): SyncState {
32
+ return {
33
+ version: 1,
34
+ lastSyncAt: null,
35
+ targets: {},
36
+ };
37
+ }
38
+
39
+ export function parseInstallState(value: unknown): InstallState {
40
+ const root = expectRecord(value, "install state");
41
+
42
+ if (root.version !== 1) {
43
+ throw new EvoDevConfigError("Invalid install state.version; expected 1");
44
+ }
45
+
46
+ if (root.initializedBy !== "evodev") {
47
+ throw new EvoDevConfigError("Invalid install state.initializedBy; expected evodev");
48
+ }
49
+
50
+ if (
51
+ !Array.isArray(root.selectedPlugins) ||
52
+ !root.selectedPlugins.every((plugin) => typeof plugin === "string")
53
+ ) {
54
+ throw new EvoDevConfigError("Invalid install state.selectedPlugins; expected string array");
55
+ }
56
+
57
+ return {
58
+ version: 1,
59
+ initializedAt: expectNullableString(root.initializedAt, "install state.initializedAt"),
60
+ initializedBy: "evodev",
61
+ selectedPlugins: [...root.selectedPlugins],
62
+ };
63
+ }
64
+
65
+ export function parseSyncState(value: unknown): SyncState {
66
+ const root = expectRecord(value, "sync state");
67
+
68
+ if (root.version !== 1) {
69
+ throw new EvoDevConfigError("Invalid sync state.version; expected 1");
70
+ }
71
+
72
+ const targets = expectRecord(root.targets, "sync state.targets");
73
+ const parsedTargets: Record<string, PluginSyncState> = {};
74
+
75
+ for (const [pluginId, state] of Object.entries(targets)) {
76
+ parsedTargets[pluginId] = parsePluginSyncState(state, `sync state.targets.${pluginId}`);
77
+ }
78
+
79
+ return {
80
+ version: 1,
81
+ lastSyncAt: expectNullableString(root.lastSyncAt, "sync state.lastSyncAt"),
82
+ targets: parsedTargets,
83
+ };
84
+ }
85
+
86
+ function parsePluginSyncState(value: unknown, path: string): PluginSyncState {
87
+ const root = expectRecord(value, path);
88
+ const status = root.status;
89
+
90
+ if (
91
+ status !== "success" &&
92
+ status !== "partial" &&
93
+ status !== "failed" &&
94
+ status !== "never-run"
95
+ ) {
96
+ throw new EvoDevConfigError(
97
+ `Invalid ${path}.status; expected success, partial, failed, or never-run`,
98
+ );
99
+ }
100
+
101
+ return {
102
+ skills: expectNonNegativeInteger(root.skills, `${path}.skills`),
103
+ agents: expectNonNegativeInteger(root.agents, `${path}.agents`),
104
+ status,
105
+ };
106
+ }
107
+
108
+ function expectRecord(value: unknown, path: string): Record<string, unknown> {
109
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
110
+ throw new EvoDevConfigError(`Invalid ${path}; expected object, got ${describeType(value)}`);
111
+ }
112
+
113
+ return value as Record<string, unknown>;
114
+ }
115
+
116
+ function expectNullableString(value: unknown, path: string): string | null {
117
+ if (value === null || typeof value === "string") {
118
+ return value;
119
+ }
120
+
121
+ throw new EvoDevConfigError(`Invalid ${path}; expected string or null`);
122
+ }
123
+
124
+ function expectNonNegativeInteger(value: unknown, path: string): number {
125
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
126
+ throw new EvoDevConfigError(`Invalid ${path}; expected non-negative integer`);
127
+ }
128
+
129
+ return value;
130
+ }