@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,779 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
3
+ import { type ProtectedZoneFinding, checkProtectedZonePaths } from "../protected-zones/index.ts";
4
+
5
+ export type PackAssetKind = "skills" | "agents" | "workflows" | "hooks" | "docs";
6
+
7
+ export interface PackManifest {
8
+ id: string;
9
+ version: string;
10
+ name: string;
11
+ description: string;
12
+ publisher?: string;
13
+ license?: string;
14
+ compatibility: {
15
+ evodev: string;
16
+ targets: string[];
17
+ };
18
+ assets: Record<PackAssetKind, string[]>;
19
+ permissions: PackPermissions;
20
+ install: {
21
+ guide: string;
22
+ requiresConfirmation: boolean;
23
+ supportsDryRun: boolean;
24
+ backupPolicy: string;
25
+ mergePolicy: string;
26
+ };
27
+ verify: {
28
+ guide: string;
29
+ requiredChecks: string[];
30
+ };
31
+ customizations: {
32
+ guide: string;
33
+ userPath: string;
34
+ projectPath: string;
35
+ };
36
+ protectedZones?: {
37
+ neverInclude?: string[];
38
+ neverWrite?: string[];
39
+ };
40
+ observability: {
41
+ metadataOnly: true;
42
+ logEvents: string[];
43
+ };
44
+ uninstall: {
45
+ supported: boolean;
46
+ deleteUserContent: boolean;
47
+ requiresDryRun: boolean;
48
+ requiresConfirmation: boolean;
49
+ };
50
+ }
51
+
52
+ export interface PackPermissions {
53
+ writesUserConfig?: boolean;
54
+ writesProjectFiles?: boolean;
55
+ usesHooks?: boolean;
56
+ usesNetwork?: boolean;
57
+ publishes?: boolean;
58
+ deletesFiles?: boolean;
59
+ spawnsAgents?: boolean;
60
+ writesMemory?: boolean;
61
+ readsSourceContent?: boolean;
62
+ readsProjectMetadata?: boolean;
63
+ }
64
+
65
+ export interface PackAssetRef {
66
+ kind: PackAssetKind;
67
+ path: string;
68
+ absolutePath: string;
69
+ }
70
+
71
+ export type PackValidationSeverity = "error" | "warning";
72
+
73
+ export interface PackValidationFinding {
74
+ severity: PackValidationSeverity;
75
+ code: string;
76
+ message: string;
77
+ path?: string;
78
+ }
79
+
80
+ export interface PackValidationResult {
81
+ ok: boolean;
82
+ packRoot: string;
83
+ manifestPath: string;
84
+ source: string;
85
+ manifest?: PackManifest;
86
+ assets: PackAssetRef[];
87
+ scannedPaths: string[];
88
+ protectedZoneFindings: ProtectedZoneFinding[];
89
+ findings: PackValidationFinding[];
90
+ }
91
+
92
+ export type PackInstallActionKind = "create" | "skip-existing" | "merge-with-backup" | "error";
93
+
94
+ export interface PackInstallAction {
95
+ action: PackInstallActionKind;
96
+ sourcePath: string;
97
+ targetPath: string;
98
+ reason: string;
99
+ }
100
+
101
+ export interface PackInstallDryRunPlan {
102
+ validation: PackValidationResult;
103
+ plannedWrites: PackInstallAction[];
104
+ plannedRegistryUpdates: string[];
105
+ plannedSettingsUpdates: string[];
106
+ permissions: Required<PackPermissions>;
107
+ riskyCapabilities: string[];
108
+ verificationPlan: string[];
109
+ uninstallPlan: string[];
110
+ blockers: string[];
111
+ warnings: string[];
112
+ }
113
+
114
+ const ASSET_KINDS: PackAssetKind[] = ["skills", "agents", "workflows", "hooks", "docs"];
115
+ const MANIFEST_FILE_NAME = "PACK.json";
116
+ const RISKY_PERMISSION_LABELS: Record<keyof PackPermissions, string> = {
117
+ writesUserConfig: "writes user-level EvoDev or Code Agent configuration",
118
+ writesProjectFiles: "writes project files",
119
+ usesHooks: "installs or enables hooks",
120
+ usesNetwork: "uses network or remote services",
121
+ publishes: "publishes packages or artifacts",
122
+ deletesFiles: "deletes files",
123
+ spawnsAgents: "spawns agents",
124
+ writesMemory: "writes memory or knowledge",
125
+ readsSourceContent: "reads source content",
126
+ readsProjectMetadata: "reads project metadata",
127
+ };
128
+ const FIRST_SLICE_DENIED_PERMISSIONS: Array<keyof PackPermissions> = [
129
+ "writesProjectFiles",
130
+ "usesHooks",
131
+ "usesNetwork",
132
+ "publishes",
133
+ "deletesFiles",
134
+ "writesMemory",
135
+ "readsSourceContent",
136
+ ];
137
+
138
+ export async function validatePack(packPath: string): Promise<PackValidationResult> {
139
+ if (isRemotePackInput(packPath)) {
140
+ throw new Error("Only local pack directories are supported in I6.");
141
+ }
142
+ const packRoot = await resolvePackRoot(packPath);
143
+ const manifestPath = join(packRoot, MANIFEST_FILE_NAME);
144
+ const findings: PackValidationFinding[] = [];
145
+ let manifest: PackManifest | undefined;
146
+ let assets: PackAssetRef[] = [];
147
+ let scannedPaths: string[] = [];
148
+
149
+ try {
150
+ manifest = parsePackManifest(JSON.parse(await readFile(manifestPath, "utf8")));
151
+ } catch (error) {
152
+ findings.push({
153
+ severity: "error",
154
+ code: "manifest-invalid",
155
+ message: formatError(error),
156
+ path: MANIFEST_FILE_NAME,
157
+ });
158
+ }
159
+
160
+ if (manifest !== undefined) {
161
+ assets = manifestAssetRefs(manifest, packRoot, findings);
162
+ await validateReferencedAssets(assets, findings);
163
+ validatePermissionBoundaries(manifest, findings);
164
+ validateGuideReferences(manifest, assets, findings);
165
+ }
166
+
167
+ try {
168
+ scannedPaths = await collectPackRelativePaths(packRoot);
169
+ } catch (error) {
170
+ findings.push({ severity: "error", code: "scan-failed", message: formatError(error) });
171
+ }
172
+
173
+ const protectedZoneFindings = checkProtectedZonePaths(scannedPaths).findings;
174
+ for (const finding of protectedZoneFindings) {
175
+ findings.push({
176
+ severity: "error",
177
+ code: `protected-zone:${finding.ruleId}`,
178
+ message: finding.reason,
179
+ path: finding.path,
180
+ });
181
+ }
182
+
183
+ return {
184
+ ok: findings.every((finding) => finding.severity !== "error"),
185
+ packRoot,
186
+ manifestPath,
187
+ source: packRoot,
188
+ manifest,
189
+ assets,
190
+ scannedPaths,
191
+ protectedZoneFindings,
192
+ findings,
193
+ };
194
+ }
195
+
196
+ export async function planPackInstallDryRun(
197
+ input: string | { packDir: string; homeDir?: string },
198
+ ): Promise<PackInstallDryRunPlan> {
199
+ const packPath = typeof input === "string" ? input : input.packDir;
200
+ const homePrefix = typeof input === "string" ? "~" : (input.homeDir ?? "~");
201
+ const displayEvodevRoot = `${homePrefix.replace(/\/$/, "")}/.evodev`;
202
+ const validation = await validatePack(packPath);
203
+ const manifest = validation.manifest;
204
+ const plannedWrites: PackInstallAction[] = [];
205
+ const warnings: string[] = [];
206
+ const blockers = validation.findings
207
+ .filter((finding) => finding.severity === "error")
208
+ .map(
209
+ (finding) => `${finding.code}${finding.path ? ` ${finding.path}` : ""}: ${finding.message}`,
210
+ );
211
+
212
+ if (manifest === undefined) {
213
+ blockers.push("Cannot plan install because PACK.json is invalid or missing.");
214
+ } else {
215
+ const permissions = normalizePackPermissions(manifest.permissions);
216
+ if (!permissions.writesUserConfig) {
217
+ warnings.push("Pack does not declare writesUserConfig; dry-run only reports source assets.");
218
+ }
219
+ for (const asset of validation.assets) {
220
+ plannedWrites.push({
221
+ action: "create",
222
+ sourcePath: asset.path,
223
+ targetPath: `${displayEvodevRoot}/PACKS/${manifest.id}/${asset.path}`,
224
+ reason: "dry-run planned pack asset write; no files are written",
225
+ });
226
+ }
227
+ }
228
+
229
+ const permissions = normalizePackPermissions(manifest?.permissions ?? {});
230
+ return {
231
+ validation,
232
+ plannedWrites,
233
+ plannedRegistryUpdates:
234
+ manifest === undefined
235
+ ? []
236
+ : [`dry-run metadata merge for registry.packs.${manifest.id}; no registry file is written`],
237
+ plannedSettingsUpdates:
238
+ manifest === undefined
239
+ ? []
240
+ : [`dry-run settings compatibility review for ${manifest.id}; no settings file is written`],
241
+ permissions,
242
+ riskyCapabilities: riskyCapabilities(permissions),
243
+ verificationPlan: manifest?.verify.requiredChecks ?? [],
244
+ uninstallPlan:
245
+ manifest === undefined
246
+ ? []
247
+ : [
248
+ "dry-run-first uninstall required",
249
+ "confirmation required before future uninstall",
250
+ "deleteUserContent=false; customizations and user content must be preserved",
251
+ ],
252
+ blockers,
253
+ warnings,
254
+ };
255
+ }
256
+
257
+ export function parsePackManifest(value: unknown): PackManifest {
258
+ if (!isRecord(value)) throw new Error("Pack manifest must be an object.");
259
+
260
+ const manifest = value as Record<string, unknown>;
261
+ validateManifestTopLevelFields(manifest);
262
+ const id = requireString(manifest, "id");
263
+ const version = requireString(manifest, "version");
264
+ const name = requireString(manifest, "name");
265
+ const description = requireString(manifest, "description");
266
+ const publisher = optionalString(manifest, "publisher");
267
+ const license = optionalString(manifest, "license");
268
+ const compatibility = requireRecord(manifest, "compatibility");
269
+ const assets = parseAssets(requireRecord(manifest, "assets"));
270
+ const permissions = parsePermissions(manifest.permissions);
271
+ const install = requireRecord(manifest, "install");
272
+ const verify = requireRecord(manifest, "verify");
273
+ const customizations = requireRecord(manifest, "customizations");
274
+ const observability = requireRecord(manifest, "observability");
275
+ const uninstall = requireRecord(manifest, "uninstall");
276
+
277
+ validateCompatibilityTargets(id, requireStringArray(compatibility, "targets"));
278
+ validateProtectedZoneDeclarations(manifest.protectedZones);
279
+
280
+ if (observability.metadataOnly !== true) {
281
+ throw new Error(`Pack manifest ${id} must declare observability.metadataOnly=true.`);
282
+ }
283
+ if (uninstall.deleteUserContent !== false) {
284
+ throw new Error(`Pack manifest ${id} must declare uninstall.deleteUserContent=false.`);
285
+ }
286
+
287
+ return {
288
+ id,
289
+ version,
290
+ name,
291
+ description,
292
+ publisher,
293
+ license,
294
+ compatibility: {
295
+ evodev: requireString(compatibility, "evodev"),
296
+ targets: requireStringArray(compatibility, "targets"),
297
+ },
298
+ assets,
299
+ permissions,
300
+ install: {
301
+ guide: requireString(install, "guide"),
302
+ requiresConfirmation: requireBoolean(install, "requiresConfirmation"),
303
+ supportsDryRun: requireBoolean(install, "supportsDryRun"),
304
+ backupPolicy: requireString(install, "backupPolicy"),
305
+ mergePolicy: requireString(install, "mergePolicy"),
306
+ },
307
+ verify: {
308
+ guide: requireString(verify, "guide"),
309
+ requiredChecks: requireStringArray(verify, "requiredChecks"),
310
+ },
311
+ customizations: {
312
+ guide: requireString(customizations, "guide"),
313
+ userPath: requireString(customizations, "userPath"),
314
+ projectPath: requireString(customizations, "projectPath"),
315
+ },
316
+ protectedZones: isRecord(manifest.protectedZones)
317
+ ? {
318
+ neverInclude: optionalStringArray(manifest.protectedZones, "neverInclude"),
319
+ neverWrite: optionalStringArray(manifest.protectedZones, "neverWrite"),
320
+ }
321
+ : undefined,
322
+ observability: {
323
+ metadataOnly: true,
324
+ logEvents: requireStringArray(observability, "logEvents"),
325
+ },
326
+ uninstall: {
327
+ supported: requireBoolean(uninstall, "supported"),
328
+ deleteUserContent: false,
329
+ requiresDryRun: requireBoolean(uninstall, "requiresDryRun"),
330
+ requiresConfirmation: requireBoolean(uninstall, "requiresConfirmation"),
331
+ },
332
+ };
333
+ }
334
+
335
+ export function normalizePackPermissions(permissions: PackPermissions): Required<PackPermissions> {
336
+ return {
337
+ writesUserConfig: permissions.writesUserConfig === true,
338
+ writesProjectFiles: permissions.writesProjectFiles === true,
339
+ usesHooks: permissions.usesHooks === true,
340
+ usesNetwork: permissions.usesNetwork === true,
341
+ publishes: permissions.publishes === true,
342
+ deletesFiles: permissions.deletesFiles === true,
343
+ spawnsAgents: permissions.spawnsAgents === true,
344
+ writesMemory: permissions.writesMemory === true,
345
+ readsSourceContent: permissions.readsSourceContent === true,
346
+ readsProjectMetadata: permissions.readsProjectMetadata === true,
347
+ };
348
+ }
349
+
350
+ export function formatPackValidation(result: PackValidationResult): string {
351
+ const manifest = result.manifest;
352
+ return [
353
+ "EvoDev pack validate",
354
+ "",
355
+ `Pack root: ${result.packRoot}`,
356
+ `Status: ${result.ok ? "PASS" : "FAIL"}`,
357
+ manifest === undefined
358
+ ? "Pack: unknown"
359
+ : `Pack: ${manifest.id}@${manifest.version} (${manifest.name})`,
360
+ `Assets: ${result.assets.length}`,
361
+ `Scanned paths: ${result.scannedPaths.length}`,
362
+ `Protected-zone blockers: ${result.protectedZoneFindings.length}`,
363
+ "",
364
+ "Findings:",
365
+ ...(result.findings.length === 0
366
+ ? [" - none"]
367
+ : result.findings.map(
368
+ (finding) =>
369
+ ` - ${finding.severity.toUpperCase()} ${finding.code}${finding.path ? ` ${finding.path}` : ""}: ${finding.message}`,
370
+ )),
371
+ ].join("\n");
372
+ }
373
+
374
+ export function formatPackInstallDryRun(plan: PackInstallDryRunPlan): string {
375
+ const manifest = plan.validation.manifest;
376
+ return [
377
+ "EvoDev pack install dry-run",
378
+ "",
379
+ manifest === undefined
380
+ ? "Pack: unknown"
381
+ : `Pack: ${manifest.id}@${manifest.version} (${manifest.name})`,
382
+ "Mode: dry-run (no writes)",
383
+ `Status: ${plan.blockers.length === 0 ? "PASS" : "FAIL"}`,
384
+ "",
385
+ `Source: ${plan.validation.source}`,
386
+ "",
387
+ "Asset inventory:",
388
+ ...formatAssetInventory(plan.validation.assets),
389
+ "Asset files:",
390
+ ...formatAssetFiles(plan.validation.assets),
391
+ "",
392
+ "Planned writes:",
393
+ ...(plan.plannedWrites.length === 0
394
+ ? [" - none"]
395
+ : plan.plannedWrites.map(
396
+ (write) =>
397
+ ` - ${write.action}: ${write.sourcePath} -> ${write.targetPath} (${write.reason})`,
398
+ )),
399
+ "",
400
+ "Planned registry/settings metadata:",
401
+ ...(plan.plannedRegistryUpdates.length === 0
402
+ ? [" - registry: none"]
403
+ : plan.plannedRegistryUpdates.map((update) => ` - registry: ${update}`)),
404
+ ...(plan.plannedSettingsUpdates.length === 0
405
+ ? [" - settings: none"]
406
+ : plan.plannedSettingsUpdates.map((update) => ` - settings: ${update}`)),
407
+ "",
408
+ "Permissions:",
409
+ ...Object.entries(plan.permissions).map(([key, value]) => ` - ${key}: ${value}`),
410
+ "",
411
+ "Risky capabilities:",
412
+ ...(plan.riskyCapabilities.length === 0
413
+ ? [" - none"]
414
+ : plan.riskyCapabilities.map((capability) => ` - ${capability}`)),
415
+ "",
416
+ "Protected-zone results:",
417
+ ...(plan.validation.protectedZoneFindings.length === 0
418
+ ? [" - PASS"]
419
+ : plan.validation.protectedZoneFindings.map(
420
+ (finding) => ` - BLOCKER ${finding.ruleId} ${finding.path}: ${finding.reason}`,
421
+ )),
422
+ "",
423
+ "Verification plan:",
424
+ ...(plan.verificationPlan.length === 0
425
+ ? [" - none"]
426
+ : plan.verificationPlan.map((check) => ` - ${check}`)),
427
+ "",
428
+ "Uninstall plan:",
429
+ ...(plan.uninstallPlan.length === 0
430
+ ? [" - none"]
431
+ : plan.uninstallPlan.map((step) => ` - ${step}`)),
432
+ "",
433
+ "Warnings:",
434
+ ...(plan.warnings.length === 0
435
+ ? [" - none"]
436
+ : plan.warnings.map((warning) => ` - ${warning}`)),
437
+ "",
438
+ "Blockers:",
439
+ ...(plan.blockers.length === 0
440
+ ? [" - none"]
441
+ : plan.blockers.map((blocker) => ` - ${blocker}`)),
442
+ ].join("\n");
443
+ }
444
+
445
+ async function resolvePackRoot(packPath: string): Promise<string> {
446
+ const pathStat = await stat(packPath);
447
+ if (pathStat.isDirectory()) return packPath;
448
+ throw new Error(`Pack path must be a local directory: ${packPath}`);
449
+ }
450
+
451
+ function manifestAssetRefs(
452
+ manifest: PackManifest,
453
+ packRoot: string,
454
+ findings: PackValidationFinding[],
455
+ ): PackAssetRef[] {
456
+ const refs: PackAssetRef[] = [];
457
+ for (const kind of ASSET_KINDS) {
458
+ for (const assetPath of manifest.assets[kind]) {
459
+ const pathFinding = validatePackRelativePath(assetPath, packRoot);
460
+ if (pathFinding !== undefined) {
461
+ findings.push({ ...pathFinding, path: assetPath });
462
+ continue;
463
+ }
464
+ refs.push({ kind, path: assetPath, absolutePath: join(packRoot, assetPath) });
465
+ }
466
+ }
467
+ return refs;
468
+ }
469
+
470
+ async function validateReferencedAssets(
471
+ assets: PackAssetRef[],
472
+ findings: PackValidationFinding[],
473
+ ): Promise<void> {
474
+ for (const asset of assets) {
475
+ try {
476
+ const assetStat = await stat(asset.absolutePath);
477
+ if (!assetStat.isFile()) {
478
+ findings.push({
479
+ severity: "error",
480
+ code: "asset-not-file",
481
+ message: "Referenced asset must be a file.",
482
+ path: asset.path,
483
+ });
484
+ }
485
+ } catch (error) {
486
+ if (isEnoent(error)) {
487
+ findings.push({
488
+ severity: "error",
489
+ code: "asset-missing",
490
+ message: "Referenced asset file does not exist.",
491
+ path: asset.path,
492
+ });
493
+ continue;
494
+ }
495
+ throw error;
496
+ }
497
+ }
498
+ }
499
+
500
+ function validatePermissionBoundaries(
501
+ manifest: PackManifest,
502
+ findings: PackValidationFinding[],
503
+ ): void {
504
+ const permissions = normalizePackPermissions(manifest.permissions);
505
+ for (const permission of FIRST_SLICE_DENIED_PERMISSIONS) {
506
+ if (permissions[permission]) {
507
+ findings.push({
508
+ severity: "error",
509
+ code: "permission-denied-first-slice",
510
+ message: `${permission} is outside I6 local validate/install dry-run scope.`,
511
+ });
512
+ }
513
+ }
514
+ if (manifest.assets.hooks.length > 0 && !permissions.usesHooks) {
515
+ findings.push({
516
+ severity: "error",
517
+ code: "permission-asset-mismatch",
518
+ message: "Pack declares hook assets but usesHooks permission is false.",
519
+ });
520
+ }
521
+ if (!manifest.install.requiresConfirmation || !manifest.install.supportsDryRun) {
522
+ findings.push({
523
+ severity: "error",
524
+ code: "install-policy-invalid",
525
+ message: "Pack install must require confirmation and support dry-run.",
526
+ });
527
+ }
528
+ if (!manifest.uninstall.requiresDryRun || !manifest.uninstall.requiresConfirmation) {
529
+ findings.push({
530
+ severity: "error",
531
+ code: "uninstall-policy-invalid",
532
+ message: "Pack uninstall must require dry-run and confirmation.",
533
+ });
534
+ }
535
+ }
536
+
537
+ function validateGuideReferences(
538
+ manifest: PackManifest,
539
+ assets: PackAssetRef[],
540
+ findings: PackValidationFinding[],
541
+ ): void {
542
+ const docsAssetPaths = new Set(
543
+ assets.filter((asset) => asset.kind === "docs").map((asset) => asset.path),
544
+ );
545
+ for (const guide of [
546
+ manifest.install.guide,
547
+ manifest.verify.guide,
548
+ manifest.customizations.guide,
549
+ ]) {
550
+ if (!docsAssetPaths.has(guide)) {
551
+ findings.push({
552
+ severity: "error",
553
+ code: "guide-not-declared",
554
+ message: "Guide file must be listed in assets.docs.",
555
+ path: guide,
556
+ });
557
+ }
558
+ }
559
+ }
560
+
561
+ async function collectPackRelativePaths(packRoot: string, dir = packRoot): Promise<string[]> {
562
+ const entries = await readdir(dir, { withFileTypes: true });
563
+ const paths: string[] = [];
564
+ for (const entry of entries) {
565
+ const absolutePath = join(dir, entry.name);
566
+ const relativePath = normalizeRelativePath(relative(packRoot, absolutePath));
567
+ paths.push(relativePath);
568
+ if (entry.isDirectory()) {
569
+ paths.push(...(await collectPackRelativePaths(packRoot, absolutePath)));
570
+ }
571
+ }
572
+ return paths.sort();
573
+ }
574
+
575
+ function validatePackRelativePath(
576
+ path: string,
577
+ packRoot: string,
578
+ ): PackValidationFinding | undefined {
579
+ if (path.length === 0) {
580
+ return { severity: "error", code: "path-empty", message: "Path must not be empty." };
581
+ }
582
+ if (path.includes("\0")) {
583
+ return { severity: "error", code: "path-invalid", message: "Path must not contain NUL bytes." };
584
+ }
585
+ if (isAbsolute(path) || path.startsWith("~")) {
586
+ return {
587
+ severity: "error",
588
+ code: "path-absolute",
589
+ message: "Path must be relative to pack root.",
590
+ };
591
+ }
592
+ const normalized = normalizeRelativePath(path);
593
+ if (normalized.split("/").includes("..")) {
594
+ return {
595
+ severity: "error",
596
+ code: "path-traversal",
597
+ message: "Path must not escape pack root.",
598
+ };
599
+ }
600
+ const absolute = join(packRoot, normalized);
601
+ const rel = relative(packRoot, absolute);
602
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
603
+ return {
604
+ severity: "error",
605
+ code: "path-traversal",
606
+ message: "Path must stay inside pack root.",
607
+ };
608
+ }
609
+ return undefined;
610
+ }
611
+
612
+ function riskyCapabilities(permissions: Required<PackPermissions>): string[] {
613
+ return (Object.entries(permissions) as Array<[keyof PackPermissions, boolean]>)
614
+ .filter(([, value]) => value)
615
+ .map(([permission]) => RISKY_PERMISSION_LABELS[permission]);
616
+ }
617
+
618
+ function formatAssetInventory(assets: PackAssetRef[]): string[] {
619
+ if (assets.length === 0) return [" - none"];
620
+ const counts = new Map<PackAssetKind, number>();
621
+ for (const kind of ASSET_KINDS) counts.set(kind, 0);
622
+ for (const asset of assets) counts.set(asset.kind, (counts.get(asset.kind) ?? 0) + 1);
623
+ return ASSET_KINDS.map((kind) => ` - ${kind}: ${counts.get(kind) ?? 0}`);
624
+ }
625
+
626
+ function formatAssetFiles(assets: PackAssetRef[]): string[] {
627
+ if (assets.length === 0) return [" - none"];
628
+ return assets.map((asset) => ` - ${asset.kind}: ${asset.path}`);
629
+ }
630
+
631
+ function parseAssets(value: Record<string, unknown>): Record<PackAssetKind, string[]> {
632
+ const knownKinds = new Set(ASSET_KINDS);
633
+ for (const key of Object.keys(value)) {
634
+ if (!knownKinds.has(key as PackAssetKind)) {
635
+ throw new Error(`Pack manifest contains unknown asset kind: ${key}`);
636
+ }
637
+ }
638
+ return {
639
+ skills: optionalStringArray(value, "skills") ?? [],
640
+ agents: optionalStringArray(value, "agents") ?? [],
641
+ workflows: optionalStringArray(value, "workflows") ?? [],
642
+ hooks: optionalStringArray(value, "hooks") ?? [],
643
+ docs: optionalStringArray(value, "docs") ?? [],
644
+ };
645
+ }
646
+
647
+ function parsePermissions(value: unknown): PackPermissions {
648
+ if (value === undefined) return {};
649
+ if (!isRecord(value)) throw new Error("Pack manifest permissions must be an object.");
650
+ const permissions: PackPermissions = {};
651
+ const knownKeys = new Set(Object.keys(RISKY_PERMISSION_LABELS));
652
+ for (const key of Object.keys(value)) {
653
+ if (!knownKeys.has(key)) throw new Error(`Pack manifest contains unknown permission: ${key}`);
654
+ }
655
+ for (const key of Object.keys(RISKY_PERMISSION_LABELS) as Array<keyof PackPermissions>) {
656
+ if (value[key] !== undefined) permissions[key] = requireBoolean(value, key);
657
+ }
658
+ return permissions;
659
+ }
660
+
661
+ function optionalString(record: Record<string, unknown>, key: string): string | undefined {
662
+ const value = record[key];
663
+ if (value === undefined) return undefined;
664
+ if (typeof value !== "string" || value.length === 0) {
665
+ throw new Error(`Pack manifest field must be a non-empty string: ${key}`);
666
+ }
667
+ return value;
668
+ }
669
+
670
+ function requireString(record: Record<string, unknown>, key: string): string {
671
+ const value = record[key];
672
+ if (typeof value !== "string" || value.length === 0) {
673
+ throw new Error(`Pack manifest missing string field: ${key}`);
674
+ }
675
+ return value;
676
+ }
677
+
678
+ function requireBoolean(record: Record<string, unknown>, key: string): boolean {
679
+ const value = record[key];
680
+ if (typeof value !== "boolean") throw new Error(`Pack manifest missing boolean field: ${key}`);
681
+ return value;
682
+ }
683
+
684
+ function requireRecord(record: Record<string, unknown>, key: string): Record<string, unknown> {
685
+ const value = record[key];
686
+ if (!isRecord(value)) throw new Error(`Pack manifest missing object field: ${key}`);
687
+ return value;
688
+ }
689
+
690
+ function requireStringArray(record: Record<string, unknown>, key: string): string[] {
691
+ const value = record[key];
692
+ if (
693
+ !Array.isArray(value) ||
694
+ value.some((item) => typeof item !== "string" || item.length === 0)
695
+ ) {
696
+ throw new Error(`Pack manifest missing string array field: ${key}`);
697
+ }
698
+ return value;
699
+ }
700
+
701
+ function optionalStringArray(record: Record<string, unknown>, key: string): string[] | undefined {
702
+ const value = record[key];
703
+ if (value === undefined) return undefined;
704
+ if (
705
+ !Array.isArray(value) ||
706
+ value.some((item) => typeof item !== "string" || item.length === 0)
707
+ ) {
708
+ throw new Error(`Pack manifest field must be a string array: ${key}`);
709
+ }
710
+ return value;
711
+ }
712
+
713
+ function validateManifestTopLevelFields(manifest: Record<string, unknown>): void {
714
+ const knownFields = new Set([
715
+ "id",
716
+ "version",
717
+ "name",
718
+ "description",
719
+ "publisher",
720
+ "license",
721
+ "compatibility",
722
+ "assets",
723
+ "permissions",
724
+ "install",
725
+ "verify",
726
+ "customizations",
727
+ "protectedZones",
728
+ "observability",
729
+ "uninstall",
730
+ ]);
731
+ for (const key of Object.keys(manifest)) {
732
+ if (!knownFields.has(key)) throw new Error(`Pack manifest contains unknown field: ${key}`);
733
+ }
734
+ }
735
+
736
+ function validateCompatibilityTargets(packId: string, targets: string[]): void {
737
+ if (targets.length === 0)
738
+ throw new Error(`Pack manifest ${packId} must declare at least one target.`);
739
+ const supportedTargets = new Set(["claude"]);
740
+ for (const target of targets) {
741
+ if (!supportedTargets.has(target)) {
742
+ throw new Error(`Pack manifest ${packId} declares unsupported target: ${target}`);
743
+ }
744
+ }
745
+ }
746
+
747
+ function validateProtectedZoneDeclarations(value: unknown): void {
748
+ if (value === undefined) return;
749
+ if (!isRecord(value)) throw new Error("Pack manifest protectedZones must be an object.");
750
+ const knownFields = new Set(["neverInclude", "neverWrite"]);
751
+ for (const key of Object.keys(value)) {
752
+ if (!knownFields.has(key))
753
+ throw new Error(`Pack manifest protectedZones contains unknown field: ${key}`);
754
+ }
755
+ optionalStringArray(value, "neverInclude");
756
+ optionalStringArray(value, "neverWrite");
757
+ }
758
+
759
+ function isRemotePackInput(packPath: string): boolean {
760
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(packPath);
761
+ }
762
+
763
+ function isRecord(value: unknown): value is Record<string, unknown> {
764
+ return typeof value === "object" && value !== null && !Array.isArray(value);
765
+ }
766
+
767
+ function normalizeRelativePath(path: string): string {
768
+ return path.split(sep).join("/").replace(/^\.\//, "");
769
+ }
770
+
771
+ function isEnoent(error: unknown): boolean {
772
+ return (
773
+ error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
774
+ );
775
+ }
776
+
777
+ function formatError(error: unknown): string {
778
+ return error instanceof Error ? error.message : String(error);
779
+ }