@ricsam/r5d-worker 0.0.78 → 0.0.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/main.cjs +1550 -335
  3. package/dist/cjs/managed-paths.cjs +101 -1
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-workspace-state.cjs +184 -5
  6. package/dist/cjs/project-worktrees.cjs +641 -56
  7. package/dist/cjs/registry-auth.cjs +310 -0
  8. package/dist/cjs/repository-transition-policy.cjs +49 -0
  9. package/dist/cjs/supervisor.cjs +62 -11
  10. package/dist/cjs/working-tree-mirror.cjs +191 -34
  11. package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
  12. package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
  14. package/dist/cjs/workspace-git-sync.cjs +1028 -54
  15. package/dist/cjs/workspace-mount-boundary.cjs +71 -0
  16. package/dist/cjs/workspace-path-move.cjs +195 -0
  17. package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
  18. package/dist/cjs/workspace-project-config-policy.cjs +46 -0
  19. package/dist/cjs/workspace-publication-evidence.cjs +46 -0
  20. package/dist/mjs/main.mjs +1584 -341
  21. package/dist/mjs/managed-paths.mjs +100 -1
  22. package/dist/mjs/package.json +1 -1
  23. package/dist/mjs/project-workspace-state.mjs +181 -5
  24. package/dist/mjs/project-worktrees.mjs +632 -55
  25. package/dist/mjs/registry-auth.mjs +269 -0
  26. package/dist/mjs/repository-transition-policy.mjs +22 -0
  27. package/dist/mjs/supervisor.mjs +61 -11
  28. package/dist/mjs/working-tree-mirror.mjs +190 -34
  29. package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
  30. package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
  31. package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
  32. package/dist/mjs/workspace-git-sync.mjs +1023 -54
  33. package/dist/mjs/workspace-mount-boundary.mjs +37 -0
  34. package/dist/mjs/workspace-path-move.mjs +160 -0
  35. package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
  36. package/dist/mjs/workspace-project-config-policy.mjs +21 -0
  37. package/dist/mjs/workspace-publication-evidence.mjs +21 -0
  38. package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
  39. package/dist/types/main.d.ts +175 -1
  40. package/dist/types/managed-paths.d.ts +11 -0
  41. package/dist/types/project-workspace-state.d.ts +45 -1
  42. package/dist/types/project-worktrees.d.ts +119 -2
  43. package/dist/types/registry-auth.d.ts +47 -0
  44. package/dist/types/repository-transition-policy.d.ts +26 -0
  45. package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
  46. package/dist/types/supervisor-signal-fixture.d.ts +1 -0
  47. package/dist/types/supervisor.d.ts +6 -4
  48. package/dist/types/working-tree-mirror.d.ts +7 -0
  49. package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
  50. package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
  51. package/dist/types/workspace-command-sync-policy.d.ts +9 -0
  52. package/dist/types/workspace-git-sync.d.ts +33 -3
  53. package/dist/types/workspace-mount-boundary.d.ts +10 -0
  54. package/dist/types/workspace-path-move.d.ts +21 -0
  55. package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
  56. package/dist/types/workspace-project-config-policy.d.ts +34 -0
  57. package/dist/types/workspace-publication-evidence.d.ts +17 -0
  58. package/package.json +1 -1
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
  };
@@ -3059,6 +3908,28 @@ async function startWorker(options) {
3059
3908
  resetToCanonical: input.resetToCanonical
3060
3909
  });
3061
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
+ });
3062
3933
  return failedWorkspaceSyncResult(attemptId, input.trigger, error);
3063
3934
  }
3064
3935
  });
@@ -3080,7 +3951,13 @@ async function startWorker(options) {
3080
3951
  };
3081
3952
  const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
3082
3953
  pendingAutomaticTrigger = trigger;
3083
- 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") {
3084
3961
  return;
3085
3962
  }
3086
3963
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -3088,8 +3965,14 @@ async function startWorker(options) {
3088
3965
  () => {
3089
3966
  workspaceAutomaticTimer = void 0;
3090
3967
  const scheduledTrigger = pendingAutomaticTrigger;
3091
- pendingAutomaticTrigger = void 0;
3092
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;
3093
3976
  automaticSyncInFlight = true;
3094
3977
  void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
3095
3978
  if (result.outcome === "failed") {
@@ -3109,7 +3992,7 @@ async function startWorker(options) {
3109
3992
  }
3110
3993
  });
3111
3994
  },
3112
- Math.max(0, delayMs)
3995
+ Math.max(0, delayMs, initialDeferral?.kind === "creation_grace" ? initialDeferral.retryAfterMs : 0)
3113
3996
  );
3114
3997
  workspaceAutomaticTimer.unref();
3115
3998
  };
@@ -3117,32 +4000,190 @@ async function startWorker(options) {
3117
4000
  scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
3118
4001
  };
3119
4002
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
3120
- const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
3121
- target,
3122
- projectsRoot,
3123
- workspaceShadowRoot,
3124
- projectConfigById
3125
- });
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
+ };
3126
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;
3127
4139
  workspaceSyncRequestsInFlight += 1;
3128
4140
  try {
3129
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
+ }
3130
4159
  workspaceConfigured = false;
4160
+ const preserveOnlyBranches = message.projects.flatMap(
4161
+ (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4162
+ );
3131
4163
  projectWorkspaceState = projectWorkspaceStateStore.reconcile({
3132
- desiredProjects: message.projects
4164
+ desiredProjects: message.projects,
4165
+ preserveOnlyBranches
3133
4166
  });
3134
- 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();
3135
4177
  const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
3136
- requiredCredentialStorePaths.add(workspaceStorePath);
3137
4178
  const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
3138
4179
  [{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
3139
- workspaceStorePath
4180
+ workspaceStorePath,
4181
+ desiredCredentialStoreFiles
3140
4182
  );
3141
4183
  if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
3142
4184
  const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
3143
4185
  for (const project of message.projects) {
3144
- requiredCredentialStorePaths.add(credentialStorePathForProject(project.projectId));
3145
- projectConnection(project);
4186
+ projectConnection(project, desiredCredentialStoreFiles);
3146
4187
  }
3147
4188
  const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
3148
4189
  ...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
@@ -3151,30 +4192,81 @@ async function startWorker(options) {
3151
4192
  ]);
3152
4193
  for (const projectId of pendingCredentialProjectIds) {
3153
4194
  const desiredProject = desiredProjectConfigById.get(projectId);
3154
- if (desiredProject) projectConnection(desiredProject);
3155
- else projectMirrorConnection(projectId);
3156
- requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
4195
+ if (desiredProject) projectConnection(desiredProject, desiredCredentialStoreFiles);
4196
+ else projectMirrorConnection(projectId, desiredCredentialStoreFiles);
3157
4197
  }
3158
4198
  for (const deletion of pendingMirrorDeletes.values()) {
3159
4199
  const desiredProject = desiredProjectConfigById.get(deletion.projectId);
3160
- const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
4200
+ const connection = desiredProject ? projectConnection(desiredProject, desiredCredentialStoreFiles) : projectMirrorConnection(deletion.projectId, desiredCredentialStoreFiles);
3161
4201
  deletion.mirrorUrl = connection.mirrorUrl;
3162
4202
  deletion.credentialHelper = connection.credentialHelper;
3163
4203
  deletion.credentialUsername = connection.mirrorCredentialUsername;
3164
4204
  }
3165
- pruneCredentialStoreFiles(requiredCredentialStorePaths);
3166
- 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;
3167
4229
  visibleGitIdentity = message.gitIdentity;
3168
4230
  workspaceGitIdentity = message.gitIdentity;
3169
4231
  workspaceRemoteUrl = message.workspaceRemoteUrl;
3170
4232
  workspaceCredentialHelper = nextWorkspaceCredentialHelper;
3171
4233
  workspaceCredentialUsername = workerCredentialUsername;
3172
- (0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, message.projects);
4234
+ (0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, projectWorkspaceState.desiredProjects);
3173
4235
  const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
3174
4236
  ...project,
3175
4237
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
3176
4238
  }));
3177
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)));
3178
4270
  for (const existingProject of projectConfigById.values()) {
3179
4271
  const nextProject = nextProjectConfigById.get(existingProject.projectId);
3180
4272
  if (!nextProject) {
@@ -3184,6 +4276,7 @@ async function startWorker(options) {
3184
4276
  const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
3185
4277
  for (const existingBranch of existingProject.branches) {
3186
4278
  if (!nextBranches.has(existingBranch.branchName)) {
4279
+ if (preserveOnlyKeys.has(projectBranchKey(existingProject.projectId, existingBranch.branchName))) continue;
3187
4280
  stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
3188
4281
  }
3189
4282
  }
@@ -3199,40 +4292,30 @@ async function startWorker(options) {
3199
4292
  for (const projectId of [...readyProjectIds]) {
3200
4293
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
3201
4294
  }
3202
- (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
3203
- workspacePath: workspaceShadowRoot,
3204
- remoteUrl: message.workspaceRemoteUrl,
3205
- credentialHelper: nextWorkspaceCredentialHelper,
3206
- credentialUsername: workerCredentialUsername,
3207
- gitIdentity: message.gitIdentity
3208
- });
3209
- pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
3210
4295
  stageDurableWorkspaceDeletions();
3211
4296
  const mountsBeforeGit = buildWorkspaceMounts();
3212
4297
  (0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
3213
4298
  workspaceShadowRoot,
3214
- 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 }
3215
4305
  );
3216
4306
  ensureConfiguredProjects();
3217
- let result = await performWorkspaceSync({
4307
+ const result = await performWorkspaceSync({
3218
4308
  attemptId: crypto.randomUUID(),
3219
4309
  trigger: { type: "connect" }
3220
4310
  });
3221
- if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
3222
- const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
3223
- if (staleAfterInbound.length > 0) {
3224
- pruneStaleOuterWorkspaceEntries(staleAfterInbound);
3225
- stageDurableWorkspaceDeletions();
3226
- result = await performWorkspaceSync({
3227
- attemptId: crypto.randomUUID(),
3228
- trigger: { type: "connect" }
3229
- });
3230
- }
3231
- }
3232
4311
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
3233
4312
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4313
+ const activeMountIds = new Set(result.activeMountIds ?? []);
3234
4314
  const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
3235
- (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] : []
3236
4319
  );
3237
4320
  if (layoutCleanupIds.length > 0) {
3238
4321
  projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
@@ -3248,18 +4331,37 @@ async function startWorker(options) {
3248
4331
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
3249
4332
  });
3250
4333
  } catch (error) {
3251
- workspaceConfigured = import_node_fs.default.existsSync(import_node_path.default.join(workspaceShadowRoot, ".git"));
3252
- for (const project of message.projects) {
3253
- for (const branch of project.branches) {
3254
- pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
3255
- projectId: project.projectId,
3256
- branchName: branch.branchName
3257
- });
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
+ );
3258
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);
3259
4359
  }
3260
4360
  return {
3261
4361
  result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
3262
- pending: [...pendingCheckouts.values()],
4362
+ pending: [...pendingCheckouts.values()].sort(
4363
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
4364
+ ),
3263
4365
  aheadOfOriginBranches: []
3264
4366
  };
3265
4367
  } finally {
@@ -3354,12 +4456,22 @@ async function startWorker(options) {
3354
4456
  ws.addEventListener("message", (event) => {
3355
4457
  void (async () => {
3356
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
+ });
3357
4468
  if (message.type === "connected") {
3358
4469
  return;
3359
4470
  }
3360
4471
  if (message.type === "workspace_config") {
4472
+ advanceWorkerAdmissionGeneration();
3361
4473
  const configured = await configureWorkerWorkspace(message);
3362
- sendWorkerMessage(ws, {
4474
+ sendWorkerMessageFromCurrentSource(ws, {
3363
4475
  type: "workspace_configured",
3364
4476
  requestId: message.requestId,
3365
4477
  result: configured.result,
@@ -3394,59 +4506,85 @@ async function startWorker(options) {
3394
4506
  }
3395
4507
  if (message.type === "create_project_branch") {
3396
4508
  try {
3397
- projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch({
4509
+ const pendingBranch = {
4510
+ branchId: message.branchId,
3398
4511
  projectId: message.projectId,
3399
4512
  branchName: message.targetBranch
3400
- });
4513
+ };
3401
4514
  const created = await workspaceSyncSingleFlight.runMutation(() => {
3402
4515
  const project = projectConfigById.get(message.projectId);
3403
4516
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3404
- if (!readyProjectIds.has(project.projectId)) {
3405
- throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
3406
- }
3407
- if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
3408
- throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
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`);
3409
4520
  }
3410
- if (project.branches.some(({ branchName }) => branchName === message.targetBranch)) {
3411
- throw new Error(`Project branch ${message.targetBranch} already exists`);
3412
- }
3413
- const createdBranch = (0, import_project_worktrees.createLinkedProjectBranch)({
3414
- projectRoot: configuredProjectRoot(projectsRoot, project),
3415
- sourceBranchName: message.sourceBranch,
3416
- branchName: message.targetBranch
3417
- });
3418
- projectConfigById.set(project.projectId, {
3419
- ...project,
3420
- branches: [
3421
- ...project.branches,
3422
- {
3423
- branchName: message.targetBranch,
3424
- sourceBranchName: message.sourceBranch,
3425
- 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`);
3426
4538
  }
3427
- ]
3428
- });
3429
- lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
3430
- 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
+ }
3431
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
+ );
3432
4578
  sendWorkerMessage(ws, {
3433
4579
  type: "operation_result",
3434
4580
  requestId: message.requestId,
3435
4581
  result: {
3436
4582
  type: "create_project_branch",
4583
+ branchId: message.branchId,
3437
4584
  branchName: message.targetBranch,
3438
4585
  baseCommitHash: created.baseCommitHash
3439
4586
  }
3440
4587
  });
3441
- markWorkspaceDirty(
3442
- {
3443
- type: "manual",
3444
- projectId: message.projectId,
3445
- branchName: message.targetBranch,
3446
- detail: `created project branch from ${message.sourceBranch}`
3447
- },
3448
- true
3449
- );
3450
4588
  } catch (error) {
3451
4589
  sendWorkerMessage(ws, {
3452
4590
  type: "operation_result",
@@ -3459,42 +4597,66 @@ async function startWorker(options) {
3459
4597
  if (message.type === "delete_project_branch") {
3460
4598
  try {
3461
4599
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
3462
- projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
3463
- projectId: message.projectId,
3464
- branchName: message.branchName
3465
- });
3466
- await workspaceSyncSingleFlight.runMutation(() => {
4600
+ const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
3467
4601
  const project = projectConfigById.get(message.projectId);
3468
4602
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3469
- 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;
3470
4628
  stageProjectBranchDeletion(project, message.branchName, true);
3471
4629
  projectConfigById.set(project.projectId, {
3472
4630
  ...project,
3473
4631
  branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
3474
4632
  });
4633
+ return true;
3475
4634
  });
3476
- const result = await runWorkspaceSync({
3477
- trigger: {
3478
- type: "manual",
3479
- projectId: message.projectId,
3480
- branchName: message.branchName,
3481
- detail: "deleted project branch"
3482
- },
3483
- confirmedLargeDiff: true,
3484
- confirmationReason: "explicit project branch deletion",
3485
- sendResult: false
3486
- });
3487
- if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
3488
- throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
3489
- }
3490
- if (pendingMirrorDeletes.has(deletionKey)) {
3491
- 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
+ }
3492
4653
  }
3493
4654
  sendWorkerMessage(ws, {
3494
4655
  type: "operation_result",
3495
4656
  requestId: message.requestId,
3496
4657
  result: {
3497
4658
  type: "delete_project_branch",
4659
+ branchId: message.branchId,
3498
4660
  branchName: message.branchName
3499
4661
  }
3500
4662
  });
@@ -3682,9 +4844,13 @@ async function startWorker(options) {
3682
4844
  });
3683
4845
  return;
3684
4846
  }
3685
- 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;
3686
4851
  let mutationLeaseTransferred = false;
3687
4852
  try {
4853
+ releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
3688
4854
  const resolvedTarget = resolveMessageTarget(message.target);
3689
4855
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
3690
4856
  `);
@@ -3693,6 +4859,7 @@ async function startWorker(options) {
3693
4859
  message,
3694
4860
  resolvedTarget,
3695
4861
  planRoot,
4862
+ assertAdmission: assertMessageAdmission,
3696
4863
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
3697
4864
  ...targetMayMutateVisibleWorkspace(message.target) ? {
3698
4865
  onTerminal: () => {
@@ -3702,13 +4869,14 @@ async function startWorker(options) {
3702
4869
  });
3703
4870
  mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
3704
4871
  } catch (error) {
3705
- sendWorkerMessage(ws, {
4872
+ sendWorkerMessageFromCurrentSource(ws, {
3706
4873
  type: "pty_error",
3707
4874
  requestId: message.requestId,
3708
4875
  ptyId: message.ptyId,
3709
4876
  error: error instanceof Error ? error.message : String(error)
3710
4877
  });
3711
4878
  } finally {
4879
+ workspaceSyncPriorityPtyTargets.delete(message.ptyId);
3712
4880
  if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
3713
4881
  }
3714
4882
  return;
@@ -3745,15 +4913,29 @@ async function startWorker(options) {
3745
4913
  return;
3746
4914
  }
3747
4915
  let result;
4916
+ let targetReserved = false;
3748
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
+ });
3749
4922
  const runCommand = async () => {
3750
4923
  const resolvedTarget = resolveMessageTarget(message.target);
3751
4924
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
3752
4925
  `);
3753
- 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
+ });
3754
4935
  };
3755
4936
  result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
3756
4937
  } catch (error) {
4938
+ if (error instanceof StaleWorkerAdmissionError) return;
3757
4939
  result = {
3758
4940
  type: "exec_result",
3759
4941
  requestId: message.requestId,
@@ -3762,6 +4944,8 @@ async function startWorker(options) {
3762
4944
  exitCode: 1,
3763
4945
  error: error instanceof Error ? error.message : String(error)
3764
4946
  };
4947
+ } finally {
4948
+ if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
3765
4949
  }
3766
4950
  ws.send(JSON.stringify(result));
3767
4951
  if (targetMayMutateVisibleWorkspace(message.target)) {
@@ -3799,7 +4983,8 @@ async function startWorker(options) {
3799
4983
  baseUrl,
3800
4984
  token,
3801
4985
  artifactRoot,
3802
- planRoot
4986
+ planRoot,
4987
+ assertAdmission: assertMessageAdmission
3803
4988
  });
3804
4989
  } finally {
3805
4990
  if (targetMayMutateVisibleWorkspace(message.target)) {
@@ -3815,9 +5000,21 @@ async function startWorker(options) {
3815
5000
  }
3816
5001
  }
3817
5002
  };
3818
- 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
+ })();
3819
5015
  void execution.catch((error) => {
3820
- sendWorkerMessage(ws, {
5016
+ if (error instanceof StaleWorkerAdmissionError) return;
5017
+ sendWorkerMessageFromCurrentSource(ws, {
3821
5018
  type: "exec_start_error",
3822
5019
  requestId: message.requestId,
3823
5020
  runId: message.runId,
@@ -3827,6 +5024,14 @@ async function startWorker(options) {
3827
5024
  return;
3828
5025
  }
3829
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;
3830
5035
  try {
3831
5036
  const result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
3832
5037
  const resolvedTarget = resolveMessageTarget(message.target);
@@ -3846,13 +5051,14 @@ async function startWorker(options) {
3846
5051
  result
3847
5052
  })
3848
5053
  );
3849
- if ((message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target)) {
3850
- markWorkspaceDirty({
5054
+ if (mutatesVisibleWorkspace) {
5055
+ dirtyTrigger = {
3851
5056
  type: message.type,
3852
5057
  sessionId: message.sessionId,
3853
5058
  toolCallId: message.requestId,
3854
5059
  ...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
3855
- });
5060
+ };
5061
+ markWorkspaceDirty(dirtyTrigger);
3856
5062
  }
3857
5063
  } catch (error) {
3858
5064
  ws.send(
@@ -3862,6 +5068,13 @@ async function startWorker(options) {
3862
5068
  error: error instanceof Error ? error.message : String(error)
3863
5069
  })
3864
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
+ }
3865
5078
  }
3866
5079
  return;
3867
5080
  }
@@ -3899,6 +5112,7 @@ async function startWorker(options) {
3899
5112
  terminalReplayTimer = void 0;
3900
5113
  }
3901
5114
  if (currentWorkerSocket === ws) {
5115
+ advanceWorkerAdmissionGeneration();
3902
5116
  currentWorkerSocket = null;
3903
5117
  }
3904
5118
  closeAllPtys();
@@ -3911,16 +5125,16 @@ async function startWorker(options) {
3911
5125
  if (shutdownAfterClose) {
3912
5126
  process.exit(0);
3913
5127
  }
3914
- if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
5128
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
3915
5129
  process.stderr.write(
3916
- `[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
3917
5131
  `
3918
5132
  );
3919
5133
  const reconnect = () => {
3920
5134
  void startWorker(options).catch((error) => {
3921
5135
  process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
3922
5136
  `);
3923
- if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
5137
+ if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
3924
5138
  setTimeout(reconnect, import_supervisor.WORKER_RECONNECT_DELAY_MS);
3925
5139
  return;
3926
5140
  }
@@ -4003,6 +5217,7 @@ if (isCliEntrypoint()) {
4003
5217
  resolveWorkerFilePath,
4004
5218
  resolveWorkerSessionTarget,
4005
5219
  syncSessionArtifacts,
5220
+ workerChildProcessEnvironment,
4006
5221
  workerGitSecurityTestHarness,
4007
5222
  writeWorkerTextFile
4008
5223
  });