@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/cjs/main.cjs CHANGED
@@ -282,7 +282,25 @@ async function readResponseText(response) {
282
282
  }
283
283
  }
284
284
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
285
+ const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
286
+ const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
285
287
  const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
288
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
289
+ function parseBuiltInToolPath(inputPath) {
290
+ const normalized = inputPath.replace(/\\/g, "/");
291
+ const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
292
+ if (!match) {
293
+ return null;
294
+ }
295
+ const ref = `$${match[1] ?? match[2]}`;
296
+ if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
297
+ throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
298
+ }
299
+ return {
300
+ ref,
301
+ relativePath: match[3] ?? ""
302
+ };
303
+ }
286
304
  function validateArtifactSessionId(sessionId) {
287
305
  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
306
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -291,7 +309,7 @@ function validateArtifactSessionId(sessionId) {
291
309
  function assertInsideRoot(rootPath, candidatePath, label) {
292
310
  const relative = import_node_path.default.relative(rootPath, candidatePath);
293
311
  if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
294
- throw new Error(`${label} escapes sync directory`);
312
+ throw new Error(`${label} escapes its allowed root`);
295
313
  }
296
314
  }
297
315
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -324,7 +342,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
324
342
  return env;
325
343
  }
326
344
  function isArtifactEnvPath(filePath) {
327
- return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
345
+ try {
346
+ return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
347
+ } catch {
348
+ return false;
349
+ }
328
350
  }
329
351
  function artifactApiBasePath(projectId, branchName, sessionId) {
330
352
  return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
@@ -431,56 +453,72 @@ async function fetchSessionArtifactManifest(input) {
431
453
  label: "artifact"
432
454
  });
433
455
  }
456
+ const sessionArtifactSyncQueues = /* @__PURE__ */ new Map();
434
457
  async function syncSessionArtifacts(input) {
435
- const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
436
- const artifacts = await fetchSessionArtifactManifest(input);
437
- return syncRemoteFileSet({
438
- targetDir,
439
- files: artifacts,
440
- label: "Artifact path",
441
- download: (artifact) => downloadRemoteSyncedFile({
442
- url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
443
- token: input.token,
458
+ const queueKey = `${import_node_path.default.resolve(input.artifactRoot)}\0${input.sessionId}`;
459
+ const previous = sessionArtifactSyncQueues.get(queueKey);
460
+ const queued = (previous ? previous.catch(() => "") : Promise.resolve("")).then(async () => {
461
+ const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
462
+ const artifacts = await fetchSessionArtifactManifest(input);
463
+ return syncRemoteFileSet({
444
464
  targetDir,
445
- file: artifact,
446
- label: "artifact"
447
- })
465
+ files: artifacts,
466
+ label: "Artifact path",
467
+ download: (artifact) => downloadRemoteSyncedFile({
468
+ url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
469
+ token: input.token,
470
+ targetDir,
471
+ file: artifact,
472
+ label: "artifact"
473
+ })
474
+ });
448
475
  });
476
+ sessionArtifactSyncQueues.set(queueKey, queued);
477
+ const release = () => {
478
+ if (sessionArtifactSyncQueues.get(queueKey) === queued) sessionArtifactSyncQueues.delete(queueKey);
479
+ };
480
+ void queued.then(release, release);
481
+ return queued;
449
482
  }
450
- async function resolveArtifactEnvFilePath(input) {
451
- if (!isArtifactEnvPath(input.filePath)) {
452
- return null;
483
+ async function prepareBuiltInToolPaths(input) {
484
+ const parsed = parseBuiltInToolPath(input.filePath);
485
+ if (!parsed) {
486
+ return void 0;
453
487
  }
454
- if (!input.sessionId) {
455
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
488
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
489
+ if (input.access === "write") {
490
+ throw new Error(
491
+ `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
492
+ );
493
+ }
494
+ if (!input.sessionId) {
495
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
496
+ }
497
+ return {
498
+ artifactsDir: await syncSessionArtifacts({
499
+ baseUrl: input.baseUrl,
500
+ token: input.token,
501
+ projectId: input.projectId,
502
+ branchName: input.branchName,
503
+ sessionId: input.sessionId,
504
+ artifactRoot: input.artifactRoot
505
+ })
506
+ };
456
507
  }
457
- if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
458
- throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
508
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
509
+ import_node_fs.default.mkdirSync(plansDir, { recursive: true });
510
+ if (parsed.ref === R5D_PLANS_DIR_REF) {
511
+ return { plansDir };
459
512
  }
460
- const targetDir = await syncSessionArtifacts({
461
- baseUrl: input.baseUrl,
462
- token: input.token,
463
- projectId: input.projectId,
464
- branchName: input.branchName,
465
- sessionId: input.sessionId,
466
- artifactRoot: input.artifactRoot
467
- });
468
- const relativePath = import_node_path.default.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
469
- if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
470
- throw new Error(`Invalid artifact path: ${input.filePath}`);
513
+ if (!input.activePlanId) {
514
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
471
515
  }
472
- const absolutePath = import_node_path.default.resolve(targetDir, ...relativePath.split("/"));
473
- assertInsideRoot(targetDir, absolutePath, "Artifact path");
516
+ validatePlanId(input.activePlanId);
474
517
  return {
475
- absolutePath,
476
- virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
518
+ plansDir,
519
+ activePlanFile: import_node_path.default.join(plansDir, `${input.activePlanId}.plan.md`)
477
520
  };
478
521
  }
479
- function rejectArtifactWritePath(filePath) {
480
- if (isArtifactEnvPath(filePath)) {
481
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
482
- }
483
- }
484
522
  async function prepareArtifactEnvForShell(input) {
485
523
  if (!input.sessionId) {
486
524
  return {};
@@ -631,10 +669,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
631
669
  throw new Error(`Invalid project file path: ${inputPath}`);
632
670
  }
633
671
  }
634
- function resolveWorkerFilePath(branchPath, inputPath) {
672
+ function assertPathInsideRealRoot(rootPath, candidatePath, label) {
673
+ const resolvedRoot = import_node_path.default.resolve(rootPath);
674
+ const resolvedCandidate = import_node_path.default.resolve(candidatePath);
675
+ assertInsideRoot(resolvedRoot, resolvedCandidate, label);
676
+ let existingPath = resolvedCandidate;
677
+ while (!import_node_fs.default.existsSync(existingPath)) {
678
+ const parentPath = import_node_path.default.dirname(existingPath);
679
+ if (parentPath === existingPath) {
680
+ throw new Error(`${label} root does not exist: ${rootPath}`);
681
+ }
682
+ existingPath = parentPath;
683
+ }
684
+ const realRoot = import_node_fs.default.realpathSync(resolvedRoot);
685
+ const realExistingPath = import_node_fs.default.realpathSync(existingPath);
686
+ assertInsideRoot(realRoot, realExistingPath, label);
687
+ }
688
+ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
689
+ if (parsed.relativePath.split("/").includes("..")) {
690
+ throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
691
+ }
692
+ let rootPath;
693
+ let absolutePath;
694
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
695
+ rootPath = builtInPaths?.artifactsDir;
696
+ } else if (parsed.ref === R5D_PLANS_DIR_REF) {
697
+ rootPath = builtInPaths?.plansDir;
698
+ } else {
699
+ if (parsed.relativePath) {
700
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
701
+ }
702
+ rootPath = builtInPaths?.plansDir;
703
+ absolutePath = builtInPaths?.activePlanFile;
704
+ }
705
+ if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
706
+ const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
707
+ throw new Error(`${parsed.ref} paths require ${requirement}`);
708
+ }
709
+ const normalizedPath = parsed.relativePath ? import_node_path.default.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
710
+ const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
711
+ absolutePath ??= normalizedRelativePath ? import_node_path.default.resolve(rootPath, ...normalizedRelativePath.split("/")) : import_node_path.default.resolve(rootPath);
712
+ assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
713
+ return {
714
+ absolutePath,
715
+ displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
716
+ repoRelativePath: null,
717
+ scope: "virtual",
718
+ virtualRootPath: import_node_path.default.resolve(rootPath)
719
+ };
720
+ }
721
+ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
635
722
  if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
636
723
  throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
637
724
  }
725
+ const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
726
+ if (parsedBuiltInPath) {
727
+ return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
728
+ }
638
729
  const resolvedBranchPath = import_node_path.default.resolve(branchPath);
639
730
  if (import_node_path.default.isAbsolute(inputPath)) {
640
731
  const absolutePath2 = import_node_path.default.resolve(inputPath);
@@ -1091,13 +1182,13 @@ async function withFileMutationQueue(key, operation) {
1091
1182
  }
1092
1183
  }
1093
1184
  function mutationQueueKey(projectId, branchName, resolved) {
1094
- if (resolved.scope === "host") {
1095
- return `host:${import_node_path.default.normalize(resolved.absolutePath)}`;
1185
+ if (resolved.scope !== "project") {
1186
+ return `${resolved.scope}:${import_node_path.default.normalize(resolved.absolutePath)}`;
1096
1187
  }
1097
1188
  return `project:${projectId}:${branchName}:${import_node_path.default.posix.normalize(resolved.repoRelativePath)}`;
1098
1189
  }
1099
- function readWorkerTextFile(branchPath, filePath, offset, limit) {
1100
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1190
+ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
1191
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1101
1192
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1102
1193
  throw new Error(`File not found: ${resolved.displayPath}`);
1103
1194
  }
@@ -1111,8 +1202,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1111
1202
  ...formatted
1112
1203
  };
1113
1204
  }
1114
- function writeWorkerTextFile(branchPath, filePath, content) {
1115
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1205
+ function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
1206
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1116
1207
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1117
1208
  import_node_fs.default.writeFileSync(resolved.absolutePath, content, "utf8");
1118
1209
  return {
@@ -1121,8 +1212,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
1121
1212
  gitBlobHash: getGitBlobHashForContent(content)
1122
1213
  };
1123
1214
  }
1124
- function editWorkerTextFile(branchPath, filePath, edits) {
1125
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1215
+ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
1216
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1126
1217
  if (!Array.isArray(edits) || edits.length === 0) {
1127
1218
  throw new Error("edit requires at least one replacement");
1128
1219
  }
@@ -1169,46 +1260,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
1169
1260
  };
1170
1261
  }
1171
1262
  async function executeReadFileOperation(input) {
1172
- const artifact = await resolveArtifactEnvFilePath({
1263
+ const builtInPaths = await prepareBuiltInToolPaths({
1173
1264
  filePath: input.message.filePath,
1174
1265
  baseUrl: input.baseUrl,
1175
1266
  token: input.token,
1176
1267
  projectId: input.projectId,
1177
1268
  branchName: input.message.branchName,
1178
1269
  sessionId: input.message.sessionId,
1179
- artifactRoot: input.artifactRoot
1270
+ activePlanId: input.message.activePlanId,
1271
+ artifactRoot: input.artifactRoot,
1272
+ planRoot: input.planRoot,
1273
+ access: "read"
1180
1274
  });
1181
- if (artifact) {
1182
- if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1183
- throw new Error(`File not found: ${artifact.virtualPath}`);
1184
- }
1185
- const buffer = import_node_fs.default.readFileSync(artifact.absolutePath);
1186
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1187
- return {
1188
- type: "read",
1189
- kind: "text",
1190
- file: artifact.virtualPath,
1191
- gitBlobHash: getGitBlobHashForContent(buffer),
1192
- ...formatted
1193
- };
1194
- }
1195
1275
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1196
- return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1276
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1197
1277
  }
1198
1278
  async function executeWriteFileOperation(input) {
1199
- rejectArtifactWritePath(input.message.filePath);
1279
+ const builtInPaths = await prepareBuiltInToolPaths({
1280
+ filePath: input.message.filePath,
1281
+ baseUrl: input.baseUrl,
1282
+ token: input.token,
1283
+ projectId: input.projectId,
1284
+ branchName: input.message.branchName,
1285
+ sessionId: input.message.sessionId,
1286
+ activePlanId: input.message.activePlanId,
1287
+ artifactRoot: input.artifactRoot,
1288
+ planRoot: input.planRoot,
1289
+ access: "write"
1290
+ });
1200
1291
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1201
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1292
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1202
1293
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1203
- return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1294
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
1204
1295
  });
1205
1296
  }
1206
1297
  async function executeEditFileOperation(input) {
1207
- rejectArtifactWritePath(input.message.filePath);
1298
+ const builtInPaths = await prepareBuiltInToolPaths({
1299
+ filePath: input.message.filePath,
1300
+ baseUrl: input.baseUrl,
1301
+ token: input.token,
1302
+ projectId: input.projectId,
1303
+ branchName: input.message.branchName,
1304
+ sessionId: input.message.sessionId,
1305
+ activePlanId: input.message.activePlanId,
1306
+ artifactRoot: input.artifactRoot,
1307
+ planRoot: input.planRoot,
1308
+ access: "write"
1309
+ });
1208
1310
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1209
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1311
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1210
1312
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1211
- return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1313
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
1212
1314
  });
1213
1315
  }
1214
1316
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1229,16 +1331,20 @@ function normalizeFindPattern(pattern) {
1229
1331
  function walkWorkerEntries(branchPath, start) {
1230
1332
  const entries = [];
1231
1333
  const visitedDirectories = /* @__PURE__ */ new Set();
1334
+ const realVirtualRoot = start.scope === "virtual" ? import_node_fs.default.realpathSync(start.virtualRootPath) : null;
1232
1335
  const visit = (absolutePath, isRoot) => {
1233
1336
  let stat;
1234
1337
  try {
1235
1338
  stat = import_node_fs.default.statSync(absolutePath);
1339
+ if (realVirtualRoot) {
1340
+ assertInsideRoot(realVirtualRoot, import_node_fs.default.realpathSync(absolutePath), `${start.displayPath} path`);
1341
+ }
1236
1342
  } catch (error) {
1237
1343
  if (isRoot) throw error;
1238
1344
  return;
1239
1345
  }
1240
- const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : import_node_path.default.resolve(absolutePath);
1241
1346
  const hostRelativePath = import_node_path.default.relative(start.absolutePath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
1347
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : import_node_path.default.resolve(absolutePath);
1242
1348
  const matchPath = start.scope === "project" ? displayPath : hostRelativePath || import_node_path.default.basename(start.absolutePath);
1243
1349
  entries.push({ absolutePath, displayPath, matchPath, stat });
1244
1350
  if (!stat.isDirectory()) return;
@@ -1269,8 +1375,8 @@ function walkWorkerEntries(branchPath, start) {
1269
1375
  function isProbablyText(bytes) {
1270
1376
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1271
1377
  }
1272
- function grepWorkerFiles(branchPath, input) {
1273
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1378
+ function grepWorkerFiles(branchPath, input, builtInPaths) {
1379
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1274
1380
  if (!import_node_fs.default.existsSync(start.absolutePath)) {
1275
1381
  throw new Error(`Path not found: ${start.displayPath}`);
1276
1382
  }
@@ -1316,17 +1422,34 @@ function grepWorkerFiles(branchPath, input) {
1316
1422
  };
1317
1423
  }
1318
1424
  async function executeGrepOperation(input) {
1319
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1320
- return grepWorkerFiles(workspace.branchPath, {
1321
- pattern: input.message.pattern,
1322
- path: input.message.path,
1323
- glob: input.message.glob,
1324
- caseSensitive: input.message.caseSensitive,
1325
- limit: input.message.limit
1425
+ const toolPath = input.message.path ?? ".";
1426
+ const builtInPaths = await prepareBuiltInToolPaths({
1427
+ filePath: toolPath,
1428
+ baseUrl: input.baseUrl,
1429
+ token: input.token,
1430
+ projectId: input.projectId,
1431
+ branchName: input.message.branchName,
1432
+ sessionId: input.message.sessionId,
1433
+ activePlanId: input.message.activePlanId,
1434
+ artifactRoot: input.artifactRoot,
1435
+ planRoot: input.planRoot,
1436
+ access: "read"
1326
1437
  });
1438
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1439
+ return grepWorkerFiles(
1440
+ workspace.branchPath,
1441
+ {
1442
+ pattern: input.message.pattern,
1443
+ path: input.message.path,
1444
+ glob: input.message.glob,
1445
+ caseSensitive: input.message.caseSensitive,
1446
+ limit: input.message.limit
1447
+ },
1448
+ builtInPaths
1449
+ );
1327
1450
  }
1328
- function findWorkerFiles(branchPath, input) {
1329
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1451
+ function findWorkerFiles(branchPath, input, builtInPaths) {
1452
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1330
1453
  if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
1331
1454
  throw new Error(`Directory not found: ${start.displayPath}`);
1332
1455
  }
@@ -1360,16 +1483,33 @@ function findWorkerFiles(branchPath, input) {
1360
1483
  };
1361
1484
  }
1362
1485
  async function executeFindOperation(input) {
1363
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1364
- return findWorkerFiles(workspace.branchPath, {
1365
- pattern: input.message.pattern,
1366
- path: input.message.path,
1367
- entryType: input.message.entryType,
1368
- limit: input.message.limit
1486
+ const toolPath = input.message.path ?? ".";
1487
+ const builtInPaths = await prepareBuiltInToolPaths({
1488
+ filePath: toolPath,
1489
+ baseUrl: input.baseUrl,
1490
+ token: input.token,
1491
+ projectId: input.projectId,
1492
+ branchName: input.message.branchName,
1493
+ sessionId: input.message.sessionId,
1494
+ activePlanId: input.message.activePlanId,
1495
+ artifactRoot: input.artifactRoot,
1496
+ planRoot: input.planRoot,
1497
+ access: "read"
1369
1498
  });
1499
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1500
+ return findWorkerFiles(
1501
+ workspace.branchPath,
1502
+ {
1503
+ pattern: input.message.pattern,
1504
+ path: input.message.path,
1505
+ entryType: input.message.entryType,
1506
+ limit: input.message.limit
1507
+ },
1508
+ builtInPaths
1509
+ );
1370
1510
  }
1371
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1372
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
1511
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
1512
+ const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
1373
1513
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
1374
1514
  throw new Error(`Directory not found: ${resolved.displayPath}`);
1375
1515
  }
@@ -1387,11 +1527,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1387
1527
  };
1388
1528
  }
1389
1529
  async function executeLsOperation(input) {
1530
+ const toolPath = input.message.path ?? ".";
1531
+ const builtInPaths = await prepareBuiltInToolPaths({
1532
+ filePath: toolPath,
1533
+ baseUrl: input.baseUrl,
1534
+ token: input.token,
1535
+ projectId: input.projectId,
1536
+ branchName: input.message.branchName,
1537
+ sessionId: input.message.sessionId,
1538
+ activePlanId: input.message.activePlanId,
1539
+ artifactRoot: input.artifactRoot,
1540
+ planRoot: input.planRoot,
1541
+ access: "read"
1542
+ });
1390
1543
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1391
- return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1544
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
1392
1545
  }
1393
- function readWorkerImageFile(branchPath, filePath) {
1394
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1546
+ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1547
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1395
1548
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1396
1549
  throw new Error(`File not found: ${resolved.displayPath}`);
1397
1550
  }
@@ -1413,38 +1566,20 @@ function readWorkerImageFile(branchPath, filePath) {
1413
1566
  };
1414
1567
  }
1415
1568
  async function executeViewFileBytesOperation(input) {
1416
- const artifact = await resolveArtifactEnvFilePath({
1569
+ const builtInPaths = await prepareBuiltInToolPaths({
1417
1570
  filePath: input.message.filePath,
1418
1571
  baseUrl: input.baseUrl,
1419
1572
  token: input.token,
1420
1573
  projectId: input.projectId,
1421
1574
  branchName: input.message.branchName,
1422
1575
  sessionId: input.message.sessionId,
1423
- artifactRoot: input.artifactRoot
1576
+ activePlanId: input.message.activePlanId,
1577
+ artifactRoot: input.artifactRoot,
1578
+ planRoot: input.planRoot,
1579
+ access: "read"
1424
1580
  });
1425
- if (artifact) {
1426
- if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1427
- throw new Error(`File not found: ${artifact.virtualPath}`);
1428
- }
1429
- const extension = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1430
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1431
- if (!mediaType) {
1432
- throw new Error("read supports PNG and JPEG image inputs only");
1433
- }
1434
- const bytes = import_node_fs.default.readFileSync(artifact.absolutePath);
1435
- const dimensions = readImageDimensions(bytes, mediaType);
1436
- return {
1437
- type: "view_file_bytes",
1438
- filePath: artifact.virtualPath,
1439
- mediaType,
1440
- base64: bytes.toString("base64"),
1441
- fileSizeBytes: bytes.length,
1442
- width: dimensions.width,
1443
- height: dimensions.height
1444
- };
1445
- }
1446
1581
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1447
- return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1582
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
1448
1583
  }
1449
1584
  async function executeOperation(input) {
1450
1585
  switch (input.message.type) {
@@ -2083,6 +2218,7 @@ async function startWorker(options) {
2083
2218
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2084
2219
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2085
2220
  const manifestByProjectId = /* @__PURE__ */ new Map();
2221
+ const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2086
2222
  const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
2087
2223
  let workspaceRemoteUrl = null;
2088
2224
  let activeWorkspaceIncidentId = null;
@@ -2091,6 +2227,7 @@ async function startWorker(options) {
2091
2227
  let periodicWorkspaceScanInFlight = false;
2092
2228
  let terminalReplayTimer;
2093
2229
  let workspaceSyncRequestsInFlight = 0;
2230
+ let sessionArtifactSyncRequestsInFlight = 0;
2094
2231
  let cliUpdateInProgress = false;
2095
2232
  let reloadAfterClose = false;
2096
2233
  const workspaceSyncInput = (trigger, overrides = {}) => {
@@ -2110,37 +2247,64 @@ async function startWorker(options) {
2110
2247
  const sendWorkspaceSyncResult = (requestId, result) => {
2111
2248
  sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2112
2249
  };
2113
- const ensureAllVisibleWorkspaceCheckouts = () => {
2250
+ const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2251
+ const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2252
+ (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2253
+ );
2254
+ const ensureVisibleWorkspaceCheckouts = (targets) => {
2114
2255
  const created = [];
2115
- for (const manifest of manifestByProjectId.values()) {
2116
- for (const branchName of manifest.branches) {
2117
- const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2118
- const existed = import_node_fs.default.existsSync(import_node_path.default.join(branchPath, ".git"));
2119
- const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2120
- ensureVisibleGitCheckout({
2121
- projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2122
- branchName,
2123
- githubRemoteUrl,
2124
- githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2125
- githubAuthHeader: manifest.repoAuthHeader,
2126
- defaultBranch: manifest.defaultBranch || "main",
2127
- reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2128
- });
2129
- if (!existed) created.push({ projectId: manifest.projectId, branchName });
2256
+ for (const target of targets) {
2257
+ const manifest = manifestByProjectId.get(target.projectId);
2258
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2259
+ if (!manifest || !manifest.branches.includes(target.branchName)) {
2260
+ pendingManifestCheckouts.delete(key);
2261
+ continue;
2130
2262
  }
2263
+ const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2264
+ const branchPath = import_node_path.default.join(projectRoot, target.branchName);
2265
+ const existed = hasNormalVisibleGitDir(branchPath);
2266
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2267
+ ensureVisibleGitCheckout({
2268
+ projectRoot,
2269
+ branchName: target.branchName,
2270
+ githubRemoteUrl,
2271
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2272
+ githubAuthHeader: manifest.repoAuthHeader,
2273
+ defaultBranch: manifest.defaultBranch || "main",
2274
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2275
+ });
2276
+ if (!existed) created.push(target);
2131
2277
  }
2132
2278
  return created;
2133
2279
  };
2134
2280
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2135
2281
  workspaceSyncRequestsInFlight += 1;
2136
2282
  try {
2137
- const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2138
- const result = await workspaceSyncSingleFlight.run(
2139
- workspaceSyncInput(trigger, {
2283
+ let pendingTargets = [];
2284
+ const result = await workspaceSyncSingleFlight.runPrepared(() => {
2285
+ const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2286
+ let checkoutTargets = [];
2287
+ if (shouldEnsureCheckouts) {
2288
+ checkoutTargets = trigger.type === "connect" ? allManifestCheckouts() : [...pendingManifestCheckouts.values()];
2289
+ }
2290
+ pendingTargets = shouldEnsureCheckouts ? [...pendingManifestCheckouts.values()] : [];
2291
+ const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets);
2292
+ const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2293
+ for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2294
+ newVisibleCheckoutByKey.set(manifestCheckoutKey(target.projectId, target.branchName), target);
2295
+ }
2296
+ const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2297
+ return workspaceSyncInput(trigger, {
2140
2298
  ...overrides,
2141
2299
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2142
- })
2143
- );
2300
+ });
2301
+ });
2302
+ if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
2303
+ for (const target of pendingTargets) {
2304
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2305
+ if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2306
+ }
2307
+ }
2144
2308
  previousPeriodicFingerprint = null;
2145
2309
  sendWorkspaceSyncResult(requestId, result);
2146
2310
  return result;
@@ -2233,9 +2397,26 @@ async function startWorker(options) {
2233
2397
  configureGitHubAuth(message.githubCredential);
2234
2398
  visibleGitIdentity = message.gitIdentity;
2235
2399
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2400
+ const previousCheckoutKeys = new Set(
2401
+ allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2402
+ );
2236
2403
  const migratedProjectIds = (0, import_managed_paths.migrateLegacyProjectRoots)(projectsRoot, message.projects);
2237
2404
  manifestByProjectId.clear();
2238
2405
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2406
+ const currentCheckouts = allManifestCheckouts();
2407
+ const currentCheckoutKeys = new Set(
2408
+ currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2409
+ );
2410
+ for (const key of pendingManifestCheckouts.keys()) {
2411
+ if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2412
+ }
2413
+ for (const checkout of currentCheckouts) {
2414
+ const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2415
+ if (previousCheckoutKeys.has(key)) continue;
2416
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2417
+ if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2418
+ }
2419
+ previousPeriodicFingerprint = null;
2239
2420
  process.stdout.write(
2240
2421
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2241
2422
  `
@@ -2246,8 +2427,15 @@ async function startWorker(options) {
2246
2427
  if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2247
2428
  periodicWorkspaceScanInFlight = true;
2248
2429
  void (async () => {
2249
- const input = workspaceSyncInput({ type: "periodic" });
2250
- const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2430
+ if (pendingManifestCheckouts.size > 0) {
2431
+ previousPeriodicFingerprint = null;
2432
+ await runRequestedWorkspaceSync(crypto.randomUUID(), {
2433
+ type: "periodic",
2434
+ detail: "workspace manifest additions"
2435
+ });
2436
+ return;
2437
+ }
2438
+ const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
2251
2439
  if (fingerprint === emptyFingerprint) {
2252
2440
  previousPeriodicFingerprint = null;
2253
2441
  return;
@@ -2287,6 +2475,34 @@ async function startWorker(options) {
2287
2475
  }
2288
2476
  return;
2289
2477
  }
2478
+ if (message.type === "sync_session_artifacts") {
2479
+ sessionArtifactSyncRequestsInFlight += 1;
2480
+ try {
2481
+ await syncSessionArtifacts({
2482
+ baseUrl,
2483
+ token,
2484
+ projectId: message.projectId,
2485
+ branchName: message.branchName,
2486
+ sessionId: message.sessionId,
2487
+ artifactRoot
2488
+ });
2489
+ sendWorkerMessage(ws, {
2490
+ type: "session_artifacts_sync_result",
2491
+ requestId: message.requestId,
2492
+ sessionId: message.sessionId
2493
+ });
2494
+ } catch (error) {
2495
+ sendWorkerMessage(ws, {
2496
+ type: "session_artifacts_sync_result",
2497
+ requestId: message.requestId,
2498
+ sessionId: message.sessionId,
2499
+ error: error instanceof Error ? error.message : String(error)
2500
+ });
2501
+ } finally {
2502
+ sessionArtifactSyncRequestsInFlight -= 1;
2503
+ }
2504
+ return;
2505
+ }
2290
2506
  if (message.type === "workspace_incident_updated") {
2291
2507
  activeWorkspaceIncidentId = message.status === "remediating" || message.status === "waiting_for_worker" ? message.incidentId : null;
2292
2508
  previousPeriodicFingerprint = null;
@@ -2305,7 +2521,7 @@ async function startWorker(options) {
2305
2521
  });
2306
2522
  return;
2307
2523
  }
2308
- if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0) {
2524
+ if (activeProcesses.size > 0 || activePtys.size > 0 || workspaceSyncRequestsInFlight > 0 || sessionArtifactSyncRequestsInFlight > 0) {
2309
2525
  sendWorkerMessage(ws, {
2310
2526
  type: "update_clis_result",
2311
2527
  requestId: message.requestId,
@@ -2486,6 +2702,7 @@ async function startWorker(options) {
2486
2702
  projectRoot,
2487
2703
  syncRoot,
2488
2704
  artifactRoot,
2705
+ planRoot,
2489
2706
  manifest
2490
2707
  });
2491
2708
  ws.send(