agent-relay-orchestrator 0.15.1 → 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 +2 -2
- package/src/control.ts +2 -0
- package/src/spawn.ts +3 -0
- package/src/workspace-probe.ts +76 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-orchestrator",
|
|
3
|
-
"version": "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.
|
|
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 };
|
package/src/workspace-probe.ts
CHANGED
|
@@ -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. */
|