agent-relay-orchestrator 0.120.2 → 0.120.4
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.120.
|
|
3
|
+
"version": "0.120.4",
|
|
4
4
|
"description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"agent-relay-providers": "0.104.3",
|
|
20
|
-
"agent-relay-sdk": "0.2.
|
|
20
|
+
"agent-relay-sdk": "0.2.109",
|
|
21
21
|
"callmux": "0.23.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
package/src/spawn/acquisition.ts
CHANGED
|
@@ -6,13 +6,19 @@ import { git, requireGit } from "../git";
|
|
|
6
6
|
import { execProcess } from "../process";
|
|
7
7
|
import type { ProjectAcquisitionManifest } from "./types";
|
|
8
8
|
|
|
9
|
+
export type ProjectRootSyncAction = "synced" | "noop" | "skipped-dirty" | "skipped-diverged";
|
|
10
|
+
|
|
9
11
|
export interface ProjectAcquisitionResult {
|
|
10
12
|
applied: boolean;
|
|
11
13
|
action: "cloned" | "synced" | "noop";
|
|
14
|
+
syncAction: ProjectRootSyncAction;
|
|
12
15
|
rootPath: string;
|
|
13
16
|
remoteUrl: string;
|
|
14
17
|
ref?: string;
|
|
15
18
|
headSha?: string;
|
|
19
|
+
baseRef?: string;
|
|
20
|
+
baseSha?: string;
|
|
21
|
+
syncTarget?: string;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
const inFlight = new Map<string, Promise<ProjectAcquisitionResult>>();
|
|
@@ -68,15 +74,19 @@ async function applyManifestLocked(manifest: ProjectAcquisitionManifest, baseDir
|
|
|
68
74
|
await cloneRoot(manifest, baseDir);
|
|
69
75
|
action = "cloned";
|
|
70
76
|
}
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
77
|
+
const sync = await syncRoot(manifest);
|
|
78
|
+
if (sync.action === "synced" && action !== "cloned") action = "synced";
|
|
73
79
|
return {
|
|
74
80
|
applied: true,
|
|
75
81
|
action,
|
|
82
|
+
syncAction: sync.action,
|
|
76
83
|
rootPath,
|
|
77
84
|
remoteUrl,
|
|
78
85
|
...(manifest.ref ? { ref: manifest.ref } : {}),
|
|
79
86
|
headSha: (await git(["rev-parse", "HEAD"], rootPath)).stdout || undefined,
|
|
87
|
+
...(sync.baseRef ? { baseRef: sync.baseRef } : {}),
|
|
88
|
+
...(sync.baseSha ? { baseSha: sync.baseSha } : {}),
|
|
89
|
+
...(sync.target ? { syncTarget: sync.target } : {}),
|
|
80
90
|
};
|
|
81
91
|
}
|
|
82
92
|
|
|
@@ -114,26 +124,48 @@ async function cloneRoot(manifest: ProjectAcquisitionManifest, baseDir: string):
|
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
|
|
127
|
+
interface SyncTarget {
|
|
128
|
+
target?: string;
|
|
129
|
+
branch?: string;
|
|
130
|
+
baseRef?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface RootSyncResult extends SyncTarget {
|
|
134
|
+
action: ProjectRootSyncAction;
|
|
135
|
+
baseSha?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function syncRoot(manifest: ProjectAcquisitionManifest): Promise<RootSyncResult> {
|
|
118
139
|
const rootPath = resolve(manifest.rootPath);
|
|
119
140
|
await assertExistingGitRoot(rootPath);
|
|
120
141
|
await assertRemote(rootPath, manifest.remoteUrl.trim());
|
|
121
|
-
const status = await git(["status", "--porcelain"], rootPath);
|
|
122
|
-
if (!status.ok) throw new Error(`git status failed for ${rootPath}: ${status.stderr}`);
|
|
123
|
-
if (status.stdout.trim()) throw new Error(`project root ${rootPath} has local changes; refusing ff-only sync before spawn`);
|
|
124
142
|
const fetch = await git(["fetch", "--prune", "origin"], rootPath);
|
|
125
143
|
if (!fetch.ok) throw new Error(`git fetch failed for ${rootPath}: ${fetch.stderr || fetch.stdout}`);
|
|
126
|
-
const target = await
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
const target = await resolveSyncTarget(rootPath, manifest.ref);
|
|
145
|
+
const baseSha = target.target ? await requireGit(["rev-parse", target.target], rootPath) : undefined;
|
|
146
|
+
const status = await git(["status", "--porcelain"], rootPath);
|
|
147
|
+
if (!status.ok) throw new Error(`git status failed for ${rootPath}: ${status.stderr}`);
|
|
148
|
+
if (status.stdout.trim()) {
|
|
149
|
+
warnSyncSkipped(rootPath, target.target, "skipped-dirty", "local changes");
|
|
150
|
+
return { ...target, action: "skipped-dirty", ...(baseSha ? { baseSha } : {}) };
|
|
151
|
+
}
|
|
152
|
+
if (!target.target) return { ...target, action: "noop", ...(baseSha ? { baseSha } : {}) };
|
|
153
|
+
|
|
154
|
+
const localRef = target.branch && await localBranchExists(rootPath, target.branch) ? `refs/heads/${target.branch}` : "HEAD";
|
|
155
|
+
const head = await requireGit(["rev-parse", localRef], rootPath);
|
|
156
|
+
if (!baseSha || head === baseSha) return { ...target, action: "noop", ...(baseSha ? { baseSha } : {}) };
|
|
157
|
+
if (!(await git(["merge-base", "--is-ancestor", localRef, target.target], rootPath)).ok) {
|
|
158
|
+
warnSyncSkipped(rootPath, target.target, "skipped-diverged", `${localRef} is ahead of or diverged from target`);
|
|
159
|
+
return { ...target, action: "skipped-diverged", baseSha };
|
|
133
160
|
}
|
|
134
|
-
|
|
161
|
+
|
|
162
|
+
if (target.branch) {
|
|
163
|
+
const checked = await checkoutBranchForSync(rootPath, target.branch, target.target);
|
|
164
|
+
if (!checked.ok) throw new Error(`git checkout ${target.branch} failed for ${rootPath}: ${checked.stderr || checked.stdout}`);
|
|
165
|
+
}
|
|
166
|
+
const merged = await git(["merge", "--ff-only", target.target], rootPath);
|
|
135
167
|
if (!merged.ok) throw new Error(`git ff-only sync failed for ${rootPath}: ${merged.stderr || merged.stdout}`);
|
|
136
|
-
return "synced";
|
|
168
|
+
return { ...target, action: "synced", baseSha };
|
|
137
169
|
}
|
|
138
170
|
|
|
139
171
|
async function assertExistingGitRoot(rootPath: string): Promise<void> {
|
|
@@ -156,34 +188,44 @@ async function assertRemote(rootPath: string, remoteUrl: string): Promise<void>
|
|
|
156
188
|
}
|
|
157
189
|
}
|
|
158
190
|
|
|
159
|
-
async function
|
|
191
|
+
async function resolveSyncTarget(rootPath: string, ref: string | undefined): Promise<SyncTarget> {
|
|
160
192
|
if (ref?.trim()) {
|
|
161
193
|
const name = ref.trim();
|
|
162
194
|
const remoteBranch = `refs/remotes/origin/${name}`;
|
|
163
195
|
if ((await git(["show-ref", "--verify", "--quiet", remoteBranch], rootPath)).ok) {
|
|
164
|
-
|
|
165
|
-
const checked = await git(["checkout", name], rootPath);
|
|
166
|
-
if (!checked.ok) throw new Error(`git checkout ${name} failed for ${rootPath}: ${checked.stderr || checked.stdout}`);
|
|
167
|
-
} else {
|
|
168
|
-
const checked = await git(["checkout", "-B", name, `origin/${name}`], rootPath);
|
|
169
|
-
if (!checked.ok) throw new Error(`git checkout ${name} failed for ${rootPath}: ${checked.stderr || checked.stdout}`);
|
|
170
|
-
}
|
|
171
|
-
return `origin/${name}`;
|
|
196
|
+
return { target: `origin/${name}`, branch: name, baseRef: name };
|
|
172
197
|
}
|
|
173
198
|
if ((await git(["rev-parse", "--verify", name], rootPath)).ok) {
|
|
174
|
-
|
|
175
|
-
if (!checked.ok) throw new Error(`git checkout ${name} failed for ${rootPath}: ${checked.stderr || checked.stdout}`);
|
|
176
|
-
return undefined;
|
|
199
|
+
return {};
|
|
177
200
|
}
|
|
178
201
|
throw new Error(`project acquisition ref "${name}" not found on origin for ${rootPath}`);
|
|
179
202
|
}
|
|
180
203
|
const upstream = await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], rootPath);
|
|
181
|
-
if (upstream.ok && upstream.stdout) return upstream.stdout;
|
|
204
|
+
if (upstream.ok && upstream.stdout) return { target: upstream.stdout, baseRef: remoteBranchName(upstream.stdout) };
|
|
182
205
|
const originHead = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], rootPath);
|
|
183
|
-
if (originHead.ok && originHead.stdout) return originHead.stdout;
|
|
206
|
+
if (originHead.ok && originHead.stdout) return { target: originHead.stdout, baseRef: remoteBranchName(originHead.stdout) };
|
|
184
207
|
throw new Error(`project root ${rootPath} has no upstream or origin/HEAD for ff-only sync`);
|
|
185
208
|
}
|
|
186
209
|
|
|
210
|
+
async function localBranchExists(rootPath: string, branch: string): Promise<boolean> {
|
|
211
|
+
return (await git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], rootPath)).ok;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function checkoutBranchForSync(rootPath: string, branch: string, target: string): Promise<{ ok: boolean; stdout: string; stderr: string }> {
|
|
215
|
+
if (await localBranchExists(rootPath, branch)) return await git(["checkout", branch], rootPath);
|
|
216
|
+
return await git(["checkout", "-B", branch, target], rootPath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function remoteBranchName(ref: string | undefined): string | undefined {
|
|
220
|
+
const trimmed = ref?.trim();
|
|
221
|
+
if (!trimmed) return undefined;
|
|
222
|
+
return trimmed.startsWith("origin/") ? trimmed.slice("origin/".length) : trimmed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function warnSyncSkipped(rootPath: string, target: string | undefined, action: ProjectRootSyncAction, reason: string): void {
|
|
226
|
+
console.warn(`[orchestrator] project acquisition root sync ${action} for ${rootPath}${target ? ` -> ${target}` : ""}: ${reason}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
187
229
|
async function runGit(args: string[], cwd: string): Promise<{ ok: boolean; stdout: string; stderr: string }> {
|
|
188
230
|
return await execProcess(["git", ...args], { cwd });
|
|
189
231
|
}
|
package/src/spawn/spawn-agent.ts
CHANGED
|
@@ -52,7 +52,7 @@ export async function spawnAgent(
|
|
|
52
52
|
if (!isWithinBaseDir(opts.cwd, config.baseDir)) {
|
|
53
53
|
throw new Error(`cwd must be within base directory: ${config.baseDir}`);
|
|
54
54
|
}
|
|
55
|
-
await d.applyProjectAcquisitionManifest(opts.acquisition, config.baseDir);
|
|
55
|
+
const acquisition = await d.applyProjectAcquisitionManifest(opts.acquisition, config.baseDir);
|
|
56
56
|
if (!existsSync(opts.cwd)) {
|
|
57
57
|
throw new Error(`cwd does not exist: ${opts.cwd}`);
|
|
58
58
|
}
|
|
@@ -63,6 +63,7 @@ export async function spawnAgent(
|
|
|
63
63
|
...opts,
|
|
64
64
|
label,
|
|
65
65
|
workspaceSymlinks: opts.workspaceSymlinks,
|
|
66
|
+
...(acquisition?.baseRef && acquisition.baseSha ? { baseRef: acquisition.baseRef, baseSha: acquisition.baseSha } : {}),
|
|
66
67
|
workspaceRoot: workspacesRoot(config.baseDir),
|
|
67
68
|
});
|
|
68
69
|
if (resolvedWorkspace.reusedExisting) {
|
|
@@ -139,8 +139,8 @@ export async function resolveSpawnWorkspace(input: WorkspaceResolutionInput): Pr
|
|
|
139
139
|
|
|
140
140
|
const id = workspaceId(input);
|
|
141
141
|
const repoRoot = probe.repoRoot;
|
|
142
|
-
const baseRef = await terminalBaseRef(repoRoot, probe.branch);
|
|
143
|
-
const baseSha = probe.headSha ?? await requireGit(["rev-parse", "HEAD"], repoRoot);
|
|
142
|
+
const baseRef = input.baseRef ?? await terminalBaseRef(repoRoot, probe.branch);
|
|
143
|
+
const baseSha = input.baseSha ?? probe.headSha ?? await requireGit(["rev-parse", "HEAD"], repoRoot);
|
|
144
144
|
const workspaceRoot = input.workspaceRoot ? resolve(input.workspaceRoot) : workspacesRoot(homedir());
|
|
145
145
|
const worktreePath = join(workspaceRoot, repoSlug(repoRoot), id);
|
|
146
146
|
const existing = await existingRegisteredWorktree(repoRoot, worktreePath);
|
|
@@ -24,6 +24,9 @@ export interface WorkspaceResolutionInput {
|
|
|
24
24
|
workspaceMode?: WorkspaceMode;
|
|
25
25
|
workspaceRoot?: string;
|
|
26
26
|
workspaceSymlinks?: string[];
|
|
27
|
+
/** Preferred base for a fresh isolated worktree, e.g. fetched origin/main after project acquisition. */
|
|
28
|
+
baseRef?: string;
|
|
29
|
+
baseSha?: string;
|
|
27
30
|
automationId?: string;
|
|
28
31
|
automationRunId?: string;
|
|
29
32
|
/** #635 — attach to or branch off an existing worktree instead of creating a fresh one. */
|