@ricsam/r5d-worker 0.0.103 → 0.0.104

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.
package/dist/cjs/main.cjs CHANGED
@@ -3509,6 +3509,7 @@ async function startWorker(options) {
3509
3509
  let terminalReplayTimer;
3510
3510
  let workspaceSyncRequestsInFlight = 0;
3511
3511
  let lastWorkspaceSyncMirrorObservationMs = 0;
3512
+ let lastInventoriedMissingCheckouts;
3512
3513
  let sessionArtifactSyncRequestsInFlight = 0;
3513
3514
  let cliUpdateInProgress = false;
3514
3515
  let reloadAfterClose = false;
@@ -3566,8 +3567,15 @@ async function startWorker(options) {
3566
3567
  const primaryBranchName = primaryProjectBranch(project);
3567
3568
  const connection = projectConnection(project);
3568
3569
  const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
3569
- if (requireLocalBranch || hasProjectWorktree(sourcePath)) {
3570
- (0, import_project_worktrees.deleteLinkedProjectBranch)({ projectRoot, primaryBranchName, branchName });
3570
+ const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryBranchName);
3571
+ if (requireLocalBranch || import_node_fs.default.existsSync(sourcePath) && hasProjectWorktree(primaryPath)) {
3572
+ const deleted = (0, import_project_worktrees.deleteLinkedProjectBranch)({ projectRoot, primaryBranchName, branchName });
3573
+ if (deleted.movedAsidePath) {
3574
+ process.stderr.write(
3575
+ `[r5d-worker] set aside ${sourcePath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3576
+ `
3577
+ );
3578
+ }
3571
3579
  }
3572
3580
  const planSourcePath = import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"));
3573
3581
  import_node_fs.default.rmSync(planSourcePath, { recursive: true, force: true });
@@ -3640,11 +3648,17 @@ async function startWorker(options) {
3640
3648
  if (branchName === primaryBranchName) {
3641
3649
  throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
3642
3650
  }
3643
- (0, import_project_worktrees.deleteLinkedProjectBranch)({
3651
+ const deleted = (0, import_project_worktrees.deleteLinkedProjectBranch)({
3644
3652
  projectRoot: configuredProjectRoot(projectsRoot, project),
3645
3653
  primaryBranchName,
3646
3654
  branchName
3647
3655
  });
3656
+ if (deleted.movedAsidePath) {
3657
+ process.stderr.write(
3658
+ `[r5d-worker] set aside ${branchPath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3659
+ `
3660
+ );
3661
+ }
3648
3662
  import_node_fs.default.rmSync(import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
3649
3663
  readyProjectIds.delete(project.projectId);
3650
3664
  reconciledProjectConfigFingerprints.delete(project.projectId);
@@ -3813,6 +3827,40 @@ async function startWorker(options) {
3813
3827
  });
3814
3828
  };
3815
3829
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3830
+ const inventoryConfiguredProjectCheckouts = () => {
3831
+ const inventory = { projects: [], missingCheckouts: [] };
3832
+ for (const project of projectConfigById.values()) {
3833
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) {
3834
+ inventory.projects.push(project);
3835
+ continue;
3836
+ }
3837
+ const checkouts = (0, import_project_worktrees.inventoryProjectCheckouts)({
3838
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3839
+ primaryBranchName: primaryProjectBranch(project),
3840
+ branchNames: project.branches.map(({ branchName }) => branchName)
3841
+ });
3842
+ if (checkouts.missingCheckouts.length === 0) {
3843
+ inventory.projects.push(project);
3844
+ continue;
3845
+ }
3846
+ for (const missing of checkouts.missingCheckouts) inventory.missingCheckouts.push({ projectId: project.projectId, ...missing });
3847
+ const linked = new Set(checkouts.linkedBranchNames);
3848
+ inventory.projects.push({ ...project, branches: project.branches.filter(({ branchName }) => linked.has(branchName)) });
3849
+ }
3850
+ inventory.missingCheckouts.sort(
3851
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
3852
+ );
3853
+ return inventory;
3854
+ };
3855
+ const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
3856
+ const logMissingCheckouts = (inventory) => {
3857
+ if (inventory.missingCheckouts.length === 0) return;
3858
+ const described = inventory.missingCheckouts.map(({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`).join(", ");
3859
+ process.stderr.write(
3860
+ `[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
3861
+ `
3862
+ );
3863
+ };
3816
3864
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3817
3865
  const mounts = [];
3818
3866
  const creatorLocalIncarnations = new Map(
@@ -4091,9 +4139,9 @@ async function startWorker(options) {
4091
4139
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
4092
4140
  );
4093
4141
  };
4094
- const captureProjectHeadsForWorkspacePublication = async () => {
4142
+ const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
4095
4143
  const heads = /* @__PURE__ */ new Map();
4096
- for (const project of projectConfigById.values()) {
4144
+ for (const project of projects) {
4097
4145
  if (!readyProjectIds.has(project.projectId)) continue;
4098
4146
  for (const branch of project.branches) {
4099
4147
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
@@ -4103,9 +4151,9 @@ async function startWorker(options) {
4103
4151
  }
4104
4152
  return heads;
4105
4153
  };
4106
- const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4154
+ const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
4107
4155
  const active = new Set(activeMountIds);
4108
- for (const project of projectConfigById.values()) {
4156
+ for (const project of projects) {
4109
4157
  if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
4110
4158
  const changed = /* @__PURE__ */ new Map();
4111
4159
  for (const branch of project.branches) {
@@ -4205,7 +4253,7 @@ async function startWorker(options) {
4205
4253
  };
4206
4254
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4207
4255
  const observationStartedAt = Date.now();
4208
- const eligibleProjects = [...projectConfigById.values()].filter(
4256
+ const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
4209
4257
  (project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
4210
4258
  );
4211
4259
  try {
@@ -4315,7 +4363,7 @@ async function startWorker(options) {
4315
4363
  }
4316
4364
  }
4317
4365
  };
4318
- const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
4366
+ const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
4319
4367
  const activeProjectBranchPublications = (0, import_workspace_publication_evidence.activeProjectBranchPublicationEvidence)([...projectConfigById.values()], result.activeMountIds);
4320
4368
  return {
4321
4369
  type: "workspace_sync",
@@ -4333,6 +4381,7 @@ async function startWorker(options) {
4333
4381
  affectedPaths: result.affectedPaths,
4334
4382
  activeMountIds: result.activeMountIds,
4335
4383
  skippedMountIds: result.skippedMountIds,
4384
+ ...missingCheckouts ? { missingCheckouts } : {},
4336
4385
  activeProjectBranchPublications,
4337
4386
  discardedPaths: [],
4338
4387
  localChangesDiscarded: false,
@@ -4360,6 +4409,7 @@ async function startWorker(options) {
4360
4409
  affectedPaths: [],
4361
4410
  discardedPaths: [],
4362
4411
  localChangesDiscarded: false,
4412
+ ...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
4363
4413
  error: error instanceof Error ? error.message : String(error)
4364
4414
  };
4365
4415
  };
@@ -4368,7 +4418,11 @@ async function startWorker(options) {
4368
4418
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4369
4419
  throw new Error("Worker workspace configuration has not been received");
4370
4420
  }
4371
- const configuredMounts = buildWorkspaceMounts();
4421
+ const checkoutInventory = inventoryConfiguredProjectCheckouts();
4422
+ logMissingCheckouts(checkoutInventory);
4423
+ const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
4424
+ lastInventoriedMissingCheckouts = missingCheckouts;
4425
+ const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
4372
4426
  if (input.resetToCanonical) {
4373
4427
  workspaceProjectionLedger.reset();
4374
4428
  const resetHasBusyLiveMount = configuredMounts.some(
@@ -4379,7 +4433,8 @@ async function startWorker(options) {
4379
4433
  }
4380
4434
  const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
4381
4435
  allowNonFastForward: true,
4382
- failClosed: true
4436
+ failClosed: true,
4437
+ projects: checkoutInventory.projects
4383
4438
  });
4384
4439
  const reset = await (0, import_workspace_git_sync.resetWorkspaceGit)({
4385
4440
  workspacePath: workspaceShadowRoot,
@@ -4407,6 +4462,7 @@ async function startWorker(options) {
4407
4462
  affectedPaths: reset.discardedPaths,
4408
4463
  activeMountIds: reset.activeMountIds,
4409
4464
  skippedMountIds: reset.skippedMountIds,
4465
+ missingCheckouts,
4410
4466
  discardedPaths: reset.discardedPaths,
4411
4467
  localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
4412
4468
  };
@@ -4414,7 +4470,7 @@ async function startWorker(options) {
4414
4470
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4415
4471
  const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
4416
4472
  if (outerRemediation) workspaceProjectionLedger.reset();
4417
- const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4473
+ const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
4418
4474
  const expectedMirrorHeads = /* @__PURE__ */ new Map();
4419
4475
  for (const bundle of observedProjectHeads) {
4420
4476
  for (const observation of bundle.observations) {
@@ -4426,7 +4482,7 @@ async function startWorker(options) {
4426
4482
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
4427
4483
  )
4428
4484
  );
4429
- const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
4485
+ const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
4430
4486
  const result = await (0, import_workspace_git_sync.synchronizeWorkspaceGit)({
4431
4487
  attemptId: input.attemptId,
4432
4488
  workerLabel: label,
@@ -4446,7 +4502,14 @@ async function startWorker(options) {
4446
4502
  advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
4447
4503
  assertStillAdmitted: input.assertStillAdmitted,
4448
4504
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4449
- await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4505
+ await pushChangedProjectHeads(
4506
+ activeMountIds,
4507
+ publishedHead,
4508
+ publicationHeads,
4509
+ expectedMirrorHeads,
4510
+ inboundMoveMountIds,
4511
+ checkoutInventory.projects
4512
+ );
4450
4513
  },
4451
4514
  hydrationHooks: workspaceHydrationTransactionHooks
4452
4515
  });
@@ -4465,7 +4528,7 @@ async function startWorker(options) {
4465
4528
  result.localHead ?? result.remoteHead
4466
4529
  );
4467
4530
  }
4468
- const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result);
4531
+ const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
4469
4532
  if (!outerRemediation) {
4470
4533
  workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
4471
4534
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.103",
3
+ "version": "0.0.104",
4
4
  "type": "commonjs"
5
5
  }
@@ -28,10 +28,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var project_worktrees_exports = {};
30
30
  __export(project_worktrees_exports, {
31
+ PROJECT_REMOVED_CHECKOUTS_DIRECTORY: () => PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
31
32
  PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
32
33
  ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
33
34
  applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
34
35
  assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
36
+ classifyProjectCheckout: () => classifyProjectCheckout,
35
37
  computeLocalProjectMirrorRefsTokens: () => computeLocalProjectMirrorRefsTokens,
36
38
  createLinkedProjectBranch: () => createLinkedProjectBranch,
37
39
  createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
@@ -40,6 +42,7 @@ __export(project_worktrees_exports, {
40
42
  ensureProjectWorktrees: () => ensureProjectWorktrees,
41
43
  fastForwardProjectHeadsFromMirror: () => fastForwardProjectHeadsFromMirror,
42
44
  hasLinkedProjectWorktreeLayout: () => hasLinkedProjectWorktreeLayout,
45
+ inventoryProjectCheckouts: () => inventoryProjectCheckouts,
43
46
  observeProjectMirrorHeads: () => observeProjectMirrorHeads,
44
47
  projectBranchMayExistAfterCreateFailure: () => projectBranchMayExistAfterCreateFailure,
45
48
  projectOriginBranchName: () => projectOriginBranchName,
@@ -334,6 +337,8 @@ function normalizeSnapshotManifest(value) {
334
337
  if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
335
338
  throw new Error("Project worktree snapshot manifest has an invalid state");
336
339
  }
340
+ const scope = record.scope === void 0 ? "project" : record.scope;
341
+ if (scope !== "project" && scope !== "branches") throw new Error("Project worktree snapshot manifest has an invalid scope");
337
342
  if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
338
343
  throw new Error("Project worktree snapshot manifest has an invalid owner process");
339
344
  }
@@ -369,6 +374,7 @@ function normalizeSnapshotManifest(value) {
369
374
  return {
370
375
  version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
371
376
  state: record.state,
377
+ scope,
372
378
  ownerProcessId: record.ownerProcessId,
373
379
  ownerSessionId: record.ownerSessionId,
374
380
  projectRoot: record.projectRoot,
@@ -405,7 +411,7 @@ function writeSnapshotManifest(snapshotRoot, manifest) {
405
411
  return readSnapshotManifest(snapshotRoot);
406
412
  }
407
413
  function assertSameSnapshotIdentity(expected, actual) {
408
- 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)) {
414
+ if (expected.scope !== actual.scope || expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
409
415
  throw new Error("Project worktree snapshot identity changed");
410
416
  }
411
417
  }
@@ -472,7 +478,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
472
478
  }
473
479
  }
474
480
  }
475
- function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
481
+ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope = "project") {
476
482
  const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
477
483
  const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
478
484
  assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
@@ -493,6 +499,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
493
499
  let manifest = writeSnapshotManifest(root, {
494
500
  version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
495
501
  state: "building",
502
+ scope,
496
503
  ownerProcessId: process.pid,
497
504
  ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
498
505
  projectRoot: resolvedProjectRoot,
@@ -599,7 +606,7 @@ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
599
606
  const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
600
607
  const destinationParent = import_node_path.default.dirname(current.projectRoot);
601
608
  import_node_fs.default.mkdirSync(destinationParent, { recursive: true });
602
- const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
609
+ const canMoveWholeSnapshot = current.scope === "project" && presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
603
610
  if (canMoveWholeSnapshot) {
604
611
  if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
605
612
  onProgress?.({
@@ -690,8 +697,12 @@ function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
690
697
  } catch {
691
698
  }
692
699
  }
693
- function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
694
- fsyncTree(snapshot.manifest.projectRoot);
700
+ function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync, durabilityPaths) {
701
+ if (durabilityPaths) {
702
+ for (const durabilityPath of durabilityPaths) if (import_node_fs.default.existsSync(durabilityPath)) fsyncTree(durabilityPath);
703
+ } else {
704
+ fsyncTree(snapshot.manifest.projectRoot);
705
+ }
695
706
  fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
696
707
  afterProjectTreeFsync?.();
697
708
  consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
@@ -989,8 +1000,10 @@ async function reconcileLinkedLayout(input) {
989
1000
  await configureRepository({ ...input, primaryPath });
990
1001
  await tryGitAsync(primaryPath, ["worktree", "prune"]);
991
1002
  await fetchProjectHeads({ ...input, primaryPath });
1003
+ const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
1004
+ if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
992
1005
  const mirrorHeads = /* @__PURE__ */ new Map();
993
- const mutatedBranches = /* @__PURE__ */ new Set();
1006
+ const plan = [];
994
1007
  for (const branch of input.branches) {
995
1008
  const checkoutPath = branchPath(input.projectRoot, branch.branchName);
996
1009
  const resolved = await seedForBranch({
@@ -1000,24 +1013,30 @@ async function reconcileLinkedLayout(input) {
1000
1013
  requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
1001
1014
  });
1002
1015
  mirrorHeads.set(branch.branchName, resolved.mirrorHead);
1003
- if (branch.branchName === input.primaryBranchName || await commonGitDirectoryAsync(checkoutPath) === await commonGitDirectoryAsync(primaryPath)) {
1004
- const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
1005
- if (currentHead === resolved.seed) continue;
1006
- input.beforeDestructiveMutation();
1007
- mutatedBranches.add(branch.branchName);
1008
- await preserveHead(checkoutPath);
1009
- await gitAsync(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
1016
+ const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
1017
+ if (!linked) {
1018
+ plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
1010
1019
  continue;
1011
1020
  }
1012
- input.beforeDestructiveMutation();
1013
- mutatedBranches.add(branch.branchName);
1014
- import_node_fs.default.rmSync(checkoutPath, { recursive: true, force: true });
1015
- await gitAsync(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
1016
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
1021
+ const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
1022
+ plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: currentHead === resolved.seed ? "keep" : "reset" });
1023
+ }
1024
+ const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
1025
+ if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
1026
+ for (const entry of plan) {
1027
+ if (entry.action === "keep") continue;
1028
+ if (entry.action === "reset") {
1029
+ await preserveHead(entry.checkoutPath);
1030
+ await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
1031
+ continue;
1032
+ }
1033
+ import_node_fs.default.rmSync(entry.checkoutPath, { recursive: true, force: true });
1034
+ await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
1035
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(entry.checkoutPath), { recursive: true });
1017
1036
  await gitAsync(
1018
1037
  primaryPath,
1019
- ["worktree", "add", "--force", checkoutPath, branch.branchName],
1020
- `create linked worktree ${branch.branchName}`
1038
+ ["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
1039
+ `create linked worktree ${entry.branchName}`
1021
1040
  );
1022
1041
  }
1023
1042
  return { mirrorHeads, mutatedBranches };
@@ -1044,8 +1063,14 @@ async function ensureProjectWorktrees(input) {
1044
1063
  const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
1045
1064
  assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
1046
1065
  let snapshot;
1047
- const ensureSnapshot = () => {
1048
- snapshot ??= snapshotBranchTrees(projectRoot, input.branches, snapshotStorageRoot, input.onSnapshotProgress);
1066
+ const ensureSnapshot = (branchNames) => {
1067
+ snapshot ??= snapshotBranchTrees(
1068
+ projectRoot,
1069
+ branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
1070
+ snapshotStorageRoot,
1071
+ input.onSnapshotProgress,
1072
+ branchNames ? "branches" : "project"
1073
+ );
1049
1074
  return snapshot;
1050
1075
  };
1051
1076
  try {
@@ -1097,7 +1122,21 @@ async function ensureProjectWorktrees(input) {
1097
1122
  });
1098
1123
  }
1099
1124
  states.sort((left, right) => left.branchName.localeCompare(right.branchName));
1100
- if (snapshot) persistReconciledProjectAndConsumeSnapshot(snapshot);
1125
+ if (snapshot) {
1126
+ const primaryGitDirectory = import_node_path.default.join(branchPath(projectRoot, input.primaryBranchName), ".git");
1127
+ persistReconciledProjectAndConsumeSnapshot(
1128
+ snapshot,
1129
+ void 0,
1130
+ snapshot.manifest.scope === "branches" ? [
1131
+ ...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
1132
+ // Branch refs and worktree registrations moved in the shared
1133
+ // administrative directory; objects are fsynced by git itself.
1134
+ import_node_path.default.join(primaryGitDirectory, "refs"),
1135
+ import_node_path.default.join(primaryGitDirectory, "packed-refs"),
1136
+ import_node_path.default.join(primaryGitDirectory, "worktrees")
1137
+ ] : void 0
1138
+ );
1139
+ }
1101
1140
  return states;
1102
1141
  } catch (error) {
1103
1142
  if (!snapshot) throw error;
@@ -1243,6 +1282,53 @@ function createOrRetryLinkedProjectBranch(input) {
1243
1282
  }
1244
1283
  return createLinkedProjectBranch(input);
1245
1284
  }
1285
+ function classifyProjectCheckout(input) {
1286
+ const checkoutPath = branchPath(input.projectRoot, input.branchName);
1287
+ let stat;
1288
+ try {
1289
+ stat = import_node_fs.default.lstatSync(checkoutPath);
1290
+ } catch {
1291
+ return "missing";
1292
+ }
1293
+ if (!stat.isDirectory()) return "not_a_worktree";
1294
+ if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
1295
+ if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
1296
+ const gitDirectory = perWorktreeGitDirectory(checkoutPath);
1297
+ if (!gitDirectory) return "not_a_worktree";
1298
+ let registry;
1299
+ let registered;
1300
+ try {
1301
+ registry = import_node_fs.default.realpathSync(import_node_path.default.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
1302
+ registered = import_node_fs.default.realpathSync(gitDirectory);
1303
+ } catch {
1304
+ return "not_a_worktree";
1305
+ }
1306
+ const relative = import_node_path.default.relative(registry, registered);
1307
+ if (!relative || relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) return "not_a_worktree";
1308
+ return "linked";
1309
+ }
1310
+ function inventoryProjectCheckouts(input) {
1311
+ const states = input.branchNames.map((branchName) => ({
1312
+ branchName,
1313
+ checkoutPath: branchPath(input.projectRoot, branchName),
1314
+ state: classifyProjectCheckout({ ...input, branchName })
1315
+ }));
1316
+ const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
1317
+ const inventory = { linkedBranchNames: [], missingCheckouts: [] };
1318
+ for (const { branchName, checkoutPath, state } of states) {
1319
+ if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
1320
+ else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
1321
+ }
1322
+ return inventory;
1323
+ }
1324
+ const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
1325
+ function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
1326
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1327
+ const target = import_node_path.default.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
1328
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(target), { recursive: true });
1329
+ import_node_fs.default.renameSync(checkoutPath, target);
1330
+ return target;
1331
+ }
1246
1332
  function deleteLinkedProjectBranch(input) {
1247
1333
  (0, import_managed_paths.validateManagedBranchName)(input.primaryBranchName);
1248
1334
  (0, import_managed_paths.validateManagedBranchName)(input.branchName);
@@ -1251,15 +1337,18 @@ function deleteLinkedProjectBranch(input) {
1251
1337
  const checkoutPath = branchPath(input.projectRoot, input.branchName);
1252
1338
  const primaryCommonDir = commonGitDirectory(primaryPath);
1253
1339
  if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
1340
+ let movedAsidePath;
1254
1341
  if (import_node_fs.default.existsSync(checkoutPath)) {
1255
- const checkoutCommonDir = commonGitDirectory(checkoutPath);
1342
+ const checkoutCommonDir = import_node_fs.default.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
1256
1343
  if (checkoutCommonDir !== primaryCommonDir) {
1257
- throw new Error(`Project branch path ${input.branchName} is not a linked worktree of the configured project`);
1258
- }
1259
- if (projectWorktreeOperationInProgress(checkoutPath)) {
1260
- throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
1344
+ movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
1345
+ tryGit(primaryPath, ["worktree", "prune"]);
1346
+ } else {
1347
+ if (projectWorktreeOperationInProgress(checkoutPath)) {
1348
+ throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
1349
+ }
1350
+ git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
1261
1351
  }
1262
- git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
1263
1352
  } else {
1264
1353
  tryGit(primaryPath, ["worktree", "prune"]);
1265
1354
  }
@@ -1267,7 +1356,7 @@ function deleteLinkedProjectBranch(input) {
1267
1356
  if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
1268
1357
  git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
1269
1358
  }
1270
- return { branchName: input.branchName };
1359
+ return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
1271
1360
  }
1272
1361
  function removeProjectWorktrees(input) {
1273
1362
  const projectRoot = import_node_path.default.resolve(input.projectRoot);
@@ -1526,7 +1615,7 @@ const projectWorktreesTestHarness = {
1526
1615
  commandArgs: gitCommandArgs,
1527
1616
  configureRepository,
1528
1617
  createPreparedSnapshot(input) {
1529
- return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
1618
+ return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
1530
1619
  },
1531
1620
  consumeSnapshot(snapshotRoot) {
1532
1621
  const manifest = readSnapshotManifest(snapshotRoot);
@@ -1542,10 +1631,12 @@ const projectWorktreesTestHarness = {
1542
1631
  };
1543
1632
  // Annotate the CommonJS export names for ESM import in node:
1544
1633
  0 && (module.exports = {
1634
+ PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
1545
1635
  PROJECT_WORKTREE_SNAPSHOT_PREFIX,
1546
1636
  ProjectBranchCreationRollbackIncompleteError,
1547
1637
  applyObservedProjectMirrorHeads,
1548
1638
  assertProjectMirrorHeadsPushed,
1639
+ classifyProjectCheckout,
1549
1640
  computeLocalProjectMirrorRefsTokens,
1550
1641
  createLinkedProjectBranch,
1551
1642
  createOrRetryLinkedProjectBranch,
@@ -1554,6 +1645,7 @@ const projectWorktreesTestHarness = {
1554
1645
  ensureProjectWorktrees,
1555
1646
  fastForwardProjectHeadsFromMirror,
1556
1647
  hasLinkedProjectWorktreeLayout,
1648
+ inventoryProjectCheckouts,
1557
1649
  observeProjectMirrorHeads,
1558
1650
  projectBranchMayExistAfterCreateFailure,
1559
1651
  projectOriginBranchName,
package/dist/mjs/main.mjs CHANGED
@@ -65,6 +65,7 @@ import {
65
65
  deleteLinkedProjectBranch,
66
66
  deleteProjectMirrorBranch,
67
67
  ensureProjectWorktrees,
68
+ inventoryProjectCheckouts,
68
69
  observeProjectMirrorHeads,
69
70
  projectBranchMayExistAfterCreateFailure,
70
71
  projectOriginBranchName,
@@ -3519,6 +3520,7 @@ async function startWorker(options) {
3519
3520
  let terminalReplayTimer;
3520
3521
  let workspaceSyncRequestsInFlight = 0;
3521
3522
  let lastWorkspaceSyncMirrorObservationMs = 0;
3523
+ let lastInventoriedMissingCheckouts;
3522
3524
  let sessionArtifactSyncRequestsInFlight = 0;
3523
3525
  let cliUpdateInProgress = false;
3524
3526
  let reloadAfterClose = false;
@@ -3576,8 +3578,15 @@ async function startWorker(options) {
3576
3578
  const primaryBranchName = primaryProjectBranch(project);
3577
3579
  const connection = projectConnection(project);
3578
3580
  const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
3579
- if (requireLocalBranch || hasProjectWorktree(sourcePath)) {
3580
- deleteLinkedProjectBranch({ projectRoot, primaryBranchName, branchName });
3581
+ const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryBranchName);
3582
+ if (requireLocalBranch || fs.existsSync(sourcePath) && hasProjectWorktree(primaryPath)) {
3583
+ const deleted = deleteLinkedProjectBranch({ projectRoot, primaryBranchName, branchName });
3584
+ if (deleted.movedAsidePath) {
3585
+ process.stderr.write(
3586
+ `[r5d-worker] set aside ${sourcePath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3587
+ `
3588
+ );
3589
+ }
3581
3590
  }
3582
3591
  const planSourcePath = path.join(planRoot, project.projectId, ...branchName.split("/"));
3583
3592
  fs.rmSync(planSourcePath, { recursive: true, force: true });
@@ -3650,11 +3659,17 @@ async function startWorker(options) {
3650
3659
  if (branchName === primaryBranchName) {
3651
3660
  throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
3652
3661
  }
3653
- deleteLinkedProjectBranch({
3662
+ const deleted = deleteLinkedProjectBranch({
3654
3663
  projectRoot: configuredProjectRoot(projectsRoot, project),
3655
3664
  primaryBranchName,
3656
3665
  branchName
3657
3666
  });
3667
+ if (deleted.movedAsidePath) {
3668
+ process.stderr.write(
3669
+ `[r5d-worker] set aside ${branchPath}, which was not a linked worktree of ${project.projectPath}, at ${deleted.movedAsidePath}
3670
+ `
3671
+ );
3672
+ }
3658
3673
  fs.rmSync(path.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
3659
3674
  readyProjectIds.delete(project.projectId);
3660
3675
  reconciledProjectConfigFingerprints.delete(project.projectId);
@@ -3823,6 +3838,40 @@ async function startWorker(options) {
3823
3838
  });
3824
3839
  };
3825
3840
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3841
+ const inventoryConfiguredProjectCheckouts = () => {
3842
+ const inventory = { projects: [], missingCheckouts: [] };
3843
+ for (const project of projectConfigById.values()) {
3844
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) {
3845
+ inventory.projects.push(project);
3846
+ continue;
3847
+ }
3848
+ const checkouts = inventoryProjectCheckouts({
3849
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3850
+ primaryBranchName: primaryProjectBranch(project),
3851
+ branchNames: project.branches.map(({ branchName }) => branchName)
3852
+ });
3853
+ if (checkouts.missingCheckouts.length === 0) {
3854
+ inventory.projects.push(project);
3855
+ continue;
3856
+ }
3857
+ for (const missing of checkouts.missingCheckouts) inventory.missingCheckouts.push({ projectId: project.projectId, ...missing });
3858
+ const linked = new Set(checkouts.linkedBranchNames);
3859
+ inventory.projects.push({ ...project, branches: project.branches.filter(({ branchName }) => linked.has(branchName)) });
3860
+ }
3861
+ inventory.missingCheckouts.sort(
3862
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
3863
+ );
3864
+ return inventory;
3865
+ };
3866
+ const reportedMissingCheckouts = (inventory) => inventory.missingCheckouts.map(({ projectId, branchName, reason }) => ({ projectId, branchName, reason }));
3867
+ const logMissingCheckouts = (inventory) => {
3868
+ if (inventory.missingCheckouts.length === 0) return;
3869
+ const described = inventory.missingCheckouts.map(({ checkoutPath, reason }) => `${checkoutPath} (${reason === "missing" ? "directory missing" : "not a linked worktree of the project"})`).join(", ");
3870
+ process.stderr.write(
3871
+ `[r5d-worker] skipping ${inventory.missingCheckouts.length} configured project checkout(s) this cycle: ${described}; delete the branch through the platform or reconnect this worker to recreate the checkout
3872
+ `
3873
+ );
3874
+ };
3826
3875
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3827
3876
  const mounts = [];
3828
3877
  const creatorLocalIncarnations = new Map(
@@ -4101,9 +4150,9 @@ async function startWorker(options) {
4101
4150
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
4102
4151
  );
4103
4152
  };
4104
- const captureProjectHeadsForWorkspacePublication = async () => {
4153
+ const captureProjectHeadsForWorkspacePublication = async (projects = projectConfigById.values()) => {
4105
4154
  const heads = /* @__PURE__ */ new Map();
4106
- for (const project of projectConfigById.values()) {
4155
+ for (const project of projects) {
4107
4156
  if (!readyProjectIds.has(project.projectId)) continue;
4108
4157
  for (const branch of project.branches) {
4109
4158
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
@@ -4113,9 +4162,9 @@ async function startWorker(options) {
4113
4162
  }
4114
4163
  return heads;
4115
4164
  };
4116
- const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
4165
+ const pushChangedProjectHeads = async (activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, suppressedMountIds = /* @__PURE__ */ new Set(), projects = projectConfigById.values()) => {
4117
4166
  const active = new Set(activeMountIds);
4118
- for (const project of projectConfigById.values()) {
4167
+ for (const project of projects) {
4119
4168
  if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
4120
4169
  const changed = /* @__PURE__ */ new Map();
4121
4170
  for (const branch of project.branches) {
@@ -4215,7 +4264,7 @@ async function startWorker(options) {
4215
4264
  };
4216
4265
  const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
4217
4266
  const observationStartedAt = Date.now();
4218
- const eligibleProjects = [...projectConfigById.values()].filter(
4267
+ const eligibleProjects = [...input.projects ?? projectConfigById.values()].filter(
4219
4268
  (project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
4220
4269
  );
4221
4270
  try {
@@ -4325,7 +4374,7 @@ async function startWorker(options) {
4325
4374
  }
4326
4375
  }
4327
4376
  };
4328
- const mapWorkspaceGitResult = async (attemptId, trigger, result) => {
4377
+ const mapWorkspaceGitResult = async (attemptId, trigger, result, missingCheckouts) => {
4329
4378
  const activeProjectBranchPublications = activeProjectBranchPublicationEvidence([...projectConfigById.values()], result.activeMountIds);
4330
4379
  return {
4331
4380
  type: "workspace_sync",
@@ -4343,6 +4392,7 @@ async function startWorker(options) {
4343
4392
  affectedPaths: result.affectedPaths,
4344
4393
  activeMountIds: result.activeMountIds,
4345
4394
  skippedMountIds: result.skippedMountIds,
4395
+ ...missingCheckouts ? { missingCheckouts } : {},
4346
4396
  activeProjectBranchPublications,
4347
4397
  discardedPaths: [],
4348
4398
  localChangesDiscarded: false,
@@ -4370,6 +4420,7 @@ async function startWorker(options) {
4370
4420
  affectedPaths: [],
4371
4421
  discardedPaths: [],
4372
4422
  localChangesDiscarded: false,
4423
+ ...lastInventoriedMissingCheckouts ? { missingCheckouts: lastInventoriedMissingCheckouts } : {},
4373
4424
  error: error instanceof Error ? error.message : String(error)
4374
4425
  };
4375
4426
  };
@@ -4378,7 +4429,11 @@ async function startWorker(options) {
4378
4429
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4379
4430
  throw new Error("Worker workspace configuration has not been received");
4380
4431
  }
4381
- const configuredMounts = buildWorkspaceMounts();
4432
+ const checkoutInventory = inventoryConfiguredProjectCheckouts();
4433
+ logMissingCheckouts(checkoutInventory);
4434
+ const missingCheckouts = reportedMissingCheckouts(checkoutInventory);
4435
+ lastInventoriedMissingCheckouts = missingCheckouts;
4436
+ const configuredMounts = buildWorkspaceMounts(checkoutInventory.projects);
4382
4437
  if (input.resetToCanonical) {
4383
4438
  workspaceProjectionLedger.reset();
4384
4439
  const resetHasBusyLiveMount = configuredMounts.some(
@@ -4389,7 +4444,8 @@ async function startWorker(options) {
4389
4444
  }
4390
4445
  const observedProjectHeads2 = await observeProjectHeadsBeforeOuterWorkspace({
4391
4446
  allowNonFastForward: true,
4392
- failClosed: true
4447
+ failClosed: true,
4448
+ projects: checkoutInventory.projects
4393
4449
  });
4394
4450
  const reset = await resetWorkspaceGit({
4395
4451
  workspacePath: workspaceShadowRoot,
@@ -4417,6 +4473,7 @@ async function startWorker(options) {
4417
4473
  affectedPaths: reset.discardedPaths,
4418
4474
  activeMountIds: reset.activeMountIds,
4419
4475
  skippedMountIds: reset.skippedMountIds,
4476
+ missingCheckouts,
4420
4477
  discardedPaths: reset.discardedPaths,
4421
4478
  localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
4422
4479
  };
@@ -4424,7 +4481,7 @@ async function startWorker(options) {
4424
4481
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4425
4482
  const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
4426
4483
  if (outerRemediation) workspaceProjectionLedger.reset();
4427
- const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4484
+ const observedProjectHeads = outerRemediation ? [] : await observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false, projects: checkoutInventory.projects });
4428
4485
  const expectedMirrorHeads = /* @__PURE__ */ new Map();
4429
4486
  for (const bundle of observedProjectHeads) {
4430
4487
  for (const observation of bundle.observations) {
@@ -4436,7 +4493,7 @@ async function startWorker(options) {
4436
4493
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
4437
4494
  )
4438
4495
  );
4439
- const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication();
4496
+ const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : await captureProjectHeadsForWorkspacePublication(checkoutInventory.projects);
4440
4497
  const result = await synchronizeWorkspaceGit({
4441
4498
  attemptId: input.attemptId,
4442
4499
  workerLabel: label,
@@ -4456,7 +4513,14 @@ async function startWorker(options) {
4456
4513
  advertisedRemoteHead: projectMirrorFetchSkippingEnabled && !outerRemediation ? advertisedWorkspaceHead : void 0,
4457
4514
  assertStillAdmitted: input.assertStillAdmitted,
4458
4515
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4459
- await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4516
+ await pushChangedProjectHeads(
4517
+ activeMountIds,
4518
+ publishedHead,
4519
+ publicationHeads,
4520
+ expectedMirrorHeads,
4521
+ inboundMoveMountIds,
4522
+ checkoutInventory.projects
4523
+ );
4460
4524
  },
4461
4525
  hydrationHooks: workspaceHydrationTransactionHooks
4462
4526
  });
@@ -4475,7 +4539,7 @@ async function startWorker(options) {
4475
4539
  result.localHead ?? result.remoteHead
4476
4540
  );
4477
4541
  }
4478
- const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result);
4542
+ const mapped = await mapWorkspaceGitResult(input.attemptId, input.trigger, result, missingCheckouts);
4479
4543
  if (!outerRemediation) {
4480
4544
  workspaceProjectionLedger.recordCycle({ result: mapped, mounts, projectedMutationTokens: result.projectedMutationTokens });
4481
4545
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.103",
3
+ "version": "0.0.104",
4
4
  "type": "module"
5
5
  }
@@ -287,6 +287,8 @@ function normalizeSnapshotManifest(value) {
287
287
  if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
288
288
  throw new Error("Project worktree snapshot manifest has an invalid state");
289
289
  }
290
+ const scope = record.scope === void 0 ? "project" : record.scope;
291
+ if (scope !== "project" && scope !== "branches") throw new Error("Project worktree snapshot manifest has an invalid scope");
290
292
  if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
291
293
  throw new Error("Project worktree snapshot manifest has an invalid owner process");
292
294
  }
@@ -322,6 +324,7 @@ function normalizeSnapshotManifest(value) {
322
324
  return {
323
325
  version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
324
326
  state: record.state,
327
+ scope,
325
328
  ownerProcessId: record.ownerProcessId,
326
329
  ownerSessionId: record.ownerSessionId,
327
330
  projectRoot: record.projectRoot,
@@ -358,7 +361,7 @@ function writeSnapshotManifest(snapshotRoot, manifest) {
358
361
  return readSnapshotManifest(snapshotRoot);
359
362
  }
360
363
  function assertSameSnapshotIdentity(expected, actual) {
361
- 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)) {
364
+ if (expected.scope !== actual.scope || expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
362
365
  throw new Error("Project worktree snapshot identity changed");
363
366
  }
364
367
  }
@@ -425,7 +428,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
425
428
  }
426
429
  }
427
430
  }
428
- function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
431
+ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope = "project") {
429
432
  const resolvedProjectRoot = path.resolve(projectRoot);
430
433
  const resolvedTemporaryRoot = path.resolve(temporaryRoot ?? os.tmpdir());
431
434
  assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
@@ -446,6 +449,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
446
449
  let manifest = writeSnapshotManifest(root, {
447
450
  version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
448
451
  state: "building",
452
+ scope,
449
453
  ownerProcessId: process.pid,
450
454
  ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
451
455
  projectRoot: resolvedProjectRoot,
@@ -552,7 +556,7 @@ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
552
556
  const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
553
557
  const destinationParent = path.dirname(current.projectRoot);
554
558
  fs.mkdirSync(destinationParent, { recursive: true });
555
- const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === fs.lstatSync(destinationParent).dev);
559
+ const canMoveWholeSnapshot = current.scope === "project" && presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === fs.lstatSync(destinationParent).dev);
556
560
  if (canMoveWholeSnapshot) {
557
561
  if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
558
562
  onProgress?.({
@@ -643,8 +647,12 @@ function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
643
647
  } catch {
644
648
  }
645
649
  }
646
- function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
647
- fsyncTree(snapshot.manifest.projectRoot);
650
+ function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync, durabilityPaths) {
651
+ if (durabilityPaths) {
652
+ for (const durabilityPath of durabilityPaths) if (fs.existsSync(durabilityPath)) fsyncTree(durabilityPath);
653
+ } else {
654
+ fsyncTree(snapshot.manifest.projectRoot);
655
+ }
648
656
  fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
649
657
  afterProjectTreeFsync?.();
650
658
  consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
@@ -942,8 +950,10 @@ async function reconcileLinkedLayout(input) {
942
950
  await configureRepository({ ...input, primaryPath });
943
951
  await tryGitAsync(primaryPath, ["worktree", "prune"]);
944
952
  await fetchProjectHeads({ ...input, primaryPath });
953
+ const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
954
+ if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
945
955
  const mirrorHeads = /* @__PURE__ */ new Map();
946
- const mutatedBranches = /* @__PURE__ */ new Set();
956
+ const plan = [];
947
957
  for (const branch of input.branches) {
948
958
  const checkoutPath = branchPath(input.projectRoot, branch.branchName);
949
959
  const resolved = await seedForBranch({
@@ -953,24 +963,30 @@ async function reconcileLinkedLayout(input) {
953
963
  requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
954
964
  });
955
965
  mirrorHeads.set(branch.branchName, resolved.mirrorHead);
956
- if (branch.branchName === input.primaryBranchName || await commonGitDirectoryAsync(checkoutPath) === await commonGitDirectoryAsync(primaryPath)) {
957
- const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
958
- if (currentHead === resolved.seed) continue;
959
- input.beforeDestructiveMutation();
960
- mutatedBranches.add(branch.branchName);
961
- await preserveHead(checkoutPath);
962
- await gitAsync(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
966
+ const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
967
+ if (!linked) {
968
+ plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
963
969
  continue;
964
970
  }
965
- input.beforeDestructiveMutation();
966
- mutatedBranches.add(branch.branchName);
967
- fs.rmSync(checkoutPath, { recursive: true, force: true });
968
- await gitAsync(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
969
- fs.mkdirSync(path.dirname(checkoutPath), { recursive: true });
971
+ const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
972
+ plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: currentHead === resolved.seed ? "keep" : "reset" });
973
+ }
974
+ const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
975
+ if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
976
+ for (const entry of plan) {
977
+ if (entry.action === "keep") continue;
978
+ if (entry.action === "reset") {
979
+ await preserveHead(entry.checkoutPath);
980
+ await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
981
+ continue;
982
+ }
983
+ fs.rmSync(entry.checkoutPath, { recursive: true, force: true });
984
+ await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
985
+ fs.mkdirSync(path.dirname(entry.checkoutPath), { recursive: true });
970
986
  await gitAsync(
971
987
  primaryPath,
972
- ["worktree", "add", "--force", checkoutPath, branch.branchName],
973
- `create linked worktree ${branch.branchName}`
988
+ ["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
989
+ `create linked worktree ${entry.branchName}`
974
990
  );
975
991
  }
976
992
  return { mirrorHeads, mutatedBranches };
@@ -997,8 +1013,14 @@ async function ensureProjectWorktrees(input) {
997
1013
  const snapshotStorageRoot = path.resolve(input.snapshotRoot ?? os.tmpdir());
998
1014
  assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
999
1015
  let snapshot;
1000
- const ensureSnapshot = () => {
1001
- snapshot ??= snapshotBranchTrees(projectRoot, input.branches, snapshotStorageRoot, input.onSnapshotProgress);
1016
+ const ensureSnapshot = (branchNames) => {
1017
+ snapshot ??= snapshotBranchTrees(
1018
+ projectRoot,
1019
+ branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
1020
+ snapshotStorageRoot,
1021
+ input.onSnapshotProgress,
1022
+ branchNames ? "branches" : "project"
1023
+ );
1002
1024
  return snapshot;
1003
1025
  };
1004
1026
  try {
@@ -1050,7 +1072,21 @@ async function ensureProjectWorktrees(input) {
1050
1072
  });
1051
1073
  }
1052
1074
  states.sort((left, right) => left.branchName.localeCompare(right.branchName));
1053
- if (snapshot) persistReconciledProjectAndConsumeSnapshot(snapshot);
1075
+ if (snapshot) {
1076
+ const primaryGitDirectory = path.join(branchPath(projectRoot, input.primaryBranchName), ".git");
1077
+ persistReconciledProjectAndConsumeSnapshot(
1078
+ snapshot,
1079
+ void 0,
1080
+ snapshot.manifest.scope === "branches" ? [
1081
+ ...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
1082
+ // Branch refs and worktree registrations moved in the shared
1083
+ // administrative directory; objects are fsynced by git itself.
1084
+ path.join(primaryGitDirectory, "refs"),
1085
+ path.join(primaryGitDirectory, "packed-refs"),
1086
+ path.join(primaryGitDirectory, "worktrees")
1087
+ ] : void 0
1088
+ );
1089
+ }
1054
1090
  return states;
1055
1091
  } catch (error) {
1056
1092
  if (!snapshot) throw error;
@@ -1196,6 +1232,53 @@ function createOrRetryLinkedProjectBranch(input) {
1196
1232
  }
1197
1233
  return createLinkedProjectBranch(input);
1198
1234
  }
1235
+ function classifyProjectCheckout(input) {
1236
+ const checkoutPath = branchPath(input.projectRoot, input.branchName);
1237
+ let stat;
1238
+ try {
1239
+ stat = fs.lstatSync(checkoutPath);
1240
+ } catch {
1241
+ return "missing";
1242
+ }
1243
+ if (!stat.isDirectory()) return "not_a_worktree";
1244
+ if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
1245
+ if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
1246
+ const gitDirectory = perWorktreeGitDirectory(checkoutPath);
1247
+ if (!gitDirectory) return "not_a_worktree";
1248
+ let registry;
1249
+ let registered;
1250
+ try {
1251
+ registry = fs.realpathSync(path.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
1252
+ registered = fs.realpathSync(gitDirectory);
1253
+ } catch {
1254
+ return "not_a_worktree";
1255
+ }
1256
+ const relative = path.relative(registry, registered);
1257
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return "not_a_worktree";
1258
+ return "linked";
1259
+ }
1260
+ function inventoryProjectCheckouts(input) {
1261
+ const states = input.branchNames.map((branchName) => ({
1262
+ branchName,
1263
+ checkoutPath: branchPath(input.projectRoot, branchName),
1264
+ state: classifyProjectCheckout({ ...input, branchName })
1265
+ }));
1266
+ const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
1267
+ const inventory = { linkedBranchNames: [], missingCheckouts: [] };
1268
+ for (const { branchName, checkoutPath, state } of states) {
1269
+ if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
1270
+ else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
1271
+ }
1272
+ return inventory;
1273
+ }
1274
+ const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
1275
+ function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
1276
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1277
+ const target = path.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
1278
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1279
+ fs.renameSync(checkoutPath, target);
1280
+ return target;
1281
+ }
1199
1282
  function deleteLinkedProjectBranch(input) {
1200
1283
  validateManagedBranchName(input.primaryBranchName);
1201
1284
  validateManagedBranchName(input.branchName);
@@ -1204,15 +1287,18 @@ function deleteLinkedProjectBranch(input) {
1204
1287
  const checkoutPath = branchPath(input.projectRoot, input.branchName);
1205
1288
  const primaryCommonDir = commonGitDirectory(primaryPath);
1206
1289
  if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
1290
+ let movedAsidePath;
1207
1291
  if (fs.existsSync(checkoutPath)) {
1208
- const checkoutCommonDir = commonGitDirectory(checkoutPath);
1292
+ const checkoutCommonDir = fs.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
1209
1293
  if (checkoutCommonDir !== primaryCommonDir) {
1210
- throw new Error(`Project branch path ${input.branchName} is not a linked worktree of the configured project`);
1211
- }
1212
- if (projectWorktreeOperationInProgress(checkoutPath)) {
1213
- throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
1294
+ movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
1295
+ tryGit(primaryPath, ["worktree", "prune"]);
1296
+ } else {
1297
+ if (projectWorktreeOperationInProgress(checkoutPath)) {
1298
+ throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
1299
+ }
1300
+ git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
1214
1301
  }
1215
- git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
1216
1302
  } else {
1217
1303
  tryGit(primaryPath, ["worktree", "prune"]);
1218
1304
  }
@@ -1220,7 +1306,7 @@ function deleteLinkedProjectBranch(input) {
1220
1306
  if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
1221
1307
  git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
1222
1308
  }
1223
- return { branchName: input.branchName };
1309
+ return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
1224
1310
  }
1225
1311
  function removeProjectWorktrees(input) {
1226
1312
  const projectRoot = path.resolve(input.projectRoot);
@@ -1479,7 +1565,7 @@ const projectWorktreesTestHarness = {
1479
1565
  commandArgs: gitCommandArgs,
1480
1566
  configureRepository,
1481
1567
  createPreparedSnapshot(input) {
1482
- return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
1568
+ return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
1483
1569
  },
1484
1570
  consumeSnapshot(snapshotRoot) {
1485
1571
  const manifest = readSnapshotManifest(snapshotRoot);
@@ -1494,10 +1580,12 @@ const projectWorktreesTestHarness = {
1494
1580
  }
1495
1581
  };
1496
1582
  export {
1583
+ PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
1497
1584
  PROJECT_WORKTREE_SNAPSHOT_PREFIX,
1498
1585
  ProjectBranchCreationRollbackIncompleteError,
1499
1586
  applyObservedProjectMirrorHeads,
1500
1587
  assertProjectMirrorHeadsPushed,
1588
+ classifyProjectCheckout,
1501
1589
  computeLocalProjectMirrorRefsTokens,
1502
1590
  createLinkedProjectBranch,
1503
1591
  createOrRetryLinkedProjectBranch,
@@ -1506,6 +1594,7 @@ export {
1506
1594
  ensureProjectWorktrees,
1507
1595
  fastForwardProjectHeadsFromMirror,
1508
1596
  hasLinkedProjectWorktreeLayout,
1597
+ inventoryProjectCheckouts,
1509
1598
  observeProjectMirrorHeads,
1510
1599
  projectBranchMayExistAfterCreateFailure,
1511
1600
  projectOriginBranchName,
@@ -93,6 +93,12 @@ type WorkspaceSyncResult = {
93
93
  affectedPaths: string[];
94
94
  activeMountIds?: string[];
95
95
  skippedMountIds?: string[];
96
+ /** Configured checkouts this cycle excluded because their directory is absent or is not a linked worktree of the project. */
97
+ missingCheckouts?: Array<{
98
+ projectId: string;
99
+ branchName: string;
100
+ reason: "missing" | "not_a_worktree";
101
+ }>;
96
102
  activeProjectBranchPublications?: Array<{
97
103
  branchId: string;
98
104
  projectId: string;
@@ -1,9 +1,17 @@
1
1
  import { type ProjectMirrorMount, type ProjectMirrorRefsTokens } from "./project-mirror-refs-token";
2
2
  export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
3
3
  declare const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION: 1;
4
+ /**
5
+ * `project` snapshots cover every configured branch and restore by replacing
6
+ * the whole project root. `branches` snapshots cover only the checkouts one
7
+ * reconciliation rewrites and restore those paths individually, so a rollback
8
+ * never touches a checkout the transaction did not.
9
+ */
10
+ type ProjectWorktreeSnapshotScope = "project" | "branches";
4
11
  type ProjectWorktreeSnapshotManifest = {
5
12
  version: typeof PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION;
6
13
  state: "building" | "prepared" | "restoring" | "consumed";
14
+ scope: ProjectWorktreeSnapshotScope;
7
15
  ownerProcessId: number;
8
16
  ownerSessionId: string;
9
17
  projectRoot: string;
@@ -157,12 +165,50 @@ export declare function createOrRetryLinkedProjectBranch(input: {
157
165
  branchPath: string;
158
166
  baseCommitHash: string;
159
167
  };
168
+ export type ProjectCheckoutState = "linked" | "missing" | "not_a_worktree";
169
+ export type ProjectCheckoutInventory = {
170
+ /** Branches whose checkout is this project's primary clone or one of its linked worktrees. */
171
+ linkedBranchNames: string[];
172
+ missingCheckouts: Array<{
173
+ branchName: string;
174
+ checkoutPath: string;
175
+ reason: Exclude<ProjectCheckoutState, "linked">;
176
+ }>;
177
+ };
178
+ /**
179
+ * Classify one configured branch checkout without spawning git, cheap enough
180
+ * to run for every configured branch on every sync cycle. The primary
181
+ * checkout is `linked` when its `.git` is a directory; another branch is
182
+ * `linked` when its `.git` pointer file resolves into that primary clone's
183
+ * `.git/worktrees` registry and the registered directory still exists.
184
+ * `missing` means the checkout path is absent. Everything else (a hydrated
185
+ * directory that never became a checkout, a foreign clone, a pointer to a
186
+ * pruned registration) is `not_a_worktree`.
187
+ */
188
+ export declare function classifyProjectCheckout(input: {
189
+ projectRoot: string;
190
+ primaryBranchName: string;
191
+ branchName: string;
192
+ }): ProjectCheckoutState;
193
+ /**
194
+ * Partition a ready project's configured branches into checkouts a sync cycle
195
+ * may use and checkouts it must skip. A primary clone that is not linked takes
196
+ * every branch with it: nothing can be observed or published through it.
197
+ */
198
+ export declare function inventoryProjectCheckouts(input: {
199
+ projectRoot: string;
200
+ primaryBranchName: string;
201
+ branchNames: readonly string[];
202
+ }): ProjectCheckoutInventory;
203
+ export declare const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
160
204
  export declare function deleteLinkedProjectBranch(input: {
161
205
  projectRoot: string;
162
206
  primaryBranchName: string;
163
207
  branchName: string;
164
208
  }): {
165
209
  branchName: string;
210
+ /** Where a path that was not this project's linked worktree was set aside instead of being deleted. */
211
+ movedAsidePath?: string;
166
212
  };
167
213
  export declare function removeProjectWorktrees(input: {
168
214
  projectRoot: string;
@@ -290,6 +336,7 @@ export declare const projectWorktreesTestHarness: {
290
336
  projectRoot: string;
291
337
  branches: readonly ProjectWorktreeBranch[];
292
338
  temporaryRoot: string;
339
+ scope?: ProjectWorktreeSnapshotScope;
293
340
  }): {
294
341
  root: string;
295
342
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.103",
3
+ "version": "0.0.104",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",