@ricsam/r5d-worker 0.0.14 → 0.0.15

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
@@ -32,7 +32,9 @@ __export(main_exports, {
32
32
  ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
33
33
  isArtifactEnvPath: () => isArtifactEnvPath,
34
34
  prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
35
+ preparePlanEnvForShell: () => preparePlanEnvForShell,
35
36
  resolveProjectFilePath: () => resolveProjectFilePath,
37
+ syncProjectPlans: () => syncProjectPlans,
36
38
  syncSessionArtifacts: () => syncSessionArtifacts
37
39
  });
38
40
  module.exports = __toCommonJS(main_exports);
@@ -71,6 +73,9 @@ function defaultSyncRoot() {
71
73
  function defaultArtifactRoot() {
72
74
  return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "artifacts");
73
75
  }
76
+ function defaultPlanRoot() {
77
+ return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "plans");
78
+ }
74
79
  function printHelp() {
75
80
  process.stdout.write(`Usage:
76
81
  r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
@@ -85,6 +90,7 @@ Options:
85
90
  --project-root <dir> Override projects checkout root (default ~/.r5d/projects)
86
91
  --sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
87
92
  --artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
93
+ --plan-root <dir> Override synced plan root (default from R5D_PLANS_ROOT or ~/.r5d/plans)
88
94
  -v, --version Show r5d-worker version
89
95
  -h, --help Show this help
90
96
  `);
@@ -206,6 +212,11 @@ function parseArgs(argv) {
206
212
  options.artifactRoot = artifactRoot;
207
213
  continue;
208
214
  }
215
+ const planRoot = readOption("--plan-root");
216
+ if (planRoot !== void 0) {
217
+ options.planRoot = planRoot;
218
+ continue;
219
+ }
209
220
  rest.push(arg);
210
221
  }
211
222
  if (options.version) {
@@ -249,6 +260,7 @@ async function readResponseText(response) {
249
260
  }
250
261
  }
251
262
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
263
+ const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
252
264
  function validateArtifactSessionId(sessionId) {
253
265
  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)) {
254
266
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -257,7 +269,7 @@ function validateArtifactSessionId(sessionId) {
257
269
  function assertInsideRoot(rootPath, candidatePath, label) {
258
270
  const relative = import_node_path.default.relative(rootPath, candidatePath);
259
271
  if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
260
- throw new Error(`${label} escapes the artifact directory`);
272
+ throw new Error(`${label} escapes sync directory`);
261
273
  }
262
274
  }
263
275
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -272,6 +284,23 @@ function artifactEnv(artifactRoot, sessionId) {
272
284
  R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
273
285
  };
274
286
  }
287
+ function projectPlanDir(planRoot, projectId, branchName) {
288
+ validateProjectId(projectId);
289
+ validateBranchName(branchName);
290
+ return import_node_path.default.join(planRoot, projectId, branchName);
291
+ }
292
+ function planEnv(planRoot, projectId, branchName, activePlanId) {
293
+ const plansDir = projectPlanDir(planRoot, projectId, branchName);
294
+ const env = {
295
+ [R5D_PLANS_DIR_ENV]: plansDir
296
+ };
297
+ if (activePlanId) {
298
+ validatePlanId(activePlanId);
299
+ env.R5D_ACTIVE_PLAN_ID = activePlanId;
300
+ env.R5D_ACTIVE_PLAN_FILE = import_node_path.default.join(plansDir, `${activePlanId}.plan.md`);
301
+ }
302
+ return env;
303
+ }
275
304
  function isArtifactEnvPath(filePath) {
276
305
  return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
277
306
  }
@@ -284,99 +313,150 @@ function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
284
313
  function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
285
314
  return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
286
315
  }
316
+ function planApiBasePath(projectId, branchName) {
317
+ return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/plans`;
318
+ }
319
+ function planManifestUrl(baseUrl, projectId, branchName) {
320
+ return new URL(planApiBasePath(projectId, branchName), baseUrl);
321
+ }
322
+ function planDownloadUrl(baseUrl, projectId, branchName, filename) {
323
+ return new URL(`${planApiBasePath(projectId, branchName)}/${encodeURIComponent(filename)}`, baseUrl);
324
+ }
287
325
  function sha256Path(filePath) {
288
326
  return (0, import_node_crypto.createHash)("sha256").update(import_node_fs.default.readFileSync(filePath)).digest("hex");
289
327
  }
290
- function parseSessionArtifactManifest(value) {
291
- if (!value || typeof value !== "object" || !Array.isArray(value.artifacts)) {
292
- throw new Error("Artifact manifest response is invalid");
328
+ function parseRemoteFileManifest(value, collectionKey, label) {
329
+ if (!value || typeof value !== "object" || !Array.isArray(value[collectionKey])) {
330
+ throw new Error(`${label} manifest response is invalid`);
293
331
  }
294
- return value.artifacts.map((entry) => {
332
+ return value[collectionKey].map((entry) => {
295
333
  if (!entry || typeof entry !== "object") {
296
- throw new Error("Artifact manifest entry is invalid");
334
+ throw new Error(`${label} manifest entry is invalid`);
297
335
  }
298
- const artifact = entry;
299
- if (typeof artifact.filename !== "string" || artifact.filename.includes("/") || artifact.filename.includes("\\")) {
300
- throw new Error("Artifact manifest entry has an invalid filename");
336
+ const file = entry;
337
+ if (typeof file.filename !== "string" || file.filename.includes("/") || file.filename.includes("\\")) {
338
+ throw new Error(`${label} manifest entry has an invalid filename`);
301
339
  }
302
- if (typeof artifact.size !== "number" || !Number.isFinite(artifact.size) || artifact.size < 0) {
303
- throw new Error(`Artifact manifest entry has an invalid size: ${artifact.filename}`);
340
+ if (typeof file.size !== "number" || !Number.isFinite(file.size) || file.size < 0) {
341
+ throw new Error(`${label} manifest entry has an invalid size: ${file.filename}`);
304
342
  }
305
- if (typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(artifact.sha256)) {
306
- throw new Error(`Artifact manifest entry has an invalid sha256: ${artifact.filename}`);
343
+ if (typeof file.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(file.sha256)) {
344
+ throw new Error(`${label} manifest entry has an invalid sha256: ${file.filename}`);
307
345
  }
308
346
  return {
309
- filename: artifact.filename,
310
- size: artifact.size,
311
- sha256: artifact.sha256.toLowerCase()
347
+ filename: file.filename,
348
+ size: file.size,
349
+ sha256: file.sha256.toLowerCase()
312
350
  };
313
351
  });
314
352
  }
315
- async function fetchSessionArtifactManifest(input) {
316
- const response = await fetch(artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId), {
353
+ async function fetchRemoteFileManifest(input) {
354
+ const response = await fetch(input.url, {
317
355
  headers: {
318
356
  Authorization: `Bearer ${input.token}`
319
357
  }
320
358
  });
321
359
  if (!response.ok) {
322
360
  const detail = (await readResponseText(response)).trim();
323
- throw new Error(`Failed to fetch artifact manifest (${response.status})${detail ? `: ${detail}` : ""}`);
361
+ throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
324
362
  }
325
- return parseSessionArtifactManifest(await response.json());
363
+ return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
326
364
  }
327
- async function downloadSessionArtifact(input) {
328
- const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
329
- const targetPath = import_node_path.default.join(targetDir, input.artifact.filename);
330
- assertInsideRoot(targetDir, targetPath, "Artifact path");
331
- const response = await fetch(
332
- artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, input.artifact.filename),
333
- {
334
- headers: {
335
- Authorization: `Bearer ${input.token}`
336
- }
365
+ async function downloadRemoteSyncedFile(input) {
366
+ const targetPath = import_node_path.default.join(input.targetDir, input.file.filename);
367
+ assertInsideRoot(input.targetDir, targetPath, input.label);
368
+ const response = await fetch(input.url, {
369
+ headers: {
370
+ Authorization: `Bearer ${input.token}`
337
371
  }
338
- );
372
+ });
339
373
  if (!response.ok) {
340
374
  const detail = (await readResponseText(response)).trim();
341
- throw new Error(`Failed to download artifact ${input.artifact.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
375
+ throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
342
376
  }
343
377
  const buffer = Buffer.from(await response.arrayBuffer());
344
378
  const receivedHash = (0, import_node_crypto.createHash)("sha256").update(buffer).digest("hex");
345
- if (receivedHash !== input.artifact.sha256) {
346
- throw new Error(`Downloaded artifact ${input.artifact.filename} failed sha256 verification`);
379
+ if (receivedHash !== input.file.sha256) {
380
+ throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
347
381
  }
348
- if (buffer.length !== input.artifact.size) {
349
- throw new Error(`Downloaded artifact ${input.artifact.filename} has unexpected size`);
382
+ if (buffer.length !== input.file.size) {
383
+ throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
350
384
  }
351
385
  const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
352
386
  import_node_fs.default.writeFileSync(tempPath, buffer);
353
387
  import_node_fs.default.renameSync(tempPath, targetPath);
354
388
  }
355
- async function syncSessionArtifacts(input) {
356
- const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
357
- import_node_fs.default.mkdirSync(targetDir, { recursive: true });
358
- const artifacts = await fetchSessionArtifactManifest(input);
359
- const manifestNames = new Set(artifacts.map((artifact) => artifact.filename));
360
- for (const artifact of artifacts) {
361
- const localPath = import_node_path.default.join(targetDir, artifact.filename);
362
- assertInsideRoot(targetDir, localPath, "Artifact path");
389
+ async function syncRemoteFileSet(input) {
390
+ import_node_fs.default.mkdirSync(input.targetDir, { recursive: true });
391
+ const manifestNames = new Set(input.files.map((file) => file.filename));
392
+ for (const file of input.files) {
393
+ const localPath = import_node_path.default.join(input.targetDir, file.filename);
394
+ assertInsideRoot(input.targetDir, localPath, input.label);
363
395
  if (import_node_fs.default.existsSync(localPath)) {
364
396
  const stats = import_node_fs.default.statSync(localPath);
365
- if (stats.isFile() && stats.size === artifact.size && sha256Path(localPath) === artifact.sha256) {
397
+ if (stats.isFile() && stats.size === file.size && sha256Path(localPath) === file.sha256) {
366
398
  continue;
367
399
  }
368
400
  import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
369
401
  }
370
- await downloadSessionArtifact({ ...input, artifact });
402
+ await input.download(file);
371
403
  }
372
- for (const localName of import_node_fs.default.readdirSync(targetDir)) {
404
+ for (const localName of import_node_fs.default.readdirSync(input.targetDir)) {
373
405
  if (!manifestNames.has(localName)) {
374
- const localPath = import_node_path.default.join(targetDir, localName);
375
- assertInsideRoot(targetDir, localPath, "Artifact path");
406
+ const localPath = import_node_path.default.join(input.targetDir, localName);
407
+ assertInsideRoot(input.targetDir, localPath, input.label);
376
408
  import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
377
409
  }
378
410
  }
379
- return targetDir;
411
+ return input.targetDir;
412
+ }
413
+ async function fetchSessionArtifactManifest(input) {
414
+ return fetchRemoteFileManifest({
415
+ url: artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId),
416
+ token: input.token,
417
+ collectionKey: "artifacts",
418
+ label: "artifact"
419
+ });
420
+ }
421
+ async function syncSessionArtifacts(input) {
422
+ const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
423
+ const artifacts = await fetchSessionArtifactManifest(input);
424
+ return syncRemoteFileSet({
425
+ targetDir,
426
+ files: artifacts,
427
+ label: "Artifact path",
428
+ download: (artifact) => downloadRemoteSyncedFile({
429
+ url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
430
+ token: input.token,
431
+ targetDir,
432
+ file: artifact,
433
+ label: "artifact"
434
+ })
435
+ });
436
+ }
437
+ async function fetchProjectPlanManifest(input) {
438
+ return fetchRemoteFileManifest({
439
+ url: planManifestUrl(input.baseUrl, input.projectId, input.branchName),
440
+ token: input.token,
441
+ collectionKey: "plans",
442
+ label: "plan"
443
+ });
444
+ }
445
+ async function syncProjectPlans(input) {
446
+ const targetDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
447
+ const plans = await fetchProjectPlanManifest(input);
448
+ return syncRemoteFileSet({
449
+ targetDir,
450
+ files: plans,
451
+ label: "Plan path",
452
+ download: (plan) => downloadRemoteSyncedFile({
453
+ url: planDownloadUrl(input.baseUrl, input.projectId, input.branchName, plan.filename),
454
+ token: input.token,
455
+ targetDir,
456
+ file: plan,
457
+ label: "plan"
458
+ })
459
+ });
380
460
  }
381
461
  async function resolveArtifactEnvFilePath(input) {
382
462
  if (!isArtifactEnvPath(input.filePath)) {
@@ -435,6 +515,19 @@ async function prepareArtifactEnvForShell(input) {
435
515
  R5D_ARTIFACTS_DIR: artifactsDir
436
516
  };
437
517
  }
518
+ async function preparePlanEnvForShell(input) {
519
+ try {
520
+ await syncProjectPlans(input);
521
+ } catch (error) {
522
+ process.stderr.write(
523
+ `[r5d-worker] plan sync skipped for ${input.projectId}/${input.branchName}: ${error instanceof Error ? error.message : String(error)}
524
+ `
525
+ );
526
+ }
527
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
528
+ import_node_fs.default.mkdirSync(plansDir, { recursive: true });
529
+ return planEnv(input.planRoot, input.projectId, input.branchName, input.activePlanId);
530
+ }
438
531
  async function verifyR5dctlAuth(baseUrl, token) {
439
532
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
440
533
  let response;
@@ -479,11 +572,24 @@ function validateLabel(label) {
479
572
  throw new Error("Worker label must start with a letter or number and may contain letters, numbers, dots, underscores, and hyphens");
480
573
  }
481
574
  }
575
+ function validateProjectId(projectId) {
576
+ if (projectId === "local" || projectId === "local-code") {
577
+ return;
578
+ }
579
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(projectId)) {
580
+ throw new Error(`Invalid project id from server: ${projectId}`);
581
+ }
582
+ }
482
583
  function validateBranchName(branchName) {
483
584
  if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
484
585
  throw new Error(`Invalid branch name from server: ${branchName}`);
485
586
  }
486
587
  }
588
+ function validatePlanId(planId) {
589
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
590
+ throw new Error(`Invalid plan id from server: ${planId}`);
591
+ }
592
+ }
487
593
  function gitExtraHeaderConfigKey(extraHeaderUrl) {
488
594
  return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
489
595
  }
@@ -1532,6 +1638,14 @@ async function executeCommand(input) {
1532
1638
  sessionId: input.message.sessionId,
1533
1639
  artifactRoot: input.artifactRoot
1534
1640
  });
1641
+ const planProcessEnv = await preparePlanEnvForShell({
1642
+ baseUrl: input.baseUrl,
1643
+ token: input.token,
1644
+ projectId: input.projectId,
1645
+ branchName: input.message.branchName,
1646
+ planRoot: input.planRoot,
1647
+ activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1648
+ });
1535
1649
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1536
1650
  const subprocess = Bun.spawn(input.message.argv, {
1537
1651
  cwd,
@@ -1542,7 +1656,8 @@ async function executeCommand(input) {
1542
1656
  ...containerRegistryEnv(),
1543
1657
  ...input.message.env ?? {},
1544
1658
  ...internalGitProcessEnv(workspace, input.message.env),
1545
- ...artifactProcessEnv
1659
+ ...artifactProcessEnv,
1660
+ ...planProcessEnv
1546
1661
  }
1547
1662
  });
1548
1663
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1613,6 +1728,14 @@ async function executeStreamingCommand(input) {
1613
1728
  sessionId: input.message.sessionId,
1614
1729
  artifactRoot: input.artifactRoot
1615
1730
  });
1731
+ const planProcessEnv = await preparePlanEnvForShell({
1732
+ baseUrl: input.baseUrl,
1733
+ token: input.token,
1734
+ projectId: input.projectId,
1735
+ branchName: input.message.branchName,
1736
+ planRoot: input.planRoot,
1737
+ activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1738
+ });
1616
1739
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1617
1740
  const subprocess = Bun.spawn(input.message.argv, {
1618
1741
  cwd,
@@ -1623,7 +1746,8 @@ async function executeStreamingCommand(input) {
1623
1746
  ...containerRegistryEnv(),
1624
1747
  ...input.message.env ?? {},
1625
1748
  ...internalGitProcessEnv(workspace, input.message.env),
1626
- ...artifactProcessEnv
1749
+ ...artifactProcessEnv,
1750
+ ...planProcessEnv
1627
1751
  }
1628
1752
  });
1629
1753
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1875,7 +1999,7 @@ function resolveHostShell() {
1875
1999
  }
1876
2000
  return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
1877
2001
  }
1878
- function openPty(input) {
2002
+ async function openPty(input) {
1879
2003
  try {
1880
2004
  const branchPath = ensureBranchWorkspace({
1881
2005
  projectId: input.projectId,
@@ -1886,6 +2010,14 @@ function openPty(input) {
1886
2010
  branchName: input.message.branchName,
1887
2011
  manifest: input.manifest
1888
2012
  });
2013
+ const planProcessEnv = await preparePlanEnvForShell({
2014
+ baseUrl: input.baseUrl,
2015
+ token: input.token,
2016
+ projectId: input.projectId,
2017
+ branchName: input.message.branchName,
2018
+ planRoot: input.planRoot,
2019
+ activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
2020
+ });
1889
2021
  const shell = resolveHostShell();
1890
2022
  const ptyProcess = createNodePtyBridge({
1891
2023
  file: shell.file,
@@ -1899,6 +2031,7 @@ function openPty(input) {
1899
2031
  ...process.env,
1900
2032
  ...containerRegistryEnv(),
1901
2033
  ...input.message.env ?? {},
2034
+ ...planProcessEnv,
1902
2035
  R5D_PROJECT_ID: input.projectId,
1903
2036
  R5D_BRANCH_NAME: input.message.branchName
1904
2037
  }
@@ -1999,6 +2132,7 @@ async function startWorker(options) {
1999
2132
  const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
2000
2133
  const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
2001
2134
  const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2135
+ const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
2002
2136
  process.stdout.write(`[r5d-worker] label: ${label}
2003
2137
  `);
2004
2138
  process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
@@ -2006,6 +2140,8 @@ async function startWorker(options) {
2006
2140
  process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
2007
2141
  `);
2008
2142
  process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
2143
+ `);
2144
+ process.stdout.write(`[r5d-worker] plan root: ${planRoot}
2009
2145
  `);
2010
2146
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
2011
2147
  `);
@@ -2014,6 +2150,7 @@ async function startWorker(options) {
2014
2150
  import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
2015
2151
  import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
2016
2152
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2153
+ import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2017
2154
  const manifestByProjectId = /* @__PURE__ */ new Map();
2018
2155
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2019
2156
  headers: {
@@ -2030,7 +2167,8 @@ async function startWorker(options) {
2030
2167
  pid: process.pid,
2031
2168
  version: getWorkerVersion(),
2032
2169
  projectRoot: projectsRoot,
2033
- artifactRoot
2170
+ artifactRoot,
2171
+ planRoot
2034
2172
  }
2035
2173
  };
2036
2174
  ws.send(JSON.stringify(hello));
@@ -2066,6 +2204,20 @@ async function startWorker(options) {
2066
2204
  branchName,
2067
2205
  manifest: project
2068
2206
  });
2207
+ try {
2208
+ await syncProjectPlans({
2209
+ baseUrl,
2210
+ token,
2211
+ projectId: project.projectId,
2212
+ branchName,
2213
+ planRoot
2214
+ });
2215
+ } catch (error) {
2216
+ process.stderr.write(
2217
+ `[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2218
+ `
2219
+ );
2220
+ }
2069
2221
  });
2070
2222
  } catch (error) {
2071
2223
  process.stderr.write(
@@ -2104,7 +2256,7 @@ async function startWorker(options) {
2104
2256
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2105
2257
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
2106
2258
  `);
2107
- openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
2259
+ await openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, planRoot, manifest });
2108
2260
  return;
2109
2261
  }
2110
2262
  if (message.type === "pty_input") {
@@ -2127,7 +2279,7 @@ async function startWorker(options) {
2127
2279
  const result = await withBranchLock(
2128
2280
  message.projectId,
2129
2281
  message.branchName,
2130
- () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
2282
+ () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
2131
2283
  );
2132
2284
  ws.send(JSON.stringify(result));
2133
2285
  return;
@@ -2146,7 +2298,7 @@ async function startWorker(options) {
2146
2298
  void withBranchLock(
2147
2299
  message.projectId,
2148
2300
  message.branchName,
2149
- () => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
2301
+ () => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
2150
2302
  ).catch((error) => {
2151
2303
  sendWorkerMessage(ws, {
2152
2304
  type: "exec_start_error",
@@ -2252,6 +2404,8 @@ if (isCliEntrypoint()) {
2252
2404
  ensureVisibleGitCheckout,
2253
2405
  isArtifactEnvPath,
2254
2406
  prepareArtifactEnvForShell,
2407
+ preparePlanEnvForShell,
2255
2408
  resolveProjectFilePath,
2409
+ syncProjectPlans,
2256
2410
  syncSessionArtifacts
2257
2411
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -33,6 +33,9 @@ function defaultSyncRoot() {
33
33
  function defaultArtifactRoot() {
34
34
  return path.join(os.homedir(), ".r5d", "artifacts");
35
35
  }
36
+ function defaultPlanRoot() {
37
+ return path.join(os.homedir(), ".r5d", "plans");
38
+ }
36
39
  function printHelp() {
37
40
  process.stdout.write(`Usage:
38
41
  r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
@@ -47,6 +50,7 @@ Options:
47
50
  --project-root <dir> Override projects checkout root (default ~/.r5d/projects)
48
51
  --sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
49
52
  --artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
53
+ --plan-root <dir> Override synced plan root (default from R5D_PLANS_ROOT or ~/.r5d/plans)
50
54
  -v, --version Show r5d-worker version
51
55
  -h, --help Show this help
52
56
  `);
@@ -168,6 +172,11 @@ function parseArgs(argv) {
168
172
  options.artifactRoot = artifactRoot;
169
173
  continue;
170
174
  }
175
+ const planRoot = readOption("--plan-root");
176
+ if (planRoot !== void 0) {
177
+ options.planRoot = planRoot;
178
+ continue;
179
+ }
171
180
  rest.push(arg);
172
181
  }
173
182
  if (options.version) {
@@ -211,6 +220,7 @@ async function readResponseText(response) {
211
220
  }
212
221
  }
213
222
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
223
+ const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
214
224
  function validateArtifactSessionId(sessionId) {
215
225
  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)) {
216
226
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -219,7 +229,7 @@ function validateArtifactSessionId(sessionId) {
219
229
  function assertInsideRoot(rootPath, candidatePath, label) {
220
230
  const relative = path.relative(rootPath, candidatePath);
221
231
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
222
- throw new Error(`${label} escapes the artifact directory`);
232
+ throw new Error(`${label} escapes sync directory`);
223
233
  }
224
234
  }
225
235
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -234,6 +244,23 @@ function artifactEnv(artifactRoot, sessionId) {
234
244
  R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
235
245
  };
236
246
  }
247
+ function projectPlanDir(planRoot, projectId, branchName) {
248
+ validateProjectId(projectId);
249
+ validateBranchName(branchName);
250
+ return path.join(planRoot, projectId, branchName);
251
+ }
252
+ function planEnv(planRoot, projectId, branchName, activePlanId) {
253
+ const plansDir = projectPlanDir(planRoot, projectId, branchName);
254
+ const env = {
255
+ [R5D_PLANS_DIR_ENV]: plansDir
256
+ };
257
+ if (activePlanId) {
258
+ validatePlanId(activePlanId);
259
+ env.R5D_ACTIVE_PLAN_ID = activePlanId;
260
+ env.R5D_ACTIVE_PLAN_FILE = path.join(plansDir, `${activePlanId}.plan.md`);
261
+ }
262
+ return env;
263
+ }
237
264
  function isArtifactEnvPath(filePath) {
238
265
  return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
239
266
  }
@@ -246,99 +273,150 @@ function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
246
273
  function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
247
274
  return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
248
275
  }
276
+ function planApiBasePath(projectId, branchName) {
277
+ return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/plans`;
278
+ }
279
+ function planManifestUrl(baseUrl, projectId, branchName) {
280
+ return new URL(planApiBasePath(projectId, branchName), baseUrl);
281
+ }
282
+ function planDownloadUrl(baseUrl, projectId, branchName, filename) {
283
+ return new URL(`${planApiBasePath(projectId, branchName)}/${encodeURIComponent(filename)}`, baseUrl);
284
+ }
249
285
  function sha256Path(filePath) {
250
286
  return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
251
287
  }
252
- function parseSessionArtifactManifest(value) {
253
- if (!value || typeof value !== "object" || !Array.isArray(value.artifacts)) {
254
- throw new Error("Artifact manifest response is invalid");
288
+ function parseRemoteFileManifest(value, collectionKey, label) {
289
+ if (!value || typeof value !== "object" || !Array.isArray(value[collectionKey])) {
290
+ throw new Error(`${label} manifest response is invalid`);
255
291
  }
256
- return value.artifacts.map((entry) => {
292
+ return value[collectionKey].map((entry) => {
257
293
  if (!entry || typeof entry !== "object") {
258
- throw new Error("Artifact manifest entry is invalid");
294
+ throw new Error(`${label} manifest entry is invalid`);
259
295
  }
260
- const artifact = entry;
261
- if (typeof artifact.filename !== "string" || artifact.filename.includes("/") || artifact.filename.includes("\\")) {
262
- throw new Error("Artifact manifest entry has an invalid filename");
296
+ const file = entry;
297
+ if (typeof file.filename !== "string" || file.filename.includes("/") || file.filename.includes("\\")) {
298
+ throw new Error(`${label} manifest entry has an invalid filename`);
263
299
  }
264
- if (typeof artifact.size !== "number" || !Number.isFinite(artifact.size) || artifact.size < 0) {
265
- throw new Error(`Artifact manifest entry has an invalid size: ${artifact.filename}`);
300
+ if (typeof file.size !== "number" || !Number.isFinite(file.size) || file.size < 0) {
301
+ throw new Error(`${label} manifest entry has an invalid size: ${file.filename}`);
266
302
  }
267
- if (typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(artifact.sha256)) {
268
- throw new Error(`Artifact manifest entry has an invalid sha256: ${artifact.filename}`);
303
+ if (typeof file.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(file.sha256)) {
304
+ throw new Error(`${label} manifest entry has an invalid sha256: ${file.filename}`);
269
305
  }
270
306
  return {
271
- filename: artifact.filename,
272
- size: artifact.size,
273
- sha256: artifact.sha256.toLowerCase()
307
+ filename: file.filename,
308
+ size: file.size,
309
+ sha256: file.sha256.toLowerCase()
274
310
  };
275
311
  });
276
312
  }
277
- async function fetchSessionArtifactManifest(input) {
278
- const response = await fetch(artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId), {
313
+ async function fetchRemoteFileManifest(input) {
314
+ const response = await fetch(input.url, {
279
315
  headers: {
280
316
  Authorization: `Bearer ${input.token}`
281
317
  }
282
318
  });
283
319
  if (!response.ok) {
284
320
  const detail = (await readResponseText(response)).trim();
285
- throw new Error(`Failed to fetch artifact manifest (${response.status})${detail ? `: ${detail}` : ""}`);
321
+ throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
286
322
  }
287
- return parseSessionArtifactManifest(await response.json());
323
+ return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
288
324
  }
289
- async function downloadSessionArtifact(input) {
290
- const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
291
- const targetPath = path.join(targetDir, input.artifact.filename);
292
- assertInsideRoot(targetDir, targetPath, "Artifact path");
293
- const response = await fetch(
294
- artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, input.artifact.filename),
295
- {
296
- headers: {
297
- Authorization: `Bearer ${input.token}`
298
- }
325
+ async function downloadRemoteSyncedFile(input) {
326
+ const targetPath = path.join(input.targetDir, input.file.filename);
327
+ assertInsideRoot(input.targetDir, targetPath, input.label);
328
+ const response = await fetch(input.url, {
329
+ headers: {
330
+ Authorization: `Bearer ${input.token}`
299
331
  }
300
- );
332
+ });
301
333
  if (!response.ok) {
302
334
  const detail = (await readResponseText(response)).trim();
303
- throw new Error(`Failed to download artifact ${input.artifact.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
335
+ throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
304
336
  }
305
337
  const buffer = Buffer.from(await response.arrayBuffer());
306
338
  const receivedHash = createHash("sha256").update(buffer).digest("hex");
307
- if (receivedHash !== input.artifact.sha256) {
308
- throw new Error(`Downloaded artifact ${input.artifact.filename} failed sha256 verification`);
339
+ if (receivedHash !== input.file.sha256) {
340
+ throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
309
341
  }
310
- if (buffer.length !== input.artifact.size) {
311
- throw new Error(`Downloaded artifact ${input.artifact.filename} has unexpected size`);
342
+ if (buffer.length !== input.file.size) {
343
+ throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
312
344
  }
313
345
  const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
314
346
  fs.writeFileSync(tempPath, buffer);
315
347
  fs.renameSync(tempPath, targetPath);
316
348
  }
317
- async function syncSessionArtifacts(input) {
318
- const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
319
- fs.mkdirSync(targetDir, { recursive: true });
320
- const artifacts = await fetchSessionArtifactManifest(input);
321
- const manifestNames = new Set(artifacts.map((artifact) => artifact.filename));
322
- for (const artifact of artifacts) {
323
- const localPath = path.join(targetDir, artifact.filename);
324
- assertInsideRoot(targetDir, localPath, "Artifact path");
349
+ async function syncRemoteFileSet(input) {
350
+ fs.mkdirSync(input.targetDir, { recursive: true });
351
+ const manifestNames = new Set(input.files.map((file) => file.filename));
352
+ for (const file of input.files) {
353
+ const localPath = path.join(input.targetDir, file.filename);
354
+ assertInsideRoot(input.targetDir, localPath, input.label);
325
355
  if (fs.existsSync(localPath)) {
326
356
  const stats = fs.statSync(localPath);
327
- if (stats.isFile() && stats.size === artifact.size && sha256Path(localPath) === artifact.sha256) {
357
+ if (stats.isFile() && stats.size === file.size && sha256Path(localPath) === file.sha256) {
328
358
  continue;
329
359
  }
330
360
  fs.rmSync(localPath, { recursive: true, force: true });
331
361
  }
332
- await downloadSessionArtifact({ ...input, artifact });
362
+ await input.download(file);
333
363
  }
334
- for (const localName of fs.readdirSync(targetDir)) {
364
+ for (const localName of fs.readdirSync(input.targetDir)) {
335
365
  if (!manifestNames.has(localName)) {
336
- const localPath = path.join(targetDir, localName);
337
- assertInsideRoot(targetDir, localPath, "Artifact path");
366
+ const localPath = path.join(input.targetDir, localName);
367
+ assertInsideRoot(input.targetDir, localPath, input.label);
338
368
  fs.rmSync(localPath, { recursive: true, force: true });
339
369
  }
340
370
  }
341
- return targetDir;
371
+ return input.targetDir;
372
+ }
373
+ async function fetchSessionArtifactManifest(input) {
374
+ return fetchRemoteFileManifest({
375
+ url: artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId),
376
+ token: input.token,
377
+ collectionKey: "artifacts",
378
+ label: "artifact"
379
+ });
380
+ }
381
+ async function syncSessionArtifacts(input) {
382
+ const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
383
+ const artifacts = await fetchSessionArtifactManifest(input);
384
+ return syncRemoteFileSet({
385
+ targetDir,
386
+ files: artifacts,
387
+ label: "Artifact path",
388
+ download: (artifact) => downloadRemoteSyncedFile({
389
+ url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
390
+ token: input.token,
391
+ targetDir,
392
+ file: artifact,
393
+ label: "artifact"
394
+ })
395
+ });
396
+ }
397
+ async function fetchProjectPlanManifest(input) {
398
+ return fetchRemoteFileManifest({
399
+ url: planManifestUrl(input.baseUrl, input.projectId, input.branchName),
400
+ token: input.token,
401
+ collectionKey: "plans",
402
+ label: "plan"
403
+ });
404
+ }
405
+ async function syncProjectPlans(input) {
406
+ const targetDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
407
+ const plans = await fetchProjectPlanManifest(input);
408
+ return syncRemoteFileSet({
409
+ targetDir,
410
+ files: plans,
411
+ label: "Plan path",
412
+ download: (plan) => downloadRemoteSyncedFile({
413
+ url: planDownloadUrl(input.baseUrl, input.projectId, input.branchName, plan.filename),
414
+ token: input.token,
415
+ targetDir,
416
+ file: plan,
417
+ label: "plan"
418
+ })
419
+ });
342
420
  }
343
421
  async function resolveArtifactEnvFilePath(input) {
344
422
  if (!isArtifactEnvPath(input.filePath)) {
@@ -397,6 +475,19 @@ async function prepareArtifactEnvForShell(input) {
397
475
  R5D_ARTIFACTS_DIR: artifactsDir
398
476
  };
399
477
  }
478
+ async function preparePlanEnvForShell(input) {
479
+ try {
480
+ await syncProjectPlans(input);
481
+ } catch (error) {
482
+ process.stderr.write(
483
+ `[r5d-worker] plan sync skipped for ${input.projectId}/${input.branchName}: ${error instanceof Error ? error.message : String(error)}
484
+ `
485
+ );
486
+ }
487
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
488
+ fs.mkdirSync(plansDir, { recursive: true });
489
+ return planEnv(input.planRoot, input.projectId, input.branchName, input.activePlanId);
490
+ }
400
491
  async function verifyR5dctlAuth(baseUrl, token) {
401
492
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
402
493
  let response;
@@ -441,11 +532,24 @@ function validateLabel(label) {
441
532
  throw new Error("Worker label must start with a letter or number and may contain letters, numbers, dots, underscores, and hyphens");
442
533
  }
443
534
  }
535
+ function validateProjectId(projectId) {
536
+ if (projectId === "local" || projectId === "local-code") {
537
+ return;
538
+ }
539
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(projectId)) {
540
+ throw new Error(`Invalid project id from server: ${projectId}`);
541
+ }
542
+ }
444
543
  function validateBranchName(branchName) {
445
544
  if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
446
545
  throw new Error(`Invalid branch name from server: ${branchName}`);
447
546
  }
448
547
  }
548
+ function validatePlanId(planId) {
549
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
550
+ throw new Error(`Invalid plan id from server: ${planId}`);
551
+ }
552
+ }
449
553
  function gitExtraHeaderConfigKey(extraHeaderUrl) {
450
554
  return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
451
555
  }
@@ -1494,6 +1598,14 @@ async function executeCommand(input) {
1494
1598
  sessionId: input.message.sessionId,
1495
1599
  artifactRoot: input.artifactRoot
1496
1600
  });
1601
+ const planProcessEnv = await preparePlanEnvForShell({
1602
+ baseUrl: input.baseUrl,
1603
+ token: input.token,
1604
+ projectId: input.projectId,
1605
+ branchName: input.message.branchName,
1606
+ planRoot: input.planRoot,
1607
+ activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1608
+ });
1497
1609
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1498
1610
  const subprocess = Bun.spawn(input.message.argv, {
1499
1611
  cwd,
@@ -1504,7 +1616,8 @@ async function executeCommand(input) {
1504
1616
  ...containerRegistryEnv(),
1505
1617
  ...input.message.env ?? {},
1506
1618
  ...internalGitProcessEnv(workspace, input.message.env),
1507
- ...artifactProcessEnv
1619
+ ...artifactProcessEnv,
1620
+ ...planProcessEnv
1508
1621
  }
1509
1622
  });
1510
1623
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1575,6 +1688,14 @@ async function executeStreamingCommand(input) {
1575
1688
  sessionId: input.message.sessionId,
1576
1689
  artifactRoot: input.artifactRoot
1577
1690
  });
1691
+ const planProcessEnv = await preparePlanEnvForShell({
1692
+ baseUrl: input.baseUrl,
1693
+ token: input.token,
1694
+ projectId: input.projectId,
1695
+ branchName: input.message.branchName,
1696
+ planRoot: input.planRoot,
1697
+ activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1698
+ });
1578
1699
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1579
1700
  const subprocess = Bun.spawn(input.message.argv, {
1580
1701
  cwd,
@@ -1585,7 +1706,8 @@ async function executeStreamingCommand(input) {
1585
1706
  ...containerRegistryEnv(),
1586
1707
  ...input.message.env ?? {},
1587
1708
  ...internalGitProcessEnv(workspace, input.message.env),
1588
- ...artifactProcessEnv
1709
+ ...artifactProcessEnv,
1710
+ ...planProcessEnv
1589
1711
  }
1590
1712
  });
1591
1713
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1837,7 +1959,7 @@ function resolveHostShell() {
1837
1959
  }
1838
1960
  return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
1839
1961
  }
1840
- function openPty(input) {
1962
+ async function openPty(input) {
1841
1963
  try {
1842
1964
  const branchPath = ensureBranchWorkspace({
1843
1965
  projectId: input.projectId,
@@ -1848,6 +1970,14 @@ function openPty(input) {
1848
1970
  branchName: input.message.branchName,
1849
1971
  manifest: input.manifest
1850
1972
  });
1973
+ const planProcessEnv = await preparePlanEnvForShell({
1974
+ baseUrl: input.baseUrl,
1975
+ token: input.token,
1976
+ projectId: input.projectId,
1977
+ branchName: input.message.branchName,
1978
+ planRoot: input.planRoot,
1979
+ activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1980
+ });
1851
1981
  const shell = resolveHostShell();
1852
1982
  const ptyProcess = createNodePtyBridge({
1853
1983
  file: shell.file,
@@ -1861,6 +1991,7 @@ function openPty(input) {
1861
1991
  ...process.env,
1862
1992
  ...containerRegistryEnv(),
1863
1993
  ...input.message.env ?? {},
1994
+ ...planProcessEnv,
1864
1995
  R5D_PROJECT_ID: input.projectId,
1865
1996
  R5D_BRANCH_NAME: input.message.branchName
1866
1997
  }
@@ -1961,6 +2092,7 @@ async function startWorker(options) {
1961
2092
  const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
1962
2093
  const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
1963
2094
  const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
2095
+ const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
1964
2096
  process.stdout.write(`[r5d-worker] label: ${label}
1965
2097
  `);
1966
2098
  process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
@@ -1968,6 +2100,8 @@ async function startWorker(options) {
1968
2100
  process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
1969
2101
  `);
1970
2102
  process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
2103
+ `);
2104
+ process.stdout.write(`[r5d-worker] plan root: ${planRoot}
1971
2105
  `);
1972
2106
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
1973
2107
  `);
@@ -1976,6 +2110,7 @@ async function startWorker(options) {
1976
2110
  fs.mkdirSync(projectsRoot, { recursive: true });
1977
2111
  fs.mkdirSync(syncRoot, { recursive: true });
1978
2112
  fs.mkdirSync(artifactRoot, { recursive: true });
2113
+ fs.mkdirSync(planRoot, { recursive: true });
1979
2114
  const manifestByProjectId = /* @__PURE__ */ new Map();
1980
2115
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
1981
2116
  headers: {
@@ -1992,7 +2127,8 @@ async function startWorker(options) {
1992
2127
  pid: process.pid,
1993
2128
  version: getWorkerVersion(),
1994
2129
  projectRoot: projectsRoot,
1995
- artifactRoot
2130
+ artifactRoot,
2131
+ planRoot
1996
2132
  }
1997
2133
  };
1998
2134
  ws.send(JSON.stringify(hello));
@@ -2028,6 +2164,20 @@ async function startWorker(options) {
2028
2164
  branchName,
2029
2165
  manifest: project
2030
2166
  });
2167
+ try {
2168
+ await syncProjectPlans({
2169
+ baseUrl,
2170
+ token,
2171
+ projectId: project.projectId,
2172
+ branchName,
2173
+ planRoot
2174
+ });
2175
+ } catch (error) {
2176
+ process.stderr.write(
2177
+ `[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2178
+ `
2179
+ );
2180
+ }
2031
2181
  });
2032
2182
  } catch (error) {
2033
2183
  process.stderr.write(
@@ -2066,7 +2216,7 @@ async function startWorker(options) {
2066
2216
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2067
2217
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
2068
2218
  `);
2069
- openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
2219
+ await openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, planRoot, manifest });
2070
2220
  return;
2071
2221
  }
2072
2222
  if (message.type === "pty_input") {
@@ -2089,7 +2239,7 @@ async function startWorker(options) {
2089
2239
  const result = await withBranchLock(
2090
2240
  message.projectId,
2091
2241
  message.branchName,
2092
- () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
2242
+ () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
2093
2243
  );
2094
2244
  ws.send(JSON.stringify(result));
2095
2245
  return;
@@ -2108,7 +2258,7 @@ async function startWorker(options) {
2108
2258
  void withBranchLock(
2109
2259
  message.projectId,
2110
2260
  message.branchName,
2111
- () => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
2261
+ () => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
2112
2262
  ).catch((error) => {
2113
2263
  sendWorkerMessage(ws, {
2114
2264
  type: "exec_start_error",
@@ -2213,6 +2363,8 @@ export {
2213
2363
  ensureVisibleGitCheckout,
2214
2364
  isArtifactEnvPath,
2215
2365
  prepareArtifactEnvForShell,
2366
+ preparePlanEnvForShell,
2216
2367
  resolveProjectFilePath,
2368
+ syncProjectPlans,
2217
2369
  syncSessionArtifacts
2218
2370
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "type": "module"
5
5
  }
@@ -8,6 +8,13 @@ export declare function syncSessionArtifacts(input: {
8
8
  sessionId: string;
9
9
  artifactRoot: string;
10
10
  }): Promise<string>;
11
+ export declare function syncProjectPlans(input: {
12
+ baseUrl: string;
13
+ token: string;
14
+ projectId: string;
15
+ branchName: string;
16
+ planRoot: string;
17
+ }): Promise<string>;
11
18
  export declare function prepareArtifactEnvForShell(input: {
12
19
  baseUrl: string;
13
20
  token: string;
@@ -16,6 +23,14 @@ export declare function prepareArtifactEnvForShell(input: {
16
23
  sessionId?: string;
17
24
  artifactRoot: string;
18
25
  }): Promise<Record<string, string>>;
26
+ export declare function preparePlanEnvForShell(input: {
27
+ baseUrl: string;
28
+ token: string;
29
+ projectId: string;
30
+ branchName: string;
31
+ planRoot: string;
32
+ activePlanId?: string;
33
+ }): Promise<Record<string, string>>;
19
34
  type GitAuth = {
20
35
  extraHeaderUrl: string;
21
36
  header: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",