@ricsam/r5d-worker 0.0.81 → 0.0.83

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.
@@ -34,6 +34,8 @@ __export(workspace_git_sync_exports, {
34
34
  WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION: () => WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
35
35
  WORKSPACE_GIT_HYDRATED_RECEIPT: () => WORKSPACE_GIT_HYDRATED_RECEIPT,
36
36
  WORKSPACE_GIT_HYDRATION_TRANSACTION: () => WORKSPACE_GIT_HYDRATION_TRANSACTION,
37
+ WorkspaceRemediationAncestryError: () => WorkspaceRemediationAncestryError,
38
+ configureExistingWorkspaceGitForRemediation: () => configureExistingWorkspaceGitForRemediation,
37
39
  ensureWorkspaceGitClone: () => ensureWorkspaceGitClone,
38
40
  hydrateWorkspaceGitMounts: () => hydrateWorkspaceGitMounts,
39
41
  recoverWorkspaceGitHydration: () => recoverWorkspaceGitHydration,
@@ -46,6 +48,7 @@ module.exports = __toCommonJS(workspace_git_sync_exports);
46
48
  var import_node_fs = __toESM(require("node:fs"), 1);
47
49
  var import_node_path = __toESM(require("node:path"), 1);
48
50
  var import_git_process_environment = require("./git-process-environment.cjs");
51
+ var import_workspace_merge_projection = require("./workspace-merge-projection.cjs");
49
52
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
50
53
  var import_workspace_mount_boundary = require("./workspace-mount-boundary.cjs");
51
54
  const WORKSPACE_GIT_BRANCH = "main";
@@ -55,6 +58,12 @@ const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
55
58
  const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
56
59
  const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
57
60
  const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
61
+ class WorkspaceRemediationAncestryError extends Error {
62
+ constructor(message) {
63
+ super(message);
64
+ this.name = "WorkspaceRemediationAncestryError";
65
+ }
66
+ }
58
67
  const NON_RECURSIVE_GIT_CONFIG = [
59
68
  "-c",
60
69
  "submodule.recurse=false",
@@ -94,6 +103,29 @@ function revParse(workspacePath, revision) {
94
103
  const result = gitResult(workspacePath, ["rev-parse", "--verify", revision]);
95
104
  return result.exitCode === 0 ? result.stdout : null;
96
105
  }
106
+ function requiredWorkspaceAncestorHeads(value) {
107
+ if (value === void 0) return [];
108
+ if (!Array.isArray(value) || value.length === 0 || value.some((head) => typeof head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head)) || new Set(value).size !== value.length) {
109
+ throw new WorkspaceRemediationAncestryError("Workspace remediation ancestor requirements are invalid");
110
+ }
111
+ return [...value];
112
+ }
113
+ function assertWorkspaceRemediationAncestry(input) {
114
+ if (input.requiredAncestorHeads.length === 0) return;
115
+ if (!input.currentHead) throw new WorkspaceRemediationAncestryError("Workspace remediation has no resolved HEAD to verify");
116
+ for (const requiredHead of input.requiredAncestorHeads) {
117
+ if (!tryGit(input.workspacePath, ["cat-file", "-e", `${requiredHead}^{commit}`]) || !tryGit(input.workspacePath, ["merge-base", "--is-ancestor", requiredHead, input.currentHead])) {
118
+ throw new WorkspaceRemediationAncestryError(
119
+ `Workspace remediation HEAD does not descend from required conflict commit ${requiredHead}`
120
+ );
121
+ }
122
+ }
123
+ if (!input.remoteHead || !tryGit(input.workspacePath, ["cat-file", "-e", `${input.remoteHead}^{commit}`]) || !tryGit(input.workspacePath, ["merge-base", "--is-ancestor", input.remoteHead, input.currentHead])) {
124
+ throw new WorkspaceRemediationAncestryError(
125
+ "Fetched workspace head is not an ancestor of the resolved remediation HEAD; merge it explicitly before synchronizing"
126
+ );
127
+ }
128
+ }
97
129
  function updateIntegratedWorkspaceHead(workspacePath, head) {
98
130
  git(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
99
131
  }
@@ -140,22 +172,52 @@ function compareWorkspaceHydrationMountBasis(left, right) {
140
172
  return left.id.localeCompare(right.id);
141
173
  }
142
174
  function workspaceHydrationReceipt(head, mounts) {
143
- const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
175
+ const bases = mounts.map((mount) => ({ ...workspaceHydrationMountBasis(mount), head })).sort(compareWorkspaceHydrationMountBasis);
144
176
  const ids = /* @__PURE__ */ new Set();
145
177
  for (const basis of bases) {
146
178
  if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
147
179
  ids.add(basis.id);
148
180
  }
149
- return { version: 2, head, mounts: bases };
181
+ return { version: 3, mounts: bases };
182
+ }
183
+ function receiptWithMountsAtHead(previous, head, mounts, allConfiguredMounts) {
184
+ const advanceIds = new Set(mounts.map(({ id }) => id));
185
+ const previousById = new Map(previous?.mounts.map((entry) => [entry.id, entry]) ?? []);
186
+ const entries = allConfiguredMounts.flatMap((mount) => {
187
+ const basis = workspaceHydrationMountBasis(mount);
188
+ if (advanceIds.has(mount.id)) return [{ ...basis, head }];
189
+ const carried = previousById.get(mount.id);
190
+ if (!carried) return [];
191
+ return [carried];
192
+ });
193
+ entries.sort(compareWorkspaceHydrationMountBasis);
194
+ const ids = /* @__PURE__ */ new Set();
195
+ for (const entry of entries) {
196
+ if (ids.has(entry.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${entry.id}`);
197
+ ids.add(entry.id);
198
+ }
199
+ return { version: 3, mounts: entries };
150
200
  }
151
201
  function workspaceHydrationReceiptsEqual(left, right) {
152
- return JSON.stringify(left) === JSON.stringify(right);
202
+ if (JSON.stringify(left) !== JSON.stringify(right)) return false;
203
+ const effectiveLegacyHead = (receipt) => receipt?.mounts.length === 0 ? receipt.legacyHead ?? null : null;
204
+ return effectiveLegacyHead(left) === effectiveLegacyHead(right);
153
205
  }
154
206
  function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
155
- if (!receipt || receipt.head !== head) return false;
207
+ if (!receipt) return false;
208
+ const expected = workspaceHydrationMountBasis(mount);
209
+ const actual = receipt.mounts.find(({ id }) => id === expected.id);
210
+ if (!actual || actual.head !== head) return false;
211
+ const { head: _head, ...actualBasis } = actual;
212
+ return JSON.stringify(actualBasis) === JSON.stringify(expected);
213
+ }
214
+ function workspaceHydrationReceiptMountBasisHead(receipt, mount) {
215
+ if (!receipt) return null;
156
216
  const expected = workspaceHydrationMountBasis(mount);
157
217
  const actual = receipt.mounts.find(({ id }) => id === expected.id);
158
- return Boolean(actual && JSON.stringify(actual) === JSON.stringify(expected));
218
+ if (!actual) return null;
219
+ const { head, ...actualBasis } = actual;
220
+ return JSON.stringify(actualBasis) === JSON.stringify(expected) ? head : null;
159
221
  }
160
222
  function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
161
223
  return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
@@ -176,7 +238,9 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
176
238
  if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
177
239
  throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
178
240
  }
179
- return { version: 2, head: legacyHead, mounts: [] };
241
+ const receipt2 = { version: 3, mounts: [] };
242
+ Object.defineProperty(receipt2, "legacyHead", { value: legacyHead, enumerable: false });
243
+ return receipt2;
180
244
  }
181
245
  let parsed;
182
246
  try {
@@ -188,7 +252,8 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
188
252
  throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
189
253
  }
190
254
  const candidate = parsed;
191
- if (candidate.version !== 2 || typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head)) {
255
+ const liftedV2Head = candidate.version === 2 && typeof candidate.head === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head) ? candidate.head : null;
256
+ if (candidate.version !== 3 && liftedV2Head === null) {
192
257
  throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
193
258
  }
194
259
  if (!Array.isArray(candidate.mounts)) {
@@ -202,23 +267,51 @@ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, conte
202
267
  const mount = rawMount;
203
268
  const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
204
269
  const durabilityRelative = typeof mount.sourcePath === "string" && typeof mount.durabilityRootPath === "string" ? import_node_path.default.relative(mount.durabilityRootPath, mount.sourcePath) : "..";
205
- if (typeof mount.id !== "string" || !mount.id || seenIds.has(mount.id) || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || import_node_path.default.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || import_node_path.default.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(durabilityRelative) || typeof mount.workspaceRelativePath !== "string" || mount.workspaceRelativePath !== normalizedRelativePath || !normalizedRelativePath || normalizedRelativePath === "." || normalizedRelativePath.split("/").some((segment) => segment === ".." || segment === ".git") || mount.sourceMode !== "all" && mount.sourceMode !== "git" || mount.hydrateDeletionMode !== "all" && mount.hydrateDeletionMode !== "git" || typeof mount.preserveLocalOnInitialOuterAbsence !== "boolean" || typeof mount.preserveLocalOnHydrationBasisChange !== "boolean" || typeof mount.deleteWhenSourceMissing !== "boolean") {
270
+ if (typeof mount.id !== "string" || !mount.id || seenIds.has(mount.id) || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || import_node_path.default.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || import_node_path.default.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(durabilityRelative) || typeof mount.workspaceRelativePath !== "string" || mount.workspaceRelativePath !== normalizedRelativePath || !normalizedRelativePath || normalizedRelativePath === "." || normalizedRelativePath.split("/").some((segment) => segment === ".." || segment === ".git") || mount.sourceMode !== "all" && mount.sourceMode !== "git" || mount.hydrateDeletionMode !== "all" && mount.hydrateDeletionMode !== "git" || typeof mount.preserveLocalOnInitialOuterAbsence !== "boolean" || typeof mount.preserveLocalOnHydrationBasisChange !== "boolean" || typeof mount.deleteWhenSourceMissing !== "boolean" || candidate.version === 3 && (typeof mount.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head))) {
206
271
  throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
207
272
  }
208
273
  seenIds.add(mount.id);
209
- return mount;
274
+ return {
275
+ id: mount.id,
276
+ incarnationKey: mount.incarnationKey,
277
+ sourcePath: mount.sourcePath,
278
+ durabilityRootPath: mount.durabilityRootPath,
279
+ workspaceRelativePath: mount.workspaceRelativePath,
280
+ sourceMode: mount.sourceMode,
281
+ hydrateDeletionMode: mount.hydrateDeletionMode,
282
+ preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence,
283
+ preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange,
284
+ deleteWhenSourceMissing: mount.deleteWhenSourceMissing,
285
+ head: candidate.version === 3 ? mount.head : liftedV2Head
286
+ };
210
287
  });
288
+ const sortedMounts = mounts.sort(compareWorkspaceHydrationMountBasis);
289
+ if (liftedV2Head) {
290
+ const legacyReceipt = {
291
+ version: 2,
292
+ head: liftedV2Head,
293
+ mounts: sortedMounts.map(({ head: _head, ...basis }) => basis)
294
+ };
295
+ if (content !== `${JSON.stringify(legacyReceipt)}
296
+ `) {
297
+ throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
298
+ }
299
+ }
211
300
  const receipt = {
212
- version: 2,
213
- head: candidate.head,
214
- mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
301
+ version: 3,
302
+ mounts: sortedMounts
215
303
  };
216
- if (content !== `${JSON.stringify(receipt)}
304
+ if (liftedV2Head) Object.defineProperty(receipt, "legacyHead", { value: liftedV2Head, enumerable: false });
305
+ if (candidate.version === 3 && content !== `${JSON.stringify(receipt)}
217
306
  `) {
218
307
  throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
219
308
  }
220
- if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
221
- throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
309
+ const receiptHeads = new Set(receipt.mounts.map((mount) => mount.head));
310
+ if (liftedV2Head) receiptHeads.add(liftedV2Head);
311
+ for (const head of receiptHeads) {
312
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${head}^{commit}`])) {
313
+ throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
314
+ }
222
315
  }
223
316
  return receipt;
224
317
  }
@@ -232,22 +325,92 @@ function readHydratedWorkspaceReceipt(workspacePath) {
232
325
  const content = import_node_fs.default.readFileSync(receiptPath, "utf8");
233
326
  return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
234
327
  }
328
+ function hydratedWorkspaceReceiptFileIsV3(workspacePath) {
329
+ const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
330
+ const status = lstatIfExists(receiptPath);
331
+ if (!status?.isFile() || status.isSymbolicLink()) return false;
332
+ try {
333
+ return JSON.parse(import_node_fs.default.readFileSync(receiptPath, "utf8")).version === 3;
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
235
338
  function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
236
339
  const resolvedWorkspacePath = import_node_path.default.resolve(workspacePath);
237
340
  if (!import_node_fs.default.existsSync(import_node_path.default.join(resolvedWorkspacePath, ".git"))) return true;
238
341
  if (mounts) validateMounts(resolvedWorkspacePath, mounts);
239
342
  const head = revParse(resolvedWorkspacePath, "HEAD");
240
343
  if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
241
- if (head === null) return true;
242
344
  const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
243
- return mounts ? workspaceHydrationReceiptMatchesMounts(receipt, head, configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts)) : receipt?.head === head;
345
+ if (!workspaceBasisRefsAreCurrent(resolvedWorkspacePath, receipt ?? { version: 3, mounts: [] })) return false;
346
+ if (head === null) return true;
347
+ if (!receipt) return false;
348
+ return mounts ? configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts).every(
349
+ (mount) => workspaceHydrationReceiptCoversMount(receipt, head, mount) || Boolean(mount.busy?.() && workspaceHydrationReceiptMountBasisHead(receipt, mount))
350
+ ) : Boolean(receipt && receipt.mounts.every((mount) => mount.head === head));
351
+ }
352
+ const WORKSPACE_GIT_BASIS_REF_PREFIX = "refs/r5d/workspace-basis/";
353
+ const WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX = "refs/r5d/workspace-basis-staging/";
354
+ function workspaceBasisRefName(mountId) {
355
+ const sanitized = Buffer.from(mountId, "utf8").toString("base64url");
356
+ if (!sanitized) throw new Error(`Cannot derive a workspace basis ref for mount id: ${mountId}`);
357
+ return `${WORKSPACE_GIT_BASIS_REF_PREFIX}${sanitized}`;
358
+ }
359
+ function listWorkspaceRefs(workspacePath, prefix) {
360
+ return git(workspacePath, ["for-each-ref", "--format=%(refname)", prefix], `list ${prefix} refs`).split("\n").filter(Boolean).sort();
361
+ }
362
+ function desiredWorkspaceBasisRefs(receipt) {
363
+ const desired = /* @__PURE__ */ new Map();
364
+ for (const mount of receipt.mounts) {
365
+ const ref = workspaceBasisRefName(mount.id);
366
+ if (desired.has(ref)) throw new Error(`Workspace hydration mount ids collide at basis ref ${ref}`);
367
+ desired.set(ref, mount.head);
368
+ }
369
+ return desired;
370
+ }
371
+ function workspaceBasisRefsMatch(workspacePath, receipt) {
372
+ const desired = desiredWorkspaceBasisRefs(receipt);
373
+ const actualRefs = listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX);
374
+ if (actualRefs.length !== desired.size) return false;
375
+ return actualRefs.every((ref) => desired.get(ref) === revParse(workspacePath, ref));
376
+ }
377
+ function workspaceBasisRefsAreCurrent(workspacePath, receipt) {
378
+ return workspaceBasisRefsMatch(workspacePath, receipt) && listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX).length === 0;
379
+ }
380
+ function repairWorkspaceBasisRefs(workspacePath, receipt) {
381
+ const effectiveReceipt = receipt ?? { version: 3, mounts: [] };
382
+ if (!workspaceBasisRefsAreCurrent(workspacePath, effectiveReceipt)) updateWorkspaceBasisRefs(workspacePath, effectiveReceipt);
383
+ }
384
+ function updateWorkspaceBasisRefs(workspacePath, receipt) {
385
+ const desired = desiredWorkspaceBasisRefs(receipt);
386
+ const transaction = ["start"];
387
+ for (const [ref, head] of [...desired].sort(([left], [right]) => left.localeCompare(right))) transaction.push(`update ${ref} ${head}`);
388
+ for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_REF_PREFIX)) {
389
+ if (!desired.has(ref)) transaction.push(`delete ${ref}`);
390
+ }
391
+ transaction.push("prepare", "commit", "");
392
+ const updated = Bun.spawnSync(gitCommandArgs(["update-ref", "--stdin"]), {
393
+ cwd: workspacePath,
394
+ stdin: Buffer.from(transaction.join("\n")),
395
+ stdout: "pipe",
396
+ stderr: "pipe",
397
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
398
+ });
399
+ if (updated.exitCode !== 0) {
400
+ throw new Error(`Synchronize workspace hydration basis refs: ${updated.stderr.toString().trim() || `git exited ${updated.exitCode}`}`);
401
+ }
402
+ for (const ref of listWorkspaceRefs(workspacePath, WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX)) {
403
+ git(workspacePath, ["update-ref", "-d", ref], "remove staged workspace hydration basis ref");
404
+ }
244
405
  }
245
406
  function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
246
- if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(receipt.head)) {
247
- throw new Error(`Cannot record invalid fully hydrated workspace object id: ${receipt.head}`);
248
- }
249
- if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
250
- throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${receipt.head}`);
407
+ for (const mount of receipt.mounts) {
408
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(mount.head)) {
409
+ throw new Error(`Cannot record invalid workspace hydration object id: ${mount.head}`);
410
+ }
411
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${mount.head}^{commit}`])) {
412
+ throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${mount.head}`);
413
+ }
251
414
  }
252
415
  readHydratedWorkspaceReceipt(workspacePath);
253
416
  const gitDirectory = import_node_path.default.join(workspacePath, ".git");
@@ -267,6 +430,12 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
267
430
  throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
268
431
  }
269
432
  const temporaryPath = import_node_path.default.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
433
+ const stagingPrefix = `${WORKSPACE_GIT_BASIS_STAGING_REF_PREFIX}${crypto.randomUUID()}/`;
434
+ desiredWorkspaceBasisRefs(receipt);
435
+ for (const mount of receipt.mounts) {
436
+ const stagedRef = `${stagingPrefix}${workspaceBasisRefName(mount.id).slice(WORKSPACE_GIT_BASIS_REF_PREFIX.length)}`;
437
+ git(workspacePath, ["update-ref", stagedRef, mount.head], "stage workspace hydration basis ref");
438
+ }
270
439
  let descriptor;
271
440
  try {
272
441
  descriptor = import_node_fs.default.openSync(temporaryPath, import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL, 384);
@@ -277,6 +446,7 @@ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
277
446
  descriptor = void 0;
278
447
  import_node_fs.default.renameSync(temporaryPath, receiptPath);
279
448
  fsyncDirectory(receiptDirectory);
449
+ updateWorkspaceBasisRefs(workspacePath, receipt);
280
450
  } catch (error) {
281
451
  if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
282
452
  import_node_fs.default.rmSync(temporaryPath, { force: true });
@@ -404,6 +574,16 @@ function workspaceRebaseInProgress(workspacePath) {
404
574
  const gitDirectory = import_node_path.default.join(workspacePath, ".git");
405
575
  return import_node_fs.default.existsSync(import_node_path.default.join(gitDirectory, "rebase-merge")) || import_node_fs.default.existsSync(import_node_path.default.join(gitDirectory, "rebase-apply"));
406
576
  }
577
+ function workspaceResolutionInProgress(workspacePath) {
578
+ return workspaceRebaseInProgress(workspacePath) || import_node_fs.default.existsSync(import_node_path.default.join(workspacePath, ".git", "MERGE_HEAD"));
579
+ }
580
+ function workspaceHasUnmergedEntries(workspacePath) {
581
+ const result = gitResult(workspacePath, ["ls-files", "-u", "-z"]);
582
+ if (result.exitCode !== 0) {
583
+ throw new Error(`Inspect unfinished workspace merge: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
584
+ }
585
+ return result.stdout.length > 0;
586
+ }
407
587
  function clearWorkspaceCheckoutTree(workspacePath) {
408
588
  for (const name of import_node_fs.default.readdirSync(workspacePath)) {
409
589
  if (name === ".git") continue;
@@ -411,15 +591,18 @@ function clearWorkspaceCheckoutTree(workspacePath) {
411
591
  }
412
592
  fsyncDirectory(workspacePath);
413
593
  }
414
- function recoverWorkspaceCheckoutDurability(workspacePath) {
594
+ function recoverWorkspaceCheckoutDurability(workspacePath, preserveResolutionInProgress = false) {
415
595
  workspacePath = import_node_path.default.resolve(workspacePath);
416
596
  const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
417
597
  const observedHead = revParse(workspacePath, "HEAD");
418
598
  if (record?.state === "durable" && record.head === observedHead) return;
599
+ if (preserveResolutionInProgress && workspaceResolutionInProgress(workspacePath)) {
600
+ throw new Error("Workspace resolution is in progress; finish or abort it before synchronizing");
601
+ }
419
602
  let targetHead = record?.state === "transition" ? record.head : observedHead;
420
603
  if (record?.state === "transition" && observedHead && observedHead !== record.head) {
421
604
  const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
422
- if (hydrationReceipt?.head === observedHead) {
605
+ if (hydrationReceipt?.legacyHead === observedHead || hydrationReceipt?.mounts.some((mount) => mount.head === observedHead)) {
423
606
  targetHead = observedHead;
424
607
  }
425
608
  }
@@ -501,7 +684,7 @@ function parseHydrationTransactionManifest(transactionPath) {
501
684
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
502
685
  }
503
686
  const candidate = parsed;
504
- if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
687
+ if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
505
688
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
506
689
  }
507
690
  const parseReceipt = (rawReceipt) => {
@@ -533,7 +716,7 @@ function parseHydrationTransactionManifest(transactionPath) {
533
716
  return mount;
534
717
  });
535
718
  return {
536
- version: 2,
719
+ version: 3,
537
720
  targetReceipt,
538
721
  receiptBefore,
539
722
  mounts
@@ -626,7 +809,7 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
626
809
  const snapshotRoot = import_node_path.default.join(stagingPath, "mounts");
627
810
  import_node_fs.default.mkdirSync(snapshotRoot, { mode: 448 });
628
811
  const manifest = {
629
- version: 2,
812
+ version: 3,
630
813
  targetReceipt,
631
814
  receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
632
815
  mounts: mounts.map((mount, index) => {
@@ -805,9 +988,22 @@ function configureWorkspaceRepository(input) {
805
988
  if (!name || !email) throw new Error("Workspace Git identity must include name and email");
806
989
  git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
807
990
  git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
808
- git(input.workspacePath, ["config", "--local", "core.fsync", "committed"], "configure durable workspace commits");
991
+ git(
992
+ input.workspacePath,
993
+ ["config", "--local", "core.fsync", "committed,reference"],
994
+ "configure durable workspace commits and references"
995
+ );
809
996
  git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
810
997
  }
998
+ function configureExistingWorkspaceGitForRemediation(input) {
999
+ const workspacePath = import_node_path.default.resolve(input.workspacePath);
1000
+ const workspaceStatus = lstatIfExists(workspacePath);
1001
+ const gitStatus = lstatIfExists(import_node_path.default.join(workspacePath, ".git"));
1002
+ if (!workspaceStatus?.isDirectory() || workspaceStatus.isSymbolicLink() || !gitStatus?.isDirectory() || gitStatus.isSymbolicLink()) {
1003
+ throw new Error("Active workspace remediation requires a regular existing canonical synchronization checkout");
1004
+ }
1005
+ configureWorkspaceRepository({ ...input, workspacePath });
1006
+ }
811
1007
  function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
812
1008
  const result = gitResult(workspacePath, [
813
1009
  ...(0, import_git_process_environment.gitTransportSecurityArgs)(remoteUrl, credentialHelper, credentialUsername),
@@ -869,7 +1065,7 @@ function ensureWorkspaceGitClone(input) {
869
1065
  }
870
1066
  }
871
1067
  configureWorkspaceRepository({ ...input, workspacePath });
872
- recoverWorkspaceCheckoutDurability(workspacePath);
1068
+ recoverWorkspaceCheckoutDurability(workspacePath, input.preserveResolutionInProgress);
873
1069
  const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
874
1070
  const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
875
1071
  if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
@@ -1019,31 +1215,40 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1019
1215
  }
1020
1216
  function hydrateWorkspaceGitMountsTransactionally(input) {
1021
1217
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1022
- const mountIds = input.mounts.map(({ id }) => id).sort();
1023
- const requiredIds = (input.requiredMounts ?? input.mounts).map(({ id }) => id).sort();
1024
- if (mountIds.length !== requiredIds.length || mountIds.some((id, index) => id !== requiredIds[index])) {
1025
- return { hydratedMountIds: [], skippedMountIds: requiredIds };
1026
- }
1027
- const busyMountIds = input.ignoreBusy ? [] : (input.durabilityMounts ?? input.mounts).filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
1028
- if (busyMountIds.length > 0) {
1029
- return { hydratedMountIds: [], skippedMountIds: busyMountIds };
1030
- }
1218
+ const durabilityMounts = input.durabilityMounts ?? input.mounts;
1219
+ const receiptMounts = input.receiptMounts ?? durabilityMounts;
1220
+ const requiredMounts = input.requiredMounts ?? input.mounts;
1221
+ const candidateMounts = [
1222
+ ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
1223
+ ];
1224
+ const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1225
+ const requestedIds = new Set(input.mounts.map(({ id }) => id));
1226
+ const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1227
+ const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1228
+ const advancedDurabilityMounts = durabilityMounts.filter(
1229
+ ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1230
+ );
1231
+ const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1031
1232
  if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
1032
1233
  throw new Error("Workspace checkout is not durably materialized at its current HEAD");
1033
1234
  }
1034
1235
  const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
1035
- const targetReceipt = targetHead ? workspaceHydrationReceipt(targetHead, input.receiptMounts ?? input.durabilityMounts ?? input.mounts) : null;
1036
- const manifest = beginHydrationTransaction(workspacePath, input.mounts, targetReceipt);
1236
+ const targetReceipt = targetHead ? receiptWithMountsAtHead(readHydratedWorkspaceReceipt(workspacePath), targetHead, advancedDurabilityMounts, receiptMounts) : null;
1237
+ const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1037
1238
  try {
1038
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, input.mounts, { ignoreBusy: true });
1039
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredIds[index])) {
1239
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1240
+ const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1241
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1040
1242
  throw new Error("Workspace hydration did not include every required mount");
1041
1243
  }
1042
- fsyncHydratedWorkspaceMounts(input.durabilityMounts ?? input.mounts);
1244
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
1043
1245
  markHydrationTransactionDurable(workspacePath);
1044
1246
  if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1045
1247
  removeHydrationTransaction(workspacePath);
1046
- return hydration;
1248
+ return {
1249
+ hydratedMountIds: hydration.hydratedMountIds,
1250
+ skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort()
1251
+ };
1047
1252
  } catch (error) {
1048
1253
  const transactionPath = hydrationTransactionPath(workspacePath);
1049
1254
  if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
@@ -1076,44 +1281,44 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1076
1281
  function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1077
1282
  workspacePath = import_node_path.default.resolve(workspacePath);
1078
1283
  validateMounts(workspacePath, mounts);
1079
- recoverWorkspaceCheckoutDurability(workspacePath);
1284
+ recoverWorkspaceCheckoutDurability(workspacePath, options.preserveResolutionInProgress);
1080
1285
  recoverPendingHydrationTransaction(workspacePath);
1081
1286
  const currentHead = revParse(workspacePath, "HEAD");
1082
- if (!currentHead) return;
1083
1287
  const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
1288
+ repairWorkspaceBasisRefs(workspacePath, currentReceipt);
1289
+ if (!currentHead) return;
1084
1290
  const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
1085
1291
  const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
1086
1292
  const uncoveredBasisMounts = configuredBasisMounts.filter(
1087
1293
  (mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
1088
1294
  );
1089
1295
  if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
1090
- const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1091
- if (busyMountIds.length > 0) {
1092
- throw new Error(
1093
- `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${busyMountIds.join(", ")}`
1094
- );
1095
- }
1096
- const hydrationTargets = uncoveredBasisMounts.filter(
1097
- (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1296
+ const busyMountIds = new Set(options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1297
+ const recoveryMounts = uncoveredBasisMounts.filter(
1298
+ (mount) => workspaceHydrationReceiptMountBasisHead(currentReceipt, mount) === null || !options.preserveStaleBases && !busyMountIds.has(mount.id)
1299
+ );
1300
+ const idleBasislessMounts = recoveryMounts.filter((mount) => !busyMountIds.has(mount.id));
1301
+ const hydrationTargets = idleBasislessMounts.filter(
1302
+ (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.mounts.find(({ id }) => id === mount.id)?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1098
1303
  );
1099
1304
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1100
1305
  workspacePath,
1101
1306
  mounts: hydrationTargets,
1102
- durabilityMounts: uncoveredBasisMounts,
1307
+ durabilityMounts: idleBasislessMounts,
1103
1308
  receiptMounts: configuredBasisMounts,
1104
1309
  requiredMounts: hydrationTargets,
1105
1310
  ignoreBusy: options.ignoreBusy,
1106
1311
  recordCurrentHead: true
1107
1312
  });
1108
- if (hydration.skippedMountIds.length > 0) {
1109
- throw new Error(
1110
- `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
1111
- );
1112
- }
1313
+ void hydration;
1113
1314
  }
1114
1315
  function resetWorkspaceGit(input) {
1115
1316
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1116
1317
  validateMounts(workspacePath, input.mounts);
1318
+ const busyResetMountIds = input.mounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1319
+ if (busyResetMountIds.length > 0) {
1320
+ throw new Error(`Workspace reset cannot run while mounts are busy: ${busyResetMountIds.sort().join(", ")}`);
1321
+ }
1117
1322
  const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1118
1323
  recoverPendingHydrationTransaction(workspacePath);
1119
1324
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
@@ -1300,69 +1505,137 @@ async function synchronizeWorkspaceGit(input) {
1300
1505
  const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
1301
1506
  const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
1302
1507
  validateMounts(workspacePath, input.mounts);
1303
- const busyLiveMountIds = input.skipMountMirror ? [] : input.mounts.filter((mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()).map(({ id }) => id).sort();
1304
- if (busyLiveMountIds.length > 0) {
1305
- const cloneExists = import_node_fs.default.existsSync(import_node_path.default.join(workspacePath, ".git"));
1306
- const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
1307
- const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
1308
- return {
1309
- outcome: "no_change",
1310
- startingHead: localHead,
1311
- localHead,
1312
- remoteHead: remoteHead2,
1313
- publishedHead: remoteHead2,
1314
- rebaseCount: 0,
1315
- diffSizeBytes: 0,
1316
- affectedPaths: [],
1317
- activeMountIds: [],
1318
- skippedMountIds: input.mounts.map(({ id }) => id).sort()
1319
- };
1320
- }
1321
- const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1508
+ const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
1509
+ const preserveResolutionInProgress = input.skipMountMirror === true;
1510
+ const initial = ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
1511
+ assertWorkspaceRemediationAncestry({
1512
+ workspacePath,
1513
+ currentHead: initial.localHead,
1514
+ remoteHead: initial.remoteHead,
1515
+ requiredAncestorHeads
1516
+ });
1517
+ const verifiedAncestorHeads = requiredAncestorHeads.length > 0 ? requiredAncestorHeads : void 0;
1322
1518
  const startingHead = initial.localHead;
1323
1519
  const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
1324
1520
  const deferredMountIds = /* @__PURE__ */ new Set();
1325
- if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead && receiptBeforeInitialHydration?.head === initial.localHead) {
1521
+ if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead) {
1326
1522
  for (const mount of input.mounts) {
1327
- if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && !workspaceHydrationReceiptCoversMount(receiptBeforeInitialHydration, initial.localHead, mount)) {
1523
+ if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && workspaceHydrationReceiptMountBasisHead(receiptBeforeInitialHydration, mount) === null) {
1328
1524
  deferredMountIds.add(mount.id);
1329
1525
  }
1330
1526
  }
1331
1527
  }
1332
1528
  recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1333
- ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
1529
+ ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
1530
+ preserveResolutionInProgress,
1531
+ preserveStaleBases: true
1334
1532
  });
1335
- const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => import_node_fs.default.existsSync(sourcePath));
1336
1533
  const selected = activeMounts(input.mounts);
1337
- const projectedActiveMounts = selected.active.filter(({ id }) => !deferredMountIds.has(id));
1338
- const projectedTombstones = selected.tombstones.filter(({ id }) => !deferredMountIds.has(id));
1534
+ const projectionMutationTokens = new Map(
1535
+ input.mounts.flatMap((mount) => mount.mutationToken ? [[mount.id, mount.mutationToken()]] : [])
1536
+ );
1537
+ const cycleSkippedMountIds = new Set(selected.skipped.map(({ id }) => id));
1538
+ const receiptRequiredLiveMounts = selected.active;
1339
1539
  const projectionBasisHead = revParse(workspacePath, "HEAD");
1340
1540
  const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
1341
- const projectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts).filter(
1342
- (mount) => !deferredMountIds.has(mount.id) && Boolean(projectionBasisHead && workspaceHydrationReceiptCoversMount(projectionBasisReceipt, projectionBasisHead, mount))
1343
- );
1541
+ const allProjectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1542
+ const fastProjectionMounts = [];
1543
+ const mergedProjectionTrees = /* @__PURE__ */ new Map();
1544
+ const mergedProjectionMounts = [];
1545
+ const projectionSkippedIds = /* @__PURE__ */ new Set();
1546
+ let projectionWarning;
1547
+ const refreshCycleSkippedMounts = (mounts = input.mounts) => {
1548
+ for (const mount of mounts) {
1549
+ const projectionToken = projectionMutationTokens.get(mount.id);
1550
+ if (projectionToken !== void 0 && mount.mutationToken?.() !== projectionToken) cycleSkippedMountIds.add(mount.id);
1551
+ if (mount.busy?.()) cycleSkippedMountIds.add(mount.id);
1552
+ }
1553
+ };
1554
+ const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
1555
+ refreshCycleSkippedMounts();
1556
+ const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
1557
+ return {
1558
+ activeMountIds: [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort(),
1559
+ skippedMountIds: [...skippedIds].sort()
1560
+ };
1561
+ };
1562
+ if (!input.skipMountMirror) {
1563
+ const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
1564
+ const mergeSupport = mergeProjectionEnabled ? (0, import_workspace_merge_projection.workspaceMergeProjectionSupport)() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
1565
+ for (const mount of [...selected.active, ...selected.tombstones]) {
1566
+ if (deferredMountIds.has(mount.id)) continue;
1567
+ const basisHead = workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount);
1568
+ if (projectionBasisHead === null || basisHead === projectionBasisHead) {
1569
+ fastProjectionMounts.push(mount);
1570
+ continue;
1571
+ }
1572
+ if (basisHead === null) {
1573
+ deferredMountIds.add(mount.id);
1574
+ continue;
1575
+ }
1576
+ if (!mergeSupport.supported) {
1577
+ projectionSkippedIds.add(mount.id);
1578
+ projectionWarning = mergeSupport.error ?? "Git 2.40 or newer is required for stale workspace mount projection";
1579
+ continue;
1580
+ }
1581
+ const merged = (0, import_workspace_merge_projection.mergeWorkspaceProjectionMount)({
1582
+ workspacePath,
1583
+ mount: {
1584
+ id: mount.id,
1585
+ sourcePath: mount.sourcePath,
1586
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
1587
+ sourceMode: mount.sourceMode
1588
+ },
1589
+ basisHead,
1590
+ currentHead: projectionBasisHead,
1591
+ attemptId
1592
+ });
1593
+ if (merged.kind === "conflict") {
1594
+ const refs = snapshotConflict({ workspacePath, attemptId, localHead: merged.oursCommit, remoteHead: projectionBasisHead });
1595
+ return {
1596
+ outcome: "conflict_blocked",
1597
+ startingHead,
1598
+ localHead: merged.oursCommit,
1599
+ remoteHead: initial.remoteHead,
1600
+ publishedHead: initial.remoteHead,
1601
+ rebaseCount: 0,
1602
+ diffSizeBytes: 0,
1603
+ affectedPaths: merged.conflictPaths,
1604
+ activeMountIds: [],
1605
+ skippedMountIds: input.mounts.map(({ id }) => id).sort(),
1606
+ conflictPaths: merged.conflictPaths,
1607
+ conflictSnapshotRefs: refs,
1608
+ conflictKind: "projection_merge",
1609
+ ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
1610
+ error: merged.error
1611
+ };
1612
+ }
1613
+ mergedProjectionTrees.set(mount.id, merged.resultTree);
1614
+ mergedProjectionMounts.push(mount);
1615
+ }
1616
+ }
1617
+ if (projectionWarning) process.stderr.write(`[r5d-worker] ${projectionWarning}
1618
+ `);
1344
1619
  const completedMountSelection = (requireCurrentHeadReceipt = false) => {
1345
1620
  const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
1346
1621
  const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1347
- if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
1348
- readHydratedWorkspaceReceipt(workspacePath),
1349
- currentHeadBeforeHydration,
1350
- completionReceiptMounts
1351
- )) {
1352
- const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
1353
- const lateBusy = new Set(lateBusyIds);
1354
- return {
1355
- activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
1356
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
1357
- };
1358
- }
1359
1622
  const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
1623
+ refreshCycleSkippedMounts(completionReceiptMounts);
1360
1624
  const mountIsAlreadyCovered = (mount) => Boolean(
1361
1625
  currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
1362
1626
  );
1363
- const uncoveredCompletionMounts = selected.active.filter((mount) => !mountIsAlreadyCovered(mount));
1364
- const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1365
- const uncoveredCompletionBasisMounts = completionReceiptMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1627
+ if (currentHeadBeforeHydration && completionReceiptMounts.every((mount) => cycleSkippedMountIds.has(mount.id) || mountIsAlreadyCovered(mount))) {
1628
+ return classifyMounts([...selected.active, ...selected.tombstones]);
1629
+ }
1630
+ const uncoveredCompletionMounts = selected.active.filter(
1631
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1632
+ );
1633
+ const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter(
1634
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1635
+ );
1636
+ const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
1637
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1638
+ );
1366
1639
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1367
1640
  workspacePath,
1368
1641
  mounts: uncoveredCompletionMounts,
@@ -1371,22 +1644,32 @@ async function synchronizeWorkspaceGit(input) {
1371
1644
  requiredMounts: uncoveredRequiredLiveMounts,
1372
1645
  recordCurrentHead: true
1373
1646
  });
1647
+ for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
1648
+ refreshCycleSkippedMounts(completionReceiptMounts);
1374
1649
  if (requireCurrentHeadReceipt) {
1375
1650
  const currentHead = revParse(workspacePath, "HEAD");
1376
- if (currentHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), currentHead, completionReceiptMounts)) {
1651
+ const receipt = readHydratedWorkspaceReceipt(workspacePath);
1652
+ const transactionSkippedMountIds = new Set(hydration.skippedMountIds);
1653
+ const idleMounts = completionReceiptMounts.filter(
1654
+ (mount) => !cycleSkippedMountIds.has(mount.id) && !transactionSkippedMountIds.has(mount.id) && !mount.busy?.() && !projectionSkippedIds.has(mount.id)
1655
+ );
1656
+ if (currentHead && !idleMounts.every((mount) => workspaceHydrationReceiptCoversMount(receipt, currentHead, mount))) {
1377
1657
  throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
1378
1658
  }
1379
1659
  }
1380
- return {
1381
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1382
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1383
- };
1660
+ return classifyMounts([...selected.active, ...selected.tombstones], hydration.skippedMountIds);
1384
1661
  };
1385
1662
  let newlyStagedPaths = [];
1386
1663
  const stageAndCommitWorkspace = () => {
1664
+ if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1665
+ throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1666
+ }
1387
1667
  git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
1388
1668
  newlyStagedPaths = stagedPaths(workspacePath);
1389
1669
  if (newlyStagedPaths.length > 0) {
1670
+ if (workspaceResolutionInProgress(workspacePath) || workspaceHasUnmergedEntries(workspacePath)) {
1671
+ throw new Error("Outer workspace clone has an unfinished merge/rebase; finish or abort it before synchronizing");
1672
+ }
1390
1673
  git(
1391
1674
  workspacePath,
1392
1675
  [
@@ -1401,11 +1684,22 @@ async function synchronizeWorkspaceGit(input) {
1401
1684
  if (!input.skipMountMirror) {
1402
1685
  beginWorkspaceCheckoutTransition(workspacePath);
1403
1686
  try {
1404
- mirrorMountsToWorkspace(workspacePath, projectedActiveMounts);
1405
- removeWorkspaceMounts(workspacePath, projectedTombstones);
1687
+ const selectedActiveIds = new Set(selected.active.map(({ id }) => id));
1688
+ const fastActiveMounts = fastProjectionMounts.filter(({ id }) => selectedActiveIds.has(id));
1689
+ const fastTombstones = fastProjectionMounts.filter(({ id }) => !selectedActiveIds.has(id));
1690
+ mirrorMountsToWorkspace(workspacePath, fastActiveMounts);
1691
+ removeWorkspaceMounts(workspacePath, fastTombstones);
1692
+ for (const mount of mergedProjectionMounts) {
1693
+ (0, import_workspace_merge_projection.materializeWorkspaceProjectionTree)({
1694
+ workspacePath,
1695
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
1696
+ resultTree: mergedProjectionTrees.get(mount.id)
1697
+ });
1698
+ }
1699
+ const restoreFromHeadIds = /* @__PURE__ */ new Set([...deferredMountIds, ...selected.skipped.map(({ id }) => id)]);
1406
1700
  restoreDeferredWorkspaceMountsFromHead(
1407
1701
  workspacePath,
1408
- input.mounts.filter(({ id }) => deferredMountIds.has(id))
1702
+ input.mounts.filter(({ id }) => restoreFromHeadIds.has(id))
1409
1703
  );
1410
1704
  stageAndCommitWorkspace();
1411
1705
  fsyncWorkspaceCheckoutTree(workspacePath);
@@ -1419,9 +1713,15 @@ async function synchronizeWorkspaceGit(input) {
1419
1713
  if (!input.skipMountMirror) {
1420
1714
  const projectedHead = revParse(workspacePath, "HEAD");
1421
1715
  if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
1422
- const targetProjectionReceipt = workspaceHydrationReceipt(projectedHead, projectionReceiptMounts);
1423
- if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), targetProjectionReceipt)) {
1424
- fsyncHydratedWorkspaceMounts([...projectedActiveMounts, ...projectedTombstones]);
1716
+ const targetProjectionReceipt = receiptWithMountsAtHead(
1717
+ projectionBasisReceipt,
1718
+ projectedHead,
1719
+ fastProjectionMounts,
1720
+ allProjectionReceiptMounts
1721
+ );
1722
+ const currentProjectionReceipt = readHydratedWorkspaceReceipt(workspacePath);
1723
+ if (!workspaceHydrationReceiptsEqual(currentProjectionReceipt, targetProjectionReceipt) || !hydratedWorkspaceReceiptFileIsV3(workspacePath) || !workspaceBasisRefsAreCurrent(workspacePath, targetProjectionReceipt)) {
1724
+ fsyncHydratedWorkspaceMounts(fastProjectionMounts);
1425
1725
  updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
1426
1726
  }
1427
1727
  completeWorkspaceCheckoutTransition(workspacePath);
@@ -1430,9 +1730,16 @@ async function synchronizeWorkspaceGit(input) {
1430
1730
  let rebaseCount = 0;
1431
1731
  let updated = false;
1432
1732
  for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
1733
+ assertWorkspaceRemediationAncestry({
1734
+ workspacePath,
1735
+ currentHead: revParse(workspacePath, "HEAD"),
1736
+ remoteHead,
1737
+ requiredAncestorHeads
1738
+ });
1433
1739
  const reconciled = synchronizeWithFetchedHead({ workspacePath, remoteHead, attemptId });
1434
1740
  if (reconciled.kind === "conflict") {
1435
1741
  const localHead2 = revParse(workspacePath, "HEAD");
1742
+ const classifiedMounts = classifyMounts([...fastProjectionMounts, ...mergedProjectionMounts], deferredMountIds);
1436
1743
  return {
1437
1744
  outcome: "conflict_blocked",
1438
1745
  startingHead,
@@ -1442,10 +1749,11 @@ async function synchronizeWorkspaceGit(input) {
1442
1749
  rebaseCount: rebaseCount + 1,
1443
1750
  diffSizeBytes: 0,
1444
1751
  affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
1445
- activeMountIds: [...projectedActiveMounts, ...projectedTombstones].map(({ id }) => id).sort(),
1446
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
1752
+ ...classifiedMounts,
1447
1753
  conflictPaths: reconciled.conflictPaths,
1448
1754
  conflictSnapshotRefs: reconciled.refs,
1755
+ conflictKind: "integration_rebase",
1756
+ ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
1449
1757
  error: reconciled.error
1450
1758
  };
1451
1759
  }
@@ -1454,6 +1762,7 @@ async function synchronizeWorkspaceGit(input) {
1454
1762
  if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
1455
1763
  const localHead = revParse(workspacePath, "HEAD");
1456
1764
  if (!localHead) {
1765
+ const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones], deferredMountIds);
1457
1766
  return {
1458
1767
  outcome: "no_change",
1459
1768
  startingHead,
@@ -1463,13 +1772,16 @@ async function synchronizeWorkspaceGit(input) {
1463
1772
  rebaseCount,
1464
1773
  diffSizeBytes: 0,
1465
1774
  affectedPaths: [],
1466
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1467
- skippedMountIds: selected.skipped.map(({ id }) => id).sort()
1775
+ ...classifiedMounts,
1776
+ ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
1777
+ ...projectionWarning ? { error: projectionWarning } : {}
1468
1778
  };
1469
1779
  }
1470
1780
  const paths = changedPaths(workspacePath, remoteHead, localHead);
1471
- const size = paths.length > 0 ? await diffSizeBytes(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
1781
+ const size = paths.length > 0 ? await (input.measureDiffSize ?? diffSizeBytes)(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
1782
+ input.assertStillAdmitted?.();
1472
1783
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
1784
+ const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
1473
1785
  return {
1474
1786
  outcome: "large_diff_blocked",
1475
1787
  startingHead,
@@ -1479,17 +1791,19 @@ async function synchronizeWorkspaceGit(input) {
1479
1791
  rebaseCount,
1480
1792
  diffSizeBytes: size,
1481
1793
  affectedPaths: paths,
1482
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1483
- skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
1794
+ ...classifiedMounts,
1795
+ ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
1484
1796
  error: `Workspace diff exceeds the ${maxDiffBytes}-byte automatic publication limit`
1485
1797
  };
1486
1798
  }
1487
1799
  if (localHead === remoteHead) {
1800
+ assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
1488
1801
  const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
1489
1802
  await input.afterWorkspacePublished?.({
1490
1803
  publishedHead: localHead,
1491
1804
  activeMountIds: completedMounts.activeMountIds
1492
1805
  });
1806
+ input.assertStillAdmitted?.();
1493
1807
  return {
1494
1808
  outcome: updated ? "updated" : "no_change",
1495
1809
  startingHead,
@@ -1499,9 +1813,12 @@ async function synchronizeWorkspaceGit(input) {
1499
1813
  rebaseCount,
1500
1814
  diffSizeBytes: size,
1501
1815
  affectedPaths: paths,
1502
- ...completedMounts
1816
+ ...completedMounts,
1817
+ ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
1818
+ ...projectionWarning ? { error: projectionWarning } : {}
1503
1819
  };
1504
1820
  }
1821
+ assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
1505
1822
  const pushArgs = [
1506
1823
  ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.remoteUrl, input.credentialHelper, input.credentialUsername),
1507
1824
  "push",
@@ -1511,12 +1828,14 @@ async function synchronizeWorkspaceGit(input) {
1511
1828
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
1512
1829
  const push = gitResult(workspacePath, pushArgs);
1513
1830
  if (push.exitCode === 0) {
1831
+ assertWorkspaceRemediationAncestry({ workspacePath, currentHead: localHead, remoteHead, requiredAncestorHeads });
1514
1832
  updateIntegratedWorkspaceHead(workspacePath, localHead);
1515
1833
  const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
1516
1834
  await input.afterWorkspacePublished?.({
1517
1835
  publishedHead: localHead,
1518
1836
  activeMountIds: completedMounts.activeMountIds
1519
1837
  });
1838
+ input.assertStillAdmitted?.();
1520
1839
  return {
1521
1840
  outcome: "pushed",
1522
1841
  startingHead,
@@ -1526,7 +1845,9 @@ async function synchronizeWorkspaceGit(input) {
1526
1845
  rebaseCount,
1527
1846
  diffSizeBytes: size,
1528
1847
  affectedPaths: paths,
1529
- ...completedMounts
1848
+ ...completedMounts,
1849
+ ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
1850
+ ...projectionWarning ? { error: projectionWarning } : {}
1530
1851
  };
1531
1852
  }
1532
1853
  if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
@@ -1552,6 +1873,8 @@ const workspaceGitSyncTestHarness = {
1552
1873
  WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
1553
1874
  WORKSPACE_GIT_HYDRATED_RECEIPT,
1554
1875
  WORKSPACE_GIT_HYDRATION_TRANSACTION,
1876
+ WorkspaceRemediationAncestryError,
1877
+ configureExistingWorkspaceGitForRemediation,
1555
1878
  ensureWorkspaceGitClone,
1556
1879
  hydrateWorkspaceGitMounts,
1557
1880
  recoverWorkspaceGitHydration,