@ricsam/r5d-worker 0.0.78 → 0.0.80

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 (58) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/main.cjs +1550 -335
  3. package/dist/cjs/managed-paths.cjs +101 -1
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-workspace-state.cjs +184 -5
  6. package/dist/cjs/project-worktrees.cjs +676 -58
  7. package/dist/cjs/registry-auth.cjs +310 -0
  8. package/dist/cjs/repository-transition-policy.cjs +49 -0
  9. package/dist/cjs/supervisor.cjs +62 -11
  10. package/dist/cjs/working-tree-mirror.cjs +196 -36
  11. package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
  12. package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
  14. package/dist/cjs/workspace-git-sync.cjs +1037 -54
  15. package/dist/cjs/workspace-mount-boundary.cjs +71 -0
  16. package/dist/cjs/workspace-path-move.cjs +195 -0
  17. package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
  18. package/dist/cjs/workspace-project-config-policy.cjs +46 -0
  19. package/dist/cjs/workspace-publication-evidence.cjs +46 -0
  20. package/dist/mjs/main.mjs +1584 -341
  21. package/dist/mjs/managed-paths.mjs +100 -1
  22. package/dist/mjs/package.json +1 -1
  23. package/dist/mjs/project-workspace-state.mjs +181 -5
  24. package/dist/mjs/project-worktrees.mjs +667 -57
  25. package/dist/mjs/registry-auth.mjs +269 -0
  26. package/dist/mjs/repository-transition-policy.mjs +22 -0
  27. package/dist/mjs/supervisor.mjs +61 -11
  28. package/dist/mjs/working-tree-mirror.mjs +195 -36
  29. package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
  30. package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
  31. package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
  32. package/dist/mjs/workspace-git-sync.mjs +1032 -54
  33. package/dist/mjs/workspace-mount-boundary.mjs +37 -0
  34. package/dist/mjs/workspace-path-move.mjs +160 -0
  35. package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
  36. package/dist/mjs/workspace-project-config-policy.mjs +21 -0
  37. package/dist/mjs/workspace-publication-evidence.mjs +21 -0
  38. package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
  39. package/dist/types/main.d.ts +175 -1
  40. package/dist/types/managed-paths.d.ts +11 -0
  41. package/dist/types/project-workspace-state.d.ts +45 -1
  42. package/dist/types/project-worktrees.d.ts +119 -2
  43. package/dist/types/registry-auth.d.ts +47 -0
  44. package/dist/types/repository-transition-policy.d.ts +26 -0
  45. package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
  46. package/dist/types/supervisor-signal-fixture.d.ts +1 -0
  47. package/dist/types/supervisor.d.ts +6 -4
  48. package/dist/types/working-tree-mirror.d.ts +14 -0
  49. package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
  50. package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
  51. package/dist/types/workspace-command-sync-policy.d.ts +9 -0
  52. package/dist/types/workspace-git-sync.d.ts +33 -3
  53. package/dist/types/workspace-mount-boundary.d.ts +10 -0
  54. package/dist/types/workspace-path-move.d.ts +21 -0
  55. package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
  56. package/dist/types/workspace-project-config-policy.d.ts +34 -0
  57. package/dist/types/workspace-publication-evidence.d.ts +17 -0
  58. package/package.json +1 -1
@@ -2,10 +2,14 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
4
4
  import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
5
+ import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
5
6
  const WORKSPACE_GIT_BRANCH = "main";
6
7
  const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
7
8
  const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
8
9
  const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
10
+ const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
11
+ const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
12
+ const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
9
13
  const NON_RECURSIVE_GIT_CONFIG = [
10
14
  "-c",
11
15
  "submodule.recurse=false",
@@ -48,6 +52,640 @@ function revParse(workspacePath, revision) {
48
52
  function updateIntegratedWorkspaceHead(workspacePath, head) {
49
53
  git(workspacePath, ["update-ref", WORKSPACE_GIT_INTEGRATED_REF, head], "record integrated workspace head");
50
54
  }
55
+ function fsyncDirectory(directoryPath) {
56
+ const descriptor = fs.openSync(directoryPath, fs.constants.O_RDONLY);
57
+ try {
58
+ fs.fsyncSync(descriptor);
59
+ } finally {
60
+ fs.closeSync(descriptor);
61
+ }
62
+ }
63
+ function lstatIfExists(targetPath) {
64
+ try {
65
+ return fs.lstatSync(targetPath);
66
+ } catch (error) {
67
+ if (error.code === "ENOENT") return null;
68
+ throw error;
69
+ }
70
+ }
71
+ function hydratedWorkspaceReceiptPath(workspacePath) {
72
+ return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATED_RECEIPT.split("/"));
73
+ }
74
+ function workspaceCheckoutDurabilityPath(workspacePath) {
75
+ return path.join(workspacePath, ".git", ...WORKSPACE_GIT_CHECKOUT_DURABILITY.split("/"));
76
+ }
77
+ function normalizedWorkspaceMountPath(workspaceRelativePath) {
78
+ return workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
79
+ }
80
+ function workspaceHydrationMountBasis(mount) {
81
+ return {
82
+ id: mount.id,
83
+ incarnationKey: mount.hydrationIncarnationKey,
84
+ sourcePath: path.resolve(mount.sourcePath),
85
+ durabilityRootPath: path.resolve(mount.durabilityRootPath),
86
+ workspaceRelativePath: normalizedWorkspaceMountPath(mount.workspaceRelativePath),
87
+ sourceMode: mount.sourceMode,
88
+ hydrateDeletionMode: mount.hydrateDeletionMode,
89
+ preserveLocalOnInitialOuterAbsence: mount.preserveLocalOnInitialOuterAbsence === true,
90
+ preserveLocalOnHydrationBasisChange: mount.preserveLocalOnHydrationBasisChange === true,
91
+ deleteWhenSourceMissing: mount.deleteWhenSourceMissing === true
92
+ };
93
+ }
94
+ function compareWorkspaceHydrationMountBasis(left, right) {
95
+ return left.id.localeCompare(right.id);
96
+ }
97
+ function workspaceHydrationReceipt(head, mounts) {
98
+ const bases = mounts.map(workspaceHydrationMountBasis).sort(compareWorkspaceHydrationMountBasis);
99
+ const ids = /* @__PURE__ */ new Set();
100
+ for (const basis of bases) {
101
+ if (ids.has(basis.id)) throw new Error(`Workspace hydration receipt mount id must be unique: ${basis.id}`);
102
+ ids.add(basis.id);
103
+ }
104
+ return { version: 2, head, mounts: bases };
105
+ }
106
+ function workspaceHydrationReceiptsEqual(left, right) {
107
+ return JSON.stringify(left) === JSON.stringify(right);
108
+ }
109
+ function workspaceHydrationReceiptCoversMount(receipt, head, mount) {
110
+ if (!receipt || receipt.head !== head) return false;
111
+ const expected = workspaceHydrationMountBasis(mount);
112
+ const actual = receipt.mounts.find(({ id }) => id === expected.id);
113
+ return Boolean(actual && JSON.stringify(actual) === JSON.stringify(expected));
114
+ }
115
+ function workspaceHydrationReceiptMatchesMounts(receipt, head, mounts) {
116
+ return workspaceHydrationReceiptsEqual(receipt, workspaceHydrationReceipt(head, mounts));
117
+ }
118
+ function configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, deferMountIds) {
119
+ const resolvedWorkspacePath = path.resolve(workspacePath);
120
+ return mounts.filter((mount) => {
121
+ if (deferMountIds?.has(mount.id)) return false;
122
+ if (mount.deleteWhenSourceMissing) return !fs.existsSync(mount.sourcePath);
123
+ const outerPath = path.join(resolvedWorkspacePath, ...normalizedWorkspaceMountPath(mount.workspaceRelativePath).split("/"));
124
+ return fs.existsSync(mount.sourcePath) || fs.existsSync(outerPath) || mount.sourceMode === "all";
125
+ });
126
+ }
127
+ function parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content) {
128
+ const legacyHead = content.endsWith("\n") ? content.slice(0, -1) : "";
129
+ if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(legacyHead) && content === `${legacyHead}
130
+ `) {
131
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${legacyHead}^{commit}`])) {
132
+ throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
133
+ }
134
+ return { version: 2, head: legacyHead, mounts: [] };
135
+ }
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(content);
139
+ } catch {
140
+ throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
141
+ }
142
+ if (!parsed || typeof parsed !== "object") {
143
+ throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
144
+ }
145
+ const candidate = parsed;
146
+ if (candidate.version !== 2 || typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head)) {
147
+ throw new Error(`Workspace hydration receipt contains an invalid object id or mount basis: ${receiptPath}`);
148
+ }
149
+ if (!Array.isArray(candidate.mounts)) {
150
+ throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
151
+ }
152
+ const seenIds = /* @__PURE__ */ new Set();
153
+ const mounts = candidate.mounts.map((rawMount) => {
154
+ if (!rawMount || typeof rawMount !== "object") {
155
+ throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
156
+ }
157
+ const mount = rawMount;
158
+ const normalizedRelativePath = typeof mount.workspaceRelativePath === "string" ? normalizedWorkspaceMountPath(mount.workspaceRelativePath) : "";
159
+ const durabilityRelative = typeof mount.sourcePath === "string" && typeof mount.durabilityRootPath === "string" ? path.relative(mount.durabilityRootPath, mount.sourcePath) : "..";
160
+ 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" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.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") {
161
+ throw new Error(`Workspace hydration receipt contains an invalid mount basis: ${receiptPath}`);
162
+ }
163
+ seenIds.add(mount.id);
164
+ return mount;
165
+ });
166
+ const receipt = {
167
+ version: 2,
168
+ head: candidate.head,
169
+ mounts: mounts.sort(compareWorkspaceHydrationMountBasis)
170
+ };
171
+ if (content !== `${JSON.stringify(receipt)}
172
+ `) {
173
+ throw new Error(`Workspace hydration receipt contains a noncanonical mount basis: ${receiptPath}`);
174
+ }
175
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
176
+ throw new Error(`Workspace hydration receipt does not resolve to a local commit: ${receiptPath}`);
177
+ }
178
+ return receipt;
179
+ }
180
+ function readHydratedWorkspaceReceipt(workspacePath) {
181
+ const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
182
+ const status = lstatIfExists(receiptPath);
183
+ if (!status) return null;
184
+ if (!status.isFile() || status.isSymbolicLink()) {
185
+ throw new Error(`Workspace hydration receipt is not a regular file: ${receiptPath}`);
186
+ }
187
+ const content = fs.readFileSync(receiptPath, "utf8");
188
+ return parseWorkspaceHydrationReceiptContent(workspacePath, receiptPath, content);
189
+ }
190
+ function workspaceGitHydrationIsCurrent(workspacePath, mounts) {
191
+ const resolvedWorkspacePath = path.resolve(workspacePath);
192
+ if (!fs.existsSync(path.join(resolvedWorkspacePath, ".git"))) return true;
193
+ if (mounts) validateMounts(resolvedWorkspacePath, mounts);
194
+ const head = revParse(resolvedWorkspacePath, "HEAD");
195
+ if (!workspaceCheckoutDurabilityIsCurrent(resolvedWorkspacePath)) return false;
196
+ if (head === null) return true;
197
+ const receipt = readHydratedWorkspaceReceipt(resolvedWorkspacePath);
198
+ return mounts ? workspaceHydrationReceiptMatchesMounts(receipt, head, configuredWorkspaceHydrationBasisMounts(resolvedWorkspacePath, mounts)) : receipt?.head === head;
199
+ }
200
+ function updateHydratedWorkspaceReceipt(workspacePath, receipt) {
201
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(receipt.head)) {
202
+ throw new Error(`Cannot record invalid fully hydrated workspace object id: ${receipt.head}`);
203
+ }
204
+ if (!tryGit(workspacePath, ["cat-file", "-e", `${receipt.head}^{commit}`])) {
205
+ throw new Error(`Cannot record workspace hydration receipt for a missing commit: ${receipt.head}`);
206
+ }
207
+ readHydratedWorkspaceReceipt(workspacePath);
208
+ const gitDirectory = path.join(workspacePath, ".git");
209
+ const gitDirectoryStatus = fs.lstatSync(gitDirectory);
210
+ if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
211
+ throw new Error(`Workspace Git directory is not a regular directory: ${gitDirectory}`);
212
+ }
213
+ const receiptPath = hydratedWorkspaceReceiptPath(workspacePath);
214
+ const receiptDirectory = path.dirname(receiptPath);
215
+ const existingReceiptDirectory = lstatIfExists(receiptDirectory);
216
+ if (!existingReceiptDirectory) {
217
+ fs.mkdirSync(receiptDirectory, { recursive: false, mode: 448 });
218
+ fsyncDirectory(gitDirectory);
219
+ }
220
+ const receiptDirectoryStatus = fs.lstatSync(receiptDirectory);
221
+ if (!receiptDirectoryStatus.isDirectory() || receiptDirectoryStatus.isSymbolicLink()) {
222
+ throw new Error(`Workspace hydration receipt directory is not a regular directory: ${receiptDirectory}`);
223
+ }
224
+ const temporaryPath = path.join(receiptDirectory, `.workspace-hydrated-head.${process.pid}.${crypto.randomUUID()}.tmp`);
225
+ let descriptor;
226
+ try {
227
+ descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
228
+ fs.writeFileSync(descriptor, `${JSON.stringify(receipt)}
229
+ `, "utf8");
230
+ fs.fsyncSync(descriptor);
231
+ fs.closeSync(descriptor);
232
+ descriptor = void 0;
233
+ fs.renameSync(temporaryPath, receiptPath);
234
+ fsyncDirectory(receiptDirectory);
235
+ } catch (error) {
236
+ if (descriptor !== void 0) fs.closeSync(descriptor);
237
+ fs.rmSync(temporaryPath, { force: true });
238
+ throw error;
239
+ }
240
+ }
241
+ function hydrationTransactionPath(workspacePath) {
242
+ return path.join(workspacePath, ".git", ...WORKSPACE_GIT_HYDRATION_TRANSACTION.split("/"));
243
+ }
244
+ function workspacePrivateStateDirectory(workspacePath) {
245
+ const gitDirectory = path.join(workspacePath, ".git");
246
+ const gitDirectoryStatus = fs.lstatSync(gitDirectory);
247
+ if (!gitDirectoryStatus.isDirectory() || gitDirectoryStatus.isSymbolicLink()) {
248
+ throw new Error(`Workspace Git directory is not a regular directory: ${gitDirectory}`);
249
+ }
250
+ const stateDirectory = path.dirname(hydrationTransactionPath(workspacePath));
251
+ const existing = lstatIfExists(stateDirectory);
252
+ if (!existing) {
253
+ fs.mkdirSync(stateDirectory, { recursive: false, mode: 448 });
254
+ fsyncDirectory(gitDirectory);
255
+ } else if (!existing.isDirectory() || existing.isSymbolicLink()) {
256
+ throw new Error(`Workspace Git private state is not a regular directory: ${stateDirectory}`);
257
+ }
258
+ return stateDirectory;
259
+ }
260
+ function writePrivateFileDurably(filePath, content) {
261
+ const descriptor = fs.openSync(
262
+ filePath,
263
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
264
+ 384
265
+ );
266
+ try {
267
+ fs.writeFileSync(descriptor, content, "utf8");
268
+ fs.fsyncSync(descriptor);
269
+ } finally {
270
+ fs.closeSync(descriptor);
271
+ }
272
+ }
273
+ function fsyncTree(rootPath) {
274
+ const syncEntry = (entryPath) => {
275
+ const status = fs.lstatSync(entryPath);
276
+ if (status.isSymbolicLink()) return;
277
+ if (status.isDirectory()) {
278
+ for (const name of fs.readdirSync(entryPath)) syncEntry(path.join(entryPath, name));
279
+ fsyncDirectory(entryPath);
280
+ return;
281
+ }
282
+ if (!status.isFile()) return;
283
+ const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
284
+ try {
285
+ fs.fsyncSync(descriptor);
286
+ } finally {
287
+ fs.closeSync(descriptor);
288
+ }
289
+ };
290
+ syncEntry(rootPath);
291
+ }
292
+ function parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, content) {
293
+ let parsed;
294
+ try {
295
+ parsed = JSON.parse(content);
296
+ } catch {
297
+ throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
298
+ }
299
+ if (!parsed || typeof parsed !== "object") {
300
+ throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
301
+ }
302
+ const candidate = parsed;
303
+ if (candidate.version !== 1 || candidate.state !== "durable" && candidate.state !== "transition" || candidate.head !== null && (typeof candidate.head !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(candidate.head))) {
304
+ throw new Error(`Workspace checkout durability record is invalid: ${recordPath}`);
305
+ }
306
+ const record = {
307
+ version: 1,
308
+ state: candidate.state,
309
+ head: candidate.head
310
+ };
311
+ if (content !== `${JSON.stringify(record)}
312
+ `) {
313
+ throw new Error(`Workspace checkout durability record is noncanonical: ${recordPath}`);
314
+ }
315
+ if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
316
+ throw new Error(`Workspace checkout durability record names a missing commit: ${recordPath}`);
317
+ }
318
+ return record;
319
+ }
320
+ function readWorkspaceCheckoutDurabilityRecord(workspacePath) {
321
+ const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
322
+ const status = lstatIfExists(recordPath);
323
+ if (!status) return null;
324
+ if (!status.isFile() || status.isSymbolicLink()) {
325
+ throw new Error(`Workspace checkout durability record is not a regular file: ${recordPath}`);
326
+ }
327
+ return parseWorkspaceCheckoutDurabilityRecord(workspacePath, recordPath, fs.readFileSync(recordPath, "utf8"));
328
+ }
329
+ function updateWorkspaceCheckoutDurabilityRecord(workspacePath, record) {
330
+ if (record.head && !tryGit(workspacePath, ["cat-file", "-e", `${record.head}^{commit}`])) {
331
+ throw new Error(`Cannot record workspace checkout durability for a missing commit: ${record.head}`);
332
+ }
333
+ readWorkspaceCheckoutDurabilityRecord(workspacePath);
334
+ const stateDirectory = workspacePrivateStateDirectory(workspacePath);
335
+ const recordPath = workspaceCheckoutDurabilityPath(workspacePath);
336
+ const temporaryPath = path.join(stateDirectory, `.workspace-checkout-durability.${process.pid}.${crypto.randomUUID()}.tmp`);
337
+ try {
338
+ writePrivateFileDurably(temporaryPath, `${JSON.stringify(record)}
339
+ `);
340
+ fs.renameSync(temporaryPath, recordPath);
341
+ fsyncDirectory(stateDirectory);
342
+ } catch (error) {
343
+ fs.rmSync(temporaryPath, { force: true });
344
+ throw error;
345
+ }
346
+ }
347
+ function fsyncWorkspaceCheckoutTree(workspacePath) {
348
+ const status = fs.lstatSync(workspacePath);
349
+ if (!status.isDirectory() || status.isSymbolicLink()) {
350
+ throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
351
+ }
352
+ for (const name of fs.readdirSync(workspacePath)) {
353
+ if (name === ".git") continue;
354
+ fsyncTree(path.join(workspacePath, name));
355
+ }
356
+ fsyncDirectory(workspacePath);
357
+ }
358
+ function workspaceRebaseInProgress(workspacePath) {
359
+ const gitDirectory = path.join(workspacePath, ".git");
360
+ return fs.existsSync(path.join(gitDirectory, "rebase-merge")) || fs.existsSync(path.join(gitDirectory, "rebase-apply"));
361
+ }
362
+ function clearWorkspaceCheckoutTree(workspacePath) {
363
+ for (const name of fs.readdirSync(workspacePath)) {
364
+ if (name === ".git") continue;
365
+ fs.rmSync(path.join(workspacePath, name), { recursive: true, force: true });
366
+ }
367
+ fsyncDirectory(workspacePath);
368
+ }
369
+ function recoverWorkspaceCheckoutDurability(workspacePath) {
370
+ workspacePath = path.resolve(workspacePath);
371
+ const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
372
+ const observedHead = revParse(workspacePath, "HEAD");
373
+ if (record?.state === "durable" && record.head === observedHead) return;
374
+ let targetHead = record?.state === "transition" ? record.head : observedHead;
375
+ if (record?.state === "transition" && observedHead && observedHead !== record.head) {
376
+ const hydrationReceipt = readHydratedWorkspaceReceipt(workspacePath);
377
+ if (hydrationReceipt?.head === observedHead) {
378
+ targetHead = observedHead;
379
+ }
380
+ }
381
+ if (workspaceRebaseInProgress(workspacePath)) {
382
+ const abort = gitResult(workspacePath, ["rebase", "--abort"]);
383
+ if (abort.exitCode !== 0) {
384
+ throw new Error(`Abort interrupted workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
385
+ }
386
+ if (record?.state !== "transition") targetHead = revParse(workspacePath, "HEAD");
387
+ }
388
+ clearWorkspaceCheckoutTree(workspacePath);
389
+ if (targetHead) {
390
+ git(workspacePath, ["reset", "--hard", targetHead], "reconstruct durable workspace checkout");
391
+ if (revParse(workspacePath, "HEAD") !== targetHead) {
392
+ throw new Error(`Workspace checkout reconstruction did not restore ${targetHead}`);
393
+ }
394
+ } else {
395
+ git(workspacePath, ["symbolic-ref", "HEAD", `refs/heads/${WORKSPACE_GIT_BRANCH}`], "restore unborn workspace HEAD");
396
+ git(workspacePath, ["update-ref", "-d", `refs/heads/${WORKSPACE_GIT_BRANCH}`], "remove interrupted workspace branch ref");
397
+ git(workspacePath, ["read-tree", "--empty"], "reconstruct unborn workspace checkout");
398
+ if (revParse(workspacePath, "HEAD") !== null) {
399
+ throw new Error("Workspace checkout reconstruction did not restore its unborn HEAD");
400
+ }
401
+ }
402
+ fsyncWorkspaceCheckoutTree(workspacePath);
403
+ updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "durable", head: targetHead });
404
+ }
405
+ function workspaceCheckoutDurabilityIsCurrent(workspacePath) {
406
+ const record = readWorkspaceCheckoutDurabilityRecord(workspacePath);
407
+ return Boolean(record?.state === "durable" && record.head === revParse(workspacePath, "HEAD"));
408
+ }
409
+ function beginWorkspaceCheckoutTransition(workspacePath) {
410
+ recoverWorkspaceCheckoutDurability(workspacePath);
411
+ const head = revParse(workspacePath, "HEAD");
412
+ updateWorkspaceCheckoutDurabilityRecord(workspacePath, { version: 1, state: "transition", head });
413
+ }
414
+ function completeWorkspaceCheckoutTransition(workspacePath) {
415
+ fsyncWorkspaceCheckoutTree(workspacePath);
416
+ updateWorkspaceCheckoutDurabilityRecord(workspacePath, {
417
+ version: 1,
418
+ state: "durable",
419
+ head: revParse(workspacePath, "HEAD")
420
+ });
421
+ }
422
+ function throwAfterWorkspaceCheckoutRecovery(workspacePath, error) {
423
+ try {
424
+ recoverWorkspaceCheckoutDurability(workspacePath);
425
+ } catch (recoveryError) {
426
+ throw new AggregateError(
427
+ [error, recoveryError],
428
+ `Workspace checkout mutation failed and its durable head could not be restored: ${error instanceof Error ? error.message : String(error)}`
429
+ );
430
+ }
431
+ throw error;
432
+ }
433
+ function runWorkspaceCheckoutTransition(workspacePath, mutate) {
434
+ beginWorkspaceCheckoutTransition(workspacePath);
435
+ try {
436
+ mutate();
437
+ completeWorkspaceCheckoutTransition(workspacePath);
438
+ } catch (error) {
439
+ throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
440
+ }
441
+ }
442
+ function parseHydrationTransactionManifest(transactionPath) {
443
+ const manifestPath = path.join(transactionPath, "manifest.json");
444
+ const workspacePath = path.resolve(transactionPath, "..", "..", "..");
445
+ const status = fs.lstatSync(manifestPath);
446
+ if (!status.isFile() || status.isSymbolicLink()) {
447
+ throw new Error(`Workspace hydration transaction manifest is not a regular file: ${manifestPath}`);
448
+ }
449
+ let parsed;
450
+ try {
451
+ parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
452
+ } catch (error) {
453
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`, { cause: error });
454
+ }
455
+ if (!parsed || typeof parsed !== "object") {
456
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
457
+ }
458
+ const candidate = parsed;
459
+ if (candidate.version !== 2 || !Array.isArray(candidate.mounts)) {
460
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
461
+ }
462
+ const parseReceipt = (rawReceipt) => {
463
+ if (rawReceipt === null) return null;
464
+ if (!rawReceipt || typeof rawReceipt !== "object") {
465
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
466
+ }
467
+ return parseWorkspaceHydrationReceiptContent(workspacePath, manifestPath, `${JSON.stringify(rawReceipt)}
468
+ `);
469
+ };
470
+ const targetReceipt = parseReceipt(candidate.targetReceipt);
471
+ const receiptBefore = parseReceipt(candidate.receiptBefore);
472
+ const seenIds = /* @__PURE__ */ new Set();
473
+ const seenSnapshots = /* @__PURE__ */ new Set();
474
+ const mounts = candidate.mounts.map((rawMount) => {
475
+ if (!rawMount || typeof rawMount !== "object") {
476
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
477
+ }
478
+ const mount = rawMount;
479
+ if (typeof mount.id !== "string" || !mount.id || typeof mount.incarnationKey !== "string" || !mount.incarnationKey || mount.incarnationKey.includes("\0") || typeof mount.sourcePath !== "string" || path.resolve(mount.sourcePath) !== mount.sourcePath || typeof mount.durabilityRootPath !== "string" || path.resolve(mount.durabilityRootPath) !== mount.durabilityRootPath || typeof mount.workspaceRelativePath !== "string" || typeof mount.existed !== "boolean" || typeof mount.snapshotName !== "string" || !/^[0-9]+$/.test(mount.snapshotName) || seenIds.has(mount.id) || seenSnapshots.has(mount.snapshotName)) {
480
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
481
+ }
482
+ const relative = path.relative(mount.durabilityRootPath, mount.sourcePath);
483
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
484
+ throw new Error(`Workspace hydration transaction source escapes its durability root: ${mount.sourcePath}`);
485
+ }
486
+ seenIds.add(mount.id);
487
+ seenSnapshots.add(mount.snapshotName);
488
+ return mount;
489
+ });
490
+ return {
491
+ version: 2,
492
+ targetReceipt,
493
+ receiptBefore,
494
+ mounts
495
+ };
496
+ }
497
+ function removeHydrationTransaction(workspacePath) {
498
+ const transactionPath = hydrationTransactionPath(workspacePath);
499
+ fs.rmSync(transactionPath, { recursive: true, force: true });
500
+ fsyncDirectory(path.dirname(transactionPath));
501
+ }
502
+ function hydrationTransactionCommitted(transactionPath) {
503
+ const markerPath = path.join(transactionPath, "committed");
504
+ const markerStatus = lstatIfExists(markerPath);
505
+ if (!markerStatus) return false;
506
+ if (!markerStatus.isFile() || markerStatus.isSymbolicLink() || fs.readFileSync(markerPath, "utf8") !== "committed\n") {
507
+ throw new Error(`Workspace hydration transaction commit marker is invalid: ${markerPath}`);
508
+ }
509
+ return true;
510
+ }
511
+ function restoreHydrationTransaction(workspacePath, manifest) {
512
+ const transactionPath = hydrationTransactionPath(workspacePath);
513
+ for (const mount of manifest.mounts) {
514
+ assertManagedDirectoryPath({
515
+ trustedRoot: mount.durabilityRootPath,
516
+ candidate: mount.sourcePath,
517
+ label: `Workspace hydration recovery mount ${mount.id}`
518
+ });
519
+ if (!mount.existed) {
520
+ fs.rmSync(mount.sourcePath, { recursive: true, force: true });
521
+ continue;
522
+ }
523
+ const snapshotPath = path.join(transactionPath, "mounts", mount.snapshotName);
524
+ const snapshotStatus = fs.lstatSync(snapshotPath);
525
+ if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
526
+ throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
527
+ }
528
+ mirrorWorkingTree({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
529
+ }
530
+ fsyncHydratedWorkspaceMounts(
531
+ manifest.mounts.map((mount) => ({
532
+ ...mount,
533
+ hydrationIncarnationKey: mount.incarnationKey,
534
+ sourceMode: "all",
535
+ hydrateDeletionMode: "all"
536
+ }))
537
+ );
538
+ }
539
+ function recoverPendingHydrationTransaction(workspacePath) {
540
+ const stateDirectory = path.dirname(hydrationTransactionPath(workspacePath));
541
+ const stateStatus = lstatIfExists(stateDirectory);
542
+ if (!stateStatus) return;
543
+ if (!stateStatus.isDirectory() || stateStatus.isSymbolicLink()) {
544
+ throw new Error(`Workspace Git private state is not a regular directory: ${stateDirectory}`);
545
+ }
546
+ for (const name of fs.readdirSync(stateDirectory)) {
547
+ if (!name.startsWith(".workspace-hydration-transaction.") || !name.endsWith(".tmp")) continue;
548
+ fs.rmSync(path.join(stateDirectory, name), { recursive: true, force: true });
549
+ fsyncDirectory(stateDirectory);
550
+ }
551
+ const transactionPath = hydrationTransactionPath(workspacePath);
552
+ const transactionStatus = lstatIfExists(transactionPath);
553
+ if (!transactionStatus) return;
554
+ if (!transactionStatus.isDirectory() || transactionStatus.isSymbolicLink()) {
555
+ throw new Error(`Workspace hydration transaction is not a regular directory: ${transactionPath}`);
556
+ }
557
+ const manifest = parseHydrationTransactionManifest(transactionPath);
558
+ if (hydrationTransactionCommitted(transactionPath)) {
559
+ const receipt2 = readHydratedWorkspaceReceipt(workspacePath);
560
+ if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(receipt2, manifest.targetReceipt)) {
561
+ updateHydratedWorkspaceReceipt(workspacePath, manifest.targetReceipt);
562
+ }
563
+ removeHydrationTransaction(workspacePath);
564
+ return;
565
+ }
566
+ const receipt = readHydratedWorkspaceReceipt(workspacePath);
567
+ if (manifest.targetReceipt && !workspaceHydrationReceiptsEqual(manifest.targetReceipt, manifest.receiptBefore) && workspaceHydrationReceiptsEqual(receipt, manifest.targetReceipt)) {
568
+ removeHydrationTransaction(workspacePath);
569
+ return;
570
+ }
571
+ restoreHydrationTransaction(workspacePath, manifest);
572
+ removeHydrationTransaction(workspacePath);
573
+ }
574
+ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
575
+ recoverPendingHydrationTransaction(workspacePath);
576
+ const stateDirectory = workspacePrivateStateDirectory(workspacePath);
577
+ const transactionPath = hydrationTransactionPath(workspacePath);
578
+ const stagingPath = path.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
579
+ fs.mkdirSync(stagingPath, { mode: 448 });
580
+ try {
581
+ const snapshotRoot = path.join(stagingPath, "mounts");
582
+ fs.mkdirSync(snapshotRoot, { mode: 448 });
583
+ const manifest = {
584
+ version: 2,
585
+ targetReceipt,
586
+ receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
587
+ mounts: mounts.map((mount, index) => {
588
+ const sourcePath = path.resolve(mount.sourcePath);
589
+ const durabilityRootPath = path.resolve(mount.durabilityRootPath);
590
+ assertManagedDirectoryPath({
591
+ trustedRoot: durabilityRootPath,
592
+ candidate: sourcePath,
593
+ label: `Workspace hydration snapshot mount ${mount.id}`
594
+ });
595
+ const sourceStatus = lstatIfExists(sourcePath);
596
+ if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
597
+ throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
598
+ }
599
+ const snapshotName = String(index);
600
+ const snapshotPath = path.join(snapshotRoot, snapshotName);
601
+ fs.mkdirSync(snapshotPath, { mode: 448 });
602
+ if (sourceStatus) {
603
+ mirrorWorkingTree({
604
+ sourceRoot: sourcePath,
605
+ targetRoot: snapshotPath,
606
+ sourceMode: "all",
607
+ deletionMode: "all",
608
+ // This tree is still private and the source authority is untouched.
609
+ // The single recursive barrier below makes the complete snapshot
610
+ // durable before its directory entry is published or hydration starts.
611
+ durability: "deferred_private_staging"
612
+ });
613
+ }
614
+ return {
615
+ id: mount.id,
616
+ incarnationKey: mount.hydrationIncarnationKey,
617
+ sourcePath,
618
+ durabilityRootPath,
619
+ workspaceRelativePath: mount.workspaceRelativePath,
620
+ existed: Boolean(sourceStatus),
621
+ snapshotName
622
+ };
623
+ })
624
+ };
625
+ writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
626
+ `);
627
+ fsyncTree(stagingPath);
628
+ fs.renameSync(stagingPath, transactionPath);
629
+ fsyncDirectory(stateDirectory);
630
+ return manifest;
631
+ } catch (error) {
632
+ fs.rmSync(stagingPath, { recursive: true, force: true });
633
+ fsyncDirectory(stateDirectory);
634
+ throw error;
635
+ }
636
+ }
637
+ function markHydrationTransactionDurable(workspacePath) {
638
+ const transactionPath = hydrationTransactionPath(workspacePath);
639
+ writePrivateFileDurably(path.join(transactionPath, "committed"), "committed\n");
640
+ fsyncDirectory(transactionPath);
641
+ }
642
+ function fsyncHydratedWorkspaceMount(mount) {
643
+ assertManagedDirectoryPath({
644
+ trustedRoot: mount.durabilityRootPath,
645
+ candidate: mount.sourcePath,
646
+ label: `Workspace mount ${mount.id} source`
647
+ });
648
+ const syncEntry = (entryPath, root) => {
649
+ const status = fs.lstatSync(entryPath);
650
+ if (status.isSymbolicLink()) return;
651
+ if (status.isDirectory()) {
652
+ for (const name of fs.readdirSync(entryPath)) {
653
+ if (root && name === ".git") continue;
654
+ syncEntry(path.join(entryPath, name), false);
655
+ }
656
+ fsyncDirectory(entryPath);
657
+ return;
658
+ }
659
+ if (!status.isFile()) return;
660
+ const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY);
661
+ try {
662
+ fs.fsyncSync(descriptor);
663
+ } finally {
664
+ fs.closeSync(descriptor);
665
+ }
666
+ };
667
+ if (fs.existsSync(mount.sourcePath)) syncEntry(mount.sourcePath, true);
668
+ const durabilityRoot = path.resolve(mount.durabilityRootPath);
669
+ let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
670
+ while (true) {
671
+ if (fs.existsSync(boundaryParent)) {
672
+ const boundaryStatus = fs.lstatSync(boundaryParent);
673
+ if (!boundaryStatus.isDirectory() || boundaryStatus.isSymbolicLink()) {
674
+ throw new Error(`Workspace mount durability boundary is not a regular directory: ${boundaryParent}`);
675
+ }
676
+ fsyncDirectory(boundaryParent);
677
+ }
678
+ if (boundaryParent === durabilityRoot) break;
679
+ const nextParent = path.dirname(boundaryParent);
680
+ if (nextParent === boundaryParent) {
681
+ throw new Error(`Workspace mount ${mount.sourcePath} does not descend from durability root ${durabilityRoot}`);
682
+ }
683
+ boundaryParent = nextParent;
684
+ }
685
+ }
686
+ function fsyncHydratedWorkspaceMounts(mounts) {
687
+ for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
688
+ }
51
689
  function workspaceIsShallow(workspacePath) {
52
690
  return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
53
691
  }
@@ -60,7 +698,22 @@ function validateMounts(workspacePath, mounts) {
60
698
  const paths = [];
61
699
  for (const mount of mounts) {
62
700
  if (!mount.id || ids.has(mount.id)) throw new Error(`Workspace mount id must be unique: ${mount.id}`);
701
+ if (!mount.hydrationIncarnationKey || mount.hydrationIncarnationKey.includes("\0")) {
702
+ throw new Error(`Workspace mount hydration incarnation is invalid: ${mount.id}`);
703
+ }
63
704
  ids.add(mount.id);
705
+ const sourcePath = path.resolve(mount.sourcePath);
706
+ const durabilityRoot = path.resolve(mount.durabilityRootPath);
707
+ const durabilityRelative = path.relative(durabilityRoot, sourcePath);
708
+ const durabilityRootStatus = fs.lstatSync(durabilityRoot);
709
+ if (!durabilityRootStatus.isDirectory() || durabilityRootStatus.isSymbolicLink() || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.isAbsolute(durabilityRelative)) {
710
+ throw new Error(`Workspace mount source escapes its durability root: ${mount.sourcePath}`);
711
+ }
712
+ assertManagedDirectoryPath({
713
+ trustedRoot: durabilityRoot,
714
+ candidate: sourcePath,
715
+ label: `Workspace mount ${mount.id} source`
716
+ });
64
717
  const normalized = mount.workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
65
718
  if (!normalized || normalized === "." || normalized.split("/").some((segment) => segment === ".." || segment === ".git")) {
66
719
  throw new Error(`Invalid workspace mount path: ${mount.workspaceRelativePath}`);
@@ -107,6 +760,8 @@ function configureWorkspaceRepository(input) {
107
760
  if (!name || !email) throw new Error("Workspace Git identity must include name and email");
108
761
  git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
109
762
  git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
763
+ git(input.workspacePath, ["config", "--local", "core.fsync", "committed"], "configure durable workspace commits");
764
+ git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
110
765
  }
111
766
  function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
112
767
  const result = gitResult(workspacePath, [
@@ -130,7 +785,24 @@ ${result.stdout}`;
130
785
  }
131
786
  function ensureWorkspaceGitClone(input) {
132
787
  const workspacePath = path.resolve(input.workspacePath);
788
+ const workspaceParent = path.dirname(workspacePath);
789
+ const parentStatus = fs.lstatSync(workspaceParent);
790
+ if (!parentStatus.isDirectory() || parentStatus.isSymbolicLink()) {
791
+ throw new Error(`Workspace clone parent is not a regular directory: ${workspaceParent}`);
792
+ }
793
+ const workspaceStatus = lstatIfExists(workspacePath);
794
+ if (workspaceStatus) {
795
+ if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink()) {
796
+ throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
797
+ }
798
+ }
133
799
  const gitPath = path.join(workspacePath, ".git");
800
+ const gitStatus = lstatIfExists(gitPath);
801
+ if (gitStatus) {
802
+ if (!gitStatus.isDirectory() || gitStatus.isSymbolicLink()) {
803
+ throw new Error(`Workspace clone Git metadata is not a regular directory: ${gitPath}`);
804
+ }
805
+ }
134
806
  if (!fs.existsSync(gitPath)) {
135
807
  fs.rmSync(workspacePath, { recursive: true, force: true });
136
808
  fs.mkdirSync(path.dirname(workspacePath), { recursive: true });
@@ -152,6 +824,7 @@ function ensureWorkspaceGitClone(input) {
152
824
  }
153
825
  }
154
826
  configureWorkspaceRepository({ ...input, workspacePath });
827
+ recoverWorkspaceCheckoutDurability(workspacePath);
155
828
  const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
156
829
  const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
157
830
  if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
@@ -160,17 +833,21 @@ function ensureWorkspaceGitClone(input) {
160
833
  const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
161
834
  let localHead = revParse(workspacePath, "HEAD");
162
835
  if (!localHead && remoteHead) {
163
- git(
164
- workspacePath,
165
- ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
166
- "check out workspace main"
167
- );
836
+ runWorkspaceCheckoutTransition(workspacePath, () => {
837
+ git(
838
+ workspacePath,
839
+ ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
840
+ "check out workspace main"
841
+ );
842
+ });
168
843
  localHead = remoteHead;
169
844
  }
170
845
  if (localHead && remoteHead && localHead === remoteHead) {
171
846
  updateIntegratedWorkspaceHead(workspacePath, remoteHead);
172
847
  } else if (localHead && remoteHead && workspaceIsShallow(workspacePath) && !tryGit(workspacePath, ["merge-base", localHead, remoteHead]) && revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) === localHead && workspaceIsClean(workspacePath)) {
173
- git(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
848
+ runWorkspaceCheckoutTransition(workspacePath, () => {
849
+ git(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
850
+ });
174
851
  updateIntegratedWorkspaceHead(workspacePath, remoteHead);
175
852
  localHead = remoteHead;
176
853
  }
@@ -190,6 +867,16 @@ function activeMounts(mounts) {
190
867
  }
191
868
  function mirrorMountsToWorkspace(workspacePath, mounts) {
192
869
  for (const mount of mounts) {
870
+ assertManagedDirectoryPath({
871
+ trustedRoot: mount.durabilityRootPath,
872
+ candidate: mount.sourcePath,
873
+ label: `Workspace mount ${mount.id} source`
874
+ });
875
+ assertManagedDirectoryPath({
876
+ trustedRoot: workspacePath,
877
+ candidate: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
878
+ label: `Workspace mount ${mount.id} outer target`
879
+ });
193
880
  mirrorWorkingTree({
194
881
  sourceRoot: mount.sourcePath,
195
882
  targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
@@ -201,6 +888,11 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
201
888
  function removeWorkspaceMounts(workspacePath, mounts) {
202
889
  for (const mount of mounts) {
203
890
  const targetPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
891
+ assertManagedDirectoryPath({
892
+ trustedRoot: workspacePath,
893
+ candidate: targetPath,
894
+ label: `Workspace mount ${mount.id} outer target`
895
+ });
204
896
  const relative = path.relative(path.resolve(workspacePath), path.resolve(targetPath));
205
897
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
206
898
  throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
@@ -208,9 +900,45 @@ function removeWorkspaceMounts(workspacePath, mounts) {
208
900
  fs.rmSync(targetPath, { recursive: true, force: true });
209
901
  }
210
902
  }
211
- function hydrateWorkspaceGitMounts(workspacePath, mounts) {
903
+ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
904
+ const head = revParse(workspacePath, "HEAD");
212
905
  for (const mount of mounts) {
906
+ const normalized = normalizedWorkspaceMountPath(mount.workspaceRelativePath);
907
+ const targetPath = path.join(workspacePath, ...normalized.split("/"));
908
+ assertManagedDirectoryPath({
909
+ trustedRoot: workspacePath,
910
+ candidate: targetPath,
911
+ label: `Deferred workspace mount ${mount.id} outer target`
912
+ });
913
+ fs.rmSync(targetPath, { recursive: true, force: true });
914
+ if (head && tryGit(workspacePath, ["cat-file", "-e", `${head}:${normalized}`])) {
915
+ git(
916
+ workspacePath,
917
+ ["checkout", "--no-recurse-submodules", head, "--", `:(literal)${normalized}`],
918
+ `restore deferred workspace mount ${mount.id} from its integration basis`
919
+ );
920
+ }
921
+ }
922
+ }
923
+ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
924
+ const hydratedMountIds = [];
925
+ const skippedMountIds = [];
926
+ for (const mount of mounts) {
927
+ if (!options.ignoreBusy && mount.busy?.()) {
928
+ skippedMountIds.push(mount.id);
929
+ continue;
930
+ }
213
931
  const sourceRoot = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
932
+ assertManagedDirectoryPath({
933
+ trustedRoot: workspacePath,
934
+ candidate: sourceRoot,
935
+ label: `Workspace mount ${mount.id} outer source`
936
+ });
937
+ assertManagedDirectoryPath({
938
+ trustedRoot: mount.durabilityRootPath,
939
+ candidate: mount.sourcePath,
940
+ label: `Workspace mount ${mount.id} hydration target`
941
+ });
214
942
  if (!fs.existsSync(sourceRoot)) {
215
943
  if (mount.hydrateDeletionMode === "all") {
216
944
  fs.rmSync(mount.sourcePath, { recursive: true, force: true });
@@ -227,37 +955,167 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts) {
227
955
  fs.rmSync(emptyRoot, { recursive: true, force: true });
228
956
  }
229
957
  }
958
+ hydratedMountIds.push(mount.id);
230
959
  continue;
231
960
  }
961
+ const targetExists = fs.existsSync(mount.sourcePath);
232
962
  mirrorWorkingTree({
233
963
  sourceRoot,
234
964
  targetRoot: mount.sourcePath,
235
965
  sourceMode: "all",
236
- deletionMode: mount.hydrateDeletionMode
966
+ // A missing checkout has no Git index to define git-only deletions. Its
967
+ // newly created directory is empty, so all-mode is equivalent and lets
968
+ // bootstrap hydration preserve the fetched tree before Git initializes.
969
+ deletionMode: targetExists ? mount.hydrateDeletionMode : "all"
237
970
  });
971
+ hydratedMountIds.push(mount.id);
972
+ }
973
+ return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
974
+ }
975
+ function hydrateWorkspaceGitMountsTransactionally(input) {
976
+ const workspacePath = path.resolve(input.workspacePath);
977
+ const mountIds = input.mounts.map(({ id }) => id).sort();
978
+ const requiredIds = (input.requiredMounts ?? input.mounts).map(({ id }) => id).sort();
979
+ if (mountIds.length !== requiredIds.length || mountIds.some((id, index) => id !== requiredIds[index])) {
980
+ return { hydratedMountIds: [], skippedMountIds: requiredIds };
981
+ }
982
+ const busyMountIds = input.ignoreBusy ? [] : (input.durabilityMounts ?? input.mounts).filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
983
+ if (busyMountIds.length > 0) {
984
+ return { hydratedMountIds: [], skippedMountIds: busyMountIds };
985
+ }
986
+ if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
987
+ throw new Error("Workspace checkout is not durably materialized at its current HEAD");
988
+ }
989
+ const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
990
+ const targetReceipt = targetHead ? workspaceHydrationReceipt(targetHead, input.receiptMounts ?? input.durabilityMounts ?? input.mounts) : null;
991
+ const manifest = beginHydrationTransaction(workspacePath, input.mounts, targetReceipt);
992
+ try {
993
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, input.mounts, { ignoreBusy: true });
994
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredIds[index])) {
995
+ throw new Error("Workspace hydration did not include every required mount");
996
+ }
997
+ fsyncHydratedWorkspaceMounts(input.durabilityMounts ?? input.mounts);
998
+ markHydrationTransactionDurable(workspacePath);
999
+ if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1000
+ removeHydrationTransaction(workspacePath);
1001
+ return hydration;
1002
+ } catch (error) {
1003
+ const transactionPath = hydrationTransactionPath(workspacePath);
1004
+ if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1005
+ throw error;
1006
+ }
1007
+ try {
1008
+ restoreHydrationTransaction(workspacePath, manifest);
1009
+ removeHydrationTransaction(workspacePath);
1010
+ } catch (restoreError) {
1011
+ throw new AggregateError(
1012
+ [error, restoreError],
1013
+ `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1014
+ );
1015
+ }
1016
+ throw error;
1017
+ }
1018
+ }
1019
+ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1020
+ const gitDirectory = path.join(path.resolve(workspacePath), ".git");
1021
+ if (!fs.existsSync(gitDirectory)) {
1022
+ return hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options);
1023
+ }
1024
+ recoverWorkspaceCheckoutDurability(path.resolve(workspacePath));
1025
+ return hydrateWorkspaceGitMountsTransactionally({
1026
+ workspacePath,
1027
+ mounts,
1028
+ ignoreBusy: options.ignoreBusy
1029
+ });
1030
+ }
1031
+ function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1032
+ workspacePath = path.resolve(workspacePath);
1033
+ validateMounts(workspacePath, mounts);
1034
+ recoverWorkspaceCheckoutDurability(workspacePath);
1035
+ recoverPendingHydrationTransaction(workspacePath);
1036
+ const currentHead = revParse(workspacePath, "HEAD");
1037
+ if (!currentHead) return;
1038
+ const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
1039
+ const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
1040
+ const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
1041
+ const uncoveredBasisMounts = configuredBasisMounts.filter(
1042
+ (mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
1043
+ );
1044
+ if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
1045
+ const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1046
+ if (busyMountIds.length > 0) {
1047
+ throw new Error(
1048
+ `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${busyMountIds.join(", ")}`
1049
+ );
1050
+ }
1051
+ const hydrationTargets = uncoveredBasisMounts.filter(
1052
+ (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1053
+ );
1054
+ const hydration = hydrateWorkspaceGitMountsTransactionally({
1055
+ workspacePath,
1056
+ mounts: hydrationTargets,
1057
+ durabilityMounts: uncoveredBasisMounts,
1058
+ receiptMounts: configuredBasisMounts,
1059
+ requiredMounts: hydrationTargets,
1060
+ ignoreBusy: options.ignoreBusy,
1061
+ recordCurrentHead: true
1062
+ });
1063
+ if (hydration.skippedMountIds.length > 0) {
1064
+ throw new Error(
1065
+ `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
1066
+ );
238
1067
  }
239
1068
  }
240
1069
  function resetWorkspaceGit(input) {
241
1070
  const workspacePath = path.resolve(input.workspacePath);
242
1071
  validateMounts(workspacePath, input.mounts);
243
1072
  const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1073
+ recoverPendingHydrationTransaction(workspacePath);
244
1074
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
245
1075
  const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
246
1076
  const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
247
1077
  if (remoteHead) {
248
- git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
249
- git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
1078
+ runWorkspaceCheckoutTransition(workspacePath, () => {
1079
+ git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
1080
+ git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
1081
+ });
250
1082
  } else if (revParse(workspacePath, "HEAD")) {
251
- git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
252
- git(workspacePath, ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })], "record empty workspace reset");
1083
+ runWorkspaceCheckoutTransition(workspacePath, () => {
1084
+ git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
1085
+ git(workspacePath, ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })], "record empty workspace reset");
1086
+ });
253
1087
  }
254
1088
  const selected = activeMounts(input.mounts);
255
- hydrateWorkspaceGitMounts(workspacePath, selected.active);
1089
+ const requiredLiveMountsBeforeHydration = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
1090
+ const bootstrapMounts = selected.skipped.filter((mount) => {
1091
+ if (mount.deleteWhenSourceMissing || fs.existsSync(mount.sourcePath) || mount.busy?.()) return false;
1092
+ const outerPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1093
+ return fs.existsSync(outerPath);
1094
+ });
1095
+ const hydrationMounts = [...selected.active, ...bootstrapMounts];
1096
+ const requiredLiveMounts = [
1097
+ ...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
1098
+ ];
1099
+ const hydration = hydrateWorkspaceGitMountsTransactionally({
1100
+ workspacePath,
1101
+ mounts: hydrationMounts,
1102
+ durabilityMounts: [...requiredLiveMounts, ...selected.tombstones],
1103
+ receiptMounts: input.mounts,
1104
+ requiredMounts: requiredLiveMounts,
1105
+ recordCurrentHead: true
1106
+ });
1107
+ const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
1108
+ const resetHead = revParse(workspacePath, "HEAD");
1109
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredLiveIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredLiveIds[index]) || resetHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), resetHead, input.mounts)) {
1110
+ throw new Error("Workspace reset could not durably hydrate every live mount; remediation remains active");
1111
+ }
256
1112
  return {
257
1113
  startingHead: initial.localHead,
258
1114
  localHead: revParse(workspacePath, "HEAD"),
259
1115
  remoteHead,
260
- discardedPaths
1116
+ discardedPaths,
1117
+ activeMountIds: hydration.hydratedMountIds,
1118
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
261
1119
  };
262
1120
  }
263
1121
  function stagedPaths(workspacePath) {
@@ -322,17 +1180,22 @@ function snapshotConflict(input) {
322
1180
  }
323
1181
  function rebaseWorkspace(input) {
324
1182
  const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
1183
+ beginWorkspaceCheckoutTransition(input.workspacePath);
325
1184
  const rebase = gitResult(
326
1185
  input.workspacePath,
327
1186
  input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
328
1187
  );
329
- if (rebase.exitCode === 0) return { ok: true };
1188
+ if (rebase.exitCode === 0) {
1189
+ completeWorkspaceCheckoutTransition(input.workspacePath);
1190
+ return { ok: true };
1191
+ }
330
1192
  const conflicts = conflictPaths(input.workspacePath);
331
1193
  const refs = snapshotConflict(input);
332
1194
  const abort = gitResult(input.workspacePath, ["rebase", "--abort"]);
333
1195
  if (abort.exitCode !== 0) {
334
1196
  throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
335
1197
  }
1198
+ completeWorkspaceCheckoutTransition(input.workspacePath);
336
1199
  return {
337
1200
  ok: false,
338
1201
  conflictPaths: conflicts,
@@ -342,26 +1205,31 @@ function rebaseWorkspace(input) {
342
1205
  }
343
1206
  function synchronizeWithFetchedHead(input) {
344
1207
  const localHead = revParse(input.workspacePath, "HEAD");
345
- if (!input.remoteHead) return { kind: "ready", updated: false, rebased: false };
1208
+ const remoteHead = input.remoteHead;
1209
+ if (!remoteHead) return { kind: "ready", updated: false, rebased: false };
346
1210
  if (!localHead) {
347
- git(
348
- input.workspacePath,
349
- ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
350
- "check out fetched workspace main"
351
- );
1211
+ runWorkspaceCheckoutTransition(input.workspacePath, () => {
1212
+ git(
1213
+ input.workspacePath,
1214
+ ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
1215
+ "check out fetched workspace main"
1216
+ );
1217
+ });
352
1218
  return { kind: "ready", updated: true, rebased: false };
353
1219
  }
354
- if (localHead === input.remoteHead) {
355
- updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
1220
+ if (localHead === remoteHead) {
1221
+ updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
356
1222
  return { kind: "ready", updated: false, rebased: false };
357
1223
  }
358
- if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, input.remoteHead])) {
359
- git(input.workspacePath, ["reset", "--hard", input.remoteHead], "fast-forward workspace main");
360
- updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
1224
+ if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
1225
+ runWorkspaceCheckoutTransition(input.workspacePath, () => {
1226
+ git(input.workspacePath, ["reset", "--hard", remoteHead], "fast-forward workspace main");
1227
+ });
1228
+ updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
361
1229
  return { kind: "ready", updated: true, rebased: false };
362
1230
  }
363
- if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", input.remoteHead, localHead])) {
364
- updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
1231
+ if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
1232
+ updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
365
1233
  return { kind: "ready", updated: false, rebased: false };
366
1234
  }
367
1235
  const integratedHead = revParse(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
@@ -374,11 +1242,11 @@ function synchronizeWithFetchedHead(input) {
374
1242
  const rebased = rebaseWorkspace({
375
1243
  workspacePath: input.workspacePath,
376
1244
  localHead,
377
- remoteHead: input.remoteHead,
1245
+ remoteHead,
378
1246
  attemptId: input.attemptId,
379
1247
  truncatedHistoryBase
380
1248
  });
381
- if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
1249
+ if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
382
1250
  return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
383
1251
  }
384
1252
  async function synchronizeWorkspaceGit(input) {
@@ -387,25 +1255,131 @@ async function synchronizeWorkspaceGit(input) {
387
1255
  const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
388
1256
  const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
389
1257
  validateMounts(workspacePath, input.mounts);
1258
+ const busyLiveMountIds = input.skipMountMirror ? [] : input.mounts.filter((mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()).map(({ id }) => id).sort();
1259
+ if (busyLiveMountIds.length > 0) {
1260
+ const cloneExists = fs.existsSync(path.join(workspacePath, ".git"));
1261
+ const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
1262
+ const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
1263
+ return {
1264
+ outcome: "no_change",
1265
+ startingHead: localHead,
1266
+ localHead,
1267
+ remoteHead: remoteHead2,
1268
+ publishedHead: remoteHead2,
1269
+ rebaseCount: 0,
1270
+ diffSizeBytes: 0,
1271
+ affectedPaths: [],
1272
+ activeMountIds: [],
1273
+ skippedMountIds: input.mounts.map(({ id }) => id).sort()
1274
+ };
1275
+ }
390
1276
  const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
391
1277
  const startingHead = initial.localHead;
1278
+ const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
1279
+ const deferredMountIds = /* @__PURE__ */ new Set();
1280
+ if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead && receiptBeforeInitialHydration?.head === initial.localHead) {
1281
+ for (const mount of input.mounts) {
1282
+ if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && !workspaceHydrationReceiptCoversMount(receiptBeforeInitialHydration, initial.localHead, mount)) {
1283
+ deferredMountIds.add(mount.id);
1284
+ }
1285
+ }
1286
+ }
1287
+ recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1288
+ ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
1289
+ });
1290
+ const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
392
1291
  const selected = activeMounts(input.mounts);
1292
+ const projectedActiveMounts = selected.active.filter(({ id }) => !deferredMountIds.has(id));
1293
+ const projectedTombstones = selected.tombstones.filter(({ id }) => !deferredMountIds.has(id));
1294
+ const projectionBasisHead = revParse(workspacePath, "HEAD");
1295
+ const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
1296
+ const projectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts).filter(
1297
+ (mount) => !deferredMountIds.has(mount.id) && Boolean(projectionBasisHead && workspaceHydrationReceiptCoversMount(projectionBasisReceipt, projectionBasisHead, mount))
1298
+ );
1299
+ const completedMountSelection = (requireCurrentHeadReceipt = false) => {
1300
+ const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
1301
+ const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1302
+ if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
1303
+ readHydratedWorkspaceReceipt(workspacePath),
1304
+ currentHeadBeforeHydration,
1305
+ completionReceiptMounts
1306
+ )) {
1307
+ const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
1308
+ const lateBusy = new Set(lateBusyIds);
1309
+ return {
1310
+ activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
1311
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
1312
+ };
1313
+ }
1314
+ const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
1315
+ const mountIsAlreadyCovered = (mount) => Boolean(
1316
+ currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
1317
+ );
1318
+ const uncoveredCompletionMounts = selected.active.filter((mount) => !mountIsAlreadyCovered(mount));
1319
+ const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1320
+ const uncoveredCompletionBasisMounts = completionReceiptMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1321
+ const hydration = hydrateWorkspaceGitMountsTransactionally({
1322
+ workspacePath,
1323
+ mounts: uncoveredCompletionMounts,
1324
+ durabilityMounts: uncoveredCompletionBasisMounts,
1325
+ receiptMounts: completionReceiptMounts,
1326
+ requiredMounts: uncoveredRequiredLiveMounts,
1327
+ recordCurrentHead: true
1328
+ });
1329
+ if (requireCurrentHeadReceipt) {
1330
+ const currentHead = revParse(workspacePath, "HEAD");
1331
+ if (currentHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), currentHead, completionReceiptMounts)) {
1332
+ throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
1333
+ }
1334
+ }
1335
+ return {
1336
+ activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1337
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1338
+ };
1339
+ };
1340
+ let newlyStagedPaths = [];
1341
+ const stageAndCommitWorkspace = () => {
1342
+ git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
1343
+ newlyStagedPaths = stagedPaths(workspacePath);
1344
+ if (newlyStagedPaths.length > 0) {
1345
+ git(
1346
+ workspacePath,
1347
+ [
1348
+ "commit",
1349
+ "-m",
1350
+ JSON.stringify({ type: "workspace_sync", worker: input.workerLabel, attemptId, detail: input.commitDetail ?? null })
1351
+ ],
1352
+ "commit workspace working trees"
1353
+ );
1354
+ }
1355
+ };
393
1356
  if (!input.skipMountMirror) {
394
- mirrorMountsToWorkspace(workspacePath, selected.active);
395
- removeWorkspaceMounts(workspacePath, selected.tombstones);
1357
+ beginWorkspaceCheckoutTransition(workspacePath);
1358
+ try {
1359
+ mirrorMountsToWorkspace(workspacePath, projectedActiveMounts);
1360
+ removeWorkspaceMounts(workspacePath, projectedTombstones);
1361
+ restoreDeferredWorkspaceMountsFromHead(
1362
+ workspacePath,
1363
+ input.mounts.filter(({ id }) => deferredMountIds.has(id))
1364
+ );
1365
+ stageAndCommitWorkspace();
1366
+ fsyncWorkspaceCheckoutTree(workspacePath);
1367
+ } catch (error) {
1368
+ throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
1369
+ }
1370
+ } else {
1371
+ stageAndCommitWorkspace();
1372
+ if (newlyStagedPaths.length > 0) completeWorkspaceCheckoutTransition(workspacePath);
396
1373
  }
397
- git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
398
- const newlyStagedPaths = stagedPaths(workspacePath);
399
- if (newlyStagedPaths.length > 0) {
400
- git(
401
- workspacePath,
402
- [
403
- "commit",
404
- "-m",
405
- JSON.stringify({ type: "workspace_sync", worker: input.workerLabel, attemptId, detail: input.commitDetail ?? null })
406
- ],
407
- "commit workspace working trees"
408
- );
1374
+ if (!input.skipMountMirror) {
1375
+ const projectedHead = revParse(workspacePath, "HEAD");
1376
+ if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
1377
+ const targetProjectionReceipt = workspaceHydrationReceipt(projectedHead, projectionReceiptMounts);
1378
+ if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), targetProjectionReceipt)) {
1379
+ fsyncHydratedWorkspaceMounts([...projectedActiveMounts, ...projectedTombstones]);
1380
+ updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
1381
+ }
1382
+ completeWorkspaceCheckoutTransition(workspacePath);
409
1383
  }
410
1384
  let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
411
1385
  let rebaseCount = 0;
@@ -423,8 +1397,8 @@ async function synchronizeWorkspaceGit(input) {
423
1397
  rebaseCount: rebaseCount + 1,
424
1398
  diffSizeBytes: 0,
425
1399
  affectedPaths: [.../* @__PURE__ */ new Set([...newlyStagedPaths, ...reconciled.conflictPaths])].sort(),
426
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
427
- skippedMountIds: selected.skipped.map(({ id }) => id).sort(),
1400
+ activeMountIds: [...projectedActiveMounts, ...projectedTombstones].map(({ id }) => id).sort(),
1401
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
428
1402
  conflictPaths: reconciled.conflictPaths,
429
1403
  conflictSnapshotRefs: reconciled.refs,
430
1404
  error: reconciled.error
@@ -432,6 +1406,7 @@ async function synchronizeWorkspaceGit(input) {
432
1406
  }
433
1407
  updated ||= reconciled.updated;
434
1408
  if (reconciled.rebased) rebaseCount += 1;
1409
+ if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
435
1410
  const localHead = revParse(workspacePath, "HEAD");
436
1411
  if (!localHead) {
437
1412
  return {
@@ -465,11 +1440,11 @@ async function synchronizeWorkspaceGit(input) {
465
1440
  };
466
1441
  }
467
1442
  if (localHead === remoteHead) {
1443
+ const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
468
1444
  await input.afterWorkspacePublished?.({
469
1445
  publishedHead: localHead,
470
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort()
1446
+ activeMountIds: completedMounts.activeMountIds
471
1447
  });
472
- if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
473
1448
  return {
474
1449
  outcome: updated ? "updated" : "no_change",
475
1450
  startingHead,
@@ -479,8 +1454,7 @@ async function synchronizeWorkspaceGit(input) {
479
1454
  rebaseCount,
480
1455
  diffSizeBytes: size,
481
1456
  affectedPaths: paths,
482
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
483
- skippedMountIds: selected.skipped.map(({ id }) => id).sort()
1457
+ ...completedMounts
484
1458
  };
485
1459
  }
486
1460
  const pushArgs = [
@@ -493,11 +1467,11 @@ async function synchronizeWorkspaceGit(input) {
493
1467
  const push = gitResult(workspacePath, pushArgs);
494
1468
  if (push.exitCode === 0) {
495
1469
  updateIntegratedWorkspaceHead(workspacePath, localHead);
1470
+ const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
496
1471
  await input.afterWorkspacePublished?.({
497
1472
  publishedHead: localHead,
498
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort()
1473
+ activeMountIds: completedMounts.activeMountIds
499
1474
  });
500
- if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
501
1475
  return {
502
1476
  outcome: "pushed",
503
1477
  startingHead,
@@ -507,8 +1481,7 @@ async function synchronizeWorkspaceGit(input) {
507
1481
  rebaseCount,
508
1482
  diffSizeBytes: size,
509
1483
  affectedPaths: paths,
510
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
511
- skippedMountIds: selected.skipped.map(({ id }) => id).sort()
1484
+ ...completedMounts
512
1485
  };
513
1486
  }
514
1487
  if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
@@ -529,10 +1502,15 @@ const workspaceGitSyncTestHarness = {
529
1502
  export {
530
1503
  MAX_WORKSPACE_GIT_DIFF_BYTES,
531
1504
  WORKSPACE_GIT_BRANCH,
1505
+ WORKSPACE_GIT_CHECKOUT_DURABILITY,
532
1506
  WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
1507
+ WORKSPACE_GIT_HYDRATED_RECEIPT,
1508
+ WORKSPACE_GIT_HYDRATION_TRANSACTION,
533
1509
  ensureWorkspaceGitClone,
534
1510
  hydrateWorkspaceGitMounts,
1511
+ recoverWorkspaceGitHydration,
535
1512
  resetWorkspaceGit,
536
1513
  synchronizeWorkspaceGit,
1514
+ workspaceGitHydrationIsCurrent,
537
1515
  workspaceGitSyncTestHarness
538
1516
  };