agent-relay-orchestrator 0.32.3 → 0.32.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 +2 -2
- package/src/git.ts +30 -0
- package/src/workspace-probe.ts +51 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-orchestrator",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.4",
|
|
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.20"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/bun": "latest",
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Low-level git primitives shared by the workspace probe/merge/cleanup helpers.
|
|
2
|
+
// Extracted from workspace-probe.ts so that giant keeps shrinking (epic #291) and the
|
|
3
|
+
// `git -C` invocation lives in one place.
|
|
4
|
+
|
|
5
|
+
interface GitResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Run `git -C cwd <args>` and capture trimmed stdout/stderr; never throws. */
|
|
12
|
+
export function git(args: string[], cwd: string): GitResult {
|
|
13
|
+
const proc = Bun.spawnSync(["git", "-C", cwd, ...args], {
|
|
14
|
+
stdin: "ignore",
|
|
15
|
+
stdout: "pipe",
|
|
16
|
+
stderr: "pipe",
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
ok: proc.exitCode === 0,
|
|
20
|
+
stdout: proc.stdout.toString().trim(),
|
|
21
|
+
stderr: proc.stderr.toString().trim(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Like {@link git} but throws on a non-zero exit, returning stdout on success. */
|
|
26
|
+
export function requireGit(args: string[], cwd: string): string {
|
|
27
|
+
const result = git(args, cwd);
|
|
28
|
+
if (!result.ok) throw new Error(result.stderr || `git ${args.join(" ")} failed`);
|
|
29
|
+
return result.stdout;
|
|
30
|
+
}
|
package/src/workspace-probe.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:pat
|
|
|
5
5
|
import type { WorkspaceDepsProvision, WorkspaceDepsRefreshDir, WorkspaceDepsRefreshResult, WorkspaceDiff, WorkspaceDiffFile, WorkspaceGitState, WorkspaceMergePreview, WorkspaceMergeResult, WorkspaceMetadata, WorkspaceMode, WorkspaceProbe, WorkspaceProbeWorktree, WorkspaceStatus, WorkspaceSymlinkProvision } from "agent-relay-sdk";
|
|
6
6
|
import { errMessage } from "agent-relay-sdk";
|
|
7
7
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
8
|
+
import { git, requireGit } from "./git";
|
|
8
9
|
|
|
9
10
|
const MAX_DIFF_PATCH_BYTES = 200_000;
|
|
10
11
|
|
|
@@ -25,31 +26,6 @@ interface WorkspaceResolution {
|
|
|
25
26
|
workspace: WorkspaceMetadata;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
interface GitResult {
|
|
29
|
-
ok: boolean;
|
|
30
|
-
stdout: string;
|
|
31
|
-
stderr: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function git(args: string[], cwd: string): GitResult {
|
|
35
|
-
const proc = Bun.spawnSync(["git", "-C", cwd, ...args], {
|
|
36
|
-
stdin: "ignore",
|
|
37
|
-
stdout: "pipe",
|
|
38
|
-
stderr: "pipe",
|
|
39
|
-
});
|
|
40
|
-
return {
|
|
41
|
-
ok: proc.exitCode === 0,
|
|
42
|
-
stdout: proc.stdout.toString().trim(),
|
|
43
|
-
stderr: proc.stderr.toString().trim(),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function requireGit(args: string[], cwd: string): string {
|
|
48
|
-
const result = git(args, cwd);
|
|
49
|
-
if (!result.ok) throw new Error(result.stderr || `git ${args.join(" ")} failed`);
|
|
50
|
-
return result.stdout;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
29
|
function shortBranch(ref: string | undefined): string | undefined {
|
|
54
30
|
if (!ref || ref === "HEAD") return undefined;
|
|
55
31
|
return ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
|
|
@@ -472,12 +448,34 @@ export function pruneWorktrees(input: { repoRoot?: string }): { repoRoot: string
|
|
|
472
448
|
return { repoRoot: repo, pruned: true, output: result.stdout.trim() || undefined };
|
|
473
449
|
}
|
|
474
450
|
|
|
451
|
+
// Repo that actually owns `worktreePath`'s git admin. A CHAINED workspace's recorded
|
|
452
|
+
// repoRoot points at the PARENT worktree (maybe deleted), not the real checkout (#278/#307);
|
|
453
|
+
// every linked worktree shares the main `.git` (--git-common-dir), so derive the owner from
|
|
454
|
+
// the worktree itself, falling back to `fallback` only when it can't be interrogated.
|
|
455
|
+
function owningRepoRoot(worktreePath: string, fallback: string): string {
|
|
456
|
+
const res = git(["rev-parse", "--git-common-dir"], worktreePath);
|
|
457
|
+
if (!res.ok || !res.stdout) return fallback;
|
|
458
|
+
let commonDir = res.stdout;
|
|
459
|
+
if (!isAbsolute(commonDir)) commonDir = resolve(worktreePath, commonDir);
|
|
460
|
+
const root = basename(commonDir) === ".git" ? dirname(commonDir) : commonDir; // .git's parent = repo
|
|
461
|
+
return existsSync(root) ? root : fallback;
|
|
462
|
+
}
|
|
463
|
+
|
|
475
464
|
export function cleanupWorkspace(workspace: { repoRoot?: string; worktreePath?: string; id?: string; branch?: string; deleteBranch?: boolean; workspacesRoot?: string }): { workspaceId?: string; removed: boolean; worktreePath?: string; branchDeleted?: boolean; containerRemoved?: boolean } {
|
|
476
465
|
if (!workspace.worktreePath) throw new Error("worktreePath required");
|
|
477
466
|
const path = resolve(workspace.worktreePath);
|
|
478
|
-
const
|
|
467
|
+
const recordedRepo = workspace.repoRoot ? resolve(workspace.repoRoot) : path;
|
|
468
|
+
// Remove via the REAL owning repo, not the recorded (chained/deleted) repoRoot which
|
|
469
|
+
// would silently no-op and leak the dir while the relay marks it `cleaned` (#307).
|
|
470
|
+
const repo = owningRepoRoot(path, recordedRepo);
|
|
479
471
|
const result = git(["worktree", "remove", "--force", path], repo);
|
|
480
|
-
|
|
472
|
+
// Trust the filesystem, not git's exit code (a remove against the wrong repo "succeeds"
|
|
473
|
+
// touching nothing). If the dir survives, surface failure so the relay leaves the row
|
|
474
|
+
// un-cleaned instead of recording a phantom removal (#307).
|
|
475
|
+
if (existsSync(path)) {
|
|
476
|
+
git(["worktree", "prune"], repo);
|
|
477
|
+
throw new Error(result.stderr || `worktree ${path} still present after \`git worktree remove\` (repo ${repo})`);
|
|
478
|
+
}
|
|
481
479
|
// Once the worktree is gone the agent/... branch is litter — delete it so
|
|
482
480
|
// branches don't accumulate. Best-effort: don't fail cleanup if it can't
|
|
483
481
|
// (e.g. branch already gone, or checked out elsewhere).
|
|
@@ -736,9 +734,9 @@ function ghAvailable(): boolean {
|
|
|
736
734
|
* Returns undefined when there's no PR, no branch, or gh fails —
|
|
737
735
|
* we never invent a merge.
|
|
738
736
|
*/
|
|
739
|
-
function prMergedState(cwd: string, branch: string | undefined): { state: "merged" | "open"; url?: string } | undefined {
|
|
737
|
+
function prMergedState(cwd: string, branch: string | undefined): { state: "merged" | "open"; url?: string; sha?: string } | undefined {
|
|
740
738
|
if (!branch) return undefined;
|
|
741
|
-
const proc = Bun.spawnSync(["gh", "pr", "view", branch, "--json", "state,url"], {
|
|
739
|
+
const proc = Bun.spawnSync(["gh", "pr", "view", branch, "--json", "state,url,mergeCommit"], {
|
|
742
740
|
cwd,
|
|
743
741
|
stdin: "ignore",
|
|
744
742
|
stdout: "pipe",
|
|
@@ -746,9 +744,10 @@ function prMergedState(cwd: string, branch: string | undefined): { state: "merge
|
|
|
746
744
|
});
|
|
747
745
|
if (proc.exitCode !== 0) return undefined; // no PR for the branch, or gh unavailable/unauthed
|
|
748
746
|
try {
|
|
749
|
-
const data = JSON.parse(proc.stdout.toString()) as { state?: string; url?: string };
|
|
747
|
+
const data = JSON.parse(proc.stdout.toString()) as { state?: string; url?: string; mergeCommit?: { oid?: string } | null };
|
|
750
748
|
if (!data.state) return undefined;
|
|
751
|
-
|
|
749
|
+
const sha = data.mergeCommit?.oid;
|
|
750
|
+
return { state: data.state === "MERGED" ? "merged" : "open", url: data.url, ...(sha ? { sha } : {}) };
|
|
752
751
|
} catch {
|
|
753
752
|
return undefined;
|
|
754
753
|
}
|
|
@@ -783,6 +782,20 @@ function worktreeForBranch(repoRoot: string, branch: string): { path: string; di
|
|
|
783
782
|
return { path: match.path, dirty: status.ok ? status.stdout.length > 0 : true };
|
|
784
783
|
}
|
|
785
784
|
|
|
785
|
+
// Populate a preview's PR fields from `gh` ground truth; returns whether the PR is merged
|
|
786
|
+
// (#168/#304). One home for the PR-landing rule so the worktree- and repoRoot-based previews
|
|
787
|
+
// can't drift. On a merge it stamps the merge SHA so the relay can finalize a parked pr land.
|
|
788
|
+
function applyPrMergeState(base: WorkspaceMergePreview, cwd: string, branch: string | undefined): boolean {
|
|
789
|
+
const pr = prMergedState(cwd, branch);
|
|
790
|
+
if (!pr) return false;
|
|
791
|
+
base.prState = pr.state;
|
|
792
|
+
if (pr.url) base.prUrl = pr.url;
|
|
793
|
+
if (pr.state !== "merged") return false;
|
|
794
|
+
base.prMerged = true;
|
|
795
|
+
if (pr.sha) base.prMergeSha = pr.sha;
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
786
799
|
export function previewBranchMerge(input: {
|
|
787
800
|
repoRoot?: string;
|
|
788
801
|
branch?: string;
|
|
@@ -815,16 +828,8 @@ export function previewBranchMerge(input: {
|
|
|
815
828
|
base.dirtyCount = 0;
|
|
816
829
|
|
|
817
830
|
let effectiveAhead = gitState.landed ? 0 : (gitState.unmergedAhead ?? gitState.ahead ?? 0);
|
|
818
|
-
if (
|
|
819
|
-
|
|
820
|
-
if (pr) {
|
|
821
|
-
base.prState = pr.state;
|
|
822
|
-
if (pr.url) base.prUrl = pr.url;
|
|
823
|
-
if (pr.state === "merged") {
|
|
824
|
-
base.prMerged = true;
|
|
825
|
-
effectiveAhead = 0;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
831
|
+
if (input.checkPr && strategy === "pr") {
|
|
832
|
+
if (applyPrMergeState(base, repoRoot, input.branch)) effectiveAhead = 0;
|
|
828
833
|
}
|
|
829
834
|
if (effectiveAhead === 0) {
|
|
830
835
|
const reason = base.prMerged
|
|
@@ -862,19 +867,11 @@ export function previewWorkspaceMerge(input: { worktreePath?: string; baseRef?:
|
|
|
862
867
|
base.dirtyCount = gitState.dirtyCount;
|
|
863
868
|
if ((gitState.dirtyCount ?? 0) > 0) return { ...base, reason: "worktree has uncommitted changes" };
|
|
864
869
|
let effectiveAhead = gitState.landed ? 0 : (gitState.unmergedAhead ?? gitState.ahead ?? 0);
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
if (pr) {
|
|
871
|
-
base.prState = pr.state;
|
|
872
|
-
if (pr.url) base.prUrl = pr.url;
|
|
873
|
-
if (pr.state === "merged") {
|
|
874
|
-
base.prMerged = true;
|
|
875
|
-
effectiveAhead = 0;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
870
|
+
// Ask gh in BOTH cases local git can't see a PR landing: a squash-merge (looks ahead)
|
|
871
|
+
// and a regular merge-commit (branch becomes an ancestor → ahead=0 no-op). A merged PR
|
|
872
|
+
// means landed; its SHA lets the relay finalize a parked pr land instead of stalling (#304).
|
|
873
|
+
if (input.checkPr && strategy === "pr" && input.worktreePath) {
|
|
874
|
+
if (applyPrMergeState(base, resolve(input.worktreePath), gitState.branch)) effectiveAhead = 0;
|
|
878
875
|
}
|
|
879
876
|
if (effectiveAhead === 0) {
|
|
880
877
|
const reason = base.prMerged
|