@ricsam/r5d-worker 0.0.77 → 0.0.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/main.cjs +1562 -335
  3. package/dist/cjs/managed-paths.cjs +101 -1
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-workspace-state.cjs +184 -5
  6. package/dist/cjs/project-worktrees.cjs +641 -56
  7. package/dist/cjs/registry-auth.cjs +310 -0
  8. package/dist/cjs/repository-transition-policy.cjs +49 -0
  9. package/dist/cjs/supervisor.cjs +62 -11
  10. package/dist/cjs/working-tree-mirror.cjs +191 -34
  11. package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
  12. package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
  14. package/dist/cjs/workspace-git-sync.cjs +1028 -54
  15. package/dist/cjs/workspace-mount-boundary.cjs +71 -0
  16. package/dist/cjs/workspace-path-move.cjs +195 -0
  17. package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
  18. package/dist/cjs/workspace-project-config-policy.cjs +46 -0
  19. package/dist/cjs/workspace-publication-evidence.cjs +46 -0
  20. package/dist/mjs/main.mjs +1596 -341
  21. package/dist/mjs/managed-paths.mjs +100 -1
  22. package/dist/mjs/package.json +1 -1
  23. package/dist/mjs/project-workspace-state.mjs +181 -5
  24. package/dist/mjs/project-worktrees.mjs +632 -55
  25. package/dist/mjs/registry-auth.mjs +269 -0
  26. package/dist/mjs/repository-transition-policy.mjs +22 -0
  27. package/dist/mjs/supervisor.mjs +61 -11
  28. package/dist/mjs/working-tree-mirror.mjs +190 -34
  29. package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
  30. package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
  31. package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
  32. package/dist/mjs/workspace-git-sync.mjs +1023 -54
  33. package/dist/mjs/workspace-mount-boundary.mjs +37 -0
  34. package/dist/mjs/workspace-path-move.mjs +160 -0
  35. package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
  36. package/dist/mjs/workspace-project-config-policy.mjs +21 -0
  37. package/dist/mjs/workspace-publication-evidence.mjs +21 -0
  38. package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
  39. package/dist/types/main.d.ts +175 -1
  40. package/dist/types/managed-paths.d.ts +11 -0
  41. package/dist/types/project-workspace-state.d.ts +45 -1
  42. package/dist/types/project-worktrees.d.ts +119 -2
  43. package/dist/types/registry-auth.d.ts +47 -0
  44. package/dist/types/repository-transition-policy.d.ts +26 -0
  45. package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
  46. package/dist/types/supervisor-signal-fixture.d.ts +1 -0
  47. package/dist/types/supervisor.d.ts +6 -4
  48. package/dist/types/working-tree-mirror.d.ts +7 -0
  49. package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
  50. package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
  51. package/dist/types/workspace-command-sync-policy.d.ts +9 -0
  52. package/dist/types/workspace-git-sync.d.ts +33 -3
  53. package/dist/types/workspace-mount-boundary.d.ts +10 -0
  54. package/dist/types/workspace-path-move.d.ts +21 -0
  55. package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
  56. package/dist/types/workspace-project-config-policy.d.ts +34 -0
  57. package/dist/types/workspace-publication-evidence.d.ts +17 -0
  58. package/package.json +1 -1
package/dist/cjs/main.cjs CHANGED
@@ -48,6 +48,7 @@ __export(main_exports, {
48
48
  resolveWorkerFilePath: () => resolveWorkerFilePath,
49
49
  resolveWorkerSessionTarget: () => resolveWorkerSessionTarget,
50
50
  syncSessionArtifacts: () => syncSessionArtifacts,
51
+ workerChildProcessEnvironment: () => workerChildProcessEnvironment,
51
52
  workerGitSecurityTestHarness: () => workerGitSecurityTestHarness,
52
53
  writeWorkerTextFile: () => writeWorkerTextFile
53
54
  });
@@ -57,21 +58,32 @@ var import_node_path = __toESM(require("node:path"), 1);
57
58
  var import_node_crypto = require("node:crypto");
58
59
  var import_node_os = __toESM(require("node:os"), 1);
59
60
  var import_node_child_process = require("node:child_process");
61
+ var import_bun_sqlite = require("bun:sqlite");
60
62
  var import_cli_update = require("./cli-update.cjs");
61
63
  var import_git_process_environment = require("./git-process-environment.cjs");
62
64
  var import_heartbeat = require("./heartbeat.cjs");
63
65
  var import_process_tree = require("./process-tree.cjs");
64
66
  var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
65
67
  var import_port_forward_client = require("./port-forward-client.cjs");
68
+ var import_registry_auth = require("./registry-auth.cjs");
66
69
  var import_workspace_incident_state = require("./workspace-incident-state.cjs");
70
+ var import_workspace_automatic_sync_policy = require("./workspace-automatic-sync-policy.cjs");
67
71
  var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
68
72
  var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
73
+ var import_workspace_path_move = require("./workspace-path-move.cjs");
74
+ var import_workspace_project_config_policy = require("./workspace-project-config-policy.cjs");
75
+ var import_workspace_preserve_only_policy = require("./workspace-preserve-only-policy.cjs");
76
+ var import_workspace_branch_incarnation_policy = require("./workspace-branch-incarnation-policy.cjs");
77
+ var import_workspace_publication_evidence = require("./workspace-publication-evidence.cjs");
78
+ var import_repository_transition_policy = require("./repository-transition-policy.cjs");
69
79
  var import_supervisor = require("./supervisor.cjs");
70
80
  var import_managed_paths = require("./managed-paths.cjs");
71
81
  var import_project_worktrees = require("./project-worktrees.cjs");
72
82
  var import_project_workspace_state = require("./project-workspace-state.cjs");
73
83
  var import_workspace_git_sync = require("./workspace-git-sync.cjs");
74
84
  const import_meta = {};
85
+ class ProjectWorkspaceConfigurationDeferredError extends Error {
86
+ }
75
87
  class WorkerServerUnavailableError extends Error {
76
88
  name = "WorkerServerUnavailableError";
77
89
  }
@@ -84,12 +96,19 @@ const MAX_LINE_LENGTH = 2e3;
84
96
  const WORKSPACE_GIT_QUIET_MS = 5e3;
85
97
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
86
98
  const activeProcesses = /* @__PURE__ */ new Map();
99
+ const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
100
+ const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
101
+ const workspaceSyncPriorityProcessTargets = /* @__PURE__ */ new Map();
102
+ const workspaceSyncPriorityOperationTargets = /* @__PURE__ */ new Map();
87
103
  const pendingProcessTerminals = /* @__PURE__ */ new Map();
88
104
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
89
105
  const activePtys = /* @__PURE__ */ new Map();
106
+ const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
90
107
  let currentWorkerSocket = null;
108
+ let workerAdmissionGeneration = 0;
91
109
  const workspaceMutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
92
110
  let workspaceSyncQueue = Promise.resolve();
111
+ let startupProjectSnapshotRecoveryCompleted = false;
93
112
  const workspaceSyncSingleFlight = {
94
113
  runExclusive(operation) {
95
114
  const queued = workspaceMutationGate.runSync(operation);
@@ -110,7 +129,29 @@ const workspaceSyncSingleFlight = {
110
129
  }
111
130
  };
112
131
  let githubCredential = null;
132
+ let configuredCredentialGenerationFingerprint = null;
133
+ let pendingCredentialGenerationIntentAtProcessStart = null;
134
+ let bootstrapCredentialGenerationFingerprintAtProcessStart = null;
135
+ let bootSessionIdentifierAtProcessStart = null;
136
+ let machineIdentifierAtProcessStart = null;
137
+ let systemdInvocationIdentifierAtProcessStart = null;
138
+ let initialCredentialBootstrapProvenEmpty = false;
113
139
  let visibleGitIdentity = null;
140
+ class StaleWorkerAdmissionError extends Error {
141
+ constructor() {
142
+ super("Worker command admission was invalidated before child creation");
143
+ this.name = "StaleWorkerAdmissionError";
144
+ }
145
+ }
146
+ function advanceWorkerAdmissionGeneration() {
147
+ workerAdmissionGeneration += 1;
148
+ return workerAdmissionGeneration;
149
+ }
150
+ function assertWorkerChildAdmission(input) {
151
+ if (input.capturedGeneration !== input.currentGeneration || !input.sourceSocketIsCurrent || !input.sourceSocketOpen || !input.workspaceConfigured || !input.runtimeHealthy) {
152
+ throw new StaleWorkerAdmissionError();
153
+ }
154
+ }
114
155
  function defaultConfigPath() {
115
156
  return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
116
157
  }
@@ -812,6 +853,183 @@ function readOnlyCredentialStoreHelper(storePath) {
812
853
  function credentialStoreDirectory() {
813
854
  return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "git-credentials");
814
855
  }
856
+ function credentialGenerationReceiptPath(directoryPath = credentialStoreDirectory()) {
857
+ return import_node_path.default.join(directoryPath, ".auth-generation");
858
+ }
859
+ function pendingCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
860
+ return import_node_path.default.join(directoryPath, ".auth-generation-pending");
861
+ }
862
+ function bootstrapCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
863
+ return import_node_path.default.join(directoryPath, ".auth-generation-bootstrap");
864
+ }
865
+ 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})$/;
866
+ 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})$/;
867
+ const SYSTEMD_INVOCATION_IDENTIFIER_PATTERN = /^[0-9a-f]{32}$/;
868
+ function exactObjectKeys(value, expected) {
869
+ const actual = Object.keys(value).sort();
870
+ const expectedKeys = [...expected].sort();
871
+ return actual.length === expectedKeys.length && actual.every((key, index) => key === expectedKeys[index]);
872
+ }
873
+ function pendingCredentialGenerationIntentContent(intent) {
874
+ return `${JSON.stringify({ version: 1, fingerprint: intent.fingerprint, boundary: intent.boundary })}
875
+ `;
876
+ }
877
+ function readPendingCredentialGenerationIntent(intentPath = pendingCredentialGenerationPath()) {
878
+ const stat = lstatIfExists(intentPath);
879
+ if (!stat) return null;
880
+ if (stat.isSymbolicLink() || !stat.isFile()) {
881
+ throw new import_registry_auth.RegistryAuthConfigurationError(new Error(`Pending credential generation intent must be a regular file: ${intentPath}`));
882
+ }
883
+ try {
884
+ const parsed = JSON.parse(import_node_fs.default.readFileSync(intentPath, "utf8"));
885
+ const boundary = parsed.boundary;
886
+ 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)) {
887
+ throw new Error("invalid intent fields");
888
+ }
889
+ const boundaryRecord = boundary;
890
+ const machineId = boundaryRecord.machineId;
891
+ if (!(machineId === null || typeof machineId === "string" && MACHINE_IDENTIFIER_PATTERN.test(machineId))) {
892
+ throw new Error("invalid machine identifier");
893
+ }
894
+ let parsedBoundary;
895
+ if (boundaryRecord.kind === "systemd" && exactObjectKeys(boundaryRecord, ["invocationId", "kind", "machineId"]) && typeof boundaryRecord.invocationId === "string" && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(boundaryRecord.invocationId)) {
896
+ parsedBoundary = {
897
+ kind: "systemd",
898
+ machineId,
899
+ invocationId: boundaryRecord.invocationId
900
+ };
901
+ } else if (boundaryRecord.kind === "boot" && exactObjectKeys(boundaryRecord, ["bootSessionId", "kind", "machineId"]) && (boundaryRecord.bootSessionId === null || typeof boundaryRecord.bootSessionId === "string" && BOOT_SESSION_IDENTIFIER_PATTERN.test(boundaryRecord.bootSessionId))) {
902
+ parsedBoundary = {
903
+ kind: "boot",
904
+ machineId,
905
+ bootSessionId: boundaryRecord.bootSessionId
906
+ };
907
+ } else {
908
+ throw new Error("invalid credential boundary");
909
+ }
910
+ return { fingerprint: parsed.fingerprint, boundary: parsedBoundary };
911
+ } catch (error) {
912
+ throw new import_registry_auth.RegistryAuthConfigurationError(
913
+ new Error(`Pending credential generation intent is malformed: ${intentPath}`, { cause: error })
914
+ );
915
+ }
916
+ }
917
+ function parseBootSessionIdentifier(probe) {
918
+ const value = probe.kernelValue.trim();
919
+ if (probe.platform === "linux") {
920
+ 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;
921
+ }
922
+ if (probe.platform === "darwin") {
923
+ 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;
924
+ }
925
+ return null;
926
+ }
927
+ function readBootSessionIdentifier(probe) {
928
+ if (probe) return parseBootSessionIdentifier(probe);
929
+ try {
930
+ if (process.platform === "linux") {
931
+ return parseBootSessionIdentifier({
932
+ platform: process.platform,
933
+ kernelValue: import_node_fs.default.readFileSync("/proc/sys/kernel/random/boot_id", "utf8")
934
+ });
935
+ }
936
+ const command = process.platform === "darwin" ? ["/usr/sbin/sysctl", "-n", "kern.bootsessionuuid"] : null;
937
+ if (!command) return null;
938
+ const result = Bun.spawnSync(command, {
939
+ stdout: "pipe",
940
+ stderr: "pipe",
941
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
942
+ timeout: 2e3
943
+ });
944
+ if (result.exitCode !== 0) return null;
945
+ return parseBootSessionIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
946
+ } catch {
947
+ return null;
948
+ }
949
+ }
950
+ function parseMachineIdentifier(probe) {
951
+ const value = probe.kernelValue.trim();
952
+ if (probe.platform === "linux") {
953
+ return /^[0-9a-f]{32}$/i.test(value) ? `linux:${value.toLowerCase()}` : null;
954
+ }
955
+ 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;
956
+ return uuid ? `${probe.platform}:${uuid.toLowerCase()}` : null;
957
+ }
958
+ function readMachineIdentifier(probe) {
959
+ if (probe) return parseMachineIdentifier(probe);
960
+ try {
961
+ if (process.platform === "linux") {
962
+ return parseMachineIdentifier({ platform: process.platform, kernelValue: import_node_fs.default.readFileSync("/etc/machine-id", "utf8") });
963
+ }
964
+ const command = process.platform === "darwin" ? ["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"] : null;
965
+ if (!command) return null;
966
+ const result = Bun.spawnSync(command, {
967
+ stdout: "pipe",
968
+ stderr: "pipe",
969
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
970
+ timeout: 2e3
971
+ });
972
+ if (result.exitCode !== 0) return null;
973
+ return parseMachineIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
974
+ } catch {
975
+ return null;
976
+ }
977
+ }
978
+ function credentialAuthorityLockPath(rootPath = import_node_path.default.join(import_node_os.default.homedir(), ".r5d")) {
979
+ return import_node_path.default.join(rootPath, ".credential-authority.sqlite");
980
+ }
981
+ function acquireCredentialAuthorityLock(input) {
982
+ const rootPath = import_node_path.default.resolve(input.rootPath ?? import_node_path.default.join(import_node_os.default.homedir(), ".r5d"));
983
+ const rootStat = lstatIfExists(rootPath);
984
+ if (rootStat) {
985
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
986
+ throw new Error(`Worker credential authority root must be a directory: ${rootPath}`);
987
+ }
988
+ } else {
989
+ import_node_fs.default.mkdirSync(rootPath, { recursive: true, mode: 448 });
990
+ }
991
+ import_node_fs.default.chmodSync(rootPath, 448);
992
+ const lockPath = credentialAuthorityLockPath(rootPath);
993
+ const existing = lstatIfExists(lockPath);
994
+ if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
995
+ throw new Error(`Worker credential authority database must be a regular file: ${lockPath}`);
996
+ }
997
+ const database = new import_bun_sqlite.Database(lockPath, { create: true, strict: true });
998
+ try {
999
+ database.exec("PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE");
1000
+ database.exec("CREATE TABLE IF NOT EXISTS authority_metadata (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), machine_id TEXT)");
1001
+ database.exec("BEGIN IMMEDIATE");
1002
+ try {
1003
+ const row = database.query("SELECT machine_id AS machineId FROM authority_metadata WHERE singleton = 1").get();
1004
+ if (row) {
1005
+ if (row.machineId === null || input.machineId === null) {
1006
+ if (row.machineId !== input.machineId) {
1007
+ throw new Error("Cannot prove that the worker credential authority database belongs to this machine");
1008
+ }
1009
+ } else if (row.machineId !== input.machineId) {
1010
+ throw new Error("Worker credential authority home belongs to a different machine; refusing to move or share credential state");
1011
+ }
1012
+ } else {
1013
+ database.query("INSERT INTO authority_metadata (singleton, machine_id) VALUES (1, ?)").run(input.machineId);
1014
+ }
1015
+ database.exec("COMMIT");
1016
+ } catch (error) {
1017
+ database.exec("ROLLBACK");
1018
+ throw error;
1019
+ }
1020
+ database.exec("BEGIN EXCLUSIVE");
1021
+ return { lockPath, database };
1022
+ } catch (error) {
1023
+ database.close(false);
1024
+ if (error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message)) {
1025
+ throw new Error("Another r5d-worker credential authority is already running under this OS account", { cause: error });
1026
+ }
1027
+ throw error;
1028
+ }
1029
+ }
1030
+ function releaseCredentialAuthorityLockHandle(handle) {
1031
+ handle.database.close(false);
1032
+ }
815
1033
  function legacySharedCredentialStorePath() {
816
1034
  return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "github-credentials");
817
1035
  }
@@ -842,9 +1060,24 @@ function ensurePrivateCredentialStoreDirectory(directoryPath) {
842
1060
  import_node_fs.default.chmodSync(directoryPath, 448);
843
1061
  }
844
1062
  function removeLegacySharedCredentialStore(storePath = legacySharedCredentialStorePath()) {
845
- import_node_fs.default.rmSync(storePath, { force: true });
1063
+ const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
1064
+ prepared.commit();
846
1065
  }
847
1066
  let credentialStoreDirectoryInitialized = false;
1067
+ let credentialAuthorityLockHandle = null;
1068
+ function releaseCredentialAuthorityLock() {
1069
+ const handle = credentialAuthorityLockHandle;
1070
+ credentialAuthorityLockHandle = null;
1071
+ if (!handle) return;
1072
+ try {
1073
+ releaseCredentialAuthorityLockHandle(handle);
1074
+ } catch (error) {
1075
+ process.stderr.write(
1076
+ `[r5d-worker] could not release credential authority lock: ${error instanceof Error ? error.message : String(error)}
1077
+ `
1078
+ );
1079
+ }
1080
+ }
848
1081
  function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory()) {
849
1082
  const stat = lstatIfExists(directoryPath);
850
1083
  if (stat) {
@@ -855,11 +1088,73 @@ function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory(
855
1088
  }
856
1089
  function initializeCredentialStoreDirectoryOnce() {
857
1090
  if (credentialStoreDirectoryInitialized) return;
858
- removeLegacySharedCredentialStore();
859
- resetCredentialStoreDirectory();
1091
+ systemdInvocationIdentifierAtProcessStart = SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? process.env.INVOCATION_ID.toLowerCase() : null;
1092
+ bootSessionIdentifierAtProcessStart = readBootSessionIdentifier();
1093
+ machineIdentifierAtProcessStart = readMachineIdentifier();
1094
+ credentialAuthorityLockHandle = acquireCredentialAuthorityLock({
1095
+ machineId: machineIdentifierAtProcessStart
1096
+ });
1097
+ process.once("exit", releaseCredentialAuthorityLock);
1098
+ initialCredentialBootstrapProvenEmpty = noPriorCredentialArtifactsAtProcessStart();
1099
+ ensurePrivateCredentialStoreDirectory(credentialStoreDirectory());
1100
+ configuredCredentialGenerationFingerprint = readCredentialGenerationReceipt();
1101
+ pendingCredentialGenerationIntentAtProcessStart = readPendingCredentialGenerationIntent();
1102
+ bootstrapCredentialGenerationFingerprintAtProcessStart = readCredentialGenerationReceipt(bootstrapCredentialGenerationPath());
860
1103
  credentialStoreDirectoryInitialized = true;
861
1104
  }
1105
+ function noPriorCredentialArtifactsAtProcessStart() {
1106
+ if (lstatIfExists(credentialStoreDirectory()) || lstatIfExists(legacySharedCredentialStorePath())) return false;
1107
+ try {
1108
+ return ![dockerConfigPath(), containersAuthPath()].some(import_registry_auth.registryAuthHasAppManagedGitHubCredential);
1109
+ } catch {
1110
+ return false;
1111
+ }
1112
+ }
1113
+ function readCredentialGenerationReceipt(receiptPath = credentialGenerationReceiptPath()) {
1114
+ const stat = lstatIfExists(receiptPath);
1115
+ if (!stat) return null;
1116
+ if (stat.isSymbolicLink() || !stat.isFile()) {
1117
+ throw new import_registry_auth.RegistryAuthConfigurationError(new Error(`Credential generation receipt must be a regular file: ${receiptPath}`));
1118
+ }
1119
+ const content = import_node_fs.default.readFileSync(receiptPath, "utf8");
1120
+ const match = /^([0-9a-f]{64})\n$/.exec(content);
1121
+ if (!match) {
1122
+ throw new import_registry_auth.RegistryAuthConfigurationError(new Error(`Credential generation receipt is malformed: ${receiptPath}`));
1123
+ }
1124
+ return match[1];
1125
+ }
862
1126
  const APP_CREDENTIAL_STORE_FILE = /^(?:project|workspace)-[0-9a-f]{64}\.store$/;
1127
+ function planCredentialStoreGeneration(desiredFiles, directoryPath = credentialStoreDirectory()) {
1128
+ ensurePrivateCredentialStoreDirectory(directoryPath);
1129
+ const resolvedDirectory = import_node_path.default.resolve(directoryPath);
1130
+ const updates = /* @__PURE__ */ new Map();
1131
+ const symlinkRemovals = /* @__PURE__ */ new Set();
1132
+ for (const [storePath, content] of desiredFiles) {
1133
+ const resolvedStorePath = import_node_path.default.resolve(storePath);
1134
+ if (import_node_path.default.dirname(resolvedStorePath) !== resolvedDirectory || !APP_CREDENTIAL_STORE_FILE.test(import_node_path.default.basename(resolvedStorePath))) {
1135
+ throw new Error("Invalid app-owned Git credential-store path");
1136
+ }
1137
+ updates.set(resolvedStorePath, content);
1138
+ }
1139
+ for (const name of import_node_fs.default.readdirSync(directoryPath)) {
1140
+ if (!APP_CREDENTIAL_STORE_FILE.test(name)) continue;
1141
+ const storePath = import_node_path.default.join(resolvedDirectory, name);
1142
+ const stat = import_node_fs.default.lstatSync(storePath);
1143
+ if (stat.isSymbolicLink()) {
1144
+ if (updates.has(storePath)) throw new Error(`Required Git credential store must not be a symlink: ${storePath}`);
1145
+ updates.set(storePath, null);
1146
+ symlinkRemovals.add(storePath);
1147
+ continue;
1148
+ }
1149
+ if (!stat.isFile()) throw new Error(`Git credential store must be a regular file: ${storePath}`);
1150
+ if (!updates.has(storePath)) updates.set(storePath, null);
1151
+ }
1152
+ return [...updates].map(([filePath, content]) => ({
1153
+ filePath,
1154
+ content,
1155
+ ...symlinkRemovals.has(filePath) ? { allowSymlinkRemoval: true } : {}
1156
+ }));
1157
+ }
863
1158
  function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentialStoreDirectory()) {
864
1159
  ensurePrivateCredentialStoreDirectory(directoryPath);
865
1160
  const resolvedDirectory = import_node_path.default.resolve(directoryPath);
@@ -877,9 +1172,10 @@ function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentia
877
1172
  const storePath = import_node_path.default.join(directoryPath, name);
878
1173
  const stat = import_node_fs.default.lstatSync(storePath);
879
1174
  if (!stat.isFile() && !stat.isSymbolicLink()) continue;
880
- import_node_fs.default.unlinkSync(storePath);
881
1175
  removed.push(storePath);
882
1176
  }
1177
+ const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)(removed.map((filePath) => ({ filePath, content: null, allowSymlinkRemoval: true })));
1178
+ prepared.commit();
883
1179
  return removed;
884
1180
  }
885
1181
  function removeProjectCredentialStore(projectId) {
@@ -887,7 +1183,8 @@ function removeProjectCredentialStore(projectId) {
887
1183
  const stat = lstatIfExists(storePath);
888
1184
  if (!stat) return;
889
1185
  if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("Git credential store must be a regular file");
890
- import_node_fs.default.unlinkSync(storePath);
1186
+ const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
1187
+ prepared.commit();
891
1188
  }
892
1189
  function decodeGitAuthHeader(authHeader) {
893
1190
  const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
@@ -907,16 +1204,10 @@ function credentialUsernameForAuthHeader(authHeader) {
907
1204
  return credentials.username;
908
1205
  }
909
1206
  function replaceCredentialStoreFile(storePath, content) {
910
- const temporaryPath = import_node_path.default.join(import_node_path.default.dirname(storePath), `.${import_node_path.default.basename(storePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
911
- try {
912
- import_node_fs.default.writeFileSync(temporaryPath, content, { flag: "wx", mode: 384 });
913
- import_node_fs.default.chmodSync(temporaryPath, 384);
914
- import_node_fs.default.renameSync(temporaryPath, storePath);
915
- } finally {
916
- import_node_fs.default.rmSync(temporaryPath, { force: true });
917
- }
1207
+ const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content }]);
1208
+ prepared.commit();
918
1209
  }
919
- function writeCredentialStoreEntries(remotes, storePath) {
1210
+ function credentialStoreContent(remotes) {
920
1211
  const authoritativeEntries = /* @__PURE__ */ new Map();
921
1212
  for (const input of remotes) {
922
1213
  let remote;
@@ -948,23 +1239,179 @@ function writeCredentialStoreEntries(remotes, storePath) {
948
1239
  }
949
1240
  authoritativeEntries.set(hostKey, replacement);
950
1241
  }
951
- const storeStat = lstatIfExists(storePath);
952
- if (storeStat) {
953
- if (storeStat.isSymbolicLink() || !storeStat.isFile()) throw new Error("Git credential store must be a regular file");
954
- }
955
1242
  const replacements = [...authoritativeEntries.values()].filter((entry) => entry !== null);
956
- if (replacements.length === 0) {
957
- if (storeStat) import_node_fs.default.unlinkSync(storePath);
958
- return null;
1243
+ return replacements.length > 0 ? `${replacements.join("\n")}
1244
+ ` : null;
1245
+ }
1246
+ function writeCredentialStoreEntries(remotes, storePath) {
1247
+ const content = credentialStoreContent(remotes);
1248
+ const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content }]);
1249
+ prepared.commit();
1250
+ return content === null ? null : { hasCredentials: true, storePath };
1251
+ }
1252
+ function credentialHelperForRemotes(remotes, storePath, desiredFiles) {
1253
+ if (!desiredFiles) {
1254
+ const credentialStore = writeCredentialStoreEntries(remotes, storePath);
1255
+ return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
1256
+ }
1257
+ const resolvedStorePath = import_node_path.default.resolve(storePath);
1258
+ const content = credentialStoreContent(remotes);
1259
+ const previous = desiredFiles.get(resolvedStorePath);
1260
+ if (desiredFiles.has(resolvedStorePath) && previous !== content) {
1261
+ throw new Error(`Conflicting desired Git credentials for ${resolvedStorePath}`);
1262
+ }
1263
+ desiredFiles.set(resolvedStorePath, content);
1264
+ return content === null ? null : readOnlyCredentialStoreHelper(resolvedStorePath);
1265
+ }
1266
+ function authenticatedRemoteCredentialSecret(remoteUrl, authHeader) {
1267
+ if (authHeader == null) return null;
1268
+ let remote;
1269
+ try {
1270
+ remote = new URL(remoteUrl);
1271
+ } catch {
1272
+ throw new Error("Invalid authenticated Git remote URL");
959
1273
  }
960
- ensurePrivateCredentialStoreDirectory(import_node_path.default.dirname(storePath));
961
- replaceCredentialStoreFile(storePath, `${replacements.join("\n")}
962
- `);
963
- return { hasCredentials: true, storePath };
1274
+ if (remote.protocol !== "https:" && remote.protocol !== "http:") {
1275
+ throw new Error("Unsupported authenticated Git remote protocol");
1276
+ }
1277
+ if (remote.username || remote.password) throw new Error("Git credentials must not be embedded in a remote URL");
1278
+ const credentials = decodeGitAuthHeader(authHeader);
1279
+ if (!credentials) throw new Error("Unsupported Git authorization header");
1280
+ return credentials.password;
1281
+ }
1282
+ function credentialMaterialCommitment(kind, values) {
1283
+ return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify([`r5d-credential-${kind}-v1`, ...values])).digest("hex");
1284
+ }
1285
+ function credentialGenerationFingerprint(config) {
1286
+ const workerApiUrl = new URL(config.workerBaseUrl);
1287
+ if (workerApiUrl.protocol !== "https:" && workerApiUrl.protocol !== "http:") {
1288
+ throw new Error("Unsupported worker API protocol");
1289
+ }
1290
+ const workerCredentials = decodeGitAuthHeader(config.workerAuthHeader);
1291
+ if (!workerCredentials) throw new Error("Unsupported worker authorization header");
1292
+ const materialCommitments = /* @__PURE__ */ new Set();
1293
+ const addSecret = (secret) => {
1294
+ materialCommitments.add(`secret:${credentialMaterialCommitment("secret", [secret])}`);
1295
+ };
1296
+ addSecret(workerCredentials.password);
1297
+ const workspaceSecret = authenticatedRemoteCredentialSecret(config.workspaceRemoteUrl, config.workerAuthHeader);
1298
+ if (workspaceSecret) addSecret(workspaceSecret);
1299
+ for (const project of config.projects) {
1300
+ if (!project.repoHttpUrl) continue;
1301
+ const secret = authenticatedRemoteCredentialSecret(project.repoHttpUrl, project.repoAuthHeader);
1302
+ if (secret) addSecret(secret);
1303
+ }
1304
+ if (config.githubCredential) {
1305
+ addSecret(config.githubCredential.token);
1306
+ materialCommitments.add(
1307
+ `github_authority:${credentialMaterialCommitment("github_authority", [
1308
+ config.githubCredential.token,
1309
+ ...[...config.githubCredential.missingScopes].sort()
1310
+ ])}`
1311
+ );
1312
+ }
1313
+ return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify([...materialCommitments].sort())).digest("hex");
1314
+ }
1315
+ function credentialGenerationTransitionPhase(input) {
1316
+ if (input.committedFingerprint === input.incomingFingerprint) return "current";
1317
+ if (input.pendingFingerprintAtProcessStart !== null) {
1318
+ return input.pendingFingerprintAtProcessStart === input.incomingFingerprint ? "publish_from_clean_restart" : "stage_pending_restart";
1319
+ }
1320
+ return "stage_pending_restart";
1321
+ }
1322
+ function initialCredentialBootstrapIsSafe(input) {
1323
+ 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;
1324
+ }
1325
+ async function fenceCredentialGenerationAtConfigureEntry(input) {
1326
+ if (input.currentFingerprint === input.incomingFingerprint) return false;
1327
+ input.revokeInMemory();
1328
+ await input.terminatePreviousGeneration();
1329
+ return true;
1330
+ }
1331
+ function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
1332
+ if (!required) return;
1333
+ fatalExit(exitCode);
1334
+ throw new Error("Credential generation restart callback returned unexpectedly");
1335
+ }
1336
+ function pendingCredentialGenerationBoundary(input) {
1337
+ if (input.systemdContract && input.systemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.systemdInvocationId)) {
1338
+ return { kind: "systemd", machineId: input.machineId, invocationId: input.systemdInvocationId };
1339
+ }
1340
+ return { kind: "boot", machineId: input.machineId, bootSessionId: input.bootSessionId };
964
1341
  }
965
- function credentialHelperForRemotes(remotes, storePath) {
966
- const credentialStore = writeCredentialStoreEntries(remotes, storePath);
967
- return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
1342
+ function credentialGenerationBoundaryIsComplete(boundary) {
1343
+ return boundary.machineId !== null && (boundary.kind === "systemd" || boundary.bootSessionId !== null);
1344
+ }
1345
+ function credentialGenerationRestartIsContained(input) {
1346
+ const pending = input.pendingBoundary;
1347
+ if (pending === null || pending.machineId === null || input.currentMachineId === null || pending.machineId !== input.currentMachineId) {
1348
+ return false;
1349
+ }
1350
+ if (pending.kind === "systemd") {
1351
+ return input.systemdContract && input.currentSystemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.currentSystemdInvocationId) && pending.invocationId !== input.currentSystemdInvocationId;
1352
+ }
1353
+ return pending.bootSessionId !== null && input.currentBootSessionId !== null && pending.bootSessionId !== input.currentBootSessionId;
1354
+ }
1355
+ function credentialReapContractStatus(probe) {
1356
+ let resolved = probe;
1357
+ if (!resolved) {
1358
+ if (process.platform !== "linux") return "direct";
1359
+ try {
1360
+ const cgroupText = import_node_fs.default.readFileSync("/proc/self/cgroup", "utf8");
1361
+ const unit = cgroupText.match(/(?:^|\/)r5d-worker\.service(?:$|\n)/m)?.[0]?.replace(/^\//, "").trim();
1362
+ if (unit !== "r5d-worker.service") {
1363
+ return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
1364
+ }
1365
+ const result = Bun.spawnSync(
1366
+ [
1367
+ "/usr/bin/systemctl",
1368
+ "show",
1369
+ unit,
1370
+ "--no-pager",
1371
+ "--property=KillMode",
1372
+ "--property=Restart",
1373
+ "--property=SendSIGKILL",
1374
+ "--property=InvocationID"
1375
+ ],
1376
+ {
1377
+ stdout: "pipe",
1378
+ stderr: "pipe",
1379
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
1380
+ timeout: 2e3
1381
+ }
1382
+ );
1383
+ if (result.exitCode !== 0) return "systemd_unverified";
1384
+ resolved = {
1385
+ platform: process.platform,
1386
+ invocationId: process.env.INVOCATION_ID,
1387
+ cgroupText,
1388
+ unitProperties: result.stdout.toString()
1389
+ };
1390
+ } catch {
1391
+ return "systemd_unverified";
1392
+ }
1393
+ }
1394
+ if (resolved.platform !== "linux") return "direct";
1395
+ if (!/(?:^|\/)r5d-worker\.service(?:$|\n)/m.test(resolved.cgroupText)) {
1396
+ return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
1397
+ }
1398
+ if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
1399
+ const properties = new Map(
1400
+ resolved.unitProperties.split(/\r?\n/).filter(Boolean).map((line) => {
1401
+ const separator = line.indexOf("=");
1402
+ return separator < 1 ? [line, ""] : [line.slice(0, separator), line.slice(separator + 1)];
1403
+ })
1404
+ );
1405
+ 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";
1406
+ }
1407
+ function verifiedCredentialReapContract(probe) {
1408
+ return credentialReapContractStatus(probe) === "verified_systemd";
1409
+ }
1410
+ function fenceUnsafeWorkspaceSyncFailure(input) {
1411
+ if (input.hydrationCurrent) return false;
1412
+ input.invalidateExecution();
1413
+ requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
1414
+ return true;
968
1415
  }
969
1416
  const workerGitSecurityTestHarness = {
970
1417
  commandArgs: workerGitCommand.commandArgs,
@@ -976,7 +1423,45 @@ const workerGitSecurityTestHarness = {
976
1423
  pruneCredentialStoreFiles,
977
1424
  readOnlyCredentialStoreHelper,
978
1425
  resetCredentialStoreDirectory,
979
- replaceCredentialStoreFile
1426
+ replaceCredentialStoreFile,
1427
+ configureGitHubAuth(credential, filePaths, configureFiles = import_registry_auth.configureGitHubRegistryAuthFiles) {
1428
+ configureGitHubAuth(credential, filePaths, configureFiles);
1429
+ },
1430
+ githubCredential: () => githubCredential,
1431
+ credentialGenerationFingerprint,
1432
+ credentialGenerationTransitionPhase,
1433
+ initialCredentialBootstrapIsSafe,
1434
+ fenceCredentialGenerationAtConfigureEntry,
1435
+ requireCredentialGenerationRestart,
1436
+ pendingCredentialGenerationBoundary,
1437
+ credentialGenerationBoundaryIsComplete,
1438
+ credentialGenerationRestartIsContained,
1439
+ credentialReapContractStatus,
1440
+ verifiedCredentialReapContract,
1441
+ fenceUnsafeWorkspaceSyncFailure,
1442
+ assertWorkerChildAdmission,
1443
+ terminateCredentialBearingChildren,
1444
+ terminateCredentialBearingChildrenWithRetention,
1445
+ async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
1446
+ return await commitCredentialGeneration(
1447
+ prepared,
1448
+ credential,
1449
+ async () => await terminateCredentialBearingChildren(children),
1450
+ beforeMutation,
1451
+ previousGenerationFenced
1452
+ );
1453
+ },
1454
+ credentialGenerationReceiptPath,
1455
+ credentialAuthorityLockPath,
1456
+ acquireCredentialAuthorityLock,
1457
+ releaseCredentialAuthorityLockHandle,
1458
+ pendingCredentialGenerationPath,
1459
+ pendingCredentialGenerationIntentContent,
1460
+ readPendingCredentialGenerationIntent,
1461
+ readBootSessionIdentifier,
1462
+ readMachineIdentifier,
1463
+ bootstrapCredentialGenerationPath,
1464
+ readCredentialGenerationReceipt
980
1465
  };
981
1466
  function dockerConfigPath() {
982
1467
  return import_node_path.default.join(import_node_os.default.homedir(), ".docker", "config.json");
@@ -984,39 +1469,14 @@ function dockerConfigPath() {
984
1469
  function containersAuthPath() {
985
1470
  return import_node_path.default.join(import_node_os.default.homedir(), ".config", "containers", "auth.json");
986
1471
  }
987
- function readJsonFile(filePath) {
988
- if (!import_node_fs.default.existsSync(filePath)) {
989
- return {};
990
- }
991
- try {
992
- const parsed = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf8"));
993
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
994
- } catch {
995
- return {};
996
- }
997
- }
998
- function writeRegistryAuthFile(filePath, credential) {
999
- const config = readJsonFile(filePath);
1000
- const existingAuths = config.auths && typeof config.auths === "object" && !Array.isArray(config.auths) ? config.auths : {};
1001
- const auths = existingAuths;
1002
- auths[credential.registry] = {
1003
- username: credential.username,
1004
- password: credential.token,
1005
- auth: Buffer.from(`${credential.username}:${credential.token}`).toString("base64")
1006
- };
1007
- config.auths = auths;
1008
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(filePath), { recursive: true });
1009
- import_node_fs.default.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
1010
- `, { mode: 384 });
1011
- import_node_fs.default.chmodSync(filePath, 384);
1472
+ function configureGitHubAuth(credential, filePaths = [dockerConfigPath(), containersAuthPath()], configureFiles = import_registry_auth.configureGitHubRegistryAuthFiles) {
1473
+ githubCredential = null;
1474
+ configureFiles(filePaths, credential);
1475
+ installGitHubCredentialGeneration(credential);
1012
1476
  }
1013
- function configureGitHubAuth(credential) {
1477
+ function installGitHubCredentialGeneration(credential) {
1014
1478
  githubCredential = credential;
1015
- if (!credential) {
1016
- return;
1017
- }
1018
- writeRegistryAuthFile(dockerConfigPath(), credential);
1019
- writeRegistryAuthFile(containersAuthPath(), credential);
1479
+ if (!credential) return;
1020
1480
  if (credential.missingScopes.length > 0) {
1021
1481
  process.stderr.write(
1022
1482
  `[r5d-worker] GitHub token is missing scope(s): ${credential.missingScopes.join(", ")}. Sign in with GitHub again if gh or GHCR access fails.
@@ -1027,6 +1487,21 @@ function configureGitHubAuth(credential) {
1027
1487
  `);
1028
1488
  }
1029
1489
  }
1490
+ async function commitCredentialGeneration(prepared, credential, terminatePreviousGeneration, beforeMutation, previousGenerationFenced = false) {
1491
+ const changed = prepared.changed;
1492
+ try {
1493
+ if (changed && !previousGenerationFenced) {
1494
+ githubCredential = null;
1495
+ await terminatePreviousGeneration();
1496
+ }
1497
+ prepared.commit(beforeMutation);
1498
+ } catch (error) {
1499
+ prepared.discard();
1500
+ throw error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
1501
+ }
1502
+ installGitHubCredentialGeneration(credential);
1503
+ return changed;
1504
+ }
1030
1505
  function containerRegistryEnv() {
1031
1506
  if (!githubCredential) {
1032
1507
  return {};
@@ -1056,6 +1531,44 @@ function githubProcessEnv() {
1056
1531
  ...githubCliEnv(githubCredential?.token)
1057
1532
  };
1058
1533
  }
1534
+ const INHERITED_WORKER_CREDENTIAL_ENV_KEYS = [
1535
+ import_supervisor.WORKER_RUNTIME_ENV,
1536
+ "INVOCATION_ID",
1537
+ "R5D_WORKER_TOKEN",
1538
+ "R5D_TOKEN",
1539
+ "R5DCTL_TOKEN",
1540
+ "R5D_API_KEY",
1541
+ "R5DCTL_API_KEY",
1542
+ "GH_TOKEN",
1543
+ "GITHUB_TOKEN",
1544
+ "GH_ENTERPRISE_TOKEN",
1545
+ "DOCKER_AUTH_CONFIG",
1546
+ "REGISTRY_AUTH_FILE",
1547
+ "R5D_GHCR_AUTH_FILE",
1548
+ "NODE_AUTH_TOKEN",
1549
+ "NPM_TOKEN",
1550
+ "BUN_AUTH_TOKEN"
1551
+ ];
1552
+ function workerChildProcessEnvironment(layers, inherited = process.env, platform = process.platform) {
1553
+ const environment = {};
1554
+ const reservedExact = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS);
1555
+ const reservedFolded = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS.map((key) => key.toLowerCase()));
1556
+ const assignLayer = (source, scrubInheritedCredentials) => {
1557
+ for (const [key, value] of Object.entries(source)) {
1558
+ const folded = key.toLowerCase();
1559
+ if (scrubInheritedCredentials && (platform === "win32" ? reservedFolded.has(folded) : reservedExact.has(key))) continue;
1560
+ if (platform === "win32") {
1561
+ for (const existing of Object.keys(environment)) {
1562
+ if (existing.toLowerCase() === folded) delete environment[existing];
1563
+ }
1564
+ }
1565
+ environment[key] = value;
1566
+ }
1567
+ };
1568
+ assignLayer(inherited, true);
1569
+ for (const layer of layers) assignLayer(layer, false);
1570
+ return environment;
1571
+ }
1059
1572
  function primaryProjectBranch(config) {
1060
1573
  if (config.branches.length === 0) throw new Error(`Project ${config.projectId} has no configured branches`);
1061
1574
  if (config.branches.some(({ branchName }) => branchName === "main")) return "main";
@@ -1083,6 +1596,8 @@ function describeWorkerSessionTarget(target) {
1083
1596
  function resolveWorkerSessionTarget(input) {
1084
1597
  if (input.target.type === "workspace") {
1085
1598
  if (input.target.rootProfile === "visible_projects") {
1599
+ const disabled = [...input.projectConfigById.values()].find(({ executionDisabled }) => executionDisabled);
1600
+ if (disabled) (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(disabled);
1086
1601
  return { target: input.target, rootPath: input.projectsRoot };
1087
1602
  }
1088
1603
  if (!import_node_fs.default.existsSync(import_node_path.default.join(input.workspaceShadowRoot, ".git"))) {
@@ -1093,6 +1608,7 @@ function resolveWorkerSessionTarget(input) {
1093
1608
  const target = input.target;
1094
1609
  const config = input.projectConfigById.get(target.projectId);
1095
1610
  if (!config) throw new Error(`Project ${target.projectId} is missing from the worker workspace configuration`);
1611
+ (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(config);
1096
1612
  if (!config.branches.some(({ branchName }) => branchName === target.branchName)) {
1097
1613
  throw new Error(`Project branch ${target.projectId}/${target.branchName} is missing from the worker workspace configuration`);
1098
1614
  }
@@ -1792,6 +2308,7 @@ async function prepareShellEnvForTarget(input) {
1792
2308
  async function executeCommand(input) {
1793
2309
  const startedAt = Date.now();
1794
2310
  let timeout;
2311
+ let spawnedProcess;
1795
2312
  try {
1796
2313
  const targetProcessEnv = await prepareShellEnvForTarget({
1797
2314
  target: input.resolvedTarget.target,
@@ -1803,18 +2320,17 @@ async function executeCommand(input) {
1803
2320
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1804
2321
  });
1805
2322
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
2323
+ input.assertAdmission();
1806
2324
  const subprocess = Bun.spawn(input.message.argv, {
1807
2325
  cwd,
1808
2326
  stdout: "pipe",
1809
2327
  stderr: "pipe",
1810
2328
  detached: true,
1811
- env: {
1812
- ...process.env,
1813
- ...githubProcessEnv(),
1814
- ...input.message.env ?? {},
1815
- ...targetProcessEnv
1816
- }
2329
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
1817
2330
  });
2331
+ spawnedProcess = subprocess;
2332
+ credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2333
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
1818
2334
  activeProcesses.set(input.message.runId, {
1819
2335
  process: subprocess,
1820
2336
  target: input.resolvedTarget.target,
@@ -1851,6 +2367,7 @@ async function executeCommand(input) {
1851
2367
  durationMs: Date.now() - startedAt
1852
2368
  };
1853
2369
  } catch (error) {
2370
+ if (error instanceof StaleWorkerAdmissionError) throw error;
1854
2371
  return {
1855
2372
  type: "exec_result",
1856
2373
  requestId: input.message.requestId,
@@ -1864,6 +2381,9 @@ async function executeCommand(input) {
1864
2381
  if (timeout) {
1865
2382
  clearTimeout(timeout);
1866
2383
  }
2384
+ if (spawnedProcess) {
2385
+ await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2386
+ }
1867
2387
  activeProcesses.delete(input.message.runId);
1868
2388
  }
1869
2389
  }
@@ -1872,6 +2392,7 @@ async function executeStreamingCommand(input) {
1872
2392
  let started = false;
1873
2393
  let timedOut = false;
1874
2394
  let timeout;
2395
+ let spawnedProcess;
1875
2396
  try {
1876
2397
  if (cancelledProcessRuns.delete(input.message.runId)) {
1877
2398
  sendWorkerMessage(input.ws, {
@@ -1893,6 +2414,7 @@ async function executeStreamingCommand(input) {
1893
2414
  });
1894
2415
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
1895
2416
  const interactive = input.message.interactive === true;
2417
+ input.assertAdmission();
1896
2418
  const subprocess = Bun.spawn(input.message.argv, {
1897
2419
  cwd,
1898
2420
  // Without an explicit stdin the process reads /dev/null and interactive
@@ -1901,13 +2423,11 @@ async function executeStreamingCommand(input) {
1901
2423
  stdout: "pipe",
1902
2424
  stderr: "pipe",
1903
2425
  detached: true,
1904
- env: {
1905
- ...process.env,
1906
- ...githubProcessEnv(),
1907
- ...input.message.env ?? {},
1908
- ...targetProcessEnv
1909
- }
2426
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
1910
2427
  });
2428
+ spawnedProcess = subprocess;
2429
+ credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2430
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
1911
2431
  activeProcesses.set(input.message.runId, {
1912
2432
  process: subprocess,
1913
2433
  target: input.resolvedTarget.target,
@@ -1972,6 +2492,7 @@ async function executeStreamingCommand(input) {
1972
2492
  pendingProcessTerminals.set(input.message.runId, terminal);
1973
2493
  sendWorkerMessage(input.ws, terminal);
1974
2494
  } catch (error) {
2495
+ if (!started && error instanceof StaleWorkerAdmissionError) throw error;
1975
2496
  const message = error instanceof Error ? error.message : String(error);
1976
2497
  if (started) {
1977
2498
  const terminal = {
@@ -1995,10 +2516,27 @@ async function executeStreamingCommand(input) {
1995
2516
  clearTimeout(timeout);
1996
2517
  }
1997
2518
  closeProcessStdin(activeProcesses.get(input.message.runId));
2519
+ if (spawnedProcess) {
2520
+ await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
2521
+ }
1998
2522
  activeProcesses.delete(input.message.runId);
1999
2523
  cancelledProcessRuns.delete(input.message.runId);
2000
2524
  }
2001
2525
  }
2526
+ async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
2527
+ try {
2528
+ await (0, import_process_tree.terminateProcessTree)(subprocess);
2529
+ if (credentialBearingProcessGroups.get(subprocess.pid) === subprocess) {
2530
+ credentialBearingProcessGroups.delete(subprocess.pid);
2531
+ credentialBearingProcessGroupTargets.delete(subprocess.pid);
2532
+ }
2533
+ } catch (error) {
2534
+ process.stderr.write(
2535
+ `[r5d-worker] retained unreaped credential-bearing process group ${subprocess.pid} from ${runId}: ${error instanceof Error ? error.message : String(error)}
2536
+ `
2537
+ );
2538
+ }
2539
+ }
2002
2540
  function closeProcessStdin(active) {
2003
2541
  if (!active?.stdin) {
2004
2542
  return;
@@ -2018,6 +2556,15 @@ function sendWorkerMessage(ws, message) {
2018
2556
  `);
2019
2557
  }
2020
2558
  }
2559
+ function sendWorkerMessageFromCurrentSource(ws, message) {
2560
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2561
+ try {
2562
+ ws.send(JSON.stringify(message));
2563
+ } catch (error) {
2564
+ process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
2565
+ `);
2566
+ }
2567
+ }
2021
2568
  function buildActiveProcessReports() {
2022
2569
  return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
2023
2570
  runId,
@@ -2097,6 +2644,19 @@ rl.on("line", (line) => {
2097
2644
 
2098
2645
  if (message.type === "close") {
2099
2646
  ptyProcess.kill();
2647
+ return;
2648
+ }
2649
+
2650
+ if (message.type === "force_close") {
2651
+ if (process.platform !== "win32" && Number.isSafeInteger(ptyProcess.pid) && ptyProcess.pid > 0) {
2652
+ try {
2653
+ process.kill(-ptyProcess.pid, "SIGKILL");
2654
+ } catch (error) {
2655
+ if (!error || error.code !== "ESRCH") throw error;
2656
+ }
2657
+ } else {
2658
+ ptyProcess.kill("SIGKILL");
2659
+ }
2100
2660
  }
2101
2661
  } catch (error) {
2102
2662
  send({ type: "error", error: error instanceof Error ? error.message : String(error) });
@@ -2128,14 +2688,26 @@ function nodePtyModulePaths() {
2128
2688
  }
2129
2689
  function createNodePtyBridge(options) {
2130
2690
  const child = (0, import_node_child_process.spawn)("node", ["-e", PTY_BRIDGE_SCRIPT], {
2131
- env: {
2132
- ...process.env,
2133
- NODE_PATH: nodePtyModulePaths()
2134
- }
2691
+ env: workerChildProcessEnvironment([{ NODE_PATH: nodePtyModulePaths() }])
2135
2692
  });
2136
2693
  let lineBuffer = "";
2137
2694
  let emittedTerminalEvent = false;
2138
2695
  let stderr = "";
2696
+ let resolveTerminal;
2697
+ const terminal = new Promise((resolve) => {
2698
+ resolveTerminal = resolve;
2699
+ });
2700
+ const markTerminal = () => {
2701
+ resolveTerminal?.();
2702
+ resolveTerminal = void 0;
2703
+ };
2704
+ const terminalWithin = async (timeoutMs) => await Promise.race([
2705
+ terminal.then(() => true),
2706
+ new Promise((resolve) => {
2707
+ const timer = setTimeout(() => resolve(false), timeoutMs);
2708
+ timer.unref();
2709
+ })
2710
+ ]);
2139
2711
  const sendToChild = (message) => {
2140
2712
  if (child.stdin.writable) {
2141
2713
  child.stdin.write(`${JSON.stringify(message)}
@@ -2153,11 +2725,13 @@ function createNodePtyBridge(options) {
2153
2725
  }
2154
2726
  if (event.type === "exit") {
2155
2727
  emittedTerminalEvent = true;
2728
+ markTerminal();
2156
2729
  options.onExit({ exitCode: event.exitCode, signal: event.signal });
2157
2730
  return;
2158
2731
  }
2159
2732
  if (event.type === "error") {
2160
2733
  emittedTerminalEvent = true;
2734
+ markTerminal();
2161
2735
  options.onError(new Error(event.error));
2162
2736
  }
2163
2737
  };
@@ -2186,9 +2760,11 @@ function createNodePtyBridge(options) {
2186
2760
  });
2187
2761
  child.on("error", (error) => {
2188
2762
  emittedTerminalEvent = true;
2763
+ markTerminal();
2189
2764
  options.onError(error);
2190
2765
  });
2191
2766
  child.on("exit", (code, signal) => {
2767
+ markTerminal();
2192
2768
  if (emittedTerminalEvent) {
2193
2769
  return;
2194
2770
  }
@@ -2209,12 +2785,15 @@ function createNodePtyBridge(options) {
2209
2785
  sendToChild({ type: "resize", cols, rows });
2210
2786
  },
2211
2787
  kill() {
2788
+ void this.terminate().catch((error) => options.onTerminationError(error instanceof Error ? error : new Error(String(error))));
2789
+ },
2790
+ async terminate() {
2212
2791
  sendToChild({ type: "close" });
2213
- setTimeout(() => {
2214
- if (!child.killed) {
2215
- child.kill();
2216
- }
2217
- }, 500);
2792
+ if (await terminalWithin(500)) return;
2793
+ sendToChild({ type: "force_close" });
2794
+ if (await terminalWithin(500)) return;
2795
+ child.kill("SIGKILL");
2796
+ if (!await terminalWithin(500)) throw new Error(`PTY bridge ${child.pid ?? "unknown"} did not terminate after SIGKILL`);
2218
2797
  }
2219
2798
  };
2220
2799
  }
@@ -2252,6 +2831,7 @@ async function openPty(input) {
2252
2831
  });
2253
2832
  }
2254
2833
  });
2834
+ input.assertAdmission();
2255
2835
  const ptyProcess = createNodePtyBridge({
2256
2836
  file: shell.file,
2257
2837
  args: shell.args,
@@ -2260,14 +2840,7 @@ async function openPty(input) {
2260
2840
  cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
2261
2841
  rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
2262
2842
  cwd: input.resolvedTarget.rootPath,
2263
- env: {
2264
- ...process.env,
2265
- ...githubProcessEnv(),
2266
- ...input.message.env ?? {},
2267
- ...planProcessEnv,
2268
- ...targetProcessEnv,
2269
- ...shell.env ?? {}
2270
- }
2843
+ env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, planProcessEnv, targetProcessEnv, shell.env ?? {}])
2271
2844
  },
2272
2845
  onOpened: () => {
2273
2846
  sendWorkerMessage(input.ws, {
@@ -2302,6 +2875,14 @@ async function openPty(input) {
2302
2875
  ptyId: input.message.ptyId,
2303
2876
  error: error.message
2304
2877
  });
2878
+ },
2879
+ onTerminationError: (error) => {
2880
+ sendWorkerMessage(input.ws, {
2881
+ type: "pty_error",
2882
+ requestId: input.message.requestId,
2883
+ ptyId: input.message.ptyId,
2884
+ error: error.message
2885
+ });
2305
2886
  }
2306
2887
  });
2307
2888
  activePtys.set(input.message.ptyId, {
@@ -2334,14 +2915,62 @@ function closePty(message) {
2334
2915
  if (!ptyProcess) {
2335
2916
  return;
2336
2917
  }
2337
- activePtys.delete(message.ptyId);
2338
2918
  ptyProcess.kill();
2339
2919
  }
2340
2920
  function closeAllPtys() {
2341
2921
  for (const ptyProcess of activePtys.values()) {
2342
2922
  ptyProcess.kill();
2343
2923
  }
2344
- activePtys.clear();
2924
+ }
2925
+ async function terminateCredentialBearingChildren(children) {
2926
+ const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
2927
+ const failures = results.flatMap(
2928
+ (result, index) => result.status === "rejected" ? [new Error(`Failed to terminate ${children[index].label}`, { cause: result.reason })] : []
2929
+ );
2930
+ if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
2931
+ }
2932
+ async function terminateCredentialBearingChildrenWithRetention(children) {
2933
+ const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
2934
+ const failures = [];
2935
+ results.forEach((result, index) => {
2936
+ const child = children[index];
2937
+ if (result.status === "fulfilled") {
2938
+ child.onTerminated();
2939
+ return;
2940
+ }
2941
+ failures.push(new Error(`Failed to terminate ${child.label}`, { cause: result.reason }));
2942
+ });
2943
+ if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
2944
+ }
2945
+ async function terminateActiveCredentialBearingChildren() {
2946
+ const processGroups = [...credentialBearingProcessGroups.entries()];
2947
+ const ptyEntries = [...activePtys.entries()];
2948
+ for (const active of activeProcesses.values()) closeProcessStdin(active);
2949
+ await terminateCredentialBearingChildrenWithRetention([
2950
+ ...processGroups.map(([processGroupId, subprocess]) => ({
2951
+ label: `process group ${processGroupId}`,
2952
+ terminate: async () => await (0, import_process_tree.terminateProcessTree)(subprocess),
2953
+ onTerminated: () => {
2954
+ if (credentialBearingProcessGroups.get(processGroupId) === subprocess) {
2955
+ credentialBearingProcessGroups.delete(processGroupId);
2956
+ credentialBearingProcessGroupTargets.delete(processGroupId);
2957
+ }
2958
+ for (const [runId, active] of activeProcesses) {
2959
+ if (active.process === subprocess) activeProcesses.delete(runId);
2960
+ }
2961
+ }
2962
+ })),
2963
+ ...ptyEntries.map(([ptyId, active]) => ({
2964
+ label: `PTY ${ptyId}`,
2965
+ terminate: async () => await active.terminate(),
2966
+ onTerminated: () => {
2967
+ if (activePtys.get(ptyId) === active) activePtys.delete(ptyId);
2968
+ }
2969
+ }))
2970
+ ]);
2971
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || credentialBearingProcessGroupTargets.size > 0 || activePtys.size > 0) {
2972
+ throw new Error("Credential-bearing child ownership remained after termination");
2973
+ }
2345
2974
  }
2346
2975
  function websocketUrl(baseUrl, label) {
2347
2976
  const url = new URL("/worker/ws", baseUrl);
@@ -2351,6 +2980,7 @@ function websocketUrl(baseUrl, label) {
2351
2980
  return url.toString();
2352
2981
  }
2353
2982
  async function startWorker(options) {
2983
+ advanceWorkerAdmissionGeneration();
2354
2984
  process.on("uncaughtException", (error) => {
2355
2985
  process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
2356
2986
  `);
@@ -2369,8 +2999,15 @@ async function startWorker(options) {
2369
2999
  const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
2370
3000
  const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2371
3001
  const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
3002
+ const projectWorktreeSnapshotsRoot = import_node_path.default.join(syncRoot, "project-worktree-snapshots");
2372
3003
  const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2373
3004
  const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
3005
+ (0, import_managed_paths.assertDisjointManagedRoots)([
3006
+ { label: "projects root", rootPath: projectsRoot },
3007
+ { label: "sync root", rootPath: syncRoot },
3008
+ { label: "artifacts root", rootPath: artifactRoot },
3009
+ { label: "plans root", rootPath: planRoot }
3010
+ ]);
2374
3011
  process.stdout.write(`[r5d-worker] label: ${label}
2375
3012
  `);
2376
3013
  process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
@@ -2383,26 +3020,43 @@ async function startWorker(options) {
2383
3020
  `);
2384
3021
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
2385
3022
  `);
2386
- const snapshotCleanup = (0, import_project_worktrees.cleanupStaleProjectWorktreeSnapshots)();
2387
- if (snapshotCleanup.removed.length > 0) {
2388
- process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
2389
- `);
2390
- }
2391
- for (const failure of snapshotCleanup.failed) {
2392
- process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
2393
- `);
2394
- }
2395
3023
  runGit(["--version"]);
2396
3024
  await verifyR5dctlAuth(baseUrl, token);
2397
- import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
2398
- import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
2399
- import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2400
- import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2401
- const projectWorkspaceStateStore = new import_project_workspace_state.ProjectWorkspaceStateStore({
2402
- stateRoot: import_node_path.default.join(syncRoot, "project-state"),
2403
- projectsRoot
3025
+ const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
3026
+ if (!startupProjectSnapshotRecoveryCompleted) {
3027
+ const snapshotRecovery = (0, import_project_worktrees.recoverStaleProjectWorktreeSnapshots)({
3028
+ projectsRoot,
3029
+ temporaryRoot: projectWorktreeSnapshotsRoot
3030
+ });
3031
+ if (snapshotRecovery.restored.length > 0) {
3032
+ process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
3033
+ `);
3034
+ }
3035
+ if (snapshotRecovery.removed.length > 0) {
3036
+ process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
3037
+ `);
3038
+ }
3039
+ for (const failure of snapshotRecovery.failed) {
3040
+ process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
3041
+ `);
3042
+ }
3043
+ if (snapshotRecovery.failed.length > 0) {
3044
+ throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
3045
+ }
3046
+ startupProjectSnapshotRecoveryCompleted = true;
3047
+ }
3048
+ import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
3049
+ import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
3050
+ import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
3051
+ import_node_fs.default.mkdirSync(planRoot, { recursive: true });
3052
+ const store = new import_project_workspace_state.ProjectWorkspaceStateStore({
3053
+ stateRoot: import_node_path.default.join(syncRoot, "project-state"),
3054
+ projectsRoot
3055
+ });
3056
+ return { store, state: store.read() };
2404
3057
  });
2405
- let projectWorkspaceState = projectWorkspaceStateStore.read();
3058
+ const projectWorkspaceStateStore = initializedWorkspaceState.store;
3059
+ let projectWorkspaceState = initializedWorkspaceState.state;
2406
3060
  const projectConfigById = /* @__PURE__ */ new Map();
2407
3061
  const pendingCheckouts = /* @__PURE__ */ new Map();
2408
3062
  const readyProjectIds = /* @__PURE__ */ new Set();
@@ -2418,6 +3072,7 @@ async function startWorker(options) {
2418
3072
  let workspaceAutomaticTimer;
2419
3073
  let workspacePeriodicTimer;
2420
3074
  let pendingAutomaticTrigger;
3075
+ const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
2421
3076
  let automaticSyncInFlight = false;
2422
3077
  let terminalReplayTimer;
2423
3078
  let workspaceSyncRequestsInFlight = 0;
@@ -2432,7 +3087,7 @@ async function startWorker(options) {
2432
3087
  const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
2433
3088
  const workspaceProjectRelativePath = (projectId, branchName) => import_node_path.default.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
2434
3089
  const workspacePlanRelativePath = (projectId, branchName) => import_node_path.default.posix.join("plans", projectId, encodeURIComponent(branchName));
2435
- const projectConnection = (project) => {
3090
+ const projectConnection = (project, desiredFiles) => {
2436
3091
  const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
2437
3092
  const originUrl = project.repoHttpUrl ?? mirrorUrl;
2438
3093
  const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
@@ -2441,7 +3096,8 @@ async function startWorker(options) {
2441
3096
  { remoteUrl: mirrorUrl, authHeader: bearerAuthHeader },
2442
3097
  { remoteUrl: originUrl, authHeader: originHeader }
2443
3098
  ],
2444
- credentialStorePathForProject(project.projectId)
3099
+ credentialStorePathForProject(project.projectId),
3100
+ desiredFiles
2445
3101
  );
2446
3102
  if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2447
3103
  return {
@@ -2452,11 +3108,12 @@ async function startWorker(options) {
2452
3108
  credentialHelper
2453
3109
  };
2454
3110
  };
2455
- const projectMirrorConnection = (projectId) => {
3111
+ const projectMirrorConnection = (projectId, desiredFiles) => {
2456
3112
  const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, projectId);
2457
3113
  const credentialHelper = credentialHelperForRemotes(
2458
3114
  [{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader }],
2459
- credentialStorePathForProject(projectId)
3115
+ credentialStorePathForProject(projectId),
3116
+ desiredFiles
2460
3117
  );
2461
3118
  if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2462
3119
  return { mirrorUrl, credentialHelper, mirrorCredentialUsername: workerCredentialUsername };
@@ -2529,6 +3186,29 @@ async function startWorker(options) {
2529
3186
  readyProjectIds.delete(project.projectId);
2530
3187
  reconciledProjectConfigFingerprints.delete(project.projectId);
2531
3188
  };
3189
+ const deactivatePreserveOnlyProjectBranch = (project, branchName) => {
3190
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branchName);
3191
+ pendingCheckouts.delete(projectBranchKey(project.projectId, branchName));
3192
+ if (!import_node_fs.default.existsSync(branchPath)) return;
3193
+ if (!hasProjectWorktree(branchPath)) {
3194
+ throw new Error(`Preserve-only project branch path ${project.projectId}/${branchName} is not a managed Git checkout`);
3195
+ }
3196
+ if (project.branches.length === 0) {
3197
+ throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} without an active anchor branch`);
3198
+ }
3199
+ const primaryBranchName = primaryProjectBranch(project);
3200
+ if (branchName === primaryBranchName) {
3201
+ throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
3202
+ }
3203
+ (0, import_project_worktrees.deleteLinkedProjectBranch)({
3204
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3205
+ primaryBranchName,
3206
+ branchName
3207
+ });
3208
+ import_node_fs.default.rmSync(import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
3209
+ readyProjectIds.delete(project.projectId);
3210
+ reconciledProjectConfigFingerprints.delete(project.projectId);
3211
+ };
2532
3212
  const cleanupConfigForDurableDeletion = (deletion) => {
2533
3213
  const desiredLayout = projectWorkspaceState.desiredProjects.find(
2534
3214
  (project) => project.projectId === deletion.projectId && project.checkoutPathSegments[0] === deletion.checkoutPathSegments[0] && project.checkoutPathSegments[1] === deletion.checkoutPathSegments[1]
@@ -2543,8 +3223,13 @@ async function startWorker(options) {
2543
3223
  repoHttpUrl: liveConfig?.repoHttpUrl ?? null,
2544
3224
  repoAuthHeader: liveConfig?.repoAuthHeader ?? null,
2545
3225
  defaultBranch,
3226
+ repositoryTransitionId: liveConfig?.repositoryTransitionId ?? null,
3227
+ executionDisabled: false,
3228
+ mirrorWritesDisabled: false,
3229
+ preserveOnlyBranches: [],
2546
3230
  branches: branchNames.map(
2547
3231
  (branchName) => liveConfig?.branches.find((branch) => branch.branchName === branchName) ?? {
3232
+ branchId: "deleted",
2548
3233
  branchName,
2549
3234
  sourceBranchName: null,
2550
3235
  // Deletion never consumes the baseline, but retain a structurally
@@ -2559,9 +3244,18 @@ async function startWorker(options) {
2559
3244
  const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
2560
3245
  if (deletion.kind === "project") {
2561
3246
  if (!deletion.projectDeleted) {
2562
- if (import_node_fs.default.existsSync(deletion.managedPath)) {
2563
- (0, import_project_worktrees.removeProjectWorktrees)({ projectRoot: deletion.managedPath, branchNames: deletion.branchNames });
2564
- }
3247
+ const nextProject = projectConfigById.get(deletion.projectId);
3248
+ if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
3249
+ (0, import_workspace_path_move.preserveProjectCheckoutPathMove)({
3250
+ oldProjectRoot: deletion.managedPath,
3251
+ newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
3252
+ projectsDurabilityRoot: projectsRoot,
3253
+ outerWorkspaceRoot: workspaceShadowRoot,
3254
+ branches: deletion.branchNames.map((branchName) => ({
3255
+ branchName,
3256
+ outerWorkspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, branchName)
3257
+ }))
3258
+ });
2565
3259
  readyProjectIds.delete(deletion.projectId);
2566
3260
  reconciledProjectConfigFingerprints.delete(deletion.projectId);
2567
3261
  continue;
@@ -2594,32 +3288,15 @@ async function startWorker(options) {
2594
3288
  });
2595
3289
  }
2596
3290
  };
2597
- const pruneStaleOuterWorkspaceEntries = (entries) => {
2598
- const remove = (candidate) => {
2599
- if (candidate) import_node_fs.default.rmSync(candidate, { recursive: true, force: true });
2600
- };
2601
- for (const entry of entries) {
2602
- if (entry.kind === "branch") {
2603
- projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
2604
- projectId: entry.projectId,
2605
- branchName: entry.branchName
2606
- });
2607
- remove(entry.projectTreePath);
2608
- remove(entry.planTreePath);
2609
- continue;
2610
- }
2611
- remove(entry.projectTreePath);
2612
- remove(entry.planTreePath);
2613
- for (const branch of entry.branches) {
2614
- remove(branch.projectTreePath);
2615
- remove(branch.planTreePath);
2616
- }
2617
- }
2618
- };
2619
3291
  const effectiveWorkerProjects = (projects) => {
2620
3292
  const incomingById = new Map(projects.map((project) => [project.projectId, project]));
2621
- const pending = new Set(
2622
- projectWorkspaceState.locallyPendingCreatedBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName))
3293
+ const preserveOnlyByKey = new Map(
3294
+ projects.flatMap(
3295
+ (project) => project.preserveOnlyBranches.map((branch) => [projectBranchKey(project.projectId, branch.branchName), branch])
3296
+ )
3297
+ );
3298
+ const pending = new Map(
3299
+ projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [projectBranchKey(branch.projectId, branch.branchName), branch])
2623
3300
  );
2624
3301
  return projectWorkspaceState.effectiveDesiredProjects.flatMap((layout) => {
2625
3302
  const incoming = incomingById.get(layout.projectId);
@@ -2627,10 +3304,21 @@ async function startWorker(options) {
2627
3304
  const existing = projectConfigById.get(layout.projectId);
2628
3305
  const branches = layout.branches.flatMap(({ branchName }) => {
2629
3306
  const configured = incoming.branches.find((branch) => branch.branchName === branchName);
2630
- if (configured) return [configured];
3307
+ const pendingBranch = pending.get(projectBranchKey(layout.projectId, branchName));
3308
+ const preservedBranch = preserveOnlyByKey.get(projectBranchKey(layout.projectId, branchName));
3309
+ const pendingIncarnationIsAuthorized = (0, import_workspace_preserve_only_policy.creatorLocalProjectBranchIsAuthorized)({
3310
+ pendingBranchId: pendingBranch?.branchId,
3311
+ preserveOnlyBranchId: preservedBranch?.branchId
3312
+ });
3313
+ const disposition = (0, import_workspace_preserve_only_policy.workerProjectBranchDisposition)({
3314
+ activeConfigured: Boolean(configured),
3315
+ locallyPendingCreated: pendingIncarnationIsAuthorized
3316
+ });
3317
+ if (configured && disposition === "active") return [configured];
3318
+ if (disposition === "preserve_only") return [];
2631
3319
  const retained = existing?.branches.find((branch) => branch.branchName === branchName);
2632
3320
  if (retained) return [retained];
2633
- if (!pending.has(projectBranchKey(layout.projectId, branchName))) return [];
3321
+ if (!pendingBranch?.branchId) return [];
2634
3322
  const branchPath = configuredProjectBranchPath(
2635
3323
  projectsRoot,
2636
3324
  { ...incoming, checkoutPathSegments: layout.checkoutPathSegments },
@@ -2639,6 +3327,7 @@ async function startWorker(options) {
2639
3327
  if (!hasProjectWorktree(branchPath)) return [];
2640
3328
  return [
2641
3329
  {
3330
+ branchId: pendingBranch.branchId,
2642
3331
  branchName,
2643
3332
  sourceBranchName: null,
2644
3333
  baseCommitHash: runGit(["rev-parse", "HEAD"], { cwd: branchPath })
@@ -2648,41 +3337,94 @@ async function startWorker(options) {
2648
3337
  return [{ ...incoming, checkoutPathSegments: [...layout.checkoutPathSegments], branches }];
2649
3338
  });
2650
3339
  };
2651
- const buildWorkspaceMounts = () => {
3340
+ const activeWorkspaceMutationTargets = () => [
3341
+ ...Array.from(activeProcesses.values(), ({ target }) => target),
3342
+ ...credentialBearingProcessGroupTargets.values(),
3343
+ ...workspaceSyncPriorityProcessTargets.values(),
3344
+ ...workspaceSyncPriorityOperationTargets.values(),
3345
+ ...Array.from(activePtys.values(), ({ target }) => target),
3346
+ ...workspaceSyncPriorityPtyTargets.values()
3347
+ ];
3348
+ const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
3349
+ const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
3350
+ const activeTargets = activeWorkspaceMutationTargets();
3351
+ return projectConfigById.get(projectId)?.executionDisabled === true || hasProjectWorktree(branchPath) && (0, import_project_worktrees.projectWorktreeOperationInProgress)(branchPath) || canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.projectBranchHasActiveWorkspaceTarget)(projectId, branchName, activeTargets);
3352
+ };
3353
+ const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3354
+ const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
2652
3355
  const mounts = [];
2653
- for (const project of [...projectConfigById.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
3356
+ const creatorLocalIncarnations = new Map(
3357
+ projectWorkspaceState.locallyPendingCreatedBranches.flatMap(
3358
+ (branch) => branch.branchId ? [[projectBranchKey(branch.projectId, branch.branchName), branch.branchId]] : []
3359
+ )
3360
+ );
3361
+ const checkoutPathMoveBranches = new Set(
3362
+ projectWorkspaceState.pendingTreeDeletions.flatMap(
3363
+ (deletion) => deletion.kind === "project" && !deletion.projectDeleted ? deletion.branchNames.map((branchName) => projectBranchKey(deletion.projectId, branchName)) : []
3364
+ )
3365
+ );
3366
+ for (const project of [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
2654
3367
  const projectRoot = configuredProjectRoot(projectsRoot, project);
2655
3368
  for (const branch of [...project.branches].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
2656
3369
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
3370
+ const branchKey = projectBranchKey(project.projectId, branch.branchName);
3371
+ const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
3372
+ const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
2657
3373
  mounts.push({
2658
3374
  id: projectMountId(project.projectId, branch.branchName),
3375
+ hydrationIncarnationKey: branch.branchId,
2659
3376
  sourcePath: branchPath,
3377
+ durabilityRootPath: projectsRoot,
2660
3378
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2661
3379
  sourceMode: "git",
2662
3380
  hydrateDeletionMode: "git",
2663
- busy: () => hasProjectWorktree(branchPath) && (0, import_project_worktrees.projectWorktreeOperationInProgress)(branchPath)
3381
+ preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3382
+ preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3383
+ busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3384
+ busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
2664
3385
  });
2665
3386
  mounts.push({
2666
3387
  id: projectPlanMountId(project.projectId, branch.branchName),
3388
+ hydrationIncarnationKey: branch.branchId,
2667
3389
  sourcePath: import_node_path.default.join(planRoot, project.projectId, ...branch.branchName.split("/")),
3390
+ durabilityRootPath: planRoot,
2668
3391
  workspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
2669
3392
  sourceMode: "all",
2670
- hydrateDeletionMode: "all"
3393
+ hydrateDeletionMode: "all",
3394
+ preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3395
+ preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3396
+ busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3397
+ busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
2671
3398
  });
2672
3399
  }
2673
3400
  void projectRoot;
2674
3401
  }
2675
3402
  mounts.push({
2676
3403
  id: "workspace-plans",
3404
+ hydrationIncarnationKey: "workspace-plans-v1",
2677
3405
  sourcePath: import_node_path.default.join(planRoot, "workspace"),
3406
+ durabilityRootPath: planRoot,
2678
3407
  workspaceRelativePath: "workspace-plans",
2679
3408
  sourceMode: "all",
2680
- hydrateDeletionMode: "all"
3409
+ hydrateDeletionMode: "all",
3410
+ // Workspace-level agents receive this directory through R5D_PLANS_DIR.
3411
+ // Keep both projection and hydration away from it while any visible-
3412
+ // projects process, PTY, or built-in write/edit is active or reserved.
3413
+ busy: () => {
3414
+ const activeTargets = activeWorkspaceMutationTargets();
3415
+ return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
3416
+ },
3417
+ busyForRecovery: () => {
3418
+ const activeTargets = activeWorkspaceMutationTargets();
3419
+ return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
3420
+ }
2681
3421
  });
2682
3422
  for (const deletion of pendingMirrorDeletes.values()) {
2683
3423
  mounts.push({
2684
3424
  id: deletion.id,
3425
+ hydrationIncarnationKey: deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`,
2685
3426
  sourcePath: deletion.sourcePath,
3427
+ durabilityRootPath: projectsRoot,
2686
3428
  workspaceRelativePath: deletion.workspaceRelativePath,
2687
3429
  sourceMode: "git",
2688
3430
  hydrateDeletionMode: "git",
@@ -2690,7 +3432,9 @@ async function startWorker(options) {
2690
3432
  });
2691
3433
  mounts.push({
2692
3434
  id: `${deletion.id}:plan`,
3435
+ hydrationIncarnationKey: `${deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`}:plan`,
2693
3436
  sourcePath: deletion.planSourcePath,
3437
+ durabilityRootPath: planRoot,
2694
3438
  workspaceRelativePath: deletion.planWorkspaceRelativePath,
2695
3439
  sourceMode: "all",
2696
3440
  hydrateDeletionMode: "all",
@@ -2718,10 +3462,34 @@ async function startWorker(options) {
2718
3462
  const ensureConfiguredProjects = () => {
2719
3463
  for (const project of projectConfigById.values()) {
2720
3464
  validateProjectId(project.projectId);
3465
+ (0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
2721
3466
  for (const branch of project.branches) validateBranchName(branch.branchName);
3467
+ if (project.branches.length === 0) {
3468
+ readyProjectIds.delete(project.projectId);
3469
+ reconciledProjectConfigFingerprints.delete(project.projectId);
3470
+ continue;
3471
+ }
3472
+ if (project.executionDisabled) {
3473
+ readyProjectIds.delete(project.projectId);
3474
+ reconciledProjectConfigFingerprints.delete(project.projectId);
3475
+ for (const branch of project.branches) {
3476
+ pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
3477
+ }
3478
+ continue;
3479
+ }
2722
3480
  const projectRoot = configuredProjectRoot(projectsRoot, project);
2723
3481
  const configFingerprint = (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: workspaceGitIdentity });
2724
- const alreadyReady = readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
3482
+ const durableEnabledTransition = projectWorkspaceState.enabledRepositoryTransitions.find(
3483
+ ({ projectId }) => projectId === project.projectId
3484
+ );
3485
+ const transitionReseedRequired = (0, import_repository_transition_policy.enabledRepositoryTransitionRequiresReseed)({
3486
+ project,
3487
+ lastEnabled: {
3488
+ known: Boolean(durableEnabledTransition),
3489
+ transitionId: durableEnabledTransition?.transitionId ?? null
3490
+ }
3491
+ });
3492
+ const alreadyReady = !transitionReseedRequired && readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
2725
3493
  if (alreadyReady) {
2726
3494
  for (const branch of project.branches) pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
2727
3495
  continue;
@@ -2730,8 +3498,11 @@ async function startWorker(options) {
2730
3498
  const connection = projectConnection(project);
2731
3499
  const states = (0, import_project_worktrees.ensureProjectWorktrees)({
2732
3500
  projectRoot,
3501
+ snapshotRoot: projectWorktreeSnapshotsRoot,
2733
3502
  primaryBranchName: primaryProjectBranch(project),
2734
3503
  branches: project.branches,
3504
+ defaultBranch: project.defaultBranch,
3505
+ ...transitionReseedRequired && project.repositoryTransitionId !== null ? { exactMirrorBranches: /* @__PURE__ */ new Set([primaryProjectBranch(project)]) } : {},
2735
3506
  originUrl: connection.originUrl,
2736
3507
  originCredentialUsername: connection.originCredentialUsername,
2737
3508
  mirrorUrl: connection.mirrorUrl,
@@ -2744,7 +3515,7 @@ async function startWorker(options) {
2744
3515
  for (const state of states) {
2745
3516
  pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
2746
3517
  const key = projectBranchKey(project.projectId, state.branchName);
2747
- if (!lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
3518
+ if (transitionReseedRequired || !lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
2748
3519
  if ((state.ahead ?? 0) > 0) {
2749
3520
  process.stderr.write(
2750
3521
  `[r5d-worker] ${project.projectPath}/${state.branchName} is ${state.ahead} commit(s) ahead of origin; switching workers may require publishing or reconciling those commits
@@ -2752,6 +3523,12 @@ async function startWorker(options) {
2752
3523
  );
2753
3524
  }
2754
3525
  }
3526
+ if (transitionReseedRequired) {
3527
+ projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
3528
+ projectId: project.projectId,
3529
+ transitionId: project.repositoryTransitionId
3530
+ });
3531
+ }
2755
3532
  } catch (error) {
2756
3533
  readyProjectIds.delete(project.projectId);
2757
3534
  reconciledProjectConfigFingerprints.delete(project.projectId);
@@ -2771,7 +3548,7 @@ async function startWorker(options) {
2771
3548
  const collectAheadOfOriginBranches = () => {
2772
3549
  const aheadBranches = [];
2773
3550
  for (const project of projectConfigById.values()) {
2774
- if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
3551
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2775
3552
  const connection = projectConnection(project);
2776
3553
  const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
2777
3554
  if (!tryGit(
@@ -2793,7 +3570,11 @@ async function startWorker(options) {
2793
3570
  if (!hasProjectWorktree(branchPath)) continue;
2794
3571
  try {
2795
3572
  let ahead = 0;
2796
- const originRef = `refs/remotes/origin/${branch.branchName}`;
3573
+ const originRef = `refs/remotes/origin/${(0, import_project_worktrees.projectOriginBranchName)({
3574
+ branchName: branch.branchName,
3575
+ primaryBranchName: primaryProjectBranch(project),
3576
+ defaultBranch: project.defaultBranch
3577
+ })}`;
2797
3578
  if (tryGit(["show-ref", "--verify", "--quiet", originRef], { cwd: branchPath })) {
2798
3579
  const counts = runGit(["rev-list", "--left-right", "--count", `${originRef}...HEAD`], { cwd: branchPath }).split(/\s+/).map(Number);
2799
3580
  ahead = Number.isSafeInteger(counts[1]) ? counts[1] : 0;
@@ -2814,18 +3595,30 @@ async function startWorker(options) {
2814
3595
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2815
3596
  );
2816
3597
  };
2817
- const pushChangedProjectHeads = (activeMountIds, publishedHead) => {
2818
- const active = new Set(activeMountIds);
3598
+ const captureProjectHeadsForWorkspacePublication = () => {
3599
+ const heads = /* @__PURE__ */ new Map();
2819
3600
  for (const project of projectConfigById.values()) {
2820
3601
  if (!readyProjectIds.has(project.projectId)) continue;
2821
- const changed = /* @__PURE__ */ new Set();
2822
3602
  for (const branch of project.branches) {
2823
- if (!active.has(projectMountId(project.projectId, branch.branchName))) continue;
2824
3603
  const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2825
3604
  if (!hasProjectWorktree(branchPath)) continue;
2826
- const head = runGit(["rev-parse", "HEAD"], { cwd: branchPath });
3605
+ heads.set(projectMountId(project.projectId, branch.branchName), runGit(["rev-parse", "HEAD"], { cwd: branchPath }));
3606
+ }
3607
+ }
3608
+ return heads;
3609
+ };
3610
+ const pushChangedProjectHeads = (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
3611
+ const active = new Set(activeMountIds);
3612
+ for (const project of projectConfigById.values()) {
3613
+ if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
3614
+ const changed = /* @__PURE__ */ new Map();
3615
+ for (const branch of project.branches) {
3616
+ const mountId = projectMountId(project.projectId, branch.branchName);
3617
+ if (!active.has(mountId) || suppressedMountIds.has(mountId)) continue;
3618
+ const head = publicationHeads.get(mountId);
3619
+ if (!head) throw new Error(`Missing pre-projection head for active project mount ${mountId}`);
2827
3620
  if (lastObservedProjectHeads.get(projectBranchKey(project.projectId, branch.branchName)) !== head) {
2828
- changed.add(branch.branchName);
3621
+ changed.set(branch.branchName, head);
2829
3622
  }
2830
3623
  }
2831
3624
  if (changed.size === 0) continue;
@@ -2836,10 +3629,12 @@ async function startWorker(options) {
2836
3629
  mirrorUrl: connection.mirrorUrl,
2837
3630
  credentialHelper: connection.credentialHelper,
2838
3631
  credentialUsername: connection.mirrorCredentialUsername,
2839
- onlyBranches: changed
3632
+ onlyBranches: new Set(changed.keys()),
3633
+ publicationHeads: changed
2840
3634
  });
3635
+ (0, import_project_worktrees.assertProjectMirrorHeadsPushed)(results);
2841
3636
  for (const result of results) {
2842
- if (result.pushed) lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
3637
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
2843
3638
  }
2844
3639
  }
2845
3640
  const publishedTombstoneIds = [
@@ -2857,6 +3652,7 @@ async function startWorker(options) {
2857
3652
  }
2858
3653
  for (const [key, deletion] of pendingMirrorDeletes) {
2859
3654
  if (!active.has(deletion.id)) continue;
3655
+ if (!(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(projectConfigById.get(deletion.projectId))) continue;
2860
3656
  if (!deletion.projectDeleted) {
2861
3657
  (0, import_project_worktrees.deleteProjectMirrorBranch)({
2862
3658
  gitDirectory: deletion.gitDirectory,
@@ -2879,73 +3675,105 @@ async function startWorker(options) {
2879
3675
  lastObservedProjectHeads.delete(key);
2880
3676
  }
2881
3677
  };
2882
- const refreshProjectHeadsAfterInboundWorkspace = () => {
3678
+ const observeProjectHeadsBeforeOuterWorkspace = (input) => {
3679
+ const bundles = [];
2883
3680
  for (const project of projectConfigById.values()) {
2884
- if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
3681
+ if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2885
3682
  try {
2886
3683
  const connection = projectConnection(project);
2887
- const refreshed = (0, import_project_worktrees.fastForwardProjectHeadsFromMirror)({
2888
- projectRoot: configuredProjectRoot(projectsRoot, project),
2889
- primaryBranchName: primaryProjectBranch(project),
2890
- branchNames: project.branches.map(({ branchName }) => branchName),
2891
- mirrorUrl: connection.mirrorUrl,
2892
- credentialHelper: connection.credentialHelper,
2893
- credentialUsername: connection.mirrorCredentialUsername
3684
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
3685
+ const primaryBranchName = primaryProjectBranch(project);
3686
+ bundles.push({
3687
+ projectId: project.projectId,
3688
+ projectRoot,
3689
+ primaryBranchName,
3690
+ observations: (0, import_project_worktrees.observeProjectMirrorHeads)({
3691
+ projectRoot,
3692
+ primaryBranchName,
3693
+ branchNames: project.branches.map(({ branchName }) => branchName),
3694
+ mirrorUrl: connection.mirrorUrl,
3695
+ credentialHelper: connection.credentialHelper,
3696
+ credentialUsername: connection.mirrorCredentialUsername,
3697
+ allowNonFastForward: input.allowNonFastForward
3698
+ })
3699
+ });
3700
+ } catch (error) {
3701
+ if (input.failClosed) throw error;
3702
+ process.stderr.write(
3703
+ `[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
3704
+ `
3705
+ );
3706
+ }
3707
+ }
3708
+ return bundles;
3709
+ };
3710
+ const applyObservedProjectHeadsAfterInboundWorkspace = (bundles, activeMountIds, requireObservedHeads) => {
3711
+ const active = new Set(activeMountIds);
3712
+ for (const bundle of bundles) {
3713
+ const project = projectConfigById.get(bundle.projectId);
3714
+ if (!project || project.executionDisabled || !readyProjectIds.has(project.projectId)) continue;
3715
+ const observations = bundle.observations.filter(({ branchName }) => active.has(projectMountId(project.projectId, branchName)));
3716
+ if (observations.length === 0) continue;
3717
+ try {
3718
+ const refreshed = (0, import_project_worktrees.applyObservedProjectMirrorHeads)({
3719
+ projectRoot: bundle.projectRoot,
3720
+ primaryBranchName: bundle.primaryBranchName,
3721
+ observations,
3722
+ branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
2894
3723
  });
3724
+ if (requireObservedHeads) {
3725
+ for (const observation of observations) {
3726
+ if (!observation.shouldMove || !observation.mirrorHead) continue;
3727
+ const checkoutPath = configuredProjectBranchPath(projectsRoot, project, observation.branchName);
3728
+ const currentHead = runGit(["rev-parse", "HEAD"], { cwd: checkoutPath });
3729
+ if (currentHead !== observation.mirrorHead) {
3730
+ throw new Error(
3731
+ `Project ${project.projectId}/${observation.branchName} did not apply observed hidden mirror head ${observation.mirrorHead}`
3732
+ );
3733
+ }
3734
+ }
3735
+ }
2895
3736
  for (const state of refreshed) {
2896
3737
  lastObservedProjectHeads.set(
2897
3738
  projectBranchKey(project.projectId, state.branchName),
2898
- state.fastForwarded && state.mirrorHead ? state.mirrorHead : state.previousHead
3739
+ state.moved && state.mirrorHead ? state.mirrorHead : state.previousHead
2899
3740
  );
2900
3741
  }
2901
3742
  } catch (error) {
3743
+ if (requireObservedHeads) throw error;
2902
3744
  process.stderr.write(
2903
- `[r5d-worker] failed to refresh project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
3745
+ `[r5d-worker] failed to apply observed project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
2904
3746
  `
2905
3747
  );
2906
3748
  }
2907
3749
  }
2908
3750
  };
2909
- const reseedProjectHeadsAfterWorkspaceReset = () => {
2910
- for (const project of projectConfigById.values()) {
2911
- if (!readyProjectIds.has(project.projectId)) continue;
2912
- const connection = projectConnection(project);
2913
- const states = (0, import_project_worktrees.ensureProjectWorktrees)({
2914
- projectRoot: configuredProjectRoot(projectsRoot, project),
2915
- primaryBranchName: primaryProjectBranch(project),
2916
- branches: project.branches,
2917
- originUrl: connection.originUrl,
2918
- originCredentialUsername: connection.originCredentialUsername,
2919
- mirrorUrl: connection.mirrorUrl,
2920
- mirrorCredentialUsername: connection.mirrorCredentialUsername,
2921
- credentialHelper: connection.credentialHelper,
2922
- gitIdentity: workspaceGitIdentity
2923
- });
2924
- for (const state of states) {
2925
- lastObservedProjectHeads.set(projectBranchKey(project.projectId, state.branchName), state.head);
2926
- }
2927
- }
3751
+ const mapWorkspaceGitResult = (attemptId, trigger, result) => {
3752
+ const activeProjectBranchPublications = (0, import_workspace_publication_evidence.activeProjectBranchPublicationEvidence)([...projectConfigById.values()], result.activeMountIds);
3753
+ return {
3754
+ type: "workspace_sync",
3755
+ attemptId,
3756
+ workerLabel: label,
3757
+ trigger,
3758
+ outcome: result.outcome === "pushed" ? "published" : result.outcome,
3759
+ startingHead: result.startingHead,
3760
+ ...result.localHead ? { localHead: result.localHead } : {},
3761
+ ...result.publishedHead ? { publishedHead: result.publishedHead } : {},
3762
+ rebaseCount: result.rebaseCount,
3763
+ diffSizeBytes: result.diffSizeBytes,
3764
+ gitStatus: workspaceGitStatus(),
3765
+ affectedProjects: affectedProjects(result.affectedPaths),
3766
+ affectedPaths: result.affectedPaths,
3767
+ activeMountIds: result.activeMountIds,
3768
+ skippedMountIds: result.skippedMountIds,
3769
+ activeProjectBranchPublications,
3770
+ discardedPaths: [],
3771
+ localChangesDiscarded: false,
3772
+ ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
3773
+ ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
3774
+ ...result.error ? { error: result.error } : {}
3775
+ };
2928
3776
  };
2929
- const mapWorkspaceGitResult = (attemptId, trigger, result) => ({
2930
- type: "workspace_sync",
2931
- attemptId,
2932
- workerLabel: label,
2933
- trigger,
2934
- outcome: result.outcome === "pushed" ? "published" : result.outcome,
2935
- startingHead: result.startingHead,
2936
- ...result.localHead ? { localHead: result.localHead } : {},
2937
- ...result.publishedHead ? { publishedHead: result.publishedHead } : {},
2938
- rebaseCount: result.rebaseCount,
2939
- diffSizeBytes: result.diffSizeBytes,
2940
- gitStatus: workspaceGitStatus(),
2941
- affectedProjects: affectedProjects(result.affectedPaths),
2942
- affectedPaths: result.affectedPaths,
2943
- discardedPaths: [],
2944
- localChangesDiscarded: false,
2945
- ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
2946
- ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
2947
- ...result.error ? { error: result.error } : {}
2948
- });
2949
3777
  const failedWorkspaceSyncResult = (attemptId, trigger, error) => ({
2950
3778
  type: "workspace_sync",
2951
3779
  attemptId,
@@ -2969,6 +3797,16 @@ async function startWorker(options) {
2969
3797
  }
2970
3798
  const mounts = buildWorkspaceMounts();
2971
3799
  if (input.resetToCanonical) {
3800
+ const resetHasBusyLiveMount = mounts.some(
3801
+ (mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
3802
+ );
3803
+ if (resetHasBusyLiveMount) {
3804
+ throw new Error("Workspace reset cannot observe project mirror heads while a live mount is busy; remediation remains active");
3805
+ }
3806
+ const observedProjectHeads2 = observeProjectHeadsBeforeOuterWorkspace({
3807
+ allowNonFastForward: true,
3808
+ failClosed: true
3809
+ });
2972
3810
  const reset = (0, import_workspace_git_sync.resetWorkspaceGit)({
2973
3811
  workspacePath: workspaceShadowRoot,
2974
3812
  remoteUrl: workspaceRemoteUrl,
@@ -2977,7 +3815,7 @@ async function startWorker(options) {
2977
3815
  gitIdentity: workspaceGitIdentity,
2978
3816
  mounts
2979
3817
  });
2980
- reseedProjectHeadsAfterWorkspaceReset();
3818
+ applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
2981
3819
  return {
2982
3820
  type: "workspace_sync",
2983
3821
  attemptId: input.attemptId,
@@ -2992,11 +3830,23 @@ async function startWorker(options) {
2992
3830
  gitStatus: workspaceGitStatus(),
2993
3831
  affectedProjects: affectedProjects(reset.discardedPaths),
2994
3832
  affectedPaths: reset.discardedPaths,
3833
+ activeMountIds: reset.activeMountIds,
3834
+ skippedMountIds: reset.skippedMountIds,
2995
3835
  discardedPaths: reset.discardedPaths,
2996
3836
  localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
2997
3837
  };
2998
3838
  }
2999
3839
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
3840
+ const ordinaryCycleHasBusyLiveMount = mounts.some(
3841
+ (mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
3842
+ );
3843
+ const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
3844
+ const inboundMoveMountIds = new Set(
3845
+ observedProjectHeads.flatMap(
3846
+ ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
3847
+ )
3848
+ );
3849
+ const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : captureProjectHeadsForWorkspacePublication();
3000
3850
  const result = await (0, import_workspace_git_sync.synchronizeWorkspaceGit)({
3001
3851
  attemptId: input.attemptId,
3002
3852
  workerLabel: label,
@@ -3009,13 +3859,12 @@ async function startWorker(options) {
3009
3859
  commitDetail: input.confirmationReason ?? input.trigger.detail,
3010
3860
  allowLargeDiff: input.confirmedLargeDiff,
3011
3861
  skipMountMirror: outerRemediation,
3012
- skipMountHydration: outerRemediation,
3013
3862
  afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
3014
- pushChangedProjectHeads(activeMountIds, publishedHead);
3863
+ pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
3015
3864
  }
3016
3865
  });
3017
3866
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
3018
- refreshProjectHeadsAfterInboundWorkspace();
3867
+ applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
3019
3868
  }
3020
3869
  return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
3021
3870
  };
@@ -3036,6 +3885,12 @@ async function startWorker(options) {
3036
3885
  const runWorkspaceSync = async (input) => {
3037
3886
  const attemptId = input.attemptId ?? crypto.randomUUID();
3038
3887
  const requestedAt = Date.now();
3888
+ if (!input.requestId && input.sendResult !== false) {
3889
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
3890
+ throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
3891
+ }
3892
+ ws.send(JSON.stringify({ type: "workspace_sync_started", attemptId }));
3893
+ }
3039
3894
  let queueEnteredAt = requestedAt;
3040
3895
  let syncStartedAt = requestedAt;
3041
3896
  workspaceSyncRequestsInFlight += 1;
@@ -3053,6 +3908,28 @@ async function startWorker(options) {
3053
3908
  resetToCanonical: input.resetToCanonical
3054
3909
  });
3055
3910
  } catch (error) {
3911
+ let hydrationCurrent = input.resetToCanonical !== true;
3912
+ if (hydrationCurrent) {
3913
+ try {
3914
+ hydrationCurrent = (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts());
3915
+ } catch {
3916
+ hydrationCurrent = false;
3917
+ }
3918
+ }
3919
+ if (!hydrationCurrent) {
3920
+ process.stderr.write(
3921
+ `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
3922
+ `
3923
+ );
3924
+ }
3925
+ fenceUnsafeWorkspaceSyncFailure({
3926
+ hydrationCurrent,
3927
+ invalidateExecution: () => {
3928
+ workspaceConfigured = false;
3929
+ advanceWorkerAdmissionGeneration();
3930
+ },
3931
+ exitCode: 1
3932
+ });
3056
3933
  return failedWorkspaceSyncResult(attemptId, input.trigger, error);
3057
3934
  }
3058
3935
  });
@@ -3074,7 +3951,13 @@ async function startWorker(options) {
3074
3951
  };
3075
3952
  const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
3076
3953
  pendingAutomaticTrigger = trigger;
3077
- if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight) {
3954
+ const pendingBranchDeferral = () => (0, import_workspace_automatic_sync_policy.pendingCreatedBranchAutomaticSyncDeferral)(
3955
+ projectWorkspaceState.locallyPendingCreatedBranches,
3956
+ activeWorkspaceMutationTargets(),
3957
+ pendingCreatedBranchPublicationNotBefore
3958
+ );
3959
+ const initialDeferral = pendingBranchDeferral();
3960
+ if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
3078
3961
  return;
3079
3962
  }
3080
3963
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -3082,13 +3965,25 @@ async function startWorker(options) {
3082
3965
  () => {
3083
3966
  workspaceAutomaticTimer = void 0;
3084
3967
  const scheduledTrigger = pendingAutomaticTrigger;
3085
- pendingAutomaticTrigger = void 0;
3086
3968
  if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
3969
+ const currentDeferral = pendingBranchDeferral();
3970
+ if (currentDeferral?.kind === "active_target") return;
3971
+ if (currentDeferral?.kind === "creation_grace") {
3972
+ scheduleAutomaticWorkspaceSync(scheduledTrigger, currentDeferral.retryAfterMs);
3973
+ return;
3974
+ }
3975
+ pendingAutomaticTrigger = void 0;
3087
3976
  automaticSyncInFlight = true;
3088
3977
  void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
3089
3978
  if (result.outcome === "failed") {
3090
3979
  pendingAutomaticTrigger = scheduledTrigger;
3091
3980
  }
3981
+ }).catch((error) => {
3982
+ pendingAutomaticTrigger = scheduledTrigger;
3983
+ process.stderr.write(
3984
+ `[r5d-worker] failed to start automatic workspace sync: ${error instanceof Error ? error.message : String(error)}
3985
+ `
3986
+ );
3092
3987
  }).finally(() => {
3093
3988
  automaticSyncInFlight = false;
3094
3989
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
@@ -3097,7 +3992,7 @@ async function startWorker(options) {
3097
3992
  }
3098
3993
  });
3099
3994
  },
3100
- Math.max(0, delayMs)
3995
+ Math.max(0, delayMs, initialDeferral?.kind === "creation_grace" ? initialDeferral.retryAfterMs : 0)
3101
3996
  );
3102
3997
  workspaceAutomaticTimer.unref();
3103
3998
  };
@@ -3105,32 +4000,190 @@ async function startWorker(options) {
3105
4000
  scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
3106
4001
  };
3107
4002
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
3108
- const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
3109
- target,
3110
- projectsRoot,
3111
- workspaceShadowRoot,
3112
- projectConfigById
3113
- });
4003
+ const resolveMessageTarget = (target) => {
4004
+ if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
4005
+ if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
4006
+ throw new Error(`Project ${target.projectId} is not ready on this worker`);
4007
+ }
4008
+ if (target.type === "workspace" && target.rootProfile === "visible_projects" && [...projectConfigById.keys()].some((projectId) => !readyProjectIds.has(projectId))) {
4009
+ throw new Error("Visible project execution is unavailable while project configuration is incomplete");
4010
+ }
4011
+ return resolveWorkerSessionTarget({
4012
+ target,
4013
+ projectsRoot,
4014
+ workspaceShadowRoot,
4015
+ projectConfigById
4016
+ });
4017
+ };
3114
4018
  const configureWorkerWorkspace = async (message) => {
4019
+ const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
4020
+ ...message,
4021
+ workerBaseUrl: baseUrl,
4022
+ workerAuthHeader: bearerAuthHeader
4023
+ });
4024
+ const credentialTransitionPhase = credentialGenerationTransitionPhase({
4025
+ committedFingerprint: configuredCredentialGenerationFingerprint,
4026
+ pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4027
+ incomingFingerprint: incomingCredentialGenerationFingerprint
4028
+ });
4029
+ const credentialReapStatus = credentialReapContractStatus();
4030
+ const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
4031
+ const credentialRestartIsContained = credentialGenerationRestartIsContained({
4032
+ systemdContract: verifiedCredentialReapContractNow,
4033
+ pendingBoundary: pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null,
4034
+ currentSystemdInvocationId: systemdInvocationIdentifierAtProcessStart,
4035
+ currentBootSessionId: bootSessionIdentifierAtProcessStart,
4036
+ currentMachineId: machineIdentifierAtProcessStart
4037
+ });
4038
+ let trackedChildTerminationProven = true;
4039
+ if (credentialTransitionPhase !== "current") {
4040
+ workspaceConfigured = false;
4041
+ githubCredential = null;
4042
+ advanceWorkerAdmissionGeneration();
4043
+ try {
4044
+ await terminateActiveCredentialBearingChildren();
4045
+ } catch (error) {
4046
+ trackedChildTerminationProven = false;
4047
+ process.stderr.write(
4048
+ `[r5d-worker] tracked credential-bearing child termination was incomplete at the credential transition boundary: ${error instanceof Error ? error.message : String(error)}
4049
+ `
4050
+ );
4051
+ }
4052
+ }
4053
+ const initialBootstrapAuthorized = initialCredentialBootstrapIsSafe({
4054
+ transitionPhase: credentialTransitionPhase,
4055
+ committedFingerprint: configuredCredentialGenerationFingerprint,
4056
+ pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4057
+ bootstrapFingerprintAtProcessStart: bootstrapCredentialGenerationFingerprintAtProcessStart,
4058
+ incomingFingerprint: incomingCredentialGenerationFingerprint,
4059
+ credentialArtifactsProvenAbsentAtProcessStart: initialCredentialBootstrapProvenEmpty,
4060
+ trackedChildTerminationProven,
4061
+ activeProcessCount: activeProcesses.size,
4062
+ credentialProcessGroupCount: credentialBearingProcessGroups.size,
4063
+ activePtyCount: activePtys.size
4064
+ });
4065
+ const staleIntentCleanupAuthorized = credentialTransitionPhase === "current" && (pendingCredentialGenerationIntentAtProcessStart !== null || bootstrapCredentialGenerationFingerprintAtProcessStart !== null);
4066
+ const credentialPublicationPreauthorized = credentialTransitionPhase === "publish_from_clean_restart" && credentialRestartIsContained || initialBootstrapAuthorized || staleIntentCleanupAuthorized;
4067
+ if (initialBootstrapAuthorized && bootstrapCredentialGenerationFingerprintAtProcessStart === null) {
4068
+ try {
4069
+ const bootstrap = (0, import_registry_auth.preparePrivateAuthFileGeneration)([
4070
+ { filePath: bootstrapCredentialGenerationPath(), content: `${incomingCredentialGenerationFingerprint}
4071
+ ` }
4072
+ ]);
4073
+ bootstrap.commit();
4074
+ bootstrapCredentialGenerationFingerprintAtProcessStart = incomingCredentialGenerationFingerprint;
4075
+ } catch (error) {
4076
+ const wrapped = error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
4077
+ return {
4078
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, wrapped),
4079
+ pending: [],
4080
+ aheadOfOriginBranches: []
4081
+ };
4082
+ }
4083
+ }
4084
+ if (credentialTransitionPhase === "stage_pending_restart" && !initialBootstrapAuthorized) {
4085
+ if (credentialReapStatus === "systemd_unverified") {
4086
+ process.stderr.write(
4087
+ "[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"
4088
+ );
4089
+ requireCredentialGenerationRestart(true, void 0, 1);
4090
+ }
4091
+ const boundary = pendingCredentialGenerationBoundary({
4092
+ systemdContract: verifiedCredentialReapContractNow,
4093
+ systemdInvocationId: systemdInvocationIdentifierAtProcessStart,
4094
+ bootSessionId: bootSessionIdentifierAtProcessStart,
4095
+ machineId: machineIdentifierAtProcessStart
4096
+ });
4097
+ if (!credentialGenerationBoundaryIsComplete(boundary)) {
4098
+ process.stderr.write(
4099
+ 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"
4100
+ );
4101
+ requireCredentialGenerationRestart(true, void 0, 1);
4102
+ }
4103
+ try {
4104
+ const pending = (0, import_registry_auth.preparePrivateAuthFileGeneration)([
4105
+ {
4106
+ filePath: pendingCredentialGenerationPath(),
4107
+ content: pendingCredentialGenerationIntentContent({
4108
+ fingerprint: incomingCredentialGenerationFingerprint,
4109
+ boundary
4110
+ })
4111
+ }
4112
+ ]);
4113
+ pending.commit();
4114
+ } catch (error) {
4115
+ process.stderr.write(
4116
+ `[r5d-worker] could not stage pending credential generation before fatal restart: ${error instanceof Error ? error.message : String(error)}
4117
+ `
4118
+ );
4119
+ requireCredentialGenerationRestart(true, void 0, 1);
4120
+ }
4121
+ process.stderr.write(
4122
+ 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"
4123
+ );
4124
+ requireCredentialGenerationRestart(true, void 0, import_supervisor.WORKER_CREDENTIAL_RESTART_EXIT_CODE);
4125
+ }
4126
+ if (credentialTransitionPhase !== "current" && !trackedChildTerminationProven) {
4127
+ requireCredentialGenerationRestart(true, void 0, 1);
4128
+ }
4129
+ if (credentialTransitionPhase === "publish_from_clean_restart" && !credentialRestartIsContained) {
4130
+ const pendingBoundary = pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null;
4131
+ 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";
4132
+ process.stderr.write(
4133
+ `[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."}
4134
+ `
4135
+ );
4136
+ requireCredentialGenerationRestart(true, void 0, import_supervisor.WORKER_CREDENTIAL_RESTART_EXIT_CODE);
4137
+ }
4138
+ if (credentialPublicationPreauthorized) workspaceConfigured = false;
3115
4139
  workspaceSyncRequestsInFlight += 1;
3116
4140
  try {
3117
4141
  return await workspaceSyncSingleFlight.runExclusive(async () => {
4142
+ for (const project of message.projects) (0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
4143
+ const busyConfigurationChanges = (0, import_workspace_project_config_policy.busyProjectConfigurationChangeIds)({
4144
+ currentProjects: [...projectConfigById.values()],
4145
+ incomingProjects: message.projects,
4146
+ activeTargets: activeWorkspaceMutationTargets(),
4147
+ currentFingerprint: (project) => (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: workspaceGitIdentity }),
4148
+ incomingFingerprint: (project) => (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: message.gitIdentity }),
4149
+ currentProjectReady: (project) => {
4150
+ const fingerprint = (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: workspaceGitIdentity });
4151
+ return readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === fingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
4152
+ }
4153
+ });
4154
+ if (busyConfigurationChanges.length > 0) {
4155
+ throw new ProjectWorkspaceConfigurationDeferredError(
4156
+ `Project workspace configuration deferred while visible checkout activity is active: ${busyConfigurationChanges.join(", ")}`
4157
+ );
4158
+ }
3118
4159
  workspaceConfigured = false;
4160
+ const preserveOnlyBranches = message.projects.flatMap(
4161
+ (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4162
+ );
3119
4163
  projectWorkspaceState = projectWorkspaceStateStore.reconcile({
3120
- desiredProjects: message.projects
4164
+ desiredProjects: message.projects,
4165
+ preserveOnlyBranches
3121
4166
  });
3122
- const requiredCredentialStorePaths = /* @__PURE__ */ new Set();
4167
+ const stillPendingCreatedBranches = new Map(
4168
+ projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4169
+ (0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(branch.projectId, branch.branchName),
4170
+ branch
4171
+ ])
4172
+ );
4173
+ for (const key of pendingCreatedBranchPublicationNotBefore.keys()) {
4174
+ if (!stillPendingCreatedBranches.has(key)) pendingCreatedBranchPublicationNotBefore.delete(key);
4175
+ }
4176
+ const desiredCredentialStoreFiles = /* @__PURE__ */ new Map();
3123
4177
  const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
3124
- requiredCredentialStorePaths.add(workspaceStorePath);
3125
4178
  const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
3126
4179
  [{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
3127
- workspaceStorePath
4180
+ workspaceStorePath,
4181
+ desiredCredentialStoreFiles
3128
4182
  );
3129
4183
  if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
3130
4184
  const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
3131
4185
  for (const project of message.projects) {
3132
- requiredCredentialStorePaths.add(credentialStorePathForProject(project.projectId));
3133
- projectConnection(project);
4186
+ projectConnection(project, desiredCredentialStoreFiles);
3134
4187
  }
3135
4188
  const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
3136
4189
  ...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
@@ -3139,30 +4192,81 @@ async function startWorker(options) {
3139
4192
  ]);
3140
4193
  for (const projectId of pendingCredentialProjectIds) {
3141
4194
  const desiredProject = desiredProjectConfigById.get(projectId);
3142
- if (desiredProject) projectConnection(desiredProject);
3143
- else projectMirrorConnection(projectId);
3144
- requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
4195
+ if (desiredProject) projectConnection(desiredProject, desiredCredentialStoreFiles);
4196
+ else projectMirrorConnection(projectId, desiredCredentialStoreFiles);
3145
4197
  }
3146
4198
  for (const deletion of pendingMirrorDeletes.values()) {
3147
4199
  const desiredProject = desiredProjectConfigById.get(deletion.projectId);
3148
- const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
4200
+ const connection = desiredProject ? projectConnection(desiredProject, desiredCredentialStoreFiles) : projectMirrorConnection(deletion.projectId, desiredCredentialStoreFiles);
3149
4201
  deletion.mirrorUrl = connection.mirrorUrl;
3150
4202
  deletion.credentialHelper = connection.credentialHelper;
3151
4203
  deletion.credentialUsername = connection.mirrorCredentialUsername;
3152
4204
  }
3153
- pruneCredentialStoreFiles(requiredCredentialStorePaths);
3154
- configureGitHubAuth(message.githubCredential);
4205
+ let preparedAuthGeneration;
4206
+ try {
4207
+ preparedAuthGeneration = (0, import_registry_auth.preparePrivateAuthFileGeneration)([
4208
+ ...planCredentialStoreGeneration(desiredCredentialStoreFiles),
4209
+ ...(0, import_registry_auth.planGitHubRegistryAuthFiles)([dockerConfigPath(), containersAuthPath()], message.githubCredential),
4210
+ { filePath: legacySharedCredentialStorePath(), content: null, allowSymlinkRemoval: true },
4211
+ { filePath: credentialGenerationReceiptPath(), content: `${incomingCredentialGenerationFingerprint}
4212
+ ` },
4213
+ { filePath: pendingCredentialGenerationPath(), content: null },
4214
+ { filePath: bootstrapCredentialGenerationPath(), content: null }
4215
+ ]);
4216
+ } catch (error) {
4217
+ throw error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
4218
+ }
4219
+ await commitCredentialGeneration(
4220
+ preparedAuthGeneration,
4221
+ message.githubCredential,
4222
+ terminateActiveCredentialBearingChildren,
4223
+ void 0,
4224
+ credentialPublicationPreauthorized
4225
+ );
4226
+ configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
4227
+ pendingCredentialGenerationIntentAtProcessStart = null;
4228
+ bootstrapCredentialGenerationFingerprintAtProcessStart = null;
3155
4229
  visibleGitIdentity = message.gitIdentity;
3156
4230
  workspaceGitIdentity = message.gitIdentity;
3157
4231
  workspaceRemoteUrl = message.workspaceRemoteUrl;
3158
4232
  workspaceCredentialHelper = nextWorkspaceCredentialHelper;
3159
4233
  workspaceCredentialUsername = workerCredentialUsername;
3160
- (0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, message.projects);
4234
+ (0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, projectWorkspaceState.desiredProjects);
3161
4235
  const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
3162
4236
  ...project,
3163
4237
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
3164
4238
  }));
3165
4239
  const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
4240
+ (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
4241
+ workspacePath: workspaceShadowRoot,
4242
+ remoteUrl: message.workspaceRemoteUrl,
4243
+ credentialHelper: nextWorkspaceCredentialHelper,
4244
+ credentialUsername: workerCredentialUsername,
4245
+ gitIdentity: message.gitIdentity
4246
+ });
4247
+ const recoverySourceOverrides = /* @__PURE__ */ new Map();
4248
+ for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
4249
+ if (deletion.kind !== "project" || deletion.projectDeleted) continue;
4250
+ for (const branchName of deletion.branchNames) {
4251
+ const oldBranchPath = import_node_path.default.join(deletion.managedPath, ...branchName.split("/"));
4252
+ if (import_node_fs.default.existsSync(oldBranchPath)) recoverySourceOverrides.set(projectMountId(deletion.projectId, branchName), oldBranchPath);
4253
+ }
4254
+ }
4255
+ const recoveryMounts = buildWorkspaceMounts(effectiveProjects).map((mount) => ({
4256
+ ...mount,
4257
+ sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
4258
+ busy: mount.busyForRecovery ?? mount.busy
4259
+ }));
4260
+ (0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts);
4261
+ for (const project of message.projects) {
4262
+ for (const { branchName } of project.preserveOnlyBranches) {
4263
+ if (stillPendingCreatedBranches.has((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(project.projectId, branchName))) {
4264
+ continue;
4265
+ }
4266
+ deactivatePreserveOnlyProjectBranch(project, branchName);
4267
+ }
4268
+ }
4269
+ const preserveOnlyKeys = new Set(preserveOnlyBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName)));
3166
4270
  for (const existingProject of projectConfigById.values()) {
3167
4271
  const nextProject = nextProjectConfigById.get(existingProject.projectId);
3168
4272
  if (!nextProject) {
@@ -3172,6 +4276,7 @@ async function startWorker(options) {
3172
4276
  const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
3173
4277
  for (const existingBranch of existingProject.branches) {
3174
4278
  if (!nextBranches.has(existingBranch.branchName)) {
4279
+ if (preserveOnlyKeys.has(projectBranchKey(existingProject.projectId, existingBranch.branchName))) continue;
3175
4280
  stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
3176
4281
  }
3177
4282
  }
@@ -3187,40 +4292,30 @@ async function startWorker(options) {
3187
4292
  for (const projectId of [...readyProjectIds]) {
3188
4293
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
3189
4294
  }
3190
- (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
3191
- workspacePath: workspaceShadowRoot,
3192
- remoteUrl: message.workspaceRemoteUrl,
3193
- credentialHelper: nextWorkspaceCredentialHelper,
3194
- credentialUsername: workerCredentialUsername,
3195
- gitIdentity: message.gitIdentity
3196
- });
3197
- pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
3198
4295
  stageDurableWorkspaceDeletions();
3199
4296
  const mountsBeforeGit = buildWorkspaceMounts();
3200
4297
  (0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
3201
4298
  workspaceShadowRoot,
3202
- mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath))
4299
+ mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath) && mount.busy?.() !== true),
4300
+ // These mounts are intentionally hydrated before their project Git
4301
+ // layouts can be marked ready. No process can resolve an unready
4302
+ // target, and preserving the fetched outer tree here prevents clean
4303
+ // checkout initialization from erasing synchronized dirt.
4304
+ { ignoreBusy: true }
3203
4305
  );
3204
4306
  ensureConfiguredProjects();
3205
- let result = await performWorkspaceSync({
4307
+ const result = await performWorkspaceSync({
3206
4308
  attemptId: crypto.randomUUID(),
3207
4309
  trigger: { type: "connect" }
3208
4310
  });
3209
- if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
3210
- const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
3211
- if (staleAfterInbound.length > 0) {
3212
- pruneStaleOuterWorkspaceEntries(staleAfterInbound);
3213
- stageDurableWorkspaceDeletions();
3214
- result = await performWorkspaceSync({
3215
- attemptId: crypto.randomUUID(),
3216
- trigger: { type: "connect" }
3217
- });
3218
- }
3219
- }
3220
4311
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
3221
4312
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4313
+ const activeMountIds = new Set(result.activeMountIds ?? []);
3222
4314
  const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
3223
- (deletion) => deletion.kind === "project" && !deletion.projectDeleted ? [deletion.tombstoneId] : []
4315
+ (deletion) => deletion.kind === "project" && !deletion.projectDeleted && (0, import_workspace_path_move.checkoutPathMovePublicationComplete)(
4316
+ deletion.branchNames.map((branchName) => projectMountId(deletion.projectId, branchName)),
4317
+ activeMountIds
4318
+ ) ? [deletion.tombstoneId] : []
3224
4319
  );
3225
4320
  if (layoutCleanupIds.length > 0) {
3226
4321
  projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
@@ -3236,18 +4331,37 @@ async function startWorker(options) {
3236
4331
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
3237
4332
  });
3238
4333
  } catch (error) {
3239
- workspaceConfigured = import_node_fs.default.existsSync(import_node_path.default.join(workspaceShadowRoot, ".git"));
3240
- for (const project of message.projects) {
3241
- for (const branch of project.branches) {
3242
- pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
3243
- projectId: project.projectId,
3244
- branchName: branch.branchName
3245
- });
4334
+ if (error instanceof import_registry_auth.RegistryAuthConfigurationError) {
4335
+ workspaceConfigured = false;
4336
+ githubCredential = null;
4337
+ configuredCredentialGenerationFingerprint = null;
4338
+ let childTerminationProven = true;
4339
+ try {
4340
+ await terminateActiveCredentialBearingChildren();
4341
+ } catch (childError) {
4342
+ childTerminationProven = false;
4343
+ process.stderr.write(
4344
+ `[r5d-worker] failed to fully terminate credential-bearing children: ${childError instanceof Error ? childError.message : String(childError)}
4345
+ `
4346
+ );
3246
4347
  }
4348
+ process.stderr.write(
4349
+ `[r5d-worker] fatal credential-generation transition failure; exiting for whole-runtime cleanup: ${error instanceof Error ? error.message : String(error)}
4350
+ `
4351
+ );
4352
+ requireCredentialGenerationRestart(true, void 0, 1);
4353
+ }
4354
+ workspaceConfigured = false;
4355
+ const deferred = error instanceof ProjectWorkspaceConfigurationDeferredError || error instanceof import_project_workspace_state.ProjectWorkspacePendingBranchPathChangeError;
4356
+ if (deferred) pendingCheckouts.clear();
4357
+ for (const pending of (0, import_workspace_project_config_policy.deferredProjectConfigurationPendingBranches)(message.projects)) {
4358
+ pendingCheckouts.set(projectBranchKey(pending.projectId, pending.branchName), pending);
3247
4359
  }
3248
4360
  return {
3249
4361
  result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
3250
- pending: [...pendingCheckouts.values()],
4362
+ pending: [...pendingCheckouts.values()].sort(
4363
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
4364
+ ),
3251
4365
  aheadOfOriginBranches: []
3252
4366
  };
3253
4367
  } finally {
@@ -3342,12 +4456,22 @@ async function startWorker(options) {
3342
4456
  ws.addEventListener("message", (event) => {
3343
4457
  void (async () => {
3344
4458
  const message = JSON.parse(String(event.data));
4459
+ const messageAdmissionGeneration = workerAdmissionGeneration;
4460
+ const assertMessageAdmission = () => assertWorkerChildAdmission({
4461
+ capturedGeneration: messageAdmissionGeneration,
4462
+ currentGeneration: workerAdmissionGeneration,
4463
+ sourceSocketIsCurrent: currentWorkerSocket === ws,
4464
+ sourceSocketOpen: ws.readyState === WebSocket.OPEN,
4465
+ workspaceConfigured,
4466
+ runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
4467
+ });
3345
4468
  if (message.type === "connected") {
3346
4469
  return;
3347
4470
  }
3348
4471
  if (message.type === "workspace_config") {
4472
+ advanceWorkerAdmissionGeneration();
3349
4473
  const configured = await configureWorkerWorkspace(message);
3350
- sendWorkerMessage(ws, {
4474
+ sendWorkerMessageFromCurrentSource(ws, {
3351
4475
  type: "workspace_configured",
3352
4476
  requestId: message.requestId,
3353
4477
  result: configured.result,
@@ -3382,59 +4506,85 @@ async function startWorker(options) {
3382
4506
  }
3383
4507
  if (message.type === "create_project_branch") {
3384
4508
  try {
3385
- projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch({
4509
+ const pendingBranch = {
4510
+ branchId: message.branchId,
3386
4511
  projectId: message.projectId,
3387
4512
  branchName: message.targetBranch
3388
- });
4513
+ };
3389
4514
  const created = await workspaceSyncSingleFlight.runMutation(() => {
3390
4515
  const project = projectConfigById.get(message.projectId);
3391
4516
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3392
- if (!readyProjectIds.has(project.projectId)) {
3393
- throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
4517
+ (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
4518
+ if (!(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) {
4519
+ throw new Error(`Project ${message.projectId} branch creation is disabled while its repository connection is transitioning`);
3394
4520
  }
3395
- if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
3396
- throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
3397
- }
3398
- if (project.branches.some(({ branchName }) => branchName === message.targetBranch)) {
3399
- throw new Error(`Project branch ${message.targetBranch} already exists`);
3400
- }
3401
- const createdBranch = (0, import_project_worktrees.createLinkedProjectBranch)({
3402
- projectRoot: configuredProjectRoot(projectsRoot, project),
3403
- sourceBranchName: message.sourceBranch,
3404
- branchName: message.targetBranch
3405
- });
3406
- projectConfigById.set(project.projectId, {
3407
- ...project,
3408
- branches: [
3409
- ...project.branches,
3410
- {
3411
- branchName: message.targetBranch,
3412
- sourceBranchName: message.sourceBranch,
3413
- baseCommitHash: createdBranch.baseCommitHash
4521
+ const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
4522
+ ({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
4523
+ );
4524
+ projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
4525
+ let gitBranchCreated = false;
4526
+ try {
4527
+ if (!readyProjectIds.has(project.projectId)) {
4528
+ throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
4529
+ }
4530
+ if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
4531
+ throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
4532
+ }
4533
+ const existingBranch = project.branches.find(({ branchName }) => branchName === message.targetBranch);
4534
+ if (existingBranch) {
4535
+ if (!pendingWasRecorded) throw new Error(`Project branch ${message.targetBranch} already exists`);
4536
+ if (existingBranch.branchId !== message.branchId) {
4537
+ throw new Error(`Pending project branch ${message.targetBranch} belongs to another durable branch incarnation`);
3414
4538
  }
3415
- ]
3416
- });
3417
- lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
3418
- return createdBranch;
4539
+ const existingPath = configuredProjectBranchPath(projectsRoot, project, message.targetBranch);
4540
+ if (!hasProjectWorktree(existingPath)) {
4541
+ throw new Error(`Pending project branch ${message.targetBranch} is not initialized on this worker`);
4542
+ }
4543
+ return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
4544
+ }
4545
+ const createdBranch = (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
4546
+ projectRoot: configuredProjectRoot(projectsRoot, project),
4547
+ primaryBranchName: primaryProjectBranch(project),
4548
+ sourceBranchName: message.sourceBranch,
4549
+ branchName: message.targetBranch,
4550
+ pendingRetry: pendingWasRecorded
4551
+ });
4552
+ gitBranchCreated = true;
4553
+ projectConfigById.set(project.projectId, {
4554
+ ...project,
4555
+ branches: [
4556
+ ...project.branches,
4557
+ {
4558
+ branchId: message.branchId,
4559
+ branchName: message.targetBranch,
4560
+ sourceBranchName: message.sourceBranch,
4561
+ baseCommitHash: createdBranch.baseCommitHash
4562
+ }
4563
+ ]
4564
+ });
4565
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
4566
+ return createdBranch;
4567
+ } catch (error) {
4568
+ if (!pendingWasRecorded && !gitBranchCreated && !(0, import_project_worktrees.projectBranchMayExistAfterCreateFailure)(error)) {
4569
+ projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
4570
+ }
4571
+ throw error;
4572
+ }
3419
4573
  });
4574
+ pendingCreatedBranchPublicationNotBefore.set(
4575
+ (0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.targetBranch),
4576
+ Date.now() + import_workspace_automatic_sync_policy.PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS
4577
+ );
3420
4578
  sendWorkerMessage(ws, {
3421
4579
  type: "operation_result",
3422
4580
  requestId: message.requestId,
3423
4581
  result: {
3424
4582
  type: "create_project_branch",
4583
+ branchId: message.branchId,
3425
4584
  branchName: message.targetBranch,
3426
4585
  baseCommitHash: created.baseCommitHash
3427
4586
  }
3428
4587
  });
3429
- markWorkspaceDirty(
3430
- {
3431
- type: "manual",
3432
- projectId: message.projectId,
3433
- branchName: message.targetBranch,
3434
- detail: `created project branch from ${message.sourceBranch}`
3435
- },
3436
- true
3437
- );
3438
4588
  } catch (error) {
3439
4589
  sendWorkerMessage(ws, {
3440
4590
  type: "operation_result",
@@ -3447,42 +4597,66 @@ async function startWorker(options) {
3447
4597
  if (message.type === "delete_project_branch") {
3448
4598
  try {
3449
4599
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
3450
- projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
3451
- projectId: message.projectId,
3452
- branchName: message.branchName
3453
- });
3454
- await workspaceSyncSingleFlight.runMutation(() => {
4600
+ const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
3455
4601
  const project = projectConfigById.get(message.projectId);
3456
4602
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3457
- if (pendingMirrorDeletes.has(deletionKey)) return;
4603
+ (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
4604
+ if (!(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) {
4605
+ throw new Error(`Project ${message.projectId} branch deletion is disabled while its repository connection is transitioning`);
4606
+ }
4607
+ const configuredBranch = project.branches.find(({ branchName }) => branchName === message.branchName);
4608
+ const pendingLocalBranch = projectWorkspaceState.locallyPendingCreatedBranches.find(
4609
+ ({ projectId, branchName }) => projectId === message.projectId && branchName === message.branchName
4610
+ );
4611
+ (0, import_workspace_branch_incarnation_policy.assertProjectBranchDeletionIncarnation)({
4612
+ requiredBranchId: message.branchId,
4613
+ configuredBranch,
4614
+ pendingLocalBranch
4615
+ });
4616
+ const hasDurableTombstone = projectWorkspaceState.tombstones.some(
4617
+ (tombstone) => tombstone.kind === "branch" && tombstone.projectId === message.projectId && tombstone.branchName === message.branchName
4618
+ );
4619
+ if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
4620
+ return false;
4621
+ }
4622
+ projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
4623
+ projectId: message.projectId,
4624
+ branchName: message.branchName
4625
+ });
4626
+ pendingCreatedBranchPublicationNotBefore.delete((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.branchName));
4627
+ if (pendingMirrorDeletes.has(deletionKey)) return true;
3458
4628
  stageProjectBranchDeletion(project, message.branchName, true);
3459
4629
  projectConfigById.set(project.projectId, {
3460
4630
  ...project,
3461
4631
  branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
3462
4632
  });
4633
+ return true;
3463
4634
  });
3464
- const result = await runWorkspaceSync({
3465
- trigger: {
3466
- type: "manual",
3467
- projectId: message.projectId,
3468
- branchName: message.branchName,
3469
- detail: "deleted project branch"
3470
- },
3471
- confirmedLargeDiff: true,
3472
- confirmationReason: "explicit project branch deletion",
3473
- sendResult: false
3474
- });
3475
- if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
3476
- throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
3477
- }
3478
- if (pendingMirrorDeletes.has(deletionKey)) {
3479
- throw new Error("Project branch deletion did not remove the hidden mirror ref");
4635
+ if (deletionNeedsSync) {
4636
+ const result = await runWorkspaceSync({
4637
+ trigger: {
4638
+ type: "manual",
4639
+ projectId: message.projectId,
4640
+ branchName: message.branchName,
4641
+ detail: "deleted project branch"
4642
+ },
4643
+ confirmedLargeDiff: true,
4644
+ confirmationReason: "explicit project branch deletion",
4645
+ sendResult: false
4646
+ });
4647
+ if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
4648
+ throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
4649
+ }
4650
+ if (pendingMirrorDeletes.has(deletionKey)) {
4651
+ throw new Error("Project branch deletion did not remove the hidden mirror ref");
4652
+ }
3480
4653
  }
3481
4654
  sendWorkerMessage(ws, {
3482
4655
  type: "operation_result",
3483
4656
  requestId: message.requestId,
3484
4657
  result: {
3485
4658
  type: "delete_project_branch",
4659
+ branchId: message.branchId,
3486
4660
  branchName: message.branchName
3487
4661
  }
3488
4662
  });
@@ -3670,9 +4844,13 @@ async function startWorker(options) {
3670
4844
  });
3671
4845
  return;
3672
4846
  }
3673
- const releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
4847
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
4848
+ workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
4849
+ });
4850
+ let releaseWorkspaceMutation;
3674
4851
  let mutationLeaseTransferred = false;
3675
4852
  try {
4853
+ releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
3676
4854
  const resolvedTarget = resolveMessageTarget(message.target);
3677
4855
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
3678
4856
  `);
@@ -3681,6 +4859,7 @@ async function startWorker(options) {
3681
4859
  message,
3682
4860
  resolvedTarget,
3683
4861
  planRoot,
4862
+ assertAdmission: assertMessageAdmission,
3684
4863
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
3685
4864
  ...targetMayMutateVisibleWorkspace(message.target) ? {
3686
4865
  onTerminal: () => {
@@ -3690,13 +4869,14 @@ async function startWorker(options) {
3690
4869
  });
3691
4870
  mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
3692
4871
  } catch (error) {
3693
- sendWorkerMessage(ws, {
4872
+ sendWorkerMessageFromCurrentSource(ws, {
3694
4873
  type: "pty_error",
3695
4874
  requestId: message.requestId,
3696
4875
  ptyId: message.ptyId,
3697
4876
  error: error instanceof Error ? error.message : String(error)
3698
4877
  });
3699
4878
  } finally {
4879
+ workspaceSyncPriorityPtyTargets.delete(message.ptyId);
3700
4880
  if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
3701
4881
  }
3702
4882
  return;
@@ -3733,15 +4913,29 @@ async function startWorker(options) {
3733
4913
  return;
3734
4914
  }
3735
4915
  let result;
4916
+ let targetReserved = false;
3736
4917
  try {
4918
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
4919
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
4920
+ targetReserved = true;
4921
+ });
3737
4922
  const runCommand = async () => {
3738
4923
  const resolvedTarget = resolveMessageTarget(message.target);
3739
4924
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
3740
4925
  `);
3741
- return await executeCommand({ message, resolvedTarget, baseUrl, token, artifactRoot, planRoot });
4926
+ return await executeCommand({
4927
+ message,
4928
+ resolvedTarget,
4929
+ baseUrl,
4930
+ token,
4931
+ artifactRoot,
4932
+ planRoot,
4933
+ assertAdmission: assertMessageAdmission
4934
+ });
3742
4935
  };
3743
4936
  result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
3744
4937
  } catch (error) {
4938
+ if (error instanceof StaleWorkerAdmissionError) return;
3745
4939
  result = {
3746
4940
  type: "exec_result",
3747
4941
  requestId: message.requestId,
@@ -3750,6 +4944,8 @@ async function startWorker(options) {
3750
4944
  exitCode: 1,
3751
4945
  error: error instanceof Error ? error.message : String(error)
3752
4946
  };
4947
+ } finally {
4948
+ if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
3753
4949
  }
3754
4950
  ws.send(JSON.stringify(result));
3755
4951
  if (targetMayMutateVisibleWorkspace(message.target)) {
@@ -3787,7 +4983,8 @@ async function startWorker(options) {
3787
4983
  baseUrl,
3788
4984
  token,
3789
4985
  artifactRoot,
3790
- planRoot
4986
+ planRoot,
4987
+ assertAdmission: assertMessageAdmission
3791
4988
  });
3792
4989
  } finally {
3793
4990
  if (targetMayMutateVisibleWorkspace(message.target)) {
@@ -3803,9 +5000,21 @@ async function startWorker(options) {
3803
5000
  }
3804
5001
  }
3805
5002
  };
3806
- const execution = (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
5003
+ const execution = (async () => {
5004
+ let targetReserved = false;
5005
+ try {
5006
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5007
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5008
+ targetReserved = true;
5009
+ });
5010
+ await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
5011
+ } finally {
5012
+ if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
5013
+ }
5014
+ })();
3807
5015
  void execution.catch((error) => {
3808
- sendWorkerMessage(ws, {
5016
+ if (error instanceof StaleWorkerAdmissionError) return;
5017
+ sendWorkerMessageFromCurrentSource(ws, {
3809
5018
  type: "exec_start_error",
3810
5019
  requestId: message.requestId,
3811
5020
  runId: message.runId,
@@ -3815,6 +5024,14 @@ async function startWorker(options) {
3815
5024
  return;
3816
5025
  }
3817
5026
  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") {
5027
+ const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
5028
+ const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
5029
+ if (reservesVisibleWorkspace) {
5030
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5031
+ workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
5032
+ });
5033
+ }
5034
+ let dirtyTrigger;
3818
5035
  try {
3819
5036
  const result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
3820
5037
  const resolvedTarget = resolveMessageTarget(message.target);
@@ -3834,13 +5051,14 @@ async function startWorker(options) {
3834
5051
  result
3835
5052
  })
3836
5053
  );
3837
- if ((message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target)) {
3838
- markWorkspaceDirty({
5054
+ if (mutatesVisibleWorkspace) {
5055
+ dirtyTrigger = {
3839
5056
  type: message.type,
3840
5057
  sessionId: message.sessionId,
3841
5058
  toolCallId: message.requestId,
3842
5059
  ...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
3843
- });
5060
+ };
5061
+ markWorkspaceDirty(dirtyTrigger);
3844
5062
  }
3845
5063
  } catch (error) {
3846
5064
  ws.send(
@@ -3850,6 +5068,13 @@ async function startWorker(options) {
3850
5068
  error: error instanceof Error ? error.message : String(error)
3851
5069
  })
3852
5070
  );
5071
+ } finally {
5072
+ if (reservesVisibleWorkspace) {
5073
+ workspaceSyncPriorityOperationTargets.delete(message.requestId);
5074
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5075
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5076
+ }
5077
+ }
3853
5078
  }
3854
5079
  return;
3855
5080
  }
@@ -3887,6 +5112,7 @@ async function startWorker(options) {
3887
5112
  terminalReplayTimer = void 0;
3888
5113
  }
3889
5114
  if (currentWorkerSocket === ws) {
5115
+ advanceWorkerAdmissionGeneration();
3890
5116
  currentWorkerSocket = null;
3891
5117
  }
3892
5118
  closeAllPtys();
@@ -3899,16 +5125,16 @@ async function startWorker(options) {
3899
5125
  if (shutdownAfterClose) {
3900
5126
  process.exit(0);
3901
5127
  }
3902
- if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
5128
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
3903
5129
  process.stderr.write(
3904
- `[r5d-worker] ${activeProcesses.size} process(es) active and ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${import_supervisor.WORKER_RECONNECT_DELAY_MS}ms
5130
+ `[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 ${import_supervisor.WORKER_RECONNECT_DELAY_MS}ms
3905
5131
  `
3906
5132
  );
3907
5133
  const reconnect = () => {
3908
5134
  void startWorker(options).catch((error) => {
3909
5135
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
3910
5136
  `);
3911
- if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
5137
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
3912
5138
  setTimeout(reconnect, import_supervisor.WORKER_RECONNECT_DELAY_MS);
3913
5139
  return;
3914
5140
  }
@@ -3991,6 +5217,7 @@ if (isCliEntrypoint()) {
3991
5217
  resolveWorkerFilePath,
3992
5218
  resolveWorkerSessionTarget,
3993
5219
  syncSessionArtifacts,
5220
+ workerChildProcessEnvironment,
3994
5221
  workerGitSecurityTestHarness,
3995
5222
  writeWorkerTextFile
3996
5223
  });