@ricsam/r5d-worker 0.0.78 → 0.0.79

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 +641 -56
  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 +191 -34
  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 +1028 -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 +632 -55
  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 +190 -34
  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 +1023 -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 +7 -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,631 @@ 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({ sourceRoot: sourcePath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
604
+ }
605
+ return {
606
+ id: mount.id,
607
+ incarnationKey: mount.hydrationIncarnationKey,
608
+ sourcePath,
609
+ durabilityRootPath,
610
+ workspaceRelativePath: mount.workspaceRelativePath,
611
+ existed: Boolean(sourceStatus),
612
+ snapshotName
613
+ };
614
+ })
615
+ };
616
+ writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
617
+ `);
618
+ fsyncTree(stagingPath);
619
+ fs.renameSync(stagingPath, transactionPath);
620
+ fsyncDirectory(stateDirectory);
621
+ return manifest;
622
+ } catch (error) {
623
+ fs.rmSync(stagingPath, { recursive: true, force: true });
624
+ fsyncDirectory(stateDirectory);
625
+ throw error;
626
+ }
627
+ }
628
+ function markHydrationTransactionDurable(workspacePath) {
629
+ const transactionPath = hydrationTransactionPath(workspacePath);
630
+ writePrivateFileDurably(path.join(transactionPath, "committed"), "committed\n");
631
+ fsyncDirectory(transactionPath);
632
+ }
633
+ function fsyncHydratedWorkspaceMount(mount) {
634
+ assertManagedDirectoryPath({
635
+ trustedRoot: mount.durabilityRootPath,
636
+ candidate: mount.sourcePath,
637
+ label: `Workspace mount ${mount.id} source`
638
+ });
639
+ const syncEntry = (entryPath, root) => {
640
+ const status = fs.lstatSync(entryPath);
641
+ if (status.isSymbolicLink()) return;
642
+ if (status.isDirectory()) {
643
+ for (const name of fs.readdirSync(entryPath)) {
644
+ if (root && name === ".git") continue;
645
+ syncEntry(path.join(entryPath, name), false);
646
+ }
647
+ fsyncDirectory(entryPath);
648
+ return;
649
+ }
650
+ if (!status.isFile()) return;
651
+ const descriptor = fs.openSync(entryPath, fs.constants.O_RDONLY);
652
+ try {
653
+ fs.fsyncSync(descriptor);
654
+ } finally {
655
+ fs.closeSync(descriptor);
656
+ }
657
+ };
658
+ if (fs.existsSync(mount.sourcePath)) syncEntry(mount.sourcePath, true);
659
+ const durabilityRoot = path.resolve(mount.durabilityRootPath);
660
+ let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
661
+ while (true) {
662
+ if (fs.existsSync(boundaryParent)) {
663
+ const boundaryStatus = fs.lstatSync(boundaryParent);
664
+ if (!boundaryStatus.isDirectory() || boundaryStatus.isSymbolicLink()) {
665
+ throw new Error(`Workspace mount durability boundary is not a regular directory: ${boundaryParent}`);
666
+ }
667
+ fsyncDirectory(boundaryParent);
668
+ }
669
+ if (boundaryParent === durabilityRoot) break;
670
+ const nextParent = path.dirname(boundaryParent);
671
+ if (nextParent === boundaryParent) {
672
+ throw new Error(`Workspace mount ${mount.sourcePath} does not descend from durability root ${durabilityRoot}`);
673
+ }
674
+ boundaryParent = nextParent;
675
+ }
676
+ }
677
+ function fsyncHydratedWorkspaceMounts(mounts) {
678
+ for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
679
+ }
51
680
  function workspaceIsShallow(workspacePath) {
52
681
  return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
53
682
  }
@@ -60,7 +689,22 @@ function validateMounts(workspacePath, mounts) {
60
689
  const paths = [];
61
690
  for (const mount of mounts) {
62
691
  if (!mount.id || ids.has(mount.id)) throw new Error(`Workspace mount id must be unique: ${mount.id}`);
692
+ if (!mount.hydrationIncarnationKey || mount.hydrationIncarnationKey.includes("\0")) {
693
+ throw new Error(`Workspace mount hydration incarnation is invalid: ${mount.id}`);
694
+ }
63
695
  ids.add(mount.id);
696
+ const sourcePath = path.resolve(mount.sourcePath);
697
+ const durabilityRoot = path.resolve(mount.durabilityRootPath);
698
+ const durabilityRelative = path.relative(durabilityRoot, sourcePath);
699
+ const durabilityRootStatus = fs.lstatSync(durabilityRoot);
700
+ if (!durabilityRootStatus.isDirectory() || durabilityRootStatus.isSymbolicLink() || !durabilityRelative || durabilityRelative === ".." || durabilityRelative.startsWith(`..${path.sep}`) || path.isAbsolute(durabilityRelative)) {
701
+ throw new Error(`Workspace mount source escapes its durability root: ${mount.sourcePath}`);
702
+ }
703
+ assertManagedDirectoryPath({
704
+ trustedRoot: durabilityRoot,
705
+ candidate: sourcePath,
706
+ label: `Workspace mount ${mount.id} source`
707
+ });
64
708
  const normalized = mount.workspaceRelativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
65
709
  if (!normalized || normalized === "." || normalized.split("/").some((segment) => segment === ".." || segment === ".git")) {
66
710
  throw new Error(`Invalid workspace mount path: ${mount.workspaceRelativePath}`);
@@ -107,6 +751,8 @@ function configureWorkspaceRepository(input) {
107
751
  if (!name || !email) throw new Error("Workspace Git identity must include name and email");
108
752
  git(input.workspacePath, ["config", "--local", "user.name", name], "configure workspace Git user name");
109
753
  git(input.workspacePath, ["config", "--local", "user.email", email], "configure workspace Git user email");
754
+ git(input.workspacePath, ["config", "--local", "core.fsync", "committed"], "configure durable workspace commits");
755
+ git(input.workspacePath, ["config", "--local", "core.fsyncMethod", "fsync"], "configure workspace fsync method");
110
756
  }
111
757
  function fetchWorkspaceHead(workspacePath, remoteUrl, credentialHelper, credentialUsername) {
112
758
  const result = gitResult(workspacePath, [
@@ -130,7 +776,24 @@ ${result.stdout}`;
130
776
  }
131
777
  function ensureWorkspaceGitClone(input) {
132
778
  const workspacePath = path.resolve(input.workspacePath);
779
+ const workspaceParent = path.dirname(workspacePath);
780
+ const parentStatus = fs.lstatSync(workspaceParent);
781
+ if (!parentStatus.isDirectory() || parentStatus.isSymbolicLink()) {
782
+ throw new Error(`Workspace clone parent is not a regular directory: ${workspaceParent}`);
783
+ }
784
+ const workspaceStatus = lstatIfExists(workspacePath);
785
+ if (workspaceStatus) {
786
+ if (!workspaceStatus.isDirectory() || workspaceStatus.isSymbolicLink()) {
787
+ throw new Error(`Workspace clone root is not a regular directory: ${workspacePath}`);
788
+ }
789
+ }
133
790
  const gitPath = path.join(workspacePath, ".git");
791
+ const gitStatus = lstatIfExists(gitPath);
792
+ if (gitStatus) {
793
+ if (!gitStatus.isDirectory() || gitStatus.isSymbolicLink()) {
794
+ throw new Error(`Workspace clone Git metadata is not a regular directory: ${gitPath}`);
795
+ }
796
+ }
134
797
  if (!fs.existsSync(gitPath)) {
135
798
  fs.rmSync(workspacePath, { recursive: true, force: true });
136
799
  fs.mkdirSync(path.dirname(workspacePath), { recursive: true });
@@ -152,6 +815,7 @@ function ensureWorkspaceGitClone(input) {
152
815
  }
153
816
  }
154
817
  configureWorkspaceRepository({ ...input, workspacePath });
818
+ recoverWorkspaceCheckoutDurability(workspacePath);
155
819
  const previousRemoteHead = revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
156
820
  const localHeadBeforeFetch = revParse(workspacePath, "HEAD");
157
821
  if (!revParse(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && tryGit(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
@@ -160,17 +824,21 @@ function ensureWorkspaceGitClone(input) {
160
824
  const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
161
825
  let localHead = revParse(workspacePath, "HEAD");
162
826
  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
- );
827
+ runWorkspaceCheckoutTransition(workspacePath, () => {
828
+ git(
829
+ workspacePath,
830
+ ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
831
+ "check out workspace main"
832
+ );
833
+ });
168
834
  localHead = remoteHead;
169
835
  }
170
836
  if (localHead && remoteHead && localHead === remoteHead) {
171
837
  updateIntegratedWorkspaceHead(workspacePath, remoteHead);
172
838
  } 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");
839
+ runWorkspaceCheckoutTransition(workspacePath, () => {
840
+ git(workspacePath, ["reset", "--hard", remoteHead], "advance workspace across retained history boundary");
841
+ });
174
842
  updateIntegratedWorkspaceHead(workspacePath, remoteHead);
175
843
  localHead = remoteHead;
176
844
  }
@@ -190,6 +858,16 @@ function activeMounts(mounts) {
190
858
  }
191
859
  function mirrorMountsToWorkspace(workspacePath, mounts) {
192
860
  for (const mount of mounts) {
861
+ assertManagedDirectoryPath({
862
+ trustedRoot: mount.durabilityRootPath,
863
+ candidate: mount.sourcePath,
864
+ label: `Workspace mount ${mount.id} source`
865
+ });
866
+ assertManagedDirectoryPath({
867
+ trustedRoot: workspacePath,
868
+ candidate: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
869
+ label: `Workspace mount ${mount.id} outer target`
870
+ });
193
871
  mirrorWorkingTree({
194
872
  sourceRoot: mount.sourcePath,
195
873
  targetRoot: path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/")),
@@ -201,6 +879,11 @@ function mirrorMountsToWorkspace(workspacePath, mounts) {
201
879
  function removeWorkspaceMounts(workspacePath, mounts) {
202
880
  for (const mount of mounts) {
203
881
  const targetPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
882
+ assertManagedDirectoryPath({
883
+ trustedRoot: workspacePath,
884
+ candidate: targetPath,
885
+ label: `Workspace mount ${mount.id} outer target`
886
+ });
204
887
  const relative = path.relative(path.resolve(workspacePath), path.resolve(targetPath));
205
888
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
206
889
  throw new Error(`Workspace mount escapes the workspace clone: ${mount.workspaceRelativePath}`);
@@ -208,9 +891,45 @@ function removeWorkspaceMounts(workspacePath, mounts) {
208
891
  fs.rmSync(targetPath, { recursive: true, force: true });
209
892
  }
210
893
  }
211
- function hydrateWorkspaceGitMounts(workspacePath, mounts) {
894
+ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
895
+ const head = revParse(workspacePath, "HEAD");
896
+ for (const mount of mounts) {
897
+ const normalized = normalizedWorkspaceMountPath(mount.workspaceRelativePath);
898
+ const targetPath = path.join(workspacePath, ...normalized.split("/"));
899
+ assertManagedDirectoryPath({
900
+ trustedRoot: workspacePath,
901
+ candidate: targetPath,
902
+ label: `Deferred workspace mount ${mount.id} outer target`
903
+ });
904
+ fs.rmSync(targetPath, { recursive: true, force: true });
905
+ if (head && tryGit(workspacePath, ["cat-file", "-e", `${head}:${normalized}`])) {
906
+ git(
907
+ workspacePath,
908
+ ["checkout", "--no-recurse-submodules", head, "--", `:(literal)${normalized}`],
909
+ `restore deferred workspace mount ${mount.id} from its integration basis`
910
+ );
911
+ }
912
+ }
913
+ }
914
+ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
915
+ const hydratedMountIds = [];
916
+ const skippedMountIds = [];
212
917
  for (const mount of mounts) {
918
+ if (!options.ignoreBusy && mount.busy?.()) {
919
+ skippedMountIds.push(mount.id);
920
+ continue;
921
+ }
213
922
  const sourceRoot = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
923
+ assertManagedDirectoryPath({
924
+ trustedRoot: workspacePath,
925
+ candidate: sourceRoot,
926
+ label: `Workspace mount ${mount.id} outer source`
927
+ });
928
+ assertManagedDirectoryPath({
929
+ trustedRoot: mount.durabilityRootPath,
930
+ candidate: mount.sourcePath,
931
+ label: `Workspace mount ${mount.id} hydration target`
932
+ });
214
933
  if (!fs.existsSync(sourceRoot)) {
215
934
  if (mount.hydrateDeletionMode === "all") {
216
935
  fs.rmSync(mount.sourcePath, { recursive: true, force: true });
@@ -227,37 +946,167 @@ function hydrateWorkspaceGitMounts(workspacePath, mounts) {
227
946
  fs.rmSync(emptyRoot, { recursive: true, force: true });
228
947
  }
229
948
  }
949
+ hydratedMountIds.push(mount.id);
230
950
  continue;
231
951
  }
952
+ const targetExists = fs.existsSync(mount.sourcePath);
232
953
  mirrorWorkingTree({
233
954
  sourceRoot,
234
955
  targetRoot: mount.sourcePath,
235
956
  sourceMode: "all",
236
- deletionMode: mount.hydrateDeletionMode
957
+ // A missing checkout has no Git index to define git-only deletions. Its
958
+ // newly created directory is empty, so all-mode is equivalent and lets
959
+ // bootstrap hydration preserve the fetched tree before Git initializes.
960
+ deletionMode: targetExists ? mount.hydrateDeletionMode : "all"
237
961
  });
962
+ hydratedMountIds.push(mount.id);
963
+ }
964
+ return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
965
+ }
966
+ function hydrateWorkspaceGitMountsTransactionally(input) {
967
+ const workspacePath = path.resolve(input.workspacePath);
968
+ const mountIds = input.mounts.map(({ id }) => id).sort();
969
+ const requiredIds = (input.requiredMounts ?? input.mounts).map(({ id }) => id).sort();
970
+ if (mountIds.length !== requiredIds.length || mountIds.some((id, index) => id !== requiredIds[index])) {
971
+ return { hydratedMountIds: [], skippedMountIds: requiredIds };
972
+ }
973
+ const busyMountIds = input.ignoreBusy ? [] : (input.durabilityMounts ?? input.mounts).filter((mount) => mount.busy?.()).map(({ id }) => id).sort();
974
+ if (busyMountIds.length > 0) {
975
+ return { hydratedMountIds: [], skippedMountIds: busyMountIds };
976
+ }
977
+ if (input.recordCurrentHead && !workspaceCheckoutDurabilityIsCurrent(workspacePath)) {
978
+ throw new Error("Workspace checkout is not durably materialized at its current HEAD");
979
+ }
980
+ const targetHead = input.recordCurrentHead ? revParse(workspacePath, "HEAD") : null;
981
+ const targetReceipt = targetHead ? workspaceHydrationReceipt(targetHead, input.receiptMounts ?? input.durabilityMounts ?? input.mounts) : null;
982
+ const manifest = beginHydrationTransaction(workspacePath, input.mounts, targetReceipt);
983
+ try {
984
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, input.mounts, { ignoreBusy: true });
985
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== requiredIds.length || hydration.hydratedMountIds.some((id, index) => id !== requiredIds[index])) {
986
+ throw new Error("Workspace hydration did not include every required mount");
987
+ }
988
+ fsyncHydratedWorkspaceMounts(input.durabilityMounts ?? input.mounts);
989
+ markHydrationTransactionDurable(workspacePath);
990
+ if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
991
+ removeHydrationTransaction(workspacePath);
992
+ return hydration;
993
+ } catch (error) {
994
+ const transactionPath = hydrationTransactionPath(workspacePath);
995
+ if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
996
+ throw error;
997
+ }
998
+ try {
999
+ restoreHydrationTransaction(workspacePath, manifest);
1000
+ removeHydrationTransaction(workspacePath);
1001
+ } catch (restoreError) {
1002
+ throw new AggregateError(
1003
+ [error, restoreError],
1004
+ `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1005
+ );
1006
+ }
1007
+ throw error;
1008
+ }
1009
+ }
1010
+ function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1011
+ const gitDirectory = path.join(path.resolve(workspacePath), ".git");
1012
+ if (!fs.existsSync(gitDirectory)) {
1013
+ return hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options);
1014
+ }
1015
+ recoverWorkspaceCheckoutDurability(path.resolve(workspacePath));
1016
+ return hydrateWorkspaceGitMountsTransactionally({
1017
+ workspacePath,
1018
+ mounts,
1019
+ ignoreBusy: options.ignoreBusy
1020
+ });
1021
+ }
1022
+ function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
1023
+ workspacePath = path.resolve(workspacePath);
1024
+ validateMounts(workspacePath, mounts);
1025
+ recoverWorkspaceCheckoutDurability(workspacePath);
1026
+ recoverPendingHydrationTransaction(workspacePath);
1027
+ const currentHead = revParse(workspacePath, "HEAD");
1028
+ if (!currentHead) return;
1029
+ const currentReceipt = readHydratedWorkspaceReceipt(workspacePath);
1030
+ const outerHeadContainsMount = (mount) => tryGit(workspacePath, ["cat-file", "-e", `HEAD:${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`]);
1031
+ const configuredBasisMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, mounts, options.deferMountIds);
1032
+ const uncoveredBasisMounts = configuredBasisMounts.filter(
1033
+ (mount) => !workspaceHydrationReceiptCoversMount(currentReceipt, currentHead, mount)
1034
+ );
1035
+ if (workspaceHydrationReceiptMatchesMounts(currentReceipt, currentHead, configuredBasisMounts)) return;
1036
+ const busyMountIds = options.ignoreBusy ? [] : uncoveredBasisMounts.filter((mount) => mount.busy?.()).map(({ id }) => id);
1037
+ if (busyMountIds.length > 0) {
1038
+ throw new Error(
1039
+ `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${busyMountIds.join(", ")}`
1040
+ );
1041
+ }
1042
+ const hydrationTargets = uncoveredBasisMounts.filter(
1043
+ (mount) => !mount.deleteWhenSourceMissing && !(mount.preserveLocalOnHydrationBasisChange === true && currentReceipt?.head === currentHead) && (outerHeadContainsMount(mount) || mount.preserveLocalOnInitialOuterAbsence !== true)
1044
+ );
1045
+ const hydration = hydrateWorkspaceGitMountsTransactionally({
1046
+ workspacePath,
1047
+ mounts: hydrationTargets,
1048
+ durabilityMounts: uncoveredBasisMounts,
1049
+ receiptMounts: configuredBasisMounts,
1050
+ requiredMounts: hydrationTargets,
1051
+ ignoreBusy: options.ignoreBusy,
1052
+ recordCurrentHead: true
1053
+ });
1054
+ if (hydration.skippedMountIds.length > 0) {
1055
+ throw new Error(
1056
+ `Workspace HEAD ${currentHead} has not been fully hydrated; busy mounts must finish before projection: ${hydration.skippedMountIds.join(", ")}`
1057
+ );
238
1058
  }
239
1059
  }
240
1060
  function resetWorkspaceGit(input) {
241
1061
  const workspacePath = path.resolve(input.workspacePath);
242
1062
  validateMounts(workspacePath, input.mounts);
243
1063
  const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
1064
+ recoverPendingHydrationTransaction(workspacePath);
244
1065
  const status = gitResult(workspacePath, ["status", "--porcelain=v1", "-z"]);
245
1066
  const discardedPaths = status.exitCode === 0 ? status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).sort() : [];
246
1067
  const remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
247
1068
  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");
1069
+ runWorkspaceCheckoutTransition(workspacePath, () => {
1070
+ git(workspacePath, ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, remoteHead], "reset workspace main");
1071
+ git(workspacePath, ["clean", "-fd", "--", "."], "remove untracked workspace changes");
1072
+ });
250
1073
  } 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");
1074
+ runWorkspaceCheckoutTransition(workspacePath, () => {
1075
+ git(workspacePath, ["rm", "-rf", "--ignore-unmatch", "--", "."], "clear unborn workspace tree");
1076
+ git(workspacePath, ["commit", "--allow-empty", "-m", JSON.stringify({ type: "workspace_reset" })], "record empty workspace reset");
1077
+ });
253
1078
  }
254
1079
  const selected = activeMounts(input.mounts);
255
- hydrateWorkspaceGitMounts(workspacePath, selected.active);
1080
+ const requiredLiveMountsBeforeHydration = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
1081
+ const bootstrapMounts = selected.skipped.filter((mount) => {
1082
+ if (mount.deleteWhenSourceMissing || fs.existsSync(mount.sourcePath) || mount.busy?.()) return false;
1083
+ const outerPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1084
+ return fs.existsSync(outerPath);
1085
+ });
1086
+ const hydrationMounts = [...selected.active, ...bootstrapMounts];
1087
+ const requiredLiveMounts = [
1088
+ ...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
1089
+ ];
1090
+ const hydration = hydrateWorkspaceGitMountsTransactionally({
1091
+ workspacePath,
1092
+ mounts: hydrationMounts,
1093
+ durabilityMounts: [...requiredLiveMounts, ...selected.tombstones],
1094
+ receiptMounts: input.mounts,
1095
+ requiredMounts: requiredLiveMounts,
1096
+ recordCurrentHead: true
1097
+ });
1098
+ const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
1099
+ const resetHead = revParse(workspacePath, "HEAD");
1100
+ 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)) {
1101
+ throw new Error("Workspace reset could not durably hydrate every live mount; remediation remains active");
1102
+ }
256
1103
  return {
257
1104
  startingHead: initial.localHead,
258
1105
  localHead: revParse(workspacePath, "HEAD"),
259
1106
  remoteHead,
260
- discardedPaths
1107
+ discardedPaths,
1108
+ activeMountIds: hydration.hydratedMountIds,
1109
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
261
1110
  };
262
1111
  }
263
1112
  function stagedPaths(workspacePath) {
@@ -322,17 +1171,22 @@ function snapshotConflict(input) {
322
1171
  }
323
1172
  function rebaseWorkspace(input) {
324
1173
  const remoteRevision = `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`;
1174
+ beginWorkspaceCheckoutTransition(input.workspacePath);
325
1175
  const rebase = gitResult(
326
1176
  input.workspacePath,
327
1177
  input.truncatedHistoryBase ? ["rebase", "--onto", remoteRevision, input.truncatedHistoryBase] : ["rebase", remoteRevision]
328
1178
  );
329
- if (rebase.exitCode === 0) return { ok: true };
1179
+ if (rebase.exitCode === 0) {
1180
+ completeWorkspaceCheckoutTransition(input.workspacePath);
1181
+ return { ok: true };
1182
+ }
330
1183
  const conflicts = conflictPaths(input.workspacePath);
331
1184
  const refs = snapshotConflict(input);
332
1185
  const abort = gitResult(input.workspacePath, ["rebase", "--abort"]);
333
1186
  if (abort.exitCode !== 0) {
334
1187
  throw new Error(`Abort workspace rebase: ${abort.stderr || abort.stdout || `git exited ${abort.exitCode}`}`);
335
1188
  }
1189
+ completeWorkspaceCheckoutTransition(input.workspacePath);
336
1190
  return {
337
1191
  ok: false,
338
1192
  conflictPaths: conflicts,
@@ -342,26 +1196,31 @@ function rebaseWorkspace(input) {
342
1196
  }
343
1197
  function synchronizeWithFetchedHead(input) {
344
1198
  const localHead = revParse(input.workspacePath, "HEAD");
345
- if (!input.remoteHead) return { kind: "ready", updated: false, rebased: false };
1199
+ const remoteHead = input.remoteHead;
1200
+ if (!remoteHead) return { kind: "ready", updated: false, rebased: false };
346
1201
  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
- );
1202
+ runWorkspaceCheckoutTransition(input.workspacePath, () => {
1203
+ git(
1204
+ input.workspacePath,
1205
+ ["checkout", "--no-recurse-submodules", "-B", WORKSPACE_GIT_BRANCH, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`],
1206
+ "check out fetched workspace main"
1207
+ );
1208
+ });
352
1209
  return { kind: "ready", updated: true, rebased: false };
353
1210
  }
354
- if (localHead === input.remoteHead) {
355
- updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
1211
+ if (localHead === remoteHead) {
1212
+ updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
356
1213
  return { kind: "ready", updated: false, rebased: false };
357
1214
  }
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);
1215
+ if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", localHead, remoteHead])) {
1216
+ runWorkspaceCheckoutTransition(input.workspacePath, () => {
1217
+ git(input.workspacePath, ["reset", "--hard", remoteHead], "fast-forward workspace main");
1218
+ });
1219
+ updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
361
1220
  return { kind: "ready", updated: true, rebased: false };
362
1221
  }
363
- if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", input.remoteHead, localHead])) {
364
- updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
1222
+ if (tryGit(input.workspacePath, ["merge-base", "--is-ancestor", remoteHead, localHead])) {
1223
+ updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
365
1224
  return { kind: "ready", updated: false, rebased: false };
366
1225
  }
367
1226
  const integratedHead = revParse(input.workspacePath, WORKSPACE_GIT_INTEGRATED_REF);
@@ -374,11 +1233,11 @@ function synchronizeWithFetchedHead(input) {
374
1233
  const rebased = rebaseWorkspace({
375
1234
  workspacePath: input.workspacePath,
376
1235
  localHead,
377
- remoteHead: input.remoteHead,
1236
+ remoteHead,
378
1237
  attemptId: input.attemptId,
379
1238
  truncatedHistoryBase
380
1239
  });
381
- if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, input.remoteHead);
1240
+ if (rebased.ok) updateIntegratedWorkspaceHead(input.workspacePath, remoteHead);
382
1241
  return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
383
1242
  }
384
1243
  async function synchronizeWorkspaceGit(input) {
@@ -387,25 +1246,131 @@ async function synchronizeWorkspaceGit(input) {
387
1246
  const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
388
1247
  const maxPushAttempts = Math.max(1, input.maxPushAttempts ?? 4);
389
1248
  validateMounts(workspacePath, input.mounts);
1249
+ const busyLiveMountIds = input.skipMountMirror ? [] : input.mounts.filter((mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()).map(({ id }) => id).sort();
1250
+ if (busyLiveMountIds.length > 0) {
1251
+ const cloneExists = fs.existsSync(path.join(workspacePath, ".git"));
1252
+ const localHead = cloneExists ? revParse(workspacePath, "HEAD") : null;
1253
+ const remoteHead2 = cloneExists ? revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`) : null;
1254
+ return {
1255
+ outcome: "no_change",
1256
+ startingHead: localHead,
1257
+ localHead,
1258
+ remoteHead: remoteHead2,
1259
+ publishedHead: remoteHead2,
1260
+ rebaseCount: 0,
1261
+ diffSizeBytes: 0,
1262
+ affectedPaths: [],
1263
+ activeMountIds: [],
1264
+ skippedMountIds: input.mounts.map(({ id }) => id).sort()
1265
+ };
1266
+ }
390
1267
  const initial = ensureWorkspaceGitClone({ ...input, workspacePath });
391
1268
  const startingHead = initial.localHead;
1269
+ const receiptBeforeInitialHydration = readHydratedWorkspaceReceipt(workspacePath);
1270
+ const deferredMountIds = /* @__PURE__ */ new Set();
1271
+ if (initial.localHead && initial.remoteHead && initial.remoteHead !== initial.localHead && receiptBeforeInitialHydration?.head === initial.localHead) {
1272
+ for (const mount of input.mounts) {
1273
+ if (!mount.deleteWhenSourceMissing && mount.preserveLocalOnInitialOuterAbsence !== true && mount.preserveLocalOnHydrationBasisChange !== true && !workspaceHydrationReceiptCoversMount(receiptBeforeInitialHydration, initial.localHead, mount)) {
1274
+ deferredMountIds.add(mount.id);
1275
+ }
1276
+ }
1277
+ }
1278
+ recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1279
+ ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {}
1280
+ });
1281
+ const receiptRequiredLiveMounts = input.mounts.filter(({ sourcePath }) => fs.existsSync(sourcePath));
392
1282
  const selected = activeMounts(input.mounts);
1283
+ const projectedActiveMounts = selected.active.filter(({ id }) => !deferredMountIds.has(id));
1284
+ const projectedTombstones = selected.tombstones.filter(({ id }) => !deferredMountIds.has(id));
1285
+ const projectionBasisHead = revParse(workspacePath, "HEAD");
1286
+ const projectionBasisReceipt = readHydratedWorkspaceReceipt(workspacePath);
1287
+ const projectionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts).filter(
1288
+ (mount) => !deferredMountIds.has(mount.id) && Boolean(projectionBasisHead && workspaceHydrationReceiptCoversMount(projectionBasisReceipt, projectionBasisHead, mount))
1289
+ );
1290
+ const completedMountSelection = (requireCurrentHeadReceipt = false) => {
1291
+ const currentHeadBeforeHydration = revParse(workspacePath, "HEAD");
1292
+ const completionReceiptMounts = configuredWorkspaceHydrationBasisMounts(workspacePath, input.mounts);
1293
+ if (currentHeadBeforeHydration && workspaceHydrationReceiptMatchesMounts(
1294
+ readHydratedWorkspaceReceipt(workspacePath),
1295
+ currentHeadBeforeHydration,
1296
+ completionReceiptMounts
1297
+ )) {
1298
+ const lateBusyIds = selected.active.filter((mount) => mount.busy?.()).map(({ id }) => id);
1299
+ const lateBusy = new Set(lateBusyIds);
1300
+ return {
1301
+ activeMountIds: [...selected.active.filter(({ id }) => !lateBusy.has(id)), ...selected.tombstones].map(({ id }) => id).sort(),
1302
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...lateBusyIds])].sort()
1303
+ };
1304
+ }
1305
+ const currentReceiptBeforeHydration = readHydratedWorkspaceReceipt(workspacePath);
1306
+ const mountIsAlreadyCovered = (mount) => Boolean(
1307
+ currentHeadBeforeHydration && workspaceHydrationReceiptCoversMount(currentReceiptBeforeHydration, currentHeadBeforeHydration, mount)
1308
+ );
1309
+ const uncoveredCompletionMounts = selected.active.filter((mount) => !mountIsAlreadyCovered(mount));
1310
+ const uncoveredRequiredLiveMounts = receiptRequiredLiveMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1311
+ const uncoveredCompletionBasisMounts = completionReceiptMounts.filter((mount) => !mountIsAlreadyCovered(mount));
1312
+ const hydration = hydrateWorkspaceGitMountsTransactionally({
1313
+ workspacePath,
1314
+ mounts: uncoveredCompletionMounts,
1315
+ durabilityMounts: uncoveredCompletionBasisMounts,
1316
+ receiptMounts: completionReceiptMounts,
1317
+ requiredMounts: uncoveredRequiredLiveMounts,
1318
+ recordCurrentHead: true
1319
+ });
1320
+ if (requireCurrentHeadReceipt) {
1321
+ const currentHead = revParse(workspacePath, "HEAD");
1322
+ if (currentHead && !workspaceHydrationReceiptMatchesMounts(readHydratedWorkspaceReceipt(workspacePath), currentHead, completionReceiptMounts)) {
1323
+ throw new Error(`Workspace inbound integration ${currentHead} could not be fully hydrated before publication continued`);
1324
+ }
1325
+ }
1326
+ return {
1327
+ activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
1328
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1329
+ };
1330
+ };
1331
+ let newlyStagedPaths = [];
1332
+ const stageAndCommitWorkspace = () => {
1333
+ git(workspacePath, ["add", "-A", "--", "."], "stage workspace working trees");
1334
+ newlyStagedPaths = stagedPaths(workspacePath);
1335
+ if (newlyStagedPaths.length > 0) {
1336
+ git(
1337
+ workspacePath,
1338
+ [
1339
+ "commit",
1340
+ "-m",
1341
+ JSON.stringify({ type: "workspace_sync", worker: input.workerLabel, attemptId, detail: input.commitDetail ?? null })
1342
+ ],
1343
+ "commit workspace working trees"
1344
+ );
1345
+ }
1346
+ };
393
1347
  if (!input.skipMountMirror) {
394
- mirrorMountsToWorkspace(workspacePath, selected.active);
395
- removeWorkspaceMounts(workspacePath, selected.tombstones);
1348
+ beginWorkspaceCheckoutTransition(workspacePath);
1349
+ try {
1350
+ mirrorMountsToWorkspace(workspacePath, projectedActiveMounts);
1351
+ removeWorkspaceMounts(workspacePath, projectedTombstones);
1352
+ restoreDeferredWorkspaceMountsFromHead(
1353
+ workspacePath,
1354
+ input.mounts.filter(({ id }) => deferredMountIds.has(id))
1355
+ );
1356
+ stageAndCommitWorkspace();
1357
+ fsyncWorkspaceCheckoutTree(workspacePath);
1358
+ } catch (error) {
1359
+ throwAfterWorkspaceCheckoutRecovery(workspacePath, error);
1360
+ }
1361
+ } else {
1362
+ stageAndCommitWorkspace();
1363
+ if (newlyStagedPaths.length > 0) completeWorkspaceCheckoutTransition(workspacePath);
396
1364
  }
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
- );
1365
+ if (!input.skipMountMirror) {
1366
+ const projectedHead = revParse(workspacePath, "HEAD");
1367
+ if (!projectedHead) throw new Error("Workspace projection did not retain a local HEAD");
1368
+ const targetProjectionReceipt = workspaceHydrationReceipt(projectedHead, projectionReceiptMounts);
1369
+ if (!workspaceHydrationReceiptsEqual(readHydratedWorkspaceReceipt(workspacePath), targetProjectionReceipt)) {
1370
+ fsyncHydratedWorkspaceMounts([...projectedActiveMounts, ...projectedTombstones]);
1371
+ updateHydratedWorkspaceReceipt(workspacePath, targetProjectionReceipt);
1372
+ }
1373
+ completeWorkspaceCheckoutTransition(workspacePath);
409
1374
  }
410
1375
  let remoteHead = fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
411
1376
  let rebaseCount = 0;
@@ -423,8 +1388,8 @@ async function synchronizeWorkspaceGit(input) {
423
1388
  rebaseCount: rebaseCount + 1,
424
1389
  diffSizeBytes: 0,
425
1390
  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(),
1391
+ activeMountIds: [...projectedActiveMounts, ...projectedTombstones].map(({ id }) => id).sort(),
1392
+ skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...deferredMountIds])].sort(),
428
1393
  conflictPaths: reconciled.conflictPaths,
429
1394
  conflictSnapshotRefs: reconciled.refs,
430
1395
  error: reconciled.error
@@ -432,6 +1397,7 @@ async function synchronizeWorkspaceGit(input) {
432
1397
  }
433
1398
  updated ||= reconciled.updated;
434
1399
  if (reconciled.rebased) rebaseCount += 1;
1400
+ if (reconciled.updated || reconciled.rebased) completedMountSelection(true);
435
1401
  const localHead = revParse(workspacePath, "HEAD");
436
1402
  if (!localHead) {
437
1403
  return {
@@ -465,11 +1431,11 @@ async function synchronizeWorkspaceGit(input) {
465
1431
  };
466
1432
  }
467
1433
  if (localHead === remoteHead) {
1434
+ const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
468
1435
  await input.afterWorkspacePublished?.({
469
1436
  publishedHead: localHead,
470
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort()
1437
+ activeMountIds: completedMounts.activeMountIds
471
1438
  });
472
- if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
473
1439
  return {
474
1440
  outcome: updated ? "updated" : "no_change",
475
1441
  startingHead,
@@ -479,8 +1445,7 @@ async function synchronizeWorkspaceGit(input) {
479
1445
  rebaseCount,
480
1446
  diffSizeBytes: size,
481
1447
  affectedPaths: paths,
482
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
483
- skippedMountIds: selected.skipped.map(({ id }) => id).sort()
1448
+ ...completedMounts
484
1449
  };
485
1450
  }
486
1451
  const pushArgs = [
@@ -493,11 +1458,11 @@ async function synchronizeWorkspaceGit(input) {
493
1458
  const push = gitResult(workspacePath, pushArgs);
494
1459
  if (push.exitCode === 0) {
495
1460
  updateIntegratedWorkspaceHead(workspacePath, localHead);
1461
+ const completedMounts = completedMountSelection(Boolean(input.skipMountMirror));
496
1462
  await input.afterWorkspacePublished?.({
497
1463
  publishedHead: localHead,
498
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort()
1464
+ activeMountIds: completedMounts.activeMountIds
499
1465
  });
500
- if (!input.skipMountHydration) hydrateWorkspaceGitMounts(workspacePath, selected.active);
501
1466
  return {
502
1467
  outcome: "pushed",
503
1468
  startingHead,
@@ -507,8 +1472,7 @@ async function synchronizeWorkspaceGit(input) {
507
1472
  rebaseCount,
508
1473
  diffSizeBytes: size,
509
1474
  affectedPaths: paths,
510
- activeMountIds: [...selected.active, ...selected.tombstones].map(({ id }) => id).sort(),
511
- skippedMountIds: selected.skipped.map(({ id }) => id).sort()
1475
+ ...completedMounts
512
1476
  };
513
1477
  }
514
1478
  if (!/(non-fast-forward|fetch first|rejected|failed to push some refs)/i.test(`${push.stderr}
@@ -529,10 +1493,15 @@ const workspaceGitSyncTestHarness = {
529
1493
  export {
530
1494
  MAX_WORKSPACE_GIT_DIFF_BYTES,
531
1495
  WORKSPACE_GIT_BRANCH,
1496
+ WORKSPACE_GIT_CHECKOUT_DURABILITY,
532
1497
  WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION,
1498
+ WORKSPACE_GIT_HYDRATED_RECEIPT,
1499
+ WORKSPACE_GIT_HYDRATION_TRANSACTION,
533
1500
  ensureWorkspaceGitClone,
534
1501
  hydrateWorkspaceGitMounts,
1502
+ recoverWorkspaceGitHydration,
535
1503
  resetWorkspaceGit,
536
1504
  synchronizeWorkspaceGit,
1505
+ workspaceGitHydrationIsCurrent,
537
1506
  workspaceGitSyncTestHarness
538
1507
  };