@danieljvdm/dev-kit 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sync.ts CHANGED
@@ -3,7 +3,12 @@ import { Cause, Effect, FileSystem, Path, Schema, Stream } from "effect";
3
3
  import { ChildProcess } from "effect/unstable/process";
4
4
 
5
5
  import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
6
- import { loadSkillCatalog, resolveSkillSources, type ResolvedSkillSource } from "./catalog.ts";
6
+ import {
7
+ loadSkillCatalog,
8
+ resolveSkillSources,
9
+ type CatalogSkill,
10
+ type ResolvedSkillSource,
11
+ } from "./catalog.ts";
7
12
  import { printDetail, printStatus, withSpinner } from "./cli-ui.ts";
8
13
  import {
9
14
  applyEffectSourcePlan,
@@ -16,16 +21,23 @@ import {
16
21
  type EffectTsgoPatchPlan,
17
22
  } from "./effect-tsgo.ts";
18
23
  import {
24
+ digestFileContent,
19
25
  digestSymlinkTarget,
20
26
  digestText,
21
27
  observePath,
22
28
  type ObservedPath,
23
29
  } from "./path-digest.ts";
30
+ import { resolvePackageSkillSelector } from "./package-skill-source.ts";
31
+ import { readDirectDependencyNames } from "./project-package.ts";
32
+ import { parseSkillSelector } from "./skill-selector.ts";
24
33
  import {
25
34
  AppliedStateSchema,
26
35
  DevKitLockSchema,
27
36
  type AppliedState,
28
37
  type DevKitLock,
38
+ type ManagedAgentInstructionsOutput,
39
+ type ManagedClaudeInstructionsOutput,
40
+ type ManagedOutput,
29
41
  type ManagedSkillOutput,
30
42
  type OwnershipReceipt,
31
43
  } from "./project-state.ts";
@@ -67,10 +79,25 @@ type DesiredSkillOutput =
67
79
  readonly linkTarget: string;
68
80
  });
69
81
 
82
+ type DesiredAgentInstructionsOutput = ManagedAgentInstructionsOutput & {
83
+ readonly content: string;
84
+ readonly destination: string;
85
+ };
86
+
87
+ type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
88
+ readonly destination: string;
89
+ readonly linkTarget: string;
90
+ };
91
+
92
+ type DesiredOutput =
93
+ | DesiredSkillOutput
94
+ | DesiredAgentInstructionsOutput
95
+ | DesiredClaudeInstructionsOutput;
96
+
70
97
  type SkillPlanAction =
71
98
  | {
72
99
  readonly action: "create" | "update";
73
- readonly desired: DesiredSkillOutput;
100
+ readonly desired: DesiredOutput;
74
101
  readonly observed: ObservedPath;
75
102
  }
76
103
  | {
@@ -81,7 +108,7 @@ type SkillPlanAction =
81
108
  }
82
109
  | {
83
110
  readonly action: "unchanged";
84
- readonly desired: DesiredSkillOutput;
111
+ readonly desired: DesiredOutput;
85
112
  readonly observed: ObservedPath;
86
113
  readonly adopted: boolean;
87
114
  }
@@ -180,10 +207,14 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
180
207
  }
181
208
  }
182
209
 
183
- const SKILL_FAMILIES: SkillCatalog = { effect: ["effect-ts"] };
210
+ const SKILL_FAMILIES: SkillCatalog = {
211
+ effect: ["effect-ts", "effect-atom-data-fetching"],
212
+ };
184
213
  export const DEFAULT_MANIFEST = "dev-kit.jsonc";
185
214
  const DEFAULT_LOCKFILE = "dev-kit.lock.json";
186
215
  const DEFAULT_STATE = ".dev-kit/state.json";
216
+ const AGENT_INSTRUCTIONS_TEMPLATE = "templates/AGENTS.md";
217
+ const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
187
218
 
188
219
  const resolvePackageRoot = Effect.fn("resolvePackageRoot")(function* () {
189
220
  const path = yield* Path.Path;
@@ -263,7 +294,7 @@ const expandSelection = (
263
294
  for (const name of include) {
264
295
  if (skillFamilies[name]) {
265
296
  for (const skill of skillFamilies[name]) selected.add(skill);
266
- } else if (availableSkills.includes(name)) {
297
+ } else if (availableSkills.includes(name) || parseSkillSelector(name)?.type === "package") {
267
298
  selected.add(name);
268
299
  } else {
269
300
  return Effect.fail(new UnknownSkillOrFamilyError({ name, known }));
@@ -315,7 +346,7 @@ const pathsOverlap = (left: string, right: string): boolean =>
315
346
  const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
316
347
  projectDir: string,
317
348
  reserved: ReadonlyArray<{ readonly label: string; readonly path: string }>,
318
- outputs: ReadonlyArray<Pick<ManagedSkillOutput | OwnershipReceipt, "path">>,
349
+ outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
319
350
  ) {
320
351
  const outputPaths = new Set<string>();
321
352
  for (const output of outputs) {
@@ -342,19 +373,21 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
342
373
  }
343
374
  });
344
375
 
345
- const outputIdentity = (output: Pick<ManagedSkillOutput, "resourceId" | "path" | "mode" | "kind" | "digest" | "catalog">) =>
376
+ const outputIdentity = (output: ManagedOutput) =>
346
377
  JSON.stringify({
347
378
  resourceId: output.resourceId,
348
379
  path: output.path,
349
380
  mode: output.mode,
350
381
  kind: output.kind,
351
382
  digest: output.digest,
352
- catalog: output.catalog,
383
+ ...("skill" in output
384
+ ? { skill: output.skill, target: output.target, catalog: output.catalog }
385
+ : { sourcePath: output.sourcePath }),
353
386
  });
354
387
 
355
388
  const validateInventory = Effect.fn("validateManagedInventory")(function* (
356
389
  projectDir: string,
357
- outputs: ReadonlyArray<ManagedSkillOutput | OwnershipReceipt>,
390
+ outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
358
391
  label: string,
359
392
  ) {
360
393
  const ids = new Set<string>();
@@ -384,7 +417,7 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
384
417
 
385
418
  const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(function* (
386
419
  projectDir: string,
387
- outputs: ReadonlyArray<Pick<ManagedSkillOutput | OwnershipReceipt, "path">>,
420
+ outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
388
421
  ) {
389
422
  const uniquePaths = new Set<string>();
390
423
  for (const output of outputs) {
@@ -403,19 +436,112 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
403
436
  }
404
437
  });
405
438
 
439
+ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
440
+ packageRoot: string,
441
+ projectDir: string,
442
+ sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
443
+ ) {
444
+ const fs = yield* FileSystem.FileSystem;
445
+ const path = yield* Path.Path;
446
+ const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
447
+ if ((yield* observePath(templatePath)).kind !== "file") {
448
+ return yield* new InvalidProjectStateError({
449
+ message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
450
+ });
451
+ }
452
+ const template = yield* fs.readFileString(templatePath);
453
+ if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
454
+ return yield* new InvalidProjectStateError({
455
+ message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
456
+ });
457
+ }
458
+
459
+ const devKitSkill = sourceBySkill.get("dev-kit");
460
+ const devKitSkillPath = devKitSkill === undefined
461
+ ? "node_modules/@danieljvdm/dev-kit/skills/dev-kit/SKILL.md"
462
+ : portablePath(
463
+ path,
464
+ path.relative(
465
+ projectDir,
466
+ path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
467
+ ),
468
+ );
469
+ const sections = [template.replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath).trimEnd()];
470
+ if ((yield* readDirectDependencyNames(projectDir)).includes("vite-plus")) {
471
+ const vitePlusTemplate = path.join(projectDir, "node_modules", "vite-plus", "AGENTS.md");
472
+ if ((yield* observePath(vitePlusTemplate)).kind !== "file") {
473
+ return yield* new InvalidProjectStateError({
474
+ message: "Vite+ is a direct dependency but its agent instructions are not a regular file: node_modules/vite-plus/AGENTS.md",
475
+ });
476
+ }
477
+ sections.push((yield* fs.readFileString(vitePlusTemplate)).trim());
478
+ }
479
+ return `${sections.join("\n\n")}\n`;
480
+ });
481
+
406
482
  const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
483
+ packageRoot: string,
407
484
  projectDir: string,
408
485
  sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
409
- skills: ReadonlyArray<string>,
486
+ skills: ReadonlyArray<CatalogSkill>,
487
+ setup: ReturnType<typeof normalizeManifest>["setup"],
410
488
  targets: ReturnType<typeof normalizeManifest>["targets"],
411
489
  ) {
412
490
  const path = yield* Path.Path;
413
- const outputs: Array<DesiredSkillOutput> = [];
491
+ const outputs: Array<DesiredOutput> = [];
492
+ if (setup.agentInstructions.enabled) {
493
+ const managed = yield* resolveManagedPath(projectDir, "AGENTS.md");
494
+ const content = yield* renderAgentInstructions(packageRoot, projectDir, sourceBySkill);
495
+ outputs.push({
496
+ resourceId: "setup:agent-instructions",
497
+ path: managed.relative,
498
+ sourcePath: AGENT_INSTRUCTIONS_TEMPLATE,
499
+ mode: "copy",
500
+ kind: "file",
501
+ digest: yield* digestFileContent(content),
502
+ destination: managed.absolute,
503
+ content,
504
+ });
505
+ }
506
+ if (setup.claudeInstructions.enabled) {
507
+ const source = yield* resolveManagedPath(projectDir, "AGENTS.md");
508
+ const sourceObservation = setup.agentInstructions.enabled
509
+ ? undefined
510
+ : yield* observePath(source.absolute);
511
+ if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
512
+ return yield* new InvalidProjectStateError({
513
+ message: "Claude instructions source is not a regular file: AGENTS.md",
514
+ });
515
+ }
516
+ const managed = yield* resolveManagedPath(projectDir, "CLAUDE.md");
517
+ const linkTarget = path.relative(path.dirname(managed.absolute), source.absolute);
518
+ outputs.push({
519
+ resourceId: "setup:claude-instructions",
520
+ path: managed.relative,
521
+ sourcePath: source.relative,
522
+ mode: "symlink",
523
+ kind: "symlink",
524
+ digest: yield* digestSymlinkTarget(linkTarget),
525
+ destination: managed.absolute,
526
+ linkTarget,
527
+ });
528
+ }
414
529
  const agentsTarget = targets.agents;
530
+ const duplicateOutput = skills.find((skill, index) =>
531
+ skills.findIndex((candidate) => candidate.name === skill.name) !== index
532
+ );
533
+ if (duplicateOutput !== undefined) {
534
+ const selectors = skills
535
+ .filter((skill) => skill.name === duplicateOutput.name)
536
+ .map((skill) => skill.selector);
537
+ return yield* new InvalidProjectStateError({
538
+ message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
539
+ });
540
+ }
415
541
  for (const skill of skills) {
416
- const resolvedSource = sourceBySkill.get(skill);
542
+ const resolvedSource = sourceBySkill.get(skill.selector);
417
543
  if (resolvedSource === undefined) {
418
- return yield* new InvalidProjectStateError({ message: `skill source is unavailable: ${skill}` });
544
+ return yield* new InvalidProjectStateError({ message: `skill source is unavailable: ${skill.selector}` });
419
545
  }
420
546
  const source = resolvedSource.path;
421
547
  const sourceObservation = yield* observePath(source);
@@ -425,12 +551,12 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
425
551
  for (const targetName of ["agents", "claude", "opencode"] as const) {
426
552
  const target = targets[targetName];
427
553
  if (!target.enabled) continue;
428
- const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill));
554
+ const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill.name));
429
555
  if (target.mode === "copy") {
430
556
  outputs.push({
431
- resourceId: `skill:${skill}@${targetName}`,
557
+ resourceId: `skill:${skill.selector}@${targetName}`,
432
558
  path: managed.relative,
433
- skill,
559
+ skill: skill.name,
434
560
  target: targetName,
435
561
  mode: "copy",
436
562
  kind: "directory",
@@ -443,14 +569,14 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
443
569
  }
444
570
  const linkSource =
445
571
  targetName === "agents" || !agentsTarget.enabled
446
- ? source
447
- : (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill))).absolute;
572
+ ? resolvedSource.linkPath ?? source
573
+ : (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name))).absolute;
448
574
  const linkTarget = path.relative(path.dirname(managed.absolute), linkSource);
449
575
  const linkDigest = yield* digestSymlinkTarget(linkTarget);
450
576
  outputs.push({
451
- resourceId: `skill:${skill}@${targetName}`,
577
+ resourceId: `skill:${skill.selector}@${targetName}`,
452
578
  path: managed.relative,
453
- skill,
579
+ skill: skill.name,
454
580
  target: targetName,
455
581
  mode: "symlink",
456
582
  kind: "symlink",
@@ -471,7 +597,7 @@ const canonicalState = (state: AppliedState): string => `${JSON.stringify(state,
471
597
 
472
598
  const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
473
599
  projectDir: string,
474
- desired: ReadonlyArray<DesiredSkillOutput>,
600
+ desired: ReadonlyArray<DesiredOutput>,
475
601
  currentLock: DevKitLock | undefined,
476
602
  currentState: AppliedState | undefined,
477
603
  nextLock: DevKitLock,
@@ -587,8 +713,8 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
587
713
  typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
588
714
  })
589
715
  : undefined;
590
- const catalog = yield* loadSkillCatalog(packageRoot);
591
- const availableSkills = catalog.skills.map((skill) => skill.name);
716
+ const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
717
+ const availableSkills = catalog.skills.map((skill) => skill.selector);
592
718
  const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
593
719
  for (const [family, familySkills] of Object.entries(skillFamilies)) {
594
720
  if (availableSkills.includes(family)) {
@@ -599,12 +725,36 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
599
725
  return yield* new InvalidSkillCatalogError({ family, message: `family references missing skills: ${missing.join(", ")}` });
600
726
  }
601
727
  }
602
- const selectedSkills = yield* expandSelection(manifest.include, manifest.exclude, availableSkills, skillFamilies);
728
+ const selectedSelectors = yield* expandSelection(manifest.include, manifest.exclude, availableSkills, skillFamilies);
729
+ const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
603
730
  const sourceBySkill = yield* withSpinner(
604
- "Fetching selected skills",
605
- resolveSkillSources(packageRoot, projectDir, selectedSkills, options.dryRun !== true),
731
+ "Resolving selected skills",
732
+ resolveSkillSources(
733
+ packageRoot,
734
+ projectDir,
735
+ catalog,
736
+ selectedSelectors,
737
+ options.dryRun !== true,
738
+ ),
739
+ );
740
+ const selectedSkills: Array<CatalogSkill> = [];
741
+ for (const selector of selectedSelectors) {
742
+ const catalogSkill = catalogBySelector.get(selector);
743
+ if (catalogSkill === undefined) {
744
+ return yield* new InvalidProjectStateError({
745
+ message: `selected skill is unavailable: ${selector}`,
746
+ });
747
+ }
748
+ selectedSkills.push(catalogSkill);
749
+ }
750
+ const desired = yield* buildDesiredOutputs(
751
+ packageRoot,
752
+ projectDir,
753
+ sourceBySkill,
754
+ selectedSkills,
755
+ manifest.setup,
756
+ manifest.targets,
606
757
  );
607
- const desired = yield* buildDesiredOutputs(projectDir, sourceBySkill, selectedSkills, manifest.targets);
608
758
  const nextLock: DevKitLock = {
609
759
  version: 1,
610
760
  toolVersion: DEV_KIT_VERSION,
@@ -631,16 +781,38 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
631
781
  },
632
782
  }),
633
783
  },
634
- outputs: desired.map(({ resourceId, path: outputPath, skill, target, mode, kind, digest, catalog }) => ({
635
- resourceId,
636
- path: outputPath,
637
- skill,
638
- target,
639
- mode,
640
- kind,
641
- digest,
642
- ...(catalog ? { catalog } : {}),
643
- })),
784
+ outputs: desired.map((output): ManagedOutput => {
785
+ if ("skill" in output) {
786
+ return {
787
+ resourceId: output.resourceId,
788
+ path: output.path,
789
+ skill: output.skill,
790
+ target: output.target,
791
+ mode: output.mode,
792
+ kind: output.kind,
793
+ digest: output.digest,
794
+ ...(output.catalog ? { catalog: output.catalog } : {}),
795
+ };
796
+ }
797
+ if (output.resourceId === "setup:agent-instructions") {
798
+ return {
799
+ resourceId: output.resourceId,
800
+ path: output.path,
801
+ sourcePath: output.sourcePath,
802
+ mode: output.mode,
803
+ kind: output.kind,
804
+ digest: output.digest,
805
+ };
806
+ }
807
+ return {
808
+ resourceId: output.resourceId,
809
+ path: output.path,
810
+ sourcePath: output.sourcePath,
811
+ mode: output.mode,
812
+ kind: output.kind,
813
+ digest: output.digest,
814
+ };
815
+ }),
644
816
  };
645
817
  const reservedPaths = [
646
818
  { label: "manifest", path: manifestManaged.relative },
@@ -654,6 +826,17 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
654
826
  yield* validateReservedPaths(projectDir, reservedPaths, desired);
655
827
  const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
656
828
  const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
829
+ if (
830
+ manifest.setup.claudeInstructions.enabled &&
831
+ !manifest.setup.agentInstructions.enabled &&
832
+ currentState?.outputs.some(
833
+ (output) => output.resourceId === "setup:agent-instructions",
834
+ )
835
+ ) {
836
+ return yield* new InvalidProjectStateError({
837
+ message: "cannot disable agentInstructions while claudeInstructions still links to its AGENTS.md wrapper",
838
+ });
839
+ }
657
840
  yield* validateReservedPaths(
658
841
  projectDir,
659
842
  reservedPaths,
@@ -692,7 +875,10 @@ const formatAction = (action: SkillPlanAction): string => {
692
875
  const verb = action.desired.mode === "copy" ? "copy" : "link";
693
876
  const adoption = action.action === "unchanged" && action.adopted ? " (adopt)" : "";
694
877
  const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "=";
695
- return `${marker} ${verb} ${action.desired.skill} ${action.desired.path}${adoption}`;
878
+ const source = "skill" in action.desired
879
+ ? action.desired.skill
880
+ : action.desired.sourcePath;
881
+ return `${marker} ${verb} ${source} → ${action.desired.path}${adoption}`;
696
882
  };
697
883
 
698
884
  const operationalChangeCount = (plan: SkillPlan): number =>
@@ -734,6 +920,47 @@ const observationsEqual = (left: ObservedPath, right: ObservedPath): boolean =>
734
920
  left.kind === right.kind &&
735
921
  (left.kind === "missing" || (right.kind !== "missing" && left.digest === right.digest));
736
922
 
923
+ const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (
924
+ root: string,
925
+ ) {
926
+ const fs = yield* FileSystem.FileSystem;
927
+ const path = yield* Path.Path;
928
+ const pending = [root];
929
+ while (pending.length > 0) {
930
+ const current = pending.pop();
931
+ if (current === undefined) continue;
932
+ if ((yield* observeSymbolicLink(current)).kind === "symlink") return current;
933
+ const info = yield* fs.stat(current);
934
+ if (info.type !== "Directory") continue;
935
+ for (const entry of yield* fs.readDirectory(current)) {
936
+ pending.push(path.join(current, entry));
937
+ }
938
+ }
939
+ return undefined;
940
+ });
941
+
942
+ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(function* (
943
+ plan: SkillPlan,
944
+ ) {
945
+ const verified = new Set<string>();
946
+ for (const action of plan.actions) {
947
+ if (action.action === "remove" || action.action === "conflict") continue;
948
+ if (!("skill" in action.desired)) continue;
949
+ const catalog = action.desired.catalog;
950
+ if (catalog === undefined || !("package" in catalog)) continue;
951
+ const selector = `${catalog.package}#${catalog.skill}`;
952
+ const key = `${selector}\0${catalog.version}\0${catalog.digest}`;
953
+ if (verified.has(key)) continue;
954
+ const resolved = yield* resolvePackageSkillSelector(plan.projectDir, selector);
955
+ const observation = yield* observePath(resolved.path);
956
+ if (resolved.path !== action.desired.source || resolved.version !== catalog.version ||
957
+ observation.kind !== "directory" || observation.digest !== catalog.digest) {
958
+ return yield* new ApplyRaceError({ path: action.desired.source });
959
+ }
960
+ verified.add(key);
961
+ }
962
+ });
963
+
737
964
  const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function* (plan: SkillPlan) {
738
965
  const conflicts = plan.actions.filter((action) => action.action === "conflict");
739
966
  if (conflicts.length > 0) {
@@ -769,7 +996,17 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
769
996
  const staged = path.join(stageDir, String(stageIndex++));
770
997
  yield* fs.makeDirectory(path.dirname(staged), { recursive: true });
771
998
  if (action.desired.mode === "copy") {
772
- yield* fs.copy(action.desired.source, staged, { overwrite: true });
999
+ if (action.desired.kind === "file") {
1000
+ yield* fs.writeFileString(staged, action.desired.content, { mode: 0o644 });
1001
+ } else {
1002
+ yield* fs.copy(action.desired.source, staged, { overwrite: true });
1003
+ const symbolicLink = yield* findNestedSymbolicLink(staged);
1004
+ if (symbolicLink !== undefined) {
1005
+ return yield* new InvalidProjectStateError({
1006
+ message: `staged skill contains a symlink: ${action.desired.path}`,
1007
+ });
1008
+ }
1009
+ }
773
1010
  } else {
774
1011
  yield* fs.symlink(action.desired.linkTarget, staged);
775
1012
  }
@@ -780,6 +1017,8 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
780
1017
  stagedByResource.set(action.desired.resourceId, staged);
781
1018
  }
782
1019
 
1020
+ yield* verifyPackageSkillSources(plan);
1021
+
783
1022
  const stagedLock = path.join(tempDir, "next-lock.json");
784
1023
  const stagedState = path.join(tempDir, "next-state.json");
785
1024
  yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
@@ -0,0 +1,9 @@
1
+ <!-- DEV KIT START -->
2
+
3
+ # Dev Kit
4
+
5
+ This project uses `@danieljvdm/dev-kit` to manage portable agent skills and reproducible setup from `dev-kit.jsonc` and `dev-kit.lock.json`.
6
+
7
+ For dev-kit operations, use the `dev-kit` skill and read `{{DEV_KIT_SKILL_PATH}}` before changing managed outputs.
8
+
9
+ <!-- DEV KIT END -->