agent-relay-orchestrator 0.127.8 → 0.127.9
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.127.
|
|
3
|
+
"version": "0.127.9",
|
|
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.4",
|
|
20
|
-
"agent-relay-sdk": "0.2.
|
|
20
|
+
"agent-relay-sdk": "0.2.122",
|
|
21
21
|
"callmux": "0.23.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { errMessage } from "agent-relay-sdk";
|
|
5
|
+
import type { LandGate } from "agent-relay-sdk";
|
|
5
6
|
import { git } from "../git";
|
|
6
7
|
import { runLandGates, type LandGatesResult } from "./land-gates-runner";
|
|
7
8
|
import { mergePhaseTimeoutMs, withMergePhaseTimeout } from "./merge-timeouts";
|
|
8
9
|
|
|
9
10
|
const LAND_COMMITTER = { name: "Agent Relay", email: "agent-relay@noreply" } as const;
|
|
11
|
+
const FULL_CI_LAND_GATE: LandGate = {
|
|
12
|
+
name: "ci-land-full",
|
|
13
|
+
command: "AGENT_RELAY_CI_LAND_FULL=1 bun run ci:land",
|
|
14
|
+
timeoutMs: 30 * 60 * 1000,
|
|
15
|
+
};
|
|
16
|
+
const DEPENDENCY_LINK_PATHS = [
|
|
17
|
+
"node_modules",
|
|
18
|
+
"dashboard/node_modules",
|
|
19
|
+
"sdk/node_modules",
|
|
20
|
+
"runner/node_modules",
|
|
21
|
+
"orchestrator/node_modules",
|
|
22
|
+
"codex/node_modules",
|
|
23
|
+
"claude/node_modules",
|
|
24
|
+
"client/node_modules",
|
|
25
|
+
] as const;
|
|
10
26
|
|
|
11
27
|
// Synthesize, but do not advance any ref to, the no-ff merge commit of branchSha
|
|
12
28
|
// into baseSha. The resulting commit/tree is the exact integrated tree that will
|
|
@@ -32,12 +48,40 @@ export async function synthesizeNoFfMerge(
|
|
|
32
48
|
return { ok: true, mergeSha: commit.stdout };
|
|
33
49
|
}
|
|
34
50
|
|
|
51
|
+
async function baseDeclaresCiLand(repoRoot: string, baseSha: string, signal?: AbortSignal): Promise<boolean> {
|
|
52
|
+
const pkg = await git(["show", `${baseSha}:package.json`], repoRoot, { timeoutMs: mergePhaseTimeoutMs("gates"), timeoutLabel: "workspace merge inspect base package scripts", signal });
|
|
53
|
+
if (!pkg.ok || !pkg.stdout) return false;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(pkg.stdout) as { scripts?: Record<string, unknown> };
|
|
56
|
+
return typeof parsed.scripts?.["ci:land"] === "string";
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function linkDependencyDirs(sourceWorktree: string, gateWorktree: string): void {
|
|
63
|
+
for (const rel of DEPENDENCY_LINK_PATHS) {
|
|
64
|
+
const from = join(sourceWorktree, rel);
|
|
65
|
+
const to = join(gateWorktree, rel);
|
|
66
|
+
if (!existsSync(from) || existsSync(to)) continue;
|
|
67
|
+
try {
|
|
68
|
+
const stat = lstatSync(from);
|
|
69
|
+
if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
|
|
70
|
+
mkdirSync(join(to, ".."), { recursive: true });
|
|
71
|
+
symlinkSync(realpathSync(from), to, "dir");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(`[orchestrator] land-gate dependency link skipped ${rel}: ${errMessage(err)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
35
78
|
/**
|
|
36
79
|
* Run land gates against the exact tree that would land.
|
|
37
80
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
81
|
+
* Always materializes that tree in a detached checkout. Even a fast-forwardable
|
|
82
|
+
* branch is gated outside the worker worktree so worker-only topology, untracked
|
|
83
|
+
* files, and symlink layout cannot make the boundary check pass differently than
|
|
84
|
+
* the tree that will advance the base ref.
|
|
41
85
|
*/
|
|
42
86
|
export async function runLandGatesOnIntegratedTree(
|
|
43
87
|
repoRoot: string,
|
|
@@ -49,30 +93,32 @@ export async function runLandGatesOnIntegratedTree(
|
|
|
49
93
|
signal?: AbortSignal,
|
|
50
94
|
): Promise<{ gates: LandGatesResult } | { abort: { conflict?: boolean; error: string } }> {
|
|
51
95
|
console.error("[orchestrator] workspace.merge gate-start worktree=" + worktreePath + " behind=" + behind);
|
|
52
|
-
if (behind === 0) {
|
|
53
|
-
return { gates: await withMergePhaseTimeout("gates", () => runLandGates(worktreePath), { signal }) };
|
|
54
|
-
}
|
|
55
96
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
const requiredGates: LandGate[] = await baseDeclaresCiLand(repoRoot, integrationBaseSha, signal) ? [FULL_CI_LAND_GATE] : [];
|
|
98
|
+
let gateRef = headSha;
|
|
99
|
+
if (behind > 0) {
|
|
100
|
+
let synth: Awaited<ReturnType<typeof synthesizeNoFfMerge>>;
|
|
101
|
+
try {
|
|
102
|
+
synth = await withMergePhaseTimeout(
|
|
103
|
+
"synthesize",
|
|
104
|
+
(phaseSignal) => synthesizeNoFfMerge(repoRoot, integrationBaseSha, headSha, mergeMessage, mergePhaseTimeoutMs("synthesize"), phaseSignal),
|
|
105
|
+
{ signal },
|
|
106
|
+
);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return { abort: { error: errMessage(err) } };
|
|
109
|
+
}
|
|
110
|
+
if (!synth.ok) return { abort: { conflict: synth.conflict, error: synth.error } };
|
|
111
|
+
gateRef = synth.mergeSha;
|
|
65
112
|
}
|
|
66
|
-
if (!synth.ok) return { abort: { conflict: synth.conflict, error: synth.error } };
|
|
67
113
|
|
|
68
114
|
const tmpParent = mkdtempSync(join(tmpdir(), "agent-relay-landgate-"));
|
|
69
|
-
const tmpWorktree = join(tmpParent, "
|
|
115
|
+
const tmpWorktree = join(tmpParent, "checkout");
|
|
70
116
|
let add: Awaited<ReturnType<typeof git>>;
|
|
71
117
|
try {
|
|
72
118
|
add = await withMergePhaseTimeout(
|
|
73
119
|
"worktree-add",
|
|
74
120
|
(phaseSignal) => git(
|
|
75
|
-
["worktree", "add", "--detach", tmpWorktree,
|
|
121
|
+
["worktree", "add", "--detach", tmpWorktree, gateRef],
|
|
76
122
|
repoRoot,
|
|
77
123
|
{ timeoutMs: mergePhaseTimeoutMs("worktree-add"), timeoutLabel: "workspace merge land-gate worktree add", signal: phaseSignal },
|
|
78
124
|
),
|
|
@@ -88,7 +134,8 @@ export async function runLandGatesOnIntegratedTree(
|
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
try {
|
|
91
|
-
|
|
137
|
+
linkDependencyDirs(worktreePath, tmpWorktree);
|
|
138
|
+
return { gates: await withMergePhaseTimeout("gates", () => runLandGates(tmpWorktree, requiredGates), { signal }) };
|
|
92
139
|
} finally {
|
|
93
140
|
try {
|
|
94
141
|
await withMergePhaseTimeout(
|
|
@@ -3,15 +3,13 @@ import { errMessage } from "agent-relay-sdk";
|
|
|
3
3
|
import type { LandGate, LandGateRunResult } from "agent-relay-sdk";
|
|
4
4
|
import { loadRepoLandGates } from "agent-relay-sdk/land-gates";
|
|
5
5
|
|
|
6
|
-
// #902 — execute
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* are meant to be a FAST high-signal subset, not the full suite — keep the ceiling
|
|
14
|
-
* generous but bounded so a hung gate can't wedge the per-repo merge lease forever. */
|
|
6
|
+
// #902/#1199 — execute mandatory boundary gates and any repo-configured land gates
|
|
7
|
+
// against the candidate merged tree BEFORE the ref advance in `mergeRebaseFf`.
|
|
8
|
+
// A required gate failing blocks the land (ref NOT advanced); optional gates only warn.
|
|
9
|
+
|
|
10
|
+
/** Per-gate default timeout when the gate didn't declare `timeoutMs`. Keep the
|
|
11
|
+
* ceiling generous but bounded so a hung gate can't wedge the per-repo merge
|
|
12
|
+
* lease forever. Full-suite boundary gates should declare their own timeout. */
|
|
15
13
|
const DEFAULT_GATE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
16
14
|
const TIMEOUT_KILL_GRACE_MS = 1_000;
|
|
17
15
|
const OUTPUT_CANCEL_GRACE_MS = 50;
|
|
@@ -189,21 +187,20 @@ export interface LandGatesResult {
|
|
|
189
187
|
}
|
|
190
188
|
|
|
191
189
|
/**
|
|
192
|
-
* Load + run the repo's configured land gates against `worktreePath`.
|
|
193
|
-
* `ran: 0` with no failure/warnings when the
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* land); optional failures accumulate as warnings and never block.
|
|
190
|
+
* Load + run required boundary gates plus the repo's configured land gates against `worktreePath`.
|
|
191
|
+
* Returns `ran: 0` with no failure/warnings only when neither the boundary nor the repo
|
|
192
|
+
* declares gates. Stops at the first failing REQUIRED gate (it blocks the land);
|
|
193
|
+
* optional failures accumulate as warnings and never block.
|
|
197
194
|
*
|
|
198
195
|
* A PRESENT-but-malformed `.agent-relay/land-gates.json` is itself a blocking failure
|
|
199
196
|
* (surfaced as a synthetic required gate) rather than an unhandled throw that would
|
|
200
197
|
* crash the merge command — the worker fixes the config and re-lands.
|
|
201
198
|
*/
|
|
202
|
-
export async function runLandGates(worktreePath: string): Promise<LandGatesResult> {
|
|
199
|
+
export async function runLandGates(worktreePath: string, requiredGates: LandGate[] = []): Promise<LandGatesResult> {
|
|
203
200
|
const warnings: LandGateRunResult[] = [];
|
|
204
201
|
let gates: LandGate[];
|
|
205
202
|
try {
|
|
206
|
-
gates = loadRepoLandGates(worktreePath);
|
|
203
|
+
gates = [...requiredGates, ...loadRepoLandGates(worktreePath)];
|
|
207
204
|
} catch (err) {
|
|
208
205
|
const output = `invalid .agent-relay/land-gates.json: ${errMessage(err)}`;
|
|
209
206
|
return {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export type MergePhase = "total" | "prep" | "preview" | "fetch" | "rebase" | "gates" | "synthesize" | "worktree-add" | "cleanup";
|
|
2
2
|
|
|
3
3
|
const MERGE_PHASE_TIMEOUTS_MS: Record<MergePhase, number> = {
|
|
4
|
-
total:
|
|
4
|
+
total: 40 * 60_000,
|
|
5
5
|
prep: 60_000,
|
|
6
6
|
preview: 30_000,
|
|
7
7
|
fetch: 60_000,
|
|
8
8
|
rebase: 60_000,
|
|
9
|
-
gates:
|
|
9
|
+
gates: 35 * 60_000,
|
|
10
10
|
synthesize: 60_000,
|
|
11
11
|
"worktree-add": 60_000,
|
|
12
12
|
cleanup: 30_000,
|