@ricsam/r5d-worker 0.0.123 → 0.0.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/cjs/main.cjs +488 -238
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  4. package/dist/cjs/project-workspace-state.cjs +51 -35
  5. package/dist/cjs/project-worktrees.cjs +79 -34
  6. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  7. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  8. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  9. package/dist/cjs/recovery-journal.cjs +735 -0
  10. package/dist/cjs/recovery-store.cjs +8 -0
  11. package/dist/cjs/session-file-mutations.cjs +61 -0
  12. package/dist/cjs/working-tree-mirror.cjs +1 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  14. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  15. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  16. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  17. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  18. package/dist/cjs/workspace-git-sync.cjs +275 -201
  19. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  20. package/dist/mjs/main.mjs +491 -244
  21. package/dist/mjs/package.json +1 -1
  22. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  23. package/dist/mjs/project-workspace-state.mjs +51 -35
  24. package/dist/mjs/project-worktrees.mjs +74 -34
  25. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  26. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  27. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  28. package/dist/mjs/recovery-journal.mjs +687 -0
  29. package/dist/mjs/recovery-store.mjs +8 -0
  30. package/dist/mjs/session-file-mutations.mjs +37 -0
  31. package/dist/mjs/working-tree-mirror.mjs +1 -0
  32. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  33. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  34. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  35. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  36. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  37. package/dist/mjs/workspace-git-sync.mjs +264 -202
  38. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  39. package/dist/types/main.d.ts +24 -19
  40. package/dist/types/project-checkout-garbage.d.ts +19 -2
  41. package/dist/types/project-workspace-state.d.ts +9 -9
  42. package/dist/types/project-worktrees.d.ts +49 -5
  43. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  44. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  45. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  46. package/dist/types/recovery-journal-thread.d.ts +1 -0
  47. package/dist/types/recovery-journal.d.ts +246 -0
  48. package/dist/types/recovery-store.d.ts +6 -0
  49. package/dist/types/session-file-mutations.d.ts +22 -0
  50. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  51. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  52. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  53. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  54. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  55. package/dist/types/workspace-git-sync.d.ts +113 -7
  56. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  57. package/package.json +1 -1
  58. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  59. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  60. package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
@@ -3,9 +3,9 @@ import path from "node:path";
3
3
  import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
4
4
  import {
5
5
  materializeWorkspaceProjectionTree,
6
- mergeWorkspaceProjectionMount,
7
6
  workspaceMergeProjectionSupport
8
7
  } from "./workspace-merge-projection.mjs";
8
+ import { workspaceFilesystemExecutor } from "./workspace-filesystem-executor.mjs";
9
9
  import {
10
10
  captureWorkingTreeMirrorPlan,
11
11
  fsyncWorkingTreePaths,
@@ -23,6 +23,22 @@ const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
23
23
  const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
24
24
  const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
25
25
  const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
26
+ function workspaceGitMountData(mount) {
27
+ return {
28
+ id: mount.id,
29
+ hydrationIncarnationKey: mount.hydrationIncarnationKey,
30
+ sourcePath: mount.sourcePath,
31
+ durabilityRootPath: mount.durabilityRootPath,
32
+ workspaceRelativePath: mount.workspaceRelativePath,
33
+ sourceMode: mount.sourceMode,
34
+ hydrateDeletionMode: mount.hydrateDeletionMode,
35
+ ...mount.preserveLocalOnInitialOuterAbsence === void 0 ? {} : { preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence },
36
+ ...mount.preserveLocalOnHydrationBasisChange === void 0 ? {} : { preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange },
37
+ ...mount.initialPublicationSourceRelativePath === void 0 ? {} : { initialPublicationSourceRelativePath: mount.initialPublicationSourceRelativePath },
38
+ ...mount.deleteWhenSourceMissing === void 0 ? {} : { deleteWhenSourceMissing: mount.deleteWhenSourceMissing },
39
+ ...mount.lastProjectedMutationToken === void 0 ? {} : { lastProjectedMutationToken: mount.lastProjectedMutationToken }
40
+ };
41
+ }
26
42
  function workspaceGitMountsForRemediation(mounts) {
27
43
  return mounts.map((mount) => ({ ...mount, busy: mount.busyForRecovery ?? mount.busy }));
28
44
  }
@@ -114,9 +130,6 @@ async function revParseAsync(workspacePath, revision) {
114
130
  const result = await gitResultAsync(workspacePath, ["rev-parse", "--verify", revision]);
115
131
  return result.exitCode === 0 ? result.stdout : null;
116
132
  }
117
- function yieldToWorkerEventLoop() {
118
- return new Promise((resolve) => setImmediate(resolve));
119
- }
120
133
  function workspaceCommitsExist(workspacePath, heads) {
121
134
  const uniqueHeads = [...new Set(heads)];
122
135
  if (uniqueHeads.length === 0) return true;
@@ -381,20 +394,35 @@ function readHydratedWorkspaceReceipt(workspacePath) {
381
394
  }
382
395
  return unverified.receipt;
383
396
  }
397
+ async function readHydratedWorkspaceReceiptUnverifiedAsync(workspacePath) {
398
+ const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
399
+ let status;
400
+ try {
401
+ status = await fs.promises.lstat(receiptPath);
402
+ } catch (error) {
403
+ if (error.code === "ENOENT") return null;
404
+ throw error;
405
+ }
406
+ if (!status.isFile() || status.isSymbolicLink()) {
407
+ throw new Error(`Workspace hydration receipt is not a regular file: ${receiptPath}`);
408
+ }
409
+ const content = await fs.promises.readFile(receiptPath, "utf8");
410
+ return { ...parseWorkspaceHydrationReceiptContentUnverified(receiptPath, content), receiptPath };
411
+ }
384
412
  async function readHydratedWorkspaceReceiptAsync(workspacePath) {
385
- const unverified = readHydratedWorkspaceReceiptUnverified(workspacePath);
413
+ const unverified = await readHydratedWorkspaceReceiptUnverifiedAsync(workspacePath);
386
414
  if (!unverified) return null;
387
415
  if (!await workspaceCommitsExistAsync(workspacePath, unverified.headsToVerify)) {
388
416
  throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${unverified.receiptPath}`);
389
417
  }
390
418
  return unverified.receipt;
391
419
  }
392
- function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
420
+ async function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
393
421
  const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
394
- const status = lstatIfExists(receiptPath);
395
- if (!status?.isFile() || status.isSymbolicLink()) return false;
396
422
  try {
397
- return JSON.parse(fs.readFileSync(receiptPath, "utf8")).version === 3;
423
+ const status = await fs.promises.lstat(receiptPath);
424
+ if (!status.isFile() || status.isSymbolicLink()) return false;
425
+ return JSON.parse(await fs.promises.readFile(receiptPath, "utf8")).version === 3;
398
426
  } catch {
399
427
  return false;
400
428
  }
@@ -491,24 +519,6 @@ function runWorkspaceRefUpdateBatch(workspacePath, instructions, action) {
491
519
  throw new Error(`${action}: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
492
520
  }
493
521
  }
494
- async function runWorkspaceRefUpdateBatchAsync(workspacePath, instructions, action) {
495
- if (instructions.length === 0) return;
496
- const subprocess = Bun.spawn(gitCommandArgs(["update-ref", "--stdin"]), {
497
- cwd: workspacePath,
498
- stdin: Buffer.from([...instructions, ""].join("\n")),
499
- stdout: "pipe",
500
- stderr: "pipe",
501
- env: workerGitProcessEnvironment()
502
- });
503
- const [, stderr, exitCode] = await Promise.all([
504
- new Response(subprocess.stdout).text(),
505
- new Response(subprocess.stderr).text(),
506
- subprocess.exited
507
- ]);
508
- if (exitCode !== 0) {
509
- throw new Error(`${action}: ${stderr.trim() || `git exited ${exitCode}`}`);
510
- }
511
- }
512
522
  function workspaceBasisRefTransaction(receipt, stableRefs) {
513
523
  const desired = desiredWorkspaceBasisRefs(receipt);
514
524
  const transaction = ["start"];
@@ -531,24 +541,6 @@ function updateWorkspaceBasisRefs(workspacePath, receipt) {
531
541
  "remove staged workspace hydration basis refs"
532
542
  );
533
543
  }
534
- async function updateWorkspaceBasisRefsAsync(workspacePath, receipt) {
535
- const stableRefs = (await gitAsync(workspacePath, ["for-each-ref", "--format=%(refname)", WORKSPACE_GIT_BASIS_REF_PREFIX], "list workspace basis refs")).split("\n").filter(Boolean).sort();
536
- await runWorkspaceRefUpdateBatchAsync(
537
- workspacePath,
538
- workspaceBasisRefTransaction(receipt, stableRefs),
539
- "Synchronize workspace hydration basis refs"
540
- );
541
- const stagedRefs = (await gitAsync(
542
- workspacePath,
543
- ["for-each-ref", "--format=%(refname)", WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX],
544
- "list staged workspace basis refs"
545
- )).split("\n").filter(Boolean).sort();
546
- await runWorkspaceRefUpdateBatchAsync(
547
- workspacePath,
548
- stagedRefs.map((ref) => `delete ${ref}`),
549
- "remove staged workspace hydration basis refs"
550
- );
551
- }
552
544
  function assertCanonicalHydrationReceiptHeads(receipt) {
553
545
  for (const mount of receipt.mounts) {
554
546
  if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
@@ -562,20 +554,15 @@ function throwMissingHydrationReceiptCommit(workspacePath, receipt) {
562
554
  }
563
555
  function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
564
556
  assertCanonicalHydrationReceiptHeads(receipt);
565
- if (!workspaceCommitsExist(workspacePath, receipt.mounts.map(({ head }) => head))) {
557
+ if (!workspaceCommitsExist(
558
+ workspacePath,
559
+ receipt.mounts.map(({ head }) => head)
560
+ )) {
566
561
  throwMissingHydrationReceiptCommit(workspacePath, receipt);
567
562
  }
568
563
  readHydratedWorkspaceReceipt(workspacePath);
569
564
  writeHydratedWorkspaceReceiptCore(workspacePath, receipt);
570
565
  }
571
- async function updateHydratedWorkspaceReceiptAsync(workspacePath, receipt) {
572
- assertCanonicalHydrationReceiptHeads(receipt);
573
- if (!await workspaceCommitsExistAsync(workspacePath, receipt.mounts.map(({ head }) => head))) {
574
- throwMissingHydrationReceiptCommit(workspacePath, receipt);
575
- }
576
- await readHydratedWorkspaceReceiptAsync(workspacePath);
577
- await writeHydratedWorkspaceReceiptCoreAsync(workspacePath, receipt);
578
- }
579
566
  function prepareHydratedWorkspaceReceiptWrite(workspacePath) {
580
567
  const gitDirectory = path.join(workspacePath, ".git");
581
568
  const gitDirectoryStatus = fs.lstatSync(gitDirectory);
@@ -630,17 +617,6 @@ function writeHydratedWorkspaceReceiptCore(workspacePath, receipt) {
630
617
  publishHydratedWorkspaceReceiptFile(workspacePath, receipt, paths);
631
618
  updateWorkspaceBasisRefs(workspacePath, receipt);
632
619
  }
633
- async function writeHydratedWorkspaceReceiptCoreAsync(workspacePath, receipt) {
634
- const paths = prepareHydratedWorkspaceReceiptWrite(workspacePath);
635
- const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
636
- await runWorkspaceRefUpdateBatchAsync(
637
- workspacePath,
638
- stagedWorkspaceBasisRefInstructions(receipt, stagingPrefix),
639
- "stage workspace hydration basis refs"
640
- );
641
- publishHydratedWorkspaceReceiptFile(workspacePath, receipt, paths);
642
- await updateWorkspaceBasisRefsAsync(workspacePath, receipt);
643
- }
644
620
  function hydrationTransactionPath(workspacePath) {
645
621
  return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_TRANSACTION.split("/"));
646
622
  }
@@ -735,8 +711,21 @@ function readWorkspaceCheckoutDurabilityRecord(workspacePath) {
735
711
  return unverified.record;
736
712
  }
737
713
  async function readWorkspaceCheckoutDurabilityRecordAsync(workspacePath) {
738
- const unverified = readWorkspaceCheckoutDurabilityRecordUnverified(workspacePath);
739
- if (!unverified) return null;
714
+ const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
715
+ let status;
716
+ try {
717
+ status = await fs.promises.lstat(recordPath);
718
+ } catch (error) {
719
+ if (error.code === "ENOENT") return null;
720
+ throw error;
721
+ }
722
+ if (!status.isFile() || status.isSymbolicLink()) {
723
+ throw new Error(`Workspace checkout durability record is not a regular file: ${recordPath}`);
724
+ }
725
+ const unverified = {
726
+ record: parseWorkspaceCheckoutDurabilityRecordUnverified(recordPath, await fs.promises.readFile(recordPath, "utf8")),
727
+ recordPath
728
+ };
740
729
  if (unverified.record.head && !await tryGitAsync(workspacePath, ["cat-file", "-e", `${unverified.record.head}^{commit}`])) {
741
730
  throw new Error(`Workspace checkout durability record names a missing commit: ${unverified.recordPath}`);
742
731
  }
@@ -763,13 +752,6 @@ function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
763
752
  readWorkspaceCheckoutDurabilityRecord(workspacePath);
764
753
  writeWorkspaceCheckoutDurabilityRecordFile(workspacePath, record);
765
754
  }
766
- async function updateWorkspaceCheckoutDurabilityRecordAsync(workspacePath, record) {
767
- if (record.head && !await tryGitAsync(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
768
- throw new Error(`Cannot record workspace checkout durability for a missing commit: ${record.head}`);
769
- }
770
- await readWorkspaceCheckoutDurabilityRecordAsync(workspacePath);
771
- writeWorkspaceCheckoutDurabilityRecordFile(workspacePath, record);
772
- }
773
755
  function fsyncWorkspaceCheckoutTree(workspacePath) {
774
756
  const status = fs.lstatSync(workspacePath);
775
757
  if (!status.isDirectory() || status.isSymbolicLink()) {
@@ -781,40 +763,6 @@ function fsyncWorkspaceCheckoutTree(workspacePath) {
781
763
  }
782
764
  fsyncDirectory(workspacePath);
783
765
  }
784
- const WORKSPACE_FSYNC_YIELD_INTERVAL = 64;
785
- async function fsyncWorkspaceCheckoutTreeYielding(workspacePath) {
786
- const status = fs.lstatSync(workspacePath);
787
- if (!status.isDirectory() || status.isSymbolicLink()) {
788
- throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
789
- }
790
- let entriesSinceYield = 0;
791
- const syncEntry = async (entryPath) => {
792
- const entryStatus = fs.lstatSync(entryPath);
793
- if (entryStatus.isSymbolicLink()) return;
794
- if (entryStatus.isDirectory()) {
795
- for (const name of fs.readdirSync(entryPath)) await syncEntry(path.join(entryPath, name));
796
- fsyncDirectory(entryPath);
797
- return;
798
- }
799
- if (!entryStatus.isFile()) return;
800
- const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
801
- try {
802
- fs.fsyncSync(descriptor);
803
- } finally {
804
- fs.closeSync(descriptor);
805
- }
806
- entriesSinceYield += 1;
807
- if (entriesSinceYield >= WORKSPACE_FSYNC_YIELD_INTERVAL) {
808
- entriesSinceYield = 0;
809
- await yieldToWorkerEventLoop();
810
- }
811
- };
812
- for (const name of fs.readdirSync(workspacePath)) {
813
- if (name === ".git") continue;
814
- await syncEntry(path.join(workspacePath, name));
815
- }
816
- fsyncDirectory(workspacePath);
817
- }
818
766
  function workspaceRebaseInProgress(workspacePath) {
819
767
  const gitDirectory = path.join(workspacePath, ".git");
820
768
  return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
@@ -883,22 +831,36 @@ async function workspaceCheckoutDurabilityIsCurrentAsync(workspacePath) {
883
831
  const record = await readWorkspaceCheckoutDurabilityRecordAsync(workspacePath);
884
832
  return Boolean(record?.state === "durable" && record.head === await revParseAsync(workspacePath, "HEAD"));
885
833
  }
886
- function beginWorkspaceCheckoutTransition(workspacePath) {
834
+ function runCheckoutTransitionBeginJob(input) {
835
+ const workspacePath = path.resolve(input.workspacePath);
887
836
  recoverWorkspaceCheckoutDurability(workspacePath);
888
837
  const head = revParse(workspacePath, "HEAD");
889
838
  updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "transition", head });
839
+ return null;
840
+ }
841
+ function runCheckoutTransitionCompleteJob(input) {
842
+ const workspacePath = path.resolve(input.workspacePath);
843
+ fsyncWorkspaceCheckoutTree(workspacePath);
844
+ updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "durable", head: revParse(workspacePath, "HEAD") });
845
+ return null;
846
+ }
847
+ function runCheckoutDurabilityRecoveryJob(input) {
848
+ recoverWorkspaceCheckoutDurability(path.resolve(input.workspacePath), input.preserveResolutionInProgress);
849
+ return null;
850
+ }
851
+ function runOuterCheckoutFsyncJob(input) {
852
+ fsyncWorkspaceCheckoutTree(path.resolve(input.workspacePath));
853
+ return null;
854
+ }
855
+ async function beginWorkspaceCheckoutTransition(workspacePath) {
856
+ await workspaceFilesystemExecutor().run("checkout_transition_begin", { workspacePath });
890
857
  }
891
858
  async function completeWorkspaceCheckoutTransition(workspacePath) {
892
- await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
893
- await updateWorkspaceCheckoutDurabilityRecordAsync(workspacePath, {
894
- version: 1,
895
- state: "durable",
896
- head: await revParseAsync(workspacePath, "HEAD")
897
- });
859
+ await workspaceFilesystemExecutor().run("checkout_transition_complete", { workspacePath });
898
860
  }
899
- function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
861
+ async function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
900
862
  try {
901
- recoverWorkspaceCheckoutDurability(workspacePath);
863
+ await workspaceFilesystemExecutor().run("checkout_durability_recovery", { workspacePath, preserveResolutionInProgress: false });
902
864
  } catch (recoveryError) {
903
865
  throw new AggregateError(
904
866
  [error, recoveryError],
@@ -908,12 +870,12 @@ function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
908
870
  throw error;
909
871
  }
910
872
  async function runWorkspaceCheckoutTransition(workspacePath, mutate) {
911
- beginWorkspaceCheckoutTransition(workspacePath);
873
+ await beginWorkspaceCheckoutTransition(workspacePath);
912
874
  try {
913
875
  await mutate();
914
876
  await completeWorkspaceCheckoutTransition(workspacePath);
915
877
  } catch (error) {
916
- throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
878
+ await throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
917
879
  }
918
880
  }
919
881
  function parseHydrationTransactionManifest(transactionPath) {
@@ -1295,8 +1257,16 @@ async function configureWorkspaceRepository(input) {
1295
1257
  await gitAsync(input.workspacePath, ["remote", "add", "origin", input.remoteUrl], "configure workspace origin");
1296
1258
  }
1297
1259
  await tryGitAsync(input.workspacePath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
1298
- await gitAsync(input.workspacePath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset workspace credential helpers");
1299
- await gitAsync(input.workspacePath, ["config", "--local", "credential.useHttpPath", "false"], "configure workspace credential path matching");
1260
+ await gitAsync(
1261
+ input.workspacePath,
1262
+ ["config", "--local", "--replace-all", "credential.helper", ""],
1263
+ "reset workspace credential helpers"
1264
+ );
1265
+ await gitAsync(
1266
+ input.workspacePath,
1267
+ ["config", "--local", "credential.useHttpPath", "false"],
1268
+ "configure workspace credential path matching"
1269
+ );
1300
1270
  if (input.credentialUsername) {
1301
1271
  await gitAsync(
1302
1272
  input.workspacePath,
@@ -1394,19 +1364,24 @@ async function ensureWorkspaceGitClone(input) {
1394
1364
  input.credentialHelper,
1395
1365
  input.credentialUsername
1396
1366
  );
1397
- const clone = Bun.spawnSync(cloneCommand, {
1367
+ const clone = Bun.spawn(cloneCommand, {
1368
+ stdin: "ignore",
1398
1369
  stdout: "pipe",
1399
1370
  stderr: "pipe",
1400
1371
  env: workerGitProcessEnvironment()
1401
1372
  });
1402
- if (clone.exitCode !== 0) {
1373
+ const [, , cloneExitCode] = await Promise.all([new Response(clone.stdout).text(), new Response(clone.stderr).text(), clone.exited]);
1374
+ if (cloneExitCode !== 0) {
1403
1375
  fs.rmSync(workspacePath, { recursive: true, force: true });
1404
1376
  fs.mkdirSync(workspacePath, { recursive: true });
1405
1377
  git(workspacePath, ["init", `--initial-branch=${WORKSPACE_GIT_BRANCH}`], "initialize workspace clone");
1406
1378
  }
1407
1379
  }
1408
1380
  await configureWorkspaceRepository({ ...input, workspacePath });
1409
- recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
1381
+ await workspaceFilesystemExecutor().run("checkout_durability_recovery", {
1382
+ workspacePath,
1383
+ preserveResolutionInProgress: input.preserveResolutionInProgress === true
1384
+ });
1410
1385
  const previousRemoteHead = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
1411
1386
  const localHeadBeforeFetch = await revParseAsync(workspacePath, "HEAD");
1412
1387
  if (!await revParseAsync(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && await tryGitAsync(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
@@ -1685,7 +1660,7 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1685
1660
  };
1686
1661
  };
1687
1662
  const noopCandidate = computeSelection();
1688
- if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
1663
+ if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && await hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
1689
1664
  return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
1690
1665
  }
1691
1666
  const hooks = input.hydrationHooks;
@@ -1698,47 +1673,92 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1698
1673
  try {
1699
1674
  const selection = computeSelection();
1700
1675
  const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1701
- const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1676
+ const heldMountIds = [...new Set([...hydrationMounts, ...advancedDurabilityMounts].map(({ id }) => id))];
1677
+ const releaseHold = hooks?.holdMounts(heldMountIds, `hydration transaction ${context.transactionId}`);
1702
1678
  try {
1703
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1704
- const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1705
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1706
- throw new Error("Workspace hydration did not include every required mount");
1707
- }
1708
- fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
1709
- markHydrationTransactionDurable(workspacePath);
1710
- if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1711
- removeHydrationTransaction(workspacePath);
1712
- return {
1713
- hydratedMountIds: hydration.hydratedMountIds,
1714
- skippedMountIds: selection.skippedMountIds
1715
- };
1716
- } catch (error) {
1717
- const transactionPath = hydrationTransactionPath(workspacePath);
1718
- if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1719
- throw error;
1720
- }
1721
- try {
1722
- restoreHydrationTransaction(workspacePath, manifest);
1723
- removeHydrationTransaction(workspacePath);
1724
- } catch (restoreError) {
1725
- throw new AggregateError(
1726
- [error, restoreError],
1727
- `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1728
- );
1729
- }
1730
- throw error;
1679
+ const hydration = await workspaceFilesystemExecutor().run("hydration_transaction", {
1680
+ workspacePath,
1681
+ hydrationMounts: hydrationMounts.map(workspaceGitMountData),
1682
+ advancedDurabilityMounts: advancedDurabilityMounts.map(workspaceGitMountData),
1683
+ targetReceipt
1684
+ });
1685
+ return { hydratedMountIds: hydration.hydratedMountIds, skippedMountIds: selection.skippedMountIds };
1686
+ } finally {
1687
+ releaseHold?.();
1731
1688
  }
1732
1689
  } finally {
1733
1690
  hooks?.afterTransaction(context);
1734
1691
  }
1735
1692
  }
1693
+ function runHydrationTransactionJob(input) {
1694
+ const workspacePath = path.resolve(input.workspacePath);
1695
+ const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = input;
1696
+ const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1697
+ try {
1698
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1699
+ const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1700
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1701
+ throw new Error("Workspace hydration did not include every required mount");
1702
+ }
1703
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
1704
+ markHydrationTransactionDurable(workspacePath);
1705
+ if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1706
+ removeHydrationTransaction(workspacePath);
1707
+ return { hydratedMountIds: hydration.hydratedMountIds };
1708
+ } catch (error) {
1709
+ const transactionPath = hydrationTransactionPath(workspacePath);
1710
+ if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1711
+ throw error;
1712
+ }
1713
+ try {
1714
+ restoreHydrationTransaction(workspacePath, manifest);
1715
+ removeHydrationTransaction(workspacePath);
1716
+ } catch (restoreError) {
1717
+ throw new AggregateError(
1718
+ [error, restoreError],
1719
+ `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1720
+ );
1721
+ }
1722
+ throw error;
1723
+ }
1724
+ }
1725
+ function runHydrationRawJob(input) {
1726
+ return {
1727
+ hydratedMountIds: hydrateWorkspaceGitMountsRaw(path.resolve(input.workspacePath), input.mounts, { ignoreBusy: true }).hydratedMountIds
1728
+ };
1729
+ }
1730
+ function runHydrationRecoveryJob(input) {
1731
+ const workspacePath = path.resolve(input.workspacePath);
1732
+ recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
1733
+ recoverPendingHydrationTransaction(workspacePath);
1734
+ repairWorkspaceBasisRefs(workspacePath, readHydratedWorkspaceReceipt(workspacePath));
1735
+ return null;
1736
+ }
1736
1737
  async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1737
- const gitDirectory = path.join(path.resolve(workspacePath), ".git");
1738
+ const resolvedWorkspacePath = path.resolve(workspacePath);
1739
+ const gitDirectory = path.join(resolvedWorkspacePath, ".git");
1738
1740
  if (!fs.existsSync(gitDirectory)) {
1739
- return hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options);
1741
+ const skippedMountIds = options.ignoreBusy ? [] : mounts.filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
1742
+ const skipped = new Set(skippedMountIds);
1743
+ const selected = mounts.filter(({ id }) => !skipped.has(id));
1744
+ const releaseHold = options.hydrationHooks?.holdMounts(
1745
+ selected.map(({ id }) => id),
1746
+ "pre-clone hydration"
1747
+ );
1748
+ try {
1749
+ const hydration = await workspaceFilesystemExecutor().run("hydration_raw", {
1750
+ workspacePath: resolvedWorkspacePath,
1751
+ mounts: selected.map(workspaceGitMountData)
1752
+ });
1753
+ return { hydratedMountIds: hydration.hydratedMountIds, skippedMountIds };
1754
+ } finally {
1755
+ releaseHold?.();
1756
+ }
1740
1757
  }
1741
- recoverWorkspaceCheckoutDurability(path.resolve(workspacePath));
1758
+ await workspaceFilesystemExecutor().run("checkout_durability_recovery", {
1759
+ workspacePath: resolvedWorkspacePath,
1760
+ preserveResolutionInProgress: false
1761
+ });
1742
1762
  return hydrateWorkspaceGitMountsTransactionally({
1743
1763
  workspacePath,
1744
1764
  mounts,
@@ -1749,13 +1769,12 @@ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1749
1769
  async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1750
1770
  workspacePath = path.resolve(workspacePath);
1751
1771
  validateMounts(workspacePath, mounts);
1752
- recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
1753
- recoverPendingHydrationTransaction(workspacePath);
1772
+ await workspaceFilesystemExecutor().run("hydration_recovery", {
1773
+ workspacePath,
1774
+ preserveResolutionInProgress: options.preserveResolutionInProgress === true
1775
+ });
1754
1776
  const currentHead = await revParseAsync(workspacePath, "HEAD");
1755
1777
  const currentReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
1756
- if (!await workspaceBasisRefsAreCurrentAsync(workspacePath, currentReceipt ?? { version: 3, mounts: [] })) {
1757
- repairWorkspaceBasisRefs(workspacePath, currentReceipt);
1758
- }
1759
1778
  if (!currentHead) return;
1760
1779
  const outerHeadContainsMount = (mount) => tryGitAsync(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
1761
1780
  const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
@@ -1796,13 +1815,17 @@ async function resetWorkspaceGit(input) {
1796
1815
  throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
1797
1816
  }
1798
1817
  const initial = await ensureWorkspaceGitClone({ ...input, workspacePath });
1799
- recoverPendingHydrationTransaction(workspacePath);
1818
+ await workspaceFilesystemExecutor().run("hydration_recovery", { workspacePath, preserveResolutionInProgress: false });
1800
1819
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
1801
1820
  const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
1802
1821
  const remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
1803
1822
  if (remoteHead) {
1804
1823
  await runWorkspaceCheckoutTransition(workspacePath, async () => {
1805
- await gitAsync(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
1824
+ await gitAsync(
1825
+ workspacePath,
1826
+ ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead],
1827
+ "reset workspace main"
1828
+ );
1806
1829
  await gitAsync(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
1807
1830
  });
1808
1831
  } else if (revParse(workspacePath, "HEAD")) {
@@ -2013,7 +2036,7 @@ function snapshotConflict(input) {
2013
2036
  }
2014
2037
  async function rebaseWorkspace(input) {
2015
2038
  const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
2016
- beginWorkspaceCheckoutTransition(input.workspacePath);
2039
+ await beginWorkspaceCheckoutTransition(input.workspacePath);
2017
2040
  const rebase = await gitResultAsync(
2018
2041
  input.workspacePath,
2019
2042
  input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
@@ -2183,18 +2206,24 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2183
2206
  cycleSkippedMountIds.add(mount.id);
2184
2207
  continue;
2185
2208
  }
2186
- const merged = mergeWorkspaceProjectionMount({
2187
- workspacePath,
2188
- mount: {
2189
- id: mount.id,
2190
- sourcePath: mount.sourcePath,
2191
- workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
2192
- sourceMode: mount.sourceMode
2193
- },
2194
- basisHead,
2195
- currentHead: projectionBasisHead,
2196
- attemptId
2197
- });
2209
+ const releaseMergeHold = input.hydrationHooks?.holdMounts([mount.id], `merge projection of mount ${mount.id}`);
2210
+ let merged;
2211
+ try {
2212
+ merged = await workspaceFilesystemExecutor().run("projection_merge", {
2213
+ workspacePath,
2214
+ mount: {
2215
+ id: mount.id,
2216
+ sourcePath: mount.sourcePath,
2217
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
2218
+ sourceMode: mount.sourceMode
2219
+ },
2220
+ basisHead,
2221
+ currentHead: projectionBasisHead,
2222
+ attemptId
2223
+ });
2224
+ } finally {
2225
+ releaseMergeHold?.();
2226
+ }
2198
2227
  if (merged.kind === "conflict") {
2199
2228
  const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
2200
2229
  return {
@@ -2217,7 +2246,6 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2217
2246
  }
2218
2247
  mergedProjectionTrees.set(mount.id, merged.resultTree);
2219
2248
  mergedProjectionMounts.push(mount);
2220
- await yieldToWorkerEventLoop();
2221
2249
  }
2222
2250
  }
2223
2251
  if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
@@ -2300,7 +2328,7 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2300
2328
  const projectedFastMounts = [];
2301
2329
  const currentProjectionMounts = [];
2302
2330
  if (!input.skipMountMirror) {
2303
- beginWorkspaceCheckoutTransition(workspacePath);
2331
+ await beginWorkspaceCheckoutTransition(workspacePath);
2304
2332
  try {
2305
2333
  const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
2306
2334
  const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
@@ -2314,30 +2342,34 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2314
2342
  currentProjectionMounts.push(mount);
2315
2343
  continue;
2316
2344
  }
2317
- projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
2345
+ const releaseMirrorHold = input.hydrationHooks?.holdMounts([mount.id], `projection of mount ${mount.id}`);
2346
+ try {
2347
+ const mirrored = await workspaceFilesystemExecutor().run("projection_mirror", {
2348
+ workspacePath,
2349
+ mount: workspaceGitMountData(mount)
2350
+ });
2351
+ projectedGitlinks.push({ mount, gitlinks: mirrored.gitlinks });
2352
+ } finally {
2353
+ releaseMirrorHold?.();
2354
+ }
2318
2355
  projectedFastMounts.push(mount);
2319
- await yieldToWorkerEventLoop();
2320
2356
  }
2321
- removeWorkspaceMounts(workspacePath, fastTombstones);
2322
- projectedFastMounts.push(...fastTombstones);
2323
- for (const mount of mergedProjectionMounts) {
2324
- materializeWorkspaceProjectionTree({
2325
- workspacePath,
2357
+ await workspaceFilesystemExecutor().run("projection_outer_apply", {
2358
+ workspacePath,
2359
+ removeMounts: fastTombstones.map(workspaceGitMountData),
2360
+ materialize: mergedProjectionMounts.map((mount) => ({
2326
2361
  workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
2327
2362
  resultTree: mergedProjectionTrees.get(mount.id)
2328
- });
2329
- await yieldToWorkerEventLoop();
2330
- }
2331
- restoreDeferredWorkspaceMountsFromHead(
2332
- workspacePath,
2333
- input.mounts.filter(
2363
+ })),
2364
+ restoreFromHead: input.mounts.filter(
2334
2365
  (mount) => deferredMountIds.has(mount.id) || mount.preserveLocalOnHydrationBasisChange === true && cycleSkippedMountIds.has(mount.id)
2335
- )
2336
- );
2366
+ ).map(workspaceGitMountData)
2367
+ });
2368
+ projectedFastMounts.push(...fastTombstones);
2337
2369
  await stageAndCommitWorkspace();
2338
- await fsyncWorkspaceCheckoutTreeYielding(workspacePath);
2370
+ await workspaceFilesystemExecutor().run("outer_checkout_fsync", { workspacePath });
2339
2371
  } catch (error) {
2340
- throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
2372
+ await throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
2341
2373
  }
2342
2374
  } else {
2343
2375
  await stageAndCommitWorkspace();
@@ -2353,12 +2385,12 @@ async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
2353
2385
  allProjectionReceiptMounts
2354
2386
  );
2355
2387
  const currentProjectionReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
2356
- if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !await workspaceBasisRefsAreCurrentAsync(workspacePath, targetProjectionReceipt)) {
2357
- for (const mount of projectedFastMounts) {
2358
- fsyncHydratedWorkspaceMount(mount);
2359
- await yieldToWorkerEventLoop();
2360
- }
2361
- await updateHydratedWorkspaceReceiptAsync(workspacePath, targetProjectionReceipt);
2388
+ if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !await hydratedWorkspaceReceiptFileIsV3(workspacePath) || !await workspaceBasisRefsAreCurrentAsync(workspacePath, targetProjectionReceipt)) {
2389
+ await workspaceFilesystemExecutor().run("projection_receipt", {
2390
+ workspacePath,
2391
+ fsyncMounts: projectedFastMounts.map(workspaceGitMountData),
2392
+ receipt: targetProjectionReceipt
2393
+ });
2362
2394
  }
2363
2395
  await completeWorkspaceCheckoutTransition(workspacePath);
2364
2396
  }
@@ -2525,6 +2557,25 @@ ${push.stdout}`)) {
2525
2557
  }
2526
2558
  throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
2527
2559
  }
2560
+ function runProjectionMirrorJob(input) {
2561
+ const projected = mirrorMountsToWorkspace(path.resolve(input.workspacePath), [input.mount]);
2562
+ return { gitlinks: projected[0]?.gitlinks ?? [] };
2563
+ }
2564
+ function runProjectionOuterApplyJob(input) {
2565
+ const workspacePath = path.resolve(input.workspacePath);
2566
+ removeWorkspaceMounts(workspacePath, input.removeMounts);
2567
+ for (const { workspaceRelativePath, resultTree } of input.materialize) {
2568
+ materializeWorkspaceProjectionTree({ workspacePath, workspaceRelativePath, resultTree });
2569
+ }
2570
+ restoreDeferredWorkspaceMountsFromHead(workspacePath, input.restoreFromHead);
2571
+ return null;
2572
+ }
2573
+ function runProjectionReceiptJob(input) {
2574
+ const workspacePath = path.resolve(input.workspacePath);
2575
+ for (const mount of input.fsyncMounts) fsyncHydratedWorkspaceMount(mount);
2576
+ updateHydratedWorkspaceReceipt(workspacePath, input.receipt);
2577
+ return null;
2578
+ }
2528
2579
  const workspaceGitSyncTestHarness = {
2529
2580
  commandArgs: gitCommandArgs,
2530
2581
  workspaceCloneCommandArgs,
@@ -2545,9 +2596,20 @@ export {
2545
2596
  hydrateWorkspaceGitMounts,
2546
2597
  recoverWorkspaceGitHydration,
2547
2598
  resetWorkspaceGit,
2599
+ runCheckoutDurabilityRecoveryJob,
2600
+ runCheckoutTransitionBeginJob,
2601
+ runCheckoutTransitionCompleteJob,
2602
+ runHydrationRawJob,
2603
+ runHydrationRecoveryJob,
2604
+ runHydrationTransactionJob,
2605
+ runOuterCheckoutFsyncJob,
2606
+ runProjectionMirrorJob,
2607
+ runProjectionOuterApplyJob,
2608
+ runProjectionReceiptJob,
2548
2609
  synchronizeWorkspaceGit,
2549
2610
  workspaceGitHydrationIsCurrent,
2550
2611
  workspaceGitHydrationIsRecoverable,
2612
+ workspaceGitMountData,
2551
2613
  workspaceGitMountsForRemediation,
2552
2614
  workspaceGitSyncTestHarness
2553
2615
  };