@danieljvdm/dev-kit 0.14.0 → 0.15.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/manifest.ts CHANGED
@@ -53,31 +53,28 @@ export const VitePlusHooksSetupSchema = Schema.Struct({
53
53
  enabled: Schema.optional(Schema.Boolean),
54
54
  });
55
55
 
56
- export const VitePlusQualityWorkflowStepSchema = Schema.Struct({
57
- name: Schema.String,
58
- run: Schema.Array(Schema.String),
59
- });
60
- export type VitePlusQualityWorkflowStep = typeof VitePlusQualityWorkflowStepSchema.Type;
61
-
62
- export const VitePlusQualityWorkflowSetupSchema = Schema.Struct({
56
+ export const VitePlusWorkflowSetupSchema = Schema.Struct({
63
57
  enabled: Schema.optional(Schema.Boolean),
64
- beforeChecks: Schema.optional(Schema.Array(VitePlusQualityWorkflowStepSchema)),
65
- typecheck: Schema.optional(Schema.Array(Schema.String)),
66
- });
67
- export type VitePlusQualityWorkflowSetup = typeof VitePlusQualityWorkflowSetupSchema.Type;
68
-
69
- export const VitePlusQualitySetupSchema = Schema.Struct({
70
- workflow: Schema.optional(VitePlusQualityWorkflowSetupSchema),
71
58
  });
72
- export type VitePlusQualitySetup = typeof VitePlusQualitySetupSchema.Type;
59
+ export type VitePlusWorkflowSetup = typeof VitePlusWorkflowSetupSchema.Type;
73
60
 
74
61
  export const VitePlusSetupSchema = Schema.Struct({
75
62
  hooks: Schema.optional(VitePlusHooksSetupSchema),
76
- quality: Schema.optional(VitePlusQualitySetupSchema),
63
+ workflow: Schema.optional(VitePlusWorkflowSetupSchema),
77
64
  });
78
65
 
79
66
  export type VitePlusSetup = typeof VitePlusSetupSchema.Type;
80
67
 
68
+ export const WorktrunkConfigSetupSchema = Schema.Struct({
69
+ enabled: Schema.optional(Schema.Boolean),
70
+ });
71
+ export type WorktrunkConfigSetup = typeof WorktrunkConfigSetupSchema.Type;
72
+
73
+ export const WorktrunkSetupSchema = Schema.Struct({
74
+ config: Schema.optional(WorktrunkConfigSetupSchema),
75
+ });
76
+ export type WorktrunkSetup = typeof WorktrunkSetupSchema.Type;
77
+
81
78
  export const DevKitManifestSchema = Schema.Struct({
82
79
  $schema: Schema.optional(Schema.String),
83
80
  include: Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN))),
@@ -91,6 +88,7 @@ export const DevKitManifestSchema = Schema.Struct({
91
88
  effectSource: Schema.optional(EffectSourceSetupSchema),
92
89
  effectTsgo: Schema.optional(EffectTsgoSetupSchema),
93
90
  vitePlus: Schema.optional(VitePlusSetupSchema),
91
+ worktrunk: Schema.optional(WorktrunkSetupSchema),
94
92
  }),
95
93
  ),
96
94
  targets: Schema.optional(
@@ -135,12 +133,13 @@ export type NormalizedManifest = {
135
133
  readonly hooks: {
136
134
  readonly enabled: boolean;
137
135
  };
138
- readonly quality: {
139
- readonly workflow: {
140
- readonly enabled: boolean;
141
- readonly beforeChecks: ReadonlyArray<VitePlusQualityWorkflowStep>;
142
- readonly typecheck: ReadonlyArray<string>;
143
- };
136
+ readonly workflow: {
137
+ readonly enabled: boolean;
138
+ };
139
+ };
140
+ readonly worktrunk: {
141
+ readonly config: {
142
+ readonly enabled: boolean;
144
143
  };
145
144
  };
146
145
  };
@@ -175,7 +174,6 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
175
174
  };
176
175
  }
177
176
  }
178
- const quality = manifest.setup?.vitePlus?.quality;
179
177
 
180
178
  return {
181
179
  exclude: manifest.exclude ?? [],
@@ -203,12 +201,13 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
203
201
  hooks: {
204
202
  enabled: manifest.setup?.vitePlus?.hooks?.enabled ?? false,
205
203
  },
206
- quality: {
207
- workflow: {
208
- enabled: quality?.workflow?.enabled ?? false,
209
- beforeChecks: quality?.workflow?.beforeChecks ?? [],
210
- typecheck: quality?.workflow?.typecheck ?? ["vp run typecheck"],
211
- },
204
+ workflow: {
205
+ enabled: manifest.setup?.vitePlus?.workflow?.enabled ?? false,
206
+ },
207
+ },
208
+ worktrunk: {
209
+ config: {
210
+ enabled: manifest.setup?.worktrunk?.config?.enabled ?? false,
212
211
  },
213
212
  },
214
213
  },
@@ -27,7 +27,6 @@ const textEncoder = new TextEncoder();
27
27
  // Git preserves only the executable distinction for regular files. Canonicalizing
28
28
  // the remaining bits keeps digests stable across checkout and copy umasks.
29
29
  const canonicalFileMode = (mode: number): number => ((mode & 0o111) === 0 ? 0o644 : 0o755);
30
- const rawFileMode = (mode: number): number => mode & 0o777;
31
30
 
32
31
  const frame = (value: string | Uint8Array): Uint8Array => {
33
32
  const bytes = typeof value === "string" ? textEncoder.encode(value) : value;
@@ -153,18 +152,6 @@ export const observePath = Effect.fn("observeManagedPath")(function* (absolutePa
153
152
  );
154
153
  });
155
154
 
156
- export const observePathWithRawModes = Effect.fn("observeManagedPathWithRawModes")(function* (
157
- absolutePath: string,
158
- ) {
159
- return yield* digestFileSystemPath(absolutePath, rawFileMode).pipe(
160
- Effect.mapError((cause) =>
161
- Schema.is(PathInspectionError)(cause)
162
- ? cause
163
- : PathInspectionError.make({ path: absolutePath, operation: "inspect", cause }),
164
- ),
165
- );
166
- });
167
-
168
155
  export const digestText = Effect.fn("digestText")(function* (value: string) {
169
156
  return yield* digestFrames(["text-v1", value]);
170
157
  });
@@ -40,6 +40,57 @@ export const readProjectPackage = Effect.fn("readProjectPackage")(function* (pro
40
40
  return manifest;
41
41
  });
42
42
 
43
+ export const PACKAGE_MANAGER_COMMANDS = {
44
+ bun: { install: "bun install", label: "Bun" },
45
+ npm: { install: "npm install", label: "npm" },
46
+ pnpm: { install: "pnpm install", label: "pnpm" },
47
+ yarn: { install: "yarn install", label: "Yarn" },
48
+ } as const;
49
+
50
+ export type PackageManagerName = keyof typeof PACKAGE_MANAGER_COMMANDS;
51
+
52
+ const packageManagerName = (declaration: string | undefined): PackageManagerName | undefined => {
53
+ const name = declaration?.split("@", 1)[0];
54
+
55
+ return name !== undefined && name in PACKAGE_MANAGER_COMMANDS
56
+ ? (name as PackageManagerName)
57
+ : undefined;
58
+ };
59
+
60
+ export const detectPackageManager = Effect.fn("detectPackageManager")(function* (
61
+ projectDir: string,
62
+ declaration: string | undefined,
63
+ ) {
64
+ const declared = packageManagerName(declaration);
65
+
66
+ if (declared !== undefined || declaration !== undefined) return declared;
67
+ const fs = yield* FileSystem.FileSystem;
68
+ const path = yield* Path.Path;
69
+ const lockfiles: ReadonlyArray<readonly [PackageManagerName, ReadonlyArray<string>]> = [
70
+ ["bun", ["bun.lock", "bun.lockb"]],
71
+ ["npm", ["package-lock.json", "npm-shrinkwrap.json"]],
72
+ ["pnpm", ["pnpm-lock.yaml"]],
73
+ ["yarn", ["yarn.lock"]],
74
+ ];
75
+ const detected: Array<PackageManagerName> = [];
76
+
77
+ for (const [manager, files] of lockfiles) {
78
+ let found = false;
79
+
80
+ for (const file of files) {
81
+ if (yield* fs.exists(path.join(projectDir, file))) {
82
+ found = true;
83
+ break;
84
+ }
85
+ }
86
+ if (found) {
87
+ detected.push(manager);
88
+ }
89
+ }
90
+
91
+ return detected.length === 1 ? detected[0] : undefined;
92
+ });
93
+
43
94
  export const readDirectDependencyNames = Effect.fn("readDirectDependencyNames")(function* (
44
95
  projectDir: string,
45
96
  ) {
@@ -49,6 +49,9 @@ export const ManagedClaudeInstructionsOutputSchema = Schema.Struct({
49
49
  });
50
50
  export type ManagedClaudeInstructionsOutput = typeof ManagedClaudeInstructionsOutputSchema.Type;
51
51
 
52
+ // Legacy (dev-kit ≤0.14) lock entry for the previously managed check workflow.
53
+ // Kept only so old locks and receipts still decode; planning discards these
54
+ // entries, releasing the file to the repository. Never produced anymore.
52
55
  export const ManagedGeneratedFileOutputSchema = Schema.Struct({
53
56
  resourceId: Schema.Literal("setup:vite-plus-github-actions"),
54
57
  path: Schema.String,
@@ -0,0 +1,79 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+
3
+ import { observeSymbolicLink } from "./node-symbolic-link.ts";
4
+
5
+ export class ScaffoldTemplateError extends Schema.TaggedError<ScaffoldTemplateError>()(
6
+ "ScaffoldTemplateError",
7
+ { message: Schema.String },
8
+ ) {}
9
+
10
+ export type ScaffoldPlan = {
11
+ readonly action: "scaffold" | "unchanged";
12
+ readonly path: string;
13
+ readonly destination: string;
14
+ readonly content?: string;
15
+ };
16
+
17
+ export const replaceUniqueTemplateMarker = (
18
+ template: string,
19
+ marker: string,
20
+ replacement: string,
21
+ ): string => {
22
+ const parts = template.split(marker);
23
+
24
+ if (parts.length !== 2) {
25
+ throw new Error(`expected exactly one generated template marker: ${marker}`);
26
+ }
27
+
28
+ return `${parts[0]}${replacement}${parts[1]}`;
29
+ };
30
+
31
+ export const readScaffoldTemplate = Effect.fn("readScaffoldTemplate")(function* (
32
+ packageRoot: string,
33
+ templatePath: string,
34
+ ) {
35
+ const fs = yield* FileSystem.FileSystem;
36
+ const path = yield* Path.Path;
37
+ const absolute = path.join(packageRoot, templatePath);
38
+
39
+ if (!(yield* fs.exists(absolute))) {
40
+ return yield* ScaffoldTemplateError.make({
41
+ message: `dev-kit scaffold template is missing: ${templatePath}`,
42
+ });
43
+ }
44
+
45
+ return yield* fs.readFileString(absolute);
46
+ });
47
+
48
+ // Scaffolds are created once and repository-owned afterwards: an existing
49
+ // destination is never read, compared, updated, or removed, and `content` is
50
+ // only computed (and validated) when the file is actually being created.
51
+ export const planScaffold = Effect.fn("planScaffold")(function* <E, R>(options: {
52
+ readonly projectDir: string;
53
+ readonly path: string;
54
+ readonly content: Effect.Effect<string, E, R>;
55
+ }) {
56
+ const path = yield* Path.Path;
57
+ const destination = path.join(options.projectDir, ...options.path.split("/"));
58
+ const observed = yield* observeSymbolicLink(destination);
59
+
60
+ if (observed.kind !== "missing") {
61
+ return { action: "unchanged", path: options.path, destination } satisfies ScaffoldPlan;
62
+ }
63
+
64
+ return {
65
+ action: "scaffold",
66
+ path: options.path,
67
+ destination,
68
+ content: yield* options.content,
69
+ } satisfies ScaffoldPlan;
70
+ });
71
+
72
+ export const applyScaffoldPlan = Effect.fn("applyScaffoldPlan")(function* (plan: ScaffoldPlan) {
73
+ if (plan.action !== "scaffold" || plan.content === undefined) return;
74
+ const fs = yield* FileSystem.FileSystem;
75
+ const path = yield* Path.Path;
76
+
77
+ yield* fs.makeDirectory(path.dirname(plan.destination), { recursive: true });
78
+ yield* fs.writeFileString(plan.destination, plan.content);
79
+ });
package/src/sync.ts CHANGED
@@ -23,10 +23,15 @@ import {
23
23
  digestSymlinkTarget,
24
24
  digestText,
25
25
  observePath,
26
- observePathWithRawModes,
27
26
  type ObservedPath,
28
27
  } from "./path-digest.ts";
29
- import { readDirectDependencyNames, readProjectPackage } from "./project-package.ts";
28
+ import {
29
+ detectPackageManager,
30
+ PACKAGE_MANAGER_COMMANDS,
31
+ readDirectDependencyNames,
32
+ readProjectPackage,
33
+ type PackageManagerName,
34
+ } from "./project-package.ts";
30
35
  import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-process-lock.ts";
31
36
  import {
32
37
  AppliedStateSchema,
@@ -38,11 +43,11 @@ import {
38
43
  type DevKitLock,
39
44
  type ManagedAgentInstructionsOutput,
40
45
  type ManagedClaudeInstructionsOutput,
41
- type ManagedGeneratedFileOutput,
42
46
  type ManagedOutput,
43
47
  type ManagedSkillOutput,
44
48
  type OwnershipReceipt,
45
49
  } from "./project-state.ts";
50
+ import { applyScaffoldPlan, type ScaffoldPlan } from "./scaffold.ts";
46
51
  import { parseSkillSelector } from "./skill-selector.ts";
47
52
  import { DEV_KIT_VERSION } from "./tool-metadata.ts";
48
53
  import {
@@ -50,12 +55,8 @@ import {
50
55
  planVitePlusHooks,
51
56
  type VitePlusHooksPlan,
52
57
  } from "./vite-plus-hooks.ts";
53
- import {
54
- renderVitePlusWorkflowTemplate,
55
- validateVitePlusQualitySupport,
56
- VITE_PLUS_GITHUB_ACTIONS_PATH,
57
- VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
58
- } from "./vite-plus-quality.ts";
58
+ import { planVitePlusWorkflow } from "./vite-plus-workflow.ts";
59
+ import { planWorktrunkConfig } from "./worktrunk-config.ts";
59
60
 
60
61
  export type SyncOptions = {
61
62
  readonly manifestPath?: string;
@@ -98,17 +99,10 @@ type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
98
99
  readonly linkTarget: string;
99
100
  };
100
101
 
101
- type DesiredGeneratedFileOutput = ManagedGeneratedFileOutput & {
102
- readonly adoptIfExact: true;
103
- readonly content: string;
104
- readonly destination: string;
105
- };
106
-
107
102
  type DesiredOutput =
108
103
  | DesiredSkillOutput
109
104
  | DesiredAgentInstructionsOutput
110
- | DesiredClaudeInstructionsOutput
111
- | DesiredGeneratedFileOutput;
105
+ | DesiredClaudeInstructionsOutput;
112
106
 
113
107
  type SkillPlanAction =
114
108
  | {
@@ -144,6 +138,8 @@ export type SkillPlan = {
144
138
  readonly effectSource?: EffectSourcePlan;
145
139
  readonly effectTsgo?: EffectTsgoPatchPlan;
146
140
  readonly vitePlusHooks?: VitePlusHooksPlan;
141
+ readonly vitePlusWorkflow?: ScaffoldPlan;
142
+ readonly worktrunkConfig?: ScaffoldPlan;
147
143
  readonly nextLock: DevKitLock;
148
144
  readonly nextState: AppliedState;
149
145
  readonly metadataChanged: boolean;
@@ -436,57 +432,6 @@ const renderVitePlusCommandPolicy = (
436
432
  ].join("\n");
437
433
  };
438
434
 
439
- const PACKAGE_MANAGER_COMMANDS = {
440
- bun: { install: "bun install", label: "Bun" },
441
- npm: { install: "npm install", label: "npm" },
442
- pnpm: { install: "pnpm install", label: "pnpm" },
443
- yarn: { install: "yarn install", label: "Yarn" },
444
- } as const;
445
-
446
- type PackageManagerName = keyof typeof PACKAGE_MANAGER_COMMANDS;
447
-
448
- const packageManagerName = (declaration: string | undefined): PackageManagerName | undefined => {
449
- const name = declaration?.split("@", 1)[0];
450
-
451
- return name !== undefined && name in PACKAGE_MANAGER_COMMANDS
452
- ? (name as PackageManagerName)
453
- : undefined;
454
- };
455
-
456
- const detectPackageManager = Effect.fn("detectPackageManager")(function* (
457
- projectDir: string,
458
- declaration: string | undefined,
459
- ) {
460
- const declared = packageManagerName(declaration);
461
-
462
- if (declared !== undefined || declaration !== undefined) return declared;
463
- const fs = yield* FileSystem.FileSystem;
464
- const path = yield* Path.Path;
465
- const lockfiles: ReadonlyArray<readonly [PackageManagerName, ReadonlyArray<string>]> = [
466
- ["bun", ["bun.lock", "bun.lockb"]],
467
- ["npm", ["package-lock.json", "npm-shrinkwrap.json"]],
468
- ["pnpm", ["pnpm-lock.yaml"]],
469
- ["yarn", ["yarn.lock"]],
470
- ];
471
- const detected: Array<PackageManagerName> = [];
472
-
473
- for (const [manager, files] of lockfiles) {
474
- let found = false;
475
-
476
- for (const file of files) {
477
- if (yield* fs.exists(path.join(projectDir, file))) {
478
- found = true;
479
- break;
480
- }
481
- }
482
- if (found) {
483
- detected.push(manager);
484
- }
485
- }
486
-
487
- return detected.length === 1 ? detected[0] : undefined;
488
- });
489
-
490
435
  const renderPackageScriptCommandPolicy = (
491
436
  manager: PackageManagerName | undefined,
492
437
  scripts: Readonly<Record<string, string>>,
@@ -746,16 +691,6 @@ const outputOwnershipIdentity = (output: ManagedOutput) =>
746
691
  },
747
692
  );
748
693
 
749
- const usesRawFileModeDigests = (toolVersion: string): boolean => {
750
- const match = /^(\d+)\.(\d+)\./.exec(toolVersion);
751
-
752
- if (match === null) return false;
753
- const major = Number(match[1]);
754
- const minor = Number(match[2]);
755
-
756
- return major === 0 && minor <= 6;
757
- };
758
-
759
694
  const validateInventory = Effect.fn("validateManagedInventory")(function* (
760
695
  projectDir: string,
761
696
  outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
@@ -904,23 +839,6 @@ guide doesn't cover, search through the source code in \`node_modules/effect/src
904
839
  return `${devKitInstructions}\n`;
905
840
  });
906
841
 
907
- const readGeneratedFileTemplate = Effect.fn("readGeneratedFileTemplate")(function* (
908
- packageRoot: string,
909
- sourcePath: string,
910
- ) {
911
- const fs = yield* FileSystem.FileSystem;
912
- const path = yield* Path.Path;
913
- const templatePath = path.join(packageRoot, sourcePath);
914
-
915
- if ((yield* observePath(templatePath)).kind !== "file") {
916
- return yield* InvalidProjectStateError.make({
917
- message: `dev-kit generated file template is not a regular file: ${sourcePath}`,
918
- });
919
- }
920
-
921
- return yield* fs.readFileString(templatePath);
922
- });
923
-
924
842
  const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
925
843
  packageRoot: string,
926
844
  projectDir: string,
@@ -938,9 +856,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
938
856
  packageRoot,
939
857
  projectDir,
940
858
  sourceBySkill,
941
- setup.vitePlus.quality.workflow.enabled &&
942
- setup.vitePlus.quality.workflow.typecheck.length === 1 &&
943
- setup.vitePlus.quality.workflow.typecheck[0] === "vp run typecheck",
859
+ setup.vitePlus.workflow.enabled,
944
860
  targets,
945
861
  );
946
862
 
@@ -980,32 +896,6 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
980
896
  linkTarget,
981
897
  });
982
898
  }
983
- if (setup.vitePlus.quality.workflow.enabled) {
984
- const managed = yield* resolveManagedPath(projectDir, VITE_PLUS_GITHUB_ACTIONS_PATH);
985
- const template = yield* readGeneratedFileTemplate(
986
- packageRoot,
987
- VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
988
- );
989
- const content = renderVitePlusWorkflowTemplate(template, {
990
- devKitCommand:
991
- projectDir === packageRoot
992
- ? "./bin/dev-kit.mjs apply --locked"
993
- : "bun ./node_modules/@danieljvdm/dev-kit/bin/dev-kit.mjs apply --locked",
994
- workflow: setup.vitePlus.quality.workflow,
995
- });
996
-
997
- outputs.push({
998
- resourceId: "setup:vite-plus-github-actions",
999
- path: managed.relative,
1000
- sourcePath: VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
1001
- mode: "copy",
1002
- kind: "file",
1003
- digest: yield* digestFileContent(content),
1004
- destination: managed.absolute,
1005
- content,
1006
- adoptIfExact: true,
1007
- });
1008
- }
1009
899
  const agentsTarget = targets.agents;
1010
900
  const duplicateOutput = skills.find(
1011
901
  (skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
@@ -1116,20 +1006,10 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
1116
1006
  observed.kind === locked.kind
1117
1007
  ? locked
1118
1008
  : undefined;
1119
- const rawModeObservation =
1120
- matchingLockedOutput !== undefined &&
1121
- observed.kind !== "missing" &&
1122
- observed.digest !== matchingLockedOutput.digest &&
1123
- currentLock !== undefined &&
1124
- usesRawFileModeDigests(currentLock.toolVersion)
1125
- ? yield* observePathWithRawModes(output.destination)
1126
- : undefined;
1127
1009
  const lockedOwnsObserved =
1128
1010
  matchingLockedOutput !== undefined &&
1129
1011
  observed.kind !== "missing" &&
1130
- (observed.digest === matchingLockedOutput.digest ||
1131
- (rawModeObservation?.kind === matchingLockedOutput.kind &&
1132
- rawModeObservation.digest === matchingLockedOutput.digest));
1012
+ observed.digest === matchingLockedOutput.digest;
1133
1013
 
1134
1014
  if (output.resourceId === "setup:agent-instructions" && "content" in output) {
1135
1015
  if (observed.kind === "missing") {
@@ -1217,7 +1097,7 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
1217
1097
  if (observed.kind === "missing") {
1218
1098
  actions.push({ action: "create", desired: output, observed });
1219
1099
  } else if (observed.kind === output.kind && observed.digest === output.digest) {
1220
- if (sameReceipt || lockedOwnsObserved || ("adoptIfExact" in output && output.adoptIfExact)) {
1100
+ if (sameReceipt || lockedOwnsObserved) {
1221
1101
  actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
1222
1102
  } else {
1223
1103
  actions.push({
@@ -1375,28 +1255,6 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1375
1255
  const packageRoot = yield* resolvePackageRoot();
1376
1256
  const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
1377
1257
 
1378
- const vitePlusQuality = manifest.setup.vitePlus.quality;
1379
- const vitePlusQualityEnabled = vitePlusQuality.workflow.enabled;
1380
-
1381
- if (vitePlusQualityEnabled) {
1382
- if (!manifest.setup.effectTsgo.enabled) {
1383
- return yield* InvalidProjectStateError.make({
1384
- message:
1385
- "setup.vitePlus.quality requires setup.effectTsgo.enabled so managed quality setup converges the Effect-patched compiler",
1386
- });
1387
- }
1388
- yield* validateVitePlusQualitySupport(
1389
- projectDir,
1390
- packageRoot,
1391
- manifest.setup.effectTsgo.typescriptPackage,
1392
- {
1393
- workflow: {
1394
- beforeChecks: vitePlusQuality.workflow.beforeChecks,
1395
- typecheck: vitePlusQuality.workflow.typecheck,
1396
- },
1397
- },
1398
- );
1399
- }
1400
1258
  const effectSource = manifest.setup.effectSource.enabled
1401
1259
  ? yield* planEffectSource({
1402
1260
  packageName: manifest.setup.effectSource.packageName,
@@ -1415,6 +1273,17 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1415
1273
  const vitePlusHooks = manifest.setup.vitePlus.hooks.enabled
1416
1274
  ? yield* planVitePlusHooks(projectDir)
1417
1275
  : undefined;
1276
+ const vitePlusWorkflow = manifest.setup.vitePlus.workflow.enabled
1277
+ ? yield* planVitePlusWorkflow({
1278
+ packageRoot,
1279
+ projectDir,
1280
+ effectTsgoEnabled: manifest.setup.effectTsgo.enabled,
1281
+ typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
1282
+ })
1283
+ : undefined;
1284
+ const worktrunkConfig = manifest.setup.worktrunk.config.enabled
1285
+ ? yield* planWorktrunkConfig(packageRoot, projectDir)
1286
+ : undefined;
1418
1287
  const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
1419
1288
  const availableSkills = catalog.skills.map((skill) => skill.selector);
1420
1289
  const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
@@ -1521,16 +1390,6 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1521
1390
  digest: output.digest,
1522
1391
  };
1523
1392
  }
1524
- if (output.resourceId === "setup:claude-instructions") {
1525
- return {
1526
- resourceId: output.resourceId,
1527
- path: output.path,
1528
- sourcePath: output.sourcePath,
1529
- mode: output.mode,
1530
- kind: output.kind,
1531
- digest: output.digest,
1532
- };
1533
- }
1534
1393
 
1535
1394
  return {
1536
1395
  resourceId: output.resourceId,
@@ -1553,8 +1412,22 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1553
1412
  ];
1554
1413
 
1555
1414
  yield* validateReservedPaths(projectDir, reservedPaths, desired);
1556
- const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
1557
- const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
1415
+ const rawLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
1416
+ const rawState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
1417
+ // Migration: dev-kit ≤0.14 owned the check workflow as a managed output. It
1418
+ // is a scaffold now, so stale lock entries and receipts are dropped on read —
1419
+ // releasing ownership to the repository instead of planning a removal.
1420
+ const dropRetiredOutputs = <O extends { readonly resourceId: string }>(
1421
+ outputs: ReadonlyArray<O>,
1422
+ ) => outputs.filter((output) => output.resourceId !== "setup:vite-plus-github-actions");
1423
+ const currentLock =
1424
+ rawLock === undefined
1425
+ ? undefined
1426
+ : { ...rawLock, outputs: dropRetiredOutputs(rawLock.outputs) };
1427
+ const currentState =
1428
+ rawState === undefined
1429
+ ? undefined
1430
+ : { ...rawState, outputs: dropRetiredOutputs(rawState.outputs) };
1558
1431
 
1559
1432
  yield* validateReservedPaths(projectDir, reservedPaths, [
1560
1433
  ...(currentLock?.outputs ?? []),
@@ -1601,6 +1474,8 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1601
1474
  ...(effectSource === undefined ? {} : { effectSource }),
1602
1475
  ...(effectTsgo === undefined ? {} : { effectTsgo }),
1603
1476
  ...(vitePlusHooks === undefined ? {} : { vitePlusHooks }),
1477
+ ...(vitePlusWorkflow === undefined ? {} : { vitePlusWorkflow }),
1478
+ ...(worktrunkConfig === undefined ? {} : { worktrunkConfig }),
1604
1479
  nextLock,
1605
1480
  nextState: planned.nextState,
1606
1481
  metadataChanged:
@@ -1627,7 +1502,9 @@ const operationalChangeCount = (plan: SkillPlan): number =>
1627
1502
  plan.actions.filter((action) => action.action !== "unchanged").length +
1628
1503
  (plan.effectSource?.action === "sync" ? 1 : 0) +
1629
1504
  (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0) +
1630
- (plan.vitePlusHooks?.action === "configure" ? 1 : 0);
1505
+ (plan.vitePlusHooks?.action === "configure" ? 1 : 0) +
1506
+ (plan.vitePlusWorkflow?.action === "scaffold" ? 1 : 0) +
1507
+ (plan.worktrunkConfig?.action === "scaffold" ? 1 : 0);
1631
1508
 
1632
1509
  const plannedChangeCount = (plan: SkillPlan): number => {
1633
1510
  const operational = operationalChangeCount(plan);
@@ -1658,6 +1535,12 @@ export const printSkillPlan = Effect.fn("printSkillPlan")(function* (plan: Skill
1658
1535
  if (plan.vitePlusHooks?.action === "configure") {
1659
1536
  yield* printDetail(`+ Vite+ hooks → ${plan.vitePlusHooks.hooksPath}`);
1660
1537
  }
1538
+ if (plan.vitePlusWorkflow?.action === "scaffold") {
1539
+ yield* printDetail(`+ scaffold check workflow → ${plan.vitePlusWorkflow.path}`);
1540
+ }
1541
+ if (plan.worktrunkConfig?.action === "scaffold") {
1542
+ yield* printDetail(`+ scaffold Worktrunk config → ${plan.worktrunkConfig.path}`);
1543
+ }
1661
1544
  if (operationalChangeCount(plan) === 0 && plan.metadataChanged) {
1662
1545
  yield* printDetail("+ Dev kit metadata");
1663
1546
  }
@@ -1952,6 +1835,8 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1952
1835
  effectSource: plan.effectSource,
1953
1836
  effectTsgo: plan.effectTsgo,
1954
1837
  vitePlusHooks: plan.vitePlusHooks,
1838
+ vitePlusWorkflow: plan.vitePlusWorkflow,
1839
+ worktrunkConfig: plan.worktrunkConfig,
1955
1840
  nextLock: plan.nextLock,
1956
1841
  nextState: plan.nextState,
1957
1842
  });
@@ -1960,6 +1845,8 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1960
1845
  effectSource: replanned.effectSource,
1961
1846
  effectTsgo: replanned.effectTsgo,
1962
1847
  vitePlusHooks: replanned.vitePlusHooks,
1848
+ vitePlusWorkflow: replanned.vitePlusWorkflow,
1849
+ worktrunkConfig: replanned.worktrunkConfig,
1963
1850
  nextLock: replanned.nextLock,
1964
1851
  nextState: replanned.nextState,
1965
1852
  });
@@ -1981,6 +1868,12 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1981
1868
  if (replanned.vitePlusHooks !== undefined) {
1982
1869
  yield* applyVitePlusHooksPlan(replanned.vitePlusHooks);
1983
1870
  }
1871
+ if (replanned.vitePlusWorkflow !== undefined) {
1872
+ yield* applyScaffoldPlan(replanned.vitePlusWorkflow);
1873
+ }
1874
+ if (replanned.worktrunkConfig !== undefined) {
1875
+ yield* applyScaffoldPlan(replanned.worktrunkConfig);
1876
+ }
1984
1877
  yield* applyPlannedSkillChanges(replanned);
1985
1878
  }),
1986
1879
  );