@brainervirus/workit-core 8.28.1 → 8.29.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "8.28.1",
3
+ "version": "8.29.0",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -10,6 +10,10 @@
10
10
  // self-ignoring `.workit/`, never at the repo root. A legacy root
11
11
  // `workit.checks.json` is still read when `.workit/checks.json` is absent,
12
12
  // with a note that it moved.
13
+ // In a linked worktree (a fanout slice's, an agent's) that has neither
14
+ // file, the main checkout's is read: `.workit/` is git-ignored, so a new
15
+ // worktree never has one, and it shares the main checkout's checks
16
+ // (#426). Configured commands still run from the worktree's own top.
13
17
  // 2. Built-in ecosystem defaults detected from the repo: package.json scripts
14
18
  // `test`, `lint`, `typecheck`, `check` (`<pm> run <script>`, pm from the
15
19
  // lockfile); go.mod → `go test ./...`; Cargo.toml → `cargo test`;
@@ -216,10 +220,25 @@ function detectedChecks(root: string): NamedCheck[] {
216
220
 
217
221
  const GATES = ["testing", "verification"] as const;
218
222
 
223
+ /**
224
+ * The checks config file for `root`: its own, else (in a linked worktree
225
+ * without one) the main checkout's, else the path a new config belongs at.
226
+ */
227
+ function configFileFor(root: string): string {
228
+ const own = checksConfigFile(root);
229
+ if (fs.existsSync(own)) return own;
230
+ const common = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
231
+ if (!common || path.basename(common) !== ".git") return own;
232
+ const main = path.dirname(common);
233
+ if (path.resolve(main) === path.resolve(root)) return own;
234
+ const shared = checksConfigFile(main);
235
+ return fs.existsSync(shared) ? shared : own;
236
+ }
237
+
219
238
  /** The named checks for the repository containing `cwd` (see the header). */
220
239
  export function loadCheckConfig(cwd: string): CheckConfig {
221
240
  const root = checkRoot(cwd);
222
- const file = checksConfigFile(root);
241
+ const file = configFileFor(root);
223
242
  const legacy = path.basename(file) === LEGACY_CHECKS_FILE;
224
243
  const label = legacy ? LEGACY_CHECKS_FILE : CHECKS_FILE;
225
244
  const note = legacy
@@ -255,9 +255,56 @@ export type CreateOutcome = {
255
255
  created: boolean;
256
256
  /** node_modules linked from the main checkout (worktree-deps.ts); null when none were. */
257
257
  dependencies: LinkedDependencies | null;
258
+ /** The install command to run in the worktree when its lockfile differs from the main checkout's. */
259
+ install: string | null;
258
260
  notes: string[];
259
261
  };
260
262
 
263
+ /** Lockfiles in the order a package manager is picked, with its frozen install for a directory. */
264
+ const LOCKFILES: ReadonlyArray<[string, (dir: string) => string]> = [
265
+ ["bun.lock", (dir) => `bun install --frozen-lockfile --cwd ${dir}`],
266
+ ["bun.lockb", (dir) => `bun install --frozen-lockfile --cwd ${dir}`],
267
+ ["pnpm-lock.yaml", (dir) => `pnpm install --frozen-lockfile --dir ${dir}`],
268
+ ["yarn.lock", (dir) => `yarn install --frozen-lockfile --cwd ${dir}`],
269
+ ["package-lock.json", (dir) => `npm ci --prefix ${dir}`],
270
+ ];
271
+
272
+ const readBytes = (file: string): Buffer | null => {
273
+ try {
274
+ return fs.readFileSync(file);
275
+ } catch {
276
+ return null;
277
+ }
278
+ };
279
+
280
+ /**
281
+ * Link the main checkout's node_modules into `worktree` only when every
282
+ * lockfile is byte-identical in both: a stale main checkout would otherwise
283
+ * hand the worktree the wrong dependency versions (#426). When one differs,
284
+ * nothing is linked and `install` is the command the worker runs instead.
285
+ */
286
+ function worktreeDependencies(
287
+ main: string,
288
+ worktree: string,
289
+ ): { linked: LinkedDependencies | null; install: string | null; notes: string[] } {
290
+ for (const [name, install] of LOCKFILES) {
291
+ const ours = readBytes(path.join(main, name));
292
+ const theirs = readBytes(path.join(worktree, name));
293
+ if (ours === null && theirs === null) continue;
294
+ if (ours !== null && theirs !== null && ours.equals(theirs)) continue;
295
+ if (fs.existsSync(path.join(worktree, "node_modules"))) break;
296
+ const command = install(worktree);
297
+ return {
298
+ linked: null,
299
+ install: command,
300
+ notes: [
301
+ `${name} differs from the main checkout's (${main}), so its node_modules was not linked; install dependencies first: ${command}`,
302
+ ],
303
+ };
304
+ }
305
+ return { ...linkWorktreeDependencies(main, worktree), install: null };
306
+ }
307
+
261
308
  function ensureScratch(worktree: string): string {
262
309
  const scratch = path.join(worktree, SCRATCH_DIR);
263
310
  fs.mkdirSync(scratch, { recursive: true });
@@ -303,7 +350,7 @@ export function createSliceWorktree(
303
350
  `remove it yourself (git worktree remove ${target}) once you know what it holds`,
304
351
  );
305
352
  excludeScratch(cwd);
306
- const deps = linkWorktreeDependencies(mainCheckout(cwd), target);
353
+ const deps = worktreeDependencies(mainCheckout(cwd), target);
307
354
  return {
308
355
  ok: true,
309
356
  data: {
@@ -313,6 +360,7 @@ export function createSliceWorktree(
313
360
  mode: resolveRef(cwd, localRef) ? "resume" : "new",
314
361
  created: false,
315
362
  dependencies: deps.linked,
363
+ install: deps.install,
316
364
  notes: [
317
365
  deps.linked
318
366
  ? "the worktree already exists; only its missing dependencies were linked"
@@ -392,7 +440,7 @@ export function createSliceWorktree(
392
440
  }
393
441
  excludeScratch(cwd);
394
442
  const scratch = ensureScratch(target);
395
- const deps = linkWorktreeDependencies(mainCheckout(cwd), target);
443
+ const deps = worktreeDependencies(mainCheckout(cwd), target);
396
444
  notes.push(...deps.notes);
397
445
  const head = gitRun(target, ["rev-parse", "HEAD"]).stdout.trim();
398
446
  const baseSha = mode === "new" ? head : null;
@@ -415,7 +463,16 @@ export function createSliceWorktree(
415
463
  });
416
464
  return {
417
465
  ok: true,
418
- data: { ...base, scratch, head, mode, created: true, dependencies: deps.linked, notes },
466
+ data: {
467
+ ...base,
468
+ scratch,
469
+ head,
470
+ mode,
471
+ created: true,
472
+ dependencies: deps.linked,
473
+ install: deps.install,
474
+ notes,
475
+ },
419
476
  };
420
477
  }
421
478