@ricsam/r5d-worker 0.0.44 → 0.0.46

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
@@ -246,7 +246,25 @@ async function readResponseText(response) {
246
246
  }
247
247
  }
248
248
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
249
+ const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
250
+ const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
249
251
  const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
252
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
253
+ function parseBuiltInToolPath(inputPath) {
254
+ const normalized = inputPath.replace(/\\/g, "/");
255
+ const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
256
+ if (!match) {
257
+ return null;
258
+ }
259
+ const ref = `$${match[1] ?? match[2]}`;
260
+ if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
261
+ throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
262
+ }
263
+ return {
264
+ ref,
265
+ relativePath: match[3] ?? ""
266
+ };
267
+ }
250
268
  function validateArtifactSessionId(sessionId) {
251
269
  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
270
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -255,7 +273,7 @@ function validateArtifactSessionId(sessionId) {
255
273
  function assertInsideRoot(rootPath, candidatePath, label) {
256
274
  const relative = path.relative(rootPath, candidatePath);
257
275
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
258
- throw new Error(`${label} escapes sync directory`);
276
+ throw new Error(`${label} escapes its allowed root`);
259
277
  }
260
278
  }
261
279
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -288,7 +306,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
288
306
  return env;
289
307
  }
290
308
  function isArtifactEnvPath(filePath) {
291
- return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
309
+ try {
310
+ return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
311
+ } catch {
312
+ return false;
313
+ }
292
314
  }
293
315
  function artifactApiBasePath(projectId, branchName, sessionId) {
294
316
  return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
@@ -395,56 +417,72 @@ async function fetchSessionArtifactManifest(input) {
395
417
  label: "artifact"
396
418
  });
397
419
  }
420
+ const sessionArtifactSyncQueues = /* @__PURE__ */ new Map();
398
421
  async function syncSessionArtifacts(input) {
399
- const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
400
- const artifacts = await fetchSessionArtifactManifest(input);
401
- return syncRemoteFileSet({
402
- targetDir,
403
- files: artifacts,
404
- label: "Artifact path",
405
- download: (artifact) => downloadRemoteSyncedFile({
406
- url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
407
- token: input.token,
422
+ const queueKey = `${path.resolve(input.artifactRoot)}\0${input.sessionId}`;
423
+ const previous = sessionArtifactSyncQueues.get(queueKey);
424
+ const queued = (previous ? previous.catch(() => "") : Promise.resolve("")).then(async () => {
425
+ const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
426
+ const artifacts = await fetchSessionArtifactManifest(input);
427
+ return syncRemoteFileSet({
408
428
  targetDir,
409
- file: artifact,
410
- label: "artifact"
411
- })
429
+ files: artifacts,
430
+ label: "Artifact path",
431
+ download: (artifact) => downloadRemoteSyncedFile({
432
+ url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
433
+ token: input.token,
434
+ targetDir,
435
+ file: artifact,
436
+ label: "artifact"
437
+ })
438
+ });
412
439
  });
440
+ sessionArtifactSyncQueues.set(queueKey, queued);
441
+ const release = () => {
442
+ if (sessionArtifactSyncQueues.get(queueKey) === queued) sessionArtifactSyncQueues.delete(queueKey);
443
+ };
444
+ void queued.then(release, release);
445
+ return queued;
413
446
  }
414
- async function resolveArtifactEnvFilePath(input) {
415
- if (!isArtifactEnvPath(input.filePath)) {
416
- return null;
447
+ async function prepareBuiltInToolPaths(input) {
448
+ const parsed = parseBuiltInToolPath(input.filePath);
449
+ if (!parsed) {
450
+ return void 0;
417
451
  }
418
- if (!input.sessionId) {
419
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
452
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
453
+ if (input.access === "write") {
454
+ throw new Error(
455
+ `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
456
+ );
457
+ }
458
+ if (!input.sessionId) {
459
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
460
+ }
461
+ return {
462
+ artifactsDir: await syncSessionArtifacts({
463
+ baseUrl: input.baseUrl,
464
+ token: input.token,
465
+ projectId: input.projectId,
466
+ branchName: input.branchName,
467
+ sessionId: input.sessionId,
468
+ artifactRoot: input.artifactRoot
469
+ })
470
+ };
420
471
  }
421
- if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
422
- throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
472
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
473
+ fs.mkdirSync(plansDir, { recursive: true });
474
+ if (parsed.ref === R5D_PLANS_DIR_REF) {
475
+ return { plansDir };
423
476
  }
424
- const targetDir = await syncSessionArtifacts({
425
- baseUrl: input.baseUrl,
426
- token: input.token,
427
- projectId: input.projectId,
428
- branchName: input.branchName,
429
- sessionId: input.sessionId,
430
- artifactRoot: input.artifactRoot
431
- });
432
- const relativePath = path.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
433
- if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
434
- throw new Error(`Invalid artifact path: ${input.filePath}`);
477
+ if (!input.activePlanId) {
478
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
435
479
  }
436
- const absolutePath = path.resolve(targetDir, ...relativePath.split("/"));
437
- assertInsideRoot(targetDir, absolutePath, "Artifact path");
480
+ validatePlanId(input.activePlanId);
438
481
  return {
439
- absolutePath,
440
- virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
482
+ plansDir,
483
+ activePlanFile: path.join(plansDir, `${input.activePlanId}.plan.md`)
441
484
  };
442
485
  }
443
- function rejectArtifactWritePath(filePath) {
444
- if (isArtifactEnvPath(filePath)) {
445
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
446
- }
447
- }
448
486
  async function prepareArtifactEnvForShell(input) {
449
487
  if (!input.sessionId) {
450
488
  return {};
@@ -595,10 +633,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
595
633
  throw new Error(`Invalid project file path: ${inputPath}`);
596
634
  }
597
635
  }
598
- function resolveWorkerFilePath(branchPath, inputPath) {
636
+ function assertPathInsideRealRoot(rootPath, candidatePath, label) {
637
+ const resolvedRoot = path.resolve(rootPath);
638
+ const resolvedCandidate = path.resolve(candidatePath);
639
+ assertInsideRoot(resolvedRoot, resolvedCandidate, label);
640
+ let existingPath = resolvedCandidate;
641
+ while (!fs.existsSync(existingPath)) {
642
+ const parentPath = path.dirname(existingPath);
643
+ if (parentPath === existingPath) {
644
+ throw new Error(`${label} root does not exist: ${rootPath}`);
645
+ }
646
+ existingPath = parentPath;
647
+ }
648
+ const realRoot = fs.realpathSync(resolvedRoot);
649
+ const realExistingPath = fs.realpathSync(existingPath);
650
+ assertInsideRoot(realRoot, realExistingPath, label);
651
+ }
652
+ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
653
+ if (parsed.relativePath.split("/").includes("..")) {
654
+ throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
655
+ }
656
+ let rootPath;
657
+ let absolutePath;
658
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
659
+ rootPath = builtInPaths?.artifactsDir;
660
+ } else if (parsed.ref === R5D_PLANS_DIR_REF) {
661
+ rootPath = builtInPaths?.plansDir;
662
+ } else {
663
+ if (parsed.relativePath) {
664
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
665
+ }
666
+ rootPath = builtInPaths?.plansDir;
667
+ absolutePath = builtInPaths?.activePlanFile;
668
+ }
669
+ if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
670
+ const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
671
+ throw new Error(`${parsed.ref} paths require ${requirement}`);
672
+ }
673
+ const normalizedPath = parsed.relativePath ? path.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
674
+ const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
675
+ absolutePath ??= normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
676
+ assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
677
+ return {
678
+ absolutePath,
679
+ displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
680
+ repoRelativePath: null,
681
+ scope: "virtual",
682
+ virtualRootPath: path.resolve(rootPath)
683
+ };
684
+ }
685
+ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
599
686
  if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
600
687
  throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
601
688
  }
689
+ const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
690
+ if (parsedBuiltInPath) {
691
+ return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
692
+ }
602
693
  const resolvedBranchPath = path.resolve(branchPath);
603
694
  if (path.isAbsolute(inputPath)) {
604
695
  const absolutePath2 = path.resolve(inputPath);
@@ -1055,13 +1146,13 @@ async function withFileMutationQueue(key, operation) {
1055
1146
  }
1056
1147
  }
1057
1148
  function mutationQueueKey(projectId, branchName, resolved) {
1058
- if (resolved.scope === "host") {
1059
- return `host:${path.normalize(resolved.absolutePath)}`;
1149
+ if (resolved.scope !== "project") {
1150
+ return `${resolved.scope}:${path.normalize(resolved.absolutePath)}`;
1060
1151
  }
1061
1152
  return `project:${projectId}:${branchName}:${path.posix.normalize(resolved.repoRelativePath)}`;
1062
1153
  }
1063
- function readWorkerTextFile(branchPath, filePath, offset, limit) {
1064
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1154
+ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
1155
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1065
1156
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1066
1157
  throw new Error(`File not found: ${resolved.displayPath}`);
1067
1158
  }
@@ -1075,8 +1166,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1075
1166
  ...formatted
1076
1167
  };
1077
1168
  }
1078
- function writeWorkerTextFile(branchPath, filePath, content) {
1079
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1169
+ function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
1170
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1080
1171
  fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1081
1172
  fs.writeFileSync(resolved.absolutePath, content, "utf8");
1082
1173
  return {
@@ -1085,8 +1176,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
1085
1176
  gitBlobHash: getGitBlobHashForContent(content)
1086
1177
  };
1087
1178
  }
1088
- function editWorkerTextFile(branchPath, filePath, edits) {
1089
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1179
+ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
1180
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1090
1181
  if (!Array.isArray(edits) || edits.length === 0) {
1091
1182
  throw new Error("edit requires at least one replacement");
1092
1183
  }
@@ -1133,46 +1224,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
1133
1224
  };
1134
1225
  }
1135
1226
  async function executeReadFileOperation(input) {
1136
- const artifact = await resolveArtifactEnvFilePath({
1227
+ const builtInPaths = await prepareBuiltInToolPaths({
1137
1228
  filePath: input.message.filePath,
1138
1229
  baseUrl: input.baseUrl,
1139
1230
  token: input.token,
1140
1231
  projectId: input.projectId,
1141
1232
  branchName: input.message.branchName,
1142
1233
  sessionId: input.message.sessionId,
1143
- artifactRoot: input.artifactRoot
1234
+ activePlanId: input.message.activePlanId,
1235
+ artifactRoot: input.artifactRoot,
1236
+ planRoot: input.planRoot,
1237
+ access: "read"
1144
1238
  });
1145
- if (artifact) {
1146
- if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1147
- throw new Error(`File not found: ${artifact.virtualPath}`);
1148
- }
1149
- const buffer = fs.readFileSync(artifact.absolutePath);
1150
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1151
- return {
1152
- type: "read",
1153
- kind: "text",
1154
- file: artifact.virtualPath,
1155
- gitBlobHash: getGitBlobHashForContent(buffer),
1156
- ...formatted
1157
- };
1158
- }
1159
1239
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1160
- return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1240
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1161
1241
  }
1162
1242
  async function executeWriteFileOperation(input) {
1163
- rejectArtifactWritePath(input.message.filePath);
1243
+ const builtInPaths = await prepareBuiltInToolPaths({
1244
+ filePath: input.message.filePath,
1245
+ baseUrl: input.baseUrl,
1246
+ token: input.token,
1247
+ projectId: input.projectId,
1248
+ branchName: input.message.branchName,
1249
+ sessionId: input.message.sessionId,
1250
+ activePlanId: input.message.activePlanId,
1251
+ artifactRoot: input.artifactRoot,
1252
+ planRoot: input.planRoot,
1253
+ access: "write"
1254
+ });
1164
1255
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1165
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1256
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1166
1257
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1167
- return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1258
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
1168
1259
  });
1169
1260
  }
1170
1261
  async function executeEditFileOperation(input) {
1171
- rejectArtifactWritePath(input.message.filePath);
1262
+ const builtInPaths = await prepareBuiltInToolPaths({
1263
+ filePath: input.message.filePath,
1264
+ baseUrl: input.baseUrl,
1265
+ token: input.token,
1266
+ projectId: input.projectId,
1267
+ branchName: input.message.branchName,
1268
+ sessionId: input.message.sessionId,
1269
+ activePlanId: input.message.activePlanId,
1270
+ artifactRoot: input.artifactRoot,
1271
+ planRoot: input.planRoot,
1272
+ access: "write"
1273
+ });
1172
1274
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1173
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1275
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1174
1276
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1175
- return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1277
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
1176
1278
  });
1177
1279
  }
1178
1280
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1193,16 +1295,20 @@ function normalizeFindPattern(pattern) {
1193
1295
  function walkWorkerEntries(branchPath, start) {
1194
1296
  const entries = [];
1195
1297
  const visitedDirectories = /* @__PURE__ */ new Set();
1298
+ const realVirtualRoot = start.scope === "virtual" ? fs.realpathSync(start.virtualRootPath) : null;
1196
1299
  const visit = (absolutePath, isRoot) => {
1197
1300
  let stat;
1198
1301
  try {
1199
1302
  stat = fs.statSync(absolutePath);
1303
+ if (realVirtualRoot) {
1304
+ assertInsideRoot(realVirtualRoot, fs.realpathSync(absolutePath), `${start.displayPath} path`);
1305
+ }
1200
1306
  } catch (error) {
1201
1307
  if (isRoot) throw error;
1202
1308
  return;
1203
1309
  }
1204
- const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : path.resolve(absolutePath);
1205
1310
  const hostRelativePath = path.relative(start.absolutePath, absolutePath).split(path.sep).join(path.posix.sep);
1311
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : path.resolve(absolutePath);
1206
1312
  const matchPath = start.scope === "project" ? displayPath : hostRelativePath || path.basename(start.absolutePath);
1207
1313
  entries.push({ absolutePath, displayPath, matchPath, stat });
1208
1314
  if (!stat.isDirectory()) return;
@@ -1233,8 +1339,8 @@ function walkWorkerEntries(branchPath, start) {
1233
1339
  function isProbablyText(bytes) {
1234
1340
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1235
1341
  }
1236
- function grepWorkerFiles(branchPath, input) {
1237
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1342
+ function grepWorkerFiles(branchPath, input, builtInPaths) {
1343
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1238
1344
  if (!fs.existsSync(start.absolutePath)) {
1239
1345
  throw new Error(`Path not found: ${start.displayPath}`);
1240
1346
  }
@@ -1280,17 +1386,34 @@ function grepWorkerFiles(branchPath, input) {
1280
1386
  };
1281
1387
  }
1282
1388
  async function executeGrepOperation(input) {
1283
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1284
- return grepWorkerFiles(workspace.branchPath, {
1285
- pattern: input.message.pattern,
1286
- path: input.message.path,
1287
- glob: input.message.glob,
1288
- caseSensitive: input.message.caseSensitive,
1289
- limit: input.message.limit
1389
+ const toolPath = input.message.path ?? ".";
1390
+ const builtInPaths = await prepareBuiltInToolPaths({
1391
+ filePath: toolPath,
1392
+ baseUrl: input.baseUrl,
1393
+ token: input.token,
1394
+ projectId: input.projectId,
1395
+ branchName: input.message.branchName,
1396
+ sessionId: input.message.sessionId,
1397
+ activePlanId: input.message.activePlanId,
1398
+ artifactRoot: input.artifactRoot,
1399
+ planRoot: input.planRoot,
1400
+ access: "read"
1290
1401
  });
1402
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1403
+ return grepWorkerFiles(
1404
+ workspace.branchPath,
1405
+ {
1406
+ pattern: input.message.pattern,
1407
+ path: input.message.path,
1408
+ glob: input.message.glob,
1409
+ caseSensitive: input.message.caseSensitive,
1410
+ limit: input.message.limit
1411
+ },
1412
+ builtInPaths
1413
+ );
1291
1414
  }
1292
- function findWorkerFiles(branchPath, input) {
1293
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1415
+ function findWorkerFiles(branchPath, input, builtInPaths) {
1416
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1294
1417
  if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
1295
1418
  throw new Error(`Directory not found: ${start.displayPath}`);
1296
1419
  }
@@ -1324,16 +1447,33 @@ function findWorkerFiles(branchPath, input) {
1324
1447
  };
1325
1448
  }
1326
1449
  async function executeFindOperation(input) {
1327
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1328
- return findWorkerFiles(workspace.branchPath, {
1329
- pattern: input.message.pattern,
1330
- path: input.message.path,
1331
- entryType: input.message.entryType,
1332
- limit: input.message.limit
1450
+ const toolPath = input.message.path ?? ".";
1451
+ const builtInPaths = await prepareBuiltInToolPaths({
1452
+ filePath: toolPath,
1453
+ baseUrl: input.baseUrl,
1454
+ token: input.token,
1455
+ projectId: input.projectId,
1456
+ branchName: input.message.branchName,
1457
+ sessionId: input.message.sessionId,
1458
+ activePlanId: input.message.activePlanId,
1459
+ artifactRoot: input.artifactRoot,
1460
+ planRoot: input.planRoot,
1461
+ access: "read"
1333
1462
  });
1463
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1464
+ return findWorkerFiles(
1465
+ workspace.branchPath,
1466
+ {
1467
+ pattern: input.message.pattern,
1468
+ path: input.message.path,
1469
+ entryType: input.message.entryType,
1470
+ limit: input.message.limit
1471
+ },
1472
+ builtInPaths
1473
+ );
1334
1474
  }
1335
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1336
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
1475
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
1476
+ const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
1337
1477
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
1338
1478
  throw new Error(`Directory not found: ${resolved.displayPath}`);
1339
1479
  }
@@ -1351,11 +1491,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1351
1491
  };
1352
1492
  }
1353
1493
  async function executeLsOperation(input) {
1494
+ const toolPath = input.message.path ?? ".";
1495
+ const builtInPaths = await prepareBuiltInToolPaths({
1496
+ filePath: toolPath,
1497
+ baseUrl: input.baseUrl,
1498
+ token: input.token,
1499
+ projectId: input.projectId,
1500
+ branchName: input.message.branchName,
1501
+ sessionId: input.message.sessionId,
1502
+ activePlanId: input.message.activePlanId,
1503
+ artifactRoot: input.artifactRoot,
1504
+ planRoot: input.planRoot,
1505
+ access: "read"
1506
+ });
1354
1507
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1355
- return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1508
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
1356
1509
  }
1357
- function readWorkerImageFile(branchPath, filePath) {
1358
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1510
+ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1511
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1359
1512
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1360
1513
  throw new Error(`File not found: ${resolved.displayPath}`);
1361
1514
  }
@@ -1377,38 +1530,20 @@ function readWorkerImageFile(branchPath, filePath) {
1377
1530
  };
1378
1531
  }
1379
1532
  async function executeViewFileBytesOperation(input) {
1380
- const artifact = await resolveArtifactEnvFilePath({
1533
+ const builtInPaths = await prepareBuiltInToolPaths({
1381
1534
  filePath: input.message.filePath,
1382
1535
  baseUrl: input.baseUrl,
1383
1536
  token: input.token,
1384
1537
  projectId: input.projectId,
1385
1538
  branchName: input.message.branchName,
1386
1539
  sessionId: input.message.sessionId,
1387
- artifactRoot: input.artifactRoot
1540
+ activePlanId: input.message.activePlanId,
1541
+ artifactRoot: input.artifactRoot,
1542
+ planRoot: input.planRoot,
1543
+ access: "read"
1388
1544
  });
1389
- if (artifact) {
1390
- if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1391
- throw new Error(`File not found: ${artifact.virtualPath}`);
1392
- }
1393
- const extension = path.extname(artifact.absolutePath).toLowerCase();
1394
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1395
- if (!mediaType) {
1396
- throw new Error("read supports PNG and JPEG image inputs only");
1397
- }
1398
- const bytes = fs.readFileSync(artifact.absolutePath);
1399
- const dimensions = readImageDimensions(bytes, mediaType);
1400
- return {
1401
- type: "view_file_bytes",
1402
- filePath: artifact.virtualPath,
1403
- mediaType,
1404
- base64: bytes.toString("base64"),
1405
- fileSizeBytes: bytes.length,
1406
- width: dimensions.width,
1407
- height: dimensions.height
1408
- };
1409
- }
1410
1545
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1411
- return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1546
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
1412
1547
  }
1413
1548
  async function executeOperation(input) {
1414
1549
  switch (input.message.type) {
@@ -2047,6 +2182,7 @@ async function startWorker(options) {
2047
2182
  fs.mkdirSync(artifactRoot, { recursive: true });
2048
2183
  fs.mkdirSync(planRoot, { recursive: true });
2049
2184
  const manifestByProjectId = /* @__PURE__ */ new Map();
2185
+ const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2050
2186
  const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
2051
2187
  let workspaceRemoteUrl = null;
2052
2188
  let activeWorkspaceIncidentId = null;
@@ -2055,6 +2191,7 @@ async function startWorker(options) {
2055
2191
  let periodicWorkspaceScanInFlight = false;
2056
2192
  let terminalReplayTimer;
2057
2193
  let workspaceSyncRequestsInFlight = 0;
2194
+ let sessionArtifactSyncRequestsInFlight = 0;
2058
2195
  let cliUpdateInProgress = false;
2059
2196
  let reloadAfterClose = false;
2060
2197
  const workspaceSyncInput = (trigger, overrides = {}) => {
@@ -2074,37 +2211,64 @@ async function startWorker(options) {
2074
2211
  const sendWorkspaceSyncResult = (requestId, result) => {
2075
2212
  sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2076
2213
  };
2077
- const ensureAllVisibleWorkspaceCheckouts = () => {
2214
+ const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2215
+ const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2216
+ (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2217
+ );
2218
+ const ensureVisibleWorkspaceCheckouts = (targets) => {
2078
2219
  const created = [];
2079
- for (const manifest of manifestByProjectId.values()) {
2080
- for (const branchName of manifest.branches) {
2081
- const branchPath = path.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2082
- const existed = fs.existsSync(path.join(branchPath, ".git"));
2083
- const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2084
- ensureVisibleGitCheckout({
2085
- projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2086
- branchName,
2087
- githubRemoteUrl,
2088
- githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2089
- githubAuthHeader: manifest.repoAuthHeader,
2090
- defaultBranch: manifest.defaultBranch || "main",
2091
- reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2092
- });
2093
- if (!existed) created.push({ projectId: manifest.projectId, branchName });
2220
+ for (const target of targets) {
2221
+ const manifest = manifestByProjectId.get(target.projectId);
2222
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2223
+ if (!manifest || !manifest.branches.includes(target.branchName)) {
2224
+ pendingManifestCheckouts.delete(key);
2225
+ continue;
2094
2226
  }
2227
+ const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2228
+ const branchPath = path.join(projectRoot, target.branchName);
2229
+ const existed = hasNormalVisibleGitDir(branchPath);
2230
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2231
+ ensureVisibleGitCheckout({
2232
+ projectRoot,
2233
+ branchName: target.branchName,
2234
+ githubRemoteUrl,
2235
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2236
+ githubAuthHeader: manifest.repoAuthHeader,
2237
+ defaultBranch: manifest.defaultBranch || "main",
2238
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2239
+ });
2240
+ if (!existed) created.push(target);
2095
2241
  }
2096
2242
  return created;
2097
2243
  };
2098
2244
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2099
2245
  workspaceSyncRequestsInFlight += 1;
2100
2246
  try {
2101
- const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2102
- const result = await workspaceSyncSingleFlight.run(
2103
- workspaceSyncInput(trigger, {
2247
+ let pendingTargets = [];
2248
+ const result = await workspaceSyncSingleFlight.runPrepared(() => {
2249
+ const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2250
+ let checkoutTargets = [];
2251
+ if (shouldEnsureCheckouts) {
2252
+ checkoutTargets = trigger.type === "connect" ? allManifestCheckouts() : [...pendingManifestCheckouts.values()];
2253
+ }
2254
+ pendingTargets = shouldEnsureCheckouts ? [...pendingManifestCheckouts.values()] : [];
2255
+ const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets);
2256
+ const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2257
+ for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2258
+ newVisibleCheckoutByKey.set(manifestCheckoutKey(target.projectId, target.branchName), target);
2259
+ }
2260
+ const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2261
+ return workspaceSyncInput(trigger, {
2104
2262
  ...overrides,
2105
2263
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2106
- })
2107
- );
2264
+ });
2265
+ });
2266
+ if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
2267
+ for (const target of pendingTargets) {
2268
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2269
+ if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2270
+ }
2271
+ }
2108
2272
  previousPeriodicFingerprint = null;
2109
2273
  sendWorkspaceSyncResult(requestId, result);
2110
2274
  return result;
@@ -2197,9 +2361,26 @@ async function startWorker(options) {
2197
2361
  configureGitHubAuth(message.githubCredential);
2198
2362
  visibleGitIdentity = message.gitIdentity;
2199
2363
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2364
+ const previousCheckoutKeys = new Set(
2365
+ allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2366
+ );
2200
2367
  const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
2201
2368
  manifestByProjectId.clear();
2202
2369
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2370
+ const currentCheckouts = allManifestCheckouts();
2371
+ const currentCheckoutKeys = new Set(
2372
+ currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2373
+ );
2374
+ for (const key of pendingManifestCheckouts.keys()) {
2375
+ if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2376
+ }
2377
+ for (const checkout of currentCheckouts) {
2378
+ const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2379
+ if (previousCheckoutKeys.has(key)) continue;
2380
+ const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2381
+ if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2382
+ }
2383
+ previousPeriodicFingerprint = null;
2203
2384
  process.stdout.write(
2204
2385
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2205
2386
  `
@@ -2210,8 +2391,15 @@ async function startWorker(options) {
2210
2391
  if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2211
2392
  periodicWorkspaceScanInFlight = true;
2212
2393
  void (async () => {
2213
- const input = workspaceSyncInput({ type: "periodic" });
2214
- const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2394
+ if (pendingManifestCheckouts.size > 0) {
2395
+ previousPeriodicFingerprint = null;
2396
+ await runRequestedWorkspaceSync(crypto.randomUUID(), {
2397
+ type: "periodic",
2398
+ detail: "workspace manifest additions"
2399
+ });
2400
+ return;
2401
+ }
2402
+ const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
2215
2403
  if (fingerprint === emptyFingerprint) {
2216
2404
  previousPeriodicFingerprint = null;
2217
2405
  return;
@@ -2251,6 +2439,34 @@ async function startWorker(options) {
2251
2439
  }
2252
2440
  return;
2253
2441
  }
2442
+ if (message.type === "sync_session_artifacts") {
2443
+ sessionArtifactSyncRequestsInFlight += 1;
2444
+ try {
2445
+ await syncSessionArtifacts({
2446
+ baseUrl,
2447
+ token,
2448
+ projectId: message.projectId,
2449
+ branchName: message.branchName,
2450
+ sessionId: message.sessionId,
2451
+ artifactRoot
2452
+ });
2453
+ sendWorkerMessage(ws, {
2454
+ type: "session_artifacts_sync_result",
2455
+ requestId: message.requestId,
2456
+ sessionId: message.sessionId
2457
+ });
2458
+ } catch (error) {
2459
+ sendWorkerMessage(ws, {
2460
+ type: "session_artifacts_sync_result",
2461
+ requestId: message.requestId,
2462
+ sessionId: message.sessionId,
2463
+ error: error instanceof Error ? error.message : String(error)
2464
+ });
2465
+ } finally {
2466
+ sessionArtifactSyncRequestsInFlight -= 1;
2467
+ }
2468
+ return;
2469
+ }
2254
2470
  if (message.type === "workspace_incident_updated") {
2255
2471
  activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
2256
2472
  previousPeriodicFingerprint = null;
@@ -2269,7 +2485,7 @@ async function startWorker(options) {
2269
2485
  });
2270
2486
  return;
2271
2487
  }
2272
- if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2488
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
2273
2489
  sendWorkerMessage(ws, {
2274
2490
  type: "update_clis_result",
2275
2491
  requestId: message.requestId,
@@ -2450,6 +2666,7 @@ async function startWorker(options) {
2450
2666
  projectRoot,
2451
2667
  syncRoot,
2452
2668
  artifactRoot,
2669
+ planRoot,
2453
2670
  manifest
2454
2671
  });
2455
2672
  ws.send(