@ricsam/r5d-worker 0.0.47 → 0.0.49

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/mjs/main.mjs CHANGED
@@ -8,6 +8,7 @@ import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
8
8
  import { configureVisibleGitIdentity } from "./git-identity.mjs";
9
9
  import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
10
10
  import { terminateProcessTree } from "./process-tree.mjs";
11
+ import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
11
12
  import {
12
13
  isRetryableWorkerServerStatus,
13
14
  superviseWorkerRuntime,
@@ -294,8 +295,15 @@ function projectPlanDir(planRoot, projectId, branchName) {
294
295
  validateBranchName(branchName);
295
296
  return path.join(planRoot, projectId, branchName);
296
297
  }
297
- function planEnv(planRoot, projectId, branchName, activePlanId) {
298
- const plansDir = projectPlanDir(planRoot, projectId, branchName);
298
+ function targetPlanDir(planRoot, target) {
299
+ if (target.type === "project") return projectPlanDir(planRoot, target.projectId, target.branchName);
300
+ if (target.rootProfile === "canonical_sync") {
301
+ throw new Error("Ordinary plans are unavailable in canonical synchronization sessions");
302
+ }
303
+ return path.join(planRoot, "workspace");
304
+ }
305
+ function planEnv(planRoot, target, activePlanId) {
306
+ const plansDir = targetPlanDir(planRoot, target);
299
307
  const env = {
300
308
  [R5D_PLANS_DIR_ENV]: plansDir
301
309
  };
@@ -313,14 +321,14 @@ function isArtifactEnvPath(filePath) {
313
321
  return false;
314
322
  }
315
323
  }
316
- function artifactApiBasePath(projectId, branchName, sessionId) {
317
- return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
324
+ function artifactApiBasePath(sessionId) {
325
+ return `/preview-api/artifacts/${encodeURIComponent(sessionId)}`;
318
326
  }
319
- function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
320
- return new URL(artifactApiBasePath(projectId, branchName, sessionId), baseUrl);
327
+ function artifactManifestUrl(baseUrl, sessionId) {
328
+ return new URL(artifactApiBasePath(sessionId), baseUrl);
321
329
  }
322
- function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
323
- return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
330
+ function artifactDownloadUrl(baseUrl, sessionId, filename) {
331
+ return new URL(`${artifactApiBasePath(sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
324
332
  }
325
333
  function sha256Path(filePath) {
326
334
  return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
@@ -412,7 +420,7 @@ async function syncRemoteFileSet(input) {
412
420
  }
413
421
  async function fetchSessionArtifactManifest(input) {
414
422
  return fetchRemoteFileManifest({
415
- url: artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId),
423
+ url: artifactManifestUrl(input.baseUrl, input.sessionId),
416
424
  token: input.token,
417
425
  collectionKey: "artifacts",
418
426
  label: "artifact"
@@ -430,7 +438,7 @@ async function syncSessionArtifacts(input) {
430
438
  files: artifacts,
431
439
  label: "Artifact path",
432
440
  download: (artifact) => downloadRemoteSyncedFile({
433
- url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
441
+ url: artifactDownloadUrl(input.baseUrl, input.sessionId, artifact.filename),
434
442
  token: input.token,
435
443
  targetDir,
436
444
  file: artifact,
@@ -463,14 +471,12 @@ async function prepareBuiltInToolPaths(input) {
463
471
  artifactsDir: await syncSessionArtifacts({
464
472
  baseUrl: input.baseUrl,
465
473
  token: input.token,
466
- projectId: input.projectId,
467
- branchName: input.branchName,
468
474
  sessionId: input.sessionId,
469
475
  artifactRoot: input.artifactRoot
470
476
  })
471
477
  };
472
478
  }
473
- const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
479
+ const plansDir = targetPlanDir(input.planRoot, input.target);
474
480
  fs.mkdirSync(plansDir, { recursive: true });
475
481
  if (parsed.ref === R5D_PLANS_DIR_REF) {
476
482
  return { plansDir };
@@ -492,8 +498,6 @@ async function prepareArtifactEnvForShell(input) {
492
498
  await syncSessionArtifacts({
493
499
  baseUrl: input.baseUrl,
494
500
  token: input.token,
495
- projectId: input.projectId,
496
- branchName: input.branchName,
497
501
  sessionId: input.sessionId,
498
502
  artifactRoot: input.artifactRoot
499
503
  });
@@ -510,9 +514,9 @@ async function prepareArtifactEnvForShell(input) {
510
514
  };
511
515
  }
512
516
  function preparePlanEnvForShell(input) {
513
- const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
517
+ const plansDir = targetPlanDir(input.planRoot, input.target);
514
518
  fs.mkdirSync(plansDir, { recursive: true });
515
- return planEnv(input.planRoot, input.projectId, input.branchName, input.activePlanId);
519
+ return planEnv(input.planRoot, input.target, input.activePlanId);
516
520
  }
517
521
  async function verifyR5dctlAuth(baseUrl, token) {
518
522
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
@@ -1127,6 +1131,32 @@ function ensureBranchWorkspace(input) {
1127
1131
  })
1128
1132
  };
1129
1133
  }
1134
+ function describeWorkerSessionTarget(target) {
1135
+ return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
1136
+ }
1137
+ function resolveWorkerSessionTarget(input) {
1138
+ if (input.target.type === "workspace") {
1139
+ if (input.target.rootProfile === "visible_projects") {
1140
+ return { target: input.target, rootPath: input.projectsRoot };
1141
+ }
1142
+ if (!fs.existsSync(path.join(input.workspaceShadowRoot, ".git"))) {
1143
+ throw new Error("The canonical workspace checkout is not initialized on this worker");
1144
+ }
1145
+ return { target: input.target, rootPath: input.workspaceShadowRoot };
1146
+ }
1147
+ const manifest = input.manifestByProjectId.get(input.target.projectId);
1148
+ const projectRoot = projectRootFor(input.projectsRoot, input.target.projectId, input.manifestByProjectId);
1149
+ const workspace = ensureBranchWorkspace({
1150
+ projectId: input.target.projectId,
1151
+ baseUrl: input.baseUrl,
1152
+ token: input.token,
1153
+ projectRoot,
1154
+ syncRoot: input.syncRoot,
1155
+ branchName: input.target.branchName,
1156
+ manifest
1157
+ });
1158
+ return { target: input.target, rootPath: workspace.branchPath, manifest };
1159
+ }
1130
1160
  function workspaceSyncCheckoutTargets(input) {
1131
1161
  const selected = /* @__PURE__ */ new Map();
1132
1162
  const add = (target) => {
@@ -1155,7 +1185,7 @@ function resolveCommandCwd(branchPath, cwd) {
1155
1185
  const resolved = path.resolve(branchPath, cwd);
1156
1186
  const relative = path.relative(branchPath, resolved);
1157
1187
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
1158
- throw new Error(`Relative cwd escapes the project branch: ${cwd}`);
1188
+ throw new Error(`Relative cwd escapes the execution root: ${cwd}`);
1159
1189
  }
1160
1190
  return resolved;
1161
1191
  }
@@ -1190,8 +1220,8 @@ async function streamCommandOutput(stream, onData) {
1190
1220
  reader.releaseLock();
1191
1221
  }
1192
1222
  }
1193
- function ensureOperationBranch(input) {
1194
- return ensureBranchWorkspace(input);
1223
+ async function prepareBuiltInToolPathsForTarget(input) {
1224
+ return prepareBuiltInToolPaths(input);
1195
1225
  }
1196
1226
  function formatLineNumberedContent(content, offset, limit) {
1197
1227
  const allLines = content.split("\n");
@@ -1317,11 +1347,11 @@ async function withFileMutationQueue(key, operation) {
1317
1347
  }
1318
1348
  }
1319
1349
  }
1320
- function mutationQueueKey(projectId, branchName, resolved) {
1350
+ function mutationQueueKey(target, resolved) {
1321
1351
  if (resolved.scope !== "project") {
1322
1352
  return `${resolved.scope}:${path.normalize(resolved.absolutePath)}`;
1323
1353
  }
1324
- return `project:${projectId}:${branchName}:${path.posix.normalize(resolved.repoRelativePath)}`;
1354
+ return `${describeWorkerSessionTarget(target)}:${path.posix.normalize(resolved.repoRelativePath)}`;
1325
1355
  }
1326
1356
  function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
1327
1357
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
@@ -1396,57 +1426,51 @@ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
1396
1426
  };
1397
1427
  }
1398
1428
  async function executeReadFileOperation(input) {
1399
- const builtInPaths = await prepareBuiltInToolPaths({
1429
+ const builtInPaths = await prepareBuiltInToolPathsForTarget({
1430
+ target: input.resolvedTarget.target,
1400
1431
  filePath: input.message.filePath,
1401
1432
  baseUrl: input.baseUrl,
1402
1433
  token: input.token,
1403
- projectId: input.projectId,
1404
- branchName: input.message.branchName,
1405
1434
  sessionId: input.message.sessionId,
1406
1435
  activePlanId: input.message.activePlanId,
1407
1436
  artifactRoot: input.artifactRoot,
1408
1437
  planRoot: input.planRoot,
1409
1438
  access: "read"
1410
1439
  });
1411
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1412
- return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1440
+ return readWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1413
1441
  }
1414
1442
  async function executeWriteFileOperation(input) {
1415
- const builtInPaths = await prepareBuiltInToolPaths({
1443
+ const builtInPaths = await prepareBuiltInToolPathsForTarget({
1444
+ target: input.resolvedTarget.target,
1416
1445
  filePath: input.message.filePath,
1417
1446
  baseUrl: input.baseUrl,
1418
1447
  token: input.token,
1419
- projectId: input.projectId,
1420
- branchName: input.message.branchName,
1421
1448
  sessionId: input.message.sessionId,
1422
1449
  activePlanId: input.message.activePlanId,
1423
1450
  artifactRoot: input.artifactRoot,
1424
1451
  planRoot: input.planRoot,
1425
1452
  access: "write"
1426
1453
  });
1427
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1428
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1429
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1430
- return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
1454
+ const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
1455
+ return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
1456
+ return writeWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.content, builtInPaths);
1431
1457
  });
1432
1458
  }
1433
1459
  async function executeEditFileOperation(input) {
1434
- const builtInPaths = await prepareBuiltInToolPaths({
1460
+ const builtInPaths = await prepareBuiltInToolPathsForTarget({
1461
+ target: input.resolvedTarget.target,
1435
1462
  filePath: input.message.filePath,
1436
1463
  baseUrl: input.baseUrl,
1437
1464
  token: input.token,
1438
- projectId: input.projectId,
1439
- branchName: input.message.branchName,
1440
1465
  sessionId: input.message.sessionId,
1441
1466
  activePlanId: input.message.activePlanId,
1442
1467
  artifactRoot: input.artifactRoot,
1443
1468
  planRoot: input.planRoot,
1444
1469
  access: "write"
1445
1470
  });
1446
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1447
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1448
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1449
- return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
1471
+ const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
1472
+ return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
1473
+ return editWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.edits, builtInPaths);
1450
1474
  });
1451
1475
  }
1452
1476
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1559,21 +1583,19 @@ function grepWorkerFiles(branchPath, input, builtInPaths) {
1559
1583
  }
1560
1584
  async function executeGrepOperation(input) {
1561
1585
  const toolPath = input.message.path ?? ".";
1562
- const builtInPaths = await prepareBuiltInToolPaths({
1586
+ const builtInPaths = await prepareBuiltInToolPathsForTarget({
1587
+ target: input.resolvedTarget.target,
1563
1588
  filePath: toolPath,
1564
1589
  baseUrl: input.baseUrl,
1565
1590
  token: input.token,
1566
- projectId: input.projectId,
1567
- branchName: input.message.branchName,
1568
1591
  sessionId: input.message.sessionId,
1569
1592
  activePlanId: input.message.activePlanId,
1570
1593
  artifactRoot: input.artifactRoot,
1571
1594
  planRoot: input.planRoot,
1572
1595
  access: "read"
1573
1596
  });
1574
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1575
1597
  return grepWorkerFiles(
1576
- workspace.branchPath,
1598
+ input.resolvedTarget.rootPath,
1577
1599
  {
1578
1600
  pattern: input.message.pattern,
1579
1601
  path: input.message.path,
@@ -1620,21 +1642,19 @@ function findWorkerFiles(branchPath, input, builtInPaths) {
1620
1642
  }
1621
1643
  async function executeFindOperation(input) {
1622
1644
  const toolPath = input.message.path ?? ".";
1623
- const builtInPaths = await prepareBuiltInToolPaths({
1645
+ const builtInPaths = await prepareBuiltInToolPathsForTarget({
1646
+ target: input.resolvedTarget.target,
1624
1647
  filePath: toolPath,
1625
1648
  baseUrl: input.baseUrl,
1626
1649
  token: input.token,
1627
- projectId: input.projectId,
1628
- branchName: input.message.branchName,
1629
1650
  sessionId: input.message.sessionId,
1630
1651
  activePlanId: input.message.activePlanId,
1631
1652
  artifactRoot: input.artifactRoot,
1632
1653
  planRoot: input.planRoot,
1633
1654
  access: "read"
1634
1655
  });
1635
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1636
1656
  return findWorkerFiles(
1637
- workspace.branchPath,
1657
+ input.resolvedTarget.rootPath,
1638
1658
  {
1639
1659
  pattern: input.message.pattern,
1640
1660
  path: input.message.path,
@@ -1664,20 +1684,18 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPat
1664
1684
  }
1665
1685
  async function executeLsOperation(input) {
1666
1686
  const toolPath = input.message.path ?? ".";
1667
- const builtInPaths = await prepareBuiltInToolPaths({
1687
+ const builtInPaths = await prepareBuiltInToolPathsForTarget({
1688
+ target: input.resolvedTarget.target,
1668
1689
  filePath: toolPath,
1669
1690
  baseUrl: input.baseUrl,
1670
1691
  token: input.token,
1671
- projectId: input.projectId,
1672
- branchName: input.message.branchName,
1673
1692
  sessionId: input.message.sessionId,
1674
1693
  activePlanId: input.message.activePlanId,
1675
1694
  artifactRoot: input.artifactRoot,
1676
1695
  planRoot: input.planRoot,
1677
1696
  access: "read"
1678
1697
  });
1679
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1680
- return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
1698
+ return listWorkerDirectory(input.resolvedTarget.rootPath, input.message.path, input.message.limit, builtInPaths);
1681
1699
  }
1682
1700
  function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1683
1701
  const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
@@ -1702,20 +1720,18 @@ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1702
1720
  };
1703
1721
  }
1704
1722
  async function executeViewFileBytesOperation(input) {
1705
- const builtInPaths = await prepareBuiltInToolPaths({
1723
+ const builtInPaths = await prepareBuiltInToolPathsForTarget({
1724
+ target: input.resolvedTarget.target,
1706
1725
  filePath: input.message.filePath,
1707
1726
  baseUrl: input.baseUrl,
1708
1727
  token: input.token,
1709
- projectId: input.projectId,
1710
- branchName: input.message.branchName,
1711
1728
  sessionId: input.message.sessionId,
1712
1729
  activePlanId: input.message.activePlanId,
1713
1730
  artifactRoot: input.artifactRoot,
1714
1731
  planRoot: input.planRoot,
1715
1732
  access: "read"
1716
1733
  });
1717
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1718
- return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
1734
+ return readWorkerImageFile(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
1719
1735
  }
1720
1736
  async function executeOperation(input) {
1721
1737
  switch (input.message.type) {
@@ -1735,38 +1751,56 @@ async function executeOperation(input) {
1735
1751
  return executeViewFileBytesOperation({ ...input, message: input.message });
1736
1752
  }
1737
1753
  }
1754
+ function targetIdentityEnv(target) {
1755
+ return target.type === "project" ? {
1756
+ R5D_PROJECT_ID: target.projectId,
1757
+ R5D_BRANCH_NAME: target.branchName,
1758
+ R5D_WORKSPACE_OWNER_ID: "",
1759
+ R5D_WORKSPACE_ROOT_PROFILE: ""
1760
+ } : {
1761
+ R5D_WORKSPACE_OWNER_ID: target.ownerUserId,
1762
+ R5D_WORKSPACE_ROOT_PROFILE: target.rootProfile,
1763
+ R5D_PROJECT_ID: "",
1764
+ R5D_BRANCH_NAME: ""
1765
+ };
1766
+ }
1767
+ async function prepareShellEnvForTarget(input) {
1768
+ const artifactProcessEnv = await prepareArtifactEnvForShell({
1769
+ baseUrl: input.baseUrl,
1770
+ token: input.token,
1771
+ sessionId: input.sessionId,
1772
+ artifactRoot: input.artifactRoot
1773
+ });
1774
+ if (input.target.type === "workspace" && input.target.rootProfile === "canonical_sync") {
1775
+ return {
1776
+ ...artifactProcessEnv,
1777
+ ...targetIdentityEnv(input.target)
1778
+ };
1779
+ }
1780
+ return {
1781
+ ...artifactProcessEnv,
1782
+ ...preparePlanEnvForShell({
1783
+ target: input.target,
1784
+ planRoot: input.planRoot,
1785
+ activePlanId: input.activePlanId
1786
+ }),
1787
+ ...targetIdentityEnv(input.target)
1788
+ };
1789
+ }
1738
1790
  async function executeCommand(input) {
1739
1791
  const startedAt = Date.now();
1740
1792
  let timeout;
1741
1793
  try {
1742
- const workspace = ensureBranchWorkspace({
1743
- projectId: input.projectId,
1744
- baseUrl: input.baseUrl,
1745
- token: input.token,
1746
- projectRoot: input.projectRoot,
1747
- syncRoot: input.syncRoot,
1748
- branchName: input.message.branchName,
1749
- manifest: input.manifest
1750
- });
1751
- const artifactProcessEnv = await prepareArtifactEnvForShell({
1794
+ const targetProcessEnv = await prepareShellEnvForTarget({
1795
+ target: input.resolvedTarget.target,
1752
1796
  baseUrl: input.baseUrl,
1753
1797
  token: input.token,
1754
- projectId: input.projectId,
1755
- branchName: input.message.branchName,
1756
1798
  sessionId: input.message.sessionId,
1757
- artifactRoot: input.artifactRoot
1758
- });
1759
- const planProcessEnv = preparePlanEnvForShell({
1760
- projectId: input.projectId,
1761
- branchName: input.message.branchName,
1799
+ artifactRoot: input.artifactRoot,
1762
1800
  planRoot: input.planRoot,
1763
1801
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1764
1802
  });
1765
- const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1766
- if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
1767
- throw new Error("The canonical workspace checkout is not initialized on this worker");
1768
- }
1769
- const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
1803
+ const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
1770
1804
  const subprocess = Bun.spawn(input.message.argv, {
1771
1805
  cwd,
1772
1806
  stdout: "pipe",
@@ -1776,15 +1810,13 @@ async function executeCommand(input) {
1776
1810
  ...process.env,
1777
1811
  ...githubProcessEnv(),
1778
1812
  ...input.message.env ?? {},
1779
- ...artifactProcessEnv,
1780
- ...planProcessEnv
1813
+ ...targetProcessEnv
1781
1814
  }
1782
1815
  });
1783
1816
  activeProcesses.set(input.message.runId, {
1784
1817
  process: subprocess,
1785
- projectId: input.projectId,
1818
+ target: input.resolvedTarget.target,
1786
1819
  sessionId: input.message.sessionId ?? "",
1787
- branchName: input.message.branchName,
1788
1820
  mode: "foreground",
1789
1821
  pid: subprocess.pid,
1790
1822
  processGroupId: process.platform === "win32" ? void 0 : subprocess.pid,
@@ -1848,34 +1880,16 @@ async function executeStreamingCommand(input) {
1848
1880
  });
1849
1881
  return;
1850
1882
  }
1851
- const workspace = ensureBranchWorkspace({
1852
- projectId: input.projectId,
1853
- baseUrl: input.baseUrl,
1854
- token: input.token,
1855
- projectRoot: input.projectRoot,
1856
- syncRoot: input.syncRoot,
1857
- branchName: input.message.branchName,
1858
- manifest: input.manifest
1859
- });
1860
- const artifactProcessEnv = await prepareArtifactEnvForShell({
1883
+ const targetProcessEnv = await prepareShellEnvForTarget({
1884
+ target: input.resolvedTarget.target,
1861
1885
  baseUrl: input.baseUrl,
1862
1886
  token: input.token,
1863
- projectId: input.projectId,
1864
- branchName: input.message.branchName,
1865
1887
  sessionId: input.message.sessionId,
1866
- artifactRoot: input.artifactRoot
1867
- });
1868
- const planProcessEnv = preparePlanEnvForShell({
1869
- projectId: input.projectId,
1870
- branchName: input.message.branchName,
1888
+ artifactRoot: input.artifactRoot,
1871
1889
  planRoot: input.planRoot,
1872
1890
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1873
1891
  });
1874
- const commandRoot = input.message.env?.R5D_USE_WORKSPACE_GIT === "1" ? input.workspaceShadowRoot : workspace.branchPath;
1875
- if (input.message.env?.R5D_USE_WORKSPACE_GIT === "1" && !fs.existsSync(path.join(commandRoot, ".git"))) {
1876
- throw new Error("The canonical workspace checkout is not initialized on this worker");
1877
- }
1878
- const cwd = resolveCommandCwd(commandRoot, input.message.cwd);
1892
+ const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
1879
1893
  const subprocess = Bun.spawn(input.message.argv, {
1880
1894
  cwd,
1881
1895
  stdout: "pipe",
@@ -1885,15 +1899,13 @@ async function executeStreamingCommand(input) {
1885
1899
  ...process.env,
1886
1900
  ...githubProcessEnv(),
1887
1901
  ...input.message.env ?? {},
1888
- ...artifactProcessEnv,
1889
- ...planProcessEnv
1902
+ ...targetProcessEnv
1890
1903
  }
1891
1904
  });
1892
1905
  activeProcesses.set(input.message.runId, {
1893
1906
  process: subprocess,
1894
- projectId: input.projectId,
1907
+ target: input.resolvedTarget.target,
1895
1908
  sessionId: input.message.sessionId,
1896
- branchName: input.message.branchName,
1897
1909
  mode: input.message.mode,
1898
1910
  pid: subprocess.pid,
1899
1911
  processGroupId: process.platform === "win32" ? void 0 : subprocess.pid,
@@ -1990,9 +2002,8 @@ function sendWorkerMessage(ws, message) {
1990
2002
  function buildActiveProcessReports() {
1991
2003
  return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
1992
2004
  runId,
1993
- projectId: active.projectId,
2005
+ target: active.target,
1994
2006
  sessionId: active.sessionId,
1995
- branchName: active.branchName,
1996
2007
  platform: process.platform,
1997
2008
  arch: process.arch,
1998
2009
  mode: active.mode,
@@ -2200,21 +2211,12 @@ function resolveHostShell(command, platform = process.platform) {
2200
2211
  }
2201
2212
  async function openPty(input) {
2202
2213
  try {
2203
- const branchPath = ensureBranchWorkspace({
2204
- projectId: input.projectId,
2205
- baseUrl: input.baseUrl,
2206
- token: input.token,
2207
- projectRoot: input.projectRoot,
2208
- syncRoot: input.syncRoot,
2209
- branchName: input.message.branchName,
2210
- manifest: input.manifest
2211
- });
2212
- const planProcessEnv = preparePlanEnvForShell({
2213
- projectId: input.projectId,
2214
- branchName: input.message.branchName,
2214
+ const planProcessEnv = !(input.resolvedTarget.target.type === "workspace" && input.resolvedTarget.target.rootProfile === "canonical_sync") ? preparePlanEnvForShell({
2215
+ target: input.resolvedTarget.target,
2215
2216
  planRoot: input.planRoot,
2216
2217
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2217
- });
2218
+ }) : {};
2219
+ const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
2218
2220
  const shell = resolveHostShell(input.message.command);
2219
2221
  const ptyProcess = createNodePtyBridge({
2220
2222
  file: shell.file,
@@ -2223,14 +2225,13 @@ async function openPty(input) {
2223
2225
  name: "xterm-256color",
2224
2226
  cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
2225
2227
  rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
2226
- cwd: branchPath.branchPath,
2228
+ cwd: input.resolvedTarget.rootPath,
2227
2229
  env: {
2228
2230
  ...process.env,
2229
2231
  ...githubProcessEnv(),
2230
2232
  ...input.message.env ?? {},
2231
2233
  ...planProcessEnv,
2232
- R5D_PROJECT_ID: input.projectId,
2233
- R5D_BRANCH_NAME: input.message.branchName
2234
+ ...targetProcessEnv
2234
2235
  }
2235
2236
  },
2236
2237
  onOpened: () => {
@@ -2268,8 +2269,7 @@ async function openPty(input) {
2268
2269
  });
2269
2270
  activePtys.set(input.message.ptyId, {
2270
2271
  ...ptyProcess,
2271
- projectId: input.projectId,
2272
- branchName: input.message.branchName
2272
+ target: input.resolvedTarget.target
2273
2273
  });
2274
2274
  } catch (error) {
2275
2275
  sendWorkerMessage(input.ws, {
@@ -2388,6 +2388,15 @@ async function startWorker(options) {
2388
2388
  const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2389
2389
  (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2390
2390
  );
2391
+ const resolveMessageTarget = (target) => resolveWorkerSessionTarget({
2392
+ target,
2393
+ baseUrl,
2394
+ token,
2395
+ projectsRoot,
2396
+ syncRoot,
2397
+ workspaceShadowRoot,
2398
+ manifestByProjectId
2399
+ });
2391
2400
  const ensureVisibleWorkspaceCheckouts = (targets, options2 = {}) => {
2392
2401
  const created = [];
2393
2402
  for (const target of targets) {
@@ -2530,7 +2539,8 @@ async function startWorker(options) {
2530
2539
  r5dctlVersion: readInstalledCliVersion("r5dctl"),
2531
2540
  capabilities: {
2532
2541
  updateClis: true,
2533
- canonicalResolverCheckout: true
2542
+ canonicalResolverCheckout: true,
2543
+ browserPortForwarding: true
2534
2544
  },
2535
2545
  projectRoot: projectsRoot,
2536
2546
  artifactRoot,
@@ -2642,7 +2652,11 @@ async function startWorker(options) {
2642
2652
  }
2643
2653
  if (message.type === "workspace_head_updated") {
2644
2654
  if (!activeWorkspaceIncidentId) {
2645
- await runRequestedWorkspaceSync(crypto.randomUUID(), { type: "inbound_head", detail: message.commitHash });
2655
+ await runRequestedWorkspaceSync(
2656
+ crypto.randomUUID(),
2657
+ { type: "inbound_head", detail: message.commitHash },
2658
+ { skipVisibleMirror: true }
2659
+ );
2646
2660
  }
2647
2661
  return;
2648
2662
  }
@@ -2652,8 +2666,6 @@ async function startWorker(options) {
2652
2666
  await syncSessionArtifacts({
2653
2667
  baseUrl,
2654
2668
  token,
2655
- projectId: message.projectId,
2656
- branchName: message.branchName,
2657
2669
  sessionId: message.sessionId,
2658
2670
  artifactRoot
2659
2671
  });
@@ -2683,6 +2695,17 @@ async function startWorker(options) {
2683
2695
  pendingProcessTerminals.delete(message.runId);
2684
2696
  return;
2685
2697
  }
2698
+ if (message.type === "port_forward_connect") {
2699
+ openWorkerPortForwardRelay({
2700
+ baseUrl,
2701
+ token,
2702
+ label,
2703
+ forwardId: message.forwardId,
2704
+ relayConnectionId: message.relayConnectionId,
2705
+ workerPort: message.workerPort
2706
+ });
2707
+ return;
2708
+ }
2686
2709
  if (message.type === "update_clis") {
2687
2710
  if (cliUpdateInProgress) {
2688
2711
  sendWorkerMessage(ws, {
@@ -2762,11 +2785,19 @@ async function startWorker(options) {
2762
2785
  });
2763
2786
  return;
2764
2787
  }
2765
- const manifest = manifestByProjectId.get(message.projectId);
2766
- const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2767
- process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
2788
+ try {
2789
+ const resolvedTarget = resolveMessageTarget(message.target);
2790
+ process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
2768
2791
  `);
2769
- await openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, planRoot, manifest });
2792
+ await openPty({ ws, message, resolvedTarget, planRoot });
2793
+ } catch (error) {
2794
+ sendWorkerMessage(ws, {
2795
+ type: "pty_error",
2796
+ requestId: message.requestId,
2797
+ ptyId: message.ptyId,
2798
+ error: error instanceof Error ? error.message : String(error)
2799
+ });
2800
+ }
2770
2801
  return;
2771
2802
  }
2772
2803
  if (message.type === "pty_input") {
@@ -2792,22 +2823,22 @@ async function startWorker(options) {
2792
2823
  });
2793
2824
  return;
2794
2825
  }
2795
- const manifest = manifestByProjectId.get(message.projectId);
2796
- const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2797
- process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
2826
+ let result;
2827
+ try {
2828
+ const resolvedTarget = resolveMessageTarget(message.target);
2829
+ process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
2798
2830
  `);
2799
- const result = await executeCommand({
2800
- message,
2801
- projectId: message.projectId,
2802
- baseUrl,
2803
- token,
2804
- projectRoot,
2805
- syncRoot,
2806
- artifactRoot,
2807
- planRoot,
2808
- workspaceShadowRoot,
2809
- manifest
2810
- });
2831
+ result = await executeCommand({ message, resolvedTarget, baseUrl, token, artifactRoot, planRoot });
2832
+ } catch (error) {
2833
+ result = {
2834
+ type: "exec_result",
2835
+ requestId: message.requestId,
2836
+ stdout: "",
2837
+ stderr: "",
2838
+ exitCode: 1,
2839
+ error: error instanceof Error ? error.message : String(error)
2840
+ };
2841
+ }
2811
2842
  ws.send(JSON.stringify(result));
2812
2843
  return;
2813
2844
  }
@@ -2827,22 +2858,17 @@ async function startWorker(options) {
2827
2858
  runId: message.runId
2828
2859
  });
2829
2860
  try {
2830
- const manifest = manifestByProjectId.get(message.projectId);
2831
- const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2861
+ const resolvedTarget = resolveMessageTarget(message.target);
2832
2862
  process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
2833
2863
  `);
2834
2864
  void executeStreamingCommand({
2835
2865
  ws,
2836
2866
  message,
2837
- projectId: message.projectId,
2867
+ resolvedTarget,
2838
2868
  baseUrl,
2839
2869
  token,
2840
- projectRoot,
2841
- syncRoot,
2842
2870
  artifactRoot,
2843
- planRoot,
2844
- workspaceShadowRoot,
2845
- manifest
2871
+ planRoot
2846
2872
  }).catch((error) => {
2847
2873
  sendWorkerMessage(ws, {
2848
2874
  type: "exec_start_error",
@@ -2863,18 +2889,14 @@ async function startWorker(options) {
2863
2889
  }
2864
2890
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes") {
2865
2891
  try {
2866
- const manifest = manifestByProjectId.get(message.projectId);
2867
- const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2892
+ const resolvedTarget = resolveMessageTarget(message.target);
2868
2893
  const result = await executeOperation({
2869
2894
  message,
2870
- projectId: message.projectId,
2895
+ resolvedTarget,
2871
2896
  baseUrl,
2872
2897
  token,
2873
- projectRoot,
2874
- syncRoot,
2875
2898
  artifactRoot,
2876
- planRoot,
2877
- manifest
2899
+ planRoot
2878
2900
  });
2879
2901
  ws.send(
2880
2902
  JSON.stringify({
@@ -3001,6 +3023,7 @@ if (isCliEntrypoint()) {
3001
3023
  });
3002
3024
  }
3003
3025
  export {
3026
+ describeWorkerSessionTarget,
3004
3027
  editWorkerTextFile,
3005
3028
  ensureVisibleGitCheckout,
3006
3029
  findWorkerFiles,
@@ -3009,11 +3032,14 @@ export {
3009
3032
  isArtifactEnvPath,
3010
3033
  listWorkerDirectory,
3011
3034
  prepareArtifactEnvForShell,
3035
+ prepareBuiltInToolPathsForTarget,
3012
3036
  preparePlanEnvForShell,
3037
+ prepareShellEnvForTarget,
3013
3038
  readWorkerImageFile,
3014
3039
  readWorkerTextFile,
3015
3040
  resolveHostShell,
3016
3041
  resolveWorkerFilePath,
3042
+ resolveWorkerSessionTarget,
3017
3043
  syncSessionArtifacts,
3018
3044
  visibleCheckoutGitTestHarness,
3019
3045
  visibleCheckoutRemoteFor,