@danieljvdm/dev-kit 0.11.3 → 0.12.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.
Files changed (74) hide show
  1. package/README.md +70 -76
  2. package/dev-kit.example.jsonc +0 -4
  3. package/package.json +10 -6
  4. package/schema/dev-kit.schema.json +1 -46
  5. package/skills/build-effect-apis/SKILL.md +77 -0
  6. package/skills/build-effect-apis/agents/openai.yaml +4 -0
  7. package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
  8. package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
  9. package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
  10. package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
  11. package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
  12. package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
  13. package/skills/build-effect-apis/references/shared-contracts.md +108 -0
  14. package/skills/build-effect-apis/references/tanstack-start.md +86 -0
  15. package/skills/build-effect-apis/references/verification.md +50 -0
  16. package/skills/dev-kit/SKILL.md +58 -46
  17. package/skills/effect-architecture-audit/SKILL.md +26 -0
  18. package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
  19. package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
  20. package/skills/effect-ts/SKILL.md +21 -256
  21. package/skills/effect-ts/agents/openai.yaml +3 -3
  22. package/skills/testing/SKILL.md +5 -0
  23. package/src/catalog-manager.ts +16 -17
  24. package/src/catalog.ts +71 -16
  25. package/src/effect-source.ts +46 -24
  26. package/src/effect-tsgo.ts +49 -24
  27. package/src/gitignore.ts +5 -5
  28. package/src/index.ts +0 -6
  29. package/src/manifest.ts +0 -34
  30. package/src/node-symbolic-link.ts +2 -2
  31. package/src/oxfmt.js +5 -0
  32. package/src/oxfmt.ts +5 -0
  33. package/src/oxlint.js +5 -0
  34. package/src/oxlint.ts +5 -0
  35. package/src/package-skill-source.ts +51 -59
  36. package/src/path-digest.ts +7 -7
  37. package/src/project-package.ts +8 -7
  38. package/src/project-process-lock.ts +17 -12
  39. package/src/project-state.ts +1 -1
  40. package/src/skill-manager.ts +16 -14
  41. package/src/skill-selector.ts +12 -0
  42. package/src/sync.ts +170 -120
  43. package/src/tool-ignore-patterns.js +9 -0
  44. package/src/tool-ignore-patterns.ts +15 -0
  45. package/src/vendor.ts +67 -61
  46. package/src/vite-plus-dependency.ts +10 -11
  47. package/src/vite-plus-hooks.ts +24 -14
  48. package/src/vite-plus-quality.ts +21 -172
  49. package/src/vite-plus.js +81 -0
  50. package/src/vite-plus.ts +102 -0
  51. package/templates/AGENTS.md +1 -1
  52. package/skills/effect-ts/UPSTREAM.md +0 -28
  53. package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
  54. package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
  55. package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
  56. package/skills/effect-ts/references/atom-testing.md +0 -67
  57. package/skills/effect-ts/references/audit-services.md +0 -144
  58. package/skills/effect-ts/references/features.md +0 -525
  59. package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
  60. package/skills/effect-ts/references/guide-cli.md +0 -107
  61. package/skills/effect-ts/references/guide-datetime.md +0 -72
  62. package/skills/effect-ts/references/guide-effect.md +0 -440
  63. package/skills/effect-ts/references/guide-error-handling.md +0 -565
  64. package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
  65. package/skills/effect-ts/references/guide-layers.md +0 -989
  66. package/skills/effect-ts/references/guide-observability.md +0 -746
  67. package/skills/effect-ts/references/guide-retries.md +0 -434
  68. package/skills/effect-ts/references/guide-schedule.md +0 -343
  69. package/skills/effect-ts/references/guide-schema.md +0 -664
  70. package/skills/effect-ts/references/guide-sql.md +0 -536
  71. package/skills/effect-ts/references/guide-testing.md +0 -532
  72. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
  73. package/skills/effect-ts/references/version-and-source.md +0 -86
  74. package/templates/vite-plus/vite.config.ts +0 -22
package/src/sync.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Cause, Effect, FileSystem, Path, Schema, Stream } from "effect";
1
+ import { Cause, Effect, FileSystem, Path, Schema, SchemaGetter, Stream } from "effect";
2
2
  import { ChildProcess } from "effect/unstable/process";
3
3
  import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
4
4
 
@@ -31,6 +31,9 @@ import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-
31
31
  import {
32
32
  AppliedStateSchema,
33
33
  DevKitLockSchema,
34
+ EffectSourceLockSchema,
35
+ EffectTsgoLockSchema,
36
+ ManagedOutputSchema,
34
37
  type AppliedState,
35
38
  type DevKitLock,
36
39
  type ManagedAgentInstructionsOutput,
@@ -48,11 +51,8 @@ import {
48
51
  type VitePlusHooksPlan,
49
52
  } from "./vite-plus-hooks.ts";
50
53
  import {
51
- renderVitePlusConfigTemplate,
52
54
  renderVitePlusWorkflowTemplate,
53
55
  validateVitePlusQualitySupport,
54
- VITE_PLUS_CONFIG_PATH,
55
- VITE_PLUS_CONFIG_TEMPLATE,
56
56
  VITE_PLUS_GITHUB_ACTIONS_PATH,
57
57
  VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
58
58
  } from "./vite-plus-quality.ts";
@@ -149,7 +149,7 @@ export type SkillPlan = {
149
149
  readonly metadataChanged: boolean;
150
150
  };
151
151
 
152
- class ManifestNotFoundError extends Schema.TaggedErrorClass<ManifestNotFoundError>()(
152
+ class ManifestNotFoundError extends Schema.TaggedError<ManifestNotFoundError>()(
153
153
  "ManifestNotFoundError",
154
154
  { path: Schema.String },
155
155
  ) {
@@ -158,12 +158,12 @@ class ManifestNotFoundError extends Schema.TaggedErrorClass<ManifestNotFoundErro
158
158
  }
159
159
  }
160
160
 
161
- class StructuredFileError extends Schema.TaggedErrorClass<StructuredFileError>()(
162
- "StructuredFileError",
163
- { path: Schema.String, message: Schema.String },
164
- ) {}
161
+ class StructuredFileError extends Schema.TaggedError<StructuredFileError>()("StructuredFileError", {
162
+ path: Schema.String,
163
+ message: Schema.String,
164
+ }) {}
165
165
 
166
- class UnknownSkillOrFamilyError extends Schema.TaggedErrorClass<UnknownSkillOrFamilyError>()(
166
+ class UnknownSkillOrFamilyError extends Schema.TaggedError<UnknownSkillOrFamilyError>()(
167
167
  "UnknownSkillOrFamilyError",
168
168
  { name: Schema.String, known: Schema.Array(Schema.String) },
169
169
  ) {
@@ -172,12 +172,12 @@ class UnknownSkillOrFamilyError extends Schema.TaggedErrorClass<UnknownSkillOrFa
172
172
  }
173
173
  }
174
174
 
175
- class InvalidSkillCatalogError extends Schema.TaggedErrorClass<InvalidSkillCatalogError>()(
175
+ class InvalidSkillCatalogError extends Schema.TaggedError<InvalidSkillCatalogError>()(
176
176
  "InvalidSkillCatalogError",
177
177
  { family: Schema.String, message: Schema.String },
178
178
  ) {}
179
179
 
180
- class CommandError extends Schema.TaggedErrorClass<CommandError>()("CommandError", {
180
+ class CommandError extends Schema.TaggedError<CommandError>()("CommandError", {
181
181
  command: Schema.String,
182
182
  exitCode: Schema.Int,
183
183
  output: Schema.String,
@@ -189,7 +189,7 @@ class CommandError extends Schema.TaggedErrorClass<CommandError>()("CommandError
189
189
  }
190
190
  }
191
191
 
192
- class UnsafeManagedPathError extends Schema.TaggedErrorClass<UnsafeManagedPathError>()(
192
+ class UnsafeManagedPathError extends Schema.TaggedError<UnsafeManagedPathError>()(
193
193
  "UnsafeManagedPathError",
194
194
  { path: Schema.String, reason: Schema.String },
195
195
  ) {
@@ -198,17 +198,17 @@ class UnsafeManagedPathError extends Schema.TaggedErrorClass<UnsafeManagedPathEr
198
198
  }
199
199
  }
200
200
 
201
- class InvalidProjectStateError extends Schema.TaggedErrorClass<InvalidProjectStateError>()(
201
+ class InvalidProjectStateError extends Schema.TaggedError<InvalidProjectStateError>()(
202
202
  "InvalidProjectStateError",
203
203
  { message: Schema.String },
204
204
  ) {}
205
205
 
206
- class LockedPlanMismatchError extends Schema.TaggedErrorClass<LockedPlanMismatchError>()(
206
+ class LockedPlanMismatchError extends Schema.TaggedError<LockedPlanMismatchError>()(
207
207
  "LockedPlanMismatchError",
208
208
  { message: Schema.String },
209
209
  ) {}
210
210
 
211
- class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()("PlanConflictError", {
211
+ class PlanConflictError extends Schema.TaggedError<PlanConflictError>()("PlanConflictError", {
212
212
  conflicts: Schema.Array(Schema.String),
213
213
  }) {
214
214
  override get message() {
@@ -218,7 +218,7 @@ class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()("Pl
218
218
  }
219
219
  }
220
220
 
221
- class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRaceError", {
221
+ class ApplyRaceError extends Schema.TaggedError<ApplyRaceError>()("ApplyRaceError", {
222
222
  path: Schema.String,
223
223
  }) {
224
224
  override get message() {
@@ -226,9 +226,51 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
226
226
  }
227
227
  }
228
228
 
229
+ const fromJsonString = <S extends Schema.Constraint>(schema: S, space?: number) =>
230
+ space === undefined
231
+ ? Schema.fromJsonString(schema)
232
+ : Schema.String.pipe(
233
+ Schema.decodeTo(Schema.toCodecJson(schema), {
234
+ decode: SchemaGetter.parseJson(),
235
+ encode: SchemaGetter.stringifyJson({ space }),
236
+ }),
237
+ );
238
+
239
+ const DevKitSetupSchema = Schema.Struct({
240
+ effectSource: Schema.optional(EffectSourceLockSchema),
241
+ effectTsgo: Schema.optional(EffectTsgoLockSchema),
242
+ });
243
+ const OutputOwnershipIdentitySchema = Schema.Union([
244
+ Schema.Struct({
245
+ resourceId: Schema.String,
246
+ path: Schema.String,
247
+ mode: Schema.Literals(["copy", "symlink"]),
248
+ kind: Schema.Literals(["directory", "symlink"]),
249
+ skill: Schema.String,
250
+ target: Schema.Literals(["agents", "claude", "opencode"]),
251
+ }),
252
+ Schema.Struct({
253
+ resourceId: Schema.String,
254
+ path: Schema.String,
255
+ mode: Schema.Literals(["copy", "symlink"]),
256
+ kind: Schema.Literals(["file", "symlink"]),
257
+ sourcePath: Schema.String,
258
+ }),
259
+ ]);
260
+ const encodeAppliedStateJson = Schema.encodeSync(fromJsonString(AppliedStateSchema));
261
+ const encodeDevKitLockJson = Schema.encodeSync(fromJsonString(DevKitLockSchema));
262
+ const encodeDevKitLockPrettyJson = Schema.encodeSync(fromJsonString(DevKitLockSchema, 2));
263
+ const encodeDevKitSetupJson = Schema.encodeSync(fromJsonString(DevKitSetupSchema));
264
+ const encodeManifestJson = Schema.encodeSync(fromJsonString(DevKitManifestSchema));
265
+ const encodeManagedOutputJson = Schema.encodeSync(fromJsonString(ManagedOutputSchema));
266
+ const encodeOutputOwnershipIdentityJson = Schema.encodeSync(
267
+ fromJsonString(OutputOwnershipIdentitySchema),
268
+ );
269
+ const encodePlanSnapshotJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
270
+ const encodeAppliedStatePrettyJson = Schema.encodeSync(fromJsonString(AppliedStateSchema, 2));
271
+
229
272
  const SKILL_FAMILIES: SkillCatalog = {
230
- effect: ["effect-ts"],
231
- "effect-atom-data-fetching": ["effect-ts"],
273
+ effect: ["effect-ts", "effect-architecture-audit", "build-effect-apis"],
232
274
  };
233
275
 
234
276
  export const DEFAULT_MANIFEST = "dev-kit.jsonc";
@@ -236,6 +278,7 @@ const DEFAULT_LOCKFILE = "dev-kit.lock.json";
236
278
  const DEFAULT_STATE = ".dev-kit/state.json";
237
279
  const AGENT_INSTRUCTIONS_TEMPLATE = "templates/AGENTS.md";
238
280
  const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
281
+ const EFFECT_INSTRUCTIONS_PLACEHOLDER = "{{EFFECT_INSTRUCTIONS}}";
239
282
  const PROJECT_COMMAND_POLICY_PLACEHOLDER = "{{PROJECT_COMMAND_POLICY}}";
240
283
  const AGENT_INSTRUCTION_MARKERS = [
241
284
  { start: "<!-- DEV KIT START -->", end: "<!-- DEV KIT END -->" },
@@ -507,7 +550,7 @@ const runCommand = Effect.fn("runCommand")(function* (
507
550
  const trimmed = output.trim();
508
551
 
509
552
  if (exitCode !== 0) {
510
- return yield* new CommandError({ command: formatted, exitCode, output: trimmed });
553
+ return yield* CommandError.make({ command: formatted, exitCode, output: trimmed });
511
554
  }
512
555
 
513
556
  return trimmed;
@@ -527,14 +570,16 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
527
570
  const first = errors[0];
528
571
 
529
572
  if (first !== undefined) {
530
- return yield* new StructuredFileError({
573
+ return yield* StructuredFileError.make({
531
574
  path: filePath,
532
575
  message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
533
576
  });
534
577
  }
535
578
 
536
579
  return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
537
- Effect.mapError((cause) => new StructuredFileError({ path: filePath, message: cause.message })),
580
+ Effect.mapError((cause) =>
581
+ StructuredFileError.make({ path: filePath, message: cause.message }),
582
+ ),
538
583
  );
539
584
  });
540
585
 
@@ -542,7 +587,7 @@ const readManifest = Effect.fn("readManifest")(function* (manifestPath: string)
542
587
  const fs = yield* FileSystem.FileSystem;
543
588
 
544
589
  if (!(yield* fs.exists(manifestPath))) {
545
- return yield* new ManifestNotFoundError({ path: manifestPath });
590
+ return yield* ManifestNotFoundError.make({ path: manifestPath });
546
591
  }
547
592
  const raw = yield* fs.readFileString(manifestPath);
548
593
 
@@ -577,7 +622,7 @@ const expandSelection = (
577
622
  } else if (availableSkills.includes(name) || parseSkillSelector(name)?.type === "package") {
578
623
  selected.add(name);
579
624
  } else {
580
- return Effect.fail(new UnknownSkillOrFamilyError({ name, known }));
625
+ return Effect.fail(UnknownSkillOrFamilyError.make({ name, known }));
581
626
  }
582
627
  }
583
628
  for (const name of exclude) {
@@ -600,7 +645,7 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
600
645
  const path = yield* Path.Path;
601
646
 
602
647
  if (candidate.length === 0 || path.isAbsolute(candidate)) {
603
- return yield* new UnsafeManagedPathError({
648
+ return yield* UnsafeManagedPathError.make({
604
649
  path: candidate,
605
650
  reason: "must be a non-empty project-relative path",
606
651
  });
@@ -614,7 +659,7 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
614
659
  relative.startsWith(`..${path.sep}`) ||
615
660
  path.isAbsolute(relative)
616
661
  ) {
617
- return yield* new UnsafeManagedPathError({
662
+ return yield* UnsafeManagedPathError.make({
618
663
  path: candidate,
619
664
  reason: "resolves outside the project",
620
665
  });
@@ -628,7 +673,7 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
628
673
  const target = yield* observeSymbolicLink(ancestor);
629
674
 
630
675
  if (target.kind === "symlink") {
631
- return yield* new UnsafeManagedPathError({
676
+ return yield* UnsafeManagedPathError.make({
632
677
  path: candidate,
633
678
  reason: `ancestor is a symlink: ${portablePath(path, path.relative(projectDir, ancestor))}`,
634
679
  });
@@ -658,14 +703,14 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
658
703
  if (current === undefined) continue;
659
704
  for (const other of reserved.slice(index + 1)) {
660
705
  if (pathsOverlap(current.path, other.path)) {
661
- return yield* new InvalidProjectStateError({
706
+ return yield* InvalidProjectStateError.make({
662
707
  message: `${current.label} path ${current.path} overlaps ${other.label} path ${other.path}`,
663
708
  });
664
709
  }
665
710
  }
666
711
  for (const outputPath of outputPaths) {
667
712
  if (pathsOverlap(current.path, outputPath)) {
668
- return yield* new InvalidProjectStateError({
713
+ return yield* InvalidProjectStateError.make({
669
714
  message: `${current.label} path ${current.path} overlaps managed output ${outputPath}`,
670
715
  });
671
716
  }
@@ -673,28 +718,27 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
673
718
  }
674
719
  });
675
720
 
676
- const outputIdentity = (output: ManagedOutput) =>
677
- JSON.stringify({
678
- resourceId: output.resourceId,
679
- path: output.path,
680
- mode: output.mode,
681
- kind: output.kind,
682
- digest: output.digest,
683
- ...("skill" in output
684
- ? { skill: output.skill, target: output.target, catalog: output.catalog }
685
- : { sourcePath: output.sourcePath }),
686
- });
721
+ const outputIdentity = (output: ManagedOutput) => encodeManagedOutputJson(output);
687
722
 
688
723
  const outputOwnershipIdentity = (output: ManagedOutput) =>
689
- JSON.stringify({
690
- resourceId: output.resourceId,
691
- path: output.path,
692
- mode: output.mode,
693
- kind: output.kind,
694
- ...("skill" in output
695
- ? { skill: output.skill, target: output.target }
696
- : { sourcePath: output.sourcePath }),
697
- });
724
+ encodeOutputOwnershipIdentityJson(
725
+ "skill" in output
726
+ ? {
727
+ resourceId: output.resourceId,
728
+ path: output.path,
729
+ mode: output.mode,
730
+ kind: output.kind,
731
+ skill: output.skill,
732
+ target: output.target,
733
+ }
734
+ : {
735
+ resourceId: output.resourceId,
736
+ path: output.path,
737
+ mode: output.mode,
738
+ kind: output.kind,
739
+ sourcePath: output.sourcePath,
740
+ },
741
+ );
698
742
 
699
743
  const usesRawFileModeDigests = (toolVersion: string): boolean => {
700
744
  const match = /^(\d+)\.(\d+)\./.exec(toolVersion);
@@ -717,12 +761,12 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
717
761
 
718
762
  for (const output of outputs) {
719
763
  if (ids.has(output.resourceId)) {
720
- return yield* new InvalidProjectStateError({
764
+ return yield* InvalidProjectStateError.make({
721
765
  message: `${label} contains duplicate resource id ${output.resourceId}`,
722
766
  });
723
767
  }
724
768
  if (paths.has(output.path)) {
725
- return yield* new InvalidProjectStateError({
769
+ return yield* InvalidProjectStateError.make({
726
770
  message: `${label} contains duplicate path ${output.path}`,
727
771
  });
728
772
  }
@@ -737,7 +781,7 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
737
781
 
738
782
  if (previous === undefined || current === undefined) continue;
739
783
  if (current.startsWith(`${previous}/`)) {
740
- return yield* new InvalidProjectStateError({
784
+ return yield* InvalidProjectStateError.make({
741
785
  message: `${label} contains overlapping paths ${previous} and ${current}`,
742
786
  });
743
787
  }
@@ -761,7 +805,7 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
761
805
 
762
806
  if (previous === undefined || current === undefined) continue;
763
807
  if (current.startsWith(`${previous}/`)) {
764
- return yield* new InvalidProjectStateError({
808
+ return yield* InvalidProjectStateError.make({
765
809
  message: `desired and previously owned paths overlap: ${previous} and ${current}`,
766
810
  });
767
811
  }
@@ -772,7 +816,7 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
772
816
  packageRoot: string,
773
817
  projectDir: string,
774
818
  sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
775
- managesVitePlusQuality: boolean,
819
+ usesRecommendedVitePlusTasks: boolean,
776
820
  targets: ReturnType<typeof normalizeManifest>["targets"],
777
821
  ) {
778
822
  const fs = yield* FileSystem.FileSystem;
@@ -780,19 +824,24 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
780
824
  const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
781
825
 
782
826
  if ((yield* observePath(templatePath)).kind !== "file") {
783
- return yield* new InvalidProjectStateError({
827
+ return yield* InvalidProjectStateError.make({
784
828
  message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
785
829
  });
786
830
  }
787
831
  const template = yield* fs.readFileString(templatePath);
788
832
 
789
833
  if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
790
- return yield* new InvalidProjectStateError({
834
+ return yield* InvalidProjectStateError.make({
791
835
  message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
792
836
  });
793
837
  }
838
+ if (!template.includes(EFFECT_INSTRUCTIONS_PLACEHOLDER)) {
839
+ return yield* InvalidProjectStateError.make({
840
+ message: `dev-kit agent instructions template is missing ${EFFECT_INSTRUCTIONS_PLACEHOLDER}`,
841
+ });
842
+ }
794
843
  if (!template.includes(PROJECT_COMMAND_POLICY_PLACEHOLDER)) {
795
- return yield* new InvalidProjectStateError({
844
+ return yield* InvalidProjectStateError.make({
796
845
  message: `dev-kit agent instructions template is missing ${PROJECT_COMMAND_POLICY_PLACEHOLDER}`,
797
846
  });
798
847
  }
@@ -813,7 +862,24 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
813
862
  path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
814
863
  ),
815
864
  );
816
- const usesVitePlus = (yield* readDirectDependencyNames(projectDir)).includes("vite-plus");
865
+ const directDependencyNames = yield* readDirectDependencyNames(projectDir);
866
+ const usesVitePlus = directDependencyNames.includes("vite-plus");
867
+ const effectInstructions =
868
+ directDependencyNames.includes("effect") &&
869
+ (yield* observePath(path.join(projectDir, "node_modules", "effect", "AGENTS.md"))).kind ===
870
+ "file"
871
+ ? `# Learning more about the Effect
872
+
873
+ This repository uses the Effect Typescript library.
874
+
875
+ Before writing any Effect code, first read \`node_modules/effect/AGENTS.md\`
876
+ **completely**, and follow the links in the file when required.
877
+
878
+ If you need to learn more about particular Effect apis and concepts that the
879
+ guide doesn't cover, search through the source code in \`node_modules/effect/src\`.
880
+
881
+ `
882
+ : "";
817
883
  const projectPackage = yield* readProjectPackage(projectDir).pipe(
818
884
  Effect.catchTag("ProjectPackageError", (error) =>
819
885
  error.message.startsWith("package.json not found:") ? Effect.void : Effect.fail(error),
@@ -821,10 +887,11 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
821
887
  );
822
888
  const manager = yield* detectPackageManager(projectDir, projectPackage?.packageManager);
823
889
  const commandPolicy = usesVitePlus
824
- ? renderVitePlusCommandPolicy(projectPackage?.scripts ?? {}, managesVitePlusQuality)
890
+ ? renderVitePlusCommandPolicy(projectPackage?.scripts ?? {}, usesRecommendedVitePlusTasks)
825
891
  : renderPackageScriptCommandPolicy(manager, projectPackage?.scripts ?? {});
826
892
  const devKitInstructions = template
827
893
  .replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath)
894
+ .replaceAll(EFFECT_INSTRUCTIONS_PLACEHOLDER, effectInstructions)
828
895
  .replaceAll(PROJECT_COMMAND_POLICY_PLACEHOLDER, commandPolicy)
829
896
  .trimEnd();
830
897
 
@@ -840,7 +907,7 @@ const readGeneratedFileTemplate = Effect.fn("readGeneratedFileTemplate")(functio
840
907
  const templatePath = path.join(packageRoot, sourcePath);
841
908
 
842
909
  if ((yield* observePath(templatePath)).kind !== "file") {
843
- return yield* new InvalidProjectStateError({
910
+ return yield* InvalidProjectStateError.make({
844
911
  message: `dev-kit generated file template is not a regular file: ${sourcePath}`,
845
912
  });
846
913
  }
@@ -865,7 +932,9 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
865
932
  packageRoot,
866
933
  projectDir,
867
934
  sourceBySkill,
868
- setup.vitePlus.quality.config.enabled,
935
+ setup.vitePlus.quality.workflow.enabled &&
936
+ setup.vitePlus.quality.workflow.typecheck.length === 1 &&
937
+ setup.vitePlus.quality.workflow.typecheck[0] === "vp run typecheck",
869
938
  targets,
870
939
  );
871
940
 
@@ -887,7 +956,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
887
956
  : yield* observePath(source.absolute);
888
957
 
889
958
  if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
890
- return yield* new InvalidProjectStateError({
959
+ return yield* InvalidProjectStateError.make({
891
960
  message: "Claude instructions source is not a regular file: AGENTS.md",
892
961
  });
893
962
  }
@@ -905,23 +974,6 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
905
974
  linkTarget,
906
975
  });
907
976
  }
908
- if (setup.vitePlus.quality.config.enabled) {
909
- const managed = yield* resolveManagedPath(projectDir, VITE_PLUS_CONFIG_PATH);
910
- const template = yield* readGeneratedFileTemplate(packageRoot, VITE_PLUS_CONFIG_TEMPLATE);
911
- const content = renderVitePlusConfigTemplate(template, setup.vitePlus.quality.config.typecheck);
912
-
913
- outputs.push({
914
- resourceId: "setup:vite-plus-config",
915
- path: managed.relative,
916
- sourcePath: VITE_PLUS_CONFIG_TEMPLATE,
917
- mode: "copy",
918
- kind: "file",
919
- digest: yield* digestFileContent(content),
920
- destination: managed.absolute,
921
- content,
922
- adoptIfExact: true,
923
- });
924
- }
925
977
  if (setup.vitePlus.quality.workflow.enabled) {
926
978
  const managed = yield* resolveManagedPath(projectDir, VITE_PLUS_GITHUB_ACTIONS_PATH);
927
979
  const template = yield* readGeneratedFileTemplate(
@@ -958,7 +1010,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
958
1010
  .filter((skill) => skill.name === duplicateOutput.name)
959
1011
  .map((skill) => skill.selector);
960
1012
 
961
- return yield* new InvalidProjectStateError({
1013
+ return yield* InvalidProjectStateError.make({
962
1014
  message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
963
1015
  });
964
1016
  }
@@ -966,7 +1018,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
966
1018
  const resolvedSource = sourceBySkill.get(skill.selector);
967
1019
 
968
1020
  if (resolvedSource === undefined) {
969
- return yield* new InvalidProjectStateError({
1021
+ return yield* InvalidProjectStateError.make({
970
1022
  message: `skill source is unavailable: ${skill.selector}`,
971
1023
  });
972
1024
  }
@@ -974,7 +1026,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
974
1026
  const sourceObservation = yield* observePath(source);
975
1027
 
976
1028
  if (sourceObservation.kind !== "directory") {
977
- return yield* new InvalidProjectStateError({
1029
+ return yield* InvalidProjectStateError.make({
978
1030
  message: `skill source is not a directory: ${source}`,
979
1031
  });
980
1032
  }
@@ -1027,8 +1079,8 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
1027
1079
  return outputs.sort((left, right) => left.path.localeCompare(right.path));
1028
1080
  });
1029
1081
 
1030
- const canonicalLock = (lock: DevKitLock): string => `${JSON.stringify(lock, null, 2)}\n`;
1031
- const canonicalState = (state: AppliedState): string => `${JSON.stringify(state, null, 2)}\n`;
1082
+ const canonicalLock = (lock: DevKitLock): string => `${encodeDevKitLockPrettyJson(lock)}\n`;
1083
+ const canonicalState = (state: AppliedState): string => `${encodeAppliedStatePrettyJson(state)}\n`;
1032
1084
 
1033
1085
  const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
1034
1086
  projectDir: string,
@@ -1284,7 +1336,7 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
1284
1336
  const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
1285
1337
  current.toolVersion === next.toolVersion &&
1286
1338
  current.manifestDigest === next.manifestDigest &&
1287
- JSON.stringify(current.setup) === JSON.stringify(next.setup) &&
1339
+ encodeDevKitSetupJson(current.setup ?? {}) === encodeDevKitSetupJson(next.setup ?? {}) &&
1288
1340
  current.outputs.length === next.outputs.length &&
1289
1341
  current.outputs.every((output, index) => {
1290
1342
  const nextOutput = next.outputs[index];
@@ -1318,11 +1370,11 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1318
1370
  const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
1319
1371
 
1320
1372
  const vitePlusQuality = manifest.setup.vitePlus.quality;
1321
- const vitePlusQualityEnabled = vitePlusQuality.config.enabled || vitePlusQuality.workflow.enabled;
1373
+ const vitePlusQualityEnabled = vitePlusQuality.workflow.enabled;
1322
1374
 
1323
1375
  if (vitePlusQualityEnabled) {
1324
1376
  if (!manifest.setup.effectTsgo.enabled) {
1325
- return yield* new InvalidProjectStateError({
1377
+ return yield* InvalidProjectStateError.make({
1326
1378
  message:
1327
1379
  "setup.vitePlus.quality requires setup.effectTsgo.enabled so managed quality setup converges the Effect-patched compiler",
1328
1380
  });
@@ -1332,15 +1384,10 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1332
1384
  packageRoot,
1333
1385
  manifest.setup.effectTsgo.typescriptPackage,
1334
1386
  {
1335
- ...(vitePlusQuality.config.enabled ? { config: vitePlusQuality.config.typecheck } : {}),
1336
- ...(vitePlusQuality.workflow.enabled
1337
- ? {
1338
- workflow: {
1339
- beforeChecks: vitePlusQuality.workflow.beforeChecks,
1340
- typecheck: vitePlusQuality.workflow.typecheck,
1341
- },
1342
- }
1343
- : {}),
1387
+ workflow: {
1388
+ beforeChecks: vitePlusQuality.workflow.beforeChecks,
1389
+ typecheck: vitePlusQuality.workflow.typecheck,
1390
+ },
1344
1391
  },
1345
1392
  );
1346
1393
  }
@@ -1368,7 +1415,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1368
1415
 
1369
1416
  for (const [family, familySkills] of Object.entries(skillFamilies)) {
1370
1417
  if (availableSkills.includes(family)) {
1371
- return yield* new InvalidSkillCatalogError({
1418
+ return yield* InvalidSkillCatalogError.make({
1372
1419
  family,
1373
1420
  message: `family name conflicts with a skill name: ${family}`,
1374
1421
  });
@@ -1376,7 +1423,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1376
1423
  const missing = familySkills.filter((skill) => !availableSkills.includes(skill));
1377
1424
 
1378
1425
  if (missing.length > 0) {
1379
- return yield* new InvalidSkillCatalogError({
1426
+ return yield* InvalidSkillCatalogError.make({
1380
1427
  family,
1381
1428
  message: `family references missing skills: ${missing.join(", ")}`,
1382
1429
  });
@@ -1405,7 +1452,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1405
1452
  const catalogSkill = catalogBySelector.get(selector);
1406
1453
 
1407
1454
  if (catalogSkill === undefined) {
1408
- return yield* new InvalidProjectStateError({
1455
+ return yield* InvalidProjectStateError.make({
1409
1456
  message: `selected skill is unavailable: ${selector}`,
1410
1457
  });
1411
1458
  }
@@ -1422,7 +1469,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1422
1469
  const nextLock: DevKitLock = {
1423
1470
  version: 1,
1424
1471
  toolVersion: DEV_KIT_VERSION,
1425
- manifestDigest: yield* digestText(JSON.stringify(manifest)),
1472
+ manifestDigest: yield* digestText(encodeManifestJson(manifest)),
1426
1473
  setup: {
1427
1474
  ...(effectSource === undefined
1428
1475
  ? {}
@@ -1509,12 +1556,12 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1509
1556
  ]);
1510
1557
  if (options.locked) {
1511
1558
  if (!currentLock) {
1512
- return yield* new LockedPlanMismatchError({
1559
+ return yield* LockedPlanMismatchError.make({
1513
1560
  message: "dev-kit.lock.json is required with --locked",
1514
1561
  });
1515
1562
  }
1516
1563
  if (!lockedPlanMatches(currentLock, nextLock)) {
1517
- return yield* new LockedPlanMismatchError({
1564
+ return yield* LockedPlanMismatchError.make({
1518
1565
  message: "manifest or packaged skills differ from dev-kit.lock.json",
1519
1566
  });
1520
1567
  }
@@ -1534,7 +1581,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1534
1581
  );
1535
1582
 
1536
1583
  if (manifest.setup.claudeInstructions.enabled && removesClaudeInstructionsSource) {
1537
- return yield* new InvalidProjectStateError({
1584
+ return yield* InvalidProjectStateError.make({
1538
1585
  message:
1539
1586
  "cannot disable agentInstructions while claudeInstructions still links to an AGENTS.md that would be removed",
1540
1587
  });
@@ -1551,8 +1598,10 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1551
1598
  nextLock,
1552
1599
  nextState: planned.nextState,
1553
1600
  metadataChanged:
1554
- JSON.stringify(currentLock) !== JSON.stringify(nextLock) ||
1555
- JSON.stringify(currentState) !== JSON.stringify(planned.nextState),
1601
+ currentLock === undefined ||
1602
+ encodeDevKitLockJson(currentLock) !== encodeDevKitLockJson(nextLock) ||
1603
+ currentState === undefined ||
1604
+ encodeAppliedStateJson(currentState) !== encodeAppliedStateJson(planned.nextState),
1556
1605
  } satisfies SkillPlan;
1557
1606
  });
1558
1607
 
@@ -1652,12 +1701,11 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
1652
1701
  const observation = yield* observePath(resolved.path);
1653
1702
 
1654
1703
  if (
1655
- resolved.path !== action.desired.source ||
1656
1704
  resolved.version !== catalog.version ||
1657
1705
  observation.kind !== "directory" ||
1658
1706
  observation.digest !== catalog.digest
1659
1707
  ) {
1660
- return yield* new ApplyRaceError({ path: action.desired.source });
1708
+ return yield* ApplyRaceError.make({ path: action.desired.source });
1661
1709
  }
1662
1710
  verified.add(key);
1663
1711
  }
@@ -1667,7 +1715,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1667
1715
  const conflicts = plan.actions.filter((action) => action.action === "conflict");
1668
1716
 
1669
1717
  if (conflicts.length > 0) {
1670
- return yield* new PlanConflictError({
1718
+ return yield* PlanConflictError.make({
1671
1719
  conflicts: conflicts.map((action) =>
1672
1720
  action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
1673
1721
  ),
@@ -1677,7 +1725,9 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1677
1725
  const fs = yield* FileSystem.FileSystem;
1678
1726
  const path = yield* Path.Path;
1679
1727
  const mutating = plan.actions.filter(
1680
- (action): action is Exclude<SkillPlanAction, { readonly action: "unchanged" | "conflict" }> =>
1728
+ (
1729
+ action,
1730
+ ): action is Extract<SkillPlanAction, { readonly action: "create" | "update" | "remove" }> =>
1681
1731
  action.action === "create" || action.action === "update" || action.action === "remove",
1682
1732
  );
1683
1733
 
@@ -1686,7 +1736,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1686
1736
  action.action === "remove" ? action.destination : action.desired.destination;
1687
1737
 
1688
1738
  if (!observationsEqual(yield* observePath(destination), action.observed)) {
1689
- return yield* new ApplyRaceError({
1739
+ return yield* ApplyRaceError.make({
1690
1740
  path: action.action === "remove" ? action.previous.path : action.desired.path,
1691
1741
  });
1692
1742
  }
@@ -1718,7 +1768,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1718
1768
  yield* fs.writeFileString(staged, action.stagedContent);
1719
1769
  } else if (action.action === "remove") {
1720
1770
  if (action.stagedContent === undefined) {
1721
- return yield* new InvalidProjectStateError({
1771
+ return yield* InvalidProjectStateError.make({
1722
1772
  message: `missing staged content for ${action.previous.resourceId}`,
1723
1773
  });
1724
1774
  }
@@ -1733,7 +1783,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1733
1783
  const symbolicLink = yield* findNestedSymbolicLink(staged);
1734
1784
 
1735
1785
  if (symbolicLink !== undefined) {
1736
- return yield* new InvalidProjectStateError({
1786
+ return yield* InvalidProjectStateError.make({
1737
1787
  message: `staged skill contains a symlink: ${action.desired.path}`,
1738
1788
  });
1739
1789
  }
@@ -1753,7 +1803,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1753
1803
  : undefined;
1754
1804
 
1755
1805
  if (observation.kind !== "file" || digest !== action.desired.digest) {
1756
- return yield* new InvalidProjectStateError({
1806
+ return yield* InvalidProjectStateError.make({
1757
1807
  message: `staged output digest mismatch for ${action.desired.path}`,
1758
1808
  });
1759
1809
  }
@@ -1761,7 +1811,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1761
1811
  observation.kind !== action.desired.kind ||
1762
1812
  observation.digest !== action.desired.digest
1763
1813
  ) {
1764
- return yield* new InvalidProjectStateError({
1814
+ return yield* InvalidProjectStateError.make({
1765
1815
  message: `staged output digest mismatch for ${action.desired.path}`,
1766
1816
  });
1767
1817
  }
@@ -1800,7 +1850,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1800
1850
  (action.action !== "remove" || action.stagedContent !== undefined) &&
1801
1851
  staged === undefined
1802
1852
  ) {
1803
- return yield* new InvalidProjectStateError({
1853
+ return yield* InvalidProjectStateError.make({
1804
1854
  message: `missing staged output for ${
1805
1855
  action.action === "remove" ? action.previous.resourceId : action.desired.resourceId
1806
1856
  }`,
@@ -1849,7 +1899,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1849
1899
  replacement.expected !== undefined &&
1850
1900
  !observationsEqual(observed, replacement.expected)
1851
1901
  ) {
1852
- return yield* new ApplyRaceError({ path: replacement.path });
1902
+ return yield* ApplyRaceError.make({ path: replacement.path });
1853
1903
  }
1854
1904
  if (observed.kind !== "missing") {
1855
1905
  yield* fs.makeDirectory(path.dirname(replacement.backup), { recursive: true });
@@ -1887,7 +1937,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1887
1937
  const conflicts = plan.actions.filter((action) => action.action === "conflict");
1888
1938
 
1889
1939
  if (conflicts.length > 0) {
1890
- return yield* new PlanConflictError({
1940
+ return yield* PlanConflictError.make({
1891
1941
  conflicts: conflicts.map((action) =>
1892
1942
  action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
1893
1943
  ),
@@ -1897,7 +1947,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1897
1947
 
1898
1948
  yield* acquireProjectProcessLock(plan.projectDir);
1899
1949
  const replanned = yield* planProjectSkills(options);
1900
- const originalSignature = JSON.stringify({
1950
+ const originalSignature = encodePlanSnapshotJson({
1901
1951
  actions: plan.actions,
1902
1952
  effectSource: plan.effectSource,
1903
1953
  effectTsgo: plan.effectTsgo,
@@ -1905,7 +1955,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1905
1955
  nextLock: plan.nextLock,
1906
1956
  nextState: plan.nextState,
1907
1957
  });
1908
- const nextSignature = JSON.stringify({
1958
+ const nextSignature = encodePlanSnapshotJson({
1909
1959
  actions: replanned.actions,
1910
1960
  effectSource: replanned.effectSource,
1911
1961
  effectTsgo: replanned.effectTsgo,
@@ -1915,7 +1965,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1915
1965
  });
1916
1966
 
1917
1967
  if (originalSignature !== nextSignature) {
1918
- return yield* new ApplyRaceError({ path: "project state" });
1968
+ return yield* ApplyRaceError.make({ path: "project state" });
1919
1969
  }
1920
1970
  const changes = plannedChangeCount(replanned);
1921
1971