@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
@@ -29,18 +29,26 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  var project_worktrees_exports = {};
30
30
  __export(project_worktrees_exports, {
31
31
  PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
32
- cleanupStaleProjectWorktreeSnapshots: () => cleanupStaleProjectWorktreeSnapshots,
32
+ ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
33
+ applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
34
+ assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
33
35
  createLinkedProjectBranch: () => createLinkedProjectBranch,
36
+ createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
34
37
  deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
35
38
  deleteProjectMirrorBranch: () => deleteProjectMirrorBranch,
36
39
  ensureProjectWorktrees: () => ensureProjectWorktrees,
37
40
  fastForwardProjectHeadsFromMirror: () => fastForwardProjectHeadsFromMirror,
38
41
  hasLinkedProjectWorktreeLayout: () => hasLinkedProjectWorktreeLayout,
42
+ observeProjectMirrorHeads: () => observeProjectMirrorHeads,
43
+ projectBranchMayExistAfterCreateFailure: () => projectBranchMayExistAfterCreateFailure,
44
+ projectOriginBranchName: () => projectOriginBranchName,
39
45
  projectWorktreeConfigurationFingerprint: () => projectWorktreeConfigurationFingerprint,
40
46
  projectWorktreeOperationInProgress: () => projectWorktreeOperationInProgress,
41
47
  projectWorktreesTestHarness: () => projectWorktreesTestHarness,
42
48
  pushProjectMirrorHeads: () => pushProjectMirrorHeads,
43
- removeProjectWorktrees: () => removeProjectWorktrees
49
+ recoverStaleProjectWorktreeSnapshots: () => recoverStaleProjectWorktreeSnapshots,
50
+ removeProjectWorktrees: () => removeProjectWorktrees,
51
+ reseedProjectHeadsFromMirror: () => reseedProjectHeadsFromMirror
44
52
  });
45
53
  module.exports = __toCommonJS(project_worktrees_exports);
46
54
  var import_node_crypto = require("node:crypto");
@@ -51,9 +59,26 @@ var import_git_process_environment = require("./git-process-environment.cjs");
51
59
  var import_managed_paths = require("./managed-paths.cjs");
52
60
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
53
61
  const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
62
+ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
63
+ const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
64
+ const MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES = 64 * 1024;
65
+ const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID = (0, import_node_crypto.randomUUID)();
66
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
54
67
  const PROJECT_WORKTREE_SNAPSHOT_NAME = new RegExp(
55
68
  `^${PROJECT_WORKTREE_SNAPSHOT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([1-9]\\d*)-([A-Za-z0-9]{6})$`
56
69
  );
70
+ class ProjectBranchCreationRollbackIncompleteError extends Error {
71
+ branchMayExist = true;
72
+ cleanupFailures;
73
+ constructor(branchName, cleanupFailures, cause) {
74
+ super(`Project branch ${branchName} may still exist after incomplete rollback: ${cleanupFailures.join("; ")}`, { cause });
75
+ this.name = "ProjectBranchCreationRollbackIncompleteError";
76
+ this.cleanupFailures = [...cleanupFailures];
77
+ }
78
+ }
79
+ function projectBranchMayExistAfterCreateFailure(error) {
80
+ return error instanceof ProjectBranchCreationRollbackIncompleteError || typeof error === "object" && error !== null && "branchMayExist" in error && error.branchMayExist === true;
81
+ }
57
82
  function projectWorktreeConfigurationFingerprint(input) {
58
83
  return (0, import_node_crypto.createHash)("sha256").update(
59
84
  JSON.stringify({
@@ -62,7 +87,12 @@ function projectWorktreeConfigurationFingerprint(input) {
62
87
  repoHttpUrl: input.repoHttpUrl,
63
88
  repoAuthHeader: input.repoAuthHeader,
64
89
  defaultBranch: input.defaultBranch,
65
- branches: [...input.branches].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchName, sourceBranchName, baseCommitHash }) => ({
90
+ repositoryTransitionId: input.repositoryTransitionId ?? null,
91
+ executionDisabled: input.executionDisabled ?? false,
92
+ mirrorWritesDisabled: input.mirrorWritesDisabled ?? false,
93
+ preserveOnlyBranches: [...input.preserveOnlyBranches ?? []].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchId, branchName }) => ({ branchId: branchId ?? null, branchName })),
94
+ branches: [...input.branches].sort((left, right) => left.branchName.localeCompare(right.branchName)).map(({ branchId, branchName, sourceBranchName, baseCommitHash }) => ({
95
+ branchId: branchId ?? null,
66
96
  branchName,
67
97
  sourceBranchName: sourceBranchName ?? null,
68
98
  baseCommitHash
@@ -112,6 +142,27 @@ function branchPath(projectRoot, branchName) {
112
142
  }
113
143
  return result;
114
144
  }
145
+ function assertNoSymlinkBranchPathComponents(projectRoot, branchPath2) {
146
+ const resolvedRoot = import_node_path.default.resolve(projectRoot);
147
+ const resolvedBranchPath = import_node_path.default.resolve(branchPath2);
148
+ const relative = import_node_path.default.relative(resolvedRoot, resolvedBranchPath);
149
+ if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
150
+ throw new Error(`Branch path escapes its project root: ${branchPath2}`);
151
+ }
152
+ let current = resolvedRoot;
153
+ for (const component of ["", ...relative.split(import_node_path.default.sep).filter(Boolean)]) {
154
+ if (component) current = import_node_path.default.join(current, component);
155
+ try {
156
+ if (import_node_fs.default.lstatSync(current).isSymbolicLink()) {
157
+ throw new Error(`Project branch path contains a symbolic link: ${current}`);
158
+ }
159
+ } catch (error) {
160
+ const code = error.code;
161
+ if (code === "ENOENT" || code === "ENOTDIR") return;
162
+ throw error;
163
+ }
164
+ }
165
+ }
115
166
  function assertNonOverlappingBranches(branches) {
116
167
  const names = branches.map(({ branchName }) => branchName).sort();
117
168
  if (new Set(names).size !== names.length) throw new Error("Project worktree branch names must be unique");
@@ -164,26 +215,309 @@ function hasCompatibleLinkedProjectWorktreeLayout(input) {
164
215
  return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
165
216
  });
166
217
  }
167
- function createProjectWorktreeSnapshotRoot() {
168
- return import_node_fs.default.mkdtempSync(import_node_path.default.join(import_node_os.default.tmpdir(), `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
218
+ function fsyncDirectory(directory) {
219
+ const descriptor = import_node_fs.default.openSync(directory, "r");
220
+ try {
221
+ import_node_fs.default.fsyncSync(descriptor);
222
+ } finally {
223
+ import_node_fs.default.closeSync(descriptor);
224
+ }
225
+ }
226
+ function fsyncTree(targetPath) {
227
+ const stat = import_node_fs.default.lstatSync(targetPath);
228
+ if (stat.isSymbolicLink()) return;
229
+ if (stat.isDirectory()) {
230
+ for (const entry of import_node_fs.default.readdirSync(targetPath).sort()) fsyncTree(import_node_path.default.join(targetPath, entry));
231
+ fsyncDirectory(targetPath);
232
+ return;
233
+ }
234
+ if (!stat.isFile()) return;
235
+ const descriptor = import_node_fs.default.openSync(targetPath, "r");
236
+ try {
237
+ import_node_fs.default.fsyncSync(descriptor);
238
+ } finally {
239
+ import_node_fs.default.closeSync(descriptor);
240
+ }
241
+ }
242
+ function fsyncDirectoryChain(directory, root) {
243
+ const resolvedDirectory = import_node_path.default.resolve(directory);
244
+ const resolvedRoot = import_node_path.default.resolve(root);
245
+ const relative = import_node_path.default.relative(resolvedRoot, resolvedDirectory);
246
+ if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
247
+ throw new Error(`Directory is outside its durability root: ${resolvedDirectory}`);
248
+ }
249
+ let current = resolvedDirectory;
250
+ while (true) {
251
+ assertRealDirectory(current, "Durability path");
252
+ fsyncDirectory(current);
253
+ if (current === resolvedRoot) break;
254
+ current = import_node_path.default.dirname(current);
255
+ }
256
+ }
257
+ function removeSnapshotStorage(snapshotRoot) {
258
+ const storageRoot = import_node_path.default.dirname(import_node_path.default.resolve(snapshotRoot));
259
+ import_node_fs.default.rmSync(snapshotRoot, { recursive: true, force: true });
260
+ fsyncDirectory(storageRoot);
261
+ }
262
+ function fsyncManagedProjectRootEntry(projectRoot) {
263
+ fsyncDirectoryChain(projectRoot, import_node_path.default.dirname(import_node_path.default.dirname(projectRoot)));
264
+ }
265
+ function createProjectWorktreeSnapshotRoot(temporaryRoot = import_node_os.default.tmpdir()) {
266
+ const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot);
267
+ const storageRootExisted = import_node_fs.default.existsSync(resolvedTemporaryRoot);
268
+ import_node_fs.default.mkdirSync(resolvedTemporaryRoot, { recursive: true, mode: 448 });
269
+ assertRealDirectory(resolvedTemporaryRoot, "Project worktree snapshot storage root");
270
+ if (!storageRootExisted) fsyncDirectory(import_node_path.default.dirname(resolvedTemporaryRoot));
271
+ fsyncDirectory(resolvedTemporaryRoot);
272
+ const snapshotRoot = import_node_fs.default.mkdtempSync(import_node_path.default.join(resolvedTemporaryRoot, `${PROJECT_WORKTREE_SNAPSHOT_PREFIX}${process.pid}-`));
273
+ fsyncDirectory(snapshotRoot);
274
+ fsyncDirectory(resolvedTemporaryRoot);
275
+ return snapshotRoot;
276
+ }
277
+ function snapshotManifestPath(snapshotRoot) {
278
+ return import_node_path.default.join(snapshotRoot, PROJECT_WORKTREE_SNAPSHOT_MANIFEST);
279
+ }
280
+ function assertRealDirectory(directory, label) {
281
+ const stat = import_node_fs.default.lstatSync(directory);
282
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} must be a real directory`);
283
+ }
284
+ function normalizeSnapshotManifest(value) {
285
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
286
+ throw new Error("Project worktree snapshot manifest must be an object");
287
+ }
288
+ const record = value;
289
+ if (record.version !== PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION) {
290
+ throw new Error("Unsupported project worktree snapshot manifest version");
291
+ }
292
+ if (record.state !== "building" && record.state !== "prepared" && record.state !== "consumed") {
293
+ throw new Error("Project worktree snapshot manifest has an invalid state");
294
+ }
295
+ if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
296
+ throw new Error("Project worktree snapshot manifest has an invalid owner process");
297
+ }
298
+ if (typeof record.ownerSessionId !== "string" || !UUID_PATTERN.test(record.ownerSessionId)) {
299
+ throw new Error("Project worktree snapshot manifest has an invalid owner session");
300
+ }
301
+ if (typeof record.projectRoot !== "string" || !import_node_path.default.isAbsolute(record.projectRoot) || import_node_path.default.resolve(record.projectRoot) !== record.projectRoot) {
302
+ throw new Error("Project worktree snapshot manifest has an invalid project root");
303
+ }
304
+ if (typeof record.createdAt !== "string" || !Number.isFinite(Date.parse(record.createdAt))) {
305
+ throw new Error("Project worktree snapshot manifest has an invalid creation time");
306
+ }
307
+ if (!Array.isArray(record.branches)) throw new Error("Project worktree snapshot manifest has invalid branches");
308
+ const branches = record.branches.map((value2) => {
309
+ if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) {
310
+ throw new Error("Project worktree snapshot manifest has an invalid branch");
311
+ }
312
+ const branch = value2;
313
+ if (typeof branch.branchName !== "string" || typeof branch.snapshotPresent !== "boolean") {
314
+ throw new Error("Project worktree snapshot manifest has an invalid branch");
315
+ }
316
+ (0, import_managed_paths.validateManagedBranchName)(branch.branchName);
317
+ return { branchName: branch.branchName, snapshotPresent: branch.snapshotPresent };
318
+ });
319
+ const names = branches.map(({ branchName }) => branchName).sort();
320
+ if (new Set(names).size !== names.length) throw new Error("Project worktree snapshot branch names must be unique");
321
+ for (let index = 0; index < names.length; index += 1) {
322
+ const name = names[index];
323
+ if (names.slice(index + 1).some((candidate) => candidate.startsWith(`${name}/`))) {
324
+ throw new Error("Project worktree snapshot branch paths overlap");
325
+ }
326
+ }
327
+ return {
328
+ version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
329
+ state: record.state,
330
+ ownerProcessId: record.ownerProcessId,
331
+ ownerSessionId: record.ownerSessionId,
332
+ projectRoot: record.projectRoot,
333
+ branches,
334
+ createdAt: record.createdAt
335
+ };
336
+ }
337
+ function readSnapshotManifest(snapshotRoot) {
338
+ assertRealDirectory(snapshotRoot, "Project worktree snapshot root");
339
+ const manifestPath = snapshotManifestPath(snapshotRoot);
340
+ const stat = import_node_fs.default.lstatSync(manifestPath);
341
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Project worktree snapshot manifest must be a regular file");
342
+ if (stat.size > MAX_PROJECT_WORKTREE_SNAPSHOT_MANIFEST_BYTES) throw new Error("Project worktree snapshot manifest is too large");
343
+ return normalizeSnapshotManifest(JSON.parse(import_node_fs.default.readFileSync(manifestPath, "utf8")));
344
+ }
345
+ function writeSnapshotManifest(snapshotRoot, manifest) {
346
+ assertRealDirectory(snapshotRoot, "Project worktree snapshot root");
347
+ const normalized = normalizeSnapshotManifest(manifest);
348
+ const temporaryPath = import_node_path.default.join(snapshotRoot, `.${PROJECT_WORKTREE_SNAPSHOT_MANIFEST}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
349
+ let descriptor;
350
+ try {
351
+ descriptor = import_node_fs.default.openSync(temporaryPath, "wx", 384);
352
+ import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify(normalized, null, 2)}
353
+ `, "utf8");
354
+ import_node_fs.default.fsyncSync(descriptor);
355
+ import_node_fs.default.closeSync(descriptor);
356
+ descriptor = void 0;
357
+ import_node_fs.default.renameSync(temporaryPath, snapshotManifestPath(snapshotRoot));
358
+ fsyncDirectory(snapshotRoot);
359
+ } finally {
360
+ if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
361
+ import_node_fs.default.rmSync(temporaryPath, { force: true });
362
+ }
363
+ return readSnapshotManifest(snapshotRoot);
364
+ }
365
+ function assertSameSnapshotIdentity(expected, actual) {
366
+ if (expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
367
+ throw new Error("Project worktree snapshot identity changed");
368
+ }
369
+ }
370
+ function advanceSnapshotState(snapshotRoot, expected, state) {
371
+ const current = readSnapshotManifest(snapshotRoot);
372
+ assertSameSnapshotIdentity(expected, current);
373
+ const allowed = current.state === "building" && state === "prepared" || current.state === "prepared" && state === "consumed" || current.state === state;
374
+ if (!allowed) throw new Error(`Project worktree snapshot cannot advance from ${current.state} to ${state}`);
375
+ return current.state === state ? current : writeSnapshotManifest(snapshotRoot, { ...current, state });
376
+ }
377
+ function fullSnapshotPath(snapshotRoot, branchName) {
378
+ (0, import_managed_paths.validateManagedBranchName)(branchName);
379
+ return import_node_path.default.join(snapshotRoot, "trees", ...branchName.split("/"));
169
380
  }
170
- function snapshotBranchTrees(projectRoot, branches) {
171
- const root = createProjectWorktreeSnapshotRoot();
381
+ function copyCheckoutTree(sourceRoot, targetRoot) {
382
+ const sourceStat = import_node_fs.default.lstatSync(sourceRoot);
383
+ if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
384
+ throw new Error(`Project checkout snapshot source must be a real directory: ${sourceRoot}`);
385
+ }
386
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetRoot), { recursive: true });
387
+ import_node_fs.default.cpSync(sourceRoot, targetRoot, {
388
+ recursive: true,
389
+ dereference: false,
390
+ errorOnExist: true,
391
+ force: false,
392
+ preserveTimestamps: true,
393
+ verbatimSymlinks: true
394
+ });
395
+ }
396
+ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
397
+ let entries;
398
+ try {
399
+ assertRealDirectory(temporaryRoot, "Project worktree snapshot storage root");
400
+ entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
401
+ } catch (error) {
402
+ if (error.code === "ENOENT") return;
403
+ throw error;
404
+ }
405
+ for (const entry of entries) {
406
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !PROJECT_WORKTREE_SNAPSHOT_NAME.test(entry.name)) continue;
407
+ let manifest;
408
+ try {
409
+ manifest = readSnapshotManifest(import_node_path.default.join(temporaryRoot, entry.name));
410
+ } catch (error) {
411
+ throw new Error(`Project checkout snapshot state is unreadable at ${import_node_path.default.join(temporaryRoot, entry.name)}`, { cause: error });
412
+ }
413
+ if (manifest.projectRoot === projectRoot && manifest.state !== "consumed") {
414
+ throw new Error(`Project checkout has an unfinished recovery snapshot at ${import_node_path.default.join(temporaryRoot, entry.name)}`);
415
+ }
416
+ }
417
+ }
418
+ function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
419
+ const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
420
+ const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
421
+ assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
422
+ const root = createProjectWorktreeSnapshotRoot(resolvedTemporaryRoot);
172
423
  const paths = /* @__PURE__ */ new Map();
173
424
  try {
174
- for (const { branchName } of branches) {
175
- const checkoutPath = branchPath(projectRoot, branchName);
176
- if (!import_node_fs.default.existsSync(checkoutPath)) continue;
177
- const snapshotPath = import_node_path.default.join(root, ...branchName.split("/"));
178
- (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: checkoutPath, targetRoot: snapshotPath, sourceMode: "all", deletionMode: "all" });
425
+ const branchSnapshots = branches.map(({ branchName }) => {
426
+ const checkoutPath = branchPath(resolvedProjectRoot, branchName);
427
+ let snapshotPresent = false;
428
+ try {
429
+ import_node_fs.default.lstatSync(checkoutPath);
430
+ snapshotPresent = true;
431
+ } catch (error) {
432
+ if (error.code !== "ENOENT") throw error;
433
+ }
434
+ return { branchName, snapshotPresent };
435
+ });
436
+ let manifest = writeSnapshotManifest(root, {
437
+ version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
438
+ state: "building",
439
+ ownerProcessId: process.pid,
440
+ ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
441
+ projectRoot: resolvedProjectRoot,
442
+ branches: branchSnapshots,
443
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
444
+ });
445
+ for (const { branchName, snapshotPresent } of branchSnapshots) {
446
+ if (!snapshotPresent) continue;
447
+ const snapshotPath = fullSnapshotPath(root, branchName);
448
+ copyCheckoutTree(branchPath(resolvedProjectRoot, branchName), snapshotPath);
179
449
  paths.set(branchName, snapshotPath);
180
450
  }
181
- return { root, paths };
451
+ const treesRoot = import_node_path.default.join(root, "trees");
452
+ if (import_node_fs.default.existsSync(treesRoot)) fsyncTree(treesRoot);
453
+ manifest = advanceSnapshotState(root, manifest, "prepared");
454
+ return { root, manifest, paths };
182
455
  } catch (error) {
183
- import_node_fs.default.rmSync(root, { recursive: true, force: true });
456
+ removeSnapshotStorage(root);
184
457
  throw error;
185
458
  }
186
459
  }
460
+ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest) {
461
+ const current = readSnapshotManifest(snapshotRoot);
462
+ assertSameSnapshotIdentity(manifest, current);
463
+ if (current.state !== "prepared") throw new Error(`Project worktree snapshot is ${current.state}, not prepared`);
464
+ for (const { branchName, snapshotPresent } of current.branches) {
465
+ const targetPath = branchPath(current.projectRoot, branchName);
466
+ assertNoSymlinkBranchPathComponents(current.projectRoot, targetPath);
467
+ if (!snapshotPresent) continue;
468
+ const sourcePath = fullSnapshotPath(snapshotRoot, branchName);
469
+ const stat = import_node_fs.default.lstatSync(sourcePath);
470
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
471
+ throw new Error(`Project worktree snapshot branch must be a real directory: ${branchName}`);
472
+ }
473
+ }
474
+ for (const { branchName } of [...current.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
475
+ const targetPath = branchPath(current.projectRoot, branchName);
476
+ const targetParent = import_node_path.default.dirname(targetPath);
477
+ import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
478
+ if (import_node_fs.default.existsSync(targetParent)) fsyncDirectoryChain(targetParent, current.projectRoot);
479
+ }
480
+ for (const { branchName, snapshotPresent } of current.branches) {
481
+ if (!snapshotPresent) continue;
482
+ const targetPath = branchPath(current.projectRoot, branchName);
483
+ copyCheckoutTree(fullSnapshotPath(snapshotRoot, branchName), targetPath);
484
+ fsyncTree(targetPath);
485
+ fsyncDirectoryChain(import_node_path.default.dirname(targetPath), current.projectRoot);
486
+ }
487
+ import_node_fs.default.mkdirSync(current.projectRoot, { recursive: true });
488
+ fsyncManagedProjectRootEntry(current.projectRoot);
489
+ }
490
+ function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
491
+ const consumed = advanceSnapshotState(snapshotRoot, manifest, "consumed");
492
+ try {
493
+ assertSameSnapshotIdentity(manifest, consumed);
494
+ removeSnapshotStorage(snapshotRoot);
495
+ } catch {
496
+ }
497
+ }
498
+ function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
499
+ fsyncTree(snapshot.manifest.projectRoot);
500
+ fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
501
+ afterProjectTreeFsync?.();
502
+ consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
503
+ }
504
+ function rollbackProjectWorktreeSnapshot(snapshot) {
505
+ const current = readSnapshotManifest(snapshot.root);
506
+ assertSameSnapshotIdentity(snapshot.manifest, current);
507
+ if (current.state === "prepared") {
508
+ restoreProjectWorktreeSnapshot(snapshot.root, current);
509
+ consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, current);
510
+ return;
511
+ }
512
+ if (current.state === "consumed") {
513
+ try {
514
+ removeSnapshotStorage(snapshot.root);
515
+ } catch {
516
+ }
517
+ return;
518
+ }
519
+ throw new Error(`Project worktree snapshot is unexpectedly ${current.state}`);
520
+ }
187
521
  function isProcessAlive(processId) {
188
522
  try {
189
523
  process.kill(processId, 0);
@@ -192,35 +526,105 @@ function isProcessAlive(processId) {
192
526
  return error.code !== "ESRCH";
193
527
  }
194
528
  }
195
- function cleanupStaleProjectWorktreeSnapshots(input = {}) {
529
+ function isPathInside(root, candidate) {
530
+ const relative = import_node_path.default.relative(import_node_path.default.resolve(root), import_node_path.default.resolve(candidate));
531
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
532
+ }
533
+ function assertNoSymlinkPathComponents(root, candidate) {
534
+ const resolvedRoot = import_node_path.default.resolve(root);
535
+ const resolvedCandidate = import_node_path.default.resolve(candidate);
536
+ if (!isPathInside(resolvedRoot, resolvedCandidate)) throw new Error("Project worktree snapshot target escapes the projects root");
537
+ let current = resolvedRoot;
538
+ for (const component of ["", ...import_node_path.default.relative(resolvedRoot, resolvedCandidate).split(import_node_path.default.sep).filter(Boolean)]) {
539
+ if (component) current = import_node_path.default.join(current, component);
540
+ try {
541
+ if (import_node_fs.default.lstatSync(current).isSymbolicLink()) {
542
+ throw new Error(`Project worktree snapshot target contains a symbolic link: ${current}`);
543
+ }
544
+ } catch (error) {
545
+ const code = error.code;
546
+ if (code === "ENOENT" || code === "ENOTDIR") return;
547
+ throw error;
548
+ }
549
+ }
550
+ }
551
+ function recoverStaleProjectWorktreeSnapshots(input) {
196
552
  const temporaryRoot = import_node_path.default.resolve(input.temporaryRoot ?? import_node_os.default.tmpdir());
553
+ const projectsRoot = import_node_path.default.resolve(input.projectsRoot);
197
554
  const processAlive = input.processAlive ?? isProcessAlive;
555
+ const currentProcessId = input.currentProcessId ?? process.pid;
556
+ const currentOwnerSessionId = input.currentOwnerSessionId ?? PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID;
557
+ if (!Number.isSafeInteger(currentProcessId) || currentProcessId <= 0) throw new Error("Current snapshot recovery process ID is invalid");
558
+ if (!UUID_PATTERN.test(currentOwnerSessionId)) throw new Error("Current snapshot recovery owner session is invalid");
559
+ const restored = [];
198
560
  const removed = [];
199
561
  const failed = [];
200
562
  let entries;
201
563
  try {
564
+ assertRealDirectory(temporaryRoot, "Project worktree snapshot storage root");
202
565
  entries = import_node_fs.default.readdirSync(temporaryRoot, { withFileTypes: true });
203
566
  } catch (error) {
204
- if (error.code === "ENOENT") return { removed, failed };
205
- return { removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
567
+ if (error.code === "ENOENT") return { restored, removed, failed };
568
+ return { restored, removed, failed: [{ path: temporaryRoot, error: error instanceof Error ? error.message : String(error) }] };
206
569
  }
207
570
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
208
571
  const match = PROJECT_WORKTREE_SNAPSHOT_NAME.exec(entry.name);
209
572
  if (!match) continue;
210
573
  const ownerProcessId = Number(match[1]);
211
- if (!Number.isSafeInteger(ownerProcessId) || processAlive(ownerProcessId)) continue;
574
+ if (!Number.isSafeInteger(ownerProcessId)) continue;
212
575
  const candidate = import_node_path.default.join(temporaryRoot, entry.name);
213
576
  try {
214
577
  const stat = import_node_fs.default.lstatSync(candidate);
215
578
  if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
216
- import_node_fs.default.rmSync(candidate, { recursive: true, force: true });
217
- removed.push(candidate);
579
+ const manifest = readSnapshotManifest(candidate);
580
+ if (manifest.ownerProcessId !== ownerProcessId) {
581
+ throw new Error("Project worktree snapshot owner does not match its directory name");
582
+ }
583
+ if (!isPathInside(projectsRoot, manifest.projectRoot)) {
584
+ throw new Error("Project worktree snapshot target is outside the configured projects root");
585
+ }
586
+ assertNoSymlinkPathComponents(projectsRoot, manifest.projectRoot);
587
+ const ownedByThisProcessSession = manifest.ownerProcessId === currentProcessId && manifest.ownerSessionId === currentOwnerSessionId;
588
+ if (ownedByThisProcessSession || ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
589
+ if (manifest.state === "prepared") {
590
+ restoreProjectWorktreeSnapshot(candidate, manifest);
591
+ const consumed = advanceSnapshotState(candidate, manifest, "consumed");
592
+ removeSnapshotStorage(candidate);
593
+ assertSameSnapshotIdentity(manifest, consumed);
594
+ restored.push(candidate);
595
+ } else {
596
+ removeSnapshotStorage(candidate);
597
+ removed.push(candidate);
598
+ }
218
599
  } catch (error) {
219
- if (error.code === "ENOENT") continue;
600
+ if (ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
601
+ if (error.code === "ENOENT") {
602
+ try {
603
+ import_node_fs.default.lstatSync(candidate);
604
+ } catch (candidateError) {
605
+ if (candidateError.code === "ENOENT") continue;
606
+ }
607
+ }
220
608
  failed.push({ path: candidate, error: error instanceof Error ? error.message : String(error) });
221
609
  }
222
610
  }
223
- return { removed, failed };
611
+ return { restored, removed, failed };
612
+ }
613
+ function projectOriginBranchName(input) {
614
+ (0, import_managed_paths.validateManagedBranchName)(input.branchName);
615
+ (0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
616
+ (0, import_managed_paths.validateManagedBranchName)(input.defaultBranch);
617
+ return input.branchName === input.primaryBranchName ? input.defaultBranch : input.branchName;
618
+ }
619
+ function configureProjectBranchTracking(checkoutPath, branchName, originBranchName) {
620
+ (0, import_managed_paths.validateManagedBranchName)(branchName);
621
+ (0, import_managed_paths.validateManagedBranchName)(originBranchName);
622
+ git(checkoutPath, ["config", "--local", `branch.${branchName}.remote`, "origin"], `configure origin for ${branchName}`);
623
+ git(
624
+ checkoutPath,
625
+ ["config", "--local", `branch.${branchName}.merge`, `refs/heads/${originBranchName}`],
626
+ `configure upstream branch for ${branchName}`
627
+ );
224
628
  }
225
629
  function configureRepository(input) {
226
630
  if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
@@ -252,6 +656,17 @@ function configureRepository(input) {
252
656
  git(input.primaryPath, ["config", "--local", "user.name", name], "configure project Git user name");
253
657
  git(input.primaryPath, ["config", "--local", "user.email", email], "configure project Git user email");
254
658
  }
659
+ if (input.primaryBranchName && input.defaultBranch && input.branches) {
660
+ git(input.primaryPath, ["config", "--local", "push.default", "upstream"], "configure project push mapping");
661
+ for (const { branchName } of input.branches) {
662
+ const originBranchName = projectOriginBranchName({
663
+ branchName,
664
+ primaryBranchName: input.primaryBranchName,
665
+ defaultBranch: input.defaultBranch
666
+ });
667
+ configureProjectBranchTracking(input.primaryPath, branchName, originBranchName);
668
+ }
669
+ }
255
670
  }
256
671
  function fetchProjectHeads(input) {
257
672
  const originFetch = gitResult(input.primaryPath, [
@@ -301,6 +716,9 @@ function ensureCommitAvailable(input) {
301
716
  function seedForBranch(input) {
302
717
  const mirrorRef = `refs/r5d/mirror/${input.branch.branchName}`;
303
718
  const mirrorHead = tryGit(input.primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(input.primaryPath, ["rev-parse", mirrorRef], "resolve project mirror head") : null;
719
+ if (input.requireMirrorHead && !mirrorHead) {
720
+ throw new Error(`Exact hidden project mirror head is missing for ${input.branch.branchName}`);
721
+ }
304
722
  const seed = mirrorHead ?? input.branch.baseCommitHash;
305
723
  ensureCommitAvailable({ ...input, commitHash: seed });
306
724
  return { seed, mirrorHead };
@@ -322,7 +740,12 @@ function initializeLinkedLayout(input) {
322
740
  const seeds = /* @__PURE__ */ new Map();
323
741
  const mirrorHeads = /* @__PURE__ */ new Map();
324
742
  for (const branch of input.branches) {
325
- const resolved = seedForBranch({ ...input, primaryPath, branch });
743
+ const resolved = seedForBranch({
744
+ ...input,
745
+ primaryPath,
746
+ branch,
747
+ requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
748
+ });
326
749
  seeds.set(branch.branchName, resolved.seed);
327
750
  mirrorHeads.set(branch.branchName, resolved.mirrorHead);
328
751
  }
@@ -348,7 +771,12 @@ function reconcileLinkedLayout(input) {
348
771
  const mirrorHeads = /* @__PURE__ */ new Map();
349
772
  for (const branch of input.branches) {
350
773
  const checkoutPath = branchPath(input.projectRoot, branch.branchName);
351
- const resolved = seedForBranch({ ...input, primaryPath, branch });
774
+ const resolved = seedForBranch({
775
+ ...input,
776
+ primaryPath,
777
+ branch,
778
+ requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
779
+ });
352
780
  mirrorHeads.set(branch.branchName, resolved.mirrorHead);
353
781
  if (branch.branchName === input.primaryBranchName || commonGitDirectory(checkoutPath) === commonGitDirectory(primaryPath)) {
354
782
  preserveHead(checkoutPath);
@@ -362,8 +790,9 @@ function reconcileLinkedLayout(input) {
362
790
  }
363
791
  return mirrorHeads;
364
792
  }
365
- function aheadBehind(checkoutPath, branchName) {
366
- const originRef = `refs/remotes/origin/${branchName}`;
793
+ function aheadBehind(checkoutPath, branchName, primaryBranchName, defaultBranch) {
794
+ const originBranchName = projectOriginBranchName({ branchName, primaryBranchName, defaultBranch });
795
+ const originRef = `refs/remotes/origin/${originBranchName}`;
367
796
  if (!tryGit(checkoutPath, ["show-ref", "--verify", "--quiet", originRef])) return { ahead: null, behind: null };
368
797
  const output = git(checkoutPath, ["rev-list", "--left-right", "--count", `${originRef}...HEAD`], "measure project origin divergence");
369
798
  const [behind, ahead] = output.split(/\s+/).map(Number);
@@ -371,11 +800,12 @@ function aheadBehind(checkoutPath, branchName) {
371
800
  }
372
801
  function ensureProjectWorktrees(input) {
373
802
  const projectRoot = import_node_path.default.resolve(input.projectRoot);
803
+ const defaultBranch = input.defaultBranch ?? input.primaryBranchName;
374
804
  assertNonOverlappingBranches(input.branches);
375
805
  if (!input.branches.some(({ branchName }) => branchName === input.primaryBranchName)) {
376
806
  throw new Error(`Primary project branch ${input.primaryBranchName} is missing from the workspace configuration`);
377
807
  }
378
- const snapshot = snapshotBranchTrees(projectRoot, input.branches);
808
+ const snapshot = snapshotBranchTrees(projectRoot, input.branches, input.snapshotRoot);
379
809
  try {
380
810
  import_node_fs.default.mkdirSync(projectRoot, { recursive: true });
381
811
  const linked = hasCompatibleLinkedProjectWorktreeLayout({
@@ -383,7 +813,7 @@ function ensureProjectWorktrees(input) {
383
813
  primaryBranchName: input.primaryBranchName,
384
814
  branches: input.branches
385
815
  });
386
- const mirrorHeads = linked ? reconcileLinkedLayout({ ...input, projectRoot }) : initializeLinkedLayout({ ...input, projectRoot });
816
+ const mirrorHeads = linked ? reconcileLinkedLayout({ ...input, projectRoot, defaultBranch }) : initializeLinkedLayout({ ...input, projectRoot, defaultBranch });
387
817
  for (const branch of input.branches) {
388
818
  const snapshotPath = snapshot.paths.get(branch.branchName);
389
819
  if (!snapshotPath) continue;
@@ -394,9 +824,9 @@ function ensureProjectWorktrees(input) {
394
824
  deletionMode: "git"
395
825
  });
396
826
  }
397
- return input.branches.map((branch) => {
827
+ const states = input.branches.map((branch) => {
398
828
  const checkoutPath = branchPath(projectRoot, branch.branchName);
399
- const divergence = aheadBehind(checkoutPath, branch.branchName);
829
+ const divergence = aheadBehind(checkoutPath, branch.branchName, input.primaryBranchName, defaultBranch);
400
830
  return {
401
831
  branchName: branch.branchName,
402
832
  branchPath: checkoutPath,
@@ -405,8 +835,18 @@ function ensureProjectWorktrees(input) {
405
835
  ...divergence
406
836
  };
407
837
  }).sort((left, right) => left.branchName.localeCompare(right.branchName));
408
- } finally {
409
- import_node_fs.default.rmSync(snapshot.root, { recursive: true, force: true });
838
+ persistReconciledProjectAndConsumeSnapshot(snapshot);
839
+ return states;
840
+ } catch (error) {
841
+ try {
842
+ rollbackProjectWorktreeSnapshot(snapshot);
843
+ } catch (recoveryError) {
844
+ throw new Error(
845
+ `Project worktree reconciliation failed and its snapshot could not be fully restored; recovery state is retained at ${snapshot.root}`,
846
+ { cause: new AggregateError([error, recoveryError]) }
847
+ );
848
+ }
849
+ throw error;
410
850
  }
411
851
  }
412
852
  function projectWorktreeOperationInProgress(checkoutPath) {
@@ -424,6 +864,8 @@ function createLinkedProjectBranch(input) {
424
864
  if (input.sourceBranchName === input.branchName) throw new Error("Source and target branch names must differ");
425
865
  const sourcePath = branchPath(input.projectRoot, input.sourceBranchName);
426
866
  const targetPath = branchPath(input.projectRoot, input.branchName);
867
+ assertNoSymlinkBranchPathComponents(input.projectRoot, sourcePath);
868
+ assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
427
869
  if (!commonGitDirectory(sourcePath)) throw new Error(`Source worktree ${input.sourceBranchName} is unavailable`);
428
870
  if (projectWorktreeOperationInProgress(sourcePath)) {
429
871
  throw new Error(`Source worktree ${input.sourceBranchName} has an in-progress Git operation`);
@@ -439,25 +881,84 @@ function createLinkedProjectBranch(input) {
439
881
  });
440
882
  if (overlappingWorktree) throw new Error(`Project branch folder overlaps linked worktree ${overlappingWorktree}`);
441
883
  const sourceHead = git(sourcePath, ["rev-parse", "HEAD"], "resolve source branch head");
442
- const snapshotRoot = createProjectWorktreeSnapshotRoot();
884
+ let branchCreated = false;
443
885
  try {
444
- (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: snapshotRoot, sourceMode: "git", deletionMode: "all" });
445
886
  git(sourcePath, ["branch", input.branchName, sourceHead], `create project branch ${input.branchName}`);
887
+ branchCreated = true;
446
888
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
447
- try {
448
- git(sourcePath, ["worktree", "add", "--force", targetPath, input.branchName], `create linked worktree ${input.branchName}`);
449
- (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: snapshotRoot, targetRoot: targetPath, sourceMode: "all", deletionMode: "git" });
450
- } catch (error) {
451
- import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
452
- tryGit(sourcePath, ["worktree", "prune"]);
453
- tryGit(sourcePath, ["branch", "-D", input.branchName]);
454
- throw error;
455
- }
889
+ assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
890
+ git(
891
+ sourcePath,
892
+ ["worktree", "add", "--force", "--no-checkout", targetPath, input.branchName],
893
+ `create linked worktree ${input.branchName}`
894
+ );
895
+ configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
896
+ assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
897
+ git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
898
+ (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
456
899
  return { branchPath: targetPath, baseCommitHash: sourceHead };
457
- } finally {
458
- import_node_fs.default.rmSync(snapshotRoot, { recursive: true, force: true });
900
+ } catch (error) {
901
+ if (branchCreated) {
902
+ let removeError;
903
+ try {
904
+ import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
905
+ } catch (cleanupError) {
906
+ removeError = cleanupError;
907
+ }
908
+ const prune = gitResult(sourcePath, ["worktree", "prune"]);
909
+ const deleteBranch = gitResult(sourcePath, ["branch", "-D", input.branchName]);
910
+ const cleanupFailures = [];
911
+ try {
912
+ import_node_fs.default.lstatSync(targetPath);
913
+ cleanupFailures.push(`target path remains at ${targetPath}`);
914
+ } catch (verificationError) {
915
+ const code = verificationError.code;
916
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
917
+ cleanupFailures.push(
918
+ `could not verify target removal${removeError ? ` after ${removeError instanceof Error ? removeError.message : String(removeError)}` : ""}: ${verificationError instanceof Error ? verificationError.message : String(verificationError)}`
919
+ );
920
+ }
921
+ }
922
+ const worktrees = gitResult(sourcePath, ["worktree", "list", "--porcelain"]);
923
+ if (worktrees.exitCode !== 0) {
924
+ cleanupFailures.push(
925
+ `could not verify linked-worktree removal: ${worktrees.stderr || worktrees.stdout || `git exited ${worktrees.exitCode}`}`
926
+ );
927
+ } else {
928
+ const targetRegistered = worktrees.stdout.split(/\r?\n/).some((line) => line.startsWith("worktree ") && import_node_path.default.resolve(line.slice("worktree ".length)) === import_node_path.default.resolve(targetPath));
929
+ if (targetRegistered) {
930
+ cleanupFailures.push(
931
+ `linked worktree remains registered at ${targetPath}${prune.exitCode === 0 ? "" : ` (${prune.stderr || prune.stdout || `git exited ${prune.exitCode}`})`}`
932
+ );
933
+ }
934
+ }
935
+ const branchRef = gitResult(sourcePath, ["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`]);
936
+ if (branchRef.exitCode === 0) {
937
+ cleanupFailures.push(
938
+ `branch ref remains${deleteBranch.exitCode === 0 ? "" : ` (${deleteBranch.stderr || deleteBranch.stdout || `git exited ${deleteBranch.exitCode}`})`}`
939
+ );
940
+ } else if (branchRef.exitCode !== 1) {
941
+ cleanupFailures.push(
942
+ `could not verify branch ref removal: ${branchRef.stderr || branchRef.stdout || `git exited ${branchRef.exitCode}`}`
943
+ );
944
+ }
945
+ if (cleanupFailures.length > 0) {
946
+ throw new ProjectBranchCreationRollbackIncompleteError(input.branchName, cleanupFailures, error);
947
+ }
948
+ }
949
+ throw error;
459
950
  }
460
951
  }
952
+ function createOrRetryLinkedProjectBranch(input) {
953
+ if (input.pendingRetry) {
954
+ deleteLinkedProjectBranch({
955
+ projectRoot: input.projectRoot,
956
+ primaryBranchName: input.primaryBranchName,
957
+ branchName: input.branchName
958
+ });
959
+ }
960
+ return createLinkedProjectBranch(input);
961
+ }
461
962
  function deleteLinkedProjectBranch(input) {
462
963
  (0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
463
964
  (0, import_managed_paths.validateManagedBranchName)(input.branchName);
@@ -517,7 +1018,13 @@ function pushProjectMirrorHeads(input) {
517
1018
  for (const branchName of [...input.branchNames].sort()) {
518
1019
  if (input.onlyBranches && !input.onlyBranches.has(branchName)) continue;
519
1020
  const checkoutPath = branchPath(input.projectRoot, branchName);
520
- const head = git(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
1021
+ const head = input.publicationHeads ? input.publicationHeads.get(branchName) : git(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
1022
+ if (!head || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(head)) {
1023
+ throw new Error(`Missing exact publication head for project branch ${branchName}`);
1024
+ }
1025
+ if (!tryGit(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
1026
+ throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
1027
+ }
521
1028
  if (projectWorktreeOperationInProgress(checkoutPath)) {
522
1029
  results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
523
1030
  continue;
@@ -529,7 +1036,7 @@ function pushProjectMirrorHeads(input) {
529
1036
  "push",
530
1037
  "--no-recurse-submodules",
531
1038
  input.mirrorUrl,
532
- `+HEAD:refs/heads/${branchName}`
1039
+ `+${head}:refs/heads/${branchName}`
533
1040
  ],
534
1041
  `push hidden project mirror for ${branchName}`
535
1042
  );
@@ -537,7 +1044,13 @@ function pushProjectMirrorHeads(input) {
537
1044
  }
538
1045
  return results;
539
1046
  }
540
- function fastForwardProjectHeadsFromMirror(input) {
1047
+ function assertProjectMirrorHeadsPushed(results) {
1048
+ const skipped = results.find((result) => !result.pushed);
1049
+ if (skipped) {
1050
+ throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
1051
+ }
1052
+ }
1053
+ function observeProjectMirrorHeads(input) {
541
1054
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
542
1055
  const fetch = gitResult(primaryPath, [
543
1056
  ...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
@@ -554,31 +1067,103 @@ function fastForwardProjectHeadsFromMirror(input) {
554
1067
  const previousHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
555
1068
  const mirrorRef = `refs/r5d/mirror/${branchName}`;
556
1069
  const mirrorHead = tryGit(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
557
- if (!mirrorHead || mirrorHead === previousHead || projectWorktreeOperationInProgress(checkoutPath) || !tryGit(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead])) {
558
- return { branchName, previousHead, mirrorHead, fastForwarded: false };
1070
+ const shouldMove = Boolean(
1071
+ mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || tryGit(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
1072
+ );
1073
+ return {
1074
+ branchName,
1075
+ previousHead,
1076
+ mirrorHead,
1077
+ mode: input.allowNonFastForward ? "reseed" : "fast_forward",
1078
+ shouldMove
1079
+ };
1080
+ });
1081
+ }
1082
+ function applyObservedProjectMirrorHeads(input) {
1083
+ return [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName)).map((observation) => {
1084
+ const { branchName, mirrorHead } = observation;
1085
+ const checkoutPath = branchPath(input.projectRoot, branchName);
1086
+ const currentHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1087
+ if (!observation.shouldMove || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1088
+ return { branchName, previousHead: currentHead, mirrorHead, moved: false };
1089
+ }
1090
+ if (!tryGit(checkoutPath, ["cat-file", "-e", `${mirrorHead}^{commit}`])) {
1091
+ throw new Error(`Observed hidden mirror head is no longer available for ${branchName}: ${mirrorHead}`);
559
1092
  }
560
- git(checkoutPath, ["update-ref", `refs/heads/${branchName}`, mirrorHead, previousHead], `fast-forward project branch ${branchName}`);
561
- git(checkoutPath, ["reset", "--mixed", mirrorHead], `refresh project index ${branchName}`);
562
- return { branchName, previousHead, mirrorHead, fastForwarded: true };
1093
+ const mutationAllowed = () => !projectWorktreeOperationInProgress(checkoutPath) && input.branchMutationAllowed?.(branchName, checkoutPath) !== false;
1094
+ if (!mutationAllowed()) {
1095
+ return { branchName, previousHead: currentHead, mirrorHead, moved: false };
1096
+ }
1097
+ if (observation.mode === "reseed") {
1098
+ preserveHead(checkoutPath);
1099
+ if (!mutationAllowed()) return { branchName, previousHead: currentHead, mirrorHead, moved: false };
1100
+ }
1101
+ const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
1102
+ git(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
1103
+ git(checkoutPath, ["update-ref", `refs/heads/${branchName}`, mirrorHead, currentHead], `${action} project branch ${branchName}`);
1104
+ return { branchName, previousHead: currentHead, mirrorHead, moved: true };
1105
+ });
1106
+ }
1107
+ function updateProjectHeadsFromMirror(input) {
1108
+ const observations = observeProjectMirrorHeads(input);
1109
+ return applyObservedProjectMirrorHeads({
1110
+ projectRoot: input.projectRoot,
1111
+ primaryBranchName: input.primaryBranchName,
1112
+ observations,
1113
+ branchMutationAllowed: input.branchMutationAllowed
563
1114
  });
564
1115
  }
1116
+ function fastForwardProjectHeadsFromMirror(input) {
1117
+ return updateProjectHeadsFromMirror({ ...input, allowNonFastForward: false }).map(({ moved, ...state }) => ({
1118
+ ...state,
1119
+ fastForwarded: moved
1120
+ }));
1121
+ }
1122
+ function reseedProjectHeadsFromMirror(input) {
1123
+ return updateProjectHeadsFromMirror({ ...input, allowNonFastForward: true }).map(({ moved, ...state }) => ({
1124
+ ...state,
1125
+ reseeded: moved
1126
+ }));
1127
+ }
565
1128
  const projectWorktreesTestHarness = {
566
1129
  commandArgs: gitCommandArgs,
567
- configureRepository
1130
+ configureRepository,
1131
+ createPreparedSnapshot(input) {
1132
+ return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
1133
+ },
1134
+ consumeSnapshot(snapshotRoot) {
1135
+ const manifest = readSnapshotManifest(snapshotRoot);
1136
+ advanceSnapshotState(snapshotRoot, manifest, "consumed");
1137
+ },
1138
+ persistProjectAndConsumeSnapshot(snapshotRoot, afterProjectTreeFsync) {
1139
+ const manifest = readSnapshotManifest(snapshotRoot);
1140
+ persistReconciledProjectAndConsumeSnapshot({ root: snapshotRoot, manifest, paths: /* @__PURE__ */ new Map() }, afterProjectTreeFsync);
1141
+ },
1142
+ snapshotState(snapshotRoot) {
1143
+ return readSnapshotManifest(snapshotRoot).state;
1144
+ }
568
1145
  };
569
1146
  // Annotate the CommonJS export names for ESM import in node:
570
1147
  0 && (module.exports = {
571
1148
  PROJECT_WORKTREE_SNAPSHOT_PREFIX,
572
- cleanupStaleProjectWorktreeSnapshots,
1149
+ ProjectBranchCreationRollbackIncompleteError,
1150
+ applyObservedProjectMirrorHeads,
1151
+ assertProjectMirrorHeadsPushed,
573
1152
  createLinkedProjectBranch,
1153
+ createOrRetryLinkedProjectBranch,
574
1154
  deleteLinkedProjectBranch,
575
1155
  deleteProjectMirrorBranch,
576
1156
  ensureProjectWorktrees,
577
1157
  fastForwardProjectHeadsFromMirror,
578
1158
  hasLinkedProjectWorktreeLayout,
1159
+ observeProjectMirrorHeads,
1160
+ projectBranchMayExistAfterCreateFailure,
1161
+ projectOriginBranchName,
579
1162
  projectWorktreeConfigurationFingerprint,
580
1163
  projectWorktreeOperationInProgress,
581
1164
  projectWorktreesTestHarness,
582
1165
  pushProjectMirrorHeads,
583
- removeProjectWorktrees
1166
+ recoverStaleProjectWorktreeSnapshots,
1167
+ removeProjectWorktrees,
1168
+ reseedProjectHeadsFromMirror
584
1169
  });