@ricsam/r5d-worker 0.0.34 → 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,6 +42,7 @@ __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,
46
48
  selectManifestSyncTargets: () => selectManifestSyncTargets,
@@ -744,6 +746,11 @@ function resolveWorkerFilePath(branchPath, inputPath) {
744
746
  const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
745
747
  const repoRelativePath2 = displayPath === "." ? "" : displayPath;
746
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
+ }
747
754
  return {
748
755
  absolutePath: absolutePath2,
749
756
  displayPath,
@@ -762,6 +769,11 @@ function resolveWorkerFilePath(branchPath, inputPath) {
762
769
  if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
763
770
  throw new Error(`Invalid project file path: ${inputPath}`);
764
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
+ }
765
777
  return {
766
778
  absolutePath,
767
779
  displayPath: repoRelativePath || ".",
@@ -773,11 +785,60 @@ function resolveProjectFilePath(branchPath, inputPath) {
773
785
  const resolved = resolveWorkerFilePath(branchPath, inputPath);
774
786
  if (resolved.scope === "host") {
775
787
  throw new Error(
776
- `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}`
777
789
  );
778
790
  }
779
791
  return resolved;
780
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
+ }
781
842
  function resolveRemoteUrl(baseUrl, remoteUrl) {
782
843
  if (/^https?:\/\//i.test(remoteUrl)) {
783
844
  return remoteUrl;
@@ -1146,6 +1207,9 @@ async function forceSyncManifestBranchFromInternal(input) {
1146
1207
  if (!target) {
1147
1208
  throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1148
1209
  }
1210
+ if (hasInternalWorktreeChanges(context)) {
1211
+ return null;
1212
+ }
1149
1213
  await runInternalGitAsync(context, ["reset", "--hard", target]);
1150
1214
  await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1151
1215
  return getInternalCommitHash(context);
@@ -1153,10 +1217,6 @@ async function forceSyncManifestBranchFromInternal(input) {
1153
1217
  function findRepositorySyncBlockers(input) {
1154
1218
  const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1155
1219
  const blockedBy = [];
1156
- for (const process2 of input.processes) {
1157
- const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
1158
- if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
1159
- }
1160
1220
  for (const shell of input.shells) {
1161
1221
  const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1162
1222
  if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
@@ -1192,11 +1252,6 @@ async function syncManifestProjectsFromInternal(input) {
1192
1252
  projectPath: manifest.projectPath,
1193
1253
  branchName
1194
1254
  })),
1195
- processes: [...activeProcesses].map(([id, active]) => ({
1196
- id,
1197
- projectId: active.projectId,
1198
- branchName: active.branchName
1199
- })),
1200
1255
  shells: [...activePtys].map(([id, active]) => ({
1201
1256
  id,
1202
1257
  projectId: active.projectId,
@@ -1219,6 +1274,16 @@ async function syncManifestProjectsFromInternal(input) {
1219
1274
  branchName,
1220
1275
  manifest
1221
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
+ }
1222
1287
  results.push({
1223
1288
  projectId: manifest.projectId,
1224
1289
  projectPath: manifest.projectPath,
@@ -1291,6 +1356,60 @@ async function streamCommandOutput(stream, onData) {
1291
1356
  function ensureOperationBranch(input) {
1292
1357
  return ensureBranchWorkspace(input);
1293
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
+ }
1294
1413
  function internalGitProcessEnv(workspace, env) {
1295
1414
  if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1296
1415
  return {};
@@ -1857,6 +1976,22 @@ function pullBranch(input) {
1857
1976
  }
1858
1977
  async function executeOperation(input) {
1859
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
+ });
1860
1995
  case "read":
1861
1996
  return executeReadFileOperation({ ...input, message: input.message });
1862
1997
  case "write":
@@ -1959,9 +2094,11 @@ async function executeCommand(input) {
1959
2094
  projectId: input.projectId,
1960
2095
  sessionId: input.message.sessionId ?? "",
1961
2096
  branchName: input.message.branchName,
2097
+ mode: "foreground",
2098
+ startCommitHash: "",
1962
2099
  argv: input.message.argv,
1963
2100
  command: input.message.argv.join(" "),
1964
- cwd: input.message.cwd,
2101
+ cwd,
1965
2102
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1966
2103
  });
1967
2104
  if (input.message.timeoutMs) {
@@ -2064,17 +2201,28 @@ async function executeStreamingCommand(input) {
2064
2201
  projectId: input.projectId,
2065
2202
  sessionId: input.message.sessionId,
2066
2203
  branchName: input.message.branchName,
2204
+ mode: input.message.mode,
2205
+ startCommitHash: input.message.startCommitHash,
2206
+ launchWorkspaceActionId: input.message.launchWorkspaceActionId,
2067
2207
  credentialId: input.message.credentialId,
2208
+ pid: subprocess.pid,
2209
+ processGroupId: subprocess.pid,
2068
2210
  argv: input.message.argv,
2069
2211
  command: input.message.command,
2070
- 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,
2071
2216
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2072
2217
  });
2073
2218
  started = true;
2074
2219
  sendWorkerMessage(input.ws, {
2075
2220
  type: "exec_started",
2076
2221
  requestId: input.message.requestId,
2077
- runId: input.message.runId
2222
+ runId: input.message.runId,
2223
+ cwd,
2224
+ pid: subprocess.pid,
2225
+ processGroupId: subprocess.pid
2078
2226
  });
2079
2227
  if (input.message.timeoutMs) {
2080
2228
  timeout = setTimeout(() => {
@@ -2153,7 +2301,12 @@ function buildActiveProcessReports() {
2153
2301
  projectId: active.projectId,
2154
2302
  sessionId: active.sessionId,
2155
2303
  branchName: active.branchName,
2304
+ mode: active.mode,
2305
+ startCommitHash: active.startCommitHash,
2306
+ ...active.launchWorkspaceActionId ? { launchWorkspaceActionId: active.launchWorkspaceActionId } : {},
2156
2307
  ...active.credentialId ? { credentialId: active.credentialId } : {},
2308
+ ...active.pid !== void 0 ? { pid: active.pid } : {},
2309
+ ...active.processGroupId !== void 0 ? { processGroupId: active.processGroupId } : {},
2157
2310
  argv: active.argv,
2158
2311
  command: active.command,
2159
2312
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2650,6 +2803,7 @@ async function startWorker(options) {
2650
2803
  requestId: message.requestId,
2651
2804
  result: syncResult
2652
2805
  });
2806
+ sendActiveProcessReport(ws);
2653
2807
  return;
2654
2808
  }
2655
2809
  if (message.type === "update_clis") {
@@ -2831,7 +2985,7 @@ async function startWorker(options) {
2831
2985
  }
2832
2986
  return;
2833
2987
  }
2834
- 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") {
2835
2989
  try {
2836
2990
  await repositorySyncQueue;
2837
2991
  const manifest = manifestByProjectId.get(message.projectId);
@@ -2844,6 +2998,8 @@ async function startWorker(options) {
2844
2998
  projectRoot,
2845
2999
  syncRoot,
2846
3000
  artifactRoot,
3001
+ projectsRoot,
3002
+ manifests: [...manifestByProjectId.values()],
2847
3003
  manifest
2848
3004
  });
2849
3005
  ws.send(
@@ -2959,6 +3115,7 @@ if (isCliEntrypoint()) {
2959
3115
  findWorkerFiles,
2960
3116
  githubCliEnv,
2961
3117
  grepWorkerFiles,
3118
+ inspectManagedWorkspace,
2962
3119
  isArtifactEnvPath,
2963
3120
  listWorkerDirectory,
2964
3121
  prepareArtifactEnvForShell,
@@ -2966,6 +3123,7 @@ if (isCliEntrypoint()) {
2966
3123
  readWorkerImageFile,
2967
3124
  readWorkerTextFile,
2968
3125
  resolveHostShell,
3126
+ resolveManagedCheckoutPath,
2969
3127
  resolveProjectFilePath,
2970
3128
  resolveWorkerFilePath,
2971
3129
  selectManifestSyncTargets,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.34",
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,10 +1164,6 @@ 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 });
@@ -1141,11 +1199,6 @@ async function syncManifestProjectsFromInternal(input) {
1141
1199
  projectPath: manifest.projectPath,
1142
1200
  branchName
1143
1201
  })),
1144
- processes: [...activeProcesses].map(([id, active]) => ({
1145
- id,
1146
- projectId: active.projectId,
1147
- branchName: active.branchName
1148
- })),
1149
1202
  shells: [...activePtys].map(([id, active]) => ({
1150
1203
  id,
1151
1204
  projectId: active.projectId,
@@ -1168,6 +1221,16 @@ async function syncManifestProjectsFromInternal(input) {
1168
1221
  branchName,
1169
1222
  manifest
1170
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
+ }
1171
1234
  results.push({
1172
1235
  projectId: manifest.projectId,
1173
1236
  projectPath: manifest.projectPath,
@@ -1240,6 +1303,60 @@ async function streamCommandOutput(stream, onData) {
1240
1303
  function ensureOperationBranch(input) {
1241
1304
  return ensureBranchWorkspace(input);
1242
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
+ }
1243
1360
  function internalGitProcessEnv(workspace, env) {
1244
1361
  if (env?.R5D_USE_INTERNAL_GIT !== "1") {
1245
1362
  return {};
@@ -1806,6 +1923,22 @@ function pullBranch(input) {
1806
1923
  }
1807
1924
  async function executeOperation(input) {
1808
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
+ });
1809
1942
  case "read":
1810
1943
  return executeReadFileOperation({ ...input, message: input.message });
1811
1944
  case "write":
@@ -1908,9 +2041,11 @@ async function executeCommand(input) {
1908
2041
  projectId: input.projectId,
1909
2042
  sessionId: input.message.sessionId ?? "",
1910
2043
  branchName: input.message.branchName,
2044
+ mode: "foreground",
2045
+ startCommitHash: "",
1911
2046
  argv: input.message.argv,
1912
2047
  command: input.message.argv.join(" "),
1913
- cwd: input.message.cwd,
2048
+ cwd,
1914
2049
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1915
2050
  });
1916
2051
  if (input.message.timeoutMs) {
@@ -2013,17 +2148,28 @@ async function executeStreamingCommand(input) {
2013
2148
  projectId: input.projectId,
2014
2149
  sessionId: input.message.sessionId,
2015
2150
  branchName: input.message.branchName,
2151
+ mode: input.message.mode,
2152
+ startCommitHash: input.message.startCommitHash,
2153
+ launchWorkspaceActionId: input.message.launchWorkspaceActionId,
2016
2154
  credentialId: input.message.credentialId,
2155
+ pid: subprocess.pid,
2156
+ processGroupId: subprocess.pid,
2017
2157
  argv: input.message.argv,
2018
2158
  command: input.message.command,
2019
- 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,
2020
2163
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2021
2164
  });
2022
2165
  started = true;
2023
2166
  sendWorkerMessage(input.ws, {
2024
2167
  type: "exec_started",
2025
2168
  requestId: input.message.requestId,
2026
- runId: input.message.runId
2169
+ runId: input.message.runId,
2170
+ cwd,
2171
+ pid: subprocess.pid,
2172
+ processGroupId: subprocess.pid
2027
2173
  });
2028
2174
  if (input.message.timeoutMs) {
2029
2175
  timeout = setTimeout(() => {
@@ -2102,7 +2248,12 @@ function buildActiveProcessReports() {
2102
2248
  projectId: active.projectId,
2103
2249
  sessionId: active.sessionId,
2104
2250
  branchName: active.branchName,
2251
+ mode: active.mode,
2252
+ startCommitHash: active.startCommitHash,
2253
+ ...active.launchWorkspaceActionId ? { launchWorkspaceActionId: active.launchWorkspaceActionId } : {},
2105
2254
  ...active.credentialId ? { credentialId: active.credentialId } : {},
2255
+ ...active.pid !== void 0 ? { pid: active.pid } : {},
2256
+ ...active.processGroupId !== void 0 ? { processGroupId: active.processGroupId } : {},
2106
2257
  argv: active.argv,
2107
2258
  command: active.command,
2108
2259
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2599,6 +2750,7 @@ async function startWorker(options) {
2599
2750
  requestId: message.requestId,
2600
2751
  result: syncResult
2601
2752
  });
2753
+ sendActiveProcessReport(ws);
2602
2754
  return;
2603
2755
  }
2604
2756
  if (message.type === "update_clis") {
@@ -2780,7 +2932,7 @@ async function startWorker(options) {
2780
2932
  }
2781
2933
  return;
2782
2934
  }
2783
- 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") {
2784
2936
  try {
2785
2937
  await repositorySyncQueue;
2786
2938
  const manifest = manifestByProjectId.get(message.projectId);
@@ -2793,6 +2945,8 @@ async function startWorker(options) {
2793
2945
  projectRoot,
2794
2946
  syncRoot,
2795
2947
  artifactRoot,
2948
+ projectsRoot,
2949
+ manifests: [...manifestByProjectId.values()],
2796
2950
  manifest
2797
2951
  });
2798
2952
  ws.send(
@@ -2907,6 +3061,7 @@ export {
2907
3061
  findWorkerFiles,
2908
3062
  githubCliEnv,
2909
3063
  grepWorkerFiles,
3064
+ inspectManagedWorkspace,
2910
3065
  isArtifactEnvPath,
2911
3066
  listWorkerDirectory,
2912
3067
  prepareArtifactEnvForShell,
@@ -2914,6 +3069,7 @@ export {
2914
3069
  readWorkerImageFile,
2915
3070
  readWorkerTextFile,
2916
3071
  resolveHostShell,
3072
+ resolveManagedCheckoutPath,
2917
3073
  resolveProjectFilePath,
2918
3074
  resolveWorkerFilePath,
2919
3075
  selectManifestSyncTargets,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "type": "module"
5
5
  }
@@ -40,7 +40,7 @@ type WorkerRepositorySyncResult = {
40
40
  error: string;
41
41
  })>;
42
42
  blockedBy: Array<WorkerRepositorySyncTarget & {
43
- kind: "process" | "shell";
43
+ kind: "dirty_checkout" | "shell";
44
44
  id: string;
45
45
  }>;
46
46
  };
@@ -72,6 +72,28 @@ type WorkerViewFileBytesResult = {
72
72
  width: number;
73
73
  height: number;
74
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
+ };
75
97
  export declare function isArtifactEnvPath(filePath: string): boolean;
76
98
  export declare function syncSessionArtifacts(input: {
77
99
  baseUrl: string;
@@ -123,6 +145,13 @@ export declare function resolveWorkerFilePath(branchPath: string, inputPath: str
123
145
  export declare function resolveProjectFilePath(branchPath: string, inputPath: string): Extract<ResolvedWorkerFilePath, {
124
146
  scope: "project";
125
147
  }>;
148
+ export declare function resolveManagedCheckoutPath(input: {
149
+ projectsRoot: string;
150
+ manifests: WorkerProjectManifestEntry[];
151
+ originProjectId: string;
152
+ originBranchName: string;
153
+ inputPath: string;
154
+ }): WorkerResolveManagedPathResult;
126
155
  export declare function githubCliEnv(token: string | null | undefined): Record<string, string>;
127
156
  export declare function ensureVisibleGitCheckout(input: {
128
157
  projectRoot: string;
@@ -135,11 +164,6 @@ export declare function ensureVisibleGitCheckout(input: {
135
164
  }): string;
136
165
  export declare function findRepositorySyncBlockers(input: {
137
166
  targets: WorkerRepositorySyncTarget[];
138
- processes: Array<{
139
- id: string;
140
- projectId: string;
141
- branchName: string;
142
- }>;
143
167
  shells: Array<{
144
168
  id: string;
145
169
  projectId: string;
@@ -161,6 +185,13 @@ export declare function syncManifestProjectsFromInternal(input: {
161
185
  manifests: WorkerProjectManifestEntry[];
162
186
  requestedTargets?: WorkerRepositorySyncSelector[];
163
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>;
164
195
  export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number): WorkerReadFileResult;
165
196
  export declare function grepWorkerFiles(branchPath: string, input: {
166
197
  pattern: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",