agent-relay-orchestrator 0.127.9 → 0.127.10
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 +3 -0
- package/src/workspace-probe/integrated-land-gates.ts +47 -15
- package/src/workspace-probe/land-gates-runner.ts +7 -6
- package/src/workspace-probe/merge-timeouts.ts +5 -4
- package/src/workspace-probe/merge.ts +39 -23
- package/src/workspace-probe/types.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-orchestrator",
|
|
3
|
-
"version": "0.127.
|
|
3
|
+
"version": "0.127.10",
|
|
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.123",
|
|
21
21
|
"callmux": "0.23.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
package/src/control.ts
CHANGED
|
@@ -433,6 +433,9 @@ export function createControlHandler(
|
|
|
433
433
|
prTitle: typeof command.params.prTitle === "string" ? command.params.prTitle : undefined,
|
|
434
434
|
prBody: typeof command.params.prBody === "string" ? command.params.prBody : undefined,
|
|
435
435
|
autoMerge: command.params.autoMerge === "on-green" || command.params.autoMerge === "on-approval" || command.params.autoMerge === "manual" ? command.params.autoMerge : undefined,
|
|
436
|
+
gateLevel: command.params.gateLevel === "subset" || command.params.gateLevel === "none" ? command.params.gateLevel : undefined,
|
|
437
|
+
gateReason: typeof command.params.gateReason === "string" ? command.params.gateReason : undefined,
|
|
438
|
+
gateRequestedBy: typeof command.params.gateRequestedBy === "string" ? command.params.gateRequestedBy : undefined,
|
|
436
439
|
prLanded: isRecord(command.params.prLanded)
|
|
437
440
|
? {
|
|
438
441
|
sha: typeof command.params.prLanded.sha === "string" ? command.params.prLanded.sha : undefined,
|
|
@@ -3,16 +3,14 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { errMessage } from "agent-relay-sdk";
|
|
5
5
|
import type { LandGate } from "agent-relay-sdk";
|
|
6
|
+
import type { WorkspaceLandGateLevel } from "agent-relay-sdk";
|
|
7
|
+
import { cleanLandGatesConfig, landGatesTimeoutBudgetMs, loadRepoLandGates } from "agent-relay-sdk/land-gates";
|
|
6
8
|
import { git } from "../git";
|
|
7
9
|
import { runLandGates, type LandGatesResult } from "./land-gates-runner";
|
|
8
10
|
import { mergePhaseTimeoutMs, withMergePhaseTimeout } from "./merge-timeouts";
|
|
9
11
|
|
|
10
12
|
const LAND_COMMITTER = { name: "Agent Relay", email: "agent-relay@noreply" } as const;
|
|
11
|
-
const
|
|
12
|
-
name: "ci-land-full",
|
|
13
|
-
command: "AGENT_RELAY_CI_LAND_FULL=1 bun run ci:land",
|
|
14
|
-
timeoutMs: 30 * 60 * 1000,
|
|
15
|
-
};
|
|
13
|
+
const LAND_GATES_FILE = ".agent-relay/land-gates.json";
|
|
16
14
|
const DEPENDENCY_LINK_PATHS = [
|
|
17
15
|
"node_modules",
|
|
18
16
|
"dashboard/node_modules",
|
|
@@ -48,17 +46,23 @@ export async function synthesizeNoFfMerge(
|
|
|
48
46
|
return { ok: true, mergeSha: commit.stdout };
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
async function
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
49
|
+
async function resolveRequiredLandGates(repoRoot: string, baseSha: string, signal?: AbortSignal): Promise<LandGate[]> {
|
|
50
|
+
const config = await git(["show", `${baseSha}:${LAND_GATES_FILE}`], repoRoot, { timeoutMs: mergePhaseTimeoutMs("gates"), timeoutLabel: "workspace merge inspect base land-gates config", signal });
|
|
51
|
+
if (!config.ok || !config.stdout) return [];
|
|
54
52
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return false;
|
|
53
|
+
return cleanLandGatesConfig(JSON.parse(config.stdout), repoRoot);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`invalid ${LAND_GATES_FILE} at ${baseSha}: ${errMessage(err)}`);
|
|
59
56
|
}
|
|
60
57
|
}
|
|
61
58
|
|
|
59
|
+
function requiredGatesForLevel(requiredGates: LandGate[], gateLevel: WorkspaceLandGateLevel): LandGate[] {
|
|
60
|
+
if (gateLevel === "full") return requiredGates;
|
|
61
|
+
// subset = candidate-tree repo gates only. The base-required list is omitted;
|
|
62
|
+
// runLandGates still reads the candidate checkout config below.
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
62
66
|
function linkDependencyDirs(sourceWorktree: string, gateWorktree: string): void {
|
|
63
67
|
for (const rel of DEPENDENCY_LINK_PATHS) {
|
|
64
68
|
const from = join(sourceWorktree, rel);
|
|
@@ -90,11 +94,24 @@ export async function runLandGatesOnIntegratedTree(
|
|
|
90
94
|
integrationBaseSha: string,
|
|
91
95
|
headSha: string,
|
|
92
96
|
mergeMessage: string,
|
|
97
|
+
gateLevel: WorkspaceLandGateLevel = "full",
|
|
93
98
|
signal?: AbortSignal,
|
|
94
99
|
): Promise<{ gates: LandGatesResult } | { abort: { conflict?: boolean; error: string } }> {
|
|
95
|
-
console.error("[orchestrator] workspace.merge gate-start worktree=" + worktreePath + " behind=" + behind);
|
|
100
|
+
console.error("[orchestrator] workspace.merge gate-start worktree=" + worktreePath + " behind=" + behind + " level=" + gateLevel);
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
if (gateLevel === "none") {
|
|
103
|
+
console.error("[orchestrator] workspace.merge gate-skip level=none worktree=" + worktreePath);
|
|
104
|
+
return { gates: { ran: 0, warnings: [] } };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let baseRequiredGates: LandGate[];
|
|
108
|
+
try {
|
|
109
|
+
baseRequiredGates = await resolveRequiredLandGates(repoRoot, integrationBaseSha, signal);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return { abort: { error: errMessage(err) } };
|
|
112
|
+
}
|
|
113
|
+
const requiredGates = requiredGatesForLevel(baseRequiredGates, gateLevel);
|
|
114
|
+
const loadCandidateRepoGates = gateLevel === "subset";
|
|
98
115
|
let gateRef = headSha;
|
|
99
116
|
if (behind > 0) {
|
|
100
117
|
let synth: Awaited<ReturnType<typeof synthesizeNoFfMerge>>;
|
|
@@ -135,7 +152,22 @@ export async function runLandGatesOnIntegratedTree(
|
|
|
135
152
|
|
|
136
153
|
try {
|
|
137
154
|
linkDependencyDirs(worktreePath, tmpWorktree);
|
|
138
|
-
|
|
155
|
+
let candidateRepoGates: LandGate[] | undefined;
|
|
156
|
+
if (loadCandidateRepoGates) {
|
|
157
|
+
try {
|
|
158
|
+
candidateRepoGates = loadRepoLandGates(tmpWorktree);
|
|
159
|
+
} catch {
|
|
160
|
+
candidateRepoGates = undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const gateTimeoutBudgetMs = landGatesTimeoutBudgetMs([...requiredGates, ...(candidateRepoGates ?? [])]);
|
|
164
|
+
return {
|
|
165
|
+
gates: await withMergePhaseTimeout(
|
|
166
|
+
"gates",
|
|
167
|
+
() => runLandGates(tmpWorktree, requiredGates, { loadRepoGates: loadCandidateRepoGates, repoGates: candidateRepoGates }),
|
|
168
|
+
{ signal, minimumMs: gateTimeoutBudgetMs },
|
|
169
|
+
),
|
|
170
|
+
};
|
|
139
171
|
} finally {
|
|
140
172
|
try {
|
|
141
173
|
await withMergePhaseTimeout(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { errMessage } from "agent-relay-sdk";
|
|
3
3
|
import type { LandGate, LandGateRunResult } from "agent-relay-sdk";
|
|
4
|
-
import { loadRepoLandGates } from "agent-relay-sdk/land-gates";
|
|
4
|
+
import { DEFAULT_LAND_GATE_TIMEOUT_MS, loadRepoLandGates } from "agent-relay-sdk/land-gates";
|
|
5
5
|
|
|
6
6
|
// #902/#1199 — execute mandatory boundary gates and any repo-configured land gates
|
|
7
7
|
// against the candidate merged tree BEFORE the ref advance in `mergeRebaseFf`.
|
|
@@ -10,7 +10,7 @@ import { loadRepoLandGates } from "agent-relay-sdk/land-gates";
|
|
|
10
10
|
/** Per-gate default timeout when the gate didn't declare `timeoutMs`. Keep the
|
|
11
11
|
* ceiling generous but bounded so a hung gate can't wedge the per-repo merge
|
|
12
12
|
* lease forever. Full-suite boundary gates should declare their own timeout. */
|
|
13
|
-
const DEFAULT_GATE_TIMEOUT_MS =
|
|
13
|
+
const DEFAULT_GATE_TIMEOUT_MS = DEFAULT_LAND_GATE_TIMEOUT_MS;
|
|
14
14
|
const TIMEOUT_KILL_GRACE_MS = 1_000;
|
|
15
15
|
const OUTPUT_CANCEL_GRACE_MS = 50;
|
|
16
16
|
/** Cap on the full output streamed to the relay artifact (the notification only ever
|
|
@@ -187,8 +187,8 @@ export interface LandGatesResult {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
|
-
*
|
|
191
|
-
* Returns `ran: 0` with no failure/warnings only when neither
|
|
190
|
+
* Run required boundary gates plus, when requested, repo-configured land gates against
|
|
191
|
+
* `worktreePath`. Returns `ran: 0` with no failure/warnings only when neither set
|
|
192
192
|
* declares gates. Stops at the first failing REQUIRED gate (it blocks the land);
|
|
193
193
|
* optional failures accumulate as warnings and never block.
|
|
194
194
|
*
|
|
@@ -196,11 +196,12 @@ export interface LandGatesResult {
|
|
|
196
196
|
* (surfaced as a synthetic required gate) rather than an unhandled throw that would
|
|
197
197
|
* crash the merge command — the worker fixes the config and re-lands.
|
|
198
198
|
*/
|
|
199
|
-
export async function runLandGates(worktreePath: string, requiredGates: LandGate[] = []): Promise<LandGatesResult> {
|
|
199
|
+
export async function runLandGates(worktreePath: string, requiredGates: LandGate[] = [], options: { loadRepoGates?: boolean; repoGates?: LandGate[] } = {}): Promise<LandGatesResult> {
|
|
200
200
|
const warnings: LandGateRunResult[] = [];
|
|
201
201
|
let gates: LandGate[];
|
|
202
202
|
try {
|
|
203
|
-
|
|
203
|
+
const repoGates = options.repoGates ?? (options.loadRepoGates === false ? [] : loadRepoLandGates(worktreePath));
|
|
204
|
+
gates = [...requiredGates, ...repoGates];
|
|
204
205
|
} catch (err) {
|
|
205
206
|
const output = `invalid .agent-relay/land-gates.json: ${errMessage(err)}`;
|
|
206
207
|
return {
|
|
@@ -19,11 +19,12 @@ function positiveEnvMs(name: string): number | undefined {
|
|
|
19
19
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function mergePhaseTimeoutMs(phase: MergePhase): number {
|
|
22
|
+
export function mergePhaseTimeoutMs(phase: MergePhase, minimumMs = 0): number {
|
|
23
23
|
const key = `AGENT_RELAY_WORKSPACE_MERGE_${phase.toUpperCase().replace(/-/g, "_")}_TIMEOUT_MS`;
|
|
24
|
-
|
|
24
|
+
const configured = positiveEnvMs(key)
|
|
25
25
|
?? positiveEnvMs("AGENT_RELAY_WORKSPACE_MERGE_PHASE_TIMEOUT_MS")
|
|
26
26
|
?? MERGE_PHASE_TIMEOUTS_MS[phase];
|
|
27
|
+
return Math.max(configured, minimumMs);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function abortError(signal: AbortSignal): Error {
|
|
@@ -40,9 +41,9 @@ export function throwIfMergeAborted(signal?: AbortSignal): void {
|
|
|
40
41
|
export async function withMergePhaseTimeout<T>(
|
|
41
42
|
phase: MergePhase,
|
|
42
43
|
run: (signal: AbortSignal) => Promise<T>,
|
|
43
|
-
options: { signal?: AbortSignal } = {},
|
|
44
|
+
options: { signal?: AbortSignal; minimumMs?: number } = {},
|
|
44
45
|
): Promise<T> {
|
|
45
|
-
const timeoutMs = mergePhaseTimeoutMs(phase);
|
|
46
|
+
const timeoutMs = mergePhaseTimeoutMs(phase, options.minimumMs);
|
|
46
47
|
const controller = new AbortController();
|
|
47
48
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
48
49
|
const abortFromParent = () => controller.abort(abortError(options.signal!));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { errMessage, type BaseWorktreeSyncResult, type WorkspaceMergePreview, type WorkspaceMergeResult } from "agent-relay-sdk";
|
|
3
|
+
import { errMessage, type BaseWorktreeSyncResult, type WorkspaceLandGateLevel, type WorkspaceMergePreview, type WorkspaceMergeResult } from "agent-relay-sdk";
|
|
4
4
|
import { git, gitRaw } from "../git";
|
|
5
5
|
import { execProcess } from "../process";
|
|
6
6
|
import { deleteBranchIfSafe, owningRepoRoot } from "./cleanup";
|
|
@@ -825,6 +825,7 @@ async function mergeRebaseFf(
|
|
|
825
825
|
signal?: AbortSignal,
|
|
826
826
|
): Promise<WorkspaceMergeResult> {
|
|
827
827
|
throwIfMergeAborted(signal);
|
|
828
|
+
const gateLevel: WorkspaceLandGateLevel = input.gateLevel === "subset" || input.gateLevel === "none" ? input.gateLevel : "full";
|
|
828
829
|
const base = preview.baseRef;
|
|
829
830
|
if (!base) return head({ status: "review_requested", error: "no base branch to merge into" });
|
|
830
831
|
if (!branch) return head({ status: "review_requested", error: "cannot determine agent branch" });
|
|
@@ -833,16 +834,11 @@ async function mergeRebaseFf(
|
|
|
833
834
|
// upstream (e.g. main -> origin/main) and we'll push, fetch it and check whether
|
|
834
835
|
// origin has moved ahead of local base.
|
|
835
836
|
//
|
|
836
|
-
// Origin-ahead is
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
// FRESH origin (the no-ff merge below ties the branch onto the advanced base).
|
|
842
|
-
// This is a clean recovery — no steward escalation.
|
|
843
|
-
// - Refuse ONLY on genuine divergence: local base has commits not on origin, so
|
|
844
|
-
// a sync would rewrite/discard published-or-local history. A real failure must
|
|
845
|
-
// still set `error` so the relay's no-progress backoff (#638) can engage.
|
|
837
|
+
// Origin-ahead is common under concurrency: a sibling lane lands and advances
|
|
838
|
+
// origin/<base> while this lane still sits on a stale local base. Direct land
|
|
839
|
+
// must not silently synthesize a no-ff merge or PR fallback in that upstream-stale
|
|
840
|
+
// state (#1184). Local-only base advancement still uses the existing no-ff path:
|
|
841
|
+
// land gates, timeout cancellation, and abort recovery all exercise that machinery.
|
|
846
842
|
// Tracks whether the DIRTY base checkout caught up to the advanced HEAD across BOTH heal
|
|
847
843
|
// points (the upstream sync below and the final land). A mixed state from either is surfaced
|
|
848
844
|
// loudly rather than swallowed as a log warning (#824).
|
|
@@ -855,8 +851,8 @@ async function mergeRebaseFf(
|
|
|
855
851
|
|
|
856
852
|
// SHA preservation (#287): never rebase the agent branch — rewriting its commits
|
|
857
853
|
// gives them new SHAs and breaks traceability (the branch.landed SHA must exist on
|
|
858
|
-
// base verbatim). headSha is the preserved landed commit;
|
|
859
|
-
//
|
|
854
|
+
// base verbatim). headSha is the preserved landed commit; local no-ff lands tie the
|
|
855
|
+
// branch in with a merge commit so the agent's commits keep their identity.
|
|
860
856
|
const headResult = await mergeGit(["rev-parse", "HEAD"], worktreePath, "rebase", "workspace merge resolve workspace HEAD before gates", signal);
|
|
861
857
|
const headSha = headResult.stdout;
|
|
862
858
|
if (!headResult.ok || !headSha) return head({ status: "review_requested", error: gitError(headResult, "failed to resolve workspace HEAD before gates") });
|
|
@@ -865,8 +861,7 @@ async function mergeRebaseFf(
|
|
|
865
861
|
const landedSubject = (await mergeGit(["log", "-1", "--format=%s", headSha], worktreePath, "rebase", "workspace merge read landed commit subject", signal)).stdout || undefined;
|
|
866
862
|
|
|
867
863
|
// Resolve the SHA the work will integrate onto WITHOUT moving the ref. Origin-ahead is
|
|
868
|
-
// the
|
|
869
|
-
// but defer the local-base sync until the final base-ref mutation block below.
|
|
864
|
+
// the #1184 fail-fast case; local-base no-ff is left to the existing gate/ref machinery.
|
|
870
865
|
const integrationBaseResult = await mergeGit(["rev-parse", base], repoRoot, "rebase", `workspace merge resolve integration base ${base}`, signal);
|
|
871
866
|
let integrationBaseSha = integrationBaseResult.stdout;
|
|
872
867
|
if (!integrationBaseResult.ok || !integrationBaseSha) return head({ status: "review_requested", error: gitError(integrationBaseResult, `failed to resolve integration base ${base}`) });
|
|
@@ -883,25 +878,46 @@ async function mergeRebaseFf(
|
|
|
883
878
|
return head({ status: "review_requested", error: errMessage(err) });
|
|
884
879
|
}
|
|
885
880
|
if (!(await mergeGit(["merge-base", "--is-ancestor", upstream, base], worktreePath, "rebase", `workspace merge compare ${upstream} to ${base}`, signal)).ok) {
|
|
886
|
-
// Origin moved ahead.
|
|
887
|
-
//
|
|
881
|
+
// Origin moved ahead. If local base is not cleanly behind, it's genuine divergence
|
|
882
|
+
// and we refuse without mutating. If it is cleanly behind, compute against upstream;
|
|
883
|
+
// the behind check below decides whether direct land can still fast-forward.
|
|
888
884
|
if (!(await mergeGit(["merge-base", "--is-ancestor", base, upstream], worktreePath, "rebase", `workspace merge compare ${base} to ${upstream}`, signal)).ok) {
|
|
889
885
|
return head({ status: "review_requested", error: `local ${base} has diverged from ${upstream} (commits not on origin); sync before landing` });
|
|
890
886
|
}
|
|
891
887
|
const upstreamSha = (await mergeGit(["rev-parse", "--verify", upstream], worktreePath, "rebase", `workspace merge resolve ${upstream} for integration`, signal)).stdout;
|
|
892
888
|
if (!upstreamSha) return head({ status: "review_requested", error: `cannot resolve ${upstream} to sync ${base}` });
|
|
893
|
-
//
|
|
889
|
+
// Consider fresh origin the integration base, but DON'T advance local base yet.
|
|
894
890
|
integrationBaseSha = upstreamSha;
|
|
895
891
|
needSync = true;
|
|
896
892
|
}
|
|
897
893
|
}
|
|
898
894
|
|
|
899
|
-
// Behind relative to
|
|
895
|
+
// Behind relative to fresh upstream means direct land is not a fast-forward of the
|
|
896
|
+
// instance's true integration base. Fail before gates/ref mutation so callers see
|
|
897
|
+
// the reason and can rebase or choose PR explicitly. Local-only behind remains the
|
|
898
|
+
// pre-existing no-ff path to preserve gate, timeout, and abort-recovery behavior.
|
|
900
899
|
const behindResult = await countBehind(worktreePath, integrationBaseSha, signal);
|
|
901
900
|
if ("error" in behindResult) return head({ status: "review_requested", error: behindResult.error });
|
|
902
901
|
const behind = behindResult.behind;
|
|
902
|
+
if (behind > 0 && needSync) {
|
|
903
|
+
const branchName = branch ?? "workspace branch";
|
|
904
|
+
const baseName = base ?? "base";
|
|
905
|
+
const advanced = integrationBaseSha ? ` (base advanced to ${integrationBaseSha.slice(0, 12)})` : "";
|
|
906
|
+
return head({
|
|
907
|
+
status: "review_requested",
|
|
908
|
+
error: `cannot direct-land: ${branchName} is not a fast-forward of ${baseName}${advanced}; rebase and retry, or land with --strategy pr`,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
903
911
|
|
|
904
|
-
const
|
|
912
|
+
const gateSkippedField = gateLevel !== "full" ? {
|
|
913
|
+
gateSkipped: {
|
|
914
|
+
level: gateLevel,
|
|
915
|
+
reason: input.gateReason ?? "",
|
|
916
|
+
by: input.gateRequestedBy ?? "unknown",
|
|
917
|
+
commit: headSha,
|
|
918
|
+
},
|
|
919
|
+
} : {};
|
|
920
|
+
const gateRun = await runLandGatesOnIntegratedTree(repoRoot, worktreePath, behind, integrationBaseSha, headSha, landMergeMessage(branch, landedSubject), gateLevel, signal);
|
|
905
921
|
throwIfMergeAborted(signal);
|
|
906
922
|
if ("abort" in gateRun) {
|
|
907
923
|
return gateRun.abort.conflict
|
|
@@ -1062,14 +1078,14 @@ async function mergeRebaseFf(
|
|
|
1062
1078
|
const depsRefresh = await refreshWorkspaceDeps(ownerRepo, worktreePath);
|
|
1063
1079
|
throwIfMergeAborted(signal);
|
|
1064
1080
|
const reportDeps = depsRefresh.refreshed || depsRefresh.stale || depsRefresh.error;
|
|
1065
|
-
return head({ merged: true, status: "active", mergedSha: headSha, baseSha: baseTip, subject: landedSubject, worktreeRemoved: false, branch: fresh, newBranch: fresh, ...deleteResult, pushed, ...(reportDeps ? { depsRefresh } : {}), ...baseWorktreeSyncField, ...gateWarningsField, error: undefined });
|
|
1081
|
+
return head({ merged: true, status: "active", mergedSha: headSha, baseSha: baseTip, subject: landedSubject, worktreeRemoved: false, branch: fresh, newBranch: fresh, ...deleteResult, pushed, ...(reportDeps ? { depsRefresh } : {}), ...baseWorktreeSyncField, ...gateWarningsField, ...gateSkippedField, error: undefined });
|
|
1066
1082
|
}
|
|
1067
1083
|
// Recycle failed — keep the existing branch. Still landed, still active.
|
|
1068
|
-
return head({ merged: true, status: "active", mergedSha: headSha, baseSha: baseTip, subject: landedSubject, worktreeRemoved: false, branchDeleted: false, pushed, ...baseWorktreeSyncField, ...gateWarningsField, error: undefined });
|
|
1084
|
+
return head({ merged: true, status: "active", mergedSha: headSha, baseSha: baseTip, subject: landedSubject, worktreeRemoved: false, branchDeleted: false, pushed, ...baseWorktreeSyncField, ...gateWarningsField, ...gateSkippedField, error: undefined });
|
|
1069
1085
|
}
|
|
1070
1086
|
throwIfMergeAborted(signal);
|
|
1071
1087
|
const removed = await mergeGit(["worktree", "remove", "--force", worktreePath], ownerRepo, "cleanup", "workspace merge remove landed worktree", signal);
|
|
1072
1088
|
const worktreeRemoved = removed.ok;
|
|
1073
1089
|
const deleteResult = worktreeRemoved ? await deleteBranchIfSafe(ownerRepo, branch, undefined, baseTip, signal, input.id) : { branchDeleted: false };
|
|
1074
|
-
return head({ merged: true, status: "merged", mergedSha: headSha, baseSha: baseTip, subject: landedSubject, worktreeRemoved, ...deleteResult, pushed, ...baseWorktreeSyncField, ...gateWarningsField, error: undefined });
|
|
1090
|
+
return head({ merged: true, status: "merged", mergedSha: headSha, baseSha: baseTip, subject: landedSubject, worktreeRemoved, ...deleteResult, pushed, ...baseWorktreeSyncField, ...gateWarningsField, ...gateSkippedField, error: undefined });
|
|
1075
1091
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WorkspaceMetadata, WorkspaceMode } from "agent-relay-sdk";
|
|
2
|
-
import type { WorkspaceMergeResult } from "agent-relay-sdk";
|
|
2
|
+
import type { WorkspaceLandGateLevel, WorkspaceMergeResult } from "agent-relay-sdk";
|
|
3
3
|
|
|
4
4
|
/** Attach to or branch off an existing managed worktree instead of creating a fresh one (#635). */
|
|
5
5
|
export interface ResumeWorkspaceTarget {
|
|
@@ -59,6 +59,11 @@ export interface WorkspaceMergeInput {
|
|
|
59
59
|
* - "on-approval": open PR, do NOT arm; reviewer pipeline arms it later.
|
|
60
60
|
* - "manual": open PR, do NOT arm (today's legacy behavior). */
|
|
61
61
|
autoMerge?: "on-green" | "on-approval" | "manual";
|
|
62
|
+
/** Land-gate level. Omitted/"full" runs the base-declared required gates. */
|
|
63
|
+
gateLevel?: WorkspaceLandGateLevel;
|
|
64
|
+
/** Required by Relay before subset/none reaches the host; carried for audit output. */
|
|
65
|
+
gateReason?: string;
|
|
66
|
+
gateRequestedBy?: string;
|
|
62
67
|
/**
|
|
63
68
|
* Set when the relay observed this branch's PR already merged on the remote and is
|
|
64
69
|
* dispatching a land-and-continue RECYCLE (#423). Carries the relay's ground truth (the
|