@ricsam/r5d-worker 0.0.33 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/main.cjs CHANGED
@@ -34,6 +34,7 @@ __export(main_exports, {
34
34
  findWorkerFiles: () => findWorkerFiles,
35
35
  githubCliEnv: () => githubCliEnv,
36
36
  grepWorkerFiles: () => grepWorkerFiles,
37
+ inspectManagedWorkspace: () => inspectManagedWorkspace,
37
38
  isArtifactEnvPath: () => isArtifactEnvPath,
38
39
  listWorkerDirectory: () => listWorkerDirectory,
39
40
  prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
@@ -41,8 +42,10 @@ __export(main_exports, {
41
42
  readWorkerImageFile: () => readWorkerImageFile,
42
43
  readWorkerTextFile: () => readWorkerTextFile,
43
44
  resolveHostShell: () => resolveHostShell,
45
+ resolveManagedCheckoutPath: () => resolveManagedCheckoutPath,
44
46
  resolveProjectFilePath: () => resolveProjectFilePath,
45
47
  resolveWorkerFilePath: () => resolveWorkerFilePath,
48
+ selectManifestSyncTargets: () => selectManifestSyncTargets,
46
49
  syncManifestProjectsFromInternal: () => syncManifestProjectsFromInternal,
47
50
  syncProjectPlans: () => syncProjectPlans,
48
51
  syncSessionArtifacts: () => syncSessionArtifacts
@@ -743,6 +746,11 @@ function resolveWorkerFilePath(branchPath, inputPath) {
743
746
  const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
744
747
  const repoRelativePath2 = displayPath === "." ? "" : displayPath;
745
748
  assertAllowedProjectPath(repoRelativePath2, inputPath);
749
+ const realBranchPath2 = import_node_fs.default.existsSync(resolvedBranchPath) ? import_node_fs.default.realpathSync(resolvedBranchPath) : resolvedBranchPath;
750
+ const realTargetPath2 = resolveThroughExistingAncestor(absolutePath2);
751
+ if (!isInsideBranchPath(realBranchPath2, realTargetPath2)) {
752
+ throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
753
+ }
746
754
  return {
747
755
  absolutePath: absolutePath2,
748
756
  displayPath,
@@ -761,6 +769,11 @@ function resolveWorkerFilePath(branchPath, inputPath) {
761
769
  if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
762
770
  throw new Error(`Invalid project file path: ${inputPath}`);
763
771
  }
772
+ const realBranchPath = import_node_fs.default.existsSync(resolvedBranchPath) ? import_node_fs.default.realpathSync(resolvedBranchPath) : resolvedBranchPath;
773
+ const realTargetPath = resolveThroughExistingAncestor(absolutePath);
774
+ if (!isInsideBranchPath(realBranchPath, realTargetPath)) {
775
+ throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
776
+ }
764
777
  return {
765
778
  absolutePath,
766
779
  displayPath: repoRelativePath || ".",
@@ -772,11 +785,60 @@ function resolveProjectFilePath(branchPath, inputPath) {
772
785
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
773
786
  if (resolved.scope === "host") {
774
787
  throw new Error(
775
- `Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
788
+ `Project file writes must resolve inside the selected managed checkout: ${inputPath}`
776
789
  );
777
790
  }
778
791
  return resolved;
779
792
  }
793
+ function resolveThroughExistingAncestor(inputPath) {
794
+ const suffix = [];
795
+ let cursor = import_node_path.default.resolve(inputPath);
796
+ while (!import_node_fs.default.existsSync(cursor)) {
797
+ const parent = import_node_path.default.dirname(cursor);
798
+ if (parent === cursor) return import_node_path.default.resolve(inputPath);
799
+ suffix.unshift(import_node_path.default.basename(cursor));
800
+ cursor = parent;
801
+ }
802
+ return import_node_path.default.resolve(import_node_fs.default.realpathSync(cursor), ...suffix);
803
+ }
804
+ function resolveManagedCheckoutPath(input) {
805
+ const originManifest = input.manifests.find((manifest) => manifest.projectId === input.originProjectId);
806
+ const candidate = import_node_path.default.isAbsolute(input.inputPath) ? import_node_path.default.resolve(input.inputPath) : import_node_path.default.resolve(
807
+ input.projectsRoot,
808
+ originManifest?.repoSlug ?? input.originProjectId,
809
+ input.originBranchName,
810
+ input.inputPath
811
+ );
812
+ const resolvedCandidate = resolveThroughExistingAncestor(candidate);
813
+ for (const manifest of input.manifests) {
814
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
815
+ for (const branchName of branches) {
816
+ const checkoutPath = import_node_path.default.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName);
817
+ if (!hasNormalVisibleGitDir(checkoutPath)) continue;
818
+ const resolvedCheckoutPath = import_node_fs.default.existsSync(checkoutPath) ? import_node_fs.default.realpathSync(checkoutPath) : checkoutPath;
819
+ const matchesLexically = isInsideBranchPath(checkoutPath, candidate);
820
+ if (!matchesLexically) continue;
821
+ if (!isInsideBranchPath(resolvedCheckoutPath, resolvedCandidate)) {
822
+ throw new Error(`Managed path escapes its checkout through a symbolic link: ${input.inputPath}`);
823
+ }
824
+ const repoRelativePath = toProjectDisplayPath(resolvedCheckoutPath, resolvedCandidate);
825
+ assertAllowedProjectPath(repoRelativePath === "." ? "" : repoRelativePath, input.inputPath);
826
+ return {
827
+ type: "resolve_managed_path",
828
+ resolved: {
829
+ projectId: manifest.projectId,
830
+ projectPath: manifest.projectPath,
831
+ branchName,
832
+ checkoutPath,
833
+ inputPath: input.inputPath,
834
+ absolutePath: candidate,
835
+ repoRelativePath: repoRelativePath === "." ? "" : repoRelativePath
836
+ }
837
+ };
838
+ }
839
+ }
840
+ return { type: "resolve_managed_path", resolved: null };
841
+ }
780
842
  function resolveRemoteUrl(baseUrl, remoteUrl) {
781
843
  if (/^https?:\/\//i.test(remoteUrl)) {
782
844
  return remoteUrl;
@@ -1145,6 +1207,9 @@ async function forceSyncManifestBranchFromInternal(input) {
1145
1207
  if (!target) {
1146
1208
  throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1147
1209
  }
1210
+ if (hasInternalWorktreeChanges(context)) {
1211
+ return null;
1212
+ }
1148
1213
  await runInternalGitAsync(context, ["reset", "--hard", target]);
1149
1214
  await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1150
1215
  return getInternalCommitHash(context);
@@ -1152,21 +1217,34 @@ async function forceSyncManifestBranchFromInternal(input) {
1152
1217
  function findRepositorySyncBlockers(input) {
1153
1218
  const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1154
1219
  const blockedBy = [];
1155
- for (const process2 of input.processes) {
1156
- const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
1157
- if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
1158
- }
1159
1220
  for (const shell of input.shells) {
1160
1221
  const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1161
1222
  if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1162
1223
  }
1163
1224
  return blockedBy;
1164
1225
  }
1226
+ function selectManifestSyncTargets(input) {
1227
+ if (!input.requestedTargets) {
1228
+ return input.manifests.flatMap((manifest) => {
1229
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1230
+ return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1231
+ });
1232
+ }
1233
+ const manifestsByProjectId = new Map(input.manifests.map((manifest) => [manifest.projectId, manifest]));
1234
+ const selected = /* @__PURE__ */ new Map();
1235
+ for (const target of input.requestedTargets) {
1236
+ const manifest = manifestsByProjectId.get(target.projectId);
1237
+ if (!manifest) {
1238
+ continue;
1239
+ }
1240
+ selected.set(`${target.projectId}\0${target.branchName}`, { manifest, branchName: target.branchName });
1241
+ }
1242
+ return [...selected.values()];
1243
+ }
1165
1244
  async function syncManifestProjectsFromInternal(input) {
1166
- const requestedProjectIds = input.projectIds ? new Set(input.projectIds) : null;
1167
- const targets = input.manifests.filter((manifest) => !requestedProjectIds || requestedProjectIds.has(manifest.projectId)).flatMap((manifest) => {
1168
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1169
- return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1245
+ const targets = selectManifestSyncTargets({
1246
+ manifests: input.manifests,
1247
+ requestedTargets: input.requestedTargets
1170
1248
  });
1171
1249
  const blockedBy = findRepositorySyncBlockers({
1172
1250
  targets: targets.map(({ manifest, branchName }) => ({
@@ -1174,22 +1252,18 @@ async function syncManifestProjectsFromInternal(input) {
1174
1252
  projectPath: manifest.projectPath,
1175
1253
  branchName
1176
1254
  })),
1177
- processes: [...activeProcesses].map(([id, active]) => ({
1178
- id,
1179
- projectId: active.projectId,
1180
- branchName: active.branchName
1181
- })),
1182
1255
  shells: [...activePtys].map(([id, active]) => ({
1183
1256
  id,
1184
1257
  projectId: active.projectId,
1185
1258
  branchName: active.branchName
1186
1259
  }))
1187
1260
  });
1188
- if (blockedBy.length > 0) {
1189
- return { status: "blocked", results: [], blockedBy };
1190
- }
1261
+ const blockedTargetKeys = new Set(blockedBy.map((blocker) => `${blocker.projectId}\0${blocker.branchName}`));
1191
1262
  const results = [];
1192
1263
  for (const { manifest, branchName } of targets) {
1264
+ if (blockedTargetKeys.has(`${manifest.projectId}\0${branchName}`)) {
1265
+ continue;
1266
+ }
1193
1267
  try {
1194
1268
  const commitHash = await forceSyncManifestBranchFromInternal({
1195
1269
  projectId: manifest.projectId,
@@ -1200,6 +1274,16 @@ async function syncManifestProjectsFromInternal(input) {
1200
1274
  branchName,
1201
1275
  manifest
1202
1276
  });
1277
+ if (commitHash === null) {
1278
+ blockedBy.push({
1279
+ projectId: manifest.projectId,
1280
+ projectPath: manifest.projectPath,
1281
+ branchName,
1282
+ kind: "dirty_checkout",
1283
+ id: `${manifest.projectId}:${branchName}`
1284
+ });
1285
+ continue;
1286
+ }
1203
1287
  results.push({
1204
1288
  projectId: manifest.projectId,
1205
1289
  projectPath: manifest.projectPath,
@@ -1217,10 +1301,11 @@ async function syncManifestProjectsFromInternal(input) {
1217
1301
  });
1218
1302
  }
1219
1303
  }
1304
+ const hasFailures = results.some((result) => result.status === "failed");
1220
1305
  return {
1221
- status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1306
+ status: blockedBy.length > 0 && results.length === 0 ? "blocked" : blockedBy.length > 0 || hasFailures ? "partial" : "completed",
1222
1307
  results,
1223
- blockedBy: []
1308
+ blockedBy
1224
1309
  };
1225
1310
  }
1226
1311
  function resolveCommandCwd(branchPath, cwd) {
@@ -1271,6 +1356,60 @@ async function streamCommandOutput(stream, onData) {
1271
1356
  function ensureOperationBranch(input) {
1272
1357
  return ensureBranchWorkspace(input);
1273
1358
  }
1359
+ async function inspectManagedWorkspace(input) {
1360
+ const candidates = input.manifests.flatMap((manifest) => {
1361
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1362
+ return branches.map((branchName) => ({
1363
+ manifest,
1364
+ branchName,
1365
+ checkoutPath: import_node_path.default.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName)
1366
+ }));
1367
+ });
1368
+ const inspected = await Promise.all(
1369
+ candidates.map(async ({ manifest, branchName, checkoutPath }) => {
1370
+ const gitDir = internalGitDirFor(input.syncRoot, manifest.projectId, branchName);
1371
+ if (!hasNormalVisibleGitDir(checkoutPath) || !import_node_fs.default.existsSync(gitDir)) return null;
1372
+ const context = {
1373
+ gitDir,
1374
+ workTree: checkoutPath,
1375
+ auth: internalGitAuth(input.baseUrl, input.token)
1376
+ };
1377
+ try {
1378
+ const status = await runInternalGitAsync(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
1379
+ let commitHash = null;
1380
+ for (const revision of [
1381
+ "HEAD",
1382
+ `refs/remotes/${R5D_REMOTE_NAME}/${branchName}`,
1383
+ `refs/remotes/${R5D_REMOTE_NAME}/main`
1384
+ ]) {
1385
+ try {
1386
+ commitHash = await runInternalGitAsync(context, ["rev-parse", "--verify", revision]);
1387
+ break;
1388
+ } catch {
1389
+ }
1390
+ }
1391
+ if (!commitHash) return null;
1392
+ return {
1393
+ projectId: manifest.projectId,
1394
+ projectPath: manifest.projectPath,
1395
+ branchName,
1396
+ checkoutPath,
1397
+ commitHash,
1398
+ status,
1399
+ dirty: status.trim().length > 0
1400
+ };
1401
+ } catch {
1402
+ return null;
1403
+ }
1404
+ })
1405
+ );
1406
+ return {
1407
+ type: "workspace_status",
1408
+ checkouts: inspected.filter((checkout) => checkout !== null).sort(
1409
+ (left, right) => left.projectPath.localeCompare(right.projectPath) || left.branchName.localeCompare(right.branchName)
1410
+ )
1411
+ };
1412
+ }
1274
1413
  function internalGitProcessEnv(workspace, env) {
1275
1414
  if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1276
1415
  return {};
@@ -1837,6 +1976,22 @@ function pullBranch(input) {
1837
1976
  }
1838
1977
  async function executeOperation(input) {
1839
1978
  switch (input.message.type) {
1979
+ case "resolve_managed_path":
1980
+ return resolveManagedCheckoutPath({
1981
+ projectsRoot: input.projectsRoot,
1982
+ manifests: input.manifests,
1983
+ originProjectId: input.message.projectId,
1984
+ originBranchName: input.message.branchName,
1985
+ inputPath: input.message.inputPath
1986
+ });
1987
+ case "workspace_status":
1988
+ return await inspectManagedWorkspace({
1989
+ baseUrl: input.baseUrl,
1990
+ token: input.token,
1991
+ projectsRoot: input.projectsRoot,
1992
+ syncRoot: input.syncRoot,
1993
+ manifests: input.manifests
1994
+ });
1840
1995
  case "read":
1841
1996
  return executeReadFileOperation({ ...input, message: input.message });
1842
1997
  case "write":
@@ -1939,9 +2094,11 @@ async function executeCommand(input) {
1939
2094
  projectId: input.projectId,
1940
2095
  sessionId: input.message.sessionId ?? "",
1941
2096
  branchName: input.message.branchName,
2097
+ mode: "foreground",
2098
+ startCommitHash: "",
1942
2099
  argv: input.message.argv,
1943
2100
  command: input.message.argv.join(" "),
1944
- cwd: input.message.cwd,
2101
+ cwd,
1945
2102
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1946
2103
  });
1947
2104
  if (input.message.timeoutMs) {
@@ -2044,17 +2201,28 @@ async function executeStreamingCommand(input) {
2044
2201
  projectId: input.projectId,
2045
2202
  sessionId: input.message.sessionId,
2046
2203
  branchName: input.message.branchName,
2204
+ mode: input.message.mode,
2205
+ startCommitHash: input.message.startCommitHash,
2206
+ launchWorkspaceActionId: input.message.launchWorkspaceActionId,
2047
2207
  credentialId: input.message.credentialId,
2208
+ pid: subprocess.pid,
2209
+ processGroupId: subprocess.pid,
2048
2210
  argv: input.message.argv,
2049
2211
  command: input.message.command,
2050
- cwd: input.message.cwd,
2212
+ // Persist the resolved absolute cwd so reconnect reports and process
2213
+ // history retain the actual execution location, including cross-project
2214
+ // managed paths.
2215
+ cwd,
2051
2216
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2052
2217
  });
2053
2218
  started = true;
2054
2219
  sendWorkerMessage(input.ws, {
2055
2220
  type: "exec_started",
2056
2221
  requestId: input.message.requestId,
2057
- runId: input.message.runId
2222
+ runId: input.message.runId,
2223
+ cwd,
2224
+ pid: subprocess.pid,
2225
+ processGroupId: subprocess.pid
2058
2226
  });
2059
2227
  if (input.message.timeoutMs) {
2060
2228
  timeout = setTimeout(() => {
@@ -2133,7 +2301,12 @@ function buildActiveProcessReports() {
2133
2301
  projectId: active.projectId,
2134
2302
  sessionId: active.sessionId,
2135
2303
  branchName: active.branchName,
2304
+ mode: active.mode,
2305
+ startCommitHash: active.startCommitHash,
2306
+ ...active.launchWorkspaceActionId ? { launchWorkspaceActionId: active.launchWorkspaceActionId } : {},
2136
2307
  ...active.credentialId ? { credentialId: active.credentialId } : {},
2308
+ ...active.pid !== void 0 ? { pid: active.pid } : {},
2309
+ ...active.processGroupId !== void 0 ? { processGroupId: active.processGroupId } : {},
2137
2310
  argv: active.argv,
2138
2311
  command: active.command,
2139
2312
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2580,7 +2753,7 @@ async function startWorker(options) {
2580
2753
  projectsRoot,
2581
2754
  syncRoot,
2582
2755
  manifests: [...manifestByProjectId.values()],
2583
- projectIds: message.projectIds
2756
+ requestedTargets: message.targets
2584
2757
  });
2585
2758
  for (const result of syncResult.results) {
2586
2759
  if (result.status !== "synced") continue;
@@ -2630,6 +2803,7 @@ async function startWorker(options) {
2630
2803
  requestId: message.requestId,
2631
2804
  result: syncResult
2632
2805
  });
2806
+ sendActiveProcessReport(ws);
2633
2807
  return;
2634
2808
  }
2635
2809
  if (message.type === "update_clis") {
@@ -2811,7 +2985,7 @@ async function startWorker(options) {
2811
2985
  }
2812
2986
  return;
2813
2987
  }
2814
- 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 === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2988
+ if (message.type === "resolve_managed_path" || message.type === "workspace_status" || 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 === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2815
2989
  try {
2816
2990
  await repositorySyncQueue;
2817
2991
  const manifest = manifestByProjectId.get(message.projectId);
@@ -2824,6 +2998,8 @@ async function startWorker(options) {
2824
2998
  projectRoot,
2825
2999
  syncRoot,
2826
3000
  artifactRoot,
3001
+ projectsRoot,
3002
+ manifests: [...manifestByProjectId.values()],
2827
3003
  manifest
2828
3004
  });
2829
3005
  ws.send(
@@ -2939,6 +3115,7 @@ if (isCliEntrypoint()) {
2939
3115
  findWorkerFiles,
2940
3116
  githubCliEnv,
2941
3117
  grepWorkerFiles,
3118
+ inspectManagedWorkspace,
2942
3119
  isArtifactEnvPath,
2943
3120
  listWorkerDirectory,
2944
3121
  prepareArtifactEnvForShell,
@@ -2946,8 +3123,10 @@ if (isCliEntrypoint()) {
2946
3123
  readWorkerImageFile,
2947
3124
  readWorkerTextFile,
2948
3125
  resolveHostShell,
3126
+ resolveManagedCheckoutPath,
2949
3127
  resolveProjectFilePath,
2950
3128
  resolveWorkerFilePath,
3129
+ selectManifestSyncTargets,
2951
3130
  syncManifestProjectsFromInternal,
2952
3131
  syncProjectPlans,
2953
3132
  syncSessionArtifacts
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -693,6 +693,11 @@ function resolveWorkerFilePath(branchPath, inputPath) {
693
693
  const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
694
694
  const repoRelativePath2 = displayPath === "." ? "" : displayPath;
695
695
  assertAllowedProjectPath(repoRelativePath2, inputPath);
696
+ const realBranchPath2 = fs.existsSync(resolvedBranchPath) ? fs.realpathSync(resolvedBranchPath) : resolvedBranchPath;
697
+ const realTargetPath2 = resolveThroughExistingAncestor(absolutePath2);
698
+ if (!isInsideBranchPath(realBranchPath2, realTargetPath2)) {
699
+ throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
700
+ }
696
701
  return {
697
702
  absolutePath: absolutePath2,
698
703
  displayPath,
@@ -711,6 +716,11 @@ function resolveWorkerFilePath(branchPath, inputPath) {
711
716
  if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
712
717
  throw new Error(`Invalid project file path: ${inputPath}`);
713
718
  }
719
+ const realBranchPath = fs.existsSync(resolvedBranchPath) ? fs.realpathSync(resolvedBranchPath) : resolvedBranchPath;
720
+ const realTargetPath = resolveThroughExistingAncestor(absolutePath);
721
+ if (!isInsideBranchPath(realBranchPath, realTargetPath)) {
722
+ throw new Error(`Project file path escapes its checkout through a symbolic link: ${inputPath}`);
723
+ }
714
724
  return {
715
725
  absolutePath,
716
726
  displayPath: repoRelativePath || ".",
@@ -722,11 +732,60 @@ function resolveProjectFilePath(branchPath, inputPath) {
722
732
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
723
733
  if (resolved.scope === "host") {
724
734
  throw new Error(
725
- `Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
735
+ `Project file writes must resolve inside the selected managed checkout: ${inputPath}`
726
736
  );
727
737
  }
728
738
  return resolved;
729
739
  }
740
+ function resolveThroughExistingAncestor(inputPath) {
741
+ const suffix = [];
742
+ let cursor = path.resolve(inputPath);
743
+ while (!fs.existsSync(cursor)) {
744
+ const parent = path.dirname(cursor);
745
+ if (parent === cursor) return path.resolve(inputPath);
746
+ suffix.unshift(path.basename(cursor));
747
+ cursor = parent;
748
+ }
749
+ return path.resolve(fs.realpathSync(cursor), ...suffix);
750
+ }
751
+ function resolveManagedCheckoutPath(input) {
752
+ const originManifest = input.manifests.find((manifest) => manifest.projectId === input.originProjectId);
753
+ const candidate = path.isAbsolute(input.inputPath) ? path.resolve(input.inputPath) : path.resolve(
754
+ input.projectsRoot,
755
+ originManifest?.repoSlug ?? input.originProjectId,
756
+ input.originBranchName,
757
+ input.inputPath
758
+ );
759
+ const resolvedCandidate = resolveThroughExistingAncestor(candidate);
760
+ for (const manifest of input.manifests) {
761
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
762
+ for (const branchName of branches) {
763
+ const checkoutPath = path.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName);
764
+ if (!hasNormalVisibleGitDir(checkoutPath)) continue;
765
+ const resolvedCheckoutPath = fs.existsSync(checkoutPath) ? fs.realpathSync(checkoutPath) : checkoutPath;
766
+ const matchesLexically = isInsideBranchPath(checkoutPath, candidate);
767
+ if (!matchesLexically) continue;
768
+ if (!isInsideBranchPath(resolvedCheckoutPath, resolvedCandidate)) {
769
+ throw new Error(`Managed path escapes its checkout through a symbolic link: ${input.inputPath}`);
770
+ }
771
+ const repoRelativePath = toProjectDisplayPath(resolvedCheckoutPath, resolvedCandidate);
772
+ assertAllowedProjectPath(repoRelativePath === "." ? "" : repoRelativePath, input.inputPath);
773
+ return {
774
+ type: "resolve_managed_path",
775
+ resolved: {
776
+ projectId: manifest.projectId,
777
+ projectPath: manifest.projectPath,
778
+ branchName,
779
+ checkoutPath,
780
+ inputPath: input.inputPath,
781
+ absolutePath: candidate,
782
+ repoRelativePath: repoRelativePath === "." ? "" : repoRelativePath
783
+ }
784
+ };
785
+ }
786
+ }
787
+ return { type: "resolve_managed_path", resolved: null };
788
+ }
730
789
  function resolveRemoteUrl(baseUrl, remoteUrl) {
731
790
  if (/^https?:\/\//i.test(remoteUrl)) {
732
791
  return remoteUrl;
@@ -1095,6 +1154,9 @@ async function forceSyncManifestBranchFromInternal(input) {
1095
1154
  if (!target) {
1096
1155
  throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1097
1156
  }
1157
+ if (hasInternalWorktreeChanges(context)) {
1158
+ return null;
1159
+ }
1098
1160
  await runInternalGitAsync(context, ["reset", "--hard", target]);
1099
1161
  await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1100
1162
  return getInternalCommitHash(context);
@@ -1102,21 +1164,34 @@ async function forceSyncManifestBranchFromInternal(input) {
1102
1164
  function findRepositorySyncBlockers(input) {
1103
1165
  const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1104
1166
  const blockedBy = [];
1105
- for (const process2 of input.processes) {
1106
- const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
1107
- if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
1108
- }
1109
1167
  for (const shell of input.shells) {
1110
1168
  const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1111
1169
  if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1112
1170
  }
1113
1171
  return blockedBy;
1114
1172
  }
1173
+ function selectManifestSyncTargets(input) {
1174
+ if (!input.requestedTargets) {
1175
+ return input.manifests.flatMap((manifest) => {
1176
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1177
+ return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1178
+ });
1179
+ }
1180
+ const manifestsByProjectId = new Map(input.manifests.map((manifest) => [manifest.projectId, manifest]));
1181
+ const selected = /* @__PURE__ */ new Map();
1182
+ for (const target of input.requestedTargets) {
1183
+ const manifest = manifestsByProjectId.get(target.projectId);
1184
+ if (!manifest) {
1185
+ continue;
1186
+ }
1187
+ selected.set(`${target.projectId}\0${target.branchName}`, { manifest, branchName: target.branchName });
1188
+ }
1189
+ return [...selected.values()];
1190
+ }
1115
1191
  async function syncManifestProjectsFromInternal(input) {
1116
- const requestedProjectIds = input.projectIds ? new Set(input.projectIds) : null;
1117
- const targets = input.manifests.filter((manifest) => !requestedProjectIds || requestedProjectIds.has(manifest.projectId)).flatMap((manifest) => {
1118
- const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1119
- return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1192
+ const targets = selectManifestSyncTargets({
1193
+ manifests: input.manifests,
1194
+ requestedTargets: input.requestedTargets
1120
1195
  });
1121
1196
  const blockedBy = findRepositorySyncBlockers({
1122
1197
  targets: targets.map(({ manifest, branchName }) => ({
@@ -1124,22 +1199,18 @@ async function syncManifestProjectsFromInternal(input) {
1124
1199
  projectPath: manifest.projectPath,
1125
1200
  branchName
1126
1201
  })),
1127
- processes: [...activeProcesses].map(([id, active]) => ({
1128
- id,
1129
- projectId: active.projectId,
1130
- branchName: active.branchName
1131
- })),
1132
1202
  shells: [...activePtys].map(([id, active]) => ({
1133
1203
  id,
1134
1204
  projectId: active.projectId,
1135
1205
  branchName: active.branchName
1136
1206
  }))
1137
1207
  });
1138
- if (blockedBy.length > 0) {
1139
- return { status: "blocked", results: [], blockedBy };
1140
- }
1208
+ const blockedTargetKeys = new Set(blockedBy.map((blocker) => `${blocker.projectId}\0${blocker.branchName}`));
1141
1209
  const results = [];
1142
1210
  for (const { manifest, branchName } of targets) {
1211
+ if (blockedTargetKeys.has(`${manifest.projectId}\0${branchName}`)) {
1212
+ continue;
1213
+ }
1143
1214
  try {
1144
1215
  const commitHash = await forceSyncManifestBranchFromInternal({
1145
1216
  projectId: manifest.projectId,
@@ -1150,6 +1221,16 @@ async function syncManifestProjectsFromInternal(input) {
1150
1221
  branchName,
1151
1222
  manifest
1152
1223
  });
1224
+ if (commitHash === null) {
1225
+ blockedBy.push({
1226
+ projectId: manifest.projectId,
1227
+ projectPath: manifest.projectPath,
1228
+ branchName,
1229
+ kind: "dirty_checkout",
1230
+ id: `${manifest.projectId}:${branchName}`
1231
+ });
1232
+ continue;
1233
+ }
1153
1234
  results.push({
1154
1235
  projectId: manifest.projectId,
1155
1236
  projectPath: manifest.projectPath,
@@ -1167,10 +1248,11 @@ async function syncManifestProjectsFromInternal(input) {
1167
1248
  });
1168
1249
  }
1169
1250
  }
1251
+ const hasFailures = results.some((result) => result.status === "failed");
1170
1252
  return {
1171
- status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1253
+ status: blockedBy.length > 0 && results.length === 0 ? "blocked" : blockedBy.length > 0 || hasFailures ? "partial" : "completed",
1172
1254
  results,
1173
- blockedBy: []
1255
+ blockedBy
1174
1256
  };
1175
1257
  }
1176
1258
  function resolveCommandCwd(branchPath, cwd) {
@@ -1221,6 +1303,60 @@ async function streamCommandOutput(stream, onData) {
1221
1303
  function ensureOperationBranch(input) {
1222
1304
  return ensureBranchWorkspace(input);
1223
1305
  }
1306
+ async function inspectManagedWorkspace(input) {
1307
+ const candidates = input.manifests.flatMap((manifest) => {
1308
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1309
+ return branches.map((branchName) => ({
1310
+ manifest,
1311
+ branchName,
1312
+ checkoutPath: path.resolve(input.projectsRoot, manifest.repoSlug || manifest.projectId, branchName)
1313
+ }));
1314
+ });
1315
+ const inspected = await Promise.all(
1316
+ candidates.map(async ({ manifest, branchName, checkoutPath }) => {
1317
+ const gitDir = internalGitDirFor(input.syncRoot, manifest.projectId, branchName);
1318
+ if (!hasNormalVisibleGitDir(checkoutPath) || !fs.existsSync(gitDir)) return null;
1319
+ const context = {
1320
+ gitDir,
1321
+ workTree: checkoutPath,
1322
+ auth: internalGitAuth(input.baseUrl, input.token)
1323
+ };
1324
+ try {
1325
+ const status = await runInternalGitAsync(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
1326
+ let commitHash = null;
1327
+ for (const revision of [
1328
+ "HEAD",
1329
+ `refs/remotes/${R5D_REMOTE_NAME}/${branchName}`,
1330
+ `refs/remotes/${R5D_REMOTE_NAME}/main`
1331
+ ]) {
1332
+ try {
1333
+ commitHash = await runInternalGitAsync(context, ["rev-parse", "--verify", revision]);
1334
+ break;
1335
+ } catch {
1336
+ }
1337
+ }
1338
+ if (!commitHash) return null;
1339
+ return {
1340
+ projectId: manifest.projectId,
1341
+ projectPath: manifest.projectPath,
1342
+ branchName,
1343
+ checkoutPath,
1344
+ commitHash,
1345
+ status,
1346
+ dirty: status.trim().length > 0
1347
+ };
1348
+ } catch {
1349
+ return null;
1350
+ }
1351
+ })
1352
+ );
1353
+ return {
1354
+ type: "workspace_status",
1355
+ checkouts: inspected.filter((checkout) => checkout !== null).sort(
1356
+ (left, right) => left.projectPath.localeCompare(right.projectPath) || left.branchName.localeCompare(right.branchName)
1357
+ )
1358
+ };
1359
+ }
1224
1360
  function internalGitProcessEnv(workspace, env) {
1225
1361
  if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1226
1362
  return {};
@@ -1787,6 +1923,22 @@ function pullBranch(input) {
1787
1923
  }
1788
1924
  async function executeOperation(input) {
1789
1925
  switch (input.message.type) {
1926
+ case "resolve_managed_path":
1927
+ return resolveManagedCheckoutPath({
1928
+ projectsRoot: input.projectsRoot,
1929
+ manifests: input.manifests,
1930
+ originProjectId: input.message.projectId,
1931
+ originBranchName: input.message.branchName,
1932
+ inputPath: input.message.inputPath
1933
+ });
1934
+ case "workspace_status":
1935
+ return await inspectManagedWorkspace({
1936
+ baseUrl: input.baseUrl,
1937
+ token: input.token,
1938
+ projectsRoot: input.projectsRoot,
1939
+ syncRoot: input.syncRoot,
1940
+ manifests: input.manifests
1941
+ });
1790
1942
  case "read":
1791
1943
  return executeReadFileOperation({ ...input, message: input.message });
1792
1944
  case "write":
@@ -1889,9 +2041,11 @@ async function executeCommand(input) {
1889
2041
  projectId: input.projectId,
1890
2042
  sessionId: input.message.sessionId ?? "",
1891
2043
  branchName: input.message.branchName,
2044
+ mode: "foreground",
2045
+ startCommitHash: "",
1892
2046
  argv: input.message.argv,
1893
2047
  command: input.message.argv.join(" "),
1894
- cwd: input.message.cwd,
2048
+ cwd,
1895
2049
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1896
2050
  });
1897
2051
  if (input.message.timeoutMs) {
@@ -1994,17 +2148,28 @@ async function executeStreamingCommand(input) {
1994
2148
  projectId: input.projectId,
1995
2149
  sessionId: input.message.sessionId,
1996
2150
  branchName: input.message.branchName,
2151
+ mode: input.message.mode,
2152
+ startCommitHash: input.message.startCommitHash,
2153
+ launchWorkspaceActionId: input.message.launchWorkspaceActionId,
1997
2154
  credentialId: input.message.credentialId,
2155
+ pid: subprocess.pid,
2156
+ processGroupId: subprocess.pid,
1998
2157
  argv: input.message.argv,
1999
2158
  command: input.message.command,
2000
- cwd: input.message.cwd,
2159
+ // Persist the resolved absolute cwd so reconnect reports and process
2160
+ // history retain the actual execution location, including cross-project
2161
+ // managed paths.
2162
+ cwd,
2001
2163
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2002
2164
  });
2003
2165
  started = true;
2004
2166
  sendWorkerMessage(input.ws, {
2005
2167
  type: "exec_started",
2006
2168
  requestId: input.message.requestId,
2007
- runId: input.message.runId
2169
+ runId: input.message.runId,
2170
+ cwd,
2171
+ pid: subprocess.pid,
2172
+ processGroupId: subprocess.pid
2008
2173
  });
2009
2174
  if (input.message.timeoutMs) {
2010
2175
  timeout = setTimeout(() => {
@@ -2083,7 +2248,12 @@ function buildActiveProcessReports() {
2083
2248
  projectId: active.projectId,
2084
2249
  sessionId: active.sessionId,
2085
2250
  branchName: active.branchName,
2251
+ mode: active.mode,
2252
+ startCommitHash: active.startCommitHash,
2253
+ ...active.launchWorkspaceActionId ? { launchWorkspaceActionId: active.launchWorkspaceActionId } : {},
2086
2254
  ...active.credentialId ? { credentialId: active.credentialId } : {},
2255
+ ...active.pid !== void 0 ? { pid: active.pid } : {},
2256
+ ...active.processGroupId !== void 0 ? { processGroupId: active.processGroupId } : {},
2087
2257
  argv: active.argv,
2088
2258
  command: active.command,
2089
2259
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2530,7 +2700,7 @@ async function startWorker(options) {
2530
2700
  projectsRoot,
2531
2701
  syncRoot,
2532
2702
  manifests: [...manifestByProjectId.values()],
2533
- projectIds: message.projectIds
2703
+ requestedTargets: message.targets
2534
2704
  });
2535
2705
  for (const result of syncResult.results) {
2536
2706
  if (result.status !== "synced") continue;
@@ -2580,6 +2750,7 @@ async function startWorker(options) {
2580
2750
  requestId: message.requestId,
2581
2751
  result: syncResult
2582
2752
  });
2753
+ sendActiveProcessReport(ws);
2583
2754
  return;
2584
2755
  }
2585
2756
  if (message.type === "update_clis") {
@@ -2761,7 +2932,7 @@ async function startWorker(options) {
2761
2932
  }
2762
2933
  return;
2763
2934
  }
2764
- 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 === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2935
+ if (message.type === "resolve_managed_path" || message.type === "workspace_status" || 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 === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2765
2936
  try {
2766
2937
  await repositorySyncQueue;
2767
2938
  const manifest = manifestByProjectId.get(message.projectId);
@@ -2774,6 +2945,8 @@ async function startWorker(options) {
2774
2945
  projectRoot,
2775
2946
  syncRoot,
2776
2947
  artifactRoot,
2948
+ projectsRoot,
2949
+ manifests: [...manifestByProjectId.values()],
2777
2950
  manifest
2778
2951
  });
2779
2952
  ws.send(
@@ -2888,6 +3061,7 @@ export {
2888
3061
  findWorkerFiles,
2889
3062
  githubCliEnv,
2890
3063
  grepWorkerFiles,
3064
+ inspectManagedWorkspace,
2891
3065
  isArtifactEnvPath,
2892
3066
  listWorkerDirectory,
2893
3067
  prepareArtifactEnvForShell,
@@ -2895,8 +3069,10 @@ export {
2895
3069
  readWorkerImageFile,
2896
3070
  readWorkerTextFile,
2897
3071
  resolveHostShell,
3072
+ resolveManagedCheckoutPath,
2898
3073
  resolveProjectFilePath,
2899
3074
  resolveWorkerFilePath,
3075
+ selectManifestSyncTargets,
2900
3076
  syncManifestProjectsFromInternal,
2901
3077
  syncProjectPlans,
2902
3078
  syncSessionArtifacts
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "type": "module"
5
5
  }
@@ -26,6 +26,10 @@ type WorkerRepositorySyncTarget = {
26
26
  projectPath: string;
27
27
  branchName: string;
28
28
  };
29
+ type WorkerRepositorySyncSelector = {
30
+ projectId: string;
31
+ branchName: string;
32
+ };
29
33
  type WorkerRepositorySyncResult = {
30
34
  status: "completed" | "partial" | "blocked";
31
35
  results: Array<WorkerRepositorySyncTarget & ({
@@ -36,7 +40,7 @@ type WorkerRepositorySyncResult = {
36
40
  error: string;
37
41
  })>;
38
42
  blockedBy: Array<WorkerRepositorySyncTarget & {
39
- kind: "process" | "shell";
43
+ kind: "dirty_checkout" | "shell";
40
44
  id: string;
41
45
  }>;
42
46
  };
@@ -68,6 +72,28 @@ type WorkerViewFileBytesResult = {
68
72
  width: number;
69
73
  height: number;
70
74
  };
75
+ type WorkerManagedCheckout = {
76
+ projectId: string;
77
+ projectPath: string;
78
+ branchName: string;
79
+ checkoutPath: string;
80
+ };
81
+ type WorkerResolveManagedPathResult = {
82
+ type: "resolve_managed_path";
83
+ resolved: (WorkerManagedCheckout & {
84
+ inputPath: string;
85
+ absolutePath: string;
86
+ repoRelativePath: string;
87
+ }) | null;
88
+ };
89
+ type WorkerWorkspaceStatusResult = {
90
+ type: "workspace_status";
91
+ checkouts: Array<WorkerManagedCheckout & {
92
+ commitHash: string;
93
+ status: string;
94
+ dirty: boolean;
95
+ }>;
96
+ };
71
97
  export declare function isArtifactEnvPath(filePath: string): boolean;
72
98
  export declare function syncSessionArtifacts(input: {
73
99
  baseUrl: string;
@@ -119,6 +145,13 @@ export declare function resolveWorkerFilePath(branchPath: string, inputPath: str
119
145
  export declare function resolveProjectFilePath(branchPath: string, inputPath: string): Extract<ResolvedWorkerFilePath, {
120
146
  scope: "project";
121
147
  }>;
148
+ export declare function resolveManagedCheckoutPath(input: {
149
+ projectsRoot: string;
150
+ manifests: WorkerProjectManifestEntry[];
151
+ originProjectId: string;
152
+ originBranchName: string;
153
+ inputPath: string;
154
+ }): WorkerResolveManagedPathResult;
122
155
  export declare function githubCliEnv(token: string | null | undefined): Record<string, string>;
123
156
  export declare function ensureVisibleGitCheckout(input: {
124
157
  projectRoot: string;
@@ -131,25 +164,34 @@ export declare function ensureVisibleGitCheckout(input: {
131
164
  }): string;
132
165
  export declare function findRepositorySyncBlockers(input: {
133
166
  targets: WorkerRepositorySyncTarget[];
134
- processes: Array<{
135
- id: string;
136
- projectId: string;
137
- branchName: string;
138
- }>;
139
167
  shells: Array<{
140
168
  id: string;
141
169
  projectId: string;
142
170
  branchName: string;
143
171
  }>;
144
172
  }): WorkerRepositorySyncResult["blockedBy"];
173
+ export declare function selectManifestSyncTargets(input: {
174
+ manifests: WorkerProjectManifestEntry[];
175
+ requestedTargets?: WorkerRepositorySyncSelector[];
176
+ }): Array<{
177
+ manifest: WorkerProjectManifestEntry;
178
+ branchName: string;
179
+ }>;
145
180
  export declare function syncManifestProjectsFromInternal(input: {
146
181
  baseUrl: string;
147
182
  token: string;
148
183
  projectsRoot: string;
149
184
  syncRoot: string;
150
185
  manifests: WorkerProjectManifestEntry[];
151
- projectIds?: string[];
186
+ requestedTargets?: WorkerRepositorySyncSelector[];
152
187
  }): Promise<WorkerRepositorySyncResult>;
188
+ export declare function inspectManagedWorkspace(input: {
189
+ baseUrl: string;
190
+ token: string;
191
+ projectsRoot: string;
192
+ syncRoot: string;
193
+ manifests: WorkerProjectManifestEntry[];
194
+ }): Promise<WorkerWorkspaceStatusResult>;
153
195
  export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number): WorkerReadFileResult;
154
196
  export declare function grepWorkerFiles(branchPath: string, input: {
155
197
  pattern: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",