agent-relay-orchestrator 0.58.0 → 0.59.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 +6 -0
- package/src/workspace-pr.ts +5 -1
- package/src/workspace-probe.ts +58 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-orchestrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.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.35"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/bun": "latest",
|
package/src/control.ts
CHANGED
|
@@ -122,6 +122,12 @@ export function createControlHandler(
|
|
|
122
122
|
prTitle: typeof command.params.prTitle === "string" ? command.params.prTitle : undefined,
|
|
123
123
|
prBody: typeof command.params.prBody === "string" ? command.params.prBody : undefined,
|
|
124
124
|
autoMerge: command.params.autoMerge === "on-green" || command.params.autoMerge === "on-approval" || command.params.autoMerge === "manual" ? command.params.autoMerge : undefined,
|
|
125
|
+
prLanded: isRecord(command.params.prLanded)
|
|
126
|
+
? {
|
|
127
|
+
sha: typeof command.params.prLanded.sha === "string" ? command.params.prLanded.sha : undefined,
|
|
128
|
+
subject: typeof command.params.prLanded.subject === "string" ? command.params.prLanded.subject : undefined,
|
|
129
|
+
}
|
|
130
|
+
: undefined,
|
|
125
131
|
});
|
|
126
132
|
await relay.updateCommand(command.id, "succeeded", result as unknown as Record<string, unknown>);
|
|
127
133
|
} else if (command.type === "workspace.pr-arm-auto-merge") {
|
package/src/workspace-pr.ts
CHANGED
|
@@ -7,6 +7,7 @@ interface PullRequestState {
|
|
|
7
7
|
url?: string;
|
|
8
8
|
sha?: string;
|
|
9
9
|
number?: number;
|
|
10
|
+
title?: string;
|
|
10
11
|
reviewDecision?: PrReviewDecision;
|
|
11
12
|
statusCheckRollup?: unknown[];
|
|
12
13
|
mergeable?: string;
|
|
@@ -19,7 +20,7 @@ function prReviewDecision(value: unknown): PrReviewDecision | undefined {
|
|
|
19
20
|
|
|
20
21
|
export function prMergedState(cwd: string, branch: string | undefined): PullRequestState | undefined {
|
|
21
22
|
if (!branch) return undefined;
|
|
22
|
-
const proc = Bun.spawnSync(["gh", "pr", "view", branch, "--json", "state,url,mergeCommit,number,reviewDecision,statusCheckRollup,mergeable,mergeStateStatus"], {
|
|
23
|
+
const proc = Bun.spawnSync(["gh", "pr", "view", branch, "--json", "state,url,mergeCommit,number,title,reviewDecision,statusCheckRollup,mergeable,mergeStateStatus"], {
|
|
23
24
|
cwd,
|
|
24
25
|
stdin: "ignore",
|
|
25
26
|
stdout: "pipe",
|
|
@@ -33,6 +34,7 @@ export function prMergedState(cwd: string, branch: string | undefined): PullRequ
|
|
|
33
34
|
url?: string;
|
|
34
35
|
mergeCommit?: { oid?: string } | null;
|
|
35
36
|
number?: number;
|
|
37
|
+
title?: unknown;
|
|
36
38
|
reviewDecision?: unknown;
|
|
37
39
|
statusCheckRollup?: unknown;
|
|
38
40
|
mergeable?: unknown;
|
|
@@ -41,6 +43,7 @@ export function prMergedState(cwd: string, branch: string | undefined): PullRequ
|
|
|
41
43
|
if (!data.state) return undefined;
|
|
42
44
|
const sha = data.mergeCommit?.oid;
|
|
43
45
|
const number = typeof data.number === "number" && Number.isSafeInteger(data.number) ? data.number : undefined;
|
|
46
|
+
const title = typeof data.title === "string" && data.title.trim() ? data.title.trim() : undefined;
|
|
44
47
|
const reviewDecision = prReviewDecision(data.reviewDecision);
|
|
45
48
|
const statusCheckRollup = Array.isArray(data.statusCheckRollup) ? data.statusCheckRollup : undefined;
|
|
46
49
|
const mergeable = typeof data.mergeable === "string" ? data.mergeable : undefined;
|
|
@@ -50,6 +53,7 @@ export function prMergedState(cwd: string, branch: string | undefined): PullRequ
|
|
|
50
53
|
url: data.url,
|
|
51
54
|
...(sha ? { sha } : {}),
|
|
52
55
|
...(number ? { number } : {}),
|
|
56
|
+
...(title ? { title } : {}),
|
|
53
57
|
...(reviewDecision ? { reviewDecision } : {}),
|
|
54
58
|
...(statusCheckRollup ? { statusCheckRollup } : {}),
|
|
55
59
|
...(mergeable ? { mergeable } : {}),
|
package/src/workspace-probe.ts
CHANGED
|
@@ -599,6 +599,25 @@ function upstreamRef(worktreePath: string, base: string): string | undefined {
|
|
|
599
599
|
return res.ok && res.stdout ? res.stdout : undefined;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Best-effort fetch of `base`'s upstream and return the ref to cut a recycled branch from.
|
|
604
|
+
* Used by the #423 PR-land recycle: a squash merge lands on ORIGIN while the worktree's local
|
|
605
|
+
* base is never advanced, so cutting from local base would recycle onto stale main. Fetching
|
|
606
|
+
* the upstream and returning `origin/<base>` (when it resolves) yields a branch off CURRENT
|
|
607
|
+
* base. Falls back to the local base ref when there is no upstream (no remote) or the
|
|
608
|
+
* upstream ref can't be resolved — the caller still verifies it contains the merge SHA.
|
|
609
|
+
*/
|
|
610
|
+
function syncBaseFromOrigin(worktreePath: string, base: string | undefined): string | undefined {
|
|
611
|
+
if (!base) return undefined;
|
|
612
|
+
const upstream = upstreamRef(worktreePath, base);
|
|
613
|
+
if (!upstream) return base;
|
|
614
|
+
const slash = upstream.indexOf("/");
|
|
615
|
+
const remote = slash > 0 ? upstream.slice(0, slash) : undefined;
|
|
616
|
+
const remoteBranch = slash > 0 ? upstream.slice(slash + 1) : base;
|
|
617
|
+
if (remote) git(["fetch", remote, remoteBranch], worktreePath); // best-effort freshness
|
|
618
|
+
return git(["rev-parse", "--verify", "--quiet", upstream], worktreePath).ok ? upstream : base;
|
|
619
|
+
}
|
|
620
|
+
|
|
602
621
|
function resolveBaseRef(worktreePath: string, baseRef?: string, baseSha?: string): string | undefined {
|
|
603
622
|
for (const candidate of [baseRef, baseSha]) {
|
|
604
623
|
if (candidate && git(["rev-parse", "--verify", "--quiet", `${candidate}^{commit}`], worktreePath).ok) {
|
|
@@ -721,6 +740,15 @@ interface WorkspaceMergeInput {
|
|
|
721
740
|
* - "on-approval": open PR, do NOT arm; reviewer pipeline arms it later.
|
|
722
741
|
* - "manual": open PR, do NOT arm (today's legacy behavior). */
|
|
723
742
|
autoMerge?: "on-green" | "on-approval" | "manual";
|
|
743
|
+
/**
|
|
744
|
+
* Set when the relay observed this branch's PR already merged on the remote and is
|
|
745
|
+
* dispatching a land-and-continue RECYCLE (#423). Carries the relay's ground truth (the
|
|
746
|
+
* merge SHA/subject). Its presence forces the live-owner recycle even when local git can't
|
|
747
|
+
* see the landing — a squash onto an advanced base re-creates the work as a new commit
|
|
748
|
+
* (patch ids drift, trees diverge) so `previewWorkspaceMerge` reports ahead>0 and would
|
|
749
|
+
* otherwise loop via review_requested. prMerged is monotonic and was read from THIS host's
|
|
750
|
+
* own `checkPr` scan, so trusting it is safe; we still refuse on a dirty tree. */
|
|
751
|
+
prLanded?: { sha?: string; subject?: string };
|
|
724
752
|
}
|
|
725
753
|
|
|
726
754
|
/** Behind-count of HEAD relative to `base`, from inside `worktreePath`. */
|
|
@@ -791,6 +819,7 @@ function applyPrMergeState(base: WorkspaceMergePreview, cwd: string, branch: str
|
|
|
791
819
|
if (pr.state !== "merged") return false;
|
|
792
820
|
base.prMerged = true;
|
|
793
821
|
if (pr.sha) base.prMergeSha = pr.sha;
|
|
822
|
+
if (pr.title) base.prMergeSubject = pr.title;
|
|
794
823
|
return true;
|
|
795
824
|
}
|
|
796
825
|
|
|
@@ -915,6 +944,27 @@ export function mergeWorkspace(input: WorkspaceMergeInput): WorkspaceMergeResult
|
|
|
915
944
|
|
|
916
945
|
if (preview.missing) return head({ status: "cleaned", error: preview.reason });
|
|
917
946
|
if (preview.error) return head({ status: "review_requested", error: preview.error });
|
|
947
|
+
// #423 squash-recycle: the relay observed this branch's PR merged on the remote (ground
|
|
948
|
+
// truth) and asked for a land-and-continue recycle. For a squash onto an ADVANCED base —
|
|
949
|
+
// the common case in a busy PR-land instance — local cherry/tree detection can't see the
|
|
950
|
+
// landing, so `preview.noop` is false and we'd fall through to mergeRebaseFf → "origin
|
|
951
|
+
// moved ahead" → review_requested → re-fired every ~2min (soft-loop). Trust prLanded and
|
|
952
|
+
// recycle straight to a fresh branch off the UPDATED upstream base (the squash landed on
|
|
953
|
+
// origin; local base was never advanced).
|
|
954
|
+
//
|
|
955
|
+
// Two hard guardrails, because this resets the owner's branch:
|
|
956
|
+
// 1. Dirty tree → never reset (would blow away uncommitted work). Fall through to the
|
|
957
|
+
// reason guard below → review_requested, retried next scan.
|
|
958
|
+
// 2. Only recycle once the fetched base actually CONTAINS the merge SHA. If the fetch
|
|
959
|
+
// hasn't propagated the PR merge yet, fall through rather than reset onto a base
|
|
960
|
+
// missing the landed work — never reset on an unverified ground-truth/refs mismatch.
|
|
961
|
+
if (input.prLanded?.sha && (preview.dirtyCount ?? 0) === 0) {
|
|
962
|
+
const startRef = syncBaseFromOrigin(worktreePath, preview.baseRef);
|
|
963
|
+
if (startRef && git(["merge-base", "--is-ancestor", input.prLanded.sha, startRef], worktreePath).ok) {
|
|
964
|
+
const landedPreview: WorkspaceMergePreview = { ...preview, noop: true, prMerged: true, prMergeSha: input.prLanded.sha, reason: "PR merged on remote" };
|
|
965
|
+
return resolveNoopMerge(input, worktreePath, repoRoot, branch, landedPreview, head, startRef);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
918
968
|
// Nothing to land (ahead=0, clean): the branch tree is already in base. Resolve it
|
|
919
969
|
// to a terminal state so it leaves the steward queue instead of looping forever in
|
|
920
970
|
// review_requested (#230). Reclaim the spent worktree/branch when the owner is gone.
|
|
@@ -943,16 +993,22 @@ function resolveNoopMerge(
|
|
|
943
993
|
branch: string | undefined,
|
|
944
994
|
preview: WorkspaceMergePreview,
|
|
945
995
|
head: (field: Partial<WorkspaceMergeResult>) => WorkspaceMergeResult,
|
|
996
|
+
// Start point for the recycled branch. Defaults to the local base ref (the rebase-ff
|
|
997
|
+
// noop path, where the relay just advanced local base). The PR-land recycle (#423) passes
|
|
998
|
+
// the upstream tip (e.g. origin/main) so the live owner continues off UPDATED base rather
|
|
999
|
+
// than the worktree's stale local base.
|
|
1000
|
+
startRef?: string,
|
|
946
1001
|
): WorkspaceMergeResult {
|
|
947
1002
|
// Live owner (#327): recycle-to-continue instead of bricking the session.
|
|
948
1003
|
if (input.deleteBranch === false) {
|
|
949
1004
|
const base = preview.baseRef;
|
|
950
1005
|
if (base && branch) {
|
|
951
1006
|
const fresh = nextBranchName(repoRoot, branch);
|
|
952
|
-
|
|
1007
|
+
const start = startRef ?? base;
|
|
1008
|
+
if (git(["checkout", "-B", fresh, start], worktreePath).ok) {
|
|
953
1009
|
// Old branch's tree is already in base (that's what noop means) — safe to drop.
|
|
954
1010
|
const oldDeleted = git(["branch", "-D", branch], repoRoot).ok;
|
|
955
|
-
const baseSha = git(["rev-parse",
|
|
1011
|
+
const baseSha = git(["rev-parse", start], worktreePath).stdout || undefined;
|
|
956
1012
|
// Recycled onto base, which may declare deps the symlinked node_modules lacks (#51).
|
|
957
1013
|
const depsRefresh = refreshWorkspaceDeps(repoRoot, worktreePath);
|
|
958
1014
|
const reportDeps = depsRefresh.refreshed || depsRefresh.stale || depsRefresh.error;
|