@ricsam/r5d-worker 0.0.45 → 0.0.47

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
@@ -21,7 +21,8 @@ import {
21
21
  WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
22
22
  WorkspaceSyncSingleFlight,
23
23
  calculateWorkspaceDiffFingerprint,
24
- synchronizeWorkspace
24
+ synchronizeWorkspace,
25
+ workspaceProjectsForSync
25
26
  } from "./workspace-sync.mjs";
26
27
  class WorkerServerUnavailableError extends Error {
27
28
  name = "WorkerServerUnavailableError";
@@ -246,7 +247,25 @@ async function readResponseText(response) {
246
247
  }
247
248
  }
248
249
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
250
+ const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
251
+ const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
249
252
  const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
253
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
254
+ function parseBuiltInToolPath(inputPath) {
255
+ const normalized = inputPath.replace(/\\/g, "/");
256
+ const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
257
+ if (!match) {
258
+ return null;
259
+ }
260
+ const ref = `$${match[1] ?? match[2]}`;
261
+ if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
262
+ throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
263
+ }
264
+ return {
265
+ ref,
266
+ relativePath: match[3] ?? ""
267
+ };
268
+ }
250
269
  function validateArtifactSessionId(sessionId) {
251
270
  if (!/^(migration-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
252
271
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -255,7 +274,7 @@ function validateArtifactSessionId(sessionId) {
255
274
  function assertInsideRoot(rootPath, candidatePath, label) {
256
275
  const relative = path.relative(rootPath, candidatePath);
257
276
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
258
- throw new Error(`${label} escapes sync directory`);
277
+ throw new Error(`${label} escapes its allowed root`);
259
278
  }
260
279
  }
261
280
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -288,7 +307,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
288
307
  return env;
289
308
  }
290
309
  function isArtifactEnvPath(filePath) {
291
- return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
310
+ try {
311
+ return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
312
+ } catch {
313
+ return false;
314
+ }
292
315
  }
293
316
  function artifactApiBasePath(projectId, branchName, sessionId) {
294
317
  return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
@@ -422,40 +445,45 @@ async function syncSessionArtifacts(input) {
422
445
  void queued.then(release, release);
423
446
  return queued;
424
447
  }
425
- async function resolveArtifactEnvFilePath(input) {
426
- if (!isArtifactEnvPath(input.filePath)) {
427
- return null;
448
+ async function prepareBuiltInToolPaths(input) {
449
+ const parsed = parseBuiltInToolPath(input.filePath);
450
+ if (!parsed) {
451
+ return void 0;
428
452
  }
429
- if (!input.sessionId) {
430
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
453
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
454
+ if (input.access === "write") {
455
+ throw new Error(
456
+ `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
457
+ );
458
+ }
459
+ if (!input.sessionId) {
460
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
461
+ }
462
+ return {
463
+ artifactsDir: await syncSessionArtifacts({
464
+ baseUrl: input.baseUrl,
465
+ token: input.token,
466
+ projectId: input.projectId,
467
+ branchName: input.branchName,
468
+ sessionId: input.sessionId,
469
+ artifactRoot: input.artifactRoot
470
+ })
471
+ };
431
472
  }
432
- if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
433
- throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
473
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
474
+ fs.mkdirSync(plansDir, { recursive: true });
475
+ if (parsed.ref === R5D_PLANS_DIR_REF) {
476
+ return { plansDir };
434
477
  }
435
- const targetDir = await syncSessionArtifacts({
436
- baseUrl: input.baseUrl,
437
- token: input.token,
438
- projectId: input.projectId,
439
- branchName: input.branchName,
440
- sessionId: input.sessionId,
441
- artifactRoot: input.artifactRoot
442
- });
443
- const relativePath = path.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
444
- if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
445
- throw new Error(`Invalid artifact path: ${input.filePath}`);
478
+ if (!input.activePlanId) {
479
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
446
480
  }
447
- const absolutePath = path.resolve(targetDir, ...relativePath.split("/"));
448
- assertInsideRoot(targetDir, absolutePath, "Artifact path");
481
+ validatePlanId(input.activePlanId);
449
482
  return {
450
- absolutePath,
451
- virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
483
+ plansDir,
484
+ activePlanFile: path.join(plansDir, `${input.activePlanId}.plan.md`)
452
485
  };
453
486
  }
454
- function rejectArtifactWritePath(filePath) {
455
- if (isArtifactEnvPath(filePath)) {
456
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
457
- }
458
- }
459
487
  async function prepareArtifactEnvForShell(input) {
460
488
  if (!input.sessionId) {
461
489
  return {};
@@ -551,6 +579,14 @@ function validatePlanId(planId) {
551
579
  throw new Error(`Invalid plan id from server: ${planId}`);
552
580
  }
553
581
  }
582
+ const NON_RECURSIVE_GIT_CONFIG_ARGS = [
583
+ "-c",
584
+ "submodule.recurse=false",
585
+ "-c",
586
+ "fetch.recurseSubmodules=false",
587
+ "-c",
588
+ "push.recurseSubmodules=false"
589
+ ];
554
590
  function gitExtraHeaderConfigKey(extraHeaderUrl) {
555
591
  return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
556
592
  }
@@ -558,10 +594,23 @@ function gitAuthArgs(auth) {
558
594
  if (!auth) {
559
595
  return [];
560
596
  }
561
- return ["-c", `${gitExtraHeaderConfigKey(auth.extraHeaderUrl)}=${auth.header}`];
597
+ const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
598
+ return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
599
+ }
600
+ const visibleCheckoutGitTestHarness = {
601
+ gitAuthArgs,
602
+ commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args],
603
+ cloneArgs: (remoteUrl, branchPath) => ["clone", "--no-recurse-submodules", "--origin", "origin", remoteUrl, branchPath],
604
+ fetchArgs: (...args) => ["fetch", "--no-recurse-submodules", ...args]
605
+ };
606
+ function visibleCheckoutCloneArgs(remoteUrl, branchPath) {
607
+ return visibleCheckoutGitTestHarness.cloneArgs(remoteUrl, branchPath);
608
+ }
609
+ function visibleCheckoutFetchArgs(...args) {
610
+ return visibleCheckoutGitTestHarness.fetchArgs(...args);
562
611
  }
563
612
  function runGit(args, options = {}) {
564
- const command = ["git", ...gitAuthArgs(options.auth), ...args];
613
+ const command = visibleCheckoutGitTestHarness.commandArgs(args, options.auth);
565
614
  const result = Bun.spawnSync(command, {
566
615
  cwd: options.cwd,
567
616
  stdout: "pipe",
@@ -606,10 +655,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
606
655
  throw new Error(`Invalid project file path: ${inputPath}`);
607
656
  }
608
657
  }
609
- function resolveWorkerFilePath(branchPath, inputPath) {
658
+ function assertPathInsideRealRoot(rootPath, candidatePath, label) {
659
+ const resolvedRoot = path.resolve(rootPath);
660
+ const resolvedCandidate = path.resolve(candidatePath);
661
+ assertInsideRoot(resolvedRoot, resolvedCandidate, label);
662
+ let existingPath = resolvedCandidate;
663
+ while (!fs.existsSync(existingPath)) {
664
+ const parentPath = path.dirname(existingPath);
665
+ if (parentPath === existingPath) {
666
+ throw new Error(`${label} root does not exist: ${rootPath}`);
667
+ }
668
+ existingPath = parentPath;
669
+ }
670
+ const realRoot = fs.realpathSync(resolvedRoot);
671
+ const realExistingPath = fs.realpathSync(existingPath);
672
+ assertInsideRoot(realRoot, realExistingPath, label);
673
+ }
674
+ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
675
+ if (parsed.relativePath.split("/").includes("..")) {
676
+ throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
677
+ }
678
+ let rootPath;
679
+ let absolutePath;
680
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
681
+ rootPath = builtInPaths?.artifactsDir;
682
+ } else if (parsed.ref === R5D_PLANS_DIR_REF) {
683
+ rootPath = builtInPaths?.plansDir;
684
+ } else {
685
+ if (parsed.relativePath) {
686
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
687
+ }
688
+ rootPath = builtInPaths?.plansDir;
689
+ absolutePath = builtInPaths?.activePlanFile;
690
+ }
691
+ if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
692
+ const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
693
+ throw new Error(`${parsed.ref} paths require ${requirement}`);
694
+ }
695
+ const normalizedPath = parsed.relativePath ? path.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
696
+ const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
697
+ absolutePath ??= normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
698
+ assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
699
+ return {
700
+ absolutePath,
701
+ displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
702
+ repoRelativePath: null,
703
+ scope: "virtual",
704
+ virtualRootPath: path.resolve(rootPath)
705
+ };
706
+ }
707
+ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
610
708
  if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
611
709
  throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
612
710
  }
711
+ const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
712
+ if (parsedBuiltInPath) {
713
+ return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
714
+ }
613
715
  const resolvedBranchPath = path.resolve(branchPath);
614
716
  if (path.isAbsolute(inputPath)) {
615
717
  const absolutePath2 = path.resolve(inputPath);
@@ -652,8 +754,28 @@ function resolveWorkerFilePath(branchPath, inputPath) {
652
754
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
653
755
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
654
756
  }
655
- function githubRemoteUrlFor(baseUrl, projectId, manifest) {
656
- return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
757
+ function visibleCheckoutRemoteFor(input) {
758
+ const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
759
+ const isCanonicalManifestBranch = Boolean(canonicalCheckout);
760
+ const usesCanonicalRemote = isCanonicalManifestBranch || !input.manifest?.repoHttpUrl;
761
+ if (usesCanonicalRemote) {
762
+ return {
763
+ remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
764
+ authHeader: `Authorization: Bearer ${input.token}`,
765
+ persistAuth: false,
766
+ reconcileExistingOrigin: isCanonicalManifestBranch,
767
+ requireRemoteBranch: isCanonicalManifestBranch,
768
+ requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
769
+ };
770
+ }
771
+ return {
772
+ remoteUrl: input.manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
773
+ authHeader: input.manifest?.repoAuthHeader ?? null,
774
+ persistAuth: true,
775
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl),
776
+ requireRemoteBranch: false,
777
+ requiredBaseCommit: null
778
+ };
657
779
  }
658
780
  function gitExtraHeaderUrlForRemote(remoteUrl) {
659
781
  try {
@@ -682,6 +804,10 @@ function ensureOriginRemote(branchPath, remoteUrl) {
682
804
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
683
805
  }
684
806
  }
807
+ function originRemoteUrl(branchPath) {
808
+ if (!tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) return null;
809
+ return runGit(["remote", "get-url", "origin"], { cwd: branchPath });
810
+ }
685
811
  function shellQuote(value) {
686
812
  return `'${value.replace(/'/g, "'\\''")}'`;
687
813
  }
@@ -750,6 +876,18 @@ function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
750
876
  }
751
877
  runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
752
878
  }
879
+ function clearVisibleGitAuthentication(branchPath) {
880
+ let extraHeaderKeys = [];
881
+ try {
882
+ extraHeaderKeys = runGit(["config", "--local", "--name-only", "--get-regexp", "^http\\..*extraheader$"], { cwd: branchPath }).split(/\r?\n/).filter(Boolean);
883
+ } catch {
884
+ }
885
+ for (const key of extraHeaderKeys) {
886
+ tryGit(["config", "--local", "--unset-all", key], { cwd: branchPath });
887
+ }
888
+ runGit(["config", "--local", "--replace-all", "credential.helper", ""], { cwd: branchPath });
889
+ tryGit(["config", "--local", "--unset-all", "credential.useHttpPath"], { cwd: branchPath });
890
+ }
753
891
  function dockerConfigPath() {
754
892
  return path.join(os.homedir(), ".docker", "config.json");
755
893
  }
@@ -838,18 +976,58 @@ function checkoutVisibleBranch(input) {
838
976
  });
839
977
  const clean = !hasWorktreeChanges(input.branchPath);
840
978
  if (remoteBranchExists && clean) {
841
- runGit(["checkout", "-B", input.branchName, `origin/${input.branchName}`], { cwd: input.branchPath });
979
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
980
+ cwd: input.branchPath
981
+ });
842
982
  return;
843
983
  }
844
984
  if (branchExists) {
845
- runGit(["checkout", input.branchName], { cwd: input.branchPath });
985
+ runGit(["checkout", "--no-recurse-submodules", input.branchName], { cwd: input.branchPath });
846
986
  return;
847
987
  }
848
988
  if (defaultRemoteExists) {
849
- runGit(["checkout", "-B", input.branchName, `origin/${input.defaultBranch}`], { cwd: input.branchPath });
989
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.defaultBranch}`], {
990
+ cwd: input.branchPath
991
+ });
850
992
  return;
851
993
  }
852
- runGit(["checkout", "-B", input.branchName], { cwd: input.branchPath });
994
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
995
+ }
996
+ function migrateExistingCheckoutToRequiredRemote(input) {
997
+ if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
998
+ if (hasWorktreeChanges(input.branchPath)) {
999
+ throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
1000
+ }
1001
+ runGit(
1002
+ visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1003
+ {
1004
+ cwd: input.branchPath,
1005
+ auth: input.remoteAuth
1006
+ }
1007
+ );
1008
+ const canonicalRemoteRef = `refs/remotes/origin/${input.branchName}`;
1009
+ if (!tryGit(["show-ref", "--verify", "--quiet", canonicalRemoteRef], { cwd: input.branchPath })) {
1010
+ throw new Error(`Canonical branch ${input.branchName} is unavailable during checkout migration`);
1011
+ }
1012
+ if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
1013
+ throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1014
+ }
1015
+ if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
1016
+ const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1017
+ runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1018
+ }
1019
+ ensureOriginRemote(input.branchPath, input.remoteUrl);
1020
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1021
+ cwd: input.branchPath
1022
+ });
1023
+ }
1024
+ function assertCheckoutDerivedFromRequiredBase(input) {
1025
+ const remoteRevision = `refs/remotes/origin/${input.branchName}`;
1026
+ for (const revision of [remoteRevision, "HEAD"]) {
1027
+ if (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, revision], { cwd: input.branchPath })) {
1028
+ throw new Error(`Canonical checkout ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1029
+ }
1030
+ }
853
1031
  }
854
1032
  function ensureVisibleGitCheckout(input) {
855
1033
  validateBranchName(input.branchName);
@@ -859,40 +1037,114 @@ function ensureVisibleGitCheckout(input) {
859
1037
  if (createdCheckout) {
860
1038
  fs.rmSync(branchPath, { recursive: true, force: true });
861
1039
  fs.mkdirSync(path.dirname(branchPath), { recursive: true });
862
- if (!tryGit(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
1040
+ const cloned = tryGit(visibleCheckoutCloneArgs(input.remoteUrl, branchPath), {
1041
+ auth: input.remoteAuth
1042
+ });
1043
+ if (!cloned && input.requireRemoteBranch) {
1044
+ fs.rmSync(branchPath, { recursive: true, force: true });
1045
+ throw new Error(`Failed to clone canonical checkout for branch ${input.branchName}`);
1046
+ }
1047
+ if (!cloned) {
863
1048
  fs.mkdirSync(branchPath, { recursive: true });
864
1049
  runGit(["init"], { cwd: branchPath });
865
1050
  }
866
- ensureOriginRemote(branchPath, input.githubRemoteUrl);
867
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1051
+ ensureOriginRemote(branchPath, input.remoteUrl);
1052
+ if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1053
+ else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
868
1054
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
869
- tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1055
+ tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
1056
+ if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1057
+ fs.rmSync(branchPath, { recursive: true, force: true });
1058
+ throw new Error(`Canonical branch ${input.branchName} is unavailable after clone`);
1059
+ }
870
1060
  checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1061
+ if (input.requiredBaseCommit) {
1062
+ try {
1063
+ assertCheckoutDerivedFromRequiredBase({
1064
+ branchPath,
1065
+ branchName: input.branchName,
1066
+ requiredBaseCommit: input.requiredBaseCommit
1067
+ });
1068
+ } catch (error) {
1069
+ fs.rmSync(branchPath, { recursive: true, force: true });
1070
+ throw error;
1071
+ }
1072
+ }
871
1073
  return branchPath;
872
1074
  }
1075
+ if (input.requireRemoteBranch) {
1076
+ if (input.allowRequiredRemoteMigration === false && originRemoteUrl(branchPath) !== input.remoteUrl) {
1077
+ throw new Error(`Canonical checkout ${input.branchName} no longer uses its required remote`);
1078
+ }
1079
+ migrateExistingCheckoutToRequiredRemote({
1080
+ branchPath,
1081
+ branchName: input.branchName,
1082
+ remoteUrl: input.remoteUrl,
1083
+ remoteAuth: input.remoteAuth,
1084
+ requiredBaseCommit: input.requiredBaseCommit
1085
+ });
1086
+ runGit(
1087
+ visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1088
+ {
1089
+ cwd: branchPath,
1090
+ auth: input.remoteAuth
1091
+ }
1092
+ );
1093
+ }
873
1094
  if (input.reconcileExistingOrigin) {
874
- ensureOriginRemote(branchPath, input.githubRemoteUrl);
1095
+ ensureOriginRemote(branchPath, input.remoteUrl);
1096
+ }
1097
+ if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1098
+ throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
875
1099
  }
876
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1100
+ if (input.requiredBaseCommit) {
1101
+ assertCheckoutDerivedFromRequiredBase({
1102
+ branchPath,
1103
+ branchName: input.branchName,
1104
+ requiredBaseCommit: input.requiredBaseCommit
1105
+ });
1106
+ }
1107
+ if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1108
+ else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
877
1109
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
878
1110
  return branchPath;
879
1111
  }
880
1112
  function ensureBranchWorkspace(input) {
881
1113
  const defaultBranch = input.manifest?.defaultBranch || "main";
882
- const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
883
- const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
1114
+ const remote = visibleCheckoutRemoteFor(input);
884
1115
  return {
885
1116
  branchPath: ensureVisibleGitCheckout({
886
1117
  projectRoot: input.projectRoot,
887
1118
  branchName: input.branchName,
888
- githubRemoteUrl,
889
- githubAuth,
890
- githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
1119
+ remoteUrl: remote.remoteUrl,
1120
+ remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
1121
+ persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
891
1122
  defaultBranch,
892
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
1123
+ reconcileExistingOrigin: remote.reconcileExistingOrigin,
1124
+ requireRemoteBranch: remote.requireRemoteBranch,
1125
+ requiredBaseCommit: remote.requiredBaseCommit,
1126
+ clearPersistentAuth: !remote.persistAuth
893
1127
  })
894
1128
  };
895
1129
  }
1130
+ function workspaceSyncCheckoutTargets(input) {
1131
+ const selected = /* @__PURE__ */ new Map();
1132
+ const add = (target) => {
1133
+ selected.set(`${target.projectId}\0${target.branchName}`, target);
1134
+ };
1135
+ if (input.canonicalCheckoutOnly) {
1136
+ add(input.canonicalCheckoutOnly);
1137
+ } else if (input.includeAllManifestCheckouts) {
1138
+ for (const project of input.projects) {
1139
+ for (const branchName of project.branches) add({ projectId: project.projectId, branchName });
1140
+ }
1141
+ } else {
1142
+ for (const target of input.pendingTargets) add(target);
1143
+ }
1144
+ return [...selected.values()].sort(
1145
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
1146
+ );
1147
+ }
896
1148
  function resolveCommandCwd(branchPath, cwd) {
897
1149
  if (!cwd) {
898
1150
  return branchPath;
@@ -1066,13 +1318,13 @@ async function withFileMutationQueue(key, operation) {
1066
1318
  }
1067
1319
  }
1068
1320
  function mutationQueueKey(projectId, branchName, resolved) {
1069
- if (resolved.scope === "host") {
1070
- return `host:${path.normalize(resolved.absolutePath)}`;
1321
+ if (resolved.scope !== "project") {
1322
+ return `${resolved.scope}:${path.normalize(resolved.absolutePath)}`;
1071
1323
  }
1072
1324
  return `project:${projectId}:${branchName}:${path.posix.normalize(resolved.repoRelativePath)}`;
1073
1325
  }
1074
- function readWorkerTextFile(branchPath, filePath, offset, limit) {
1075
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1326
+ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
1327
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1076
1328
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1077
1329
  throw new Error(`File not found: ${resolved.displayPath}`);
1078
1330
  }
@@ -1086,8 +1338,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1086
1338
  ...formatted
1087
1339
  };
1088
1340
  }
1089
- function writeWorkerTextFile(branchPath, filePath, content) {
1090
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1341
+ function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
1342
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1091
1343
  fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1092
1344
  fs.writeFileSync(resolved.absolutePath, content, "utf8");
1093
1345
  return {
@@ -1096,8 +1348,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
1096
1348
  gitBlobHash: getGitBlobHashForContent(content)
1097
1349
  };
1098
1350
  }
1099
- function editWorkerTextFile(branchPath, filePath, edits) {
1100
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1351
+ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
1352
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1101
1353
  if (!Array.isArray(edits) || edits.length === 0) {
1102
1354
  throw new Error("edit requires at least one replacement");
1103
1355
  }
@@ -1144,46 +1396,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
1144
1396
  };
1145
1397
  }
1146
1398
  async function executeReadFileOperation(input) {
1147
- const artifact = await resolveArtifactEnvFilePath({
1399
+ const builtInPaths = await prepareBuiltInToolPaths({
1148
1400
  filePath: input.message.filePath,
1149
1401
  baseUrl: input.baseUrl,
1150
1402
  token: input.token,
1151
1403
  projectId: input.projectId,
1152
1404
  branchName: input.message.branchName,
1153
1405
  sessionId: input.message.sessionId,
1154
- artifactRoot: input.artifactRoot
1406
+ activePlanId: input.message.activePlanId,
1407
+ artifactRoot: input.artifactRoot,
1408
+ planRoot: input.planRoot,
1409
+ access: "read"
1155
1410
  });
1156
- if (artifact) {
1157
- if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1158
- throw new Error(`File not found: ${artifact.virtualPath}`);
1159
- }
1160
- const buffer = fs.readFileSync(artifact.absolutePath);
1161
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1162
- return {
1163
- type: "read",
1164
- kind: "text",
1165
- file: artifact.virtualPath,
1166
- gitBlobHash: getGitBlobHashForContent(buffer),
1167
- ...formatted
1168
- };
1169
- }
1170
1411
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1171
- return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1412
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1172
1413
  }
1173
1414
  async function executeWriteFileOperation(input) {
1174
- rejectArtifactWritePath(input.message.filePath);
1415
+ const builtInPaths = await prepareBuiltInToolPaths({
1416
+ filePath: input.message.filePath,
1417
+ baseUrl: input.baseUrl,
1418
+ token: input.token,
1419
+ projectId: input.projectId,
1420
+ branchName: input.message.branchName,
1421
+ sessionId: input.message.sessionId,
1422
+ activePlanId: input.message.activePlanId,
1423
+ artifactRoot: input.artifactRoot,
1424
+ planRoot: input.planRoot,
1425
+ access: "write"
1426
+ });
1175
1427
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1176
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1428
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1177
1429
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1178
- return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1430
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
1179
1431
  });
1180
1432
  }
1181
1433
  async function executeEditFileOperation(input) {
1182
- rejectArtifactWritePath(input.message.filePath);
1434
+ const builtInPaths = await prepareBuiltInToolPaths({
1435
+ filePath: input.message.filePath,
1436
+ baseUrl: input.baseUrl,
1437
+ token: input.token,
1438
+ projectId: input.projectId,
1439
+ branchName: input.message.branchName,
1440
+ sessionId: input.message.sessionId,
1441
+ activePlanId: input.message.activePlanId,
1442
+ artifactRoot: input.artifactRoot,
1443
+ planRoot: input.planRoot,
1444
+ access: "write"
1445
+ });
1183
1446
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1184
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1447
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1185
1448
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1186
- return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1449
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
1187
1450
  });
1188
1451
  }
1189
1452
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1204,16 +1467,20 @@ function normalizeFindPattern(pattern) {
1204
1467
  function walkWorkerEntries(branchPath, start) {
1205
1468
  const entries = [];
1206
1469
  const visitedDirectories = /* @__PURE__ */ new Set();
1470
+ const realVirtualRoot = start.scope === "virtual" ? fs.realpathSync(start.virtualRootPath) : null;
1207
1471
  const visit = (absolutePath, isRoot) => {
1208
1472
  let stat;
1209
1473
  try {
1210
1474
  stat = fs.statSync(absolutePath);
1475
+ if (realVirtualRoot) {
1476
+ assertInsideRoot(realVirtualRoot, fs.realpathSync(absolutePath), `${start.displayPath} path`);
1477
+ }
1211
1478
  } catch (error) {
1212
1479
  if (isRoot) throw error;
1213
1480
  return;
1214
1481
  }
1215
- const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : path.resolve(absolutePath);
1216
1482
  const hostRelativePath = path.relative(start.absolutePath, absolutePath).split(path.sep).join(path.posix.sep);
1483
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : path.resolve(absolutePath);
1217
1484
  const matchPath = start.scope === "project" ? displayPath : hostRelativePath || path.basename(start.absolutePath);
1218
1485
  entries.push({ absolutePath, displayPath, matchPath, stat });
1219
1486
  if (!stat.isDirectory()) return;
@@ -1244,8 +1511,8 @@ function walkWorkerEntries(branchPath, start) {
1244
1511
  function isProbablyText(bytes) {
1245
1512
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1246
1513
  }
1247
- function grepWorkerFiles(branchPath, input) {
1248
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1514
+ function grepWorkerFiles(branchPath, input, builtInPaths) {
1515
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1249
1516
  if (!fs.existsSync(start.absolutePath)) {
1250
1517
  throw new Error(`Path not found: ${start.displayPath}`);
1251
1518
  }
@@ -1291,17 +1558,34 @@ function grepWorkerFiles(branchPath, input) {
1291
1558
  };
1292
1559
  }
1293
1560
  async function executeGrepOperation(input) {
1294
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1295
- return grepWorkerFiles(workspace.branchPath, {
1296
- pattern: input.message.pattern,
1297
- path: input.message.path,
1298
- glob: input.message.glob,
1299
- caseSensitive: input.message.caseSensitive,
1300
- limit: input.message.limit
1561
+ const toolPath = input.message.path ?? ".";
1562
+ const builtInPaths = await prepareBuiltInToolPaths({
1563
+ filePath: toolPath,
1564
+ baseUrl: input.baseUrl,
1565
+ token: input.token,
1566
+ projectId: input.projectId,
1567
+ branchName: input.message.branchName,
1568
+ sessionId: input.message.sessionId,
1569
+ activePlanId: input.message.activePlanId,
1570
+ artifactRoot: input.artifactRoot,
1571
+ planRoot: input.planRoot,
1572
+ access: "read"
1301
1573
  });
1574
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1575
+ return grepWorkerFiles(
1576
+ workspace.branchPath,
1577
+ {
1578
+ pattern: input.message.pattern,
1579
+ path: input.message.path,
1580
+ glob: input.message.glob,
1581
+ caseSensitive: input.message.caseSensitive,
1582
+ limit: input.message.limit
1583
+ },
1584
+ builtInPaths
1585
+ );
1302
1586
  }
1303
- function findWorkerFiles(branchPath, input) {
1304
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1587
+ function findWorkerFiles(branchPath, input, builtInPaths) {
1588
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1305
1589
  if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
1306
1590
  throw new Error(`Directory not found: ${start.displayPath}`);
1307
1591
  }
@@ -1335,16 +1619,33 @@ function findWorkerFiles(branchPath, input) {
1335
1619
  };
1336
1620
  }
1337
1621
  async function executeFindOperation(input) {
1338
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1339
- return findWorkerFiles(workspace.branchPath, {
1340
- pattern: input.message.pattern,
1341
- path: input.message.path,
1342
- entryType: input.message.entryType,
1343
- limit: input.message.limit
1622
+ const toolPath = input.message.path ?? ".";
1623
+ const builtInPaths = await prepareBuiltInToolPaths({
1624
+ filePath: toolPath,
1625
+ baseUrl: input.baseUrl,
1626
+ token: input.token,
1627
+ projectId: input.projectId,
1628
+ branchName: input.message.branchName,
1629
+ sessionId: input.message.sessionId,
1630
+ activePlanId: input.message.activePlanId,
1631
+ artifactRoot: input.artifactRoot,
1632
+ planRoot: input.planRoot,
1633
+ access: "read"
1344
1634
  });
1635
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1636
+ return findWorkerFiles(
1637
+ workspace.branchPath,
1638
+ {
1639
+ pattern: input.message.pattern,
1640
+ path: input.message.path,
1641
+ entryType: input.message.entryType,
1642
+ limit: input.message.limit
1643
+ },
1644
+ builtInPaths
1645
+ );
1345
1646
  }
1346
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1347
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
1647
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
1648
+ const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
1348
1649
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
1349
1650
  throw new Error(`Directory not found: ${resolved.displayPath}`);
1350
1651
  }
@@ -1362,11 +1663,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1362
1663
  };
1363
1664
  }
1364
1665
  async function executeLsOperation(input) {
1666
+ const toolPath = input.message.path ?? ".";
1667
+ const builtInPaths = await prepareBuiltInToolPaths({
1668
+ filePath: toolPath,
1669
+ baseUrl: input.baseUrl,
1670
+ token: input.token,
1671
+ projectId: input.projectId,
1672
+ branchName: input.message.branchName,
1673
+ sessionId: input.message.sessionId,
1674
+ activePlanId: input.message.activePlanId,
1675
+ artifactRoot: input.artifactRoot,
1676
+ planRoot: input.planRoot,
1677
+ access: "read"
1678
+ });
1365
1679
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1366
- return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1680
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
1367
1681
  }
1368
- function readWorkerImageFile(branchPath, filePath) {
1369
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1682
+ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1683
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1370
1684
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1371
1685
  throw new Error(`File not found: ${resolved.displayPath}`);
1372
1686
  }
@@ -1388,38 +1702,20 @@ function readWorkerImageFile(branchPath, filePath) {
1388
1702
  };
1389
1703
  }
1390
1704
  async function executeViewFileBytesOperation(input) {
1391
- const artifact = await resolveArtifactEnvFilePath({
1705
+ const builtInPaths = await prepareBuiltInToolPaths({
1392
1706
  filePath: input.message.filePath,
1393
1707
  baseUrl: input.baseUrl,
1394
1708
  token: input.token,
1395
1709
  projectId: input.projectId,
1396
1710
  branchName: input.message.branchName,
1397
1711
  sessionId: input.message.sessionId,
1398
- artifactRoot: input.artifactRoot
1712
+ activePlanId: input.message.activePlanId,
1713
+ artifactRoot: input.artifactRoot,
1714
+ planRoot: input.planRoot,
1715
+ access: "read"
1399
1716
  });
1400
- if (artifact) {
1401
- if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1402
- throw new Error(`File not found: ${artifact.virtualPath}`);
1403
- }
1404
- const extension = path.extname(artifact.absolutePath).toLowerCase();
1405
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1406
- if (!mediaType) {
1407
- throw new Error("read supports PNG and JPEG image inputs only");
1408
- }
1409
- const bytes = fs.readFileSync(artifact.absolutePath);
1410
- const dimensions = readImageDimensions(bytes, mediaType);
1411
- return {
1412
- type: "view_file_bytes",
1413
- filePath: artifact.virtualPath,
1414
- mediaType,
1415
- base64: bytes.toString("base64"),
1416
- fileSizeBytes: bytes.length,
1417
- width: dimensions.width,
1418
- height: dimensions.height
1419
- };
1420
- }
1421
1717
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1422
- return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1718
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
1423
1719
  }
1424
1720
  async function executeOperation(input) {
1425
1721
  switch (input.message.type) {
@@ -2058,6 +2354,7 @@ async function startWorker(options) {
2058
2354
  fs.mkdirSync(artifactRoot, { recursive: true });
2059
2355
  fs.mkdirSync(planRoot, { recursive: true });
2060
2356
  const manifestByProjectId = /* @__PURE__ */ new Map();
2357
+ const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2061
2358
  const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
2062
2359
  let workspaceRemoteUrl = null;
2063
2360
  let activeWorkspaceIncidentId = null;
@@ -2071,6 +2368,7 @@ async function startWorker(options) {
2071
2368
  let reloadAfterClose = false;
2072
2369
  const workspaceSyncInput = (trigger, overrides = {}) => {
2073
2370
  if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2371
+ const projects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
2074
2372
  return {
2075
2373
  workerLabel: label,
2076
2374
  remoteUrl: workspaceRemoteUrl,
@@ -2078,7 +2376,7 @@ async function startWorker(options) {
2078
2376
  projectsRoot,
2079
2377
  plansRoot: planRoot,
2080
2378
  shadowRoot: workspaceShadowRoot,
2081
- projects: [...manifestByProjectId.values()],
2379
+ projects,
2082
2380
  trigger,
2083
2381
  ...overrides
2084
2382
  };
@@ -2086,37 +2384,89 @@ async function startWorker(options) {
2086
2384
  const sendWorkspaceSyncResult = (requestId, result) => {
2087
2385
  sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2088
2386
  };
2089
- const ensureAllVisibleWorkspaceCheckouts = () => {
2387
+ const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2388
+ const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2389
+ (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2390
+ );
2391
+ const ensureVisibleWorkspaceCheckouts = (targets, options2 = {}) => {
2090
2392
  const created = [];
2091
- for (const manifest of manifestByProjectId.values()) {
2092
- for (const branchName of manifest.branches) {
2093
- const branchPath = path.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2094
- const existed = fs.existsSync(path.join(branchPath, ".git"));
2095
- const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2096
- ensureVisibleGitCheckout({
2097
- projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2098
- branchName,
2099
- githubRemoteUrl,
2100
- githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2101
- githubAuthHeader: manifest.repoAuthHeader,
2102
- defaultBranch: manifest.defaultBranch || "main",
2103
- reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2104
- });
2105
- if (!existed) created.push({ projectId: manifest.projectId, branchName });
2393
+ for (const target of targets) {
2394
+ const manifest = manifestByProjectId.get(target.projectId);
2395
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2396
+ if (!manifest || !manifest.branches.includes(target.branchName)) {
2397
+ pendingManifestCheckouts.delete(key);
2398
+ continue;
2106
2399
  }
2400
+ const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2401
+ const branchPath = path.join(projectRoot, target.branchName);
2402
+ const existed = hasNormalVisibleGitDir(branchPath);
2403
+ const remote = visibleCheckoutRemoteFor({
2404
+ baseUrl,
2405
+ token,
2406
+ projectId: manifest.projectId,
2407
+ branchName: target.branchName,
2408
+ manifest
2409
+ });
2410
+ ensureVisibleGitCheckout({
2411
+ projectRoot,
2412
+ branchName: target.branchName,
2413
+ remoteUrl: remote.remoteUrl,
2414
+ remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
2415
+ persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
2416
+ defaultBranch: manifest.defaultBranch || "main",
2417
+ reconcileExistingOrigin: remote.reconcileExistingOrigin,
2418
+ requireRemoteBranch: remote.requireRemoteBranch,
2419
+ requiredBaseCommit: remote.requiredBaseCommit,
2420
+ allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requireRemoteBranch),
2421
+ clearPersistentAuth: !remote.persistAuth
2422
+ });
2423
+ if (!existed) created.push(target);
2107
2424
  }
2108
2425
  return created;
2109
2426
  };
2110
2427
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2111
2428
  workspaceSyncRequestsInFlight += 1;
2112
2429
  try {
2113
- const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2114
- const result = await workspaceSyncSingleFlight.run(
2115
- workspaceSyncInput(trigger, {
2430
+ let pendingTargets = [];
2431
+ const result = await workspaceSyncSingleFlight.runPrepared(() => {
2432
+ const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2433
+ const syncProjects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
2434
+ const allowedCheckoutKeys = new Set(
2435
+ syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
2436
+ );
2437
+ const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
2438
+ (target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2439
+ );
2440
+ let checkoutTargets = [];
2441
+ if (shouldEnsureCheckouts) {
2442
+ checkoutTargets = workspaceSyncCheckoutTargets({
2443
+ projects: syncProjects,
2444
+ pendingTargets: scopedPendingTargets,
2445
+ includeAllManifestCheckouts: trigger.type === "connect",
2446
+ ...trigger.canonicalCheckoutOnly && trigger.projectId && trigger.branchName ? { canonicalCheckoutOnly: { projectId: trigger.projectId, branchName: trigger.branchName } } : {}
2447
+ });
2448
+ }
2449
+ pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
2450
+ const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets, {
2451
+ strictCanonicalRevalidation: true
2452
+ });
2453
+ const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2454
+ for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2455
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2456
+ if (allowedCheckoutKeys.has(key)) newVisibleCheckoutByKey.set(key, target);
2457
+ }
2458
+ const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2459
+ return workspaceSyncInput(trigger, {
2116
2460
  ...overrides,
2117
2461
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2118
- })
2119
- );
2462
+ });
2463
+ });
2464
+ if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
2465
+ for (const target of pendingTargets) {
2466
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2467
+ if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2468
+ }
2469
+ }
2120
2470
  previousPeriodicFingerprint = null;
2121
2471
  sendWorkspaceSyncResult(requestId, result);
2122
2472
  return result;
@@ -2179,7 +2529,8 @@ async function startWorker(options) {
2179
2529
  version: getWorkerVersion(),
2180
2530
  r5dctlVersion: readInstalledCliVersion("r5dctl"),
2181
2531
  capabilities: {
2182
- updateClis: true
2532
+ updateClis: true,
2533
+ canonicalResolverCheckout: true
2183
2534
  },
2184
2535
  projectRoot: projectsRoot,
2185
2536
  artifactRoot,
@@ -2209,9 +2560,26 @@ async function startWorker(options) {
2209
2560
  configureGitHubAuth(message.githubCredential);
2210
2561
  visibleGitIdentity = message.gitIdentity;
2211
2562
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2563
+ const previousCheckoutKeys = new Set(
2564
+ allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2565
+ );
2212
2566
  const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
2213
2567
  manifestByProjectId.clear();
2214
2568
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2569
+ const currentCheckouts = allManifestCheckouts();
2570
+ const currentCheckoutKeys = new Set(
2571
+ currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2572
+ );
2573
+ for (const key of pendingManifestCheckouts.keys()) {
2574
+ if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2575
+ }
2576
+ for (const checkout of currentCheckouts) {
2577
+ const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2578
+ if (previousCheckoutKeys.has(key)) continue;
2579
+ const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2580
+ if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2581
+ }
2582
+ previousPeriodicFingerprint = null;
2215
2583
  process.stdout.write(
2216
2584
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2217
2585
  `
@@ -2222,8 +2590,23 @@ async function startWorker(options) {
2222
2590
  if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2223
2591
  periodicWorkspaceScanInFlight = true;
2224
2592
  void (async () => {
2225
- const input = workspaceSyncInput({ type: "periodic" });
2226
- const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2593
+ const genericCheckoutKeys = new Set(
2594
+ workspaceProjectsForSync([...manifestByProjectId.values()], { type: "periodic" }).flatMap(
2595
+ (project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName))
2596
+ )
2597
+ );
2598
+ const hasPendingGenericCheckout = [...pendingManifestCheckouts.values()].some(
2599
+ (target) => genericCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2600
+ );
2601
+ if (hasPendingGenericCheckout) {
2602
+ previousPeriodicFingerprint = null;
2603
+ await runRequestedWorkspaceSync(crypto.randomUUID(), {
2604
+ type: "periodic",
2605
+ detail: "workspace manifest additions"
2606
+ });
2607
+ return;
2608
+ }
2609
+ const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
2227
2610
  if (fingerprint === emptyFingerprint) {
2228
2611
  previousPeriodicFingerprint = null;
2229
2612
  return;
@@ -2490,6 +2873,7 @@ async function startWorker(options) {
2490
2873
  projectRoot,
2491
2874
  syncRoot,
2492
2875
  artifactRoot,
2876
+ planRoot,
2493
2877
  manifest
2494
2878
  });
2495
2879
  ws.send(
@@ -2631,5 +3015,8 @@ export {
2631
3015
  resolveHostShell,
2632
3016
  resolveWorkerFilePath,
2633
3017
  syncSessionArtifacts,
3018
+ visibleCheckoutGitTestHarness,
3019
+ visibleCheckoutRemoteFor,
3020
+ workspaceSyncCheckoutTargets,
2634
3021
  writeWorkerTextFile
2635
3022
  };