@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/cjs/main.cjs CHANGED
@@ -43,6 +43,9 @@ __export(main_exports, {
43
43
  resolveHostShell: () => resolveHostShell,
44
44
  resolveWorkerFilePath: () => resolveWorkerFilePath,
45
45
  syncSessionArtifacts: () => syncSessionArtifacts,
46
+ visibleCheckoutGitTestHarness: () => visibleCheckoutGitTestHarness,
47
+ visibleCheckoutRemoteFor: () => visibleCheckoutRemoteFor,
48
+ workspaceSyncCheckoutTargets: () => workspaceSyncCheckoutTargets,
46
49
  writeWorkerTextFile: () => writeWorkerTextFile
47
50
  });
48
51
  module.exports = __toCommonJS(main_exports);
@@ -282,7 +285,25 @@ async function readResponseText(response) {
282
285
  }
283
286
  }
284
287
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
288
+ const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
289
+ const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
285
290
  const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
291
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
292
+ function parseBuiltInToolPath(inputPath) {
293
+ const normalized = inputPath.replace(/\\/g, "/");
294
+ const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
295
+ if (!match) {
296
+ return null;
297
+ }
298
+ const ref = `$${match[1] ?? match[2]}`;
299
+ if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
300
+ throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
301
+ }
302
+ return {
303
+ ref,
304
+ relativePath: match[3] ?? ""
305
+ };
306
+ }
286
307
  function validateArtifactSessionId(sessionId) {
287
308
  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)) {
288
309
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -291,7 +312,7 @@ function validateArtifactSessionId(sessionId) {
291
312
  function assertInsideRoot(rootPath, candidatePath, label) {
292
313
  const relative = import_node_path.default.relative(rootPath, candidatePath);
293
314
  if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
294
- throw new Error(`${label} escapes sync directory`);
315
+ throw new Error(`${label} escapes its allowed root`);
295
316
  }
296
317
  }
297
318
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -324,7 +345,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
324
345
  return env;
325
346
  }
326
347
  function isArtifactEnvPath(filePath) {
327
- return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
348
+ try {
349
+ return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
350
+ } catch {
351
+ return false;
352
+ }
328
353
  }
329
354
  function artifactApiBasePath(projectId, branchName, sessionId) {
330
355
  return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
@@ -458,40 +483,45 @@ async function syncSessionArtifacts(input) {
458
483
  void queued.then(release, release);
459
484
  return queued;
460
485
  }
461
- async function resolveArtifactEnvFilePath(input) {
462
- if (!isArtifactEnvPath(input.filePath)) {
463
- return null;
486
+ async function prepareBuiltInToolPaths(input) {
487
+ const parsed = parseBuiltInToolPath(input.filePath);
488
+ if (!parsed) {
489
+ return void 0;
464
490
  }
465
- if (!input.sessionId) {
466
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
491
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
492
+ if (input.access === "write") {
493
+ throw new Error(
494
+ `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
495
+ );
496
+ }
497
+ if (!input.sessionId) {
498
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
499
+ }
500
+ return {
501
+ artifactsDir: await syncSessionArtifacts({
502
+ baseUrl: input.baseUrl,
503
+ token: input.token,
504
+ projectId: input.projectId,
505
+ branchName: input.branchName,
506
+ sessionId: input.sessionId,
507
+ artifactRoot: input.artifactRoot
508
+ })
509
+ };
467
510
  }
468
- if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
469
- throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
511
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
512
+ import_node_fs.default.mkdirSync(plansDir, { recursive: true });
513
+ if (parsed.ref === R5D_PLANS_DIR_REF) {
514
+ return { plansDir };
470
515
  }
471
- const targetDir = await syncSessionArtifacts({
472
- baseUrl: input.baseUrl,
473
- token: input.token,
474
- projectId: input.projectId,
475
- branchName: input.branchName,
476
- sessionId: input.sessionId,
477
- artifactRoot: input.artifactRoot
478
- });
479
- const relativePath = import_node_path.default.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
480
- if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
481
- throw new Error(`Invalid artifact path: ${input.filePath}`);
516
+ if (!input.activePlanId) {
517
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
482
518
  }
483
- const absolutePath = import_node_path.default.resolve(targetDir, ...relativePath.split("/"));
484
- assertInsideRoot(targetDir, absolutePath, "Artifact path");
519
+ validatePlanId(input.activePlanId);
485
520
  return {
486
- absolutePath,
487
- virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
521
+ plansDir,
522
+ activePlanFile: import_node_path.default.join(plansDir, `${input.activePlanId}.plan.md`)
488
523
  };
489
524
  }
490
- function rejectArtifactWritePath(filePath) {
491
- if (isArtifactEnvPath(filePath)) {
492
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
493
- }
494
- }
495
525
  async function prepareArtifactEnvForShell(input) {
496
526
  if (!input.sessionId) {
497
527
  return {};
@@ -587,6 +617,14 @@ function validatePlanId(planId) {
587
617
  throw new Error(`Invalid plan id from server: ${planId}`);
588
618
  }
589
619
  }
620
+ const NON_RECURSIVE_GIT_CONFIG_ARGS = [
621
+ "-c",
622
+ "submodule.recurse=false",
623
+ "-c",
624
+ "fetch.recurseSubmodules=false",
625
+ "-c",
626
+ "push.recurseSubmodules=false"
627
+ ];
590
628
  function gitExtraHeaderConfigKey(extraHeaderUrl) {
591
629
  return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
592
630
  }
@@ -594,10 +632,23 @@ function gitAuthArgs(auth) {
594
632
  if (!auth) {
595
633
  return [];
596
634
  }
597
- return ["-c", `${gitExtraHeaderConfigKey(auth.extraHeaderUrl)}=${auth.header}`];
635
+ const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
636
+ return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
637
+ }
638
+ const visibleCheckoutGitTestHarness = {
639
+ gitAuthArgs,
640
+ commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args],
641
+ cloneArgs: (remoteUrl, branchPath) => ["clone", "--no-recurse-submodules", "--origin", "origin", remoteUrl, branchPath],
642
+ fetchArgs: (...args) => ["fetch", "--no-recurse-submodules", ...args]
643
+ };
644
+ function visibleCheckoutCloneArgs(remoteUrl, branchPath) {
645
+ return visibleCheckoutGitTestHarness.cloneArgs(remoteUrl, branchPath);
646
+ }
647
+ function visibleCheckoutFetchArgs(...args) {
648
+ return visibleCheckoutGitTestHarness.fetchArgs(...args);
598
649
  }
599
650
  function runGit(args, options = {}) {
600
- const command = ["git", ...gitAuthArgs(options.auth), ...args];
651
+ const command = visibleCheckoutGitTestHarness.commandArgs(args, options.auth);
601
652
  const result = Bun.spawnSync(command, {
602
653
  cwd: options.cwd,
603
654
  stdout: "pipe",
@@ -642,10 +693,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
642
693
  throw new Error(`Invalid project file path: ${inputPath}`);
643
694
  }
644
695
  }
645
- function resolveWorkerFilePath(branchPath, inputPath) {
696
+ function assertPathInsideRealRoot(rootPath, candidatePath, label) {
697
+ const resolvedRoot = import_node_path.default.resolve(rootPath);
698
+ const resolvedCandidate = import_node_path.default.resolve(candidatePath);
699
+ assertInsideRoot(resolvedRoot, resolvedCandidate, label);
700
+ let existingPath = resolvedCandidate;
701
+ while (!import_node_fs.default.existsSync(existingPath)) {
702
+ const parentPath = import_node_path.default.dirname(existingPath);
703
+ if (parentPath === existingPath) {
704
+ throw new Error(`${label} root does not exist: ${rootPath}`);
705
+ }
706
+ existingPath = parentPath;
707
+ }
708
+ const realRoot = import_node_fs.default.realpathSync(resolvedRoot);
709
+ const realExistingPath = import_node_fs.default.realpathSync(existingPath);
710
+ assertInsideRoot(realRoot, realExistingPath, label);
711
+ }
712
+ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
713
+ if (parsed.relativePath.split("/").includes("..")) {
714
+ throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
715
+ }
716
+ let rootPath;
717
+ let absolutePath;
718
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
719
+ rootPath = builtInPaths?.artifactsDir;
720
+ } else if (parsed.ref === R5D_PLANS_DIR_REF) {
721
+ rootPath = builtInPaths?.plansDir;
722
+ } else {
723
+ if (parsed.relativePath) {
724
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
725
+ }
726
+ rootPath = builtInPaths?.plansDir;
727
+ absolutePath = builtInPaths?.activePlanFile;
728
+ }
729
+ if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
730
+ const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
731
+ throw new Error(`${parsed.ref} paths require ${requirement}`);
732
+ }
733
+ const normalizedPath = parsed.relativePath ? import_node_path.default.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
734
+ const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
735
+ absolutePath ??= normalizedRelativePath ? import_node_path.default.resolve(rootPath, ...normalizedRelativePath.split("/")) : import_node_path.default.resolve(rootPath);
736
+ assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
737
+ return {
738
+ absolutePath,
739
+ displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
740
+ repoRelativePath: null,
741
+ scope: "virtual",
742
+ virtualRootPath: import_node_path.default.resolve(rootPath)
743
+ };
744
+ }
745
+ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
646
746
  if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
647
747
  throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
648
748
  }
749
+ const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
750
+ if (parsedBuiltInPath) {
751
+ return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
752
+ }
649
753
  const resolvedBranchPath = import_node_path.default.resolve(branchPath);
650
754
  if (import_node_path.default.isAbsolute(inputPath)) {
651
755
  const absolutePath2 = import_node_path.default.resolve(inputPath);
@@ -688,8 +792,28 @@ function resolveWorkerFilePath(branchPath, inputPath) {
688
792
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
689
793
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
690
794
  }
691
- function githubRemoteUrlFor(baseUrl, projectId, manifest) {
692
- return manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId);
795
+ function visibleCheckoutRemoteFor(input) {
796
+ const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
797
+ const isCanonicalManifestBranch = Boolean(canonicalCheckout);
798
+ const usesCanonicalRemote = isCanonicalManifestBranch || !input.manifest?.repoHttpUrl;
799
+ if (usesCanonicalRemote) {
800
+ return {
801
+ remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
802
+ authHeader: `Authorization: Bearer ${input.token}`,
803
+ persistAuth: false,
804
+ reconcileExistingOrigin: isCanonicalManifestBranch,
805
+ requireRemoteBranch: isCanonicalManifestBranch,
806
+ requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
807
+ };
808
+ }
809
+ return {
810
+ remoteUrl: input.manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
811
+ authHeader: input.manifest?.repoAuthHeader ?? null,
812
+ persistAuth: true,
813
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl),
814
+ requireRemoteBranch: false,
815
+ requiredBaseCommit: null
816
+ };
693
817
  }
694
818
  function gitExtraHeaderUrlForRemote(remoteUrl) {
695
819
  try {
@@ -718,6 +842,10 @@ function ensureOriginRemote(branchPath, remoteUrl) {
718
842
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
719
843
  }
720
844
  }
845
+ function originRemoteUrl(branchPath) {
846
+ if (!tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) return null;
847
+ return runGit(["remote", "get-url", "origin"], { cwd: branchPath });
848
+ }
721
849
  function shellQuote(value) {
722
850
  return `'${value.replace(/'/g, "'\\''")}'`;
723
851
  }
@@ -786,6 +914,18 @@ function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
786
914
  }
787
915
  runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
788
916
  }
917
+ function clearVisibleGitAuthentication(branchPath) {
918
+ let extraHeaderKeys = [];
919
+ try {
920
+ extraHeaderKeys = runGit(["config", "--local", "--name-only", "--get-regexp", "^http\\..*extraheader$"], { cwd: branchPath }).split(/\r?\n/).filter(Boolean);
921
+ } catch {
922
+ }
923
+ for (const key of extraHeaderKeys) {
924
+ tryGit(["config", "--local", "--unset-all", key], { cwd: branchPath });
925
+ }
926
+ runGit(["config", "--local", "--replace-all", "credential.helper", ""], { cwd: branchPath });
927
+ tryGit(["config", "--local", "--unset-all", "credential.useHttpPath"], { cwd: branchPath });
928
+ }
789
929
  function dockerConfigPath() {
790
930
  return import_node_path.default.join(import_node_os.default.homedir(), ".docker", "config.json");
791
931
  }
@@ -874,18 +1014,58 @@ function checkoutVisibleBranch(input) {
874
1014
  });
875
1015
  const clean = !hasWorktreeChanges(input.branchPath);
876
1016
  if (remoteBranchExists && clean) {
877
- runGit(["checkout", "-B", input.branchName, `origin/${input.branchName}`], { cwd: input.branchPath });
1017
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1018
+ cwd: input.branchPath
1019
+ });
878
1020
  return;
879
1021
  }
880
1022
  if (branchExists) {
881
- runGit(["checkout", input.branchName], { cwd: input.branchPath });
1023
+ runGit(["checkout", "--no-recurse-submodules", input.branchName], { cwd: input.branchPath });
882
1024
  return;
883
1025
  }
884
1026
  if (defaultRemoteExists) {
885
- runGit(["checkout", "-B", input.branchName, `origin/${input.defaultBranch}`], { cwd: input.branchPath });
1027
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.defaultBranch}`], {
1028
+ cwd: input.branchPath
1029
+ });
886
1030
  return;
887
1031
  }
888
- runGit(["checkout", "-B", input.branchName], { cwd: input.branchPath });
1032
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
1033
+ }
1034
+ function migrateExistingCheckoutToRequiredRemote(input) {
1035
+ if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
1036
+ if (hasWorktreeChanges(input.branchPath)) {
1037
+ throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
1038
+ }
1039
+ runGit(
1040
+ visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1041
+ {
1042
+ cwd: input.branchPath,
1043
+ auth: input.remoteAuth
1044
+ }
1045
+ );
1046
+ const canonicalRemoteRef = `refs/remotes/origin/${input.branchName}`;
1047
+ if (!tryGit(["show-ref", "--verify", "--quiet", canonicalRemoteRef], { cwd: input.branchPath })) {
1048
+ throw new Error(`Canonical branch ${input.branchName} is unavailable during checkout migration`);
1049
+ }
1050
+ if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
1051
+ throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1052
+ }
1053
+ if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
1054
+ const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
1055
+ runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
1056
+ }
1057
+ ensureOriginRemote(input.branchPath, input.remoteUrl);
1058
+ runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
1059
+ cwd: input.branchPath
1060
+ });
1061
+ }
1062
+ function assertCheckoutDerivedFromRequiredBase(input) {
1063
+ const remoteRevision = `refs/remotes/origin/${input.branchName}`;
1064
+ for (const revision of [remoteRevision, "HEAD"]) {
1065
+ if (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, revision], { cwd: input.branchPath })) {
1066
+ throw new Error(`Canonical checkout ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
1067
+ }
1068
+ }
889
1069
  }
890
1070
  function ensureVisibleGitCheckout(input) {
891
1071
  validateBranchName(input.branchName);
@@ -895,40 +1075,114 @@ function ensureVisibleGitCheckout(input) {
895
1075
  if (createdCheckout) {
896
1076
  import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
897
1077
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
898
- if (!tryGit(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
1078
+ const cloned = tryGit(visibleCheckoutCloneArgs(input.remoteUrl, branchPath), {
1079
+ auth: input.remoteAuth
1080
+ });
1081
+ if (!cloned && input.requireRemoteBranch) {
1082
+ import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1083
+ throw new Error(`Failed to clone canonical checkout for branch ${input.branchName}`);
1084
+ }
1085
+ if (!cloned) {
899
1086
  import_node_fs.default.mkdirSync(branchPath, { recursive: true });
900
1087
  runGit(["init"], { cwd: branchPath });
901
1088
  }
902
- ensureOriginRemote(branchPath, input.githubRemoteUrl);
903
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1089
+ ensureOriginRemote(branchPath, input.remoteUrl);
1090
+ if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1091
+ else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
904
1092
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
905
- tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1093
+ tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
1094
+ if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1095
+ import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1096
+ throw new Error(`Canonical branch ${input.branchName} is unavailable after clone`);
1097
+ }
906
1098
  checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1099
+ if (input.requiredBaseCommit) {
1100
+ try {
1101
+ assertCheckoutDerivedFromRequiredBase({
1102
+ branchPath,
1103
+ branchName: input.branchName,
1104
+ requiredBaseCommit: input.requiredBaseCommit
1105
+ });
1106
+ } catch (error) {
1107
+ import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1108
+ throw error;
1109
+ }
1110
+ }
907
1111
  return branchPath;
908
1112
  }
1113
+ if (input.requireRemoteBranch) {
1114
+ if (input.allowRequiredRemoteMigration === false && originRemoteUrl(branchPath) !== input.remoteUrl) {
1115
+ throw new Error(`Canonical checkout ${input.branchName} no longer uses its required remote`);
1116
+ }
1117
+ migrateExistingCheckoutToRequiredRemote({
1118
+ branchPath,
1119
+ branchName: input.branchName,
1120
+ remoteUrl: input.remoteUrl,
1121
+ remoteAuth: input.remoteAuth,
1122
+ requiredBaseCommit: input.requiredBaseCommit
1123
+ });
1124
+ runGit(
1125
+ visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
1126
+ {
1127
+ cwd: branchPath,
1128
+ auth: input.remoteAuth
1129
+ }
1130
+ );
1131
+ }
909
1132
  if (input.reconcileExistingOrigin) {
910
- ensureOriginRemote(branchPath, input.githubRemoteUrl);
1133
+ ensureOriginRemote(branchPath, input.remoteUrl);
1134
+ }
1135
+ if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
1136
+ throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
911
1137
  }
912
- configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1138
+ if (input.requiredBaseCommit) {
1139
+ assertCheckoutDerivedFromRequiredBase({
1140
+ branchPath,
1141
+ branchName: input.branchName,
1142
+ requiredBaseCommit: input.requiredBaseCommit
1143
+ });
1144
+ }
1145
+ if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
1146
+ else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
913
1147
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
914
1148
  return branchPath;
915
1149
  }
916
1150
  function ensureBranchWorkspace(input) {
917
1151
  const defaultBranch = input.manifest?.defaultBranch || "main";
918
- const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
919
- const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
1152
+ const remote = visibleCheckoutRemoteFor(input);
920
1153
  return {
921
1154
  branchPath: ensureVisibleGitCheckout({
922
1155
  projectRoot: input.projectRoot,
923
1156
  branchName: input.branchName,
924
- githubRemoteUrl,
925
- githubAuth,
926
- githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
1157
+ remoteUrl: remote.remoteUrl,
1158
+ remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
1159
+ persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
927
1160
  defaultBranch,
928
- reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
1161
+ reconcileExistingOrigin: remote.reconcileExistingOrigin,
1162
+ requireRemoteBranch: remote.requireRemoteBranch,
1163
+ requiredBaseCommit: remote.requiredBaseCommit,
1164
+ clearPersistentAuth: !remote.persistAuth
929
1165
  })
930
1166
  };
931
1167
  }
1168
+ function workspaceSyncCheckoutTargets(input) {
1169
+ const selected = /* @__PURE__ */ new Map();
1170
+ const add = (target) => {
1171
+ selected.set(`${target.projectId}\0${target.branchName}`, target);
1172
+ };
1173
+ if (input.canonicalCheckoutOnly) {
1174
+ add(input.canonicalCheckoutOnly);
1175
+ } else if (input.includeAllManifestCheckouts) {
1176
+ for (const project of input.projects) {
1177
+ for (const branchName of project.branches) add({ projectId: project.projectId, branchName });
1178
+ }
1179
+ } else {
1180
+ for (const target of input.pendingTargets) add(target);
1181
+ }
1182
+ return [...selected.values()].sort(
1183
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
1184
+ );
1185
+ }
932
1186
  function resolveCommandCwd(branchPath, cwd) {
933
1187
  if (!cwd) {
934
1188
  return branchPath;
@@ -1102,13 +1356,13 @@ async function withFileMutationQueue(key, operation) {
1102
1356
  }
1103
1357
  }
1104
1358
  function mutationQueueKey(projectId, branchName, resolved) {
1105
- if (resolved.scope === "host") {
1106
- return `host:${import_node_path.default.normalize(resolved.absolutePath)}`;
1359
+ if (resolved.scope !== "project") {
1360
+ return `${resolved.scope}:${import_node_path.default.normalize(resolved.absolutePath)}`;
1107
1361
  }
1108
1362
  return `project:${projectId}:${branchName}:${import_node_path.default.posix.normalize(resolved.repoRelativePath)}`;
1109
1363
  }
1110
- function readWorkerTextFile(branchPath, filePath, offset, limit) {
1111
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1364
+ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
1365
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1112
1366
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1113
1367
  throw new Error(`File not found: ${resolved.displayPath}`);
1114
1368
  }
@@ -1122,8 +1376,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1122
1376
  ...formatted
1123
1377
  };
1124
1378
  }
1125
- function writeWorkerTextFile(branchPath, filePath, content) {
1126
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1379
+ function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
1380
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1127
1381
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1128
1382
  import_node_fs.default.writeFileSync(resolved.absolutePath, content, "utf8");
1129
1383
  return {
@@ -1132,8 +1386,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
1132
1386
  gitBlobHash: getGitBlobHashForContent(content)
1133
1387
  };
1134
1388
  }
1135
- function editWorkerTextFile(branchPath, filePath, edits) {
1136
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1389
+ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
1390
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1137
1391
  if (!Array.isArray(edits) || edits.length === 0) {
1138
1392
  throw new Error("edit requires at least one replacement");
1139
1393
  }
@@ -1180,46 +1434,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
1180
1434
  };
1181
1435
  }
1182
1436
  async function executeReadFileOperation(input) {
1183
- const artifact = await resolveArtifactEnvFilePath({
1437
+ const builtInPaths = await prepareBuiltInToolPaths({
1184
1438
  filePath: input.message.filePath,
1185
1439
  baseUrl: input.baseUrl,
1186
1440
  token: input.token,
1187
1441
  projectId: input.projectId,
1188
1442
  branchName: input.message.branchName,
1189
1443
  sessionId: input.message.sessionId,
1190
- artifactRoot: input.artifactRoot
1444
+ activePlanId: input.message.activePlanId,
1445
+ artifactRoot: input.artifactRoot,
1446
+ planRoot: input.planRoot,
1447
+ access: "read"
1191
1448
  });
1192
- if (artifact) {
1193
- if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1194
- throw new Error(`File not found: ${artifact.virtualPath}`);
1195
- }
1196
- const buffer = import_node_fs.default.readFileSync(artifact.absolutePath);
1197
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1198
- return {
1199
- type: "read",
1200
- kind: "text",
1201
- file: artifact.virtualPath,
1202
- gitBlobHash: getGitBlobHashForContent(buffer),
1203
- ...formatted
1204
- };
1205
- }
1206
1449
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1207
- return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1450
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1208
1451
  }
1209
1452
  async function executeWriteFileOperation(input) {
1210
- rejectArtifactWritePath(input.message.filePath);
1453
+ const builtInPaths = await prepareBuiltInToolPaths({
1454
+ filePath: input.message.filePath,
1455
+ baseUrl: input.baseUrl,
1456
+ token: input.token,
1457
+ projectId: input.projectId,
1458
+ branchName: input.message.branchName,
1459
+ sessionId: input.message.sessionId,
1460
+ activePlanId: input.message.activePlanId,
1461
+ artifactRoot: input.artifactRoot,
1462
+ planRoot: input.planRoot,
1463
+ access: "write"
1464
+ });
1211
1465
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1212
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1466
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1213
1467
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1214
- return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1468
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
1215
1469
  });
1216
1470
  }
1217
1471
  async function executeEditFileOperation(input) {
1218
- rejectArtifactWritePath(input.message.filePath);
1472
+ const builtInPaths = await prepareBuiltInToolPaths({
1473
+ filePath: input.message.filePath,
1474
+ baseUrl: input.baseUrl,
1475
+ token: input.token,
1476
+ projectId: input.projectId,
1477
+ branchName: input.message.branchName,
1478
+ sessionId: input.message.sessionId,
1479
+ activePlanId: input.message.activePlanId,
1480
+ artifactRoot: input.artifactRoot,
1481
+ planRoot: input.planRoot,
1482
+ access: "write"
1483
+ });
1219
1484
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1220
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1485
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1221
1486
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1222
- return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1487
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
1223
1488
  });
1224
1489
  }
1225
1490
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1240,16 +1505,20 @@ function normalizeFindPattern(pattern) {
1240
1505
  function walkWorkerEntries(branchPath, start) {
1241
1506
  const entries = [];
1242
1507
  const visitedDirectories = /* @__PURE__ */ new Set();
1508
+ const realVirtualRoot = start.scope === "virtual" ? import_node_fs.default.realpathSync(start.virtualRootPath) : null;
1243
1509
  const visit = (absolutePath, isRoot) => {
1244
1510
  let stat;
1245
1511
  try {
1246
1512
  stat = import_node_fs.default.statSync(absolutePath);
1513
+ if (realVirtualRoot) {
1514
+ assertInsideRoot(realVirtualRoot, import_node_fs.default.realpathSync(absolutePath), `${start.displayPath} path`);
1515
+ }
1247
1516
  } catch (error) {
1248
1517
  if (isRoot) throw error;
1249
1518
  return;
1250
1519
  }
1251
- const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : import_node_path.default.resolve(absolutePath);
1252
1520
  const hostRelativePath = import_node_path.default.relative(start.absolutePath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
1521
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : import_node_path.default.resolve(absolutePath);
1253
1522
  const matchPath = start.scope === "project" ? displayPath : hostRelativePath || import_node_path.default.basename(start.absolutePath);
1254
1523
  entries.push({ absolutePath, displayPath, matchPath, stat });
1255
1524
  if (!stat.isDirectory()) return;
@@ -1280,8 +1549,8 @@ function walkWorkerEntries(branchPath, start) {
1280
1549
  function isProbablyText(bytes) {
1281
1550
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1282
1551
  }
1283
- function grepWorkerFiles(branchPath, input) {
1284
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1552
+ function grepWorkerFiles(branchPath, input, builtInPaths) {
1553
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1285
1554
  if (!import_node_fs.default.existsSync(start.absolutePath)) {
1286
1555
  throw new Error(`Path not found: ${start.displayPath}`);
1287
1556
  }
@@ -1327,17 +1596,34 @@ function grepWorkerFiles(branchPath, input) {
1327
1596
  };
1328
1597
  }
1329
1598
  async function executeGrepOperation(input) {
1330
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1331
- return grepWorkerFiles(workspace.branchPath, {
1332
- pattern: input.message.pattern,
1333
- path: input.message.path,
1334
- glob: input.message.glob,
1335
- caseSensitive: input.message.caseSensitive,
1336
- limit: input.message.limit
1599
+ const toolPath = input.message.path ?? ".";
1600
+ const builtInPaths = await prepareBuiltInToolPaths({
1601
+ filePath: toolPath,
1602
+ baseUrl: input.baseUrl,
1603
+ token: input.token,
1604
+ projectId: input.projectId,
1605
+ branchName: input.message.branchName,
1606
+ sessionId: input.message.sessionId,
1607
+ activePlanId: input.message.activePlanId,
1608
+ artifactRoot: input.artifactRoot,
1609
+ planRoot: input.planRoot,
1610
+ access: "read"
1337
1611
  });
1612
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1613
+ return grepWorkerFiles(
1614
+ workspace.branchPath,
1615
+ {
1616
+ pattern: input.message.pattern,
1617
+ path: input.message.path,
1618
+ glob: input.message.glob,
1619
+ caseSensitive: input.message.caseSensitive,
1620
+ limit: input.message.limit
1621
+ },
1622
+ builtInPaths
1623
+ );
1338
1624
  }
1339
- function findWorkerFiles(branchPath, input) {
1340
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1625
+ function findWorkerFiles(branchPath, input, builtInPaths) {
1626
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1341
1627
  if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
1342
1628
  throw new Error(`Directory not found: ${start.displayPath}`);
1343
1629
  }
@@ -1371,16 +1657,33 @@ function findWorkerFiles(branchPath, input) {
1371
1657
  };
1372
1658
  }
1373
1659
  async function executeFindOperation(input) {
1374
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1375
- return findWorkerFiles(workspace.branchPath, {
1376
- pattern: input.message.pattern,
1377
- path: input.message.path,
1378
- entryType: input.message.entryType,
1379
- limit: input.message.limit
1660
+ const toolPath = input.message.path ?? ".";
1661
+ const builtInPaths = await prepareBuiltInToolPaths({
1662
+ filePath: toolPath,
1663
+ baseUrl: input.baseUrl,
1664
+ token: input.token,
1665
+ projectId: input.projectId,
1666
+ branchName: input.message.branchName,
1667
+ sessionId: input.message.sessionId,
1668
+ activePlanId: input.message.activePlanId,
1669
+ artifactRoot: input.artifactRoot,
1670
+ planRoot: input.planRoot,
1671
+ access: "read"
1380
1672
  });
1673
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1674
+ return findWorkerFiles(
1675
+ workspace.branchPath,
1676
+ {
1677
+ pattern: input.message.pattern,
1678
+ path: input.message.path,
1679
+ entryType: input.message.entryType,
1680
+ limit: input.message.limit
1681
+ },
1682
+ builtInPaths
1683
+ );
1381
1684
  }
1382
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1383
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
1685
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
1686
+ const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
1384
1687
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
1385
1688
  throw new Error(`Directory not found: ${resolved.displayPath}`);
1386
1689
  }
@@ -1398,11 +1701,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1398
1701
  };
1399
1702
  }
1400
1703
  async function executeLsOperation(input) {
1704
+ const toolPath = input.message.path ?? ".";
1705
+ const builtInPaths = await prepareBuiltInToolPaths({
1706
+ filePath: toolPath,
1707
+ baseUrl: input.baseUrl,
1708
+ token: input.token,
1709
+ projectId: input.projectId,
1710
+ branchName: input.message.branchName,
1711
+ sessionId: input.message.sessionId,
1712
+ activePlanId: input.message.activePlanId,
1713
+ artifactRoot: input.artifactRoot,
1714
+ planRoot: input.planRoot,
1715
+ access: "read"
1716
+ });
1401
1717
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1402
- return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1718
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
1403
1719
  }
1404
- function readWorkerImageFile(branchPath, filePath) {
1405
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1720
+ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1721
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1406
1722
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1407
1723
  throw new Error(`File not found: ${resolved.displayPath}`);
1408
1724
  }
@@ -1424,38 +1740,20 @@ function readWorkerImageFile(branchPath, filePath) {
1424
1740
  };
1425
1741
  }
1426
1742
  async function executeViewFileBytesOperation(input) {
1427
- const artifact = await resolveArtifactEnvFilePath({
1743
+ const builtInPaths = await prepareBuiltInToolPaths({
1428
1744
  filePath: input.message.filePath,
1429
1745
  baseUrl: input.baseUrl,
1430
1746
  token: input.token,
1431
1747
  projectId: input.projectId,
1432
1748
  branchName: input.message.branchName,
1433
1749
  sessionId: input.message.sessionId,
1434
- artifactRoot: input.artifactRoot
1750
+ activePlanId: input.message.activePlanId,
1751
+ artifactRoot: input.artifactRoot,
1752
+ planRoot: input.planRoot,
1753
+ access: "read"
1435
1754
  });
1436
- if (artifact) {
1437
- if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1438
- throw new Error(`File not found: ${artifact.virtualPath}`);
1439
- }
1440
- const extension = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1441
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1442
- if (!mediaType) {
1443
- throw new Error("read supports PNG and JPEG image inputs only");
1444
- }
1445
- const bytes = import_node_fs.default.readFileSync(artifact.absolutePath);
1446
- const dimensions = readImageDimensions(bytes, mediaType);
1447
- return {
1448
- type: "view_file_bytes",
1449
- filePath: artifact.virtualPath,
1450
- mediaType,
1451
- base64: bytes.toString("base64"),
1452
- fileSizeBytes: bytes.length,
1453
- width: dimensions.width,
1454
- height: dimensions.height
1455
- };
1456
- }
1457
1755
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1458
- return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1756
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
1459
1757
  }
1460
1758
  async function executeOperation(input) {
1461
1759
  switch (input.message.type) {
@@ -2094,6 +2392,7 @@ async function startWorker(options) {
2094
2392
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2095
2393
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2096
2394
  const manifestByProjectId = /* @__PURE__ */ new Map();
2395
+ const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2097
2396
  const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
2098
2397
  let workspaceRemoteUrl = null;
2099
2398
  let activeWorkspaceIncidentId = null;
@@ -2107,6 +2406,7 @@ async function startWorker(options) {
2107
2406
  let reloadAfterClose = false;
2108
2407
  const workspaceSyncInput = (trigger, overrides = {}) => {
2109
2408
  if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
2409
+ const projects = (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], trigger);
2110
2410
  return {
2111
2411
  workerLabel: label,
2112
2412
  remoteUrl: workspaceRemoteUrl,
@@ -2114,7 +2414,7 @@ async function startWorker(options) {
2114
2414
  projectsRoot,
2115
2415
  plansRoot: planRoot,
2116
2416
  shadowRoot: workspaceShadowRoot,
2117
- projects: [...manifestByProjectId.values()],
2417
+ projects,
2118
2418
  trigger,
2119
2419
  ...overrides
2120
2420
  };
@@ -2122,37 +2422,89 @@ async function startWorker(options) {
2122
2422
  const sendWorkspaceSyncResult = (requestId, result) => {
2123
2423
  sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2124
2424
  };
2125
- const ensureAllVisibleWorkspaceCheckouts = () => {
2425
+ const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2426
+ const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2427
+ (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2428
+ );
2429
+ const ensureVisibleWorkspaceCheckouts = (targets, options2 = {}) => {
2126
2430
  const created = [];
2127
- for (const manifest of manifestByProjectId.values()) {
2128
- for (const branchName of manifest.branches) {
2129
- const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2130
- const existed = import_node_fs.default.existsSync(import_node_path.default.join(branchPath, ".git"));
2131
- const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2132
- ensureVisibleGitCheckout({
2133
- projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2134
- branchName,
2135
- githubRemoteUrl,
2136
- githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2137
- githubAuthHeader: manifest.repoAuthHeader,
2138
- defaultBranch: manifest.defaultBranch || "main",
2139
- reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2140
- });
2141
- if (!existed) created.push({ projectId: manifest.projectId, branchName });
2431
+ for (const target of targets) {
2432
+ const manifest = manifestByProjectId.get(target.projectId);
2433
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2434
+ if (!manifest || !manifest.branches.includes(target.branchName)) {
2435
+ pendingManifestCheckouts.delete(key);
2436
+ continue;
2142
2437
  }
2438
+ const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2439
+ const branchPath = import_node_path.default.join(projectRoot, target.branchName);
2440
+ const existed = hasNormalVisibleGitDir(branchPath);
2441
+ const remote = visibleCheckoutRemoteFor({
2442
+ baseUrl,
2443
+ token,
2444
+ projectId: manifest.projectId,
2445
+ branchName: target.branchName,
2446
+ manifest
2447
+ });
2448
+ ensureVisibleGitCheckout({
2449
+ projectRoot,
2450
+ branchName: target.branchName,
2451
+ remoteUrl: remote.remoteUrl,
2452
+ remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
2453
+ persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
2454
+ defaultBranch: manifest.defaultBranch || "main",
2455
+ reconcileExistingOrigin: remote.reconcileExistingOrigin,
2456
+ requireRemoteBranch: remote.requireRemoteBranch,
2457
+ requiredBaseCommit: remote.requiredBaseCommit,
2458
+ allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requireRemoteBranch),
2459
+ clearPersistentAuth: !remote.persistAuth
2460
+ });
2461
+ if (!existed) created.push(target);
2143
2462
  }
2144
2463
  return created;
2145
2464
  };
2146
2465
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2147
2466
  workspaceSyncRequestsInFlight += 1;
2148
2467
  try {
2149
- const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2150
- const result = await workspaceSyncSingleFlight.run(
2151
- workspaceSyncInput(trigger, {
2468
+ let pendingTargets = [];
2469
+ const result = await workspaceSyncSingleFlight.runPrepared(() => {
2470
+ const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2471
+ const syncProjects = (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], trigger);
2472
+ const allowedCheckoutKeys = new Set(
2473
+ syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
2474
+ );
2475
+ const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
2476
+ (target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2477
+ );
2478
+ let checkoutTargets = [];
2479
+ if (shouldEnsureCheckouts) {
2480
+ checkoutTargets = workspaceSyncCheckoutTargets({
2481
+ projects: syncProjects,
2482
+ pendingTargets: scopedPendingTargets,
2483
+ includeAllManifestCheckouts: trigger.type === "connect",
2484
+ ...trigger.canonicalCheckoutOnly && trigger.projectId && trigger.branchName ? { canonicalCheckoutOnly: { projectId: trigger.projectId, branchName: trigger.branchName } } : {}
2485
+ });
2486
+ }
2487
+ pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
2488
+ const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets, {
2489
+ strictCanonicalRevalidation: true
2490
+ });
2491
+ const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2492
+ for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2493
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2494
+ if (allowedCheckoutKeys.has(key)) newVisibleCheckoutByKey.set(key, target);
2495
+ }
2496
+ const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2497
+ return workspaceSyncInput(trigger, {
2152
2498
  ...overrides,
2153
2499
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2154
- })
2155
- );
2500
+ });
2501
+ });
2502
+ if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
2503
+ for (const target of pendingTargets) {
2504
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2505
+ if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2506
+ }
2507
+ }
2156
2508
  previousPeriodicFingerprint = null;
2157
2509
  sendWorkspaceSyncResult(requestId, result);
2158
2510
  return result;
@@ -2215,7 +2567,8 @@ async function startWorker(options) {
2215
2567
  version: getWorkerVersion(),
2216
2568
  r5dctlVersion: (0, import_cli_update.readInstalledCliVersion)("r5dctl"),
2217
2569
  capabilities: {
2218
- updateClis: true
2570
+ updateClis: true,
2571
+ canonicalResolverCheckout: true
2219
2572
  },
2220
2573
  projectRoot: projectsRoot,
2221
2574
  artifactRoot,
@@ -2245,9 +2598,26 @@ async function startWorker(options) {
2245
2598
  configureGitHubAuth(message.githubCredential);
2246
2599
  visibleGitIdentity = message.gitIdentity;
2247
2600
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2601
+ const previousCheckoutKeys = new Set(
2602
+ allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2603
+ );
2248
2604
  const migratedProjectIds = (0, import_managed_paths.migrateLegacyProjectRoots)(projectsRoot, message.projects);
2249
2605
  manifestByProjectId.clear();
2250
2606
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2607
+ const currentCheckouts = allManifestCheckouts();
2608
+ const currentCheckoutKeys = new Set(
2609
+ currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2610
+ );
2611
+ for (const key of pendingManifestCheckouts.keys()) {
2612
+ if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2613
+ }
2614
+ for (const checkout of currentCheckouts) {
2615
+ const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2616
+ if (previousCheckoutKeys.has(key)) continue;
2617
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2618
+ if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2619
+ }
2620
+ previousPeriodicFingerprint = null;
2251
2621
  process.stdout.write(
2252
2622
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2253
2623
  `
@@ -2258,8 +2628,23 @@ async function startWorker(options) {
2258
2628
  if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2259
2629
  periodicWorkspaceScanInFlight = true;
2260
2630
  void (async () => {
2261
- const input = workspaceSyncInput({ type: "periodic" });
2262
- const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2631
+ const genericCheckoutKeys = new Set(
2632
+ (0, import_workspace_sync.workspaceProjectsForSync)([...manifestByProjectId.values()], { type: "periodic" }).flatMap(
2633
+ (project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName))
2634
+ )
2635
+ );
2636
+ const hasPendingGenericCheckout = [...pendingManifestCheckouts.values()].some(
2637
+ (target) => genericCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
2638
+ );
2639
+ if (hasPendingGenericCheckout) {
2640
+ previousPeriodicFingerprint = null;
2641
+ await runRequestedWorkspaceSync(crypto.randomUUID(), {
2642
+ type: "periodic",
2643
+ detail: "workspace manifest additions"
2644
+ });
2645
+ return;
2646
+ }
2647
+ const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
2263
2648
  if (fingerprint === emptyFingerprint) {
2264
2649
  previousPeriodicFingerprint = null;
2265
2650
  return;
@@ -2526,6 +2911,7 @@ async function startWorker(options) {
2526
2911
  projectRoot,
2527
2912
  syncRoot,
2528
2913
  artifactRoot,
2914
+ planRoot,
2529
2915
  manifest
2530
2916
  });
2531
2917
  ws.send(
@@ -2668,5 +3054,8 @@ if (isCliEntrypoint()) {
2668
3054
  resolveHostShell,
2669
3055
  resolveWorkerFilePath,
2670
3056
  syncSessionArtifacts,
3057
+ visibleCheckoutGitTestHarness,
3058
+ visibleCheckoutRemoteFor,
3059
+ workspaceSyncCheckoutTargets,
2671
3060
  writeWorkerTextFile
2672
3061
  });