agent-relay-orchestrator 0.16.0 → 0.17.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-orchestrator",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "test": "bun test"
17
17
  },
18
18
  "dependencies": {
19
- "agent-relay-sdk": "0.2.7"
19
+ "agent-relay-sdk": "0.2.8"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "latest",
package/src/control.ts CHANGED
@@ -183,6 +183,7 @@ export function spawnOptionsFromControl(ctrl: Record<string, any>, config: Orche
183
183
  effort: typeof ctrl.effort === "string" ? ctrl.effort : undefined,
184
184
  profile: typeof ctrl.profile === "string" ? ctrl.profile : undefined,
185
185
  workspaceMode: workspaceMode(ctrl.workspaceMode),
186
+ workspaceSymlinks: stringArray(ctrl.workspaceSymlinks),
186
187
  agentProfile: isRecord(ctrl.agentProfile) ? ctrl.agentProfile : undefined,
187
188
  label: typeof ctrl.label === "string" ? ctrl.label : undefined,
188
189
  agentId: typeof ctrl.agentId === "string" ? ctrl.agentId : undefined,
@@ -209,6 +210,7 @@ export function spawnOptionsFromRestartSource(restartSource: Record<string, any>
209
210
  effort: typeof restartSource.effort === "string" ? restartSource.effort : undefined,
210
211
  profile: typeof restartSource.profile === "string" ? restartSource.profile : undefined,
211
212
  workspaceMode: workspaceMode(restartSource.workspaceMode),
213
+ workspaceSymlinks: stringArray(restartSource.workspaceSymlinks),
212
214
  agentProfile: isRecord(restartSource.agentProfile) ? restartSource.agentProfile : undefined,
213
215
  label: typeof restartSource.label === "string" ? restartSource.label : undefined,
214
216
  agentId: typeof restartSource.agentId === "string" ? restartSource.agentId : undefined,
package/src/spawn.ts CHANGED
@@ -16,6 +16,8 @@ export interface SpawnOptions {
16
16
  profile?: string;
17
17
  workspaceMode?: WorkspaceMode;
18
18
  workspace?: WorkspaceMetadata;
19
+ /** Untracked files/dirs to symlink from main into an isolated worktree (relay's global workspace config). */
20
+ workspaceSymlinks?: string[];
19
21
  agentProfile?: Record<string, unknown>;
20
22
  label?: string;
21
23
  agentId?: string;
@@ -361,6 +363,7 @@ export async function spawnAgent(
361
363
  const resolvedWorkspace = await resolveSpawnWorkspace({
362
364
  ...opts,
363
365
  label,
366
+ workspaceSymlinks: opts.workspaceSymlinks,
364
367
  workspaceRoot: join(resolve(config.baseDir), ".agent-relay", "workspaces"),
365
368
  });
366
369
  const spawnOpts = { ...opts, label, agentId, cwd: resolvedWorkspace.cwd, workspace: resolvedWorkspace.workspace };
@@ -1,8 +1,8 @@
1
1
  import { existsSync, lstatSync, mkdirSync, readdirSync, statSync, symlinkSync, type Dirent } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
- import { basename, join, resolve } from "node:path";
5
- import type { WorkspaceDepsProvision, WorkspaceDiff, WorkspaceDiffFile, WorkspaceGitState, WorkspaceMergePreview, WorkspaceMergeResult, WorkspaceMetadata, WorkspaceMode, WorkspaceProbe, WorkspaceProbeWorktree, WorkspaceStatus } from "agent-relay-sdk";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import type { WorkspaceDepsProvision, WorkspaceDiff, WorkspaceDiffFile, WorkspaceGitState, WorkspaceMergePreview, WorkspaceMergeResult, WorkspaceMetadata, WorkspaceMode, WorkspaceProbe, WorkspaceProbeWorktree, WorkspaceStatus, WorkspaceSymlinkProvision } from "agent-relay-sdk";
6
6
 
7
7
  const MAX_DIFF_PATCH_BYTES = 200_000;
8
8
 
@@ -13,6 +13,7 @@ interface WorkspaceResolutionInput {
13
13
  spawnRequestId?: string;
14
14
  workspaceMode?: WorkspaceMode;
15
15
  workspaceRoot?: string;
16
+ workspaceSymlinks?: string[];
16
17
  automationId?: string;
17
18
  automationRunId?: string;
18
19
  }
@@ -134,6 +135,10 @@ export async function resolveSpawnWorkspace(input: WorkspaceResolutionInput): Pr
134
135
  // runner so the agent can typecheck/test/build without manual setup (#159).
135
136
  const deps = provisionWorkspaceDeps(repoRoot, worktreePath);
136
137
 
138
+ // Symlink configured untracked paths (AGENTS.md, .claude-rig, …) from main. Like
139
+ // node_modules, these are gitignored so the fresh worktree lacks them (#159 follow-up).
140
+ const symlinks = provisionWorkspaceSymlinks(repoRoot, worktreePath, input.workspaceSymlinks ?? []);
141
+
137
142
  return {
138
143
  cwd: worktreePath,
139
144
  workspace: {
@@ -148,6 +153,7 @@ export async function resolveSpawnWorkspace(input: WorkspaceResolutionInput): Pr
148
153
  baseSha,
149
154
  status: "active",
150
155
  deps,
156
+ ...(symlinks.linked.length || symlinks.errors ? { symlinks } : {}),
151
157
  probe,
152
158
  },
153
159
  };
@@ -226,6 +232,74 @@ function symlinkNodeModules(repoRoot: string, worktreePath: string): string[] {
226
232
  return linked;
227
233
  }
228
234
 
235
+ const GLOB_META = /[*?[\]{}!()]/;
236
+
237
+ /**
238
+ * Resolve one config entry to the relative paths it covers within `repoRoot`.
239
+ * Plain entries are taken literally (match files AND dirs via existsSync); entries
240
+ * with glob metacharacters are expanded against main with dotfiles included. Anything
241
+ * that resolves outside the repo (absolute / `..` traversal) or doesn't exist is dropped.
242
+ */
243
+ function resolveSymlinkPattern(repoRoot: string, pattern: string): string[] {
244
+ const within = (rel: string): boolean => {
245
+ const abs = resolve(repoRoot, rel);
246
+ const back = relative(repoRoot, abs);
247
+ return back !== "" && !back.startsWith("..") && !isAbsolute(back);
248
+ };
249
+
250
+ if (GLOB_META.test(pattern)) {
251
+ const matches: string[] = [];
252
+ for (const rel of new Bun.Glob(pattern).scanSync({ cwd: repoRoot, dot: true, onlyFiles: false })) {
253
+ if (within(rel)) matches.push(rel);
254
+ }
255
+ return matches;
256
+ }
257
+
258
+ if (!within(pattern)) return [];
259
+ return existsSync(join(repoRoot, pattern)) ? [pattern] : [];
260
+ }
261
+
262
+ /**
263
+ * Symlink configured untracked files/dirs from the main checkout into a fresh isolated
264
+ * worktree. Only links paths that exist in main and aren't already present in the worktree
265
+ * (git-tracked files are left untouched). Best-effort: a failed link never blocks the spawn.
266
+ */
267
+ export function provisionWorkspaceSymlinks(
268
+ repoRoot: string,
269
+ worktreePath: string,
270
+ patterns: string[],
271
+ ): WorkspaceSymlinkProvision {
272
+ const linked: string[] = [];
273
+ const seen = new Set<string>();
274
+ const errors: string[] = [];
275
+
276
+ for (const pattern of patterns) {
277
+ let rels: string[];
278
+ try {
279
+ rels = resolveSymlinkPattern(repoRoot, pattern);
280
+ } catch (error) {
281
+ errors.push(`${pattern}: ${error instanceof Error ? error.message : String(error)}`);
282
+ continue;
283
+ }
284
+ for (const rel of rels) {
285
+ if (seen.has(rel)) continue;
286
+ seen.add(rel);
287
+ const source = join(repoRoot, rel);
288
+ const target = join(worktreePath, rel);
289
+ if (pathExists(target)) continue; // git-tracked or already linked — don't clobber
290
+ try {
291
+ mkdirSync(dirname(target), { recursive: true });
292
+ symlinkSync(source, target, lstatSync(source).isDirectory() ? "dir" : "file");
293
+ linked.push(rel);
294
+ } catch (error) {
295
+ errors.push(`${rel}: ${error instanceof Error ? error.message : String(error)}`);
296
+ }
297
+ }
298
+ }
299
+
300
+ return errors.length ? { linked, errors } : { linked };
301
+ }
302
+
229
303
  /** Run a package install in each dir that owns a lockfile. Root install covers
230
304
  * the package manager's workspace members; standalone sub-projects with their
231
305
  * own lockfile (e.g. dashboard) get a separate install. */