@ricsam/r5d-worker 0.0.73 → 0.0.75

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 (32) hide show
  1. package/README.md +6 -4
  2. package/dist/cjs/main.cjs +1022 -976
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/project-workspace-state.cjs +777 -0
  5. package/dist/cjs/project-worktrees.cjs +505 -0
  6. package/dist/cjs/working-tree-mirror.cjs +225 -0
  7. package/dist/cjs/workspace-git-sync.cjs +565 -0
  8. package/dist/cjs/workspace-incident-state.cjs +2 -38
  9. package/dist/mjs/main.mjs +1038 -987
  10. package/dist/mjs/package.json +1 -1
  11. package/dist/mjs/project-workspace-state.mjs +745 -0
  12. package/dist/mjs/project-worktrees.mjs +461 -0
  13. package/dist/mjs/working-tree-mirror.mjs +190 -0
  14. package/dist/mjs/workspace-git-sync.mjs +524 -0
  15. package/dist/mjs/workspace-incident-state.mjs +1 -35
  16. package/dist/types/main.d.ts +35 -55
  17. package/dist/types/project-workspace-state.d.ts +144 -0
  18. package/dist/types/project-worktrees.d.ts +101 -0
  19. package/dist/types/working-tree-mirror.d.ts +24 -0
  20. package/dist/types/workspace-git-sync.d.ts +97 -0
  21. package/dist/types/workspace-incident-state.d.ts +0 -17
  22. package/dist/types/workspace-mutation-gate.d.ts +3 -3
  23. package/package.json +1 -1
  24. package/dist/cjs/workspace-convergence.cjs +0 -280
  25. package/dist/cjs/workspace-manifest-admission.cjs +0 -60
  26. package/dist/cjs/workspace-sync.cjs +0 -2469
  27. package/dist/mjs/workspace-convergence.mjs +0 -236
  28. package/dist/mjs/workspace-manifest-admission.mjs +0 -26
  29. package/dist/mjs/workspace-sync.mjs +0 -2417
  30. package/dist/types/workspace-convergence.d.ts +0 -93
  31. package/dist/types/workspace-manifest-admission.d.ts +0 -22
  32. package/dist/types/workspace-sync.d.ts +0 -167
package/dist/cjs/main.cjs CHANGED
@@ -31,25 +31,23 @@ var main_exports = {};
31
31
  __export(main_exports, {
32
32
  describeWorkerSessionTarget: () => describeWorkerSessionTarget,
33
33
  editWorkerTextFile: () => editWorkerTextFile,
34
- ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
35
34
  findWorkerFiles: () => findWorkerFiles,
36
35
  githubCliEnv: () => githubCliEnv,
37
36
  grepWorkerFiles: () => grepWorkerFiles,
38
37
  isArtifactEnvPath: () => isArtifactEnvPath,
38
+ listWorkerCodeDirectory: () => listWorkerCodeDirectory,
39
39
  listWorkerDirectory: () => listWorkerDirectory,
40
40
  prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
41
41
  prepareBuiltInToolPathsForTarget: () => prepareBuiltInToolPathsForTarget,
42
42
  preparePlanEnvForShell: () => preparePlanEnvForShell,
43
43
  prepareShellEnvForTarget: () => prepareShellEnvForTarget,
44
+ readWorkerCodeFile: () => readWorkerCodeFile,
44
45
  readWorkerImageFile: () => readWorkerImageFile,
45
46
  readWorkerTextFile: () => readWorkerTextFile,
46
47
  resolveHostShell: () => resolveHostShell,
47
48
  resolveWorkerFilePath: () => resolveWorkerFilePath,
48
49
  resolveWorkerSessionTarget: () => resolveWorkerSessionTarget,
49
50
  syncSessionArtifacts: () => syncSessionArtifacts,
50
- visibleCheckoutGitTestHarness: () => visibleCheckoutGitTestHarness,
51
- visibleCheckoutRemoteFor: () => visibleCheckoutRemoteFor,
52
- workspaceSyncCheckoutTargets: () => workspaceSyncCheckoutTargets,
53
51
  writeWorkerTextFile: () => writeWorkerTextFile
54
52
  });
55
53
  module.exports = __toCommonJS(main_exports);
@@ -59,18 +57,18 @@ var import_node_crypto = require("node:crypto");
59
57
  var import_node_os = __toESM(require("node:os"), 1);
60
58
  var import_node_child_process = require("node:child_process");
61
59
  var import_cli_update = require("./cli-update.cjs");
62
- var import_git_identity = require("./git-identity.cjs");
63
60
  var import_heartbeat = require("./heartbeat.cjs");
64
61
  var import_process_tree = require("./process-tree.cjs");
65
62
  var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
66
63
  var import_port_forward_client = require("./port-forward-client.cjs");
67
64
  var import_workspace_incident_state = require("./workspace-incident-state.cjs");
68
65
  var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
69
- var import_workspace_convergence = require("./workspace-convergence.cjs");
70
- var import_workspace_manifest_admission = require("./workspace-manifest-admission.cjs");
66
+ var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
71
67
  var import_supervisor = require("./supervisor.cjs");
72
68
  var import_managed_paths = require("./managed-paths.cjs");
73
- var import_workspace_sync = require("./workspace-sync.cjs");
69
+ var import_project_worktrees = require("./project-worktrees.cjs");
70
+ var import_project_workspace_state = require("./project-workspace-state.cjs");
71
+ var import_workspace_git_sync = require("./workspace-git-sync.cjs");
74
72
  const import_meta = {};
75
73
  class WorkerServerUnavailableError extends Error {
76
74
  name = "WorkerServerUnavailableError";
@@ -81,12 +79,34 @@ const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
81
79
  const DEFAULT_READ_LIMIT = 2e3;
82
80
  const DEFAULT_READ_MAX_BYTES = 5e4;
83
81
  const MAX_LINE_LENGTH = 2e3;
82
+ const WORKSPACE_GIT_QUIET_MS = 5e3;
83
+ const WORKSPACE_GIT_PERIODIC_MS = 6e4;
84
84
  const activeProcesses = /* @__PURE__ */ new Map();
85
85
  const pendingProcessTerminals = /* @__PURE__ */ new Map();
86
86
  const cancelledProcessRuns = /* @__PURE__ */ new Set();
87
87
  const activePtys = /* @__PURE__ */ new Map();
88
88
  let currentWorkerSocket = null;
89
- const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
89
+ const workspaceMutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
90
+ let workspaceSyncQueue = Promise.resolve();
91
+ const workspaceSyncSingleFlight = {
92
+ runExclusive(operation) {
93
+ const queued = workspaceMutationGate.runSync(operation);
94
+ workspaceSyncQueue = queued.then(
95
+ () => void 0,
96
+ () => void 0
97
+ );
98
+ return queued;
99
+ },
100
+ runMutation(operation) {
101
+ return workspaceMutationGate.runMutation(operation);
102
+ },
103
+ acquireMutation() {
104
+ return workspaceMutationGate.acquireMutation();
105
+ },
106
+ afterCurrent() {
107
+ return workspaceSyncQueue;
108
+ }
109
+ };
90
110
  let githubCredential = null;
91
111
  let visibleGitIdentity = null;
92
112
  function defaultConfigPath() {
@@ -649,20 +669,11 @@ function gitAuthArgs(auth) {
649
669
  const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
650
670
  return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
651
671
  }
652
- const visibleCheckoutGitTestHarness = {
653
- gitAuthArgs,
654
- commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args],
655
- cloneArgs: (remoteUrl, branchPath) => ["clone", "--no-recurse-submodules", "--origin", "origin", remoteUrl, branchPath],
656
- fetchArgs: (...args) => ["fetch", "--no-recurse-submodules", ...args]
672
+ const workerGitCommand = {
673
+ commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args]
657
674
  };
658
- function visibleCheckoutCloneArgs(remoteUrl, branchPath) {
659
- return visibleCheckoutGitTestHarness.cloneArgs(remoteUrl, branchPath);
660
- }
661
- function visibleCheckoutFetchArgs(...args) {
662
- return visibleCheckoutGitTestHarness.fetchArgs(...args);
663
- }
664
675
  function runGit(args, options = {}) {
665
- const command = visibleCheckoutGitTestHarness.commandArgs(args, options.auth);
676
+ const command = workerGitCommand.commandArgs(args, options.auth);
666
677
  const result = Bun.spawnSync(command, {
667
678
  cwd: options.cwd,
668
679
  stdout: "pipe",
@@ -691,9 +702,6 @@ function getGitBlobHashForContent(content) {
691
702
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
692
703
  return (0, import_node_crypto.createHash)("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
693
704
  }
694
- function hasWorktreeChanges(cwd) {
695
- return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
696
- }
697
705
  function isInsideBranchPath(branchPath, filePath) {
698
706
  const relative = import_node_path.default.relative(import_node_path.default.resolve(branchPath), import_node_path.default.resolve(filePath));
699
707
  return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
@@ -803,33 +811,9 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
803
811
  scope: "project"
804
812
  };
805
813
  }
806
- function fallbackInternalRemoteUrl(baseUrl, projectId) {
814
+ function canonicalProjectRemoteUrl(baseUrl, projectId) {
807
815
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
808
816
  }
809
- function visibleCheckoutRemoteFor(input) {
810
- const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
811
- if (!canonicalCheckout && input.manifest) {
812
- if (!input.manifest.repoHttpUrl) {
813
- throw new Error(`Project ${input.projectId} has no connected repository; connect one before opening its checkouts`);
814
- }
815
- return {
816
- remoteUrl: input.manifest.repoHttpUrl,
817
- authHeader: input.manifest.repoAuthHeader ?? null,
818
- persistAuth: true,
819
- reconcileExistingOrigin: true,
820
- requireRemoteBranch: false,
821
- requiredBaseCommit: null
822
- };
823
- }
824
- return {
825
- remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
826
- authHeader: `Authorization: Bearer ${input.token}`,
827
- persistAuth: false,
828
- reconcileExistingOrigin: true,
829
- requireRemoteBranch: true,
830
- requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
831
- };
832
- }
833
817
  function gitExtraHeaderUrlForRemote(remoteUrl) {
834
818
  try {
835
819
  return new URL(remoteUrl).origin;
@@ -846,44 +830,25 @@ function gitAuthForRemote(remoteUrl, authHeader) {
846
830
  header: authHeader
847
831
  };
848
832
  }
849
- function hasNormalVisibleGitDir(branchPath) {
850
- const gitPath = import_node_path.default.join(branchPath, ".git");
851
- return import_node_fs.default.existsSync(gitPath) && import_node_fs.default.statSync(gitPath).isDirectory();
852
- }
853
- function ensureOriginRemote(branchPath, remoteUrl) {
854
- if (tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) {
855
- runGit(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
856
- } else {
857
- runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
858
- }
859
- }
860
- function originRemoteUrl(branchPath) {
861
- if (!tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) return null;
862
- return runGit(["remote", "get-url", "origin"], { cwd: branchPath });
863
- }
864
833
  function shellQuote(value) {
865
834
  return `'${value.replace(/'/g, "'\\''")}'`;
866
835
  }
867
836
  function githubCredentialsPath() {
868
837
  return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "github-credentials");
869
838
  }
870
- function decodeBasicAuthHeader(authHeader) {
839
+ function decodeGitAuthHeader(authHeader) {
871
840
  const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
872
- if (!match) {
873
- return null;
841
+ if (match) {
842
+ const decoded = Buffer.from(match[1], "base64").toString("utf8");
843
+ const separatorIndex = decoded.indexOf(":");
844
+ if (separatorIndex <= 0) return null;
845
+ return { username: decoded.slice(0, separatorIndex), password: decoded.slice(separatorIndex + 1) };
874
846
  }
875
- const decoded = Buffer.from(match[1], "base64").toString("utf8");
876
- const separatorIndex = decoded.indexOf(":");
877
- if (separatorIndex <= 0) {
878
- return null;
879
- }
880
- return {
881
- username: decoded.slice(0, separatorIndex),
882
- password: decoded.slice(separatorIndex + 1)
883
- };
847
+ const bearer = /^Authorization:\s*Bearer\s+(.+)$/i.exec(authHeader.trim());
848
+ return bearer?.[1] ? { username: "r5d-worker", password: bearer[1] } : null;
884
849
  }
885
850
  function writeCredentialStoreEntry(remoteUrl, authHeader) {
886
- const credentials = decodeBasicAuthHeader(authHeader);
851
+ const credentials = decodeGitAuthHeader(authHeader);
887
852
  if (!credentials) {
888
853
  return null;
889
854
  }
@@ -917,29 +882,9 @@ function writeCredentialStoreEntry(remoteUrl, authHeader) {
917
882
  import_node_fs.default.chmodSync(storePath, 384);
918
883
  return storePath;
919
884
  }
920
- function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
921
- if (!authHeader) {
922
- return;
923
- }
885
+ function credentialHelperForRemote(remoteUrl, authHeader) {
924
886
  const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
925
- if (credentialStore) {
926
- runGit(["config", "credential.helper", `store --file=${shellQuote(credentialStore)}`], { cwd: branchPath });
927
- runGit(["config", "credential.useHttpPath", "false"], { cwd: branchPath });
928
- return;
929
- }
930
- runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
931
- }
932
- function clearVisibleGitAuthentication(branchPath) {
933
- let extraHeaderKeys = [];
934
- try {
935
- extraHeaderKeys = runGit(["config", "--local", "--name-only", "--get-regexp", "^http\\..*extraheader$"], { cwd: branchPath }).split(/\r?\n/).filter(Boolean);
936
- } catch {
937
- }
938
- for (const key of extraHeaderKeys) {
939
- tryGit(["config", "--local", "--unset-all", key], { cwd: branchPath });
940
- }
941
- runGit(["config", "--local", "--replace-all", "credential.helper", ""], { cwd: branchPath });
942
- tryGit(["config", "--local", "--unset-all", "credential.useHttpPath"], { cwd: branchPath });
887
+ return credentialStore ? `store --file=${shellQuote(credentialStore)}` : null;
943
888
  }
944
889
  function dockerConfigPath() {
945
890
  return import_node_path.default.join(import_node_os.default.homedir(), ".docker", "config.json");
@@ -1019,188 +964,26 @@ function githubProcessEnv() {
1019
964
  ...githubCliEnv(githubCredential?.token)
1020
965
  };
1021
966
  }
1022
- function checkoutVisibleBranch(input) {
1023
- const branchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`], { cwd: input.branchPath });
1024
- const remoteBranchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], {
1025
- cwd: input.branchPath
1026
- });
1027
- const defaultRemoteExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.defaultBranch}`], {
1028
- cwd: input.branchPath
1029
- });
1030
- const clean = !hasWorktreeChanges(input.branchPath);
1031
- if (remoteBranchExists && clean) {
1032
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1033
- cwd: input.branchPath
1034
- });
1035
- return;
1036
- }
1037
- if (branchExists) {
1038
- runGit(["checkout", "--no-recurse-submodules", input.branchName], { cwd: input.branchPath });
1039
- return;
1040
- }
1041
- if (defaultRemoteExists) {
1042
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.defaultBranch}`], {
1043
- cwd: input.branchPath
1044
- });
1045
- return;
1046
- }
1047
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
1048
- }
1049
- function reconcileRewrittenCanonicalHistory(input) {
1050
- const remoteRef = `refs/remotes/origin/${input.branchName}`;
1051
- if (!tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) return;
1052
- if (!tryGit(["show-ref", "--verify", "--quiet", remoteRef], { cwd: input.branchPath })) return;
1053
- if (tryGit(["merge-base", "HEAD", remoteRef], { cwd: input.branchPath })) return;
1054
- if (hasWorktreeChanges(input.branchPath)) return;
1055
- const localTree = runGit(["rev-parse", "HEAD^{tree}"], { cwd: input.branchPath });
1056
- const remoteTree = runGit(["rev-parse", `${remoteRef}^{tree}`], { cwd: input.branchPath });
1057
- if (localTree !== remoteTree) return;
1058
- const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1059
- runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1060
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, remoteRef], { cwd: input.branchPath });
1061
- }
1062
- function migrateExistingCheckoutToRequiredRemote(input) {
1063
- if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
1064
- const dirtyWorkingTree = hasWorktreeChanges(input.branchPath);
1065
- if (dirtyWorkingTree && input.requiredBaseCommit) {
1066
- throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
1067
- }
1068
- const preserveWorkingTree = dirtyWorkingTree;
1069
- runGit(
1070
- visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1071
- {
1072
- cwd: input.branchPath,
1073
- auth: input.remoteAuth
1074
- }
1075
- );
1076
- const canonicalRemoteRef = `refs/remotes/origin/${input.branchName}`;
1077
- if (!tryGit(["show-ref", "--verify", "--quiet", canonicalRemoteRef], { cwd: input.branchPath })) {
1078
- throw new Error(`Canonical branch ${input.branchName} is unavailable during checkout migration`);
1079
- }
1080
- if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
1081
- throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1082
- }
1083
- ensureOriginRemote(input.branchPath, input.remoteUrl);
1084
- if (preserveWorkingTree) return;
1085
- if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
1086
- const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1087
- runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1088
- }
1089
- runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1090
- cwd: input.branchPath
1091
- });
967
+ function primaryProjectBranch(config) {
968
+ if (config.branches.length === 0) throw new Error(`Project ${config.projectId} has no configured branches`);
969
+ if (config.branches.some(({ branchName }) => branchName === "main")) return "main";
970
+ if (config.branches.some(({ branchName }) => branchName === config.defaultBranch)) return config.defaultBranch;
971
+ return config.branches[0].branchName;
1092
972
  }
1093
- function assertCheckoutDerivedFromRequiredBase(input) {
1094
- const remoteRevision = `refs/remotes/origin/${input.branchName}`;
1095
- for (const revision of [remoteRevision, "HEAD"]) {
1096
- if (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, revision], { cwd: input.branchPath })) {
1097
- throw new Error(`Canonical checkout ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1098
- }
1099
- }
973
+ function configuredProjectRoot(projectsRoot, config) {
974
+ return (0, import_managed_paths.managedProjectRoot)(projectsRoot, config.checkoutPathSegments);
1100
975
  }
1101
- function ensureVisibleGitCheckout(input) {
1102
- validateBranchName(input.branchName);
1103
- import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
1104
- const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
1105
- const createdCheckout = !hasNormalVisibleGitDir(branchPath);
1106
- if (createdCheckout) {
1107
- import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1108
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
1109
- const cloned = tryGit(visibleCheckoutCloneArgs(input.remoteUrl, branchPath), {
1110
- auth: input.remoteAuth
1111
- });
1112
- if (!cloned && input.requireRemoteBranch) {
1113
- import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1114
- throw new Error(`Failed to clone canonical checkout for branch ${input.branchName}`);
1115
- }
1116
- if (!cloned) {
1117
- import_node_fs.default.mkdirSync(branchPath, { recursive: true });
1118
- runGit(["init"], { cwd: branchPath });
1119
- }
1120
- ensureOriginRemote(branchPath, input.remoteUrl);
1121
- if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1122
- else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
1123
- (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1124
- tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
1125
- if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1126
- import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1127
- throw new Error(`Canonical branch ${input.branchName} is unavailable after clone`);
1128
- }
1129
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1130
- if (input.requiredBaseCommit) {
1131
- try {
1132
- assertCheckoutDerivedFromRequiredBase({
1133
- branchPath,
1134
- branchName: input.branchName,
1135
- requiredBaseCommit: input.requiredBaseCommit
1136
- });
1137
- } catch (error) {
1138
- import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1139
- throw error;
1140
- }
1141
- }
1142
- return branchPath;
1143
- }
1144
- if (input.requireRemoteBranch) {
1145
- if (input.allowRequiredRemoteMigration === false && originRemoteUrl(branchPath) !== input.remoteUrl) {
1146
- throw new Error(`Canonical checkout ${input.branchName} no longer uses its required remote`);
1147
- }
1148
- migrateExistingCheckoutToRequiredRemote({
1149
- branchPath,
1150
- branchName: input.branchName,
1151
- remoteUrl: input.remoteUrl,
1152
- remoteAuth: input.remoteAuth,
1153
- requiredBaseCommit: input.requiredBaseCommit
1154
- });
1155
- runGit(
1156
- visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1157
- {
1158
- cwd: branchPath,
1159
- auth: input.remoteAuth
1160
- }
1161
- );
1162
- }
1163
- if (input.reconcileExistingOrigin) {
1164
- ensureOriginRemote(branchPath, input.remoteUrl);
1165
- }
1166
- if (!input.requireRemoteBranch) {
1167
- tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
1168
- }
1169
- if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1170
- throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
1171
- }
1172
- if (input.requireRemoteBranch) {
1173
- reconcileRewrittenCanonicalHistory({ branchPath, branchName: input.branchName });
1174
- }
1175
- if (input.requiredBaseCommit) {
1176
- assertCheckoutDerivedFromRequiredBase({
1177
- branchPath,
1178
- branchName: input.branchName,
1179
- requiredBaseCommit: input.requiredBaseCommit
1180
- });
1181
- }
1182
- if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1183
- else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
1184
- (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1185
- return branchPath;
976
+ function configuredProjectBranchPath(projectsRoot, config, branchName) {
977
+ validateBranchName(branchName);
978
+ return import_node_path.default.join(configuredProjectRoot(projectsRoot, config), ...branchName.split("/"));
1186
979
  }
1187
- function ensureBranchWorktree(input) {
1188
- const defaultBranch = input.manifest?.defaultBranch || "main";
1189
- const remote = visibleCheckoutRemoteFor(input);
1190
- return {
1191
- branchPath: ensureVisibleGitCheckout({
1192
- projectRoot: input.projectRoot,
1193
- branchName: input.branchName,
1194
- remoteUrl: remote.remoteUrl,
1195
- remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
1196
- persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
1197
- defaultBranch,
1198
- reconcileExistingOrigin: remote.reconcileExistingOrigin,
1199
- requireRemoteBranch: remote.requireRemoteBranch,
1200
- requiredBaseCommit: remote.requiredBaseCommit,
1201
- clearPersistentAuth: !remote.persistAuth
1202
- })
1203
- };
980
+ function hasProjectWorktree(checkoutPath) {
981
+ try {
982
+ const stat = import_node_fs.default.lstatSync(import_node_path.default.join(checkoutPath, ".git"));
983
+ return stat.isFile() || stat.isDirectory();
984
+ } catch {
985
+ return false;
986
+ }
1204
987
  }
1205
988
  function describeWorkerSessionTarget(target) {
1206
989
  return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
@@ -1211,40 +994,21 @@ function resolveWorkerSessionTarget(input) {
1211
994
  return { target: input.target, rootPath: input.projectsRoot };
1212
995
  }
1213
996
  if (!import_node_fs.default.existsSync(import_node_path.default.join(input.workspaceShadowRoot, ".git"))) {
1214
- throw new Error("The canonical workspace checkout is not initialized on this worker");
997
+ throw new Error("The workspace sync checkout is not initialized on this worker");
1215
998
  }
1216
999
  return { target: input.target, rootPath: input.workspaceShadowRoot };
1217
1000
  }
1218
- const manifest = input.manifestByProjectId.get(input.target.projectId);
1219
- const projectRoot = projectRootFor(input.projectsRoot, input.target.projectId, input.manifestByProjectId);
1220
- const worktree = ensureBranchWorktree({
1221
- projectId: input.target.projectId,
1222
- baseUrl: input.baseUrl,
1223
- token: input.token,
1224
- projectRoot,
1225
- syncRoot: input.syncRoot,
1226
- branchName: input.target.branchName,
1227
- manifest
1228
- });
1229
- return { target: input.target, rootPath: worktree.branchPath, manifest };
1230
- }
1231
- function workspaceSyncCheckoutTargets(input) {
1232
- const selected = /* @__PURE__ */ new Map();
1233
- const add = (target) => {
1234
- selected.set(`${target.projectId}\0${target.branchName}`, target);
1235
- };
1236
- if (input.canonicalCheckoutOnly) {
1237
- add(input.canonicalCheckoutOnly);
1238
- } else if (input.includeAllManifestCheckouts) {
1239
- for (const project of input.projects) {
1240
- for (const branchName of project.branches) add({ projectId: project.projectId, branchName });
1241
- }
1242
- } else {
1243
- for (const target of input.pendingTargets) add(target);
1001
+ const target = input.target;
1002
+ const config = input.projectConfigById.get(target.projectId);
1003
+ if (!config) throw new Error(`Project ${target.projectId} is missing from the worker workspace configuration`);
1004
+ if (!config.branches.some(({ branchName }) => branchName === target.branchName)) {
1005
+ throw new Error(`Project branch ${target.projectId}/${target.branchName} is missing from the worker workspace configuration`);
1244
1006
  }
1245
- return [...selected.values()].sort(
1246
- (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
1247
- );
1007
+ const branchPath = configuredProjectBranchPath(input.projectsRoot, config, target.branchName);
1008
+ if (!hasProjectWorktree(branchPath)) {
1009
+ throw new Error(`Project branch ${target.projectId}/${target.branchName} is not initialized on this worker`);
1010
+ }
1011
+ return { target, rootPath: branchPath, config };
1248
1012
  }
1249
1013
  function resolveCommandCwd(branchPath, cwd) {
1250
1014
  if (!cwd) {
@@ -1768,6 +1532,77 @@ async function executeLsOperation(input) {
1768
1532
  });
1769
1533
  return listWorkerDirectory(input.resolvedTarget.rootPath, input.message.path, input.message.limit, builtInPaths);
1770
1534
  }
1535
+ function resolveWorkerCodePath(branchPath, inputPath) {
1536
+ if (typeof inputPath !== "string" || !inputPath.startsWith("/") || inputPath.includes("\0")) {
1537
+ throw new Error("Code path must be an absolute virtual path");
1538
+ }
1539
+ const rawSegments = inputPath.replace(/\\/g, "/").split("/");
1540
+ if (rawSegments.includes("..")) throw new Error(`Invalid code path: ${inputPath}`);
1541
+ const displayPath = import_node_path.default.posix.normalize(inputPath.replace(/\\/g, "/"));
1542
+ const relativePath = displayPath.replace(/^\/+/, "");
1543
+ const pathSegments = relativePath ? relativePath.split("/") : [];
1544
+ if (pathSegments.some((segment) => segment.toLowerCase() === ".git")) {
1545
+ throw new Error("Code paths cannot access Git metadata");
1546
+ }
1547
+ const resolvedBranchPath = import_node_path.default.resolve(branchPath);
1548
+ const absolutePath = import_node_path.default.resolve(resolvedBranchPath, ...pathSegments);
1549
+ assertInsideRoot(resolvedBranchPath, absolutePath, "Code path");
1550
+ if (!import_node_fs.default.existsSync(absolutePath)) throw new Error(`Code path not found: ${displayPath}`);
1551
+ const realBranchPath = import_node_fs.default.realpathSync(resolvedBranchPath);
1552
+ const realPath = import_node_fs.default.realpathSync(absolutePath);
1553
+ assertInsideRoot(realBranchPath, realPath, "Code path");
1554
+ const realRelativePath = import_node_path.default.relative(realBranchPath, realPath).split(import_node_path.default.sep);
1555
+ if (realRelativePath.some((segment) => segment.toLowerCase() === ".git")) {
1556
+ throw new Error("Code paths cannot access Git metadata");
1557
+ }
1558
+ return { absolutePath, displayPath };
1559
+ }
1560
+ function listWorkerCodeDirectory(branchPath, inputPath) {
1561
+ const resolved = resolveWorkerCodePath(branchPath, inputPath);
1562
+ if (!import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
1563
+ throw new Error(`Code directory not found: ${resolved.displayPath}`);
1564
+ }
1565
+ const entries = import_node_fs.default.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => entry.name.toLowerCase() !== ".git").flatMap((entry) => {
1566
+ const entryPath = import_node_path.default.posix.join(resolved.displayPath, entry.name);
1567
+ try {
1568
+ const resolvedEntry = resolveWorkerCodePath(branchPath, entryPath);
1569
+ const stats = import_node_fs.default.statSync(resolvedEntry.absolutePath);
1570
+ if (!stats.isFile() && !stats.isDirectory()) return [];
1571
+ return [
1572
+ {
1573
+ name: entry.name,
1574
+ type: stats.isDirectory() ? "directory" : "file",
1575
+ size: stats.isDirectory() ? null : stats.size,
1576
+ mtime: stats.mtime.toISOString()
1577
+ }
1578
+ ];
1579
+ } catch {
1580
+ return [];
1581
+ }
1582
+ }).sort((left, right) => Number(right.type === "directory") - Number(left.type === "directory") || left.name.localeCompare(right.name));
1583
+ return { type: "code_list", path: resolved.displayPath, entries };
1584
+ }
1585
+ function readWorkerCodeFile(branchPath, inputPath) {
1586
+ const resolved = resolveWorkerCodePath(branchPath, inputPath);
1587
+ const stats = import_node_fs.default.statSync(resolved.absolutePath);
1588
+ if (!stats.isFile()) throw new Error(`Code file not found: ${resolved.displayPath}`);
1589
+ const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
1590
+ return {
1591
+ type: "code_read",
1592
+ path: resolved.displayPath,
1593
+ size: bytes.length,
1594
+ mtime: stats.mtime.toISOString(),
1595
+ base64: bytes.toString("base64")
1596
+ };
1597
+ }
1598
+ function executeCodeListOperation(input) {
1599
+ if (input.resolvedTarget.target.type !== "project") throw new Error("Code listing requires a project branch target");
1600
+ return listWorkerCodeDirectory(input.resolvedTarget.rootPath, input.message.path);
1601
+ }
1602
+ function executeCodeReadOperation(input) {
1603
+ if (input.resolvedTarget.target.type !== "project") throw new Error("Code reading requires a project branch target");
1604
+ return readWorkerCodeFile(input.resolvedTarget.rootPath, input.message.path);
1605
+ }
1771
1606
  function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1772
1607
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1773
1608
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
@@ -1820,6 +1655,10 @@ async function executeOperation(input) {
1820
1655
  return executeLsOperation({ ...input, message: input.message });
1821
1656
  case "view_file_bytes":
1822
1657
  return executeViewFileBytesOperation({ ...input, message: input.message });
1658
+ case "code_list":
1659
+ return executeCodeListOperation({ ...input, message: input.message });
1660
+ case "code_read":
1661
+ return executeCodeReadOperation({ ...input, message: input.message });
1823
1662
  }
1824
1663
  }
1825
1664
  function targetIdentityEnv(target) {
@@ -2419,19 +2258,16 @@ function websocketUrl(baseUrl, label) {
2419
2258
  url.searchParams.set("version", getWorkerVersion());
2420
2259
  return url.toString();
2421
2260
  }
2422
- function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
2423
- const manifest = manifestByProjectId.get(projectId);
2424
- if (!manifest) throw new Error(`Project ${projectId} is missing from the worker workspace manifest`);
2425
- return (0, import_managed_paths.managedProjectRoot)(projectsRoot, manifest.checkoutPathSegments);
2426
- }
2427
2261
  async function startWorker(options) {
2428
2262
  process.on("uncaughtException", (error) => {
2429
2263
  process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
2430
2264
  `);
2431
2265
  });
2432
2266
  process.on("unhandledRejection", (reason) => {
2433
- process.stderr.write(`[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
2434
- `);
2267
+ process.stderr.write(
2268
+ `[r5d-worker] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
2269
+ `
2270
+ );
2435
2271
  });
2436
2272
  const config = readConfig(options.configPath);
2437
2273
  const { baseUrl, token } = resolveCredentials(options, config);
@@ -2460,402 +2296,785 @@ async function startWorker(options) {
2460
2296
  import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
2461
2297
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2462
2298
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2463
- const manifestByProjectId = /* @__PURE__ */ new Map();
2464
- const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2299
+ const projectWorkspaceStateStore = new import_project_workspace_state.ProjectWorkspaceStateStore({
2300
+ stateRoot: import_node_path.default.join(syncRoot, "project-state"),
2301
+ projectsRoot
2302
+ });
2303
+ let projectWorkspaceState = projectWorkspaceStateStore.read();
2304
+ const projectConfigById = /* @__PURE__ */ new Map();
2305
+ const pendingCheckouts = /* @__PURE__ */ new Map();
2306
+ const readyProjectIds = /* @__PURE__ */ new Set();
2307
+ const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
2308
+ const lastObservedProjectHeads = /* @__PURE__ */ new Map();
2309
+ const pendingMirrorDeletes = /* @__PURE__ */ new Map();
2465
2310
  let workspaceRemoteUrl = null;
2311
+ let workspaceGitIdentity = null;
2312
+ let workspaceConfigured = false;
2466
2313
  let activeWorkspaceIncidentId = null;
2467
- const workspaceIncidentOrdering = new import_workspace_incident_state.WorkspaceIncidentOrderingFence();
2468
- const workspaceConvergence = (0, import_workspace_convergence.processWorkspaceConvergenceState)(import_node_path.default.join(syncRoot, "workspace-convergence.json"));
2469
- const workspaceManifestRevision = new import_workspace_convergence.WorkspaceManifestRevisionFence();
2470
- let workspaceReady = false;
2471
- let workspacePublicationTimer;
2472
- let workspaceSafetyScan;
2473
- let workspaceManifestHydrationTimer;
2474
- let workspaceManifestHydrationInFlight = false;
2475
- let workspaceSafetyScanInFlight = false;
2476
- let workspaceHeadConvergenceInFlight = false;
2477
- let pendingWorkspaceHead;
2478
- let pendingPublication;
2479
- const explicitWorkspacePublications = new import_workspace_convergence.ExplicitWorkspacePublicationQueue();
2314
+ let workspaceAutomaticTimer;
2315
+ let workspacePeriodicTimer;
2316
+ let pendingAutomaticTrigger;
2317
+ let automaticSyncInFlight = false;
2480
2318
  let terminalReplayTimer;
2481
2319
  let workspaceSyncRequestsInFlight = 0;
2482
2320
  let sessionArtifactSyncRequestsInFlight = 0;
2483
2321
  let cliUpdateInProgress = false;
2484
2322
  let reloadAfterClose = false;
2485
2323
  let shutdownAfterClose = false;
2486
- const workspaceSyncInput = (trigger, overrides = {}) => {
2487
- if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2488
- const projects = (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], trigger);
2324
+ const bearerAuthHeader = `Authorization: Bearer ${token}`;
2325
+ const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2326
+ const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
2327
+ const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
2328
+ const workspaceProjectRelativePath = (projectId, branchName) => import_node_path.default.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
2329
+ const workspacePlanRelativePath = (projectId, branchName) => import_node_path.default.posix.join("plans", projectId, encodeURIComponent(branchName));
2330
+ const projectConnection = (project) => {
2331
+ const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
2332
+ const mirrorAuth = gitAuthForRemote(mirrorUrl, bearerAuthHeader);
2333
+ const originUrl = project.repoHttpUrl ?? mirrorUrl;
2334
+ const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
2335
+ const originAuth = gitAuthForRemote(originUrl, originHeader);
2336
+ const credentialHelper = originHeader ? credentialHelperForRemote(originUrl, originHeader) : null;
2337
+ return { mirrorUrl, mirrorAuth, originUrl, originAuth, credentialHelper };
2338
+ };
2339
+ const durableDeletionForBranch = (projectId, checkoutPathSegments, branchName) => projectWorkspaceState.tombstones.find(
2340
+ (tombstone) => tombstone.projectId === projectId && tombstone.checkoutPathSegments[0] === checkoutPathSegments[0] && tombstone.checkoutPathSegments[1] === checkoutPathSegments[1] && (tombstone.kind === "project" || tombstone.branchName === branchName)
2341
+ );
2342
+ const stageProjectBranchDeletion = (project, branchName, requireLocalBranch) => {
2343
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2344
+ const primaryBranchName = primaryProjectBranch(project);
2345
+ const connection = projectConnection(project);
2346
+ const sourcePath = configuredProjectBranchPath(projectsRoot, project, branchName);
2347
+ if (requireLocalBranch || hasProjectWorktree(sourcePath)) {
2348
+ (0, import_project_worktrees.deleteLinkedProjectBranch)({ projectRoot, primaryBranchName, branchName });
2349
+ }
2350
+ const planSourcePath = import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"));
2351
+ import_node_fs.default.rmSync(planSourcePath, { recursive: true, force: true });
2352
+ const key = projectBranchKey(project.projectId, branchName);
2353
+ const durableDeletion = durableDeletionForBranch(project.projectId, project.checkoutPathSegments, branchName);
2354
+ pendingCheckouts.delete(key);
2355
+ pendingMirrorDeletes.set(key, {
2356
+ id: projectMountId(project.projectId, branchName),
2357
+ projectId: project.projectId,
2358
+ branchName,
2359
+ gitDirectory: configuredProjectBranchPath(projectsRoot, project, primaryBranchName),
2360
+ mirrorUrl: connection.mirrorUrl,
2361
+ mirrorAuth: connection.mirrorAuth,
2362
+ sourcePath,
2363
+ workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branchName),
2364
+ planSourcePath,
2365
+ planWorkspaceRelativePath: workspacePlanRelativePath(project.projectId, branchName),
2366
+ ...durableDeletion ? { tombstoneId: durableDeletion.id } : {},
2367
+ ...durableDeletion?.kind === "project" ? { projectDeleted: !projectWorkspaceState.desiredProjects.some(({ projectId }) => projectId === project.projectId) } : {}
2368
+ });
2369
+ };
2370
+ const stageProjectDeletion = (project) => {
2371
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2372
+ const connection = projectConnection(project);
2373
+ const branches = project.branches.map(({ branchName }) => ({
2374
+ branchName,
2375
+ sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
2376
+ planSourcePath: import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/"))
2377
+ }));
2378
+ (0, import_project_worktrees.removeProjectWorktrees)({ projectRoot, branchNames: branches.map(({ branchName }) => branchName) });
2379
+ for (const branch of branches) {
2380
+ import_node_fs.default.rmSync(branch.planSourcePath, { recursive: true, force: true });
2381
+ const key = projectBranchKey(project.projectId, branch.branchName);
2382
+ const durableDeletion = durableDeletionForBranch(project.projectId, project.checkoutPathSegments, branch.branchName);
2383
+ pendingCheckouts.delete(key);
2384
+ pendingMirrorDeletes.set(key, {
2385
+ id: projectMountId(project.projectId, branch.branchName),
2386
+ projectId: project.projectId,
2387
+ branchName: branch.branchName,
2388
+ // The project Git directory is intentionally gone before the outer
2389
+ // tree is published. Use the outer workspace repository to delete the
2390
+ // hidden ref only after that publication succeeds.
2391
+ gitDirectory: workspaceShadowRoot,
2392
+ mirrorUrl: connection.mirrorUrl,
2393
+ mirrorAuth: connection.mirrorAuth,
2394
+ sourcePath: branch.sourcePath,
2395
+ workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2396
+ planSourcePath: branch.planSourcePath,
2397
+ planWorkspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
2398
+ ...durableDeletion ? { tombstoneId: durableDeletion.id } : {},
2399
+ ...durableDeletion?.kind === "project" ? { projectDeleted: !projectWorkspaceState.desiredProjects.some(({ projectId }) => projectId === project.projectId) } : {}
2400
+ });
2401
+ }
2402
+ readyProjectIds.delete(project.projectId);
2403
+ reconciledProjectConfigFingerprints.delete(project.projectId);
2404
+ };
2405
+ const cleanupConfigForDurableDeletion = (deletion) => {
2406
+ const desiredLayout = projectWorkspaceState.desiredProjects.find(
2407
+ (project) => project.projectId === deletion.projectId && project.checkoutPathSegments[0] === deletion.checkoutPathSegments[0] && project.checkoutPathSegments[1] === deletion.checkoutPathSegments[1]
2408
+ );
2409
+ const liveConfig = projectConfigById.get(deletion.projectId);
2410
+ const branchNames = deletion.kind === "project" ? deletion.branchNames : desiredLayout?.branches.map(({ branchName }) => branchName) ?? [];
2411
+ const defaultBranch = desiredLayout?.defaultBranch ?? (branchNames.includes("main") ? "main" : branchNames[0] ?? "main");
2489
2412
  return {
2490
- workerLabel: label,
2491
- remoteUrl: workspaceRemoteUrl,
2492
- authHeader: `Authorization: Bearer ${token}`,
2493
- projectsRoot,
2494
- plansRoot: planRoot,
2495
- shadowRoot: workspaceShadowRoot,
2496
- projects,
2497
- trigger,
2498
- ...overrides
2413
+ projectId: deletion.projectId,
2414
+ checkoutPathSegments: [...deletion.checkoutPathSegments],
2415
+ projectPath: liveConfig?.projectPath ?? deletion.checkoutPathSegments.join("/"),
2416
+ repoHttpUrl: liveConfig?.repoHttpUrl ?? null,
2417
+ repoAuthHeader: liveConfig?.repoAuthHeader ?? null,
2418
+ defaultBranch,
2419
+ branches: branchNames.map(
2420
+ (branchName) => liveConfig?.branches.find((branch) => branch.branchName === branchName) ?? {
2421
+ branchName,
2422
+ sourceBranchName: null,
2423
+ // Deletion never consumes the baseline, but retain a structurally
2424
+ // valid value for the shared worker-project shape.
2425
+ baseCommitHash: "0".repeat(40)
2426
+ }
2427
+ )
2499
2428
  };
2500
2429
  };
2501
- const sendWorkspaceSyncResult = (authority, result) => {
2502
- try {
2503
- ws.send(JSON.stringify({ type: "workspace_sync_result", ...authority, result }));
2504
- } catch (error) {
2505
- process.stderr.write(
2506
- `[r5d-worker] failed to send workspace_sync_result on its issuing connection: ${error instanceof Error ? error.message : String(error)}
2507
- `
2508
- );
2430
+ const stageDurableWorkspaceDeletions = () => {
2431
+ for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
2432
+ const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
2433
+ if (deletion.kind === "project") {
2434
+ if (!deletion.projectDeleted) {
2435
+ if (import_node_fs.default.existsSync(deletion.managedPath)) {
2436
+ (0, import_project_worktrees.removeProjectWorktrees)({ projectRoot: deletion.managedPath, branchNames: deletion.branchNames });
2437
+ }
2438
+ readyProjectIds.delete(deletion.projectId);
2439
+ reconciledProjectConfigFingerprints.delete(deletion.projectId);
2440
+ continue;
2441
+ }
2442
+ stageProjectDeletion(cleanupConfig);
2443
+ continue;
2444
+ }
2445
+ const primaryPath = deletion.primaryBranchName ? configuredProjectBranchPath(projectsRoot, cleanupConfig, deletion.primaryBranchName) : null;
2446
+ stageProjectBranchDeletion(cleanupConfig, deletion.branchName, Boolean(primaryPath && hasProjectWorktree(primaryPath)));
2447
+ }
2448
+ for (const deletion of projectWorkspaceState.pendingMirrorRefDeletions) {
2449
+ const key = projectBranchKey(deletion.projectId, deletion.branchName);
2450
+ const planSourcePath = import_node_path.default.join(planRoot, deletion.projectId, ...deletion.branchName.split("/"));
2451
+ import_node_fs.default.rmSync(planSourcePath, { recursive: true, force: true });
2452
+ pendingMirrorDeletes.set(key, {
2453
+ id: projectMountId(deletion.projectId, deletion.branchName),
2454
+ projectId: deletion.projectId,
2455
+ branchName: deletion.branchName,
2456
+ gitDirectory: workspaceShadowRoot,
2457
+ mirrorUrl: canonicalProjectRemoteUrl(baseUrl, deletion.projectId),
2458
+ mirrorAuth: gitAuthForRemote(canonicalProjectRemoteUrl(baseUrl, deletion.projectId), bearerAuthHeader),
2459
+ sourcePath: deletion.managedPath,
2460
+ workspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, deletion.branchName),
2461
+ planSourcePath,
2462
+ planWorkspaceRelativePath: workspacePlanRelativePath(deletion.projectId, deletion.branchName),
2463
+ tombstoneId: deletion.tombstoneId
2464
+ });
2509
2465
  }
2510
2466
  };
2511
- const sendWorkspaceReady = () => {
2512
- const manifestRevision = workspaceManifestRevision.readyRevision();
2513
- if (!workspaceReady || !manifestRevision) return;
2514
- try {
2515
- ws.send(
2516
- JSON.stringify({
2517
- type: "workspace_ready",
2518
- manifestRevision,
2519
- pendingCheckouts: [...pendingManifestCheckouts.values()].sort(
2520
- (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2521
- ),
2522
- ...workspaceConvergence.snapshot()
2523
- })
2524
- );
2525
- } catch (error) {
2526
- process.stderr.write(`[r5d-worker] failed to send workspace readiness: ${error instanceof Error ? error.message : String(error)}
2527
- `);
2467
+ const pruneStaleOuterWorkspaceEntries = (entries) => {
2468
+ const remove = (candidate) => {
2469
+ if (candidate) import_node_fs.default.rmSync(candidate, { recursive: true, force: true });
2470
+ };
2471
+ for (const entry of entries) {
2472
+ if (entry.kind === "branch") {
2473
+ projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
2474
+ projectId: entry.projectId,
2475
+ branchName: entry.branchName
2476
+ });
2477
+ remove(entry.projectTreePath);
2478
+ remove(entry.planTreePath);
2479
+ continue;
2480
+ }
2481
+ remove(entry.projectTreePath);
2482
+ remove(entry.planTreePath);
2483
+ for (const branch of entry.branches) {
2484
+ remove(branch.projectTreePath);
2485
+ remove(branch.planTreePath);
2486
+ }
2528
2487
  }
2529
2488
  };
2530
- const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
2531
- const armWorkspacePublication = (trigger, options2 = { delayMs: import_workspace_convergence.WORKSPACE_PUBLICATION_QUIET_MS }) => {
2532
- workspaceConvergence.schedule();
2533
- if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2534
- sendWorkspaceReady();
2535
- if (options2.requestId && !explicitWorkspacePublications.schedule({ requestId: options2.requestId, trigger }, pendingPublication?.requestId)) {
2536
- return;
2537
- }
2538
- if (!options2.requestId && explicitWorkspacePublications.hasScheduled()) return;
2539
- if (workspacePublicationTimer) clearTimeout(workspacePublicationTimer);
2540
- workspacePublicationTimer = setTimeout(
2541
- () => {
2542
- workspacePublicationTimer = void 0;
2543
- if (options2.requestId) explicitWorkspacePublications.consumeScheduled(options2.requestId);
2544
- if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
2545
- if (!workspaceReady || !workspaceRemoteUrl) {
2546
- armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2547
- return;
2548
- }
2549
- if (activeWorkspaceIncidentId) {
2550
- workspaceConvergence.block();
2551
- sendWorkspaceReady();
2552
- if ((0, import_workspace_convergence.workspacePublicationIncidentAction)(options2.requestId) === "defer_automatic") return;
2553
- if (pendingPublication) {
2554
- explicitWorkspacePublications.enqueue({ requestId: options2.requestId, trigger }, pendingPublication.requestId);
2555
- return;
2489
+ const effectiveWorkerProjects = (projects) => {
2490
+ const incomingById = new Map(projects.map((project) => [project.projectId, project]));
2491
+ const pending = new Set(
2492
+ projectWorkspaceState.locallyPendingCreatedBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName))
2493
+ );
2494
+ return projectWorkspaceState.effectiveDesiredProjects.flatMap((layout) => {
2495
+ const incoming = incomingById.get(layout.projectId);
2496
+ if (!incoming) return [];
2497
+ const existing = projectConfigById.get(layout.projectId);
2498
+ const branches = layout.branches.flatMap(({ branchName }) => {
2499
+ const configured = incoming.branches.find((branch) => branch.branchName === branchName);
2500
+ if (configured) return [configured];
2501
+ const retained = existing?.branches.find((branch) => branch.branchName === branchName);
2502
+ if (retained) return [retained];
2503
+ if (!pending.has(projectBranchKey(layout.projectId, branchName))) return [];
2504
+ const branchPath = configuredProjectBranchPath(
2505
+ projectsRoot,
2506
+ { ...incoming, checkoutPathSegments: layout.checkoutPathSegments },
2507
+ branchName
2508
+ );
2509
+ if (!hasProjectWorktree(branchPath)) return [];
2510
+ return [
2511
+ {
2512
+ branchName,
2513
+ sourceBranchName: null,
2514
+ baseCommitHash: runGit(["rev-parse", "HEAD"], { cwd: branchPath })
2556
2515
  }
2557
- const snapshot2 = workspaceConvergence.snapshot();
2558
- pendingPublication = {
2559
- requestId: options2.requestId,
2560
- trigger,
2561
- dirtyGeneration: snapshot2.dirtyGeneration,
2562
- visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2563
- explicit: true,
2564
- workInFlight: false,
2565
- requestedAt: Date.now()
2566
- };
2567
- try {
2568
- ws.send(
2569
- JSON.stringify({
2570
- type: "workspace_publication_request",
2571
- requestId: options2.requestId,
2572
- trigger,
2573
- dirtyGeneration: snapshot2.dirtyGeneration,
2574
- localHead: snapshot2.localHead,
2575
- desiredCanonicalHead: snapshot2.desiredCanonicalHead
2576
- })
2577
- );
2578
- } catch (error) {
2579
- pendingPublication = void 0;
2580
- workspaceConvergence.defer();
2516
+ ];
2517
+ });
2518
+ return [{ ...incoming, checkoutPathSegments: [...layout.checkoutPathSegments], branches }];
2519
+ });
2520
+ };
2521
+ const buildWorkspaceMounts = () => {
2522
+ const mounts = [];
2523
+ for (const project of [...projectConfigById.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
2524
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2525
+ for (const branch of [...project.branches].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
2526
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2527
+ mounts.push({
2528
+ id: projectMountId(project.projectId, branch.branchName),
2529
+ sourcePath: branchPath,
2530
+ workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2531
+ sourceMode: "git",
2532
+ hydrateDeletionMode: "git",
2533
+ busy: () => hasProjectWorktree(branchPath) && (0, import_project_worktrees.projectWorktreeOperationInProgress)(branchPath)
2534
+ });
2535
+ mounts.push({
2536
+ id: projectPlanMountId(project.projectId, branch.branchName),
2537
+ sourcePath: import_node_path.default.join(planRoot, project.projectId, ...branch.branchName.split("/")),
2538
+ workspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
2539
+ sourceMode: "all",
2540
+ hydrateDeletionMode: "all"
2541
+ });
2542
+ }
2543
+ void projectRoot;
2544
+ }
2545
+ mounts.push({
2546
+ id: "workspace-plans",
2547
+ sourcePath: import_node_path.default.join(planRoot, "workspace"),
2548
+ workspaceRelativePath: "workspace-plans",
2549
+ sourceMode: "all",
2550
+ hydrateDeletionMode: "all"
2551
+ });
2552
+ for (const deletion of pendingMirrorDeletes.values()) {
2553
+ mounts.push({
2554
+ id: deletion.id,
2555
+ sourcePath: deletion.sourcePath,
2556
+ workspaceRelativePath: deletion.workspaceRelativePath,
2557
+ sourceMode: "git",
2558
+ hydrateDeletionMode: "git",
2559
+ deleteWhenSourceMissing: true
2560
+ });
2561
+ mounts.push({
2562
+ id: `${deletion.id}:plan`,
2563
+ sourcePath: deletion.planSourcePath,
2564
+ workspaceRelativePath: deletion.planWorkspaceRelativePath,
2565
+ sourceMode: "all",
2566
+ hydrateDeletionMode: "all",
2567
+ deleteWhenSourceMissing: true
2568
+ });
2569
+ }
2570
+ return mounts;
2571
+ };
2572
+ const workspaceLocalHead = () => import_node_fs.default.existsSync(import_node_path.default.join(workspaceShadowRoot, ".git")) && tryGit(["rev-parse", "--verify", "HEAD"], { cwd: workspaceShadowRoot }) ? runGit(["rev-parse", "HEAD"], { cwd: workspaceShadowRoot }) : null;
2573
+ const workspaceGitStatus = () => {
2574
+ try {
2575
+ return runGit(["status", "--porcelain"], { cwd: workspaceShadowRoot });
2576
+ } catch {
2577
+ return "";
2578
+ }
2579
+ };
2580
+ const affectedProjects = (paths) => [
2581
+ ...new Set(
2582
+ paths.flatMap((filePath) => {
2583
+ const match = /^(?:projects|plans)\/([^/]+)\//.exec(filePath);
2584
+ return match?.[1] ? [match[1]] : [];
2585
+ })
2586
+ )
2587
+ ].sort();
2588
+ const ensureConfiguredProjects = () => {
2589
+ for (const project of projectConfigById.values()) {
2590
+ validateProjectId(project.projectId);
2591
+ for (const branch of project.branches) validateBranchName(branch.branchName);
2592
+ const projectRoot = configuredProjectRoot(projectsRoot, project);
2593
+ const configFingerprint = (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: workspaceGitIdentity });
2594
+ const alreadyReady = readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
2595
+ if (alreadyReady) {
2596
+ for (const branch of project.branches) pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
2597
+ continue;
2598
+ }
2599
+ try {
2600
+ const connection = projectConnection(project);
2601
+ const states = (0, import_project_worktrees.ensureProjectWorktrees)({
2602
+ projectRoot,
2603
+ primaryBranchName: primaryProjectBranch(project),
2604
+ branches: project.branches,
2605
+ originUrl: connection.originUrl,
2606
+ originAuth: connection.originAuth,
2607
+ mirrorUrl: connection.mirrorUrl,
2608
+ mirrorAuth: connection.mirrorAuth,
2609
+ credentialHelper: connection.credentialHelper,
2610
+ gitIdentity: workspaceGitIdentity
2611
+ });
2612
+ readyProjectIds.add(project.projectId);
2613
+ reconciledProjectConfigFingerprints.set(project.projectId, configFingerprint);
2614
+ for (const state of states) {
2615
+ pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
2616
+ const key = projectBranchKey(project.projectId, state.branchName);
2617
+ if (!lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
2618
+ if ((state.ahead ?? 0) > 0) {
2581
2619
  process.stderr.write(
2582
- `[r5d-worker] failed to request blocked workspace publication authority: ${error instanceof Error ? error.message : String(error)}
2620
+ `[r5d-worker] ${project.projectPath}/${state.branchName} is ${state.ahead} commit(s) ahead of origin; switching workers may require publishing or reconciling those commits
2583
2621
  `
2584
2622
  );
2585
- armWorkspacePublication(trigger, { delayMs: 1e3, requestId: options2.requestId });
2586
2623
  }
2587
- return;
2588
2624
  }
2589
- if (pendingPublication) {
2590
- armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2591
- return;
2625
+ } catch (error) {
2626
+ readyProjectIds.delete(project.projectId);
2627
+ reconciledProjectConfigFingerprints.delete(project.projectId);
2628
+ for (const branch of project.branches) {
2629
+ pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
2630
+ projectId: project.projectId,
2631
+ branchName: branch.branchName
2632
+ });
2592
2633
  }
2593
- const snapshot = workspaceConvergence.snapshot();
2594
- const requestId = options2.requestId ?? crypto.randomUUID();
2595
- pendingPublication = {
2596
- requestId,
2597
- trigger,
2598
- dirtyGeneration: snapshot.dirtyGeneration,
2599
- visibleBaselineHead: workspaceConvergence.visibleBaselineHead(),
2600
- explicit: options2.requestId !== void 0,
2601
- workInFlight: false,
2602
- requestedAt: Date.now()
2603
- };
2634
+ process.stderr.write(
2635
+ `[r5d-worker] failed to initialize project ${project.projectPath}: ${error instanceof Error ? error.message : String(error)}
2636
+ `
2637
+ );
2638
+ }
2639
+ }
2640
+ };
2641
+ const collectAheadOfOriginBranches = () => {
2642
+ const aheadBranches = [];
2643
+ for (const project of projectConfigById.values()) {
2644
+ if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2645
+ const connection = projectConnection(project);
2646
+ const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
2647
+ if (!tryGit(["fetch", "--no-recurse-submodules", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"], {
2648
+ cwd: primaryPath,
2649
+ auth: connection.originAuth
2650
+ })) {
2651
+ process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
2652
+ `);
2653
+ }
2654
+ for (const branch of project.branches) {
2655
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2656
+ if (!hasProjectWorktree(branchPath)) continue;
2604
2657
  try {
2605
- ws.send(
2606
- JSON.stringify({
2607
- type: "workspace_publication_request",
2608
- requestId,
2609
- trigger,
2610
- dirtyGeneration: snapshot.dirtyGeneration,
2611
- localHead: snapshot.localHead,
2612
- desiredCanonicalHead: snapshot.desiredCanonicalHead
2613
- })
2614
- );
2658
+ let ahead = 0;
2659
+ const originRef = `refs/remotes/origin/${branch.branchName}`;
2660
+ if (tryGit(["show-ref", "--verify", "--quiet", originRef], { cwd: branchPath })) {
2661
+ const counts = runGit(["rev-list", "--left-right", "--count", `${originRef}...HEAD`], { cwd: branchPath }).split(/\s+/).map(Number);
2662
+ ahead = Number.isSafeInteger(counts[1]) ? counts[1] : 0;
2663
+ } else if (tryGit(["cat-file", "-e", `${branch.baseCommitHash}^{commit}`], { cwd: branchPath })) {
2664
+ const count = Number(runGit(["rev-list", "--count", `${branch.baseCommitHash}..HEAD`], { cwd: branchPath }));
2665
+ ahead = Number.isSafeInteger(count) ? count : 0;
2666
+ }
2667
+ if (ahead > 0) aheadBranches.push({ projectId: project.projectId, branchName: branch.branchName, ahead });
2615
2668
  } catch (error) {
2616
- pendingPublication = void 0;
2617
- workspaceConvergence.defer();
2618
2669
  process.stderr.write(
2619
- `[r5d-worker] failed to request workspace publication: ${error instanceof Error ? error.message : String(error)}
2670
+ `[r5d-worker] could not measure origin divergence for ${project.projectPath}/${branch.branchName}: ${error instanceof Error ? error.message : String(error)}
2620
2671
  `
2621
2672
  );
2622
- armWorkspacePublication(trigger, { delayMs: 1e3, ...options2.requestId ? { requestId: options2.requestId } : {} });
2623
2673
  }
2624
- },
2625
- Math.max(0, options2.delayMs)
2674
+ }
2675
+ }
2676
+ return aheadBranches.sort(
2677
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
2626
2678
  );
2627
- workspacePublicationTimer.unref();
2628
- };
2629
- const markWorkspaceDirty = (trigger, force = false) => {
2630
- workspaceConvergence.markDirty();
2631
- armWorkspacePublication(trigger, { delayMs: force ? 0 : import_workspace_convergence.WORKSPACE_PUBLICATION_QUIET_MS });
2632
2679
  };
2633
- const convergeAvailableWorkspaceHead = async () => {
2634
- if (pendingWorkspaceHead === void 0 || !workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || workspaceHeadConvergenceInFlight) {
2635
- return;
2680
+ const pushChangedProjectHeads = (activeMountIds, publishedHead) => {
2681
+ const active = new Set(activeMountIds);
2682
+ for (const project of projectConfigById.values()) {
2683
+ if (!readyProjectIds.has(project.projectId)) continue;
2684
+ const changed = /* @__PURE__ */ new Set();
2685
+ for (const branch of project.branches) {
2686
+ if (!active.has(projectMountId(project.projectId, branch.branchName))) continue;
2687
+ const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
2688
+ if (!hasProjectWorktree(branchPath)) continue;
2689
+ const head = runGit(["rev-parse", "HEAD"], { cwd: branchPath });
2690
+ if (lastObservedProjectHeads.get(projectBranchKey(project.projectId, branch.branchName)) !== head) {
2691
+ changed.add(branch.branchName);
2692
+ }
2693
+ }
2694
+ if (changed.size === 0) continue;
2695
+ const connection = projectConnection(project);
2696
+ const results = (0, import_project_worktrees.pushProjectMirrorHeads)({
2697
+ projectRoot: configuredProjectRoot(projectsRoot, project),
2698
+ branchNames: project.branches.map(({ branchName }) => branchName),
2699
+ mirrorUrl: connection.mirrorUrl,
2700
+ mirrorAuth: connection.mirrorAuth,
2701
+ onlyBranches: changed
2702
+ });
2703
+ for (const result of results) {
2704
+ if (result.pushed) lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
2705
+ }
2636
2706
  }
2637
- const advertisedHead = pendingWorkspaceHead;
2638
- pendingWorkspaceHead = void 0;
2639
- workspaceConvergence.observeCanonicalHead(advertisedHead);
2640
- workspaceHeadConvergenceInFlight = true;
2641
- try {
2642
- const snapshot = workspaceConvergence.snapshot();
2643
- const hydrateVisible = !pendingPublication && snapshot.dirtyGeneration === snapshot.publishedGeneration;
2644
- const converged = await workspaceSyncSingleFlight.runExclusive(
2645
- () => (0, import_workspace_sync.convergeWorkspaceHead)(workspaceSyncInput({ type: "inbound_head", detail: advertisedHead ?? "unborn" }), {
2646
- hydrateVisible
2647
- })
2648
- );
2649
- workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
2650
- if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
2651
- if (hydrateVisible && converged.localChanges) {
2652
- markWorkspaceDirty({ type: "periodic", detail: "inbound overlap retained for next publication" });
2707
+ const publishedTombstoneIds = [
2708
+ ...new Set(
2709
+ [...pendingMirrorDeletes.values()].flatMap(
2710
+ (deletion) => active.has(deletion.id) && deletion.tombstoneId ? [deletion.tombstoneId] : []
2711
+ )
2712
+ )
2713
+ ];
2714
+ if (publishedTombstoneIds.length > 0) {
2715
+ projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
2716
+ tombstoneIds: publishedTombstoneIds,
2717
+ publishedHead
2718
+ });
2719
+ }
2720
+ for (const [key, deletion] of pendingMirrorDeletes) {
2721
+ if (!active.has(deletion.id)) continue;
2722
+ if (!deletion.projectDeleted) {
2723
+ (0, import_project_worktrees.deleteProjectMirrorBranch)({
2724
+ gitDirectory: deletion.gitDirectory,
2725
+ branchName: deletion.branchName,
2726
+ mirrorUrl: deletion.mirrorUrl,
2727
+ mirrorAuth: deletion.mirrorAuth
2728
+ });
2653
2729
  }
2654
- sendWorkspaceReady();
2655
- } catch (error) {
2656
- pendingWorkspaceHead = advertisedHead;
2657
- process.stderr.write(
2658
- `[r5d-worker] failed to fetch available workspace head: ${error instanceof Error ? error.message : String(error)}
2730
+ if (deletion.tombstoneId && !deletion.projectDeleted) {
2731
+ projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
2732
+ tombstoneId: deletion.tombstoneId,
2733
+ branchName: deletion.branchName
2734
+ });
2735
+ }
2736
+ pendingMirrorDeletes.delete(key);
2737
+ lastObservedProjectHeads.delete(key);
2738
+ }
2739
+ };
2740
+ const refreshProjectHeadsAfterInboundWorkspace = () => {
2741
+ for (const project of projectConfigById.values()) {
2742
+ if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2743
+ try {
2744
+ const connection = projectConnection(project);
2745
+ const refreshed = (0, import_project_worktrees.fastForwardProjectHeadsFromMirror)({
2746
+ projectRoot: configuredProjectRoot(projectsRoot, project),
2747
+ primaryBranchName: primaryProjectBranch(project),
2748
+ branchNames: project.branches.map(({ branchName }) => branchName),
2749
+ mirrorUrl: connection.mirrorUrl,
2750
+ mirrorAuth: connection.mirrorAuth
2751
+ });
2752
+ for (const state of refreshed) {
2753
+ lastObservedProjectHeads.set(
2754
+ projectBranchKey(project.projectId, state.branchName),
2755
+ state.fastForwarded && state.mirrorHead ? state.mirrorHead : state.previousHead
2756
+ );
2757
+ }
2758
+ } catch (error) {
2759
+ process.stderr.write(
2760
+ `[r5d-worker] failed to refresh project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
2659
2761
  `
2660
- );
2661
- } finally {
2662
- workspaceHeadConvergenceInFlight = false;
2663
- heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
2664
- if (pendingWorkspaceHead !== void 0) {
2665
- setTimeout(() => void convergeAvailableWorkspaceHead(), 1e3).unref();
2762
+ );
2666
2763
  }
2667
2764
  }
2668
2765
  };
2669
- const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2670
- const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2671
- (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2672
- );
2673
- const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
2674
- target,
2675
- baseUrl,
2676
- token,
2677
- projectsRoot,
2678
- syncRoot,
2679
- workspaceShadowRoot,
2680
- manifestByProjectId
2766
+ const reseedProjectHeadsAfterWorkspaceReset = () => {
2767
+ for (const project of projectConfigById.values()) {
2768
+ if (!readyProjectIds.has(project.projectId)) continue;
2769
+ const connection = projectConnection(project);
2770
+ const states = (0, import_project_worktrees.ensureProjectWorktrees)({
2771
+ projectRoot: configuredProjectRoot(projectsRoot, project),
2772
+ primaryBranchName: primaryProjectBranch(project),
2773
+ branches: project.branches,
2774
+ originUrl: connection.originUrl,
2775
+ originAuth: connection.originAuth,
2776
+ mirrorUrl: connection.mirrorUrl,
2777
+ mirrorAuth: connection.mirrorAuth,
2778
+ credentialHelper: connection.credentialHelper,
2779
+ gitIdentity: workspaceGitIdentity
2780
+ });
2781
+ for (const state of states) {
2782
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, state.branchName), state.head);
2783
+ }
2784
+ }
2785
+ };
2786
+ const mapWorkspaceGitResult = (attemptId, trigger, result) => ({
2787
+ type: "workspace_sync",
2788
+ attemptId,
2789
+ workerLabel: label,
2790
+ trigger,
2791
+ outcome: result.outcome === "pushed" ? "published" : result.outcome,
2792
+ startingHead: result.startingHead,
2793
+ ...result.localHead ? { localHead: result.localHead } : {},
2794
+ ...result.publishedHead ? { publishedHead: result.publishedHead } : {},
2795
+ rebaseCount: result.rebaseCount,
2796
+ diffSizeBytes: result.diffSizeBytes,
2797
+ gitStatus: workspaceGitStatus(),
2798
+ affectedProjects: affectedProjects(result.affectedPaths),
2799
+ affectedPaths: result.affectedPaths,
2800
+ discardedPaths: [],
2801
+ localChangesDiscarded: false,
2802
+ ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
2803
+ ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
2804
+ ...result.error ? { error: result.error } : {}
2681
2805
  });
2682
- const ensureVisibleWorktrees = (targets, options2 = {}) => {
2683
- const created = [];
2684
- for (const target of targets) {
2685
- const manifest = manifestByProjectId.get(target.projectId);
2686
- const key = manifestCheckoutKey(target.projectId, target.branchName);
2687
- if (!manifest || !manifest.branches.includes(target.branchName)) {
2688
- pendingManifestCheckouts.delete(key);
2689
- continue;
2806
+ const failedWorkspaceSyncResult = (attemptId, trigger, error) => ({
2807
+ type: "workspace_sync",
2808
+ attemptId,
2809
+ workerLabel: label,
2810
+ trigger,
2811
+ outcome: "failed",
2812
+ startingHead: workspaceLocalHead(),
2813
+ ...workspaceLocalHead() ? { localHead: workspaceLocalHead() } : {},
2814
+ rebaseCount: 0,
2815
+ diffSizeBytes: 0,
2816
+ gitStatus: workspaceGitStatus(),
2817
+ affectedProjects: [],
2818
+ affectedPaths: [],
2819
+ discardedPaths: [],
2820
+ localChangesDiscarded: false,
2821
+ error: error instanceof Error ? error.message : String(error)
2822
+ });
2823
+ const performWorkspaceSync = async (input) => {
2824
+ if (!workspaceRemoteUrl || !workspaceGitIdentity) throw new Error("Worker workspace configuration has not been received");
2825
+ const mounts = buildWorkspaceMounts();
2826
+ const remoteAuth = gitAuthForRemote(workspaceRemoteUrl, bearerAuthHeader);
2827
+ const credentialHelper = credentialHelperForRemote(workspaceRemoteUrl, bearerAuthHeader);
2828
+ if (input.resetToCanonical) {
2829
+ const reset = (0, import_workspace_git_sync.resetWorkspaceGit)({
2830
+ workspacePath: workspaceShadowRoot,
2831
+ remoteUrl: workspaceRemoteUrl,
2832
+ remoteAuth,
2833
+ credentialHelper,
2834
+ gitIdentity: workspaceGitIdentity,
2835
+ mounts
2836
+ });
2837
+ reseedProjectHeadsAfterWorkspaceReset();
2838
+ return {
2839
+ type: "workspace_sync",
2840
+ attemptId: input.attemptId,
2841
+ workerLabel: label,
2842
+ trigger: input.trigger,
2843
+ outcome: "reset",
2844
+ startingHead: reset.startingHead,
2845
+ ...reset.localHead ? { localHead: reset.localHead } : {},
2846
+ ...reset.remoteHead ? { publishedHead: reset.remoteHead } : {},
2847
+ rebaseCount: 0,
2848
+ diffSizeBytes: 0,
2849
+ gitStatus: workspaceGitStatus(),
2850
+ affectedProjects: affectedProjects(reset.discardedPaths),
2851
+ affectedPaths: reset.discardedPaths,
2852
+ discardedPaths: reset.discardedPaths,
2853
+ localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
2854
+ };
2855
+ }
2856
+ const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
2857
+ const result = await (0, import_workspace_git_sync.synchronizeWorkspaceGit)({
2858
+ attemptId: input.attemptId,
2859
+ workerLabel: label,
2860
+ workspacePath: workspaceShadowRoot,
2861
+ remoteUrl: workspaceRemoteUrl,
2862
+ remoteAuth,
2863
+ credentialHelper,
2864
+ gitIdentity: workspaceGitIdentity,
2865
+ mounts,
2866
+ commitDetail: input.confirmationReason ?? input.trigger.detail,
2867
+ allowLargeDiff: input.confirmedLargeDiff,
2868
+ skipMountMirror: outerRemediation,
2869
+ skipMountHydration: outerRemediation,
2870
+ afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
2871
+ pushChangedProjectHeads(activeMountIds, publishedHead);
2690
2872
  }
2691
- const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2692
- const branchPath = import_node_path.default.join(projectRoot, target.branchName);
2693
- const existed = hasNormalVisibleGitDir(branchPath);
2694
- const remote = visibleCheckoutRemoteFor({
2695
- baseUrl,
2696
- token,
2697
- projectId: manifest.projectId,
2698
- branchName: target.branchName,
2699
- manifest
2873
+ });
2874
+ if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
2875
+ refreshProjectHeadsAfterInboundWorkspace();
2876
+ }
2877
+ return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
2878
+ };
2879
+ const sendWorkspaceSyncResult = (requestId, result) => {
2880
+ try {
2881
+ sendWorkerMessage(ws, {
2882
+ type: "workspace_sync_result",
2883
+ ...requestId ? { requestId } : {},
2884
+ result
2700
2885
  });
2701
- ensureVisibleGitCheckout({
2702
- projectRoot,
2703
- branchName: target.branchName,
2704
- remoteUrl: remote.remoteUrl,
2705
- remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
2706
- persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
2707
- defaultBranch: manifest.defaultBranch || "main",
2708
- reconcileExistingOrigin: remote.reconcileExistingOrigin,
2709
- requireRemoteBranch: remote.requireRemoteBranch,
2710
- requiredBaseCommit: remote.requiredBaseCommit,
2711
- allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requiredBaseCommit),
2712
- clearPersistentAuth: !remote.persistAuth
2886
+ } catch (error) {
2887
+ process.stderr.write(
2888
+ `[r5d-worker] failed to send workspace sync result: ${error instanceof Error ? error.message : String(error)}
2889
+ `
2890
+ );
2891
+ }
2892
+ };
2893
+ const runWorkspaceSync = async (input) => {
2894
+ const attemptId = input.attemptId ?? crypto.randomUUID();
2895
+ const requestedAt = Date.now();
2896
+ let queueEnteredAt = requestedAt;
2897
+ let syncStartedAt = requestedAt;
2898
+ workspaceSyncRequestsInFlight += 1;
2899
+ let result;
2900
+ try {
2901
+ result = await workspaceSyncSingleFlight.runExclusive(async () => {
2902
+ queueEnteredAt = Date.now();
2903
+ syncStartedAt = Date.now();
2904
+ try {
2905
+ return await performWorkspaceSync({
2906
+ attemptId,
2907
+ trigger: input.trigger,
2908
+ confirmedLargeDiff: input.confirmedLargeDiff,
2909
+ confirmationReason: input.confirmationReason,
2910
+ resetToCanonical: input.resetToCanonical
2911
+ });
2912
+ } catch (error) {
2913
+ return failedWorkspaceSyncResult(attemptId, input.trigger, error);
2914
+ }
2713
2915
  });
2714
- if (!existed) created.push(target);
2916
+ } finally {
2917
+ workspaceSyncRequestsInFlight -= 1;
2715
2918
  }
2716
- return created;
2919
+ const finishedAt = Date.now();
2920
+ result = {
2921
+ ...result,
2922
+ telemetry: {
2923
+ totalMs: finishedAt - requestedAt,
2924
+ queueMs: queueEnteredAt - requestedAt,
2925
+ prepareMs: 0,
2926
+ synchronizeMs: finishedAt - syncStartedAt
2927
+ }
2928
+ };
2929
+ if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result);
2930
+ return result;
2717
2931
  };
2718
- const schedulePendingManifestCheckoutHydration = (delayMs = 0) => {
2719
- if (workspaceManifestHydrationTimer || workspaceManifestHydrationInFlight || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2932
+ const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
2933
+ pendingAutomaticTrigger = trigger;
2934
+ if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight) {
2720
2935
  return;
2721
2936
  }
2722
- workspaceManifestHydrationTimer = setTimeout(
2937
+ if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
2938
+ workspaceAutomaticTimer = setTimeout(
2723
2939
  () => {
2724
- workspaceManifestHydrationTimer = void 0;
2725
- if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || pendingManifestCheckouts.size === 0 || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2726
- if (pendingManifestCheckouts.size > 0 && currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) {
2727
- schedulePendingManifestCheckoutHydration(1e3);
2940
+ workspaceAutomaticTimer = void 0;
2941
+ const scheduledTrigger = pendingAutomaticTrigger;
2942
+ pendingAutomaticTrigger = void 0;
2943
+ if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
2944
+ automaticSyncInFlight = true;
2945
+ void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
2946
+ if (result.outcome === "failed") {
2947
+ pendingAutomaticTrigger = scheduledTrigger;
2728
2948
  }
2729
- return;
2730
- }
2731
- workspaceManifestHydrationInFlight = true;
2732
- void workspaceSyncSingleFlight.runExclusive(async () => {
2733
- if (!workspaceReady || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
2734
- return;
2735
- }
2736
- const targets = [...pendingManifestCheckouts.values()];
2737
- ensureVisibleWorktrees(targets, { strictCanonicalRevalidation: true });
2738
- for (const target of targets) {
2739
- const manifest = manifestByProjectId.get(target.projectId);
2740
- if (!manifest || !manifest.branches.includes(target.branchName)) continue;
2741
- const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, target.projectId, manifestByProjectId), target.branchName);
2742
- if (hasNormalVisibleGitDir(branchPath)) {
2743
- pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
2744
- }
2745
- }
2746
- }).then(() => sendWorkspaceReady()).catch((error) => {
2747
- process.stderr.write(
2748
- `[r5d-worker] pending manifest checkout hydration failed: ${error instanceof Error ? error.message : String(error)}
2749
- `
2750
- );
2751
2949
  }).finally(() => {
2752
- workspaceManifestHydrationInFlight = false;
2950
+ automaticSyncInFlight = false;
2753
2951
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
2754
- if (pendingManifestCheckouts.size > 0) schedulePendingManifestCheckoutHydration(1e3);
2952
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
2953
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
2954
+ }
2755
2955
  });
2756
2956
  },
2757
2957
  Math.max(0, delayMs)
2758
2958
  );
2759
- workspaceManifestHydrationTimer.unref();
2959
+ workspaceAutomaticTimer.unref();
2760
2960
  };
2761
- const runRequestedWorkspaceSync = async (authority, trigger, overrides = {}) => {
2762
- const requestedAt = Date.now();
2763
- let queueEnteredAt = requestedAt;
2764
- let prepareStartedAt = requestedAt;
2765
- let synchronizeStartedAt = requestedAt;
2766
- let synchronizeFinishedAt = requestedAt;
2961
+ const markWorkspaceDirty = (trigger, force = false) => {
2962
+ scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
2963
+ };
2964
+ const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
2965
+ const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
2966
+ target,
2967
+ projectsRoot,
2968
+ workspaceShadowRoot,
2969
+ projectConfigById
2970
+ });
2971
+ const configureWorkerWorkspace = async (message) => {
2767
2972
  workspaceSyncRequestsInFlight += 1;
2768
2973
  try {
2769
- let pendingTargets = [];
2770
- const result = await workspaceSyncSingleFlight.runExclusive(async () => {
2771
- queueEnteredAt = Date.now();
2772
- const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2773
- const syncProjects = (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], trigger);
2774
- if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2775
- const allowedCheckoutKeys = new Set(
2776
- syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
2777
- );
2778
- const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
2779
- (target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2974
+ return await workspaceSyncSingleFlight.runExclusive(async () => {
2975
+ workspaceConfigured = false;
2976
+ configureGitHubAuth(message.githubCredential);
2977
+ visibleGitIdentity = message.gitIdentity;
2978
+ workspaceGitIdentity = message.gitIdentity;
2979
+ workspaceRemoteUrl = message.workspaceRemoteUrl;
2980
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
2981
+ desiredProjects: message.projects
2982
+ });
2983
+ (0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, message.projects);
2984
+ const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
2985
+ ...project,
2986
+ branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
2987
+ }));
2988
+ const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
2989
+ for (const existingProject of projectConfigById.values()) {
2990
+ const nextProject = nextProjectConfigById.get(existingProject.projectId);
2991
+ if (!nextProject) {
2992
+ stageProjectDeletion(existingProject);
2993
+ continue;
2994
+ }
2995
+ const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
2996
+ for (const existingBranch of existingProject.branches) {
2997
+ if (!nextBranches.has(existingBranch.branchName)) {
2998
+ stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
2999
+ }
3000
+ }
3001
+ }
3002
+ projectConfigById.clear();
3003
+ for (const project of effectiveProjects) projectConfigById.set(project.projectId, project);
3004
+ const configuredKeys = new Set(
3005
+ effectiveProjects.flatMap((project) => project.branches.map(({ branchName }) => projectBranchKey(project.projectId, branchName)))
2780
3006
  );
2781
- prepareStartedAt = Date.now();
2782
- let checkoutTargets = [];
2783
- if (shouldEnsureCheckouts) {
2784
- checkoutTargets = workspaceSyncCheckoutTargets({
2785
- projects: syncProjects,
2786
- pendingTargets: scopedPendingTargets,
2787
- includeAllManifestCheckouts: trigger.type === "connect",
2788
- ...trigger.canonicalCheckoutOnly && trigger.projectId && trigger.branchName ? { canonicalCheckoutOnly: { projectId: trigger.projectId, branchName: trigger.branchName } } : {}
2789
- });
3007
+ for (const key of pendingCheckouts.keys()) {
3008
+ if (!configuredKeys.has(key)) pendingCheckouts.delete(key);
2790
3009
  }
2791
- pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
2792
- const createdCheckouts = ensureVisibleWorktrees(checkoutTargets, {
2793
- strictCanonicalRevalidation: true
2794
- });
2795
- const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2796
- for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2797
- const key = manifestCheckoutKey(target.projectId, target.branchName);
2798
- if (allowedCheckoutKeys.has(key)) newVisibleCheckoutByKey.set(key, target);
3010
+ for (const projectId of [...readyProjectIds]) {
3011
+ if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
2799
3012
  }
2800
- const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2801
- const input = workspaceSyncInput(trigger, {
2802
- ...overrides,
2803
- ...shouldEnsureCheckouts && !trigger.canonicalCheckoutOnly ? { visibleBaselineHead: workspaceConvergence.visibleBaselineHead() } : {},
2804
- ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
3013
+ const remoteAuth = gitAuthForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
3014
+ const credentialHelper = credentialHelperForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
3015
+ (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
3016
+ workspacePath: workspaceShadowRoot,
3017
+ remoteUrl: message.workspaceRemoteUrl,
3018
+ remoteAuth,
3019
+ credentialHelper,
3020
+ gitIdentity: message.gitIdentity
2805
3021
  });
2806
- synchronizeStartedAt = Date.now();
2807
- let synchronized;
2808
- try {
2809
- synchronized = await (0, import_workspace_sync.synchronizeWorkspace)(input);
2810
- } finally {
2811
- synchronizeFinishedAt = Date.now();
3022
+ pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
3023
+ stageDurableWorkspaceDeletions();
3024
+ const mountsBeforeGit = buildWorkspaceMounts();
3025
+ (0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
3026
+ workspaceShadowRoot,
3027
+ mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath))
3028
+ );
3029
+ ensureConfiguredProjects();
3030
+ let result = await performWorkspaceSync({
3031
+ attemptId: crypto.randomUUID(),
3032
+ trigger: { type: "connect" }
3033
+ });
3034
+ if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
3035
+ const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
3036
+ if (staleAfterInbound.length > 0) {
3037
+ pruneStaleOuterWorkspaceEntries(staleAfterInbound);
3038
+ stageDurableWorkspaceDeletions();
3039
+ result = await performWorkspaceSync({
3040
+ attemptId: crypto.randomUUID(),
3041
+ trigger: { type: "connect" }
3042
+ });
3043
+ }
2812
3044
  }
2813
- return synchronized;
2814
- });
2815
- const completedAt = Date.now();
2816
- const resultWithTelemetry = {
2817
- ...result,
2818
- telemetry: {
2819
- totalMs: completedAt - requestedAt,
2820
- queueMs: queueEnteredAt - requestedAt,
2821
- prepareMs: synchronizeStartedAt - prepareStartedAt,
2822
- synchronizeMs: synchronizeFinishedAt - synchronizeStartedAt
3045
+ const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
3046
+ if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
3047
+ const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
3048
+ (deletion) => deletion.kind === "project" && !deletion.projectDeleted ? [deletion.tombstoneId] : []
3049
+ );
3050
+ if (layoutCleanupIds.length > 0) {
3051
+ projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
3052
+ tombstoneIds: layoutCleanupIds,
3053
+ publishedHead
3054
+ });
3055
+ }
2823
3056
  }
2824
- };
2825
- if (resultWithTelemetry.outcome === "no_change" || resultWithTelemetry.outcome === "published" || resultWithTelemetry.outcome === "updated" || resultWithTelemetry.outcome === "conflict_reset") {
2826
- for (const target of pendingTargets) {
2827
- const key = manifestCheckoutKey(target.projectId, target.branchName);
2828
- if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
3057
+ workspaceConfigured = true;
3058
+ const pending = [...pendingCheckouts.values()].sort(
3059
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
3060
+ );
3061
+ return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
3062
+ });
3063
+ } catch (error) {
3064
+ workspaceConfigured = import_node_fs.default.existsSync(import_node_path.default.join(workspaceShadowRoot, ".git"));
3065
+ for (const project of message.projects) {
3066
+ for (const branch of project.branches) {
3067
+ pendingCheckouts.set(projectBranchKey(project.projectId, branch.branchName), {
3068
+ projectId: project.projectId,
3069
+ branchName: branch.branchName
3070
+ });
2829
3071
  }
2830
3072
  }
2831
- sendWorkspaceSyncResult(authority, resultWithTelemetry);
2832
- return resultWithTelemetry;
2833
- } catch (error) {
2834
- const completedAt = Date.now();
2835
- const result = {
2836
- type: "workspace_sync",
2837
- attemptId: overrides.attemptId ?? crypto.randomUUID(),
2838
- workerLabel: label,
2839
- trigger,
2840
- outcome: "failed",
2841
- startingHead: null,
2842
- rebaseCount: 0,
2843
- diffSizeBytes: 0,
2844
- gitStatus: "",
2845
- affectedProjects: [],
2846
- affectedPaths: [],
2847
- discardedPaths: [],
2848
- localChangesDiscarded: false,
2849
- telemetry: {
2850
- totalMs: completedAt - requestedAt,
2851
- queueMs: queueEnteredAt - requestedAt,
2852
- prepareMs: Math.max(0, synchronizeStartedAt - prepareStartedAt),
2853
- synchronizeMs: Math.max(0, synchronizeFinishedAt - synchronizeStartedAt)
2854
- },
2855
- error: error instanceof Error ? error.message : String(error)
3073
+ return {
3074
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
3075
+ pending: [...pendingCheckouts.values()],
3076
+ aheadOfOriginBranches: []
2856
3077
  };
2857
- sendWorkspaceSyncResult(authority, result);
2858
- return result;
2859
3078
  } finally {
2860
3079
  workspaceSyncRequestsInFlight -= 1;
2861
3080
  }
@@ -2872,17 +3091,26 @@ async function startWorker(options) {
2872
3091
  const handleGracefulShutdown = () => {
2873
3092
  if (shutdownAfterClose) return;
2874
3093
  shutdownAfterClose = true;
2875
- void (0, import_workspace_convergence.waitForWorkspacePublicationOnShutdown)({
2876
- snapshot: () => workspaceConvergence.snapshot(),
2877
- forcePublication: () => armWorkspacePublication({ type: "manual", detail: "graceful worker shutdown" }, { delayMs: 0 })
2878
- }).then((outcome) => {
2879
- process.stdout.write(`[r5d-worker] graceful workspace publication ${outcome}
2880
- `);
3094
+ if (workspaceAutomaticTimer) {
3095
+ clearTimeout(workspaceAutomaticTimer);
3096
+ workspaceAutomaticTimer = void 0;
3097
+ }
3098
+ void (async () => {
3099
+ if (workspaceConfigured && !activeWorkspaceIncidentId) {
3100
+ await runWorkspaceSync({
3101
+ trigger: { type: "manual", detail: "graceful worker shutdown" }
3102
+ });
3103
+ }
3104
+ await workspaceSyncSingleFlight.afterCurrent();
2881
3105
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
2882
3106
  ws.close(1e3, "Worker shutting down");
2883
3107
  } else {
2884
3108
  process.exit(0);
2885
3109
  }
3110
+ })().catch((error) => {
3111
+ process.stderr.write(`[r5d-worker] graceful workspace sync failed: ${error instanceof Error ? error.message : String(error)}
3112
+ `);
3113
+ ws.close(1011, "Worker shutdown sync failed");
2886
3114
  });
2887
3115
  };
2888
3116
  process.on("SIGTERM", handleGracefulShutdown);
@@ -2915,11 +3143,7 @@ async function startWorker(options) {
2915
3143
  r5dctlVersion: (0, import_cli_update.readInstalledCliVersion)("r5dctl"),
2916
3144
  capabilities: {
2917
3145
  updateClis: true,
2918
- canonicalResolverCheckout: true,
2919
3146
  browserPortForwarding: true,
2920
- globalWorkspaceSyncLeaseV1: true,
2921
- eventualWorkspaceSyncV1: true,
2922
- workspaceIncidentCandidatePinV1: true,
2923
3147
  execStdinV1: true
2924
3148
  },
2925
3149
  projectRoot: projectsRoot,
@@ -2946,302 +3170,153 @@ async function startWorker(options) {
2946
3170
  if (message.type === "connected") {
2947
3171
  return;
2948
3172
  }
2949
- if (message.type === "workspace_manifest") {
2950
- const revisionToken = workspaceManifestRevision.begin(message.revision);
2951
- workspaceReady = false;
2952
- const manifestFilesystem = (0, import_workspace_manifest_admission.inspectWorkspaceManifestFilesystem)({
2953
- projectsRoot,
2954
- workspaceShadowRoot,
2955
- projects: message.projects
2956
- });
2957
- const completed = await workspaceSyncSingleFlight.runExclusive(async () => {
2958
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2959
- const firstBootstrap = manifestFilesystem.firstBootstrap;
2960
- configureGitHubAuth(message.githubCredential);
2961
- visibleGitIdentity = message.gitIdentity;
2962
- workspaceRemoteUrl = message.workspaceRemoteUrl;
2963
- manifestByProjectId.clear();
2964
- for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2965
- const currentCheckouts = allManifestCheckouts();
2966
- const currentCheckoutKeys = new Set(
2967
- currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2968
- );
2969
- for (const key of pendingManifestCheckouts.keys()) {
2970
- if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2971
- }
2972
- for (const checkout of currentCheckouts) {
2973
- const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2974
- const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2975
- if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2976
- }
2977
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2978
- const created = firstBootstrap ? ensureVisibleWorktrees(currentCheckouts, { strictCanonicalRevalidation: true }) : [];
2979
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2980
- const bootstrapped = await (0, import_workspace_sync.convergeWorkspaceHead)(workspaceSyncInput({ type: "connect" }, { newVisibleCheckouts: created }), {
2981
- // An existing checkout becomes routable after a fetch. Its
2982
- // visible tree converges asynchronously; first bootstrap alone
2983
- // must materialize the canonical snapshot before readiness.
2984
- // Re-check the incident immediately before convergence so a
2985
- // concurrently delivered fence forces fetch-only behavior.
2986
- hydrateVisible: firstBootstrap && !activeWorkspaceIncidentId
2987
- });
2988
- if (!workspaceManifestRevision.isCurrent(revisionToken)) return false;
2989
- if ((0, import_workspace_convergence.workspaceBootstrapEstablishesVisibleBaseline)({
2990
- hydrated: bootstrapped.hydrated,
2991
- firstBootstrap,
2992
- incidentActive: Boolean(activeWorkspaceIncidentId),
2993
- currentLocalHead: workspaceConvergence.snapshot().localHead,
2994
- currentVisibleBaselineHead: workspaceConvergence.visibleBaselineHead()
2995
- })) {
2996
- workspaceConvergence.setLocalHead(bootstrapped.localHead);
2997
- }
2998
- workspaceConvergence.observeCanonicalHead(bootstrapped.canonicalHead);
2999
- for (const checkout of currentCheckouts) {
3000
- const manifest = manifestByProjectId.get(checkout.projectId);
3001
- if (!manifest) continue;
3002
- const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
3003
- if (hasNormalVisibleGitDir(branchPath)) {
3004
- pendingManifestCheckouts.delete(manifestCheckoutKey(checkout.projectId, checkout.branchName));
3005
- }
3006
- }
3007
- process.stdout.write(`[r5d-worker] workspace manifest: ${message.projects.length} projects
3008
- `);
3009
- return true;
3173
+ if (message.type === "workspace_config") {
3174
+ const configured = await configureWorkerWorkspace(message);
3175
+ sendWorkerMessage(ws, {
3176
+ type: "workspace_configured",
3177
+ requestId: message.requestId,
3178
+ result: configured.result,
3179
+ pendingCheckouts: configured.pending,
3180
+ aheadOfOriginBranches: configured.aheadOfOriginBranches
3010
3181
  });
3011
- if (!completed || !workspaceManifestRevision.complete(revisionToken)) return;
3012
- workspaceReady = true;
3013
- sendWorkspaceReady();
3014
- schedulePendingManifestCheckoutHydration();
3015
- if (!workspaceSafetyScan) {
3016
- workspaceSafetyScan = setInterval(() => {
3017
- if (!workspaceReady || !workspaceRemoteUrl || activeWorkspaceIncidentId || pendingPublication || workspaceSafetyScanInFlight) {
3018
- return;
3019
- }
3020
- workspaceSafetyScanInFlight = true;
3021
- void (async () => {
3022
- const before = workspaceConvergence.snapshot();
3023
- const hydrateVisible = before.dirtyGeneration === before.publishedGeneration;
3024
- const converged = await workspaceSyncSingleFlight.runExclusive(
3025
- () => (0, import_workspace_sync.convergeWorkspaceHead)(workspaceSyncInput({ type: "periodic", detail: "idle convergence scan" }), {
3026
- hydrateVisible
3027
- })
3028
- );
3029
- workspaceConvergence.observeCanonicalHead(converged.canonicalHead);
3030
- if (converged.hydrated) workspaceConvergence.setLocalHead(converged.localHead);
3031
- if (converged.localChanges && hydrateVisible) {
3032
- markWorkspaceDirty({ type: "periodic", detail: "idle safety scan" });
3033
- }
3034
- sendWorkspaceReady();
3035
- })().catch((error) => {
3036
- process.stderr.write(
3037
- `[r5d-worker] idle workspace safety scan failed: ${error instanceof Error ? error.message : String(error)}
3182
+ process.stdout.write(
3183
+ `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
3038
3184
  `
3039
- );
3040
- }).finally(() => {
3041
- workspaceSafetyScanInFlight = false;
3042
- heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
3043
- });
3044
- }, import_workspace_convergence.WORKSPACE_IDLE_SAFETY_SCAN_MS);
3045
- workspaceSafetyScan.unref();
3185
+ );
3186
+ if (!workspacePeriodicTimer) {
3187
+ workspacePeriodicTimer = setInterval(() => {
3188
+ scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
3189
+ }, WORKSPACE_GIT_PERIODIC_MS);
3190
+ workspacePeriodicTimer.unref();
3046
3191
  }
3047
- pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
3048
- void convergeAvailableWorkspaceHead();
3049
- if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3050
- armWorkspacePublication({ type: "periodic", detail: "resume dirty generation after reconnect" }, { delayMs: 0 });
3192
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
3193
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
3051
3194
  }
3052
3195
  return;
3053
3196
  }
3054
3197
  if (message.type === "sync_workspace") {
3055
- await runRequestedWorkspaceSync(
3056
- {
3057
- requestId: message.requestId,
3058
- intentId: message.intentId,
3059
- leaseEpoch: message.leaseEpoch,
3060
- leaseToken: message.leaseToken
3061
- },
3062
- message.trigger,
3063
- {
3064
- attemptId: message.attemptId,
3065
- quarantineRef: message.quarantineRef,
3066
- confirmedLargeDiff: message.confirmedLargeDiff,
3067
- confirmationReason: message.confirmationReason,
3068
- expectedIncidentCandidateHead: message.expectedIncidentCandidateHead,
3069
- resetToCanonical: message.resetToCanonical,
3070
- skipVisibleMirror: message.skipVisibleMirror
3071
- }
3072
- );
3073
- return;
3074
- }
3075
- if (message.type === "workspace_head_available") {
3076
- workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3077
- pendingWorkspaceHead = message.canonicalHead;
3078
- sendWorkspaceReady();
3079
- void convergeAvailableWorkspaceHead();
3080
- return;
3081
- }
3082
- if (message.type === "workspace_publication_now") {
3083
- armWorkspacePublication(message.trigger, { delayMs: 0, requestId: message.requestId });
3198
+ await runWorkspaceSync({
3199
+ requestId: message.requestId,
3200
+ attemptId: message.attemptId,
3201
+ trigger: message.trigger,
3202
+ confirmedLargeDiff: message.confirmedLargeDiff,
3203
+ confirmationReason: message.confirmationReason,
3204
+ resetToCanonical: message.resetToCanonical
3205
+ });
3084
3206
  return;
3085
3207
  }
3086
- if (message.type === "workspace_publication_authority") {
3087
- const publication = pendingPublication;
3088
- if (!publication || publication.requestId !== message.requestId) {
3089
- process.stderr.write(`[r5d-worker] ignoring publication authority for unknown request ${message.requestId}
3090
- `);
3091
- return;
3092
- }
3093
- if (message.status !== "granted") {
3094
- pendingPublication = void 0;
3095
- workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3096
- const applyBlockedResponse = message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId);
3097
- if (applyBlockedResponse) {
3098
- workspaceConvergence.block();
3099
- if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3100
- } else {
3101
- workspaceConvergence.resetTransientPublicationState();
3102
- workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3103
- if (message.status === "busy") {
3104
- armWorkspacePublication(publication.trigger, {
3105
- delayMs: message.retryAfterMs ?? 1e3,
3106
- requestId: publication.requestId
3107
- });
3108
- } else if (!activeWorkspaceIncidentId) {
3109
- const explicit = explicitWorkspacePublications.next();
3110
- if (explicit) {
3111
- armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3112
- } else if (workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3113
- armWorkspacePublication(publication.trigger, { delayMs: 0 });
3114
- }
3115
- }
3116
- }
3117
- sendWorkspaceReady();
3118
- return;
3119
- }
3120
- publication.attemptId = message.attemptId;
3121
- publication.authorityToken = message.authorityToken;
3122
- publication.workInFlight = true;
3123
- pendingPublication = publication;
3124
- if (currentWorkerSocket !== ws) {
3125
- publication.workInFlight = false;
3126
- workspaceConvergence.resetTransientPublicationState();
3127
- return;
3128
- }
3129
- workspaceConvergence.beginPreparing();
3130
- sendWorkspaceReady();
3131
- let prepareStartedAt = Date.now();
3132
- let synchronizeStartedAt = prepareStartedAt;
3133
- let synchronizeStarted = false;
3134
- let result;
3208
+ if (message.type === "create_project_branch") {
3135
3209
  try {
3136
- result = await workspaceSyncSingleFlight.runExclusive(async () => {
3137
- prepareStartedAt = Date.now();
3138
- const pendingTargets = [...pendingManifestCheckouts.values()];
3139
- const createdCheckouts = ensureVisibleWorktrees(pendingTargets, { strictCanonicalRevalidation: true });
3140
- synchronizeStartedAt = Date.now();
3141
- synchronizeStarted = true;
3142
- const prepared = await (0, import_workspace_sync.synchronizeWorkspace)(
3143
- workspaceSyncInput(publication.trigger, {
3144
- attemptId: message.attemptId,
3145
- quarantineRef: message.quarantineRef,
3146
- expectedCanonicalHead: message.expectedHead,
3147
- visibleBaselineHead: publication.visibleBaselineHead,
3148
- newVisibleCheckouts: [...createdCheckouts, ...pendingTargets]
3149
- })
3150
- );
3151
- for (const target of pendingTargets) {
3152
- pendingManifestCheckouts.delete(manifestCheckoutKey(target.projectId, target.branchName));
3210
+ projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch({
3211
+ projectId: message.projectId,
3212
+ branchName: message.targetBranch
3213
+ });
3214
+ const created = await workspaceSyncSingleFlight.runMutation(() => {
3215
+ const project = projectConfigById.get(message.projectId);
3216
+ if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3217
+ if (!readyProjectIds.has(project.projectId)) {
3218
+ throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
3153
3219
  }
3154
- return prepared.outcome === "updated" ? { ...prepared, outcome: "no_change" } : prepared;
3220
+ if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
3221
+ throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
3222
+ }
3223
+ if (project.branches.some(({ branchName }) => branchName === message.targetBranch)) {
3224
+ throw new Error(`Project branch ${message.targetBranch} already exists`);
3225
+ }
3226
+ const createdBranch = (0, import_project_worktrees.createLinkedProjectBranch)({
3227
+ projectRoot: configuredProjectRoot(projectsRoot, project),
3228
+ sourceBranchName: message.sourceBranch,
3229
+ branchName: message.targetBranch
3230
+ });
3231
+ projectConfigById.set(project.projectId, {
3232
+ ...project,
3233
+ branches: [
3234
+ ...project.branches,
3235
+ {
3236
+ branchName: message.targetBranch,
3237
+ sourceBranchName: message.sourceBranch,
3238
+ baseCommitHash: createdBranch.baseCommitHash
3239
+ }
3240
+ ]
3241
+ });
3242
+ lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
3243
+ return createdBranch;
3155
3244
  });
3156
- } catch (error) {
3157
- if (!synchronizeStarted) synchronizeStartedAt = Date.now();
3158
- result = {
3159
- type: "workspace_sync",
3160
- attemptId: message.attemptId,
3161
- workerLabel: label,
3162
- trigger: publication.trigger,
3163
- outcome: "failed",
3164
- startingHead: message.expectedHead,
3165
- expectedHead: message.expectedHead,
3166
- rebaseCount: 0,
3167
- diffSizeBytes: 0,
3168
- gitStatus: "",
3169
- affectedProjects: [],
3170
- affectedPaths: [],
3171
- discardedPaths: [],
3172
- localChangesDiscarded: false,
3173
- error: error instanceof Error ? error.message : String(error)
3174
- };
3175
- }
3176
- const finishedAt = Date.now();
3177
- result = {
3178
- ...result,
3179
- dirtyGeneration: publication.dirtyGeneration,
3180
- telemetry: (0, import_workspace_convergence.workspacePublicationTelemetry)({
3181
- requestedAt: publication.requestedAt,
3182
- prepareStartedAt,
3183
- synchronizeStartedAt,
3184
- finishedAt
3185
- })
3186
- };
3187
- workspaceConvergence.beginSubmitting();
3188
- sendWorkspaceReady();
3189
- try {
3190
- ws.send(
3191
- JSON.stringify({
3192
- type: "workspace_publication_result",
3193
- requestId: publication.requestId,
3194
- attemptId: message.attemptId,
3195
- authorityToken: message.authorityToken,
3196
- dirtyGeneration: publication.dirtyGeneration,
3197
- result
3198
- })
3245
+ sendWorkerMessage(ws, {
3246
+ type: "operation_result",
3247
+ requestId: message.requestId,
3248
+ result: {
3249
+ type: "create_project_branch",
3250
+ branchName: message.targetBranch,
3251
+ baseCommitHash: created.baseCommitHash
3252
+ }
3253
+ });
3254
+ markWorkspaceDirty(
3255
+ {
3256
+ type: "manual",
3257
+ projectId: message.projectId,
3258
+ branchName: message.targetBranch,
3259
+ detail: `created project branch from ${message.sourceBranch}`
3260
+ },
3261
+ true
3199
3262
  );
3200
3263
  } catch (error) {
3201
- workspaceConvergence.resetTransientPublicationState();
3202
- throw error;
3203
- } finally {
3204
- publication.workInFlight = false;
3205
- if (currentWorkerSocket !== ws) workspaceConvergence.resetTransientPublicationState();
3264
+ sendWorkerMessage(ws, {
3265
+ type: "operation_result",
3266
+ requestId: message.requestId,
3267
+ error: error instanceof Error ? error.message : String(error)
3268
+ });
3206
3269
  }
3207
3270
  return;
3208
3271
  }
3209
- if (message.type === "workspace_publication_completed") {
3210
- const publication = pendingPublication;
3211
- if (!publication || publication.requestId !== message.requestId || publication.attemptId !== message.attemptId) {
3212
- process.stderr.write(`[r5d-worker] ignoring completion for unknown publication ${message.requestId}
3213
- `);
3214
- return;
3215
- }
3216
- pendingPublication = void 0;
3217
- workspaceConvergence.observeCanonicalHead(message.canonicalHead);
3218
- if (message.status === "published" || message.status === "no_change") {
3219
- workspaceConvergence.complete({
3220
- generation: Math.min(message.publishedGeneration ?? publication.dirtyGeneration, publication.dirtyGeneration),
3221
- canonicalHead: message.canonicalHead,
3222
- published: true
3272
+ if (message.type === "delete_project_branch") {
3273
+ try {
3274
+ const deletionKey = projectBranchKey(message.projectId, message.branchName);
3275
+ projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
3276
+ projectId: message.projectId,
3277
+ branchName: message.branchName
3223
3278
  });
3224
- } else if (message.status === "blocked" && workspaceIncidentOrdering.permitsBlockedResponse(message.incidentId)) {
3225
- if (message.visibleBaselineHead) workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3226
- workspaceConvergence.block();
3227
- if (message.incidentId) activeWorkspaceIncidentId = message.incidentId;
3228
- } else {
3229
- workspaceConvergence.resetTransientPublicationState();
3230
- workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3231
- }
3232
- sendWorkspaceReady();
3233
- const explicit = message.status === "retry" && publication.explicit ? { requestId: publication.requestId, trigger: publication.trigger } : explicitWorkspacePublications.next();
3234
- if (explicit) {
3235
- armWorkspacePublication(explicit.trigger, {
3236
- delayMs: message.retryAfterMs ?? 0,
3237
- requestId: explicit.requestId
3279
+ await workspaceSyncSingleFlight.runMutation(() => {
3280
+ const project = projectConfigById.get(message.projectId);
3281
+ if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
3282
+ if (pendingMirrorDeletes.has(deletionKey)) return;
3283
+ stageProjectBranchDeletion(project, message.branchName, true);
3284
+ projectConfigById.set(project.projectId, {
3285
+ ...project,
3286
+ branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
3287
+ });
3288
+ });
3289
+ const result = await runWorkspaceSync({
3290
+ trigger: {
3291
+ type: "manual",
3292
+ projectId: message.projectId,
3293
+ branchName: message.branchName,
3294
+ detail: "deleted project branch"
3295
+ },
3296
+ confirmedLargeDiff: true,
3297
+ confirmationReason: "explicit project branch deletion",
3298
+ sendResult: false
3299
+ });
3300
+ if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
3301
+ throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
3302
+ }
3303
+ if (pendingMirrorDeletes.has(deletionKey)) {
3304
+ throw new Error("Project branch deletion did not remove the hidden mirror ref");
3305
+ }
3306
+ sendWorkerMessage(ws, {
3307
+ type: "operation_result",
3308
+ requestId: message.requestId,
3309
+ result: {
3310
+ type: "delete_project_branch",
3311
+ branchName: message.branchName
3312
+ }
3313
+ });
3314
+ } catch (error) {
3315
+ sendWorkerMessage(ws, {
3316
+ type: "operation_result",
3317
+ requestId: message.requestId,
3318
+ error: error instanceof Error ? error.message : String(error)
3238
3319
  });
3239
- } else if (message.status === "retry" || message.status === "failed" || workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3240
- armWorkspacePublication(publication.trigger, { delayMs: message.retryAfterMs ?? 1e3 });
3241
- }
3242
- if (message.canonicalHead !== workspaceConvergence.snapshot().localHead) {
3243
- pendingWorkspaceHead = message.canonicalHead;
3244
- void convergeAvailableWorkspaceHead();
3245
3320
  }
3246
3321
  return;
3247
3322
  }
@@ -3272,31 +3347,11 @@ async function startWorker(options) {
3272
3347
  return;
3273
3348
  }
3274
3349
  if (message.type === "workspace_incident_updated") {
3275
- workspaceIncidentOrdering.observe(message);
3276
3350
  const previousIncidentId = activeWorkspaceIncidentId;
3277
- const nextIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
3278
- if (message.incidentId && (previousIncidentId === message.incidentId || nextIncidentId === message.incidentId) && message.originWorkerLabel === label && message.visibleBaselineHead) {
3279
- workspaceConvergence.setVisibleBaselineHead(message.visibleBaselineHead);
3280
- }
3281
- activeWorkspaceIncidentId = nextIncidentId;
3282
- const releasedHead = (0, import_workspace_incident_state.releasePendingWorkspaceHead)(
3283
- previousIncidentId,
3284
- activeWorkspaceIncidentId,
3285
- message.status === "resolved" || message.status === "confirmed" || message.status === "reset" ? message.incidentId : null
3286
- );
3287
- workspaceConvergence.observeIncidentState(Boolean(activeWorkspaceIncidentId));
3288
- if (releasedHead.fetchAuthoritativeHead) {
3289
- pendingWorkspaceHead = workspaceConvergence.snapshot().desiredCanonicalHead;
3290
- void convergeAvailableWorkspaceHead();
3351
+ activeWorkspaceIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
3352
+ if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
3353
+ scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
3291
3354
  }
3292
- const explicit = !activeWorkspaceIncidentId ? explicitWorkspacePublications.next() : void 0;
3293
- if (explicit) {
3294
- armWorkspacePublication(explicit.trigger, { delayMs: 0, requestId: explicit.requestId });
3295
- } else if (!activeWorkspaceIncidentId && workspaceConvergence.snapshot().dirtyGeneration > workspaceConvergence.snapshot().publishedGeneration) {
3296
- armWorkspacePublication({ type: "periodic", detail: "resume after workspace incident" }, { delayMs: 0 });
3297
- }
3298
- if (!activeWorkspaceIncidentId) schedulePendingManifestCheckoutHydration();
3299
- sendWorkspaceReady();
3300
3355
  return;
3301
3356
  }
3302
3357
  if (message.type === "exec_terminal_ack") {
@@ -3487,7 +3542,7 @@ async function startWorker(options) {
3487
3542
  const activePty = activePtys.get(message.ptyId);
3488
3543
  closePty(message);
3489
3544
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
3490
- armWorkspacePublication({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, { delayMs: 0 });
3545
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true);
3491
3546
  }
3492
3547
  return;
3493
3548
  }
@@ -3584,7 +3639,7 @@ async function startWorker(options) {
3584
3639
  });
3585
3640
  return;
3586
3641
  }
3587
- if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
3642
+ 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") {
3588
3643
  try {
3589
3644
  const result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
3590
3645
  const resolvedTarget = resolveMessageTarget(message.target);
@@ -3644,20 +3699,13 @@ async function startWorker(options) {
3644
3699
  process.off("SIGTERM", handleGracefulShutdown);
3645
3700
  process.off("SIGINT", handleGracefulShutdown);
3646
3701
  stopHeartbeatWatchdog();
3647
- if (workspacePublicationTimer) {
3648
- clearTimeout(workspacePublicationTimer);
3649
- workspacePublicationTimer = void 0;
3650
- }
3651
- if (!pendingPublication?.workInFlight) {
3652
- workspaceConvergence.resetTransientPublicationState();
3653
- }
3654
- if (workspaceSafetyScan) {
3655
- clearInterval(workspaceSafetyScan);
3656
- workspaceSafetyScan = void 0;
3702
+ if (workspaceAutomaticTimer) {
3703
+ clearTimeout(workspaceAutomaticTimer);
3704
+ workspaceAutomaticTimer = void 0;
3657
3705
  }
3658
- if (workspaceManifestHydrationTimer) {
3659
- clearTimeout(workspaceManifestHydrationTimer);
3660
- workspaceManifestHydrationTimer = void 0;
3706
+ if (workspacePeriodicTimer) {
3707
+ clearInterval(workspacePeriodicTimer);
3708
+ workspacePeriodicTimer = void 0;
3661
3709
  }
3662
3710
  if (terminalReplayTimer) {
3663
3711
  clearInterval(terminalReplayTimer);
@@ -3751,24 +3799,22 @@ if (isCliEntrypoint()) {
3751
3799
  0 && (module.exports = {
3752
3800
  describeWorkerSessionTarget,
3753
3801
  editWorkerTextFile,
3754
- ensureVisibleGitCheckout,
3755
3802
  findWorkerFiles,
3756
3803
  githubCliEnv,
3757
3804
  grepWorkerFiles,
3758
3805
  isArtifactEnvPath,
3806
+ listWorkerCodeDirectory,
3759
3807
  listWorkerDirectory,
3760
3808
  prepareArtifactEnvForShell,
3761
3809
  prepareBuiltInToolPathsForTarget,
3762
3810
  preparePlanEnvForShell,
3763
3811
  prepareShellEnvForTarget,
3812
+ readWorkerCodeFile,
3764
3813
  readWorkerImageFile,
3765
3814
  readWorkerTextFile,
3766
3815
  resolveHostShell,
3767
3816
  resolveWorkerFilePath,
3768
3817
  resolveWorkerSessionTarget,
3769
3818
  syncSessionArtifacts,
3770
- visibleCheckoutGitTestHarness,
3771
- visibleCheckoutRemoteFor,
3772
- workspaceSyncCheckoutTargets,
3773
3819
  writeWorkerTextFile
3774
3820
  });