@ricsam/r5d-worker 0.0.78 → 0.0.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/main.cjs +1550 -335
  3. package/dist/cjs/managed-paths.cjs +101 -1
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-workspace-state.cjs +184 -5
  6. package/dist/cjs/project-worktrees.cjs +676 -58
  7. package/dist/cjs/registry-auth.cjs +310 -0
  8. package/dist/cjs/repository-transition-policy.cjs +49 -0
  9. package/dist/cjs/supervisor.cjs +62 -11
  10. package/dist/cjs/working-tree-mirror.cjs +196 -36
  11. package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
  12. package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
  14. package/dist/cjs/workspace-git-sync.cjs +1037 -54
  15. package/dist/cjs/workspace-mount-boundary.cjs +71 -0
  16. package/dist/cjs/workspace-path-move.cjs +195 -0
  17. package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
  18. package/dist/cjs/workspace-project-config-policy.cjs +46 -0
  19. package/dist/cjs/workspace-publication-evidence.cjs +46 -0
  20. package/dist/mjs/main.mjs +1584 -341
  21. package/dist/mjs/managed-paths.mjs +100 -1
  22. package/dist/mjs/package.json +1 -1
  23. package/dist/mjs/project-workspace-state.mjs +181 -5
  24. package/dist/mjs/project-worktrees.mjs +667 -57
  25. package/dist/mjs/registry-auth.mjs +269 -0
  26. package/dist/mjs/repository-transition-policy.mjs +22 -0
  27. package/dist/mjs/supervisor.mjs +61 -11
  28. package/dist/mjs/working-tree-mirror.mjs +195 -36
  29. package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
  30. package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
  31. package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
  32. package/dist/mjs/workspace-git-sync.mjs +1032 -54
  33. package/dist/mjs/workspace-mount-boundary.mjs +37 -0
  34. package/dist/mjs/workspace-path-move.mjs +160 -0
  35. package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
  36. package/dist/mjs/workspace-project-config-policy.mjs +21 -0
  37. package/dist/mjs/workspace-publication-evidence.mjs +21 -0
  38. package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
  39. package/dist/types/main.d.ts +175 -1
  40. package/dist/types/managed-paths.d.ts +11 -0
  41. package/dist/types/project-workspace-state.d.ts +45 -1
  42. package/dist/types/project-worktrees.d.ts +119 -2
  43. package/dist/types/registry-auth.d.ts +47 -0
  44. package/dist/types/repository-transition-policy.d.ts +26 -0
  45. package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
  46. package/dist/types/supervisor-signal-fixture.d.ts +1 -0
  47. package/dist/types/supervisor.d.ts +6 -4
  48. package/dist/types/working-tree-mirror.d.ts +14 -0
  49. package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
  50. package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
  51. package/dist/types/workspace-command-sync-policy.d.ts +9 -0
  52. package/dist/types/workspace-git-sync.d.ts +33 -3
  53. package/dist/types/workspace-mount-boundary.d.ts +10 -0
  54. package/dist/types/workspace-path-move.d.ts +21 -0
  55. package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
  56. package/dist/types/workspace-project-config-policy.d.ts +34 -0
  57. package/dist/types/workspace-publication-evidence.d.ts +17 -0
  58. package/package.json +1 -1
package/dist/mjs/main.mjs CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { createHash } from "node:crypto";
5
5
  import os, { hostname } from "node:os";
6
6
  import { spawn as spawnChildProcess } from "node:child_process";
7
+ import { Database } from "bun:sqlite";
7
8
  import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
8
9
  import { gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
9
10
  import {
@@ -14,40 +15,79 @@ import {
14
15
  import { terminateProcessTree } from "./process-tree.mjs";
15
16
  import { createPtyOutputCoalescer } from "./pty-output-coalescer.mjs";
16
17
  import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
18
+ import {
19
+ configureGitHubRegistryAuthFiles,
20
+ planGitHubRegistryAuthFiles,
21
+ preparePrivateAuthFileGeneration,
22
+ registryAuthHasAppManagedGitHubCredential,
23
+ RegistryAuthConfigurationError
24
+ } from "./registry-auth.mjs";
17
25
  import { applyWorkspaceIncidentUpdate } from "./workspace-incident-state.mjs";
18
- import { acquireWorkspaceCommandMutation, runWorkspaceCommand } from "./workspace-command-sync-policy.mjs";
26
+ import {
27
+ hasActiveVisibleProjectsWorkspaceTarget,
28
+ PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS,
29
+ pendingCreatedBranchAutomaticSyncDeferral,
30
+ pendingCreatedBranchKey,
31
+ projectBranchHasActiveWorkspaceTarget
32
+ } from "./workspace-automatic-sync-policy.mjs";
33
+ import {
34
+ acquireWorkspaceCommandMutation,
35
+ reserveWorkspaceCommandAfterCurrentSync,
36
+ runWorkspaceCommand
37
+ } from "./workspace-command-sync-policy.mjs";
19
38
  import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
39
+ import { checkoutPathMovePublicationComplete, preserveProjectCheckoutPathMove } from "./workspace-path-move.mjs";
40
+ import { busyProjectConfigurationChangeIds, deferredProjectConfigurationPendingBranches } from "./workspace-project-config-policy.mjs";
41
+ import { creatorLocalProjectBranchIsAuthorized, workerProjectBranchDisposition } from "./workspace-preserve-only-policy.mjs";
42
+ import { assertProjectBranchDeletionIncarnation } from "./workspace-branch-incarnation-policy.mjs";
43
+ import { activeProjectBranchPublicationEvidence } from "./workspace-publication-evidence.mjs";
44
+ import {
45
+ assertRepositoryExecutionEnabled,
46
+ assertRepositoryTransitionState,
47
+ enabledRepositoryTransitionRequiresReseed,
48
+ repositoryMirrorWritesAllowed
49
+ } from "./repository-transition-policy.mjs";
20
50
  import {
21
51
  isRetryableWorkerServerStatus,
22
52
  superviseWorkerRuntime,
53
+ WORKER_CREDENTIAL_RESTART_EXIT_CODE,
23
54
  WORKER_RECONNECT_DELAY_MS,
24
55
  WORKER_RECONNECT_EXIT_CODE,
25
56
  WORKER_RELOAD_EXIT_CODE,
26
57
  WORKER_RUNTIME_ENV
27
58
  } from "./supervisor.mjs";
28
- import { managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
59
+ import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
29
60
  import {
30
- cleanupStaleProjectWorktreeSnapshots,
31
- createLinkedProjectBranch,
61
+ applyObservedProjectMirrorHeads,
62
+ assertProjectMirrorHeadsPushed,
63
+ recoverStaleProjectWorktreeSnapshots,
64
+ createOrRetryLinkedProjectBranch,
32
65
  deleteLinkedProjectBranch,
33
66
  deleteProjectMirrorBranch,
34
67
  ensureProjectWorktrees,
35
- fastForwardProjectHeadsFromMirror,
68
+ observeProjectMirrorHeads,
69
+ projectBranchMayExistAfterCreateFailure,
70
+ projectOriginBranchName,
36
71
  projectWorktreeConfigurationFingerprint,
37
72
  projectWorktreeOperationInProgress,
38
73
  pushProjectMirrorHeads,
39
74
  removeProjectWorktrees
40
75
  } from "./project-worktrees.mjs";
41
76
  import {
77
+ ProjectWorkspacePendingBranchPathChangeError,
42
78
  ProjectWorkspaceStateStore,
43
79
  pruneAuthoritativelyDesiredBranchDeletions
44
80
  } from "./project-workspace-state.mjs";
45
81
  import {
46
82
  ensureWorkspaceGitClone,
47
83
  hydrateWorkspaceGitMounts,
84
+ recoverWorkspaceGitHydration,
48
85
  resetWorkspaceGit,
49
- synchronizeWorkspaceGit
86
+ synchronizeWorkspaceGit,
87
+ workspaceGitHydrationIsCurrent
50
88
  } from "./workspace-git-sync.mjs";
89
+ class ProjectWorkspaceConfigurationDeferredError extends Error {
90
+ }
51
91
  class WorkerServerUnavailableError extends Error {
52
92
  name = "WorkerServerUnavailableError";
53
93
  }
@@ -60,12 +100,19 @@ const MAX_LINE_LENGTH = 2e3;
60
100
  const WORKSPACE_GIT_QUIET_MS = 5e3;
61
101
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
62
102
  const activeProcesses = /* @__PURE__ */ new Map();
103
+ const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
104
+ const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
105
+ const workspaceSyncPriorityProcessTargets = /* @__PURE__ */ new Map();
106
+ const workspaceSyncPriorityOperationTargets = /* @__PURE__ */ new Map();
63
107
  const pendingProcessTerminals = /* @__PURE__ */ new Map();
64
108
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
65
109
  const activePtys = /* @__PURE__ */ new Map();
110
+ const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
66
111
  let currentWorkerSocket = null;
112
+ let workerAdmissionGeneration = 0;
67
113
  const workspaceMutationGate = new WorkspaceMutationGate();
68
114
  let workspaceSyncQueue = Promise.resolve();
115
+ let startupProjectSnapshotRecoveryCompleted = false;
69
116
  const workspaceSyncSingleFlight = {
70
117
  runExclusive(operation) {
71
118
  const queued = workspaceMutationGate.runSync(operation);
@@ -86,7 +133,29 @@ const workspaceSyncSingleFlight = {
86
133
  }
87
134
  };
88
135
  let githubCredential = null;
136
+ let configuredCredentialGenerationFingerprint = null;
137
+ let pendingCredentialGenerationIntentAtProcessStart = null;
138
+ let bootstrapCredentialGenerationFingerprintAtProcessStart = null;
139
+ let bootSessionIdentifierAtProcessStart = null;
140
+ let machineIdentifierAtProcessStart = null;
141
+ let systemdInvocationIdentifierAtProcessStart = null;
142
+ let initialCredentialBootstrapProvenEmpty = false;
89
143
  let visibleGitIdentity = null;
144
+ class StaleWorkerAdmissionError extends Error {
145
+ constructor() {
146
+ super("Worker command admission was invalidated before child creation");
147
+ this.name = "StaleWorkerAdmissionError";
148
+ }
149
+ }
150
+ function advanceWorkerAdmissionGeneration() {
151
+ workerAdmissionGeneration += 1;
152
+ return workerAdmissionGeneration;
153
+ }
154
+ function assertWorkerChildAdmission(input) {
155
+ if (input.capturedGeneration !== input.currentGeneration || !input.sourceSocketIsCurrent || !input.sourceSocketOpen || !input.workspaceConfigured || !input.runtimeHealthy) {
156
+ throw new StaleWorkerAdmissionError();
157
+ }
158
+ }
90
159
  function defaultConfigPath() {
91
160
  return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
92
161
  }
@@ -788,6 +857,183 @@ function readOnlyCredentialStoreHelper(storePath) {
788
857
  function credentialStoreDirectory() {
789
858
  return path.join(os.homedir(), ".r5d", "git-credentials");
790
859
  }
860
+ function credentialGenerationReceiptPath(directoryPath = credentialStoreDirectory()) {
861
+ return path.join(directoryPath, ".auth-generation");
862
+ }
863
+ function pendingCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
864
+ return path.join(directoryPath, ".auth-generation-pending");
865
+ }
866
+ function bootstrapCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
867
+ return path.join(directoryPath, ".auth-generation-bootstrap");
868
+ }
869
+ const BOOT_SESSION_IDENTIFIER_PATTERN = /^(?:linux:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|darwin:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
870
+ const MACHINE_IDENTIFIER_PATTERN = /^(?:linux:[0-9a-f]{32}|darwin:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|win32:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
871
+ const SYSTEMD_INVOCATION_IDENTIFIER_PATTERN = /^[0-9a-f]{32}$/;
872
+ function exactObjectKeys(value, expected) {
873
+ const actual = Object.keys(value).sort();
874
+ const expectedKeys = [...expected].sort();
875
+ return actual.length === expectedKeys.length && actual.every((key, index) => key === expectedKeys[index]);
876
+ }
877
+ function pendingCredentialGenerationIntentContent(intent) {
878
+ return `${JSON.stringify({ version: 1, fingerprint: intent.fingerprint, boundary: intent.boundary })}
879
+ `;
880
+ }
881
+ function readPendingCredentialGenerationIntent(intentPath = pendingCredentialGenerationPath()) {
882
+ const stat = lstatIfExists(intentPath);
883
+ if (!stat) return null;
884
+ if (stat.isSymbolicLink() || !stat.isFile()) {
885
+ throw new RegistryAuthConfigurationError(new Error(`Pending credential generation intent must be a regular file: ${intentPath}`));
886
+ }
887
+ try {
888
+ const parsed = JSON.parse(fs.readFileSync(intentPath, "utf8"));
889
+ const boundary = parsed.boundary;
890
+ if (!parsed || Array.isArray(parsed) || !exactObjectKeys(parsed, ["boundary", "fingerprint", "version"]) || parsed.version !== 1 || typeof parsed.fingerprint !== "string" || !/^[0-9a-f]{64}$/.test(parsed.fingerprint) || !boundary || typeof boundary !== "object" || Array.isArray(boundary)) {
891
+ throw new Error("invalid intent fields");
892
+ }
893
+ const boundaryRecord = boundary;
894
+ const machineId = boundaryRecord.machineId;
895
+ if (!(machineId === null || typeof machineId === "string" && MACHINE_IDENTIFIER_PATTERN.test(machineId))) {
896
+ throw new Error("invalid machine identifier");
897
+ }
898
+ let parsedBoundary;
899
+ if (boundaryRecord.kind === "systemd" && exactObjectKeys(boundaryRecord, ["invocationId", "kind", "machineId"]) && typeof boundaryRecord.invocationId === "string" && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(boundaryRecord.invocationId)) {
900
+ parsedBoundary = {
901
+ kind: "systemd",
902
+ machineId,
903
+ invocationId: boundaryRecord.invocationId
904
+ };
905
+ } else if (boundaryRecord.kind === "boot" && exactObjectKeys(boundaryRecord, ["bootSessionId", "kind", "machineId"]) && (boundaryRecord.bootSessionId === null || typeof boundaryRecord.bootSessionId === "string" && BOOT_SESSION_IDENTIFIER_PATTERN.test(boundaryRecord.bootSessionId))) {
906
+ parsedBoundary = {
907
+ kind: "boot",
908
+ machineId,
909
+ bootSessionId: boundaryRecord.bootSessionId
910
+ };
911
+ } else {
912
+ throw new Error("invalid credential boundary");
913
+ }
914
+ return { fingerprint: parsed.fingerprint, boundary: parsedBoundary };
915
+ } catch (error) {
916
+ throw new RegistryAuthConfigurationError(
917
+ new Error(`Pending credential generation intent is malformed: ${intentPath}`, { cause: error })
918
+ );
919
+ }
920
+ }
921
+ function parseBootSessionIdentifier(probe) {
922
+ const value = probe.kernelValue.trim();
923
+ if (probe.platform === "linux") {
924
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? `linux:${value.toLowerCase()}` : null;
925
+ }
926
+ if (probe.platform === "darwin") {
927
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? `darwin:${value.toLowerCase()}` : null;
928
+ }
929
+ return null;
930
+ }
931
+ function readBootSessionIdentifier(probe) {
932
+ if (probe) return parseBootSessionIdentifier(probe);
933
+ try {
934
+ if (process.platform === "linux") {
935
+ return parseBootSessionIdentifier({
936
+ platform: process.platform,
937
+ kernelValue: fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8")
938
+ });
939
+ }
940
+ const command = process.platform === "darwin" ? ["/usr/sbin/sysctl", "-n", "kern.bootsessionuuid"] : null;
941
+ if (!command) return null;
942
+ const result = Bun.spawnSync(command, {
943
+ stdout: "pipe",
944
+ stderr: "pipe",
945
+ env: workerGitProcessEnvironment(),
946
+ timeout: 2e3
947
+ });
948
+ if (result.exitCode !== 0) return null;
949
+ return parseBootSessionIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
950
+ } catch {
951
+ return null;
952
+ }
953
+ }
954
+ function parseMachineIdentifier(probe) {
955
+ const value = probe.kernelValue.trim();
956
+ if (probe.platform === "linux") {
957
+ return /^[0-9a-f]{32}$/i.test(value) ? `linux:${value.toLowerCase()}` : null;
958
+ }
959
+ const uuid = probe.platform === "darwin" ? /["']?IOPlatformUUID["']?\s*=\s*["']([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})["']/i.exec(value)?.[1] : probe.platform === "win32" ? /^\{?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}?$/i.exec(value)?.[1] : null;
960
+ return uuid ? `${probe.platform}:${uuid.toLowerCase()}` : null;
961
+ }
962
+ function readMachineIdentifier(probe) {
963
+ if (probe) return parseMachineIdentifier(probe);
964
+ try {
965
+ if (process.platform === "linux") {
966
+ return parseMachineIdentifier({ platform: process.platform, kernelValue: fs.readFileSync("/etc/machine-id", "utf8") });
967
+ }
968
+ const command = process.platform === "darwin" ? ["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"] : null;
969
+ if (!command) return null;
970
+ const result = Bun.spawnSync(command, {
971
+ stdout: "pipe",
972
+ stderr: "pipe",
973
+ env: workerGitProcessEnvironment(),
974
+ timeout: 2e3
975
+ });
976
+ if (result.exitCode !== 0) return null;
977
+ return parseMachineIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
978
+ } catch {
979
+ return null;
980
+ }
981
+ }
982
+ function credentialAuthorityLockPath(rootPath = path.join(os.homedir(), ".r5d")) {
983
+ return path.join(rootPath, ".credential-authority.sqlite");
984
+ }
985
+ function acquireCredentialAuthorityLock(input) {
986
+ const rootPath = path.resolve(input.rootPath ?? path.join(os.homedir(), ".r5d"));
987
+ const rootStat = lstatIfExists(rootPath);
988
+ if (rootStat) {
989
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
990
+ throw new Error(`Worker credential authority root must be a directory: ${rootPath}`);
991
+ }
992
+ } else {
993
+ fs.mkdirSync(rootPath, { recursive: true, mode: 448 });
994
+ }
995
+ fs.chmodSync(rootPath, 448);
996
+ const lockPath = credentialAuthorityLockPath(rootPath);
997
+ const existing = lstatIfExists(lockPath);
998
+ if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
999
+ throw new Error(`Worker credential authority database must be a regular file: ${lockPath}`);
1000
+ }
1001
+ const database = new Database(lockPath, { create: true, strict: true });
1002
+ try {
1003
+ database.exec("PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE");
1004
+ database.exec("CREATE TABLE IF NOT EXISTS authority_metadata (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), machine_id TEXT)");
1005
+ database.exec("BEGIN IMMEDIATE");
1006
+ try {
1007
+ const row = database.query("SELECT machine_id AS machineId FROM authority_metadata WHERE singleton = 1").get();
1008
+ if (row) {
1009
+ if (row.machineId === null || input.machineId === null) {
1010
+ if (row.machineId !== input.machineId) {
1011
+ throw new Error("Cannot prove that the worker credential authority database belongs to this machine");
1012
+ }
1013
+ } else if (row.machineId !== input.machineId) {
1014
+ throw new Error("Worker credential authority home belongs to a different machine; refusing to move or share credential state");
1015
+ }
1016
+ } else {
1017
+ database.query("INSERT INTO authority_metadata (singleton, machine_id) VALUES (1, ?)").run(input.machineId);
1018
+ }
1019
+ database.exec("COMMIT");
1020
+ } catch (error) {
1021
+ database.exec("ROLLBACK");
1022
+ throw error;
1023
+ }
1024
+ database.exec("BEGIN EXCLUSIVE");
1025
+ return { lockPath, database };
1026
+ } catch (error) {
1027
+ database.close(false);
1028
+ if (error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message)) {
1029
+ throw new Error("Another r5d-worker credential authority is already running under this OS account", { cause: error });
1030
+ }
1031
+ throw error;
1032
+ }
1033
+ }
1034
+ function releaseCredentialAuthorityLockHandle(handle) {
1035
+ handle.database.close(false);
1036
+ }
791
1037
  function legacySharedCredentialStorePath() {
792
1038
  return path.join(os.homedir(), ".r5d", "github-credentials");
793
1039
  }
@@ -818,9 +1064,24 @@ function ensurePrivateCredentialStoreDirectory(directoryPath) {
818
1064
  fs.chmodSync(directoryPath, 448);
819
1065
  }
820
1066
  function removeLegacySharedCredentialStore(storePath = legacySharedCredentialStorePath()) {
821
- fs.rmSync(storePath, { force: true });
1067
+ const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
1068
+ prepared.commit();
822
1069
  }
823
1070
  let credentialStoreDirectoryInitialized = false;
1071
+ let credentialAuthorityLockHandle = null;
1072
+ function releaseCredentialAuthorityLock() {
1073
+ const handle = credentialAuthorityLockHandle;
1074
+ credentialAuthorityLockHandle = null;
1075
+ if (!handle) return;
1076
+ try {
1077
+ releaseCredentialAuthorityLockHandle(handle);
1078
+ } catch (error) {
1079
+ process.stderr.write(
1080
+ `[r5d-worker] could not release credential authority lock: ${error instanceof Error ? error.message : String(error)}
1081
+ `
1082
+ );
1083
+ }
1084
+ }
824
1085
  function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory()) {
825
1086
  const stat = lstatIfExists(directoryPath);
826
1087
  if (stat) {
@@ -831,11 +1092,73 @@ function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory(
831
1092
  }
832
1093
  function initializeCredentialStoreDirectoryOnce() {
833
1094
  if (credentialStoreDirectoryInitialized) return;
834
- removeLegacySharedCredentialStore();
835
- resetCredentialStoreDirectory();
1095
+ systemdInvocationIdentifierAtProcessStart = SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? process.env.INVOCATION_ID.toLowerCase() : null;
1096
+ bootSessionIdentifierAtProcessStart = readBootSessionIdentifier();
1097
+ machineIdentifierAtProcessStart = readMachineIdentifier();
1098
+ credentialAuthorityLockHandle = acquireCredentialAuthorityLock({
1099
+ machineId: machineIdentifierAtProcessStart
1100
+ });
1101
+ process.once("exit", releaseCredentialAuthorityLock);
1102
+ initialCredentialBootstrapProvenEmpty = noPriorCredentialArtifactsAtProcessStart();
1103
+ ensurePrivateCredentialStoreDirectory(credentialStoreDirectory());
1104
+ configuredCredentialGenerationFingerprint = readCredentialGenerationReceipt();
1105
+ pendingCredentialGenerationIntentAtProcessStart = readPendingCredentialGenerationIntent();
1106
+ bootstrapCredentialGenerationFingerprintAtProcessStart = readCredentialGenerationReceipt(bootstrapCredentialGenerationPath());
836
1107
  credentialStoreDirectoryInitialized = true;
837
1108
  }
1109
+ function noPriorCredentialArtifactsAtProcessStart() {
1110
+ if (lstatIfExists(credentialStoreDirectory()) || lstatIfExists(legacySharedCredentialStorePath())) return false;
1111
+ try {
1112
+ return ![dockerConfigPath(), containersAuthPath()].some(registryAuthHasAppManagedGitHubCredential);
1113
+ } catch {
1114
+ return false;
1115
+ }
1116
+ }
1117
+ function readCredentialGenerationReceipt(receiptPath = credentialGenerationReceiptPath()) {
1118
+ const stat = lstatIfExists(receiptPath);
1119
+ if (!stat) return null;
1120
+ if (stat.isSymbolicLink() || !stat.isFile()) {
1121
+ throw new RegistryAuthConfigurationError(new Error(`Credential generation receipt must be a regular file: ${receiptPath}`));
1122
+ }
1123
+ const content = fs.readFileSync(receiptPath, "utf8");
1124
+ const match = /^([0-9a-f]{64})\n$/.exec(content);
1125
+ if (!match) {
1126
+ throw new RegistryAuthConfigurationError(new Error(`Credential generation receipt is malformed: ${receiptPath}`));
1127
+ }
1128
+ return match[1];
1129
+ }
838
1130
  const APP_CREDENTIAL_STORE_FILE = /^(?:project|workspace)-[0-9a-f]{64}\.store$/;
1131
+ function planCredentialStoreGeneration(desiredFiles, directoryPath = credentialStoreDirectory()) {
1132
+ ensurePrivateCredentialStoreDirectory(directoryPath);
1133
+ const resolvedDirectory = path.resolve(directoryPath);
1134
+ const updates = /* @__PURE__ */ new Map();
1135
+ const symlinkRemovals = /* @__PURE__ */ new Set();
1136
+ for (const [storePath, content] of desiredFiles) {
1137
+ const resolvedStorePath = path.resolve(storePath);
1138
+ if (path.dirname(resolvedStorePath) !== resolvedDirectory || !APP_CREDENTIAL_STORE_FILE.test(path.basename(resolvedStorePath))) {
1139
+ throw new Error("Invalid app-owned Git credential-store path");
1140
+ }
1141
+ updates.set(resolvedStorePath, content);
1142
+ }
1143
+ for (const name of fs.readdirSync(directoryPath)) {
1144
+ if (!APP_CREDENTIAL_STORE_FILE.test(name)) continue;
1145
+ const storePath = path.join(resolvedDirectory, name);
1146
+ const stat = fs.lstatSync(storePath);
1147
+ if (stat.isSymbolicLink()) {
1148
+ if (updates.has(storePath)) throw new Error(`Required Git credential store must not be a symlink: ${storePath}`);
1149
+ updates.set(storePath, null);
1150
+ symlinkRemovals.add(storePath);
1151
+ continue;
1152
+ }
1153
+ if (!stat.isFile()) throw new Error(`Git credential store must be a regular file: ${storePath}`);
1154
+ if (!updates.has(storePath)) updates.set(storePath, null);
1155
+ }
1156
+ return [...updates].map(([filePath, content]) => ({
1157
+ filePath,
1158
+ content,
1159
+ ...symlinkRemovals.has(filePath) ? { allowSymlinkRemoval: true } : {}
1160
+ }));
1161
+ }
839
1162
  function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentialStoreDirectory()) {
840
1163
  ensurePrivateCredentialStoreDirectory(directoryPath);
841
1164
  const resolvedDirectory = path.resolve(directoryPath);
@@ -853,9 +1176,10 @@ function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentia
853
1176
  const storePath = path.join(directoryPath, name);
854
1177
  const stat = fs.lstatSync(storePath);
855
1178
  if (!stat.isFile() && !stat.isSymbolicLink()) continue;
856
- fs.unlinkSync(storePath);
857
1179
  removed.push(storePath);
858
1180
  }
1181
+ const prepared = preparePrivateAuthFileGeneration(removed.map((filePath) => ({ filePath, content: null, allowSymlinkRemoval: true })));
1182
+ prepared.commit();
859
1183
  return removed;
860
1184
  }
861
1185
  function removeProjectCredentialStore(projectId) {
@@ -863,7 +1187,8 @@ function removeProjectCredentialStore(projectId) {
863
1187
  const stat = lstatIfExists(storePath);
864
1188
  if (!stat) return;
865
1189
  if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("Git credential store must be a regular file");
866
- fs.unlinkSync(storePath);
1190
+ const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
1191
+ prepared.commit();
867
1192
  }
868
1193
  function decodeGitAuthHeader(authHeader) {
869
1194
  const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
@@ -883,16 +1208,10 @@ function credentialUsernameForAuthHeader(authHeader) {
883
1208
  return credentials.username;
884
1209
  }
885
1210
  function replaceCredentialStoreFile(storePath, content) {
886
- const temporaryPath = path.join(path.dirname(storePath), `.${path.basename(storePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
887
- try {
888
- fs.writeFileSync(temporaryPath, content, { flag: "wx", mode: 384 });
889
- fs.chmodSync(temporaryPath, 384);
890
- fs.renameSync(temporaryPath, storePath);
891
- } finally {
892
- fs.rmSync(temporaryPath, { force: true });
893
- }
1211
+ const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content }]);
1212
+ prepared.commit();
894
1213
  }
895
- function writeCredentialStoreEntries(remotes, storePath) {
1214
+ function credentialStoreContent(remotes) {
896
1215
  const authoritativeEntries = /* @__PURE__ */ new Map();
897
1216
  for (const input of remotes) {
898
1217
  let remote;
@@ -924,23 +1243,179 @@ function writeCredentialStoreEntries(remotes, storePath) {
924
1243
  }
925
1244
  authoritativeEntries.set(hostKey, replacement);
926
1245
  }
927
- const storeStat = lstatIfExists(storePath);
928
- if (storeStat) {
929
- if (storeStat.isSymbolicLink() || !storeStat.isFile()) throw new Error("Git credential store must be a regular file");
930
- }
931
1246
  const replacements = [...authoritativeEntries.values()].filter((entry) => entry !== null);
932
- if (replacements.length === 0) {
933
- if (storeStat) fs.unlinkSync(storePath);
934
- return null;
1247
+ return replacements.length > 0 ? `${replacements.join("\n")}
1248
+ ` : null;
1249
+ }
1250
+ function writeCredentialStoreEntries(remotes, storePath) {
1251
+ const content = credentialStoreContent(remotes);
1252
+ const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content }]);
1253
+ prepared.commit();
1254
+ return content === null ? null : { hasCredentials: true, storePath };
1255
+ }
1256
+ function credentialHelperForRemotes(remotes, storePath, desiredFiles) {
1257
+ if (!desiredFiles) {
1258
+ const credentialStore = writeCredentialStoreEntries(remotes, storePath);
1259
+ return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
1260
+ }
1261
+ const resolvedStorePath = path.resolve(storePath);
1262
+ const content = credentialStoreContent(remotes);
1263
+ const previous = desiredFiles.get(resolvedStorePath);
1264
+ if (desiredFiles.has(resolvedStorePath) && previous !== content) {
1265
+ throw new Error(`Conflicting desired Git credentials for ${resolvedStorePath}`);
1266
+ }
1267
+ desiredFiles.set(resolvedStorePath, content);
1268
+ return content === null ? null : readOnlyCredentialStoreHelper(resolvedStorePath);
1269
+ }
1270
+ function authenticatedRemoteCredentialSecret(remoteUrl, authHeader) {
1271
+ if (authHeader == null) return null;
1272
+ let remote;
1273
+ try {
1274
+ remote = new URL(remoteUrl);
1275
+ } catch {
1276
+ throw new Error("Invalid authenticated Git remote URL");
935
1277
  }
936
- ensurePrivateCredentialStoreDirectory(path.dirname(storePath));
937
- replaceCredentialStoreFile(storePath, `${replacements.join("\n")}
938
- `);
939
- return { hasCredentials: true, storePath };
1278
+ if (remote.protocol !== "https:" && remote.protocol !== "http:") {
1279
+ throw new Error("Unsupported authenticated Git remote protocol");
1280
+ }
1281
+ if (remote.username || remote.password) throw new Error("Git credentials must not be embedded in a remote URL");
1282
+ const credentials = decodeGitAuthHeader(authHeader);
1283
+ if (!credentials) throw new Error("Unsupported Git authorization header");
1284
+ return credentials.password;
1285
+ }
1286
+ function credentialMaterialCommitment(kind, values) {
1287
+ return createHash("sha256").update(JSON.stringify([`r5d-credential-${kind}-v1`, ...values])).digest("hex");
1288
+ }
1289
+ function credentialGenerationFingerprint(config) {
1290
+ const workerApiUrl = new URL(config.workerBaseUrl);
1291
+ if (workerApiUrl.protocol !== "https:" && workerApiUrl.protocol !== "http:") {
1292
+ throw new Error("Unsupported worker API protocol");
1293
+ }
1294
+ const workerCredentials = decodeGitAuthHeader(config.workerAuthHeader);
1295
+ if (!workerCredentials) throw new Error("Unsupported worker authorization header");
1296
+ const materialCommitments = /* @__PURE__ */ new Set();
1297
+ const addSecret = (secret) => {
1298
+ materialCommitments.add(`secret:${credentialMaterialCommitment("secret", [secret])}`);
1299
+ };
1300
+ addSecret(workerCredentials.password);
1301
+ const workspaceSecret = authenticatedRemoteCredentialSecret(config.workspaceRemoteUrl, config.workerAuthHeader);
1302
+ if (workspaceSecret) addSecret(workspaceSecret);
1303
+ for (const project of config.projects) {
1304
+ if (!project.repoHttpUrl) continue;
1305
+ const secret = authenticatedRemoteCredentialSecret(project.repoHttpUrl, project.repoAuthHeader);
1306
+ if (secret) addSecret(secret);
1307
+ }
1308
+ if (config.githubCredential) {
1309
+ addSecret(config.githubCredential.token);
1310
+ materialCommitments.add(
1311
+ `github_authority:${credentialMaterialCommitment("github_authority", [
1312
+ config.githubCredential.token,
1313
+ ...[...config.githubCredential.missingScopes].sort()
1314
+ ])}`
1315
+ );
1316
+ }
1317
+ return createHash("sha256").update(JSON.stringify([...materialCommitments].sort())).digest("hex");
1318
+ }
1319
+ function credentialGenerationTransitionPhase(input) {
1320
+ if (input.committedFingerprint === input.incomingFingerprint) return "current";
1321
+ if (input.pendingFingerprintAtProcessStart !== null) {
1322
+ return input.pendingFingerprintAtProcessStart === input.incomingFingerprint ? "publish_from_clean_restart" : "stage_pending_restart";
1323
+ }
1324
+ return "stage_pending_restart";
1325
+ }
1326
+ function initialCredentialBootstrapIsSafe(input) {
1327
+ return input.transitionPhase === "stage_pending_restart" && input.committedFingerprint === null && input.pendingFingerprintAtProcessStart === null && (input.bootstrapFingerprintAtProcessStart === null && input.credentialArtifactsProvenAbsentAtProcessStart || input.bootstrapFingerprintAtProcessStart === input.incomingFingerprint) && input.trackedChildTerminationProven && input.activeProcessCount === 0 && input.credentialProcessGroupCount === 0 && input.activePtyCount === 0;
940
1328
  }
941
- function credentialHelperForRemotes(remotes, storePath) {
942
- const credentialStore = writeCredentialStoreEntries(remotes, storePath);
943
- return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
1329
+ async function fenceCredentialGenerationAtConfigureEntry(input) {
1330
+ if (input.currentFingerprint === input.incomingFingerprint) return false;
1331
+ input.revokeInMemory();
1332
+ await input.terminatePreviousGeneration();
1333
+ return true;
1334
+ }
1335
+ function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
1336
+ if (!required) return;
1337
+ fatalExit(exitCode);
1338
+ throw new Error("Credential generation restart callback returned unexpectedly");
1339
+ }
1340
+ function pendingCredentialGenerationBoundary(input) {
1341
+ if (input.systemdContract && input.systemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.systemdInvocationId)) {
1342
+ return { kind: "systemd", machineId: input.machineId, invocationId: input.systemdInvocationId };
1343
+ }
1344
+ return { kind: "boot", machineId: input.machineId, bootSessionId: input.bootSessionId };
1345
+ }
1346
+ function credentialGenerationBoundaryIsComplete(boundary) {
1347
+ return boundary.machineId !== null && (boundary.kind === "systemd" || boundary.bootSessionId !== null);
1348
+ }
1349
+ function credentialGenerationRestartIsContained(input) {
1350
+ const pending = input.pendingBoundary;
1351
+ if (pending === null || pending.machineId === null || input.currentMachineId === null || pending.machineId !== input.currentMachineId) {
1352
+ return false;
1353
+ }
1354
+ if (pending.kind === "systemd") {
1355
+ return input.systemdContract && input.currentSystemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.currentSystemdInvocationId) && pending.invocationId !== input.currentSystemdInvocationId;
1356
+ }
1357
+ return pending.bootSessionId !== null && input.currentBootSessionId !== null && pending.bootSessionId !== input.currentBootSessionId;
1358
+ }
1359
+ function credentialReapContractStatus(probe) {
1360
+ let resolved = probe;
1361
+ if (!resolved) {
1362
+ if (process.platform !== "linux") return "direct";
1363
+ try {
1364
+ const cgroupText = fs.readFileSync("/proc/self/cgroup", "utf8");
1365
+ const unit = cgroupText.match(/(?:^|\/)r5d-worker\.service(?:$|\n)/m)?.[0]?.replace(/^\//, "").trim();
1366
+ if (unit !== "r5d-worker.service") {
1367
+ return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
1368
+ }
1369
+ const result = Bun.spawnSync(
1370
+ [
1371
+ "/usr/bin/systemctl",
1372
+ "show",
1373
+ unit,
1374
+ "--no-pager",
1375
+ "--property=KillMode",
1376
+ "--property=Restart",
1377
+ "--property=SendSIGKILL",
1378
+ "--property=InvocationID"
1379
+ ],
1380
+ {
1381
+ stdout: "pipe",
1382
+ stderr: "pipe",
1383
+ env: workerGitProcessEnvironment(),
1384
+ timeout: 2e3
1385
+ }
1386
+ );
1387
+ if (result.exitCode !== 0) return "systemd_unverified";
1388
+ resolved = {
1389
+ platform: process.platform,
1390
+ invocationId: process.env.INVOCATION_ID,
1391
+ cgroupText,
1392
+ unitProperties: result.stdout.toString()
1393
+ };
1394
+ } catch {
1395
+ return "systemd_unverified";
1396
+ }
1397
+ }
1398
+ if (resolved.platform !== "linux") return "direct";
1399
+ if (!/(?:^|\/)r5d-worker\.service(?:$|\n)/m.test(resolved.cgroupText)) {
1400
+ return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
1401
+ }
1402
+ if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
1403
+ const properties = new Map(
1404
+ resolved.unitProperties.split(/\r?\n/).filter(Boolean).map((line) => {
1405
+ const separator = line.indexOf("=");
1406
+ return separator < 1 ? [line, ""] : [line.slice(0, separator), line.slice(separator + 1)];
1407
+ })
1408
+ );
1409
+ return properties.get("KillMode") === "control-group" && properties.get("Restart") === "always" && properties.get("SendSIGKILL") === "yes" && properties.get("InvocationID")?.toLowerCase() === resolved.invocationId?.toLowerCase() ? "verified_systemd" : "systemd_unverified";
1410
+ }
1411
+ function verifiedCredentialReapContract(probe) {
1412
+ return credentialReapContractStatus(probe) === "verified_systemd";
1413
+ }
1414
+ function fenceUnsafeWorkspaceSyncFailure(input) {
1415
+ if (input.hydrationCurrent) return false;
1416
+ input.invalidateExecution();
1417
+ requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
1418
+ return true;
944
1419
  }
945
1420
  const workerGitSecurityTestHarness = {
946
1421
  commandArgs: workerGitCommand.commandArgs,
@@ -952,7 +1427,45 @@ const workerGitSecurityTestHarness = {
952
1427
  pruneCredentialStoreFiles,
953
1428
  readOnlyCredentialStoreHelper,
954
1429
  resetCredentialStoreDirectory,
955
- replaceCredentialStoreFile
1430
+ replaceCredentialStoreFile,
1431
+ configureGitHubAuth(credential, filePaths, configureFiles = configureGitHubRegistryAuthFiles) {
1432
+ configureGitHubAuth(credential, filePaths, configureFiles);
1433
+ },
1434
+ githubCredential: () => githubCredential,
1435
+ credentialGenerationFingerprint,
1436
+ credentialGenerationTransitionPhase,
1437
+ initialCredentialBootstrapIsSafe,
1438
+ fenceCredentialGenerationAtConfigureEntry,
1439
+ requireCredentialGenerationRestart,
1440
+ pendingCredentialGenerationBoundary,
1441
+ credentialGenerationBoundaryIsComplete,
1442
+ credentialGenerationRestartIsContained,
1443
+ credentialReapContractStatus,
1444
+ verifiedCredentialReapContract,
1445
+ fenceUnsafeWorkspaceSyncFailure,
1446
+ assertWorkerChildAdmission,
1447
+ terminateCredentialBearingChildren,
1448
+ terminateCredentialBearingChildrenWithRetention,
1449
+ async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
1450
+ return await commitCredentialGeneration(
1451
+ prepared,
1452
+ credential,
1453
+ async () => await terminateCredentialBearingChildren(children),
1454
+ beforeMutation,
1455
+ previousGenerationFenced
1456
+ );
1457
+ },
1458
+ credentialGenerationReceiptPath,
1459
+ credentialAuthorityLockPath,
1460
+ acquireCredentialAuthorityLock,
1461
+ releaseCredentialAuthorityLockHandle,
1462
+ pendingCredentialGenerationPath,
1463
+ pendingCredentialGenerationIntentContent,
1464
+ readPendingCredentialGenerationIntent,
1465
+ readBootSessionIdentifier,
1466
+ readMachineIdentifier,
1467
+ bootstrapCredentialGenerationPath,
1468
+ readCredentialGenerationReceipt
956
1469
  };
957
1470
  function dockerConfigPath() {
958
1471
  return path.join(os.homedir(), ".docker", "config.json");
@@ -960,39 +1473,14 @@ function dockerConfigPath() {
960
1473
  function containersAuthPath() {
961
1474
  return path.join(os.homedir(), ".config", "containers", "auth.json");
962
1475
  }
963
- function readJsonFile(filePath) {
964
- if (!fs.existsSync(filePath)) {
965
- return {};
966
- }
967
- try {
968
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
969
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
970
- } catch {
971
- return {};
972
- }
973
- }
974
- function writeRegistryAuthFile(filePath, credential) {
975
- const config = readJsonFile(filePath);
976
- const existingAuths = config.auths && typeof config.auths === "object" && !Array.isArray(config.auths) ? config.auths : {};
977
- const auths = existingAuths;
978
- auths[credential.registry] = {
979
- username: credential.username,
980
- password: credential.token,
981
- auth: Buffer.from(`${credential.username}:${credential.token}`).toString("base64")
982
- };
983
- config.auths = auths;
984
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
985
- fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
986
- `, { mode: 384 });
987
- fs.chmodSync(filePath, 384);
1476
+ function configureGitHubAuth(credential, filePaths = [dockerConfigPath(), containersAuthPath()], configureFiles = configureGitHubRegistryAuthFiles) {
1477
+ githubCredential = null;
1478
+ configureFiles(filePaths, credential);
1479
+ installGitHubCredentialGeneration(credential);
988
1480
  }
989
- function configureGitHubAuth(credential) {
1481
+ function installGitHubCredentialGeneration(credential) {
990
1482
  githubCredential = credential;
991
- if (!credential) {
992
- return;
993
- }
994
- writeRegistryAuthFile(dockerConfigPath(), credential);
995
- writeRegistryAuthFile(containersAuthPath(), credential);
1483
+ if (!credential) return;
996
1484
  if (credential.missingScopes.length > 0) {
997
1485
  process.stderr.write(
998
1486
  `[r5d-worker] GitHub token is missing scope(s): ${credential.missingScopes.join(", ")}. Sign in with GitHub again if gh or GHCR access fails.
@@ -1003,6 +1491,21 @@ function configureGitHubAuth(credential) {
1003
1491
  `);
1004
1492
  }
1005
1493
  }
1494
+ async function commitCredentialGeneration(prepared, credential, terminatePreviousGeneration, beforeMutation, previousGenerationFenced = false) {
1495
+ const changed = prepared.changed;
1496
+ try {
1497
+ if (changed && !previousGenerationFenced) {
1498
+ githubCredential = null;
1499
+ await terminatePreviousGeneration();
1500
+ }
1501
+ prepared.commit(beforeMutation);
1502
+ } catch (error) {
1503
+ prepared.discard();
1504
+ throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
1505
+ }
1506
+ installGitHubCredentialGeneration(credential);
1507
+ return changed;
1508
+ }
1006
1509
  function containerRegistryEnv() {
1007
1510
  if (!githubCredential) {
1008
1511
  return {};
@@ -1032,6 +1535,44 @@ function githubProcessEnv() {
1032
1535
  ...githubCliEnv(githubCredential?.token)
1033
1536
  };
1034
1537
  }
1538
+ const INHERITED_WORKER_CREDENTIAL_ENV_KEYS = [
1539
+ WORKER_RUNTIME_ENV,
1540
+ "INVOCATION_ID",
1541
+ "R5D_WORKER_TOKEN",
1542
+ "R5D_TOKEN",
1543
+ "R5DCTL_TOKEN",
1544
+ "R5D_API_KEY",
1545
+ "R5DCTL_API_KEY",
1546
+ "GH_TOKEN",
1547
+ "GITHUB_TOKEN",
1548
+ "GH_ENTERPRISE_TOKEN",
1549
+ "DOCKER_AUTH_CONFIG",
1550
+ "REGISTRY_AUTH_FILE",
1551
+ "R5D_GHCR_AUTH_FILE",
1552
+ "NODE_AUTH_TOKEN",
1553
+ "NPM_TOKEN",
1554
+ "BUN_AUTH_TOKEN"
1555
+ ];
1556
+ function workerChildProcessEnvironment(layers, inherited = process.env, platform = process.platform) {
1557
+ const environment = {};
1558
+ const reservedExact = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS);
1559
+ const reservedFolded = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS.map((key) => key.toLowerCase()));
1560
+ const assignLayer = (source, scrubInheritedCredentials) => {
1561
+ for (const [key, value] of Object.entries(source)) {
1562
+ const folded = key.toLowerCase();
1563
+ if (scrubInheritedCredentials && (platform === "win32" ? reservedFolded.has(folded) : reservedExact.has(key))) continue;
1564
+ if (platform === "win32") {
1565
+ for (const existing of Object.keys(environment)) {
1566
+ if (existing.toLowerCase() === folded) delete environment[existing];
1567
+ }
1568
+ }
1569
+ environment[key] = value;
1570
+ }
1571
+ };
1572
+ assignLayer(inherited, true);
1573
+ for (const layer of layers) assignLayer(layer, false);
1574
+ return environment;
1575
+ }
1035
1576
  function primaryProjectBranch(config) {
1036
1577
  if (config.branches.length === 0) throw new Error(`Project ${config.projectId} has no configured branches`);
1037
1578
  if (config.branches.some(({ branchName }) => branchName === "main")) return "main";
@@ -1059,6 +1600,8 @@ function describeWorkerSessionTarget(target) {
1059
1600
  function resolveWorkerSessionTarget(input) {
1060
1601
  if (input.target.type === "workspace") {
1061
1602
  if (input.target.rootProfile === "visible_projects") {
1603
+ const disabled = [...input.projectConfigById.values()].find(({ executionDisabled }) => executionDisabled);
1604
+ if (disabled) assertRepositoryExecutionEnabled(disabled);
1062
1605
  return { target: input.target, rootPath: input.projectsRoot };
1063
1606
  }
1064
1607
  if (!fs.existsSync(path.join(input.workspaceShadowRoot, ".git"))) {
@@ -1069,6 +1612,7 @@ function resolveWorkerSessionTarget(input) {
1069
1612
  const target = input.target;
1070
1613
  const config = input.projectConfigById.get(target.projectId);
1071
1614
  if (!config) throw new Error(`Project ${target.projectId} is missing from the worker workspace configuration`);
1615
+ assertRepositoryExecutionEnabled(config);
1072
1616
  if (!config.branches.some(({ branchName }) => branchName === target.branchName)) {
1073
1617
  throw new Error(`Project branch ${target.projectId}/${target.branchName} is missing from the worker workspace configuration`);
1074
1618
  }
@@ -1768,6 +2312,7 @@ async function prepareShellEnvForTarget(input) {
1768
2312
  async function executeCommand(input) {
1769
2313
  const startedAt = Date.now();
1770
2314
  let timeout;
2315
+ let spawnedProcess;
1771
2316
  try {
1772
2317
  const targetProcessEnv = await prepareShellEnvForTarget({
1773
2318
  target: input.resolvedTarget.target,
@@ -1779,18 +2324,17 @@ async function executeCommand(input) {
1779
2324
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1780
2325
  });
1781
2326
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
2327
+ input.assertAdmission();
1782
2328
  const subprocess = Bun.spawn(input.message.argv, {
1783
2329
  cwd,
1784
2330
  stdout: "pipe",
1785
2331
  stderr: "pipe",
1786
2332
  detached: true,
1787
- env: {
1788
- ...process.env,
1789
- ...githubProcessEnv(),
1790
- ...input.message.env ?? {},
1791
- ...targetProcessEnv
1792
- }
2333
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
1793
2334
  });
2335
+ spawnedProcess = subprocess;
2336
+ credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2337
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
1794
2338
  activeProcesses.set(input.message.runId, {
1795
2339
  process: subprocess,
1796
2340
  target: input.resolvedTarget.target,
@@ -1827,6 +2371,7 @@ async function executeCommand(input) {
1827
2371
  durationMs: Date.now() - startedAt
1828
2372
  };
1829
2373
  } catch (error) {
2374
+ if (error instanceof StaleWorkerAdmissionError) throw error;
1830
2375
  return {
1831
2376
  type: "exec_result",
1832
2377
  requestId: input.message.requestId,
@@ -1840,6 +2385,9 @@ async function executeCommand(input) {
1840
2385
  if (timeout) {
1841
2386
  clearTimeout(timeout);
1842
2387
  }
2388
+ if (spawnedProcess) {
2389
+ await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2390
+ }
1843
2391
  activeProcesses.delete(input.message.runId);
1844
2392
  }
1845
2393
  }
@@ -1848,6 +2396,7 @@ async function executeStreamingCommand(input) {
1848
2396
  let started = false;
1849
2397
  let timedOut = false;
1850
2398
  let timeout;
2399
+ let spawnedProcess;
1851
2400
  try {
1852
2401
  if (cancelledProcessRuns.delete(input.message.runId)) {
1853
2402
  sendWorkerMessage(input.ws, {
@@ -1869,6 +2418,7 @@ async function executeStreamingCommand(input) {
1869
2418
  });
1870
2419
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
1871
2420
  const interactive = input.message.interactive === true;
2421
+ input.assertAdmission();
1872
2422
  const subprocess = Bun.spawn(input.message.argv, {
1873
2423
  cwd,
1874
2424
  // Without an explicit stdin the process reads /dev/null and interactive
@@ -1877,13 +2427,11 @@ async function executeStreamingCommand(input) {
1877
2427
  stdout: "pipe",
1878
2428
  stderr: "pipe",
1879
2429
  detached: true,
1880
- env: {
1881
- ...process.env,
1882
- ...githubProcessEnv(),
1883
- ...input.message.env ?? {},
1884
- ...targetProcessEnv
1885
- }
2430
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
1886
2431
  });
2432
+ spawnedProcess = subprocess;
2433
+ credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2434
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
1887
2435
  activeProcesses.set(input.message.runId, {
1888
2436
  process: subprocess,
1889
2437
  target: input.resolvedTarget.target,
@@ -1948,6 +2496,7 @@ async function executeStreamingCommand(input) {
1948
2496
  pendingProcessTerminals.set(input.message.runId, terminal);
1949
2497
  sendWorkerMessage(input.ws, terminal);
1950
2498
  } catch (error) {
2499
+ if (!started && error instanceof StaleWorkerAdmissionError) throw error;
1951
2500
  const message = error instanceof Error ? error.message : String(error);
1952
2501
  if (started) {
1953
2502
  const terminal = {
@@ -1971,10 +2520,27 @@ async function executeStreamingCommand(input) {
1971
2520
  clearTimeout(timeout);
1972
2521
  }
1973
2522
  closeProcessStdin(activeProcesses.get(input.message.runId));
2523
+ if (spawnedProcess) {
2524
+ await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2525
+ }
1974
2526
  activeProcesses.delete(input.message.runId);
1975
2527
  cancelledProcessRuns.delete(input.message.runId);
1976
2528
  }
1977
2529
  }
2530
+ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
2531
+ try {
2532
+ await terminateProcessTree(subprocess);
2533
+ if (credentialBearingProcessGroups.get(subprocess.pid) === subprocess) {
2534
+ credentialBearingProcessGroups.delete(subprocess.pid);
2535
+ credentialBearingProcessGroupTargets.delete(subprocess.pid);
2536
+ }
2537
+ } catch (error) {
2538
+ process.stderr.write(
2539
+ `[r5d-worker] retained unreaped credential-bearing process group ${subprocess.pid} from ${runId}: ${error instanceof Error ? error.message : String(error)}
2540
+ `
2541
+ );
2542
+ }
2543
+ }
1978
2544
  function closeProcessStdin(active) {
1979
2545
  if (!active?.stdin) {
1980
2546
  return;
@@ -1994,6 +2560,15 @@ function sendWorkerMessage(ws, message) {
1994
2560
  `);
1995
2561
  }
1996
2562
  }
2563
+ function sendWorkerMessageFromCurrentSource(ws, message) {
2564
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2565
+ try {
2566
+ ws.send(JSON.stringify(message));
2567
+ } catch (error) {
2568
+ process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
2569
+ `);
2570
+ }
2571
+ }
1997
2572
  function buildActiveProcessReports() {
1998
2573
  return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
1999
2574
  runId,
@@ -2073,6 +2648,19 @@ rl.on("line", (line) => {
2073
2648
 
2074
2649
  if (message.type === "close") {
2075
2650
  ptyProcess.kill();
2651
+ return;
2652
+ }
2653
+
2654
+ if (message.type === "force_close") {
2655
+ if (process.platform !== "win32" && Number.isSafeInteger(ptyProcess.pid) && ptyProcess.pid > 0) {
2656
+ try {
2657
+ process.kill(-ptyProcess.pid, "SIGKILL");
2658
+ } catch (error) {
2659
+ if (!error || error.code !== "ESRCH") throw error;
2660
+ }
2661
+ } else {
2662
+ ptyProcess.kill("SIGKILL");
2663
+ }
2076
2664
  }
2077
2665
  } catch (error) {
2078
2666
  send({ type: "error", error: error instanceof Error ? error.message : String(error) });
@@ -2104,14 +2692,26 @@ function nodePtyModulePaths() {
2104
2692
  }
2105
2693
  function createNodePtyBridge(options) {
2106
2694
  const child = spawnChildProcess("node", ["-e", PTY_BRIDGE_SCRIPT], {
2107
- env: {
2108
- ...process.env,
2109
- NODE_PATH: nodePtyModulePaths()
2110
- }
2695
+ env: workerChildProcessEnvironment([{ NODE_PATH: nodePtyModulePaths() }])
2111
2696
  });
2112
2697
  let lineBuffer = "";
2113
2698
  let emittedTerminalEvent = false;
2114
2699
  let stderr = "";
2700
+ let resolveTerminal;
2701
+ const terminal = new Promise((resolve) => {
2702
+ resolveTerminal = resolve;
2703
+ });
2704
+ const markTerminal = () => {
2705
+ resolveTerminal?.();
2706
+ resolveTerminal = void 0;
2707
+ };
2708
+ const terminalWithin = async (timeoutMs) => await Promise.race([
2709
+ terminal.then(() => true),
2710
+ new Promise((resolve) => {
2711
+ const timer = setTimeout(() => resolve(false), timeoutMs);
2712
+ timer.unref();
2713
+ })
2714
+ ]);
2115
2715
  const sendToChild = (message) => {
2116
2716
  if (child.stdin.writable) {
2117
2717
  child.stdin.write(`${JSON.stringify(message)}
@@ -2129,11 +2729,13 @@ function createNodePtyBridge(options) {
2129
2729
  }
2130
2730
  if (event.type === "exit") {
2131
2731
  emittedTerminalEvent = true;
2732
+ markTerminal();
2132
2733
  options.onExit({ exitCode: event.exitCode, signal: event.signal });
2133
2734
  return;
2134
2735
  }
2135
2736
  if (event.type === "error") {
2136
2737
  emittedTerminalEvent = true;
2738
+ markTerminal();
2137
2739
  options.onError(new Error(event.error));
2138
2740
  }
2139
2741
  };
@@ -2162,9 +2764,11 @@ function createNodePtyBridge(options) {
2162
2764
  });
2163
2765
  child.on("error", (error) => {
2164
2766
  emittedTerminalEvent = true;
2767
+ markTerminal();
2165
2768
  options.onError(error);
2166
2769
  });
2167
2770
  child.on("exit", (code, signal) => {
2771
+ markTerminal();
2168
2772
  if (emittedTerminalEvent) {
2169
2773
  return;
2170
2774
  }
@@ -2185,12 +2789,15 @@ function createNodePtyBridge(options) {
2185
2789
  sendToChild({ type: "resize", cols, rows });
2186
2790
  },
2187
2791
  kill() {
2792
+ void this.terminate().catch((error) => options.onTerminationError(error instanceof Error ? error : new Error(String(error))));
2793
+ },
2794
+ async terminate() {
2188
2795
  sendToChild({ type: "close" });
2189
- setTimeout(() => {
2190
- if (!child.killed) {
2191
- child.kill();
2192
- }
2193
- }, 500);
2796
+ if (await terminalWithin(500)) return;
2797
+ sendToChild({ type: "force_close" });
2798
+ if (await terminalWithin(500)) return;
2799
+ child.kill("SIGKILL");
2800
+ if (!await terminalWithin(500)) throw new Error(`PTY bridge ${child.pid ?? "unknown"} did not terminate after SIGKILL`);
2194
2801
  }
2195
2802
  };
2196
2803
  }
@@ -2228,6 +2835,7 @@ async function openPty(input) {
2228
2835
  });
2229
2836
  }
2230
2837
  });
2838
+ input.assertAdmission();
2231
2839
  const ptyProcess = createNodePtyBridge({
2232
2840
  file: shell.file,
2233
2841
  args: shell.args,
@@ -2236,14 +2844,7 @@ async function openPty(input) {
2236
2844
  cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
2237
2845
  rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
2238
2846
  cwd: input.resolvedTarget.rootPath,
2239
- env: {
2240
- ...process.env,
2241
- ...githubProcessEnv(),
2242
- ...input.message.env ?? {},
2243
- ...planProcessEnv,
2244
- ...targetProcessEnv,
2245
- ...shell.env ?? {}
2246
- }
2847
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, planProcessEnv, targetProcessEnv, shell.env ?? {}])
2247
2848
  },
2248
2849
  onOpened: () => {
2249
2850
  sendWorkerMessage(input.ws, {
@@ -2278,6 +2879,14 @@ async function openPty(input) {
2278
2879
  ptyId: input.message.ptyId,
2279
2880
  error: error.message
2280
2881
  });
2882
+ },
2883
+ onTerminationError: (error) => {
2884
+ sendWorkerMessage(input.ws, {
2885
+ type: "pty_error",
2886
+ requestId: input.message.requestId,
2887
+ ptyId: input.message.ptyId,
2888
+ error: error.message
2889
+ });
2281
2890
  }
2282
2891
  });
2283
2892
  activePtys.set(input.message.ptyId, {
@@ -2310,14 +2919,62 @@ function closePty(message) {
2310
2919
  if (!ptyProcess) {
2311
2920
  return;
2312
2921
  }
2313
- activePtys.delete(message.ptyId);
2314
2922
  ptyProcess.kill();
2315
2923
  }
2316
2924
  function closeAllPtys() {
2317
2925
  for (const ptyProcess of activePtys.values()) {
2318
2926
  ptyProcess.kill();
2319
2927
  }
2320
- activePtys.clear();
2928
+ }
2929
+ async function terminateCredentialBearingChildren(children) {
2930
+ const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
2931
+ const failures = results.flatMap(
2932
+ (result, index) => result.status === "rejected" ? [new Error(`Failed to terminate ${children[index].label}`, { cause: result.reason })] : []
2933
+ );
2934
+ if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
2935
+ }
2936
+ async function terminateCredentialBearingChildrenWithRetention(children) {
2937
+ const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
2938
+ const failures = [];
2939
+ results.forEach((result, index) => {
2940
+ const child = children[index];
2941
+ if (result.status === "fulfilled") {
2942
+ child.onTerminated();
2943
+ return;
2944
+ }
2945
+ failures.push(new Error(`Failed to terminate ${child.label}`, { cause: result.reason }));
2946
+ });
2947
+ if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
2948
+ }
2949
+ async function terminateActiveCredentialBearingChildren() {
2950
+ const processGroups = [...credentialBearingProcessGroups.entries()];
2951
+ const ptyEntries = [...activePtys.entries()];
2952
+ for (const active of activeProcesses.values()) closeProcessStdin(active);
2953
+ await terminateCredentialBearingChildrenWithRetention([
2954
+ ...processGroups.map(([processGroupId, subprocess]) => ({
2955
+ label: `process group ${processGroupId}`,
2956
+ terminate: async () => await terminateProcessTree(subprocess),
2957
+ onTerminated: () => {
2958
+ if (credentialBearingProcessGroups.get(processGroupId) === subprocess) {
2959
+ credentialBearingProcessGroups.delete(processGroupId);
2960
+ credentialBearingProcessGroupTargets.delete(processGroupId);
2961
+ }
2962
+ for (const [runId, active] of activeProcesses) {
2963
+ if (active.process === subprocess) activeProcesses.delete(runId);
2964
+ }
2965
+ }
2966
+ })),
2967
+ ...ptyEntries.map(([ptyId, active]) => ({
2968
+ label: `PTY ${ptyId}`,
2969
+ terminate: async () => await active.terminate(),
2970
+ onTerminated: () => {
2971
+ if (activePtys.get(ptyId) === active) activePtys.delete(ptyId);
2972
+ }
2973
+ }))
2974
+ ]);
2975
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || credentialBearingProcessGroupTargets.size > 0 || activePtys.size > 0) {
2976
+ throw new Error("Credential-bearing child ownership remained after termination");
2977
+ }
2321
2978
  }
2322
2979
  function websocketUrl(baseUrl, label) {
2323
2980
  const url = new URL("/worker/ws", baseUrl);
@@ -2327,6 +2984,7 @@ function websocketUrl(baseUrl, label) {
2327
2984
  return url.toString();
2328
2985
  }
2329
2986
  async function startWorker(options) {
2987
+ advanceWorkerAdmissionGeneration();
2330
2988
  process.on("uncaughtException", (error) => {
2331
2989
  process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
2332
2990
  `);
@@ -2345,8 +3003,15 @@ async function startWorker(options) {
2345
3003
  const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
2346
3004
  const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2347
3005
  const workspaceShadowRoot = path.join(syncRoot, "workspace");
3006
+ const projectWorktreeSnapshotsRoot = path.join(syncRoot, "project-worktree-snapshots");
2348
3007
  const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2349
3008
  const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
3009
+ assertDisjointManagedRoots([
3010
+ { label: "projects root", rootPath: projectsRoot },
3011
+ { label: "sync root", rootPath: syncRoot },
3012
+ { label: "artifacts root", rootPath: artifactRoot },
3013
+ { label: "plans root", rootPath: planRoot }
3014
+ ]);
2350
3015
  process.stdout.write(`[r5d-worker] label: ${label}
2351
3016
  `);
2352
3017
  process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
@@ -2359,26 +3024,43 @@ async function startWorker(options) {
2359
3024
  `);
2360
3025
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
2361
3026
  `);
2362
- const snapshotCleanup = cleanupStaleProjectWorktreeSnapshots();
2363
- if (snapshotCleanup.removed.length > 0) {
2364
- process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
2365
- `);
2366
- }
2367
- for (const failure of snapshotCleanup.failed) {
2368
- process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
2369
- `);
2370
- }
2371
3027
  runGit(["--version"]);
2372
3028
  await verifyR5dctlAuth(baseUrl, token);
2373
- fs.mkdirSync(projectsRoot, { recursive: true });
2374
- fs.mkdirSync(syncRoot, { recursive: true });
2375
- fs.mkdirSync(artifactRoot, { recursive: true });
2376
- fs.mkdirSync(planRoot, { recursive: true });
2377
- const projectWorkspaceStateStore = new ProjectWorkspaceStateStore({
2378
- stateRoot: path.join(syncRoot, "project-state"),
2379
- projectsRoot
3029
+ const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
3030
+ if (!startupProjectSnapshotRecoveryCompleted) {
3031
+ const snapshotRecovery = recoverStaleProjectWorktreeSnapshots({
3032
+ projectsRoot,
3033
+ temporaryRoot: projectWorktreeSnapshotsRoot
3034
+ });
3035
+ if (snapshotRecovery.restored.length > 0) {
3036
+ process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
3037
+ `);
3038
+ }
3039
+ if (snapshotRecovery.removed.length > 0) {
3040
+ process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
3041
+ `);
3042
+ }
3043
+ for (const failure of snapshotRecovery.failed) {
3044
+ process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
3045
+ `);
3046
+ }
3047
+ if (snapshotRecovery.failed.length > 0) {
3048
+ throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
3049
+ }
3050
+ startupProjectSnapshotRecoveryCompleted = true;
3051
+ }
3052
+ fs.mkdirSync(projectsRoot, { recursive: true });
3053
+ fs.mkdirSync(syncRoot, { recursive: true });
3054
+ fs.mkdirSync(artifactRoot, { recursive: true });
3055
+ fs.mkdirSync(planRoot, { recursive: true });
3056
+ const store = new ProjectWorkspaceStateStore({
3057
+ stateRoot: path.join(syncRoot, "project-state"),
3058
+ projectsRoot
3059
+ });
3060
+ return { store, state: store.read() };
2380
3061
  });
2381
- let projectWorkspaceState = projectWorkspaceStateStore.read();
3062
+ const projectWorkspaceStateStore = initializedWorkspaceState.store;
3063
+ let projectWorkspaceState = initializedWorkspaceState.state;
2382
3064
  const projectConfigById = /* @__PURE__ */ new Map();
2383
3065
  const pendingCheckouts = /* @__PURE__ */ new Map();
2384
3066
  const readyProjectIds = /* @__PURE__ */ new Set();
@@ -2394,6 +3076,7 @@ async function startWorker(options) {
2394
3076
  let workspaceAutomaticTimer;
2395
3077
  let workspacePeriodicTimer;
2396
3078
  let pendingAutomaticTrigger;
3079
+ const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
2397
3080
  let automaticSyncInFlight = false;
2398
3081
  let terminalReplayTimer;
2399
3082
  let workspaceSyncRequestsInFlight = 0;
@@ -2408,7 +3091,7 @@ async function startWorker(options) {
2408
3091
  const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
2409
3092
  const workspaceProjectRelativePath = (projectId, branchName) => path.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
2410
3093
  const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
2411
- const projectConnection = (project) => {
3094
+ const projectConnection = (project, desiredFiles) => {
2412
3095
  const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
2413
3096
  const originUrl = project.repoHttpUrl ?? mirrorUrl;
2414
3097
  const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
@@ -2417,7 +3100,8 @@ async function startWorker(options) {
2417
3100
  { remoteUrl: mirrorUrl, authHeader: bearerAuthHeader },
2418
3101
  { remoteUrl: originUrl, authHeader: originHeader }
2419
3102
  ],
2420
- credentialStorePathForProject(project.projectId)
3103
+ credentialStorePathForProject(project.projectId),
3104
+ desiredFiles
2421
3105
  );
2422
3106
  if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2423
3107
  return {
@@ -2428,11 +3112,12 @@ async function startWorker(options) {
2428
3112
  credentialHelper
2429
3113
  };
2430
3114
  };
2431
- const projectMirrorConnection = (projectId) => {
3115
+ const projectMirrorConnection = (projectId, desiredFiles) => {
2432
3116
  const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, projectId);
2433
3117
  const credentialHelper = credentialHelperForRemotes(
2434
3118
  [{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader }],
2435
- credentialStorePathForProject(projectId)
3119
+ credentialStorePathForProject(projectId),
3120
+ desiredFiles
2436
3121
  );
2437
3122
  if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2438
3123
  return { mirrorUrl, credentialHelper, mirrorCredentialUsername: workerCredentialUsername };
@@ -2505,6 +3190,29 @@ async function startWorker(options) {
2505
3190
  readyProjectIds.delete(project.projectId);
2506
3191
  reconciledProjectConfigFingerprints.delete(project.projectId);
2507
3192
  };
3193
+ const deactivatePreserveOnlyProjectBranch = (project, branchName) => {
3194
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branchName);
3195
+ pendingCheckouts.delete(projectBranchKey(project.projectId, branchName));
3196
+ if (!fs.existsSync(branchPath)) return;
3197
+ if (!hasProjectWorktree(branchPath)) {
3198
+ throw new Error(`Preserve-only project branch path ${project.projectId}/${branchName} is not a managed Git checkout`);
3199
+ }
3200
+ if (project.branches.length === 0) {
3201
+ throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} without an active anchor branch`);
3202
+ }
3203
+ const primaryBranchName = primaryProjectBranch(project);
3204
+ if (branchName === primaryBranchName) {
3205
+ throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
3206
+ }
3207
+ deleteLinkedProjectBranch({
3208
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3209
+ primaryBranchName,
3210
+ branchName
3211
+ });
3212
+ fs.rmSync(path.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
3213
+ readyProjectIds.delete(project.projectId);
3214
+ reconciledProjectConfigFingerprints.delete(project.projectId);
3215
+ };
2508
3216
  const cleanupConfigForDurableDeletion = (deletion) => {
2509
3217
  const desiredLayout = projectWorkspaceState.desiredProjects.find(
2510
3218
  (project) => project.projectId === deletion.projectId && project.checkoutPathSegments[0] === deletion.checkoutPathSegments[0] && project.checkoutPathSegments[1] === deletion.checkoutPathSegments[1]
@@ -2519,8 +3227,13 @@ async function startWorker(options) {
2519
3227
  repoHttpUrl: liveConfig?.repoHttpUrl ?? null,
2520
3228
  repoAuthHeader: liveConfig?.repoAuthHeader ?? null,
2521
3229
  defaultBranch,
3230
+ repositoryTransitionId: liveConfig?.repositoryTransitionId ?? null,
3231
+ executionDisabled: false,
3232
+ mirrorWritesDisabled: false,
3233
+ preserveOnlyBranches: [],
2522
3234
  branches: branchNames.map(
2523
3235
  (branchName) => liveConfig?.branches.find((branch) => branch.branchName === branchName) ?? {
3236
+ branchId: "deleted",
2524
3237
  branchName,
2525
3238
  sourceBranchName: null,
2526
3239
  // Deletion never consumes the baseline, but retain a structurally
@@ -2535,9 +3248,18 @@ async function startWorker(options) {
2535
3248
  const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
2536
3249
  if (deletion.kind === "project") {
2537
3250
  if (!deletion.projectDeleted) {
2538
- if (fs.existsSync(deletion.managedPath)) {
2539
- removeProjectWorktrees({ projectRoot: deletion.managedPath, branchNames: deletion.branchNames });
2540
- }
3251
+ const nextProject = projectConfigById.get(deletion.projectId);
3252
+ if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
3253
+ preserveProjectCheckoutPathMove({
3254
+ oldProjectRoot: deletion.managedPath,
3255
+ newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
3256
+ projectsDurabilityRoot: projectsRoot,
3257
+ outerWorkspaceRoot: workspaceShadowRoot,
3258
+ branches: deletion.branchNames.map((branchName) => ({
3259
+ branchName,
3260
+ outerWorkspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, branchName)
3261
+ }))
3262
+ });
2541
3263
  readyProjectIds.delete(deletion.projectId);
2542
3264
  reconciledProjectConfigFingerprints.delete(deletion.projectId);
2543
3265
  continue;
@@ -2570,32 +3292,15 @@ async function startWorker(options) {
2570
3292
  });
2571
3293
  }
2572
3294
  };
2573
- const pruneStaleOuterWorkspaceEntries = (entries) => {
2574
- const remove = (candidate) => {
2575
- if (candidate) fs.rmSync(candidate, { recursive: true, force: true });
2576
- };
2577
- for (const entry of entries) {
2578
- if (entry.kind === "branch") {
2579
- projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
2580
- projectId: entry.projectId,
2581
- branchName: entry.branchName
2582
- });
2583
- remove(entry.projectTreePath);
2584
- remove(entry.planTreePath);
2585
- continue;
2586
- }
2587
- remove(entry.projectTreePath);
2588
- remove(entry.planTreePath);
2589
- for (const branch of entry.branches) {
2590
- remove(branch.projectTreePath);
2591
- remove(branch.planTreePath);
2592
- }
2593
- }
2594
- };
2595
3295
  const effectiveWorkerProjects = (projects) => {
2596
3296
  const incomingById = new Map(projects.map((project) => [project.projectId, project]));
2597
- const pending = new Set(
2598
- projectWorkspaceState.locallyPendingCreatedBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName))
3297
+ const preserveOnlyByKey = new Map(
3298
+ projects.flatMap(
3299
+ (project) => project.preserveOnlyBranches.map((branch) => [projectBranchKey(project.projectId, branch.branchName), branch])
3300
+ )
3301
+ );
3302
+ const pending = new Map(
3303
+ projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [projectBranchKey(branch.projectId, branch.branchName), branch])
2599
3304
  );
2600
3305
  return projectWorkspaceState.effectiveDesiredProjects.flatMap((layout) => {
2601
3306
  const incoming = incomingById.get(layout.projectId);
@@ -2603,10 +3308,21 @@ async function startWorker(options) {
2603
3308
  const existing = projectConfigById.get(layout.projectId);
2604
3309
  const branches = layout.branches.flatMap(({ branchName }) => {
2605
3310
  const configured = incoming.branches.find((branch) => branch.branchName === branchName);
2606
- if (configured) return [configured];
3311
+ const pendingBranch = pending.get(projectBranchKey(layout.projectId, branchName));
3312
+ const preservedBranch = preserveOnlyByKey.get(projectBranchKey(layout.projectId, branchName));
3313
+ const pendingIncarnationIsAuthorized = creatorLocalProjectBranchIsAuthorized({
3314
+ pendingBranchId: pendingBranch?.branchId,
3315
+ preserveOnlyBranchId: preservedBranch?.branchId
3316
+ });
3317
+ const disposition = workerProjectBranchDisposition({
3318
+ activeConfigured: Boolean(configured),
3319
+ locallyPendingCreated: pendingIncarnationIsAuthorized
3320
+ });
3321
+ if (configured && disposition === "active") return [configured];
3322
+ if (disposition === "preserve_only") return [];
2607
3323
  const retained = existing?.branches.find((branch) => branch.branchName === branchName);
2608
3324
  if (retained) return [retained];
2609
- if (!pending.has(projectBranchKey(layout.projectId, branchName))) return [];
3325
+ if (!pendingBranch?.branchId) return [];
2610
3326
  const branchPath = configuredProjectBranchPath(
2611
3327
  projectsRoot,
2612
3328
  { ...incoming, checkoutPathSegments: layout.checkoutPathSegments },
@@ -2615,6 +3331,7 @@ async function startWorker(options) {
2615
3331
  if (!hasProjectWorktree(branchPath)) return [];
2616
3332
  return [
2617
3333
  {
3334
+ branchId: pendingBranch.branchId,
2618
3335
  branchName,
2619
3336
  sourceBranchName: null,
2620
3337
  baseCommitHash: runGit(["rev-parse", "HEAD"], { cwd: branchPath })
@@ -2624,41 +3341,94 @@ async function startWorker(options) {
2624
3341
  return [{ ...incoming, checkoutPathSegments: [...layout.checkoutPathSegments], branches }];
2625
3342
  });
2626
3343
  };
2627
- const buildWorkspaceMounts = () => {
3344
+ const activeWorkspaceMutationTargets = () => [
3345
+ ...Array.from(activeProcesses.values(), ({ target }) => target),
3346
+ ...credentialBearingProcessGroupTargets.values(),
3347
+ ...workspaceSyncPriorityProcessTargets.values(),
3348
+ ...workspaceSyncPriorityOperationTargets.values(),
3349
+ ...Array.from(activePtys.values(), ({ target }) => target),
3350
+ ...workspaceSyncPriorityPtyTargets.values()
3351
+ ];
3352
+ const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
3353
+ const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
3354
+ const activeTargets = activeWorkspaceMutationTargets();
3355
+ return projectConfigById.get(projectId)?.executionDisabled === true || hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath) || canonicalWorkspaceMutationIsActive(activeTargets) || projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTargets);
3356
+ };
3357
+ const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3358
+ const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
2628
3359
  const mounts = [];
2629
- for (const project of [...projectConfigById.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
3360
+ const creatorLocalIncarnations = new Map(
3361
+ projectWorkspaceState.locallyPendingCreatedBranches.flatMap(
3362
+ (branch) => branch.branchId ? [[projectBranchKey(branch.projectId, branch.branchName), branch.branchId]] : []
3363
+ )
3364
+ );
3365
+ const checkoutPathMoveBranches = new Set(
3366
+ projectWorkspaceState.pendingTreeDeletions.flatMap(
3367
+ (deletion) => deletion.kind === "project" && !deletion.projectDeleted ? deletion.branchNames.map((branchName) => projectBranchKey(deletion.projectId, branchName)) : []
3368
+ )
3369
+ );
3370
+ for (const project of [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
2630
3371
  const projectRoot = configuredProjectRoot(projectsRoot, project);
2631
3372
  for (const branch of [...project.branches].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
2632
3373
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
3374
+ const branchKey = projectBranchKey(project.projectId, branch.branchName);
3375
+ const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
3376
+ const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
2633
3377
  mounts.push({
2634
3378
  id: projectMountId(project.projectId, branch.branchName),
3379
+ hydrationIncarnationKey: branch.branchId,
2635
3380
  sourcePath: branchPath,
3381
+ durabilityRootPath: projectsRoot,
2636
3382
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2637
3383
  sourceMode: "git",
2638
3384
  hydrateDeletionMode: "git",
2639
- busy: () => hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath)
3385
+ preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3386
+ preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3387
+ busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3388
+ busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
2640
3389
  });
2641
3390
  mounts.push({
2642
3391
  id: projectPlanMountId(project.projectId, branch.branchName),
3392
+ hydrationIncarnationKey: branch.branchId,
2643
3393
  sourcePath: path.join(planRoot, project.projectId, ...branch.branchName.split("/")),
3394
+ durabilityRootPath: planRoot,
2644
3395
  workspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
2645
3396
  sourceMode: "all",
2646
- hydrateDeletionMode: "all"
3397
+ hydrateDeletionMode: "all",
3398
+ preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3399
+ preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3400
+ busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3401
+ busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
2647
3402
  });
2648
3403
  }
2649
3404
  void projectRoot;
2650
3405
  }
2651
3406
  mounts.push({
2652
3407
  id: "workspace-plans",
3408
+ hydrationIncarnationKey: "workspace-plans-v1",
2653
3409
  sourcePath: path.join(planRoot, "workspace"),
3410
+ durabilityRootPath: planRoot,
2654
3411
  workspaceRelativePath: "workspace-plans",
2655
3412
  sourceMode: "all",
2656
- hydrateDeletionMode: "all"
3413
+ hydrateDeletionMode: "all",
3414
+ // Workspace-level agents receive this directory through R5D_PLANS_DIR.
3415
+ // Keep both projection and hydration away from it while any visible-
3416
+ // projects process, PTY, or built-in write/edit is active or reserved.
3417
+ busy: () => {
3418
+ const activeTargets = activeWorkspaceMutationTargets();
3419
+ return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
3420
+ },
3421
+ busyForRecovery: () => {
3422
+ const activeTargets = activeWorkspaceMutationTargets();
3423
+ return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
3424
+ }
2657
3425
  });
2658
3426
  for (const deletion of pendingMirrorDeletes.values()) {
2659
3427
  mounts.push({
2660
3428
  id: deletion.id,
3429
+ hydrationIncarnationKey: deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`,
2661
3430
  sourcePath: deletion.sourcePath,
3431
+ durabilityRootPath: projectsRoot,
2662
3432
  workspaceRelativePath: deletion.workspaceRelativePath,
2663
3433
  sourceMode: "git",
2664
3434
  hydrateDeletionMode: "git",
@@ -2666,7 +3436,9 @@ async function startWorker(options) {
2666
3436
  });
2667
3437
  mounts.push({
2668
3438
  id: `${deletion.id}:plan`,
3439
+ hydrationIncarnationKey: `${deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`}:plan`,
2669
3440
  sourcePath: deletion.planSourcePath,
3441
+ durabilityRootPath: planRoot,
2670
3442
  workspaceRelativePath: deletion.planWorkspaceRelativePath,
2671
3443
  sourceMode: "all",
2672
3444
  hydrateDeletionMode: "all",
@@ -2694,10 +3466,34 @@ async function startWorker(options) {
2694
3466
  const ensureConfiguredProjects = () => {
2695
3467
  for (const project of projectConfigById.values()) {
2696
3468
  validateProjectId(project.projectId);
3469
+ assertRepositoryTransitionState(project);
2697
3470
  for (const branch of project.branches) validateBranchName(branch.branchName);
3471
+ if (project.branches.length === 0) {
3472
+ readyProjectIds.delete(project.projectId);
3473
+ reconciledProjectConfigFingerprints.delete(project.projectId);
3474
+ continue;
3475
+ }
3476
+ if (project.executionDisabled) {
3477
+ readyProjectIds.delete(project.projectId);
3478
+ reconciledProjectConfigFingerprints.delete(project.projectId);
3479
+ for (const branch of project.branches) {
3480
+ pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
3481
+ }
3482
+ continue;
3483
+ }
2698
3484
  const projectRoot = configuredProjectRoot(projectsRoot, project);
2699
3485
  const configFingerprint = projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity });
2700
- const alreadyReady = readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
3486
+ const durableEnabledTransition = projectWorkspaceState.enabledRepositoryTransitions.find(
3487
+ ({ projectId }) => projectId === project.projectId
3488
+ );
3489
+ const transitionReseedRequired = enabledRepositoryTransitionRequiresReseed({
3490
+ project,
3491
+ lastEnabled: {
3492
+ known: Boolean(durableEnabledTransition),
3493
+ transitionId: durableEnabledTransition?.transitionId ?? null
3494
+ }
3495
+ });
3496
+ const alreadyReady = !transitionReseedRequired && readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
2701
3497
  if (alreadyReady) {
2702
3498
  for (const branch of project.branches) pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
2703
3499
  continue;
@@ -2706,8 +3502,11 @@ async function startWorker(options) {
2706
3502
  const connection = projectConnection(project);
2707
3503
  const states = ensureProjectWorktrees({
2708
3504
  projectRoot,
3505
+ snapshotRoot: projectWorktreeSnapshotsRoot,
2709
3506
  primaryBranchName: primaryProjectBranch(project),
2710
3507
  branches: project.branches,
3508
+ defaultBranch: project.defaultBranch,
3509
+ ...transitionReseedRequired && project.repositoryTransitionId !== null ? { exactMirrorBranches: /* @__PURE__ */ new Set([primaryProjectBranch(project)]) } : {},
2711
3510
  originUrl: connection.originUrl,
2712
3511
  originCredentialUsername: connection.originCredentialUsername,
2713
3512
  mirrorUrl: connection.mirrorUrl,
@@ -2720,7 +3519,7 @@ async function startWorker(options) {
2720
3519
  for (const state of states) {
2721
3520
  pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
2722
3521
  const key = projectBranchKey(project.projectId, state.branchName);
2723
- if (!lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
3522
+ if (transitionReseedRequired || !lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
2724
3523
  if ((state.ahead ?? 0) > 0) {
2725
3524
  process.stderr.write(
2726
3525
  `[r5d-worker] ${project.projectPath}/${state.branchName} is ${state.ahead} commit(s) ahead of origin; switching workers may require publishing or reconciling those commits
@@ -2728,6 +3527,12 @@ async function startWorker(options) {
2728
3527
  );
2729
3528
  }
2730
3529
  }
3530
+ if (transitionReseedRequired) {
3531
+ projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
3532
+ projectId: project.projectId,
3533
+ transitionId: project.repositoryTransitionId
3534
+ });
3535
+ }
2731
3536
  } catch (error) {
2732
3537
  readyProjectIds.delete(project.projectId);
2733
3538
  reconciledProjectConfigFingerprints.delete(project.projectId);
@@ -2747,7 +3552,7 @@ async function startWorker(options) {
2747
3552
  const collectAheadOfOriginBranches = () => {
2748
3553
  const aheadBranches = [];
2749
3554
  for (const project of projectConfigById.values()) {
2750
- if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
3555
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2751
3556
  const connection = projectConnection(project);
2752
3557
  const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
2753
3558
  if (!tryGit(
@@ -2769,7 +3574,11 @@ async function startWorker(options) {
2769
3574
  if (!hasProjectWorktree(branchPath)) continue;
2770
3575
  try {
2771
3576
  let ahead = 0;
2772
- const originRef = `refs/remotes/origin/${branch.branchName}`;
3577
+ const originRef = `refs/remotes/origin/${projectOriginBranchName({
3578
+ branchName: branch.branchName,
3579
+ primaryBranchName: primaryProjectBranch(project),
3580
+ defaultBranch: project.defaultBranch
3581
+ })}`;
2773
3582
  if (tryGit(["show-ref", "--verify", "--quiet", originRef], { cwd: branchPath })) {
2774
3583
  const counts = runGit(["rev-list", "--left-right", "--count", `${originRef}...HEAD`], { cwd: branchPath }).split(/\s+/).map(Number);
2775
3584
  ahead = Number.isSafeInteger(counts[1]) ? counts[1] : 0;
@@ -2790,18 +3599,30 @@ async function startWorker(options) {
2790
3599
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2791
3600
  );
2792
3601
  };
2793
- const pushChangedProjectHeads = (activeMountIds, publishedHead) => {
2794
- const active = new Set(activeMountIds);
3602
+ const captureProjectHeadsForWorkspacePublication = () => {
3603
+ const heads = /* @__PURE__ */ new Map();
2795
3604
  for (const project of projectConfigById.values()) {
2796
3605
  if (!readyProjectIds.has(project.projectId)) continue;
2797
- const changed = /* @__PURE__ */ new Set();
2798
3606
  for (const branch of project.branches) {
2799
- if (!active.has(projectMountId(project.projectId, branch.branchName))) continue;
2800
3607
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2801
3608
  if (!hasProjectWorktree(branchPath)) continue;
2802
- const head = runGit(["rev-parse", "HEAD"], { cwd: branchPath });
3609
+ heads.set(projectMountId(project.projectId, branch.branchName), runGit(["rev-parse", "HEAD"], { cwd: branchPath }));
3610
+ }
3611
+ }
3612
+ return heads;
3613
+ };
3614
+ const pushChangedProjectHeads = (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
3615
+ const active = new Set(activeMountIds);
3616
+ for (const project of projectConfigById.values()) {
3617
+ if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
3618
+ const changed = /* @__PURE__ */ new Map();
3619
+ for (const branch of project.branches) {
3620
+ const mountId = projectMountId(project.projectId, branch.branchName);
3621
+ if (!active.has(mountId) || suppressedMountIds.has(mountId)) continue;
3622
+ const head = publicationHeads.get(mountId);
3623
+ if (!head) throw new Error(`Missing pre-projection head for active project mount ${mountId}`);
2803
3624
  if (lastObservedProjectHeads.get(projectBranchKey(project.projectId, branch.branchName)) !== head) {
2804
- changed.add(branch.branchName);
3625
+ changed.set(branch.branchName, head);
2805
3626
  }
2806
3627
  }
2807
3628
  if (changed.size === 0) continue;
@@ -2812,10 +3633,12 @@ async function startWorker(options) {
2812
3633
  mirrorUrl: connection.mirrorUrl,
2813
3634
  credentialHelper: connection.credentialHelper,
2814
3635
  credentialUsername: connection.mirrorCredentialUsername,
2815
- onlyBranches: changed
3636
+ onlyBranches: new Set(changed.keys()),
3637
+ publicationHeads: changed
2816
3638
  });
3639
+ assertProjectMirrorHeadsPushed(results);
2817
3640
  for (const result of results) {
2818
- if (result.pushed) lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
3641
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
2819
3642
  }
2820
3643
  }
2821
3644
  const publishedTombstoneIds = [
@@ -2833,6 +3656,7 @@ async function startWorker(options) {
2833
3656
  }
2834
3657
  for (const [key, deletion] of pendingMirrorDeletes) {
2835
3658
  if (!active.has(deletion.id)) continue;
3659
+ if (!repositoryMirrorWritesAllowed(projectConfigById.get(deletion.projectId))) continue;
2836
3660
  if (!deletion.projectDeleted) {
2837
3661
  deleteProjectMirrorBranch({
2838
3662
  gitDirectory: deletion.gitDirectory,
@@ -2855,73 +3679,105 @@ async function startWorker(options) {
2855
3679
  lastObservedProjectHeads.delete(key);
2856
3680
  }
2857
3681
  };
2858
- const refreshProjectHeadsAfterInboundWorkspace = () => {
3682
+ const observeProjectHeadsBeforeOuterWorkspace = (input) => {
3683
+ const bundles = [];
2859
3684
  for (const project of projectConfigById.values()) {
2860
- if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
3685
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2861
3686
  try {
2862
3687
  const connection = projectConnection(project);
2863
- const refreshed = fastForwardProjectHeadsFromMirror({
2864
- projectRoot: configuredProjectRoot(projectsRoot, project),
2865
- primaryBranchName: primaryProjectBranch(project),
2866
- branchNames: project.branches.map(({ branchName }) => branchName),
2867
- mirrorUrl: connection.mirrorUrl,
2868
- credentialHelper: connection.credentialHelper,
2869
- credentialUsername: connection.mirrorCredentialUsername
3688
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
3689
+ const primaryBranchName = primaryProjectBranch(project);
3690
+ bundles.push({
3691
+ projectId: project.projectId,
3692
+ projectRoot,
3693
+ primaryBranchName,
3694
+ observations: observeProjectMirrorHeads({
3695
+ projectRoot,
3696
+ primaryBranchName,
3697
+ branchNames: project.branches.map(({ branchName }) => branchName),
3698
+ mirrorUrl: connection.mirrorUrl,
3699
+ credentialHelper: connection.credentialHelper,
3700
+ credentialUsername: connection.mirrorCredentialUsername,
3701
+ allowNonFastForward: input.allowNonFastForward
3702
+ })
3703
+ });
3704
+ } catch (error) {
3705
+ if (input.failClosed) throw error;
3706
+ process.stderr.write(
3707
+ `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
3708
+ `
3709
+ );
3710
+ }
3711
+ }
3712
+ return bundles;
3713
+ };
3714
+ const applyObservedProjectHeadsAfterInboundWorkspace = (bundles, activeMountIds, requireObservedHeads) => {
3715
+ const active = new Set(activeMountIds);
3716
+ for (const bundle of bundles) {
3717
+ const project = projectConfigById.get(bundle.projectId);
3718
+ if (!project || project.executionDisabled || !readyProjectIds.has(project.projectId)) continue;
3719
+ const observations = bundle.observations.filter(({ branchName }) => active.has(projectMountId(project.projectId, branchName)));
3720
+ if (observations.length === 0) continue;
3721
+ try {
3722
+ const refreshed = applyObservedProjectMirrorHeads({
3723
+ projectRoot: bundle.projectRoot,
3724
+ primaryBranchName: bundle.primaryBranchName,
3725
+ observations,
3726
+ branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
2870
3727
  });
3728
+ if (requireObservedHeads) {
3729
+ for (const observation of observations) {
3730
+ if (!observation.shouldMove || !observation.mirrorHead) continue;
3731
+ const checkoutPath = configuredProjectBranchPath(projectsRoot, project, observation.branchName);
3732
+ const currentHead = runGit(["rev-parse", "HEAD"], { cwd: checkoutPath });
3733
+ if (currentHead !== observation.mirrorHead) {
3734
+ throw new Error(
3735
+ `Project ${project.projectId}/${observation.branchName} did not apply observed hidden mirror head ${observation.mirrorHead}`
3736
+ );
3737
+ }
3738
+ }
3739
+ }
2871
3740
  for (const state of refreshed) {
2872
3741
  lastObservedProjectHeads.set(
2873
3742
  projectBranchKey(project.projectId, state.branchName),
2874
- state.fastForwarded && state.mirrorHead ? state.mirrorHead : state.previousHead
3743
+ state.moved && state.mirrorHead ? state.mirrorHead : state.previousHead
2875
3744
  );
2876
3745
  }
2877
3746
  } catch (error) {
3747
+ if (requireObservedHeads) throw error;
2878
3748
  process.stderr.write(
2879
- `[r5d-worker] failed to refresh project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
3749
+ `[r5d-worker] failed to apply observed project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
2880
3750
  `
2881
3751
  );
2882
3752
  }
2883
3753
  }
2884
3754
  };
2885
- const reseedProjectHeadsAfterWorkspaceReset = () => {
2886
- for (const project of projectConfigById.values()) {
2887
- if (!readyProjectIds.has(project.projectId)) continue;
2888
- const connection = projectConnection(project);
2889
- const states = ensureProjectWorktrees({
2890
- projectRoot: configuredProjectRoot(projectsRoot, project),
2891
- primaryBranchName: primaryProjectBranch(project),
2892
- branches: project.branches,
2893
- originUrl: connection.originUrl,
2894
- originCredentialUsername: connection.originCredentialUsername,
2895
- mirrorUrl: connection.mirrorUrl,
2896
- mirrorCredentialUsername: connection.mirrorCredentialUsername,
2897
- credentialHelper: connection.credentialHelper,
2898
- gitIdentity: workspaceGitIdentity
2899
- });
2900
- for (const state of states) {
2901
- lastObservedProjectHeads.set(projectBranchKey(project.projectId, state.branchName), state.head);
2902
- }
2903
- }
3755
+ const mapWorkspaceGitResult = (attemptId, trigger, result) => {
3756
+ const activeProjectBranchPublications = activeProjectBranchPublicationEvidence([...projectConfigById.values()], result.activeMountIds);
3757
+ return {
3758
+ type: "workspace_sync",
3759
+ attemptId,
3760
+ workerLabel: label,
3761
+ trigger,
3762
+ outcome: result.outcome === "pushed" ? "published" : result.outcome,
3763
+ startingHead: result.startingHead,
3764
+ ...result.localHead ? { localHead: result.localHead } : {},
3765
+ ...result.publishedHead ? { publishedHead: result.publishedHead } : {},
3766
+ rebaseCount: result.rebaseCount,
3767
+ diffSizeBytes: result.diffSizeBytes,
3768
+ gitStatus: workspaceGitStatus(),
3769
+ affectedProjects: affectedProjects(result.affectedPaths),
3770
+ affectedPaths: result.affectedPaths,
3771
+ activeMountIds: result.activeMountIds,
3772
+ skippedMountIds: result.skippedMountIds,
3773
+ activeProjectBranchPublications,
3774
+ discardedPaths: [],
3775
+ localChangesDiscarded: false,
3776
+ ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
3777
+ ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
3778
+ ...result.error ? { error: result.error } : {}
3779
+ };
2904
3780
  };
2905
- const mapWorkspaceGitResult = (attemptId, trigger, result) => ({
2906
- type: "workspace_sync",
2907
- attemptId,
2908
- workerLabel: label,
2909
- trigger,
2910
- outcome: result.outcome === "pushed" ? "published" : result.outcome,
2911
- startingHead: result.startingHead,
2912
- ...result.localHead ? { localHead: result.localHead } : {},
2913
- ...result.publishedHead ? { publishedHead: result.publishedHead } : {},
2914
- rebaseCount: result.rebaseCount,
2915
- diffSizeBytes: result.diffSizeBytes,
2916
- gitStatus: workspaceGitStatus(),
2917
- affectedProjects: affectedProjects(result.affectedPaths),
2918
- affectedPaths: result.affectedPaths,
2919
- discardedPaths: [],
2920
- localChangesDiscarded: false,
2921
- ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
2922
- ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
2923
- ...result.error ? { error: result.error } : {}
2924
- });
2925
3781
  const failedWorkspaceSyncResult = (attemptId, trigger, error) => ({
2926
3782
  type: "workspace_sync",
2927
3783
  attemptId,
@@ -2945,6 +3801,16 @@ async function startWorker(options) {
2945
3801
  }
2946
3802
  const mounts = buildWorkspaceMounts();
2947
3803
  if (input.resetToCanonical) {
3804
+ const resetHasBusyLiveMount = mounts.some(
3805
+ (mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
3806
+ );
3807
+ if (resetHasBusyLiveMount) {
3808
+ throw new Error("Workspace reset cannot observe project mirror heads while a live mount is busy; remediation remains active");
3809
+ }
3810
+ const observedProjectHeads2 = observeProjectHeadsBeforeOuterWorkspace({
3811
+ allowNonFastForward: true,
3812
+ failClosed: true
3813
+ });
2948
3814
  const reset = resetWorkspaceGit({
2949
3815
  workspacePath: workspaceShadowRoot,
2950
3816
  remoteUrl: workspaceRemoteUrl,
@@ -2953,7 +3819,7 @@ async function startWorker(options) {
2953
3819
  gitIdentity: workspaceGitIdentity,
2954
3820
  mounts
2955
3821
  });
2956
- reseedProjectHeadsAfterWorkspaceReset();
3822
+ applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
2957
3823
  return {
2958
3824
  type: "workspace_sync",
2959
3825
  attemptId: input.attemptId,
@@ -2968,11 +3834,23 @@ async function startWorker(options) {
2968
3834
  gitStatus: workspaceGitStatus(),
2969
3835
  affectedProjects: affectedProjects(reset.discardedPaths),
2970
3836
  affectedPaths: reset.discardedPaths,
3837
+ activeMountIds: reset.activeMountIds,
3838
+ skippedMountIds: reset.skippedMountIds,
2971
3839
  discardedPaths: reset.discardedPaths,
2972
3840
  localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
2973
3841
  };
2974
3842
  }
2975
3843
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
3844
+ const ordinaryCycleHasBusyLiveMount = mounts.some(
3845
+ (mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
3846
+ );
3847
+ const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
3848
+ const inboundMoveMountIds = new Set(
3849
+ observedProjectHeads.flatMap(
3850
+ ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
3851
+ )
3852
+ );
3853
+ const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : captureProjectHeadsForWorkspacePublication();
2976
3854
  const result = await synchronizeWorkspaceGit({
2977
3855
  attemptId: input.attemptId,
2978
3856
  workerLabel: label,
@@ -2985,13 +3863,12 @@ async function startWorker(options) {
2985
3863
  commitDetail: input.confirmationReason ?? input.trigger.detail,
2986
3864
  allowLargeDiff: input.confirmedLargeDiff,
2987
3865
  skipMountMirror: outerRemediation,
2988
- skipMountHydration: outerRemediation,
2989
3866
  afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
2990
- pushChangedProjectHeads(activeMountIds, publishedHead);
3867
+ pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
2991
3868
  }
2992
3869
  });
2993
3870
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
2994
- refreshProjectHeadsAfterInboundWorkspace();
3871
+ applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
2995
3872
  }
2996
3873
  return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
2997
3874
  };
@@ -3035,6 +3912,28 @@ async function startWorker(options) {
3035
3912
  resetToCanonical: input.resetToCanonical
3036
3913
  });
3037
3914
  } catch (error) {
3915
+ let hydrationCurrent = input.resetToCanonical !== true;
3916
+ if (hydrationCurrent) {
3917
+ try {
3918
+ hydrationCurrent = workspaceGitHydrationIsCurrent(workspaceShadowRoot, buildWorkspaceMounts());
3919
+ } catch {
3920
+ hydrationCurrent = false;
3921
+ }
3922
+ }
3923
+ if (!hydrationCurrent) {
3924
+ process.stderr.write(
3925
+ `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
3926
+ `
3927
+ );
3928
+ }
3929
+ fenceUnsafeWorkspaceSyncFailure({
3930
+ hydrationCurrent,
3931
+ invalidateExecution: () => {
3932
+ workspaceConfigured = false;
3933
+ advanceWorkerAdmissionGeneration();
3934
+ },
3935
+ exitCode: 1
3936
+ });
3038
3937
  return failedWorkspaceSyncResult(attemptId, input.trigger, error);
3039
3938
  }
3040
3939
  });
@@ -3056,7 +3955,13 @@ async function startWorker(options) {
3056
3955
  };
3057
3956
  const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
3058
3957
  pendingAutomaticTrigger = trigger;
3059
- if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight) {
3958
+ const pendingBranchDeferral = () => pendingCreatedBranchAutomaticSyncDeferral(
3959
+ projectWorkspaceState.locallyPendingCreatedBranches,
3960
+ activeWorkspaceMutationTargets(),
3961
+ pendingCreatedBranchPublicationNotBefore
3962
+ );
3963
+ const initialDeferral = pendingBranchDeferral();
3964
+ if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
3060
3965
  return;
3061
3966
  }
3062
3967
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -3064,8 +3969,14 @@ async function startWorker(options) {
3064
3969
  () => {
3065
3970
  workspaceAutomaticTimer = void 0;
3066
3971
  const scheduledTrigger = pendingAutomaticTrigger;
3067
- pendingAutomaticTrigger = void 0;
3068
3972
  if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
3973
+ const currentDeferral = pendingBranchDeferral();
3974
+ if (currentDeferral?.kind === "active_target") return;
3975
+ if (currentDeferral?.kind === "creation_grace") {
3976
+ scheduleAutomaticWorkspaceSync(scheduledTrigger, currentDeferral.retryAfterMs);
3977
+ return;
3978
+ }
3979
+ pendingAutomaticTrigger = void 0;
3069
3980
  automaticSyncInFlight = true;
3070
3981
  void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
3071
3982
  if (result.outcome === "failed") {
@@ -3085,7 +3996,7 @@ async function startWorker(options) {
3085
3996
  }
3086
3997
  });
3087
3998
  },
3088
- Math.max(0, delayMs)
3999
+ Math.max(0, delayMs, initialDeferral?.kind === "creation_grace" ? initialDeferral.retryAfterMs : 0)
3089
4000
  );
3090
4001
  workspaceAutomaticTimer.unref();
3091
4002
  };
@@ -3093,32 +4004,190 @@ async function startWorker(options) {
3093
4004
  scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
3094
4005
  };
3095
4006
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
3096
- const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
3097
- target,
3098
- projectsRoot,
3099
- workspaceShadowRoot,
3100
- projectConfigById
3101
- });
4007
+ const resolveMessageTarget = (target) => {
4008
+ if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
4009
+ if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
4010
+ throw new Error(`Project ${target.projectId} is not ready on this worker`);
4011
+ }
4012
+ if (target.type === "workspace" && target.rootProfile === "visible_projects" && [...projectConfigById.keys()].some((projectId) => !readyProjectIds.has(projectId))) {
4013
+ throw new Error("Visible project execution is unavailable while project configuration is incomplete");
4014
+ }
4015
+ return resolveWorkerSessionTarget({
4016
+ target,
4017
+ projectsRoot,
4018
+ workspaceShadowRoot,
4019
+ projectConfigById
4020
+ });
4021
+ };
3102
4022
  const configureWorkerWorkspace = async (message) => {
4023
+ const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
4024
+ ...message,
4025
+ workerBaseUrl: baseUrl,
4026
+ workerAuthHeader: bearerAuthHeader
4027
+ });
4028
+ const credentialTransitionPhase = credentialGenerationTransitionPhase({
4029
+ committedFingerprint: configuredCredentialGenerationFingerprint,
4030
+ pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4031
+ incomingFingerprint: incomingCredentialGenerationFingerprint
4032
+ });
4033
+ const credentialReapStatus = credentialReapContractStatus();
4034
+ const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
4035
+ const credentialRestartIsContained = credentialGenerationRestartIsContained({
4036
+ systemdContract: verifiedCredentialReapContractNow,
4037
+ pendingBoundary: pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null,
4038
+ currentSystemdInvocationId: systemdInvocationIdentifierAtProcessStart,
4039
+ currentBootSessionId: bootSessionIdentifierAtProcessStart,
4040
+ currentMachineId: machineIdentifierAtProcessStart
4041
+ });
4042
+ let trackedChildTerminationProven = true;
4043
+ if (credentialTransitionPhase !== "current") {
4044
+ workspaceConfigured = false;
4045
+ githubCredential = null;
4046
+ advanceWorkerAdmissionGeneration();
4047
+ try {
4048
+ await terminateActiveCredentialBearingChildren();
4049
+ } catch (error) {
4050
+ trackedChildTerminationProven = false;
4051
+ process.stderr.write(
4052
+ `[r5d-worker] tracked credential-bearing child termination was incomplete at the credential transition boundary: ${error instanceof Error ? error.message : String(error)}
4053
+ `
4054
+ );
4055
+ }
4056
+ }
4057
+ const initialBootstrapAuthorized = initialCredentialBootstrapIsSafe({
4058
+ transitionPhase: credentialTransitionPhase,
4059
+ committedFingerprint: configuredCredentialGenerationFingerprint,
4060
+ pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4061
+ bootstrapFingerprintAtProcessStart: bootstrapCredentialGenerationFingerprintAtProcessStart,
4062
+ incomingFingerprint: incomingCredentialGenerationFingerprint,
4063
+ credentialArtifactsProvenAbsentAtProcessStart: initialCredentialBootstrapProvenEmpty,
4064
+ trackedChildTerminationProven,
4065
+ activeProcessCount: activeProcesses.size,
4066
+ credentialProcessGroupCount: credentialBearingProcessGroups.size,
4067
+ activePtyCount: activePtys.size
4068
+ });
4069
+ const staleIntentCleanupAuthorized = credentialTransitionPhase === "current" && (pendingCredentialGenerationIntentAtProcessStart !== null || bootstrapCredentialGenerationFingerprintAtProcessStart !== null);
4070
+ const credentialPublicationPreauthorized = credentialTransitionPhase === "publish_from_clean_restart" && credentialRestartIsContained || initialBootstrapAuthorized || staleIntentCleanupAuthorized;
4071
+ if (initialBootstrapAuthorized && bootstrapCredentialGenerationFingerprintAtProcessStart === null) {
4072
+ try {
4073
+ const bootstrap = preparePrivateAuthFileGeneration([
4074
+ { filePath: bootstrapCredentialGenerationPath(), content: `${incomingCredentialGenerationFingerprint}
4075
+ ` }
4076
+ ]);
4077
+ bootstrap.commit();
4078
+ bootstrapCredentialGenerationFingerprintAtProcessStart = incomingCredentialGenerationFingerprint;
4079
+ } catch (error) {
4080
+ const wrapped = error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
4081
+ return {
4082
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, wrapped),
4083
+ pending: [],
4084
+ aheadOfOriginBranches: []
4085
+ };
4086
+ }
4087
+ }
4088
+ if (credentialTransitionPhase === "stage_pending_restart" && !initialBootstrapAuthorized) {
4089
+ if (credentialReapStatus === "systemd_unverified") {
4090
+ process.stderr.write(
4091
+ "[r5d-worker] this process is inside r5d-worker.service but its credential-reaping contract could not be verified; no pending credential generation was written and the service may retry safely\n"
4092
+ );
4093
+ requireCredentialGenerationRestart(true, void 0, 1);
4094
+ }
4095
+ const boundary = pendingCredentialGenerationBoundary({
4096
+ systemdContract: verifiedCredentialReapContractNow,
4097
+ systemdInvocationId: systemdInvocationIdentifierAtProcessStart,
4098
+ bootSessionId: bootSessionIdentifierAtProcessStart,
4099
+ machineId: machineIdentifierAtProcessStart
4100
+ });
4101
+ if (!credentialGenerationBoundaryIsComplete(boundary)) {
4102
+ process.stderr.write(
4103
+ process.platform === "win32" ? "[r5d-worker] credential rotation is unavailable for a direct Windows worker because no authoritative per-boot identifier is supported; move it behind the verified systemd contract or reprovision a clean worker host\n" : "[r5d-worker] could not attest this worker's stable machine and boot identifiers; no pending credential generation was written\n"
4104
+ );
4105
+ requireCredentialGenerationRestart(true, void 0, 1);
4106
+ }
4107
+ try {
4108
+ const pending = preparePrivateAuthFileGeneration([
4109
+ {
4110
+ filePath: pendingCredentialGenerationPath(),
4111
+ content: pendingCredentialGenerationIntentContent({
4112
+ fingerprint: incomingCredentialGenerationFingerprint,
4113
+ boundary
4114
+ })
4115
+ }
4116
+ ]);
4117
+ pending.commit();
4118
+ } catch (error) {
4119
+ process.stderr.write(
4120
+ `[r5d-worker] could not stage pending credential generation before fatal restart: ${error instanceof Error ? error.message : String(error)}
4121
+ `
4122
+ );
4123
+ requireCredentialGenerationRestart(true, void 0, 1);
4124
+ }
4125
+ process.stderr.write(
4126
+ verifiedCredentialReapContractNow ? "[r5d-worker] credential transition staged without secrets; restarting through the service cgroup boundary\n" : "[r5d-worker] credential transition staged without secrets; reboot this worker host or VM before starting r5d-worker again\n"
4127
+ );
4128
+ requireCredentialGenerationRestart(true, void 0, WORKER_CREDENTIAL_RESTART_EXIT_CODE);
4129
+ }
4130
+ if (credentialTransitionPhase !== "current" && !trackedChildTerminationProven) {
4131
+ requireCredentialGenerationRestart(true, void 0, 1);
4132
+ }
4133
+ if (credentialTransitionPhase === "publish_from_clean_restart" && !credentialRestartIsContained) {
4134
+ const pendingBoundary = pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null;
4135
+ const detail = pendingBoundary?.machineId === null || machineIdentifierAtProcessStart === null ? "credential transition cannot prove it is still on the same worker host; keep the credential directory host-local and ensure the OS machine identifier is available" : pendingBoundary?.kind === "systemd" ? "credential transition is waiting for a new verified r5d-worker.service invocation on the same host" : pendingBoundary?.bootSessionId === null || bootSessionIdentifierAtProcessStart === null ? "credential transition cannot prove a portable whole-host restart; ensure its kernel boot identifier is available" : "credential transition is still in the boot session that staged it";
4136
+ process.stderr.write(
4137
+ `[r5d-worker] ${detail}; no new credential bytes were published. ${pendingBoundary?.kind === "boot" ? "Reboot the entire worker host or VM before starting r5d-worker again." : "The verified systemd unit must start a new invocation before retrying."}
4138
+ `
4139
+ );
4140
+ requireCredentialGenerationRestart(true, void 0, WORKER_CREDENTIAL_RESTART_EXIT_CODE);
4141
+ }
4142
+ if (credentialPublicationPreauthorized) workspaceConfigured = false;
3103
4143
  workspaceSyncRequestsInFlight += 1;
3104
4144
  try {
3105
4145
  return await workspaceSyncSingleFlight.runExclusive(async () => {
4146
+ for (const project of message.projects) assertRepositoryTransitionState(project);
4147
+ const busyConfigurationChanges = busyProjectConfigurationChangeIds({
4148
+ currentProjects: [...projectConfigById.values()],
4149
+ incomingProjects: message.projects,
4150
+ activeTargets: activeWorkspaceMutationTargets(),
4151
+ currentFingerprint: (project) => projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity }),
4152
+ incomingFingerprint: (project) => projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: message.gitIdentity }),
4153
+ currentProjectReady: (project) => {
4154
+ const fingerprint = projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity });
4155
+ return readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === fingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
4156
+ }
4157
+ });
4158
+ if (busyConfigurationChanges.length > 0) {
4159
+ throw new ProjectWorkspaceConfigurationDeferredError(
4160
+ `Project workspace configuration deferred while visible checkout activity is active: ${busyConfigurationChanges.join(", ")}`
4161
+ );
4162
+ }
3106
4163
  workspaceConfigured = false;
4164
+ const preserveOnlyBranches = message.projects.flatMap(
4165
+ (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4166
+ );
3107
4167
  projectWorkspaceState = projectWorkspaceStateStore.reconcile({
3108
- desiredProjects: message.projects
4168
+ desiredProjects: message.projects,
4169
+ preserveOnlyBranches
3109
4170
  });
3110
- const requiredCredentialStorePaths = /* @__PURE__ */ new Set();
4171
+ const stillPendingCreatedBranches = new Map(
4172
+ projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4173
+ pendingCreatedBranchKey(branch.projectId, branch.branchName),
4174
+ branch
4175
+ ])
4176
+ );
4177
+ for (const key of pendingCreatedBranchPublicationNotBefore.keys()) {
4178
+ if (!stillPendingCreatedBranches.has(key)) pendingCreatedBranchPublicationNotBefore.delete(key);
4179
+ }
4180
+ const desiredCredentialStoreFiles = /* @__PURE__ */ new Map();
3111
4181
  const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
3112
- requiredCredentialStorePaths.add(workspaceStorePath);
3113
4182
  const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
3114
4183
  [{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
3115
- workspaceStorePath
4184
+ workspaceStorePath,
4185
+ desiredCredentialStoreFiles
3116
4186
  );
3117
4187
  if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
3118
4188
  const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
3119
4189
  for (const project of message.projects) {
3120
- requiredCredentialStorePaths.add(credentialStorePathForProject(project.projectId));
3121
- projectConnection(project);
4190
+ projectConnection(project, desiredCredentialStoreFiles);
3122
4191
  }
3123
4192
  const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
3124
4193
  ...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
@@ -3127,30 +4196,81 @@ async function startWorker(options) {
3127
4196
  ]);
3128
4197
  for (const projectId of pendingCredentialProjectIds) {
3129
4198
  const desiredProject = desiredProjectConfigById.get(projectId);
3130
- if (desiredProject) projectConnection(desiredProject);
3131
- else projectMirrorConnection(projectId);
3132
- requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
4199
+ if (desiredProject) projectConnection(desiredProject, desiredCredentialStoreFiles);
4200
+ else projectMirrorConnection(projectId, desiredCredentialStoreFiles);
3133
4201
  }
3134
4202
  for (const deletion of pendingMirrorDeletes.values()) {
3135
4203
  const desiredProject = desiredProjectConfigById.get(deletion.projectId);
3136
- const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
4204
+ const connection = desiredProject ? projectConnection(desiredProject, desiredCredentialStoreFiles) : projectMirrorConnection(deletion.projectId, desiredCredentialStoreFiles);
3137
4205
  deletion.mirrorUrl = connection.mirrorUrl;
3138
4206
  deletion.credentialHelper = connection.credentialHelper;
3139
4207
  deletion.credentialUsername = connection.mirrorCredentialUsername;
3140
4208
  }
3141
- pruneCredentialStoreFiles(requiredCredentialStorePaths);
3142
- configureGitHubAuth(message.githubCredential);
4209
+ let preparedAuthGeneration;
4210
+ try {
4211
+ preparedAuthGeneration = preparePrivateAuthFileGeneration([
4212
+ ...planCredentialStoreGeneration(desiredCredentialStoreFiles),
4213
+ ...planGitHubRegistryAuthFiles([dockerConfigPath(), containersAuthPath()], message.githubCredential),
4214
+ { filePath: legacySharedCredentialStorePath(), content: null, allowSymlinkRemoval: true },
4215
+ { filePath: credentialGenerationReceiptPath(), content: `${incomingCredentialGenerationFingerprint}
4216
+ ` },
4217
+ { filePath: pendingCredentialGenerationPath(), content: null },
4218
+ { filePath: bootstrapCredentialGenerationPath(), content: null }
4219
+ ]);
4220
+ } catch (error) {
4221
+ throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
4222
+ }
4223
+ await commitCredentialGeneration(
4224
+ preparedAuthGeneration,
4225
+ message.githubCredential,
4226
+ terminateActiveCredentialBearingChildren,
4227
+ void 0,
4228
+ credentialPublicationPreauthorized
4229
+ );
4230
+ configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
4231
+ pendingCredentialGenerationIntentAtProcessStart = null;
4232
+ bootstrapCredentialGenerationFingerprintAtProcessStart = null;
3143
4233
  visibleGitIdentity = message.gitIdentity;
3144
4234
  workspaceGitIdentity = message.gitIdentity;
3145
4235
  workspaceRemoteUrl = message.workspaceRemoteUrl;
3146
4236
  workspaceCredentialHelper = nextWorkspaceCredentialHelper;
3147
4237
  workspaceCredentialUsername = workerCredentialUsername;
3148
- pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, message.projects);
4238
+ pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, projectWorkspaceState.desiredProjects);
3149
4239
  const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
3150
4240
  ...project,
3151
4241
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
3152
4242
  }));
3153
4243
  const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
4244
+ ensureWorkspaceGitClone({
4245
+ workspacePath: workspaceShadowRoot,
4246
+ remoteUrl: message.workspaceRemoteUrl,
4247
+ credentialHelper: nextWorkspaceCredentialHelper,
4248
+ credentialUsername: workerCredentialUsername,
4249
+ gitIdentity: message.gitIdentity
4250
+ });
4251
+ const recoverySourceOverrides = /* @__PURE__ */ new Map();
4252
+ for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
4253
+ if (deletion.kind !== "project" || deletion.projectDeleted) continue;
4254
+ for (const branchName of deletion.branchNames) {
4255
+ const oldBranchPath = path.join(deletion.managedPath, ...branchName.split("/"));
4256
+ if (fs.existsSync(oldBranchPath)) recoverySourceOverrides.set(projectMountId(deletion.projectId, branchName), oldBranchPath);
4257
+ }
4258
+ }
4259
+ const recoveryMounts = buildWorkspaceMounts(effectiveProjects).map((mount) => ({
4260
+ ...mount,
4261
+ sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
4262
+ busy: mount.busyForRecovery ?? mount.busy
4263
+ }));
4264
+ recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts);
4265
+ for (const project of message.projects) {
4266
+ for (const { branchName } of project.preserveOnlyBranches) {
4267
+ if (stillPendingCreatedBranches.has(pendingCreatedBranchKey(project.projectId, branchName))) {
4268
+ continue;
4269
+ }
4270
+ deactivatePreserveOnlyProjectBranch(project, branchName);
4271
+ }
4272
+ }
4273
+ const preserveOnlyKeys = new Set(preserveOnlyBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName)));
3154
4274
  for (const existingProject of projectConfigById.values()) {
3155
4275
  const nextProject = nextProjectConfigById.get(existingProject.projectId);
3156
4276
  if (!nextProject) {
@@ -3160,6 +4280,7 @@ async function startWorker(options) {
3160
4280
  const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
3161
4281
  for (const existingBranch of existingProject.branches) {
3162
4282
  if (!nextBranches.has(existingBranch.branchName)) {
4283
+ if (preserveOnlyKeys.has(projectBranchKey(existingProject.projectId, existingBranch.branchName))) continue;
3163
4284
  stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
3164
4285
  }
3165
4286
  }
@@ -3175,40 +4296,30 @@ async function startWorker(options) {
3175
4296
  for (const projectId of [...readyProjectIds]) {
3176
4297
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
3177
4298
  }
3178
- ensureWorkspaceGitClone({
3179
- workspacePath: workspaceShadowRoot,
3180
- remoteUrl: message.workspaceRemoteUrl,
3181
- credentialHelper: nextWorkspaceCredentialHelper,
3182
- credentialUsername: workerCredentialUsername,
3183
- gitIdentity: message.gitIdentity
3184
- });
3185
- pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
3186
4299
  stageDurableWorkspaceDeletions();
3187
4300
  const mountsBeforeGit = buildWorkspaceMounts();
3188
4301
  hydrateWorkspaceGitMounts(
3189
4302
  workspaceShadowRoot,
3190
- mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath))
4303
+ mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath) && mount.busy?.() !== true),
4304
+ // These mounts are intentionally hydrated before their project Git
4305
+ // layouts can be marked ready. No process can resolve an unready
4306
+ // target, and preserving the fetched outer tree here prevents clean
4307
+ // checkout initialization from erasing synchronized dirt.
4308
+ { ignoreBusy: true }
3191
4309
  );
3192
4310
  ensureConfiguredProjects();
3193
- let result = await performWorkspaceSync({
4311
+ const result = await performWorkspaceSync({
3194
4312
  attemptId: crypto.randomUUID(),
3195
4313
  trigger: { type: "connect" }
3196
4314
  });
3197
- if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
3198
- const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
3199
- if (staleAfterInbound.length > 0) {
3200
- pruneStaleOuterWorkspaceEntries(staleAfterInbound);
3201
- stageDurableWorkspaceDeletions();
3202
- result = await performWorkspaceSync({
3203
- attemptId: crypto.randomUUID(),
3204
- trigger: { type: "connect" }
3205
- });
3206
- }
3207
- }
3208
4315
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
3209
4316
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4317
+ const activeMountIds = new Set(result.activeMountIds ?? []);
3210
4318
  const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
3211
- (deletion) => deletion.kind === "project" && !deletion.projectDeleted ? [deletion.tombstoneId] : []
4319
+ (deletion) => deletion.kind === "project" && !deletion.projectDeleted && checkoutPathMovePublicationComplete(
4320
+ deletion.branchNames.map((branchName) => projectMountId(deletion.projectId, branchName)),
4321
+ activeMountIds
4322
+ ) ? [deletion.tombstoneId] : []
3212
4323
  );
3213
4324
  if (layoutCleanupIds.length > 0) {
3214
4325
  projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
@@ -3224,18 +4335,37 @@ async function startWorker(options) {
3224
4335
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
3225
4336
  });
3226
4337
  } catch (error) {
3227
- workspaceConfigured = fs.existsSync(path.join(workspaceShadowRoot, ".git"));
3228
- for (const project of message.projects) {
3229
- for (const branch of project.branches) {
3230
- pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
3231
- projectId: project.projectId,
3232
- branchName: branch.branchName
3233
- });
4338
+ if (error instanceof RegistryAuthConfigurationError) {
4339
+ workspaceConfigured = false;
4340
+ githubCredential = null;
4341
+ configuredCredentialGenerationFingerprint = null;
4342
+ let childTerminationProven = true;
4343
+ try {
4344
+ await terminateActiveCredentialBearingChildren();
4345
+ } catch (childError) {
4346
+ childTerminationProven = false;
4347
+ process.stderr.write(
4348
+ `[r5d-worker] failed to fully terminate credential-bearing children: ${childError instanceof Error ? childError.message : String(childError)}
4349
+ `
4350
+ );
3234
4351
  }
4352
+ process.stderr.write(
4353
+ `[r5d-worker] fatal credential-generation transition failure; exiting for whole-runtime cleanup: ${error instanceof Error ? error.message : String(error)}
4354
+ `
4355
+ );
4356
+ requireCredentialGenerationRestart(true, void 0, 1);
4357
+ }
4358
+ workspaceConfigured = false;
4359
+ const deferred = error instanceof ProjectWorkspaceConfigurationDeferredError || error instanceof ProjectWorkspacePendingBranchPathChangeError;
4360
+ if (deferred) pendingCheckouts.clear();
4361
+ for (const pending of deferredProjectConfigurationPendingBranches(message.projects)) {
4362
+ pendingCheckouts.set(projectBranchKey(pending.projectId, pending.branchName), pending);
3235
4363
  }
3236
4364
  return {
3237
4365
  result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
3238
- pending: [...pendingCheckouts.values()],
4366
+ pending: [...pendingCheckouts.values()].sort(
4367
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
4368
+ ),
3239
4369
  aheadOfOriginBranches: []
3240
4370
  };
3241
4371
  } finally {
@@ -3330,12 +4460,22 @@ async function startWorker(options) {
3330
4460
  ws.addEventListener("message", (event) => {
3331
4461
  void (async () => {
3332
4462
  const message = JSON.parse(String(event.data));
4463
+ const messageAdmissionGeneration = workerAdmissionGeneration;
4464
+ const assertMessageAdmission = () => assertWorkerChildAdmission({
4465
+ capturedGeneration: messageAdmissionGeneration,
4466
+ currentGeneration: workerAdmissionGeneration,
4467
+ sourceSocketIsCurrent: currentWorkerSocket === ws,
4468
+ sourceSocketOpen: ws.readyState === WebSocket.OPEN,
4469
+ workspaceConfigured,
4470
+ runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
4471
+ });
3333
4472
  if (message.type === "connected") {
3334
4473
  return;
3335
4474
  }
3336
4475
  if (message.type === "workspace_config") {
4476
+ advanceWorkerAdmissionGeneration();
3337
4477
  const configured = await configureWorkerWorkspace(message);
3338
- sendWorkerMessage(ws, {
4478
+ sendWorkerMessageFromCurrentSource(ws, {
3339
4479
  type: "workspace_configured",
3340
4480
  requestId: message.requestId,
3341
4481
  result: configured.result,
@@ -3370,59 +4510,85 @@ async function startWorker(options) {
3370
4510
  }
3371
4511
  if (message.type === "create_project_branch") {
3372
4512
  try {
3373
- projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch({
4513
+ const pendingBranch = {
4514
+ branchId: message.branchId,
3374
4515
  projectId: message.projectId,
3375
4516
  branchName: message.targetBranch
3376
- });
4517
+ };
3377
4518
  const created = await workspaceSyncSingleFlight.runMutation(() => {
3378
4519
  const project = projectConfigById.get(message.projectId);
3379
4520
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3380
- if (!readyProjectIds.has(project.projectId)) {
3381
- throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
4521
+ assertRepositoryExecutionEnabled(project);
4522
+ if (!repositoryMirrorWritesAllowed(project)) {
4523
+ throw new Error(`Project ${message.projectId} branch creation is disabled while its repository connection is transitioning`);
3382
4524
  }
3383
- if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
3384
- throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
3385
- }
3386
- if (project.branches.some(({ branchName }) => branchName === message.targetBranch)) {
3387
- throw new Error(`Project branch ${message.targetBranch} already exists`);
3388
- }
3389
- const createdBranch = createLinkedProjectBranch({
3390
- projectRoot: configuredProjectRoot(projectsRoot, project),
3391
- sourceBranchName: message.sourceBranch,
3392
- branchName: message.targetBranch
3393
- });
3394
- projectConfigById.set(project.projectId, {
3395
- ...project,
3396
- branches: [
3397
- ...project.branches,
3398
- {
3399
- branchName: message.targetBranch,
3400
- sourceBranchName: message.sourceBranch,
3401
- baseCommitHash: createdBranch.baseCommitHash
4525
+ const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
4526
+ ({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
4527
+ );
4528
+ projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
4529
+ let gitBranchCreated = false;
4530
+ try {
4531
+ if (!readyProjectIds.has(project.projectId)) {
4532
+ throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
4533
+ }
4534
+ if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
4535
+ throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
4536
+ }
4537
+ const existingBranch = project.branches.find(({ branchName }) => branchName === message.targetBranch);
4538
+ if (existingBranch) {
4539
+ if (!pendingWasRecorded) throw new Error(`Project branch ${message.targetBranch} already exists`);
4540
+ if (existingBranch.branchId !== message.branchId) {
4541
+ throw new Error(`Pending project branch ${message.targetBranch} belongs to another durable branch incarnation`);
3402
4542
  }
3403
- ]
3404
- });
3405
- lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
3406
- return createdBranch;
4543
+ const existingPath = configuredProjectBranchPath(projectsRoot, project, message.targetBranch);
4544
+ if (!hasProjectWorktree(existingPath)) {
4545
+ throw new Error(`Pending project branch ${message.targetBranch} is not initialized on this worker`);
4546
+ }
4547
+ return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
4548
+ }
4549
+ const createdBranch = createOrRetryLinkedProjectBranch({
4550
+ projectRoot: configuredProjectRoot(projectsRoot, project),
4551
+ primaryBranchName: primaryProjectBranch(project),
4552
+ sourceBranchName: message.sourceBranch,
4553
+ branchName: message.targetBranch,
4554
+ pendingRetry: pendingWasRecorded
4555
+ });
4556
+ gitBranchCreated = true;
4557
+ projectConfigById.set(project.projectId, {
4558
+ ...project,
4559
+ branches: [
4560
+ ...project.branches,
4561
+ {
4562
+ branchId: message.branchId,
4563
+ branchName: message.targetBranch,
4564
+ sourceBranchName: message.sourceBranch,
4565
+ baseCommitHash: createdBranch.baseCommitHash
4566
+ }
4567
+ ]
4568
+ });
4569
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
4570
+ return createdBranch;
4571
+ } catch (error) {
4572
+ if (!pendingWasRecorded && !gitBranchCreated && !projectBranchMayExistAfterCreateFailure(error)) {
4573
+ projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
4574
+ }
4575
+ throw error;
4576
+ }
3407
4577
  });
4578
+ pendingCreatedBranchPublicationNotBefore.set(
4579
+ pendingCreatedBranchKey(message.projectId, message.targetBranch),
4580
+ Date.now() + PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS
4581
+ );
3408
4582
  sendWorkerMessage(ws, {
3409
4583
  type: "operation_result",
3410
4584
  requestId: message.requestId,
3411
4585
  result: {
3412
4586
  type: "create_project_branch",
4587
+ branchId: message.branchId,
3413
4588
  branchName: message.targetBranch,
3414
4589
  baseCommitHash: created.baseCommitHash
3415
4590
  }
3416
4591
  });
3417
- markWorkspaceDirty(
3418
- {
3419
- type: "manual",
3420
- projectId: message.projectId,
3421
- branchName: message.targetBranch,
3422
- detail: `created project branch from ${message.sourceBranch}`
3423
- },
3424
- true
3425
- );
3426
4592
  } catch (error) {
3427
4593
  sendWorkerMessage(ws, {
3428
4594
  type: "operation_result",
@@ -3435,42 +4601,66 @@ async function startWorker(options) {
3435
4601
  if (message.type === "delete_project_branch") {
3436
4602
  try {
3437
4603
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
3438
- projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
3439
- projectId: message.projectId,
3440
- branchName: message.branchName
3441
- });
3442
- await workspaceSyncSingleFlight.runMutation(() => {
4604
+ const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
3443
4605
  const project = projectConfigById.get(message.projectId);
3444
4606
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3445
- if (pendingMirrorDeletes.has(deletionKey)) return;
4607
+ assertRepositoryExecutionEnabled(project);
4608
+ if (!repositoryMirrorWritesAllowed(project)) {
4609
+ throw new Error(`Project ${message.projectId} branch deletion is disabled while its repository connection is transitioning`);
4610
+ }
4611
+ const configuredBranch = project.branches.find(({ branchName }) => branchName === message.branchName);
4612
+ const pendingLocalBranch = projectWorkspaceState.locallyPendingCreatedBranches.find(
4613
+ ({ projectId, branchName }) => projectId === message.projectId && branchName === message.branchName
4614
+ );
4615
+ assertProjectBranchDeletionIncarnation({
4616
+ requiredBranchId: message.branchId,
4617
+ configuredBranch,
4618
+ pendingLocalBranch
4619
+ });
4620
+ const hasDurableTombstone = projectWorkspaceState.tombstones.some(
4621
+ (tombstone) => tombstone.kind === "branch" && tombstone.projectId === message.projectId && tombstone.branchName === message.branchName
4622
+ );
4623
+ if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
4624
+ return false;
4625
+ }
4626
+ projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
4627
+ projectId: message.projectId,
4628
+ branchName: message.branchName
4629
+ });
4630
+ pendingCreatedBranchPublicationNotBefore.delete(pendingCreatedBranchKey(message.projectId, message.branchName));
4631
+ if (pendingMirrorDeletes.has(deletionKey)) return true;
3446
4632
  stageProjectBranchDeletion(project, message.branchName, true);
3447
4633
  projectConfigById.set(project.projectId, {
3448
4634
  ...project,
3449
4635
  branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
3450
4636
  });
4637
+ return true;
3451
4638
  });
3452
- const result = await runWorkspaceSync({
3453
- trigger: {
3454
- type: "manual",
3455
- projectId: message.projectId,
3456
- branchName: message.branchName,
3457
- detail: "deleted project branch"
3458
- },
3459
- confirmedLargeDiff: true,
3460
- confirmationReason: "explicit project branch deletion",
3461
- sendResult: false
3462
- });
3463
- if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
3464
- throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
3465
- }
3466
- if (pendingMirrorDeletes.has(deletionKey)) {
3467
- throw new Error("Project branch deletion did not remove the hidden mirror ref");
4639
+ if (deletionNeedsSync) {
4640
+ const result = await runWorkspaceSync({
4641
+ trigger: {
4642
+ type: "manual",
4643
+ projectId: message.projectId,
4644
+ branchName: message.branchName,
4645
+ detail: "deleted project branch"
4646
+ },
4647
+ confirmedLargeDiff: true,
4648
+ confirmationReason: "explicit project branch deletion",
4649
+ sendResult: false
4650
+ });
4651
+ if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
4652
+ throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
4653
+ }
4654
+ if (pendingMirrorDeletes.has(deletionKey)) {
4655
+ throw new Error("Project branch deletion did not remove the hidden mirror ref");
4656
+ }
3468
4657
  }
3469
4658
  sendWorkerMessage(ws, {
3470
4659
  type: "operation_result",
3471
4660
  requestId: message.requestId,
3472
4661
  result: {
3473
4662
  type: "delete_project_branch",
4663
+ branchId: message.branchId,
3474
4664
  branchName: message.branchName
3475
4665
  }
3476
4666
  });
@@ -3658,9 +4848,13 @@ async function startWorker(options) {
3658
4848
  });
3659
4849
  return;
3660
4850
  }
3661
- const releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
4851
+ await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
4852
+ workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
4853
+ });
4854
+ let releaseWorkspaceMutation;
3662
4855
  let mutationLeaseTransferred = false;
3663
4856
  try {
4857
+ releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
3664
4858
  const resolvedTarget = resolveMessageTarget(message.target);
3665
4859
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
3666
4860
  `);
@@ -3669,6 +4863,7 @@ async function startWorker(options) {
3669
4863
  message,
3670
4864
  resolvedTarget,
3671
4865
  planRoot,
4866
+ assertAdmission: assertMessageAdmission,
3672
4867
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
3673
4868
  ...targetMayMutateVisibleWorkspace(message.target) ? {
3674
4869
  onTerminal: () => {
@@ -3678,13 +4873,14 @@ async function startWorker(options) {
3678
4873
  });
3679
4874
  mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
3680
4875
  } catch (error) {
3681
- sendWorkerMessage(ws, {
4876
+ sendWorkerMessageFromCurrentSource(ws, {
3682
4877
  type: "pty_error",
3683
4878
  requestId: message.requestId,
3684
4879
  ptyId: message.ptyId,
3685
4880
  error: error instanceof Error ? error.message : String(error)
3686
4881
  });
3687
4882
  } finally {
4883
+ workspaceSyncPriorityPtyTargets.delete(message.ptyId);
3688
4884
  if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
3689
4885
  }
3690
4886
  return;
@@ -3721,15 +4917,29 @@ async function startWorker(options) {
3721
4917
  return;
3722
4918
  }
3723
4919
  let result;
4920
+ let targetReserved = false;
3724
4921
  try {
4922
+ await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
4923
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
4924
+ targetReserved = true;
4925
+ });
3725
4926
  const runCommand = async () => {
3726
4927
  const resolvedTarget = resolveMessageTarget(message.target);
3727
4928
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
3728
4929
  `);
3729
- return await executeCommand({ message, resolvedTarget, baseUrl, token, artifactRoot, planRoot });
4930
+ return await executeCommand({
4931
+ message,
4932
+ resolvedTarget,
4933
+ baseUrl,
4934
+ token,
4935
+ artifactRoot,
4936
+ planRoot,
4937
+ assertAdmission: assertMessageAdmission
4938
+ });
3730
4939
  };
3731
4940
  result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
3732
4941
  } catch (error) {
4942
+ if (error instanceof StaleWorkerAdmissionError) return;
3733
4943
  result = {
3734
4944
  type: "exec_result",
3735
4945
  requestId: message.requestId,
@@ -3738,6 +4948,8 @@ async function startWorker(options) {
3738
4948
  exitCode: 1,
3739
4949
  error: error instanceof Error ? error.message : String(error)
3740
4950
  };
4951
+ } finally {
4952
+ if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
3741
4953
  }
3742
4954
  ws.send(JSON.stringify(result));
3743
4955
  if (targetMayMutateVisibleWorkspace(message.target)) {
@@ -3775,7 +4987,8 @@ async function startWorker(options) {
3775
4987
  baseUrl,
3776
4988
  token,
3777
4989
  artifactRoot,
3778
- planRoot
4990
+ planRoot,
4991
+ assertAdmission: assertMessageAdmission
3779
4992
  });
3780
4993
  } finally {
3781
4994
  if (targetMayMutateVisibleWorkspace(message.target)) {
@@ -3791,9 +5004,21 @@ async function startWorker(options) {
3791
5004
  }
3792
5005
  }
3793
5006
  };
3794
- const execution = runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
5007
+ const execution = (async () => {
5008
+ let targetReserved = false;
5009
+ try {
5010
+ await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
5011
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5012
+ targetReserved = true;
5013
+ });
5014
+ await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
5015
+ } finally {
5016
+ if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
5017
+ }
5018
+ })();
3795
5019
  void execution.catch((error) => {
3796
- sendWorkerMessage(ws, {
5020
+ if (error instanceof StaleWorkerAdmissionError) return;
5021
+ sendWorkerMessageFromCurrentSource(ws, {
3797
5022
  type: "exec_start_error",
3798
5023
  requestId: message.requestId,
3799
5024
  runId: message.runId,
@@ -3803,6 +5028,14 @@ async function startWorker(options) {
3803
5028
  return;
3804
5029
  }
3805
5030
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
5031
+ const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
5032
+ const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
5033
+ if (reservesVisibleWorkspace) {
5034
+ await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
5035
+ workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
5036
+ });
5037
+ }
5038
+ let dirtyTrigger;
3806
5039
  try {
3807
5040
  const result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
3808
5041
  const resolvedTarget = resolveMessageTarget(message.target);
@@ -3822,13 +5055,14 @@ async function startWorker(options) {
3822
5055
  result
3823
5056
  })
3824
5057
  );
3825
- if ((message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target)) {
3826
- markWorkspaceDirty({
5058
+ if (mutatesVisibleWorkspace) {
5059
+ dirtyTrigger = {
3827
5060
  type: message.type,
3828
5061
  sessionId: message.sessionId,
3829
5062
  toolCallId: message.requestId,
3830
5063
  ...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
3831
- });
5064
+ };
5065
+ markWorkspaceDirty(dirtyTrigger);
3832
5066
  }
3833
5067
  } catch (error) {
3834
5068
  ws.send(
@@ -3838,6 +5072,13 @@ async function startWorker(options) {
3838
5072
  error: error instanceof Error ? error.message : String(error)
3839
5073
  })
3840
5074
  );
5075
+ } finally {
5076
+ if (reservesVisibleWorkspace) {
5077
+ workspaceSyncPriorityOperationTargets.delete(message.requestId);
5078
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5079
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5080
+ }
5081
+ }
3841
5082
  }
3842
5083
  return;
3843
5084
  }
@@ -3875,6 +5116,7 @@ async function startWorker(options) {
3875
5116
  terminalReplayTimer = void 0;
3876
5117
  }
3877
5118
  if (currentWorkerSocket === ws) {
5119
+ advanceWorkerAdmissionGeneration();
3878
5120
  currentWorkerSocket = null;
3879
5121
  }
3880
5122
  closeAllPtys();
@@ -3887,16 +5129,16 @@ async function startWorker(options) {
3887
5129
  if (shutdownAfterClose) {
3888
5130
  process.exit(0);
3889
5131
  }
3890
- if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
5132
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
3891
5133
  process.stderr.write(
3892
- `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
5134
+ `[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
3893
5135
  `
3894
5136
  );
3895
5137
  const reconnect = () => {
3896
5138
  void startWorker(options).catch((error) => {
3897
5139
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
3898
5140
  `);
3899
- if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
5141
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
3900
5142
  setTimeout(reconnect, WORKER_RECONNECT_DELAY_MS);
3901
5143
  return;
3902
5144
  }
@@ -3978,6 +5220,7 @@ export {
3978
5220
  resolveWorkerFilePath,
3979
5221
  resolveWorkerSessionTarget,
3980
5222
  syncSessionArtifacts,
5223
+ workerChildProcessEnvironment,
3981
5224
  workerGitSecurityTestHarness,
3982
5225
  writeWorkerTextFile
3983
5226
  };