@ricsam/r5d-worker 0.0.55 → 0.0.56

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.
@@ -64,6 +64,8 @@ const DESTRUCTIVE_CHECKOUT_MIN_TRACKED_FILES = 20;
64
64
  const DESTRUCTIVE_CHECKOUT_REMOVAL_RATIO = 0.8;
65
65
  const ACTIVE_GIT_LOCK_PATHS = ["index.lock", "HEAD.lock", "packed-refs.lock", "shallow.lock"];
66
66
  const mirrorComparisonCache = /* @__PURE__ */ new Map();
67
+ const gitObjectFormatCache = /* @__PURE__ */ new Map();
68
+ const hydrationEntryCache = /* @__PURE__ */ new Map();
67
69
  function gitArgs(input, args) {
68
70
  const configArgs = ["-c", "submodule.recurse=false", "-c", "fetch.recurseSubmodules=false", "-c", "push.recurseSubmodules=false"];
69
71
  return input.authHeader ? ["git", ...configArgs, "-c", `http.extraHeader=${input.authHeader}`, ...args] : ["git", ...configArgs, ...args];
@@ -268,6 +270,127 @@ function lstatOrNull(filePath) {
268
270
  throw error;
269
271
  }
270
272
  }
273
+ const UNSTABLE_VISIBLE_ENTRY = /* @__PURE__ */ Symbol("unstable-visible-entry");
274
+ function hydrationEntriesEqual(left, right) {
275
+ if (!left || !right || left.kind !== right.kind || left.objectId !== right.objectId) return left === right;
276
+ return left.kind === "gitlink" || right.kind === "blob" && left.mode === right.mode;
277
+ }
278
+ function gitObjectFormat(input) {
279
+ const cached = gitObjectFormatCache.get(input.shadowRoot);
280
+ if (cached) return cached;
281
+ const format = runGit(input, input.shadowRoot, ["rev-parse", "--show-object-format"], "inspect workspace object format");
282
+ if (format !== "sha1" && format !== "sha256") throw new Error(`Unsupported Git object format ${JSON.stringify(format)}`);
283
+ gitObjectFormatCache.set(input.shadowRoot, format);
284
+ return format;
285
+ }
286
+ function hashGitBlobBuffer(input, content) {
287
+ return (0, import_node_crypto.createHash)(gitObjectFormat(input)).update(`blob ${content.byteLength}\0`).update(content).digest("hex");
288
+ }
289
+ function hashGitBlobFile(input, filePath, size) {
290
+ const hash = (0, import_node_crypto.createHash)(gitObjectFormat(input));
291
+ hash.update(`blob ${size}\0`);
292
+ const file = import_node_fs.default.openSync(filePath, "r");
293
+ const buffer = Buffer.allocUnsafe(Math.min(MIRROR_COMPARE_BUFFER_BYTES, Math.max(1, size)));
294
+ try {
295
+ let offset = 0;
296
+ while (offset < size) {
297
+ const bytesRead = import_node_fs.default.readSync(file, buffer, 0, Math.min(buffer.length, size - offset), offset);
298
+ if (bytesRead === 0) return null;
299
+ hash.update(buffer.subarray(0, bytesRead));
300
+ offset += bytesRead;
301
+ }
302
+ return hash.digest("hex");
303
+ } finally {
304
+ import_node_fs.default.closeSync(file);
305
+ }
306
+ }
307
+ function filesystemHydrationEntry(input, filePath) {
308
+ const before = lstatOrNull(filePath);
309
+ if (!before) return void 0;
310
+ if (!before.isFile() && !before.isSymbolicLink()) return UNSTABLE_VISIBLE_ENTRY;
311
+ const objectFormat = gitObjectFormat(input);
312
+ const signature = entrySignature(before);
313
+ const cached = hydrationEntryCache.get(filePath);
314
+ if (cached?.signature === signature && cached.objectFormat === objectFormat) return cached.entry;
315
+ let objectId = null;
316
+ if (before.isSymbolicLink()) {
317
+ try {
318
+ objectId = hashGitBlobBuffer(input, Buffer.from(import_node_fs.default.readlinkSync(filePath)));
319
+ } catch {
320
+ return UNSTABLE_VISIBLE_ENTRY;
321
+ }
322
+ } else {
323
+ try {
324
+ objectId = hashGitBlobFile(input, filePath, before.size);
325
+ } catch {
326
+ return UNSTABLE_VISIBLE_ENTRY;
327
+ }
328
+ }
329
+ const after = lstatOrNull(filePath);
330
+ if (!objectId || !after || entrySignature(before) !== entrySignature(after)) return UNSTABLE_VISIBLE_ENTRY;
331
+ const entry = {
332
+ kind: "blob",
333
+ mode: before.isSymbolicLink() ? "120000" : (before.mode & 73) !== 0 ? "100755" : "100644",
334
+ objectId
335
+ };
336
+ if (hydrationEntryCache.size >= MAX_MIRROR_COMPARISON_CACHE_ENTRIES) hydrationEntryCache.clear();
337
+ hydrationEntryCache.set(filePath, { signature, objectFormat, entry });
338
+ return entry;
339
+ }
340
+ function projectVisibleHydrationEntry(input, visibleRoot, filePath, gitlinks) {
341
+ if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot)) return UNSTABLE_VISIBLE_ENTRY;
342
+ const gitlink = gitlinks.get(filePath);
343
+ if (gitlink) {
344
+ const target = lstatOrNull(import_node_path.default.join(visibleRoot, ...filePath.split("/")));
345
+ if (target && !target.isDirectory()) return UNSTABLE_VISIBLE_ENTRY;
346
+ return { kind: "gitlink", objectId: gitlink.objectId };
347
+ }
348
+ return filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/")));
349
+ }
350
+ function revisionHydrationPreimage(input, revision, relativeRoot) {
351
+ const result = runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "ls-tree", "-r", "-z", revision, "--", relativeRoot]);
352
+ if (result.exitCode !== 0) {
353
+ throw new Error(
354
+ `Inspect workspace hydration baseline ${revision}:${relativeRoot}: ${result.stderr || `git exited ${result.exitCode}`}`
355
+ );
356
+ }
357
+ if (!result.stdout) return /* @__PURE__ */ new Map();
358
+ const prefix = `${relativeRoot}/`;
359
+ const preimage = /* @__PURE__ */ new Map();
360
+ for (const serialized of result.stdout.split("\0").filter(Boolean)) {
361
+ const separator = serialized.indexOf(" ");
362
+ if (separator === -1) continue;
363
+ const [mode, type, objectId] = serialized.slice(0, separator).split(" ");
364
+ const workspacePath = serialized.slice(separator + 1);
365
+ if (!objectId || !workspacePath.startsWith(prefix)) continue;
366
+ const filePath = workspacePath.slice(prefix.length);
367
+ if (mode === "160000" && type === "commit") {
368
+ preimage.set(filePath, { kind: "gitlink", objectId });
369
+ } else if ((mode === "100644" || mode === "100755" || mode === "120000") && type === "blob") {
370
+ preimage.set(filePath, { kind: "blob", mode, objectId });
371
+ }
372
+ }
373
+ return preimage;
374
+ }
375
+ function indexHydrationPreimage(input, relativeRoot) {
376
+ const prefix = `${relativeRoot}/`;
377
+ const entries = parseGitIndexEntries(
378
+ runGit(input, input.shadowRoot, ["--literal-pathspecs", "ls-files", "--stage", "-z", "--", relativeRoot], "inspect hydrated workspace")
379
+ );
380
+ const unmergedPaths = entries.filter((entry) => entry.stage !== "0").map((entry) => entry.filePath);
381
+ if (unmergedPaths.length > 0) throw new Error(`Canonical workspace has unmerged Git index stages: ${JSON.stringify(unmergedPaths)}`);
382
+ const preimage = /* @__PURE__ */ new Map();
383
+ for (const entry of entries) {
384
+ if (!entry.filePath.startsWith(prefix)) continue;
385
+ const filePath = entry.filePath.slice(prefix.length);
386
+ if (entry.mode === "160000") {
387
+ preimage.set(filePath, { kind: "gitlink", objectId: entry.objectId });
388
+ } else if (entry.mode === "100644" || entry.mode === "100755" || entry.mode === "120000") {
389
+ preimage.set(filePath, { kind: "blob", mode: entry.mode, objectId: entry.objectId });
390
+ }
391
+ }
392
+ return preimage;
393
+ }
271
394
  function worktreeDiffersFromIndex(checkoutPath, filePath) {
272
395
  const result = Bun.spawnSync(
273
396
  ["git", "--literal-pathspecs", "-c", "core.fileMode=true", "diff", "--quiet", "--no-ext-diff", "--", filePath],
@@ -422,11 +545,17 @@ function removeEmptyDirectories(root, opaqueRoots = []) {
422
545
  };
423
546
  visit(root, "");
424
547
  }
425
- function copyEntry(sourceRoot, targetRoot, relativePath) {
548
+ function copyEntry(sourceRoot, targetRoot, relativePath, trustedRoots = {}) {
426
549
  const sourcePath = import_node_path.default.resolve(sourceRoot, ...relativePath.split("/"));
427
550
  const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
428
551
  assertInside(sourceRoot, sourcePath, "Workspace source path");
429
552
  assertInside(targetRoot, targetPath, "Workspace target path");
553
+ if (!hasSafeDirectoryAncestors(sourceRoot, relativePath, trustedRoots.source ?? sourceRoot)) {
554
+ throw new Error(`Workspace source path has a symlink or non-directory ancestor: ${relativePath}`);
555
+ }
556
+ if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
557
+ throw new Error(`Workspace target path has a symlink or non-directory ancestor: ${relativePath}`);
558
+ }
430
559
  const stat = import_node_fs.default.lstatSync(sourcePath);
431
560
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
432
561
  import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
@@ -439,6 +568,22 @@ function copyEntry(sourceRoot, targetRoot, relativePath) {
439
568
  import_node_fs.default.chmodSync(targetPath, stat.mode);
440
569
  }
441
570
  }
571
+ function hasSafeDirectoryAncestors(root, relativePath, trustedRoot = root) {
572
+ const resolvedTrustedRoot = import_node_path.default.resolve(trustedRoot);
573
+ const trustedRootStat = lstatOrNull(resolvedTrustedRoot);
574
+ if (trustedRootStat && (!trustedRootStat.isDirectory() || trustedRootStat.isSymbolicLink())) return false;
575
+ const resolvedRoot = import_node_path.default.resolve(root);
576
+ const targetParent = import_node_path.default.dirname(import_node_path.default.resolve(resolvedRoot, ...relativePath.split("/")));
577
+ const relativeParent = import_node_path.default.relative(resolvedTrustedRoot, targetParent);
578
+ if (relativeParent === ".." || relativeParent.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relativeParent)) return false;
579
+ let current = resolvedTrustedRoot;
580
+ for (const segment of relativeParent.split(import_node_path.default.sep).filter(Boolean)) {
581
+ current = import_node_path.default.join(current, segment);
582
+ const stat = lstatOrNull(current);
583
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) return false;
584
+ }
585
+ return true;
586
+ }
442
587
  function entrySignature(stat) {
443
588
  return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
444
589
  }
@@ -490,7 +635,10 @@ function entriesEqual(sourcePath, targetPath) {
490
635
  }
491
636
  return equal;
492
637
  }
493
- function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles, opaqueTargetRoots = [], opaqueSourceRoots = []) {
638
+ function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles, opaqueTargetRoots = [], opaqueSourceRoots = [], trustedRoots = {}) {
639
+ if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-mirror-root", trustedRoots.target ?? targetRoot)) {
640
+ throw new Error(`Workspace target root has a symlink or non-directory ancestor: ${targetRoot}`);
641
+ }
494
642
  import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
495
643
  const sourceSet = new Set(sourceFiles);
496
644
  for (const relativePath of targetFiles) {
@@ -498,18 +646,27 @@ function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles, opaqueT
498
646
  if (sourceSet.has(relativePath)) continue;
499
647
  const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
500
648
  assertInside(targetRoot, targetPath, "Workspace deletion path");
649
+ if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
650
+ throw new Error(`Workspace deletion path has a symlink or non-directory ancestor: ${relativePath}`);
651
+ }
501
652
  import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
502
653
  }
503
654
  for (const relativePath of sourceFiles) {
504
655
  if (pathIsWithinOpaqueRoot(relativePath, opaqueSourceRoots)) continue;
505
656
  if (pathIsWithinOpaqueRoot(relativePath, opaqueTargetRoots)) continue;
657
+ if (!hasSafeDirectoryAncestors(sourceRoot, relativePath, trustedRoots.source ?? sourceRoot)) {
658
+ throw new Error(`Workspace source path has a symlink or non-directory ancestor: ${relativePath}`);
659
+ }
660
+ if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
661
+ throw new Error(`Workspace target path has a symlink or non-directory ancestor: ${relativePath}`);
662
+ }
506
663
  const sourcePath = import_node_path.default.resolve(sourceRoot, ...relativePath.split("/"));
507
664
  const stat = lstatOrNull(sourcePath);
508
665
  if (!stat) continue;
509
666
  if (!stat.isFile() && !stat.isSymbolicLink()) continue;
510
667
  const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
511
668
  if (entriesEqual(sourcePath, targetPath)) continue;
512
- copyEntry(sourceRoot, targetRoot, relativePath);
669
+ copyEntry(sourceRoot, targetRoot, relativePath, trustedRoots);
513
670
  }
514
671
  removeEmptyDirectories(targetRoot, opaqueTargetRoots);
515
672
  }
@@ -521,18 +678,37 @@ function indexPathsUnderCheckoutPath(checkoutPath, filePath) {
521
678
  ).split("\0").filter(Boolean);
522
679
  }
523
680
  function replaceCheckoutIndexPathWithGitlink(checkoutPath, gitlink) {
524
- for (const indexPath of indexPathsUnderCheckoutPath(checkoutPath, gitlink.filePath)) {
525
- runCheckoutGitRaw(
526
- checkoutPath,
527
- ["--literal-pathspecs", "update-index", "--force-remove", "--", indexPath],
528
- `remove index entry beneath gitlink ${JSON.stringify(gitlink.filePath)}`
681
+ const zeroObjectId = "0".repeat(gitlink.objectId.length);
682
+ const records = [
683
+ ...indexPathsUnderCheckoutPath(checkoutPath, gitlink.filePath).map((indexPath) => `0 ${zeroObjectId} ${indexPath}`),
684
+ `160000 ${gitlink.objectId} ${gitlink.filePath}`
685
+ ];
686
+ const result = Bun.spawnSync(
687
+ [
688
+ "git",
689
+ "-c",
690
+ "submodule.recurse=false",
691
+ "-c",
692
+ "fetch.recurseSubmodules=false",
693
+ "-c",
694
+ "push.recurseSubmodules=false",
695
+ "update-index",
696
+ "-z",
697
+ "--index-info"
698
+ ],
699
+ {
700
+ cwd: checkoutPath,
701
+ stdin: Buffer.from(`${records.join("\0")}\0`),
702
+ stdout: "pipe",
703
+ stderr: "pipe",
704
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
705
+ }
706
+ );
707
+ if (result.exitCode !== 0) {
708
+ throw new Error(
709
+ `record gitlink ${JSON.stringify(gitlink.filePath)}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`
529
710
  );
530
711
  }
531
- runCheckoutGitRaw(
532
- checkoutPath,
533
- ["--literal-pathspecs", "update-index", "--add", "--cacheinfo", "160000", gitlink.objectId, gitlink.filePath],
534
- `record gitlink ${JSON.stringify(gitlink.filePath)}`
535
- );
536
712
  }
537
713
  function removeCheckoutIndexPath(checkoutPath, filePath) {
538
714
  runCheckoutGitRaw(
@@ -593,12 +769,15 @@ function mirrorVisibleGitlinksToShadow(input, relativeRoot, visibleGitlinks) {
593
769
  }
594
770
  for (const gitlink of visibleGitlinks) replaceShadowIndexPathWithGitlink(input, relativeRoot, gitlink);
595
771
  }
596
- function mirrorShadowGitlinksToVisible(desired, visibleRoot) {
772
+ function mirrorShadowGitlinksToVisible(desired, visibleRoot, trustedRoot) {
597
773
  const desiredByPath = new Map(desired.map((entry) => [entry.filePath, entry]));
598
774
  for (const current of checkoutGitlinks(visibleRoot)) {
599
775
  if (!desiredByPath.has(current.filePath)) removeCheckoutIndexPath(visibleRoot, current.filePath);
600
776
  }
601
777
  for (const gitlink of desired) {
778
+ if (!hasSafeDirectoryAncestors(visibleRoot, gitlink.filePath, trustedRoot)) {
779
+ throw new Error(`Visible gitlink path has a symlink or non-directory ancestor: ${gitlink.filePath}`);
780
+ }
602
781
  const visiblePath = import_node_path.default.join(visibleRoot, ...gitlink.filePath.split("/"));
603
782
  const existing = lstatOrNull(visiblePath);
604
783
  if (existing && !existing.isDirectory()) import_node_fs.default.rmSync(visiblePath, { recursive: true, force: true });
@@ -606,7 +785,7 @@ function mirrorShadowGitlinksToVisible(desired, visibleRoot) {
606
785
  replaceCheckoutIndexPathWithGitlink(visibleRoot, gitlink);
607
786
  }
608
787
  }
609
- function assertSafeVisibleGitlinkTransitions(visibleRoot, currentGitlinks, desiredGitlinks, shadowFiles) {
788
+ function assertSafeVisibleGitlinkTransitions(visibleRoot, trustedRoot, currentGitlinks, desiredGitlinks, shadowFiles) {
610
789
  const desiredGitlinkPaths = new Set(desiredGitlinks.map((entry) => entry.filePath));
611
790
  for (const current of currentGitlinks) {
612
791
  if (desiredGitlinkPaths.has(current.filePath)) continue;
@@ -614,6 +793,9 @@ function assertSafeVisibleGitlinkTransitions(visibleRoot, currentGitlinks, desir
614
793
  (filePath) => filePath === current.filePath || filePath.startsWith(`${current.filePath}/`)
615
794
  );
616
795
  if (!becomesOrdinaryPath) continue;
796
+ if (!hasSafeDirectoryAncestors(visibleRoot, current.filePath, trustedRoot)) {
797
+ throw new Error(`Visible gitlink path has a symlink or non-directory ancestor: ${current.filePath}`);
798
+ }
617
799
  const existing = lstatOrNull(import_node_path.default.join(visibleRoot, ...current.filePath.split("/")));
618
800
  if (!existing) continue;
619
801
  if (existing.isDirectory() && import_node_fs.default.readdirSync(import_node_path.default.join(visibleRoot, ...current.filePath.split("/"))).length === 0) {
@@ -642,15 +824,46 @@ function restoreShadowProjectSnapshot(input, relativeRoot) {
642
824
  runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "rm", "-r", "-f", "--cached", "--ignore-unmatch", "--", relativeRoot]);
643
825
  import_node_fs.default.rmSync(shadowRoot, { recursive: true, force: true });
644
826
  }
827
+ function copiedHydrationPreimage(input, shadowRoot, ordinaryFiles, gitlinks = []) {
828
+ const preimage = new Map(
829
+ gitlinks.map((entry) => [entry.filePath, { kind: "gitlink", objectId: entry.objectId }])
830
+ );
831
+ for (const filePath of ordinaryFiles) {
832
+ const entry = filesystemHydrationEntry(input, import_node_path.default.join(shadowRoot, ...filePath.split("/")));
833
+ if (!entry || entry === UNSTABLE_VISIBLE_ENTRY) return null;
834
+ preimage.set(filePath, entry);
835
+ }
836
+ return preimage;
837
+ }
838
+ function visibleProjectMatchesCopiedPreimage(input, visibleRoot, ordinaryFiles, gitlinks, preimage, opaqueRoots) {
839
+ const currentFiles = listGitEligibleFiles(visibleRoot).filter((filePath) => !pathIsWithinOpaqueRoot(filePath, opaqueRoots));
840
+ if (currentFiles.length !== ordinaryFiles.length || currentFiles.some((filePath, index) => filePath !== ordinaryFiles[index]))
841
+ return false;
842
+ const currentGitlinks = checkoutGitlinks(visibleRoot);
843
+ if (currentGitlinks.length !== gitlinks.length || currentGitlinks.some((entry, index) => entry.filePath !== gitlinks[index]?.filePath || entry.objectId !== gitlinks[index]?.objectId)) {
844
+ return false;
845
+ }
846
+ const currentGitlinksByPath = new Map(currentGitlinks.map((entry) => [entry.filePath, entry]));
847
+ return ordinaryFiles.every((filePath) => {
848
+ const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinksByPath);
849
+ return current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath));
850
+ });
851
+ }
645
852
  function mirrorVisibleProjectToShadow(input, manifest, branchName) {
646
853
  const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
647
- if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return { entries: [], opaqueRoots: [] };
854
+ const unavailableProjection = () => ({
855
+ entries: [],
856
+ opaqueRoots: [],
857
+ visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, null]])
858
+ });
859
+ if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-project-scan", input.projectsRoot)) return unavailableProjection();
860
+ if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return unavailableProjection();
648
861
  const isCanonicalCheckout = manifest.canonicalCheckouts?.some((checkout) => checkout.branchName === branchName);
649
862
  if (isCanonicalCheckout) {
650
863
  assertCanonicalCheckoutIndexMatchesWorktree(visibleRoot);
651
864
  }
652
865
  const beforeSnapshot = checkoutGitSnapshot(visibleRoot);
653
- if (!beforeSnapshot) return { entries: [], opaqueRoots: [] };
866
+ if (!beforeSnapshot) return unavailableProjection();
654
867
  const visibleGitlinks = checkoutGitlinks(visibleRoot);
655
868
  const visibleIndexEntries = checkoutIndexEntries(visibleRoot).filter((entry) => entry.stage === "0");
656
869
  const indexedGitlinkPaths = new Set(visibleGitlinks.map((entry) => entry.filePath));
@@ -665,21 +878,142 @@ function mirrorVisibleProjectToShadow(input, manifest, branchName) {
665
878
  const visibleFiles = listGitEligibleFiles(visibleRoot).filter((filePath) => !pathIsWithinOpaqueRoot(filePath, visibleGitlinkRoots));
666
879
  const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
667
880
  mirrorVisibleGitlinksToShadow(input, relativeRoot, visibleGitlinks);
668
- mirrorFileSet(visibleRoot, shadowRoot, visibleFiles, listFilesRecursively(shadowRoot), visibleGitlinkRoots, visibleGitlinkRoots);
669
- if (checkoutGitSnapshot(visibleRoot) !== beforeSnapshot) {
881
+ mirrorFileSet(visibleRoot, shadowRoot, visibleFiles, listFilesRecursively(shadowRoot), visibleGitlinkRoots, visibleGitlinkRoots, {
882
+ source: input.projectsRoot,
883
+ target: input.shadowRoot
884
+ });
885
+ const hydrationPreimage = copiedHydrationPreimage(input, shadowRoot, visibleFiles, visibleGitlinks);
886
+ if (!hydrationPreimage || checkoutGitSnapshot(visibleRoot) !== beforeSnapshot || !visibleProjectMatchesCopiedPreimage(input, visibleRoot, visibleFiles, visibleGitlinks, hydrationPreimage, visibleGitlinkRoots)) {
670
887
  restoreShadowProjectSnapshot(input, relativeRoot);
671
- return { entries: [], opaqueRoots: [] };
888
+ return { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, null]]) };
672
889
  }
673
890
  return {
674
891
  entries: visibleGitlinks.map((entry) => ({
675
892
  filePath: import_node_path.default.posix.join(relativeRoot, entry.filePath),
676
893
  objectId: entry.objectId
677
894
  })),
678
- opaqueRoots: visibleGitlinkRoots.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath))
895
+ opaqueRoots: visibleGitlinkRoots.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)),
896
+ visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, hydrationPreimage]])
679
897
  };
680
898
  }
681
- function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpaqueRoots = []) {
899
+ function pathDepth(filePath) {
900
+ return filePath.split("/").length;
901
+ }
902
+ function pathsOverlap(left, right) {
903
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
904
+ }
905
+ function removeVisibleBlob(visibleRoot, filePath, trustedRoot) {
906
+ if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return false;
907
+ const absolutePath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
908
+ const stat = lstatOrNull(absolutePath);
909
+ if (!stat || !stat.isFile() && !stat.isSymbolicLink()) return false;
910
+ import_node_fs.default.rmSync(absolutePath, { force: true });
911
+ return true;
912
+ }
913
+ function removeEmptyVisibleAncestors(visibleRoot, filePath, trustedRoot, opaqueRoots = []) {
914
+ if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return;
915
+ const resolvedRoot = import_node_path.default.resolve(visibleRoot);
916
+ let current = import_node_path.default.dirname(import_node_path.default.join(resolvedRoot, ...filePath.split("/")));
917
+ while (current !== resolvedRoot) {
918
+ const relative = import_node_path.default.relative(resolvedRoot, current).split(import_node_path.default.sep).join("/");
919
+ if (!relative || pathIsWithinOpaqueRoot(relative, opaqueRoots)) return;
920
+ const stat = lstatOrNull(current);
921
+ if (!stat) {
922
+ current = import_node_path.default.dirname(current);
923
+ continue;
924
+ }
925
+ if (!stat.isDirectory() || stat.isSymbolicLink() || import_node_fs.default.readdirSync(current).length > 0) return;
926
+ try {
927
+ import_node_fs.default.rmdirSync(current);
928
+ } catch (error) {
929
+ if (error.code === "ENOTEMPTY" || error.code === "ENOENT") return;
930
+ throw error;
931
+ }
932
+ current = import_node_path.default.dirname(current);
933
+ }
934
+ }
935
+ function copyShadowBlobToVisible(shadowRoot, visibleRoot, filePath, trustedRoot) {
936
+ if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return false;
937
+ const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
938
+ const existing = lstatOrNull(targetPath);
939
+ if (existing?.isDirectory()) {
940
+ if (import_node_fs.default.readdirSync(targetPath).length > 0) return false;
941
+ import_node_fs.default.rmdirSync(targetPath);
942
+ } else if (existing) {
943
+ import_node_fs.default.rmSync(targetPath, { force: true });
944
+ }
945
+ try {
946
+ copyEntry(shadowRoot, visibleRoot, filePath, { target: trustedRoot });
947
+ return true;
948
+ } catch (error) {
949
+ if (error.code === "EEXIST" || error.code === "ENOTDIR") return false;
950
+ throw error;
951
+ }
952
+ }
953
+ function hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot, preimage, opaqueVisibleGitlinks) {
954
+ const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
955
+ const desired = indexHydrationPreimage(input, relativeRoot);
956
+ let expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
957
+ if (!expectedCheckoutSnapshot) return;
958
+ const checkoutIndexIsStable = () => checkoutGitSnapshot(visibleRoot) === expectedCheckoutSnapshot;
959
+ const refreshCheckoutSnapshot = () => {
960
+ expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
961
+ return expectedCheckoutSnapshot !== null;
962
+ };
963
+ let currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
964
+ const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
965
+ (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
966
+ );
967
+ for (const filePath of changedPaths.filter((candidate) => !desired.has(candidate)).sort((left, right) => pathDepth(right) - pathDepth(left) || left.localeCompare(right))) {
968
+ const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
969
+ if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath))) continue;
970
+ const expected = preimage.get(filePath);
971
+ if (expected?.kind === "gitlink") {
972
+ if (!checkoutIndexIsStable()) return;
973
+ removeCheckoutIndexPath(visibleRoot, filePath);
974
+ if (!refreshCheckoutSnapshot()) return;
975
+ currentGitlinks.delete(filePath);
976
+ } else if (removeVisibleBlob(visibleRoot, filePath, input.projectsRoot)) {
977
+ removeEmptyVisibleAncestors(visibleRoot, filePath, input.projectsRoot, opaqueVisibleGitlinks);
978
+ }
979
+ }
980
+ for (const filePath of changedPaths.filter((candidate) => desired.has(candidate)).sort((left, right) => pathDepth(left) - pathDepth(right) || left.localeCompare(right))) {
981
+ const wanted = desired.get(filePath);
982
+ if (!wanted) continue;
983
+ const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
984
+ if (current === UNSTABLE_VISIBLE_ENTRY || hydrationEntriesEqual(current, wanted)) continue;
985
+ if (!hydrationEntriesEqual(current, preimage.get(filePath))) continue;
986
+ if (wanted.kind === "gitlink") {
987
+ if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot) || !checkoutIndexIsStable()) return;
988
+ const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
989
+ const existing = lstatOrNull(targetPath);
990
+ if (existing && !existing.isDirectory()) import_node_fs.default.rmSync(targetPath, { force: true });
991
+ import_node_fs.default.mkdirSync(targetPath, { recursive: true });
992
+ replaceCheckoutIndexPathWithGitlink(visibleRoot, { filePath, objectId: wanted.objectId });
993
+ if (!refreshCheckoutSnapshot()) return;
994
+ currentGitlinks.set(filePath, { filePath, objectId: wanted.objectId });
995
+ continue;
996
+ }
997
+ const expected = preimage.get(filePath);
998
+ if (expected?.kind === "gitlink") {
999
+ if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot) || !checkoutIndexIsStable()) return;
1000
+ const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
1001
+ const existing = lstatOrNull(targetPath);
1002
+ if (existing?.isDirectory() && import_node_fs.default.readdirSync(targetPath).length > 0) continue;
1003
+ removeCheckoutIndexPath(visibleRoot, filePath);
1004
+ if (!refreshCheckoutSnapshot()) return;
1005
+ currentGitlinks.delete(filePath);
1006
+ }
1007
+ copyShadowBlobToVisible(shadowRoot, visibleRoot, filePath, input.projectsRoot);
1008
+ }
1009
+ }
1010
+ function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpaqueRoots = [], hydrationPreimage) {
682
1011
  const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1012
+ if (hydrationPreimage === null) return;
1013
+ if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-project-hydration", input.projectsRoot)) {
1014
+ if (hydrationPreimage) return;
1015
+ throw new Error(`Visible project root has a symlink or non-directory ancestor: ${visibleRoot}`);
1016
+ }
683
1017
  if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return;
684
1018
  checkoutGitlinks(visibleRoot);
685
1019
  const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
@@ -693,6 +1027,7 @@ function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpa
693
1027
  ].sort();
694
1028
  assertSafeVisibleGitlinkTransitions(
695
1029
  visibleRoot,
1030
+ input.projectsRoot,
696
1031
  protectedGitlinkRoots.map((filePath) => ({ filePath, objectId: "" })),
697
1032
  desiredGitlinks,
698
1033
  shadowFiles
@@ -700,77 +1035,166 @@ function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpa
700
1035
  const opaqueVisibleGitlinks = protectedGitlinkRoots.filter(
701
1036
  (filePath) => desiredGitlinkPaths.has(filePath) || !shadowFiles.some((shadowFile) => shadowFile === filePath || shadowFile.startsWith(`${filePath}/`))
702
1037
  );
703
- mirrorFileSet(shadowRoot, visibleRoot, shadowFiles, listGitEligibleFiles(visibleRoot), opaqueVisibleGitlinks);
704
- mirrorShadowGitlinksToVisible(desiredGitlinks, visibleRoot);
1038
+ if (hydrationPreimage) {
1039
+ hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot, hydrationPreimage, opaqueVisibleGitlinks);
1040
+ return;
1041
+ }
1042
+ mirrorFileSet(shadowRoot, visibleRoot, shadowFiles, listGitEligibleFiles(visibleRoot), opaqueVisibleGitlinks, [], {
1043
+ source: input.shadowRoot,
1044
+ target: input.projectsRoot
1045
+ });
1046
+ mirrorShadowGitlinksToVisible(desiredGitlinks, visibleRoot, input.projectsRoot);
1047
+ }
1048
+ function visibleRegularRootMatchesCopiedPreimage(input, visibleRoot, files, preimage, filter) {
1049
+ const currentFiles = listFilesRecursively(visibleRoot, filter);
1050
+ if (currentFiles.length !== files.length || currentFiles.some((filePath, index) => filePath !== files[index])) return false;
1051
+ return files.every((filePath) => {
1052
+ const current = hasSafeDirectoryAncestors(visibleRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1053
+ return current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath));
1054
+ });
1055
+ }
1056
+ function mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, filter) {
1057
+ if (!hasSafeDirectoryAncestors(sourceRoot, ".r5d-plan-scan", input.plansRoot)) return null;
1058
+ if (!import_node_fs.default.existsSync(sourceRoot)) return /* @__PURE__ */ new Map();
1059
+ const sourceFiles = listFilesRecursively(sourceRoot, filter);
1060
+ mirrorFileSet(sourceRoot, targetRoot, sourceFiles, listFilesRecursively(targetRoot, filter), [], [], {
1061
+ source: input.plansRoot,
1062
+ target: input.shadowRoot
1063
+ });
1064
+ const preimage = copiedHydrationPreimage(input, targetRoot, sourceFiles);
1065
+ if (!preimage || !visibleRegularRootMatchesCopiedPreimage(input, sourceRoot, sourceFiles, preimage, filter)) {
1066
+ restoreShadowProjectSnapshot(input, relativeRoot);
1067
+ return null;
1068
+ }
1069
+ return preimage;
1070
+ }
1071
+ function hydrateRegularRootOptimistically(input, relativeRoot, targetRoot, preimage, filter) {
1072
+ const sourceRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
1073
+ if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) return;
1074
+ const desired = indexHydrationPreimage(input, relativeRoot);
1075
+ for (const [filePath, entry] of desired) {
1076
+ if (entry.kind === "gitlink" || filter && !filter(filePath)) desired.delete(filePath);
1077
+ }
1078
+ const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
1079
+ (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
1080
+ );
1081
+ for (const filePath of changedPaths.filter((candidate) => !desired.has(candidate)).sort((left, right) => pathDepth(right) - pathDepth(left) || left.localeCompare(right))) {
1082
+ const current = hasSafeDirectoryAncestors(targetRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(targetRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1083
+ if (current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath))) {
1084
+ if (removeVisibleBlob(targetRoot, filePath, input.plansRoot)) {
1085
+ removeEmptyVisibleAncestors(targetRoot, filePath, input.plansRoot);
1086
+ }
1087
+ }
1088
+ }
1089
+ for (const filePath of changedPaths.filter((candidate) => desired.has(candidate)).sort((left, right) => pathDepth(left) - pathDepth(right) || left.localeCompare(right))) {
1090
+ const current = hasSafeDirectoryAncestors(targetRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(targetRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1091
+ const wanted = desired.get(filePath);
1092
+ if (!wanted || current === UNSTABLE_VISIBLE_ENTRY || hydrationEntriesEqual(current, wanted)) continue;
1093
+ if (!hydrationEntriesEqual(current, preimage.get(filePath))) continue;
1094
+ copyShadowBlobToVisible(sourceRoot, targetRoot, filePath, input.plansRoot);
1095
+ }
705
1096
  }
706
1097
  function mirrorLocalPlansToShadow(input, manifest, branchName) {
707
1098
  const sourceRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
708
- if (!import_node_fs.default.existsSync(sourceRoot)) return;
709
- const targetRoot = import_node_path.default.join(input.shadowRoot, ...workspacePlansRelativePath(manifest.projectId, branchName).split("/"));
1099
+ const relativeRoot = workspacePlansRelativePath(manifest.projectId, branchName);
1100
+ const targetRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
710
1101
  const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
711
- mirrorFileSet(sourceRoot, targetRoot, listFilesRecursively(sourceRoot, planFilter), listFilesRecursively(targetRoot));
1102
+ return mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, planFilter);
712
1103
  }
713
- function mirrorShadowPlansToLocal(input, manifest, branchName) {
1104
+ function mirrorShadowPlansToLocal(input, manifest, branchName, hydrationPreimage) {
1105
+ if (hydrationPreimage === null) return;
714
1106
  const relativeRoot = workspacePlansRelativePath(manifest.projectId, branchName);
715
1107
  const sourceRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
716
1108
  const targetRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
717
1109
  const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
1110
+ if (hydrationPreimage) {
1111
+ hydrateRegularRootOptimistically(input, relativeRoot, targetRoot, hydrationPreimage, planFilter);
1112
+ return;
1113
+ }
1114
+ if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) {
1115
+ throw new Error(`Visible plan root has a symlink or non-directory ancestor: ${targetRoot}`);
1116
+ }
718
1117
  mirrorFileSet(
719
1118
  sourceRoot,
720
1119
  targetRoot,
721
1120
  listShadowTrackedFiles(input, relativeRoot).filter(planFilter),
722
- listFilesRecursively(targetRoot, planFilter)
1121
+ listFilesRecursively(targetRoot, planFilter),
1122
+ [],
1123
+ [],
1124
+ { source: input.shadowRoot, target: input.plansRoot }
723
1125
  );
724
1126
  }
725
1127
  function mirrorLocalWorkspacePlansToShadow(input) {
726
- if (input.trigger.canonicalCheckoutOnly) return;
1128
+ if (input.trigger.canonicalCheckoutOnly) return /* @__PURE__ */ new Map();
727
1129
  const sourceRoot = localWorkspacePlansPath(input.plansRoot);
728
- if (!import_node_fs.default.existsSync(sourceRoot)) return;
729
- const targetRoot = import_node_path.default.join(input.shadowRoot, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH);
1130
+ const relativeRoot = WORKSPACE_NATIVE_PLANS_RELATIVE_PATH;
1131
+ const targetRoot = import_node_path.default.join(input.shadowRoot, relativeRoot);
730
1132
  const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
731
- mirrorFileSet(sourceRoot, targetRoot, listFilesRecursively(sourceRoot, planFilter), listFilesRecursively(targetRoot, planFilter));
1133
+ return mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, planFilter);
732
1134
  }
733
- function mirrorShadowWorkspacePlansToLocal(input) {
1135
+ function mirrorShadowWorkspacePlansToLocal(input, hydrationPreimage) {
734
1136
  if (input.trigger.canonicalCheckoutOnly) return;
1137
+ if (hydrationPreimage === null) return;
735
1138
  const sourceRoot = import_node_path.default.join(input.shadowRoot, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH);
736
1139
  const targetRoot = localWorkspacePlansPath(input.plansRoot);
737
1140
  const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
1141
+ if (hydrationPreimage) {
1142
+ hydrateRegularRootOptimistically(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH, targetRoot, hydrationPreimage, planFilter);
1143
+ return;
1144
+ }
1145
+ if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) {
1146
+ throw new Error(`Visible workspace-plan root has a symlink or non-directory ancestor: ${targetRoot}`);
1147
+ }
738
1148
  mirrorFileSet(
739
1149
  sourceRoot,
740
1150
  targetRoot,
741
1151
  listShadowTrackedFiles(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH).filter(planFilter),
742
- listFilesRecursively(targetRoot, planFilter)
1152
+ listFilesRecursively(targetRoot, planFilter),
1153
+ [],
1154
+ [],
1155
+ { source: input.shadowRoot, target: input.plansRoot }
743
1156
  );
744
1157
  }
745
- function reconcileWorkspaceNativePlans(input) {
1158
+ function reconcileWorkspaceNativePlans(input, projection) {
746
1159
  if (input.trigger.canonicalCheckoutOnly) return;
1160
+ const localRoot = localWorkspacePlansPath(input.plansRoot);
1161
+ projection.visiblePreimages ??= /* @__PURE__ */ new Map();
1162
+ projection.visiblePreimages.set(localRoot, /* @__PURE__ */ new Map());
747
1163
  if (import_node_fs.default.existsSync(localWorkspacePlansPath(input.plansRoot))) {
748
- mirrorLocalWorkspacePlansToShadow(input);
1164
+ projection.visiblePreimages.set(localRoot, mirrorLocalWorkspacePlansToShadow(input));
749
1165
  return;
750
1166
  }
751
1167
  if (listShadowTrackedFiles(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH).length > 0 || restoreShadowRootFromRemote(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH)) {
752
- mirrorShadowWorkspacePlansToLocal(input);
1168
+ mirrorShadowWorkspacePlansToLocal(input, /* @__PURE__ */ new Map());
1169
+ projection.visiblePreimages.set(localRoot, indexHydrationPreimage(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH));
753
1170
  }
754
1171
  }
755
1172
  function mergeGitlinkProjection(target, source) {
756
1173
  target.entries.push(...source.entries);
757
1174
  target.opaqueRoots.push(...source.opaqueRoots);
1175
+ if (source.visiblePreimages) {
1176
+ target.visiblePreimages ??= /* @__PURE__ */ new Map();
1177
+ for (const [visibleRoot, preimage] of source.visiblePreimages) target.visiblePreimages.set(visibleRoot, preimage);
1178
+ }
758
1179
  }
759
1180
  function mirrorVisibleWorkspaceToShadow(input, excludedCheckouts = /* @__PURE__ */ new Set()) {
760
- const projection = { entries: [], opaqueRoots: [] };
761
- reconcileWorkspaceNativePlans(input);
1181
+ const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1182
+ reconcileWorkspaceNativePlans(input, projection);
762
1183
  for (const manifest of [...input.projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
763
1184
  for (const branchName of [...new Set(manifest.branches)].sort()) {
764
1185
  if (excludedCheckouts.has(`${manifest.projectId}\0${branchName}`)) continue;
765
1186
  mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, branchName));
766
- if (!input.trigger.canonicalCheckoutOnly) mirrorLocalPlansToShadow(input, manifest, branchName);
1187
+ if (!input.trigger.canonicalCheckoutOnly) {
1188
+ const localPlansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1189
+ projection.visiblePreimages?.set(localPlansRoot, mirrorLocalPlansToShadow(input, manifest, branchName));
1190
+ }
767
1191
  }
768
1192
  }
769
1193
  projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
770
1194
  return projection;
771
1195
  }
772
1196
  function reconcileNewVisibleCheckouts(input) {
773
- const projection = { entries: [], opaqueRoots: [] };
1197
+ const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
774
1198
  for (const target of input.newVisibleCheckouts ?? []) {
775
1199
  const manifest = input.projects.find((project) => project.projectId === target.projectId);
776
1200
  if (!manifest || !manifest.branches.includes(target.branchName)) continue;
@@ -779,6 +1203,10 @@ function reconcileNewVisibleCheckouts(input) {
779
1203
  mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, target.branchName));
780
1204
  } else if (listShadowTrackedFiles(input, projectRoot).length > 0 || restoreShadowRootFromRemote(input, projectRoot)) {
781
1205
  mirrorShadowProjectToVisible(input, manifest, target.branchName);
1206
+ projection.visiblePreimages?.set(
1207
+ visibleProjectBranchPath(input.projectsRoot, manifest, target.branchName),
1208
+ indexHydrationPreimage(input, projectRoot)
1209
+ );
782
1210
  } else {
783
1211
  mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, target.branchName));
784
1212
  }
@@ -786,23 +1214,194 @@ function reconcileNewVisibleCheckouts(input) {
786
1214
  const plansRoot = workspacePlansRelativePath(target.projectId, target.branchName);
787
1215
  if (listShadowTrackedFiles(input, plansRoot).length > 0 || restoreShadowRootFromRemote(input, plansRoot)) {
788
1216
  mirrorShadowPlansToLocal(input, manifest, target.branchName);
1217
+ projection.visiblePreimages?.set(
1218
+ localPlansBranchPath(input.plansRoot, manifest.projectId, target.branchName),
1219
+ indexHydrationPreimage(input, plansRoot)
1220
+ );
789
1221
  } else {
790
- mirrorLocalPlansToShadow(input, manifest, target.branchName);
1222
+ projection.visiblePreimages?.set(
1223
+ localPlansBranchPath(input.plansRoot, manifest.projectId, target.branchName),
1224
+ mirrorLocalPlansToShadow(input, manifest, target.branchName)
1225
+ );
791
1226
  }
792
1227
  }
793
1228
  projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
794
1229
  return projection;
795
1230
  }
796
- function mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots = []) {
797
- mirrorShadowWorkspacePlansToLocal(input);
1231
+ function mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots = [], hydrationPreimages) {
1232
+ mirrorShadowWorkspacePlansToLocal(input, hydrationPreimages?.get(localWorkspacePlansPath(input.plansRoot)));
798
1233
  for (const manifest of [...input.projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
799
1234
  for (const branchName of [...new Set(manifest.branches)].sort()) {
800
1235
  const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1236
+ const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
801
1237
  const opaqueProjectRoots = opaqueWorkspaceRoots.filter((root) => root.startsWith(`${relativeRoot}/`)).map((root) => root.slice(relativeRoot.length + 1));
802
- mirrorShadowProjectToVisible(input, manifest, branchName, opaqueProjectRoots);
803
- if (!input.trigger.canonicalCheckoutOnly) mirrorShadowPlansToLocal(input, manifest, branchName);
1238
+ mirrorShadowProjectToVisible(input, manifest, branchName, opaqueProjectRoots, hydrationPreimages?.get(visibleRoot));
1239
+ if (!input.trigger.canonicalCheckoutOnly) {
1240
+ const localPlansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1241
+ mirrorShadowPlansToLocal(input, manifest, branchName, hydrationPreimages?.get(localPlansRoot));
1242
+ }
1243
+ }
1244
+ }
1245
+ }
1246
+ function hydrationPreimagesFromShadowRevision(input, revision) {
1247
+ const preimages = /* @__PURE__ */ new Map();
1248
+ const fromRevision = (relativeRoot) => revision ? revisionHydrationPreimage(input, revision, relativeRoot) : /* @__PURE__ */ new Map();
1249
+ if (!input.trigger.canonicalCheckoutOnly) {
1250
+ preimages.set(localWorkspacePlansPath(input.plansRoot), fromRevision(WORKSPACE_NATIVE_PLANS_RELATIVE_PATH));
1251
+ }
1252
+ for (const manifest of input.projects) {
1253
+ for (const branchName of manifest.branches) {
1254
+ preimages.set(
1255
+ visibleProjectBranchPath(input.projectsRoot, manifest, branchName),
1256
+ fromRevision(workspaceProjectBranchRelativePath(manifest.projectId, branchName))
1257
+ );
1258
+ if (!input.trigger.canonicalCheckoutOnly) {
1259
+ preimages.set(
1260
+ localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
1261
+ fromRevision(workspacePlansRelativePath(manifest.projectId, branchName))
1262
+ );
1263
+ }
1264
+ }
1265
+ }
1266
+ return preimages;
1267
+ }
1268
+ function visibleHydrationConflictPaths(input, preimages, desiredRevision) {
1269
+ const conflicts = [];
1270
+ const inspectRegularRoot = (visibleRoot, relativeRoot, filter) => {
1271
+ const preimage = preimages.get(visibleRoot);
1272
+ if (!preimage) return;
1273
+ const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
1274
+ for (const [filePath, entry] of desired) {
1275
+ if (entry.kind === "gitlink" || filter && !filter(filePath)) desired.delete(filePath);
1276
+ }
1277
+ const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
1278
+ (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
1279
+ );
1280
+ if (changedPaths.length === 0) return;
1281
+ if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-hydration-scan", input.plansRoot)) {
1282
+ conflicts.push(...changedPaths.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)));
1283
+ return;
1284
+ }
1285
+ const affectsChangedTree = (filePath) => changedPaths.some((changedPath) => pathsOverlap(filePath, changedPath));
1286
+ const currentEntries = /* @__PURE__ */ new Map();
1287
+ const unstablePaths = /* @__PURE__ */ new Set();
1288
+ for (const filePath of listFilesRecursively(visibleRoot).filter(affectsChangedTree)) {
1289
+ const current = hasSafeDirectoryAncestors(visibleRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1290
+ if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
1291
+ else currentEntries.set(filePath, current);
804
1292
  }
1293
+ for (const filePath of /* @__PURE__ */ new Set([...changedPaths, ...currentEntries.keys(), ...unstablePaths])) {
1294
+ if (!affectsChangedTree(filePath)) continue;
1295
+ const current = unstablePaths.has(filePath) ? UNSTABLE_VISIBLE_ENTRY : currentEntries.get(filePath);
1296
+ if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath)) && !hydrationEntriesEqual(current, desired.get(filePath))) {
1297
+ conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
1298
+ }
1299
+ }
1300
+ };
1301
+ if (!input.trigger.canonicalCheckoutOnly) {
1302
+ inspectRegularRoot(
1303
+ localWorkspacePlansPath(input.plansRoot),
1304
+ WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
1305
+ (filePath) => filePath.endsWith(".plan.md")
1306
+ );
805
1307
  }
1308
+ for (const manifest of input.projects) {
1309
+ for (const branchName of manifest.branches) {
1310
+ const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1311
+ const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1312
+ const preimage = preimages.get(visibleRoot);
1313
+ if (preimage && import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) {
1314
+ const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
1315
+ const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
1316
+ (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
1317
+ );
1318
+ if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-hydration-scan", input.projectsRoot)) {
1319
+ conflicts.push(...changedPaths.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)));
1320
+ continue;
1321
+ }
1322
+ const affectsChangedTree = (filePath) => changedPaths.some((changedPath) => pathsOverlap(filePath, changedPath));
1323
+ const currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
1324
+ const residualOpaqueGitlinks = [...preimage].filter(
1325
+ ([filePath, entry]) => entry.kind === "gitlink" && !desired.has(filePath) && ![...desired.keys()].some((desiredPath) => desiredPath.startsWith(`${filePath}/`))
1326
+ ).map(([filePath]) => filePath);
1327
+ const opaqueRoots = [.../* @__PURE__ */ new Set([...currentGitlinks.keys(), ...residualOpaqueGitlinks])];
1328
+ const currentEntries = /* @__PURE__ */ new Map();
1329
+ const unstablePaths = /* @__PURE__ */ new Set();
1330
+ for (const filePath of [...currentGitlinks.keys()].filter(affectsChangedTree)) {
1331
+ const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
1332
+ if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
1333
+ else currentEntries.set(filePath, current);
1334
+ }
1335
+ for (const filePath of listGitEligibleFiles(visibleRoot).filter(
1336
+ (candidate) => affectsChangedTree(candidate) && !pathIsWithinOpaqueRoot(candidate, opaqueRoots)
1337
+ )) {
1338
+ const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
1339
+ if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
1340
+ else currentEntries.set(filePath, current);
1341
+ }
1342
+ for (const filePath of /* @__PURE__ */ new Set([...changedPaths, ...currentEntries.keys(), ...unstablePaths])) {
1343
+ if (!affectsChangedTree(filePath)) continue;
1344
+ const current = unstablePaths.has(filePath) ? UNSTABLE_VISIBLE_ENTRY : currentEntries.get(filePath);
1345
+ if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath)) && !hydrationEntriesEqual(current, desired.get(filePath))) {
1346
+ conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
1347
+ }
1348
+ }
1349
+ }
1350
+ if (!input.trigger.canonicalCheckoutOnly) {
1351
+ inspectRegularRoot(
1352
+ localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
1353
+ workspacePlansRelativePath(manifest.projectId, branchName),
1354
+ (filePath) => filePath.endsWith(".plan.md")
1355
+ );
1356
+ }
1357
+ }
1358
+ }
1359
+ return [...new Set(conflicts)].sort();
1360
+ }
1361
+ function unavailableHydrationConflictPaths(input, preimages, baselineRevision, desiredRevision) {
1362
+ const conflicts = [];
1363
+ const inspectRoot = (visibleRoot, relativeRoot, filter) => {
1364
+ if (preimages.get(visibleRoot) !== null) return;
1365
+ const baseline = revisionHydrationPreimage(input, baselineRevision, relativeRoot);
1366
+ const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
1367
+ for (const filePath of /* @__PURE__ */ new Set([...baseline.keys(), ...desired.keys()])) {
1368
+ if (filter && !filter(filePath)) continue;
1369
+ if (!hydrationEntriesEqual(baseline.get(filePath), desired.get(filePath))) {
1370
+ conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
1371
+ }
1372
+ }
1373
+ };
1374
+ if (!input.trigger.canonicalCheckoutOnly) {
1375
+ inspectRoot(
1376
+ localWorkspacePlansPath(input.plansRoot),
1377
+ WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
1378
+ (filePath) => filePath.endsWith(".plan.md")
1379
+ );
1380
+ }
1381
+ for (const manifest of input.projects) {
1382
+ for (const branchName of manifest.branches) {
1383
+ inspectRoot(
1384
+ visibleProjectBranchPath(input.projectsRoot, manifest, branchName),
1385
+ workspaceProjectBranchRelativePath(manifest.projectId, branchName)
1386
+ );
1387
+ if (!input.trigger.canonicalCheckoutOnly) {
1388
+ inspectRoot(
1389
+ localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
1390
+ workspacePlansRelativePath(manifest.projectId, branchName),
1391
+ (filePath) => filePath.endsWith(".plan.md")
1392
+ );
1393
+ }
1394
+ }
1395
+ }
1396
+ return [...new Set(conflicts)].sort();
1397
+ }
1398
+ function hydrationConflictPaths(input, preimages, baselineRevision, desiredRevision) {
1399
+ return [
1400
+ .../* @__PURE__ */ new Set([
1401
+ ...visibleHydrationConflictPaths(input, preimages, desiredRevision),
1402
+ ...unavailableHydrationConflictPaths(input, preimages, baselineRevision, desiredRevision)
1403
+ ])
1404
+ ].sort();
806
1405
  }
807
1406
  function forceStageCanonicalCheckoutFiles(input, preservedGitlinks = [], opaqueRoots = []) {
808
1407
  for (const manifest of input.projects) {
@@ -883,7 +1482,9 @@ function assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, revision) {
883
1482
  }
884
1483
  function ensureShadowWorkspace(input) {
885
1484
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(input.shadowRoot), { recursive: true });
1485
+ let created = false;
886
1486
  if (!import_node_fs.default.existsSync(import_node_path.default.join(input.shadowRoot, ".git"))) {
1487
+ created = true;
887
1488
  import_node_fs.default.rmSync(input.shadowRoot, { recursive: true, force: true });
888
1489
  const clone = runGitResult(input, import_node_path.default.dirname(input.shadowRoot), ["clone", "--origin", "origin", input.remoteUrl, input.shadowRoot]);
889
1490
  if (clone.exitCode !== 0) {
@@ -905,6 +1506,7 @@ function ensureShadowWorkspace(input) {
905
1506
  } else if (!hasHead) {
906
1507
  runGit(input, input.shadowRoot, ["checkout", "--orphan", WORKSPACE_BRANCH], "create canonical workspace branch");
907
1508
  }
1509
+ return created;
908
1510
  }
909
1511
  function revParse(input, revision) {
910
1512
  const result = runGitResult(input, input.shadowRoot, ["rev-parse", "--verify", revision]);
@@ -981,14 +1583,15 @@ function resetUncommittedShadowSnapshot(input) {
981
1583
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
982
1584
  }
983
1585
  function fastForwardCleanShadowForNewCheckouts(input) {
984
- if (!input.newVisibleCheckouts?.length) return;
1586
+ if (!input.newVisibleCheckouts?.length) return null;
985
1587
  runGit(input, input.shadowRoot, ["add", "-A"], "stage existing workspace changes before checkout hydration");
986
- if (gitStatus(input)) return;
1588
+ if (gitStatus(input)) return null;
987
1589
  const localHead = revParse(input, "HEAD");
988
1590
  const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
989
- if (!localHead || !remoteHead || localHead === remoteHead) return;
990
- if (!tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", localHead, remoteHead])) return;
1591
+ if (!localHead || !remoteHead || localHead === remoteHead) return null;
1592
+ if (!tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", localHead, remoteHead])) return null;
991
1593
  runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "fast-forward before checkout hydration");
1594
+ return localHead;
992
1595
  }
993
1596
  function restoreShadowRootFromRemote(input, relativeRoot) {
994
1597
  const remoteRevision = `origin/${WORKSPACE_BRANCH}`;
@@ -1185,12 +1788,12 @@ function convergeRedundantInboundCandidate(input, result) {
1185
1788
  );
1186
1789
  if (stagedTree !== remoteTree) return null;
1187
1790
  const localHead = revParse(input, "HEAD");
1791
+ if (localHead === remoteHead) return null;
1188
1792
  runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "converge redundant inbound workspace candidate");
1189
1793
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean redundant inbound workspace candidate");
1190
- mirrorShadowWorkspaceToVisible(input);
1191
1794
  return {
1192
1795
  ...result,
1193
- outcome: localHead === remoteHead ? "no_change" : "updated",
1796
+ outcome: "updated",
1194
1797
  publishedHead: remoteHead,
1195
1798
  diffSizeBytes: 0,
1196
1799
  gitStatus: gitStatus(input),
@@ -1202,10 +1805,13 @@ async function synchronizeWorkspace(rawInput) {
1202
1805
  let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
1203
1806
  try {
1204
1807
  input = { ...input, projects: workspaceProjectsForSync(input.projects, input.trigger) };
1205
- let mirroredGitlinkProjection = { entries: [], opaqueRoots: [] };
1206
- ensureShadowWorkspace(input);
1808
+ let mirroredGitlinkProjection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1809
+ let fastForwardedFromHead = null;
1810
+ const shadowWasCreated = ensureShadowWorkspace(input);
1811
+ let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
1207
1812
  const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1208
1813
  const result = baseResult(input, remoteHeadAtStart);
1814
+ const inboundConflictsAtStart = input.skipVisibleMirror && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
1209
1815
  if (input.resetToCanonical) {
1210
1816
  if (input.trigger.canonicalCheckoutOnly) {
1211
1817
  throw new Error("A canonical-checkout-only synchronization cannot reset the authoritative resolver checkout");
@@ -1224,9 +1830,10 @@ async function synchronizeWorkspace(rawInput) {
1224
1830
  if (!input.skipVisibleMirror) {
1225
1831
  const newCheckoutKeys = new Set((input.newVisibleCheckouts ?? []).map((target) => `${target.projectId}\0${target.branchName}`));
1226
1832
  mirroredGitlinkProjection = mirrorVisibleWorkspaceToShadow(input, newCheckoutKeys);
1227
- fastForwardCleanShadowForNewCheckouts(input);
1833
+ fastForwardedFromHead = fastForwardCleanShadowForNewCheckouts(input);
1228
1834
  mergeGitlinkProjection(mirroredGitlinkProjection, reconcileNewVisibleCheckouts(input));
1229
1835
  mirroredGitlinkProjection.opaqueRoots = [...new Set(mirroredGitlinkProjection.opaqueRoots)].sort();
1836
+ hydrationPreimages = mirroredGitlinkProjection.visiblePreimages ?? hydrationPreimages;
1230
1837
  }
1231
1838
  const stagePathspecs = mirroredGitlinkProjection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1232
1839
  runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage workspace changes");
@@ -1267,9 +1874,22 @@ async function synchronizeWorkspace(rawInput) {
1267
1874
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
1268
1875
  }
1269
1876
  let candidateHead = revParse(input, "HEAD");
1877
+ const candidateHeadBeforeRebase = candidateHead;
1270
1878
  if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1271
1879
  const hasRemoteMain = Boolean(revParse(input, `origin/${WORKSPACE_BRANCH}`));
1272
1880
  const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
1881
+ if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
1882
+ const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
1883
+ return {
1884
+ ...observed,
1885
+ outcome: "failed",
1886
+ candidateHead: candidateHead ?? void 0,
1887
+ affectedProjects: affectedProjects(conflictPaths),
1888
+ affectedPaths: reportedPaths(conflictPaths),
1889
+ error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation.",
1890
+ gitStatus: gitStatus(input)
1891
+ };
1892
+ }
1273
1893
  if (!hasUnpushedCommit) {
1274
1894
  const localHead = revParse(input, "HEAD");
1275
1895
  const currentRemoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
@@ -1277,7 +1897,23 @@ async function synchronizeWorkspace(rawInput) {
1277
1897
  runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "update workspace from canonical state");
1278
1898
  runGit(input, input.shadowRoot, ["clean", "-fd"], "clean updated workspace");
1279
1899
  assertPublishedCanonicalCheckoutTree(input, currentRemoteHead, sampledCanonicalCheckoutTreeHash);
1280
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
1900
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1901
+ const inboundConflicts = input.trigger.canonicalCheckoutOnly ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
1902
+ if (inboundConflicts.length > 0 && localHead) {
1903
+ runGit(input, input.shadowRoot, ["reset", "--hard", localHead], "preserve old workspace base after inbound conflict");
1904
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved old workspace base");
1905
+ const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflicts])].sort();
1906
+ return {
1907
+ ...observed,
1908
+ outcome: "failed",
1909
+ candidateHead: candidateHead ?? void 0,
1910
+ publishedHead: currentRemoteHead,
1911
+ affectedProjects: affectedProjects(conflictPaths),
1912
+ affectedPaths: reportedPaths(conflictPaths),
1913
+ error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation.",
1914
+ gitStatus: gitStatus(input)
1915
+ };
1916
+ }
1281
1917
  return {
1282
1918
  ...observed,
1283
1919
  outcome: "updated",
@@ -1289,9 +1925,27 @@ async function synchronizeWorkspace(rawInput) {
1289
1925
  const authoritativeHead = currentRemoteHead ?? localHead;
1290
1926
  if (authoritativeHead) {
1291
1927
  assertPublishedCanonicalCheckoutTree(input, authoritativeHead, sampledCanonicalCheckoutTreeHash);
1292
- }
1293
- if (input.skipVisibleMirror) {
1294
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
1928
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1929
+ const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
1930
+ if (hydrationConflicts2.length > 0 && fastForwardedFromHead) {
1931
+ runGit(
1932
+ input,
1933
+ input.shadowRoot,
1934
+ ["reset", "--hard", fastForwardedFromHead],
1935
+ "preserve old workspace base after new-checkout fast-forward conflict"
1936
+ );
1937
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved new-checkout workspace base");
1938
+ return {
1939
+ ...observed,
1940
+ outcome: "failed",
1941
+ candidateHead: candidateHead ?? void 0,
1942
+ publishedHead: currentRemoteHead ?? void 0,
1943
+ affectedProjects: affectedProjects(hydrationConflicts2),
1944
+ affectedPaths: reportedPaths(hydrationConflicts2),
1945
+ error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation.",
1946
+ gitStatus: gitStatus(input)
1947
+ };
1948
+ }
1295
1949
  }
1296
1950
  return {
1297
1951
  ...observed,
@@ -1308,7 +1962,7 @@ async function synchronizeWorkspace(rawInput) {
1308
1962
  rebaseCount += 1;
1309
1963
  if (rebase.exitCode !== 0) {
1310
1964
  const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1311
- const conflictPaths = reportedPaths([.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort());
1965
+ const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
1312
1966
  runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
1313
1967
  return {
1314
1968
  ...observed,
@@ -1316,7 +1970,8 @@ async function synchronizeWorkspace(rawInput) {
1316
1970
  expectedHead,
1317
1971
  candidateHead: candidateHead ?? void 0,
1318
1972
  rebaseCount,
1319
- affectedPaths: conflictPaths,
1973
+ affectedProjects: affectedProjects(conflictPaths),
1974
+ affectedPaths: reportedPaths(conflictPaths),
1320
1975
  error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`,
1321
1976
  gitStatus: gitStatus(input)
1322
1977
  };
@@ -1334,6 +1989,30 @@ async function synchronizeWorkspace(rawInput) {
1334
1989
  gitStatus: gitStatus(input)
1335
1990
  };
1336
1991
  }
1992
+ assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
1993
+ mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
1994
+ const hydrationConflicts = input.trigger.canonicalCheckoutOnly ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
1995
+ if (hydrationConflicts.length > 0 && candidateHeadBeforeRebase) {
1996
+ runGit(
1997
+ input,
1998
+ input.shadowRoot,
1999
+ ["reset", "--hard", candidateHeadBeforeRebase],
2000
+ "preserve old-base workspace candidate after hydration conflict"
2001
+ );
2002
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace candidate");
2003
+ const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...hydrationConflicts])].sort();
2004
+ return {
2005
+ ...observed,
2006
+ outcome: "failed",
2007
+ expectedHead,
2008
+ candidateHead,
2009
+ rebaseCount,
2010
+ affectedProjects: affectedProjects(conflictPaths),
2011
+ affectedPaths: reportedPaths(conflictPaths),
2012
+ error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved.",
2013
+ gitStatus: gitStatus(input)
2014
+ };
2015
+ }
1337
2016
  const upload = uploadWorkspaceCandidate(input, candidateHead);
1338
2017
  if (upload.exitCode !== 0) {
1339
2018
  return {
@@ -1346,8 +2025,6 @@ async function synchronizeWorkspace(rawInput) {
1346
2025
  gitStatus: gitStatus(input)
1347
2026
  };
1348
2027
  }
1349
- assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
1350
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
1351
2028
  return {
1352
2029
  ...observed,
1353
2030
  outcome: "candidate_ready",