agent-relay-server 0.62.1 → 0.62.2
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/docs/openapi.json +1 -1
- package/package.json +1 -1
- package/src/branch-landed.ts +7 -1
- package/src/db/schema.ts +2 -1
- package/src/routes/commands.ts +3 -0
- package/src/services/issue-lifecycle.ts +100 -22
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.62.
|
|
5
|
+
"version": "0.62.2",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
package/package.json
CHANGED
package/src/branch-landed.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface BranchLandedInput {
|
|
|
34
34
|
* `undefined` from older orchestrators that don't report it → message stays generic.
|
|
35
35
|
*/
|
|
36
36
|
pushed?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The land came in via a pull request — the subject's trailing `(#NNN)` is the PR
|
|
39
|
+
* number, not an issue. Forwarded to the issue-lifecycle hook so it doesn't mistake
|
|
40
|
+
* the PR number for an issue to close (#482).
|
|
41
|
+
*/
|
|
42
|
+
prLand?: boolean;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
/**
|
|
@@ -172,7 +178,7 @@ export function reconcileLandedWorkspace(ws: WorkspaceRecord, preview: Workspace
|
|
|
172
178
|
metadata: { source: "server", maintenanceJobId: "workspace-conflict-scan", workspaceId: ws.id, fromStatus: ws.status, ...(sha ? { landedSha: sha } : {}) },
|
|
173
179
|
});
|
|
174
180
|
try {
|
|
175
|
-
notifyBranchLanded({ workspace: ws, mergedSha: sha, pushed: true });
|
|
181
|
+
notifyBranchLanded({ workspace: ws, mergedSha: sha, pushed: true, prLand: via === "pr" });
|
|
176
182
|
} catch {
|
|
177
183
|
// Notification is best-effort; the merged status + activity event still stand.
|
|
178
184
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -677,7 +677,8 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
677
677
|
);
|
|
678
678
|
CREATE INDEX IF NOT EXISTS idx_activity_operator ON activity_events(operator_id, created_at);
|
|
679
679
|
CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_events(created_at);
|
|
680
|
-
CREATE INDEX IF NOT EXISTS idx_activity_agent ON activity_events(agent_id, created_at);
|
|
680
|
+
CREATE INDEX IF NOT EXISTS idx_activity_agent ON activity_events(agent_id, created_at);
|
|
681
|
+
-- issue_ref_id/issue_action indexes are created in applyMigrations() AFTER the ALTER — never inline here (#483: inline index over a not-yet-migrated column crashes initDb on existing DBs).
|
|
681
682
|
|
|
682
683
|
CREATE TABLE IF NOT EXISTS channels (
|
|
683
684
|
id TEXT PRIMARY KEY,
|
package/src/routes/commands.ts
CHANGED
|
@@ -250,6 +250,9 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
250
250
|
subject: prLanded
|
|
251
251
|
? cleanString(prLanded.subject, "params.prLanded.subject", { max: 200 })
|
|
252
252
|
: cleanString(command.result.subject, "result.subject", { max: 200 }),
|
|
253
|
+
// PR-land subjects carry a trailing `(#PR)` GitHub stamps on the squash — not an
|
|
254
|
+
// issue. Tell the lifecycle hook so it won't close the PR number as an issue (#482).
|
|
255
|
+
prLand: Boolean(prLanded),
|
|
253
256
|
newBranch,
|
|
254
257
|
pushed: prLanded ? true : typeof command.result.pushed === "boolean" ? command.result.pushed : undefined,
|
|
255
258
|
});
|
|
@@ -22,6 +22,13 @@ export interface LandedIssueLifecycleInput {
|
|
|
22
22
|
mergedSha?: string;
|
|
23
23
|
subject?: string;
|
|
24
24
|
source?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The land happened via a pull request (the squash/merge subject carries a
|
|
27
|
+
* trailing `(#PR)` that GitHub appends — the PR number, NOT an issue). Bare-backlink
|
|
28
|
+
* parsing is suppressed in this mode so relay never mistakes the PR number for an
|
|
29
|
+
* issue to close; GitHub-native keywords + durable association still apply (#482).
|
|
30
|
+
*/
|
|
31
|
+
prLand?: boolean;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface ReopenIssueLifecycleInput {
|
|
@@ -44,8 +51,10 @@ export interface IssueLifecycleOptions {
|
|
|
44
51
|
log?: LogFn;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
export type LandIssueRefSource = "association" | "commit" | "backlink";
|
|
55
|
+
|
|
47
56
|
export type CloseIssueLifecycleResult =
|
|
48
|
-
| { issueRef: IssueRef; action: "closed"; source:
|
|
57
|
+
| { issueRef: IssueRef; action: "closed"; source: LandIssueRefSource; landKey: string }
|
|
49
58
|
| { issueRef: IssueRef; action: "skipped"; reason: "already-closed" | "already-processed"; landKey: string }
|
|
50
59
|
| { issueRef: IssueRef; action: "failed"; error: string; landKey: string };
|
|
51
60
|
|
|
@@ -73,6 +82,51 @@ function defaultIssueRepo(): string | undefined {
|
|
|
73
82
|
return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
/**
|
|
86
|
+
* `owner/repo` out of any git remote URL form: `git@github.com:owner/repo.git`,
|
|
87
|
+
* `https://github.com/owner/repo.git`, `ssh://git@host/owner/repo`, trailing slash/`.git`
|
|
88
|
+
* optional. Pure — exported for tests.
|
|
89
|
+
*/
|
|
90
|
+
export function parseOwnerRepoFromRemote(url: string | undefined): string | undefined {
|
|
91
|
+
if (!url) return undefined;
|
|
92
|
+
const match = url.trim().match(/[:/]([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?\/?$/);
|
|
93
|
+
return match?.[1];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let remoteRepoCache: Map<string, string | undefined> | undefined;
|
|
97
|
+
|
|
98
|
+
function repoFromGitRemote(repoRoot: string | undefined): string | undefined {
|
|
99
|
+
if (!repoRoot) return undefined;
|
|
100
|
+
if (!remoteRepoCache) remoteRepoCache = new Map();
|
|
101
|
+
if (remoteRepoCache.has(repoRoot)) return remoteRepoCache.get(repoRoot);
|
|
102
|
+
let repo: string | undefined;
|
|
103
|
+
try {
|
|
104
|
+
const proc = Bun.spawnSync(["git", "-C", repoRoot, "remote", "get-url", "origin"], {
|
|
105
|
+
stdin: "ignore",
|
|
106
|
+
stdout: "pipe",
|
|
107
|
+
stderr: "ignore",
|
|
108
|
+
});
|
|
109
|
+
if (proc.exitCode === 0) repo = parseOwnerRepoFromRemote(Buffer.from(proc.stdout).toString("utf8"));
|
|
110
|
+
} catch {
|
|
111
|
+
repo = undefined;
|
|
112
|
+
}
|
|
113
|
+
remoteRepoCache.set(repoRoot, repo);
|
|
114
|
+
return repo;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function __resetRemoteRepoCacheForTests(): void {
|
|
118
|
+
remoteRepoCache = undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The repo a bare `#NNN` in a land's commit subject resolves against. Derived per-workspace
|
|
123
|
+
* from its own git remote so a single relay serving many repos closes the RIGHT repo's issue;
|
|
124
|
+
* env (`AGENT_RELAY_DEFAULT_ISSUE_REPO`/`GITHUB_REPOSITORY`) is an override/fallback.
|
|
125
|
+
*/
|
|
126
|
+
function landDefaultRepo(workspace: LifecycleWorkspace): string | undefined {
|
|
127
|
+
return repoFromGitRemote(workspace.repoRoot) ?? defaultIssueRepo();
|
|
128
|
+
}
|
|
129
|
+
|
|
76
130
|
function cleanText(value: string | undefined): string | undefined {
|
|
77
131
|
const cleaned = value?.trim();
|
|
78
132
|
return cleaned || undefined;
|
|
@@ -116,33 +170,57 @@ const CLOSING_KEYWORD_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+([^\n\
|
|
|
116
170
|
const ISSUE_REF_AT_START_RE = /^(https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/issues\/[0-9]+(?:[/?#][^\s,)]*)?|[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#[0-9]+|#[0-9]+)/;
|
|
117
171
|
const REF_SEPARATOR_RE = /^(?:\s*,\s*|\s+(?:and|&)\s+)/i;
|
|
118
172
|
|
|
173
|
+
// A run of issue refs ("#1, #2 and owner/repo#3") starting at the head of `segment`.
|
|
174
|
+
// Bare refs resolve against `fallbackDefaultRepo`; unresolvable ones are skipped so a
|
|
175
|
+
// land never fails here.
|
|
176
|
+
function collectRefRun(segment: string, fallbackDefaultRepo: string | undefined, into: IssueRef[]): void {
|
|
177
|
+
let rest = segment;
|
|
178
|
+
while (rest.trim()) {
|
|
179
|
+
rest = rest.trimStart();
|
|
180
|
+
const refMatch = rest.match(ISSUE_REF_AT_START_RE);
|
|
181
|
+
if (!refMatch) break;
|
|
182
|
+
try {
|
|
183
|
+
into.push(resolveIssueRefFromString({ ref: refMatch[1]!, defaultRepo: fallbackDefaultRepo }));
|
|
184
|
+
} catch {
|
|
185
|
+
// Invalid or bare-without-default refs are ignored; the land itself must never fail here.
|
|
186
|
+
}
|
|
187
|
+
rest = rest.slice(refMatch[0].length);
|
|
188
|
+
const separator = rest.match(REF_SEPARATOR_RE);
|
|
189
|
+
if (!separator) break;
|
|
190
|
+
rest = rest.slice(separator[0].length);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
119
194
|
export function parseClosingIssueRefs(text: string | undefined, fallbackDefaultRepo = defaultIssueRepo()): IssueRef[] {
|
|
120
195
|
if (!text) return [];
|
|
121
196
|
const refs: IssueRef[] = [];
|
|
122
|
-
for (const match of text.matchAll(CLOSING_KEYWORD_RE))
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!separator) break;
|
|
136
|
-
rest = rest.slice(separator[0].length);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
197
|
+
for (const match of text.matchAll(CLOSING_KEYWORD_RE)) collectRefRun(match[1] ?? "", fallbackDefaultRepo, refs);
|
|
198
|
+
return issueRefMap(refs);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Parenthesized backlink groups: the trailing `(#NNN)` convention agents (and a squash merge)
|
|
202
|
+
// stamp on a subject without a closing keyword. Non-issue parens (conventional-commit scopes
|
|
203
|
+
// like `fix(tokens)`, notes like `(see also)`) carry no `#` and fall out.
|
|
204
|
+
const BACKLINK_GROUP_RE = /\(([^()]*#[0-9]+[^()]*)\)/g;
|
|
205
|
+
|
|
206
|
+
export function parseBacklinkIssueRefs(text: string | undefined, fallbackDefaultRepo = defaultIssueRepo()): IssueRef[] {
|
|
207
|
+
if (!text) return [];
|
|
208
|
+
const refs: IssueRef[] = [];
|
|
209
|
+
for (const group of text.matchAll(BACKLINK_GROUP_RE)) collectRefRun(group[1] ?? "", fallbackDefaultRepo, refs);
|
|
139
210
|
return issueRefMap(refs);
|
|
140
211
|
}
|
|
141
212
|
|
|
142
|
-
export function resolveIssueRefsForLand(input: LandedIssueLifecycleInput): { source:
|
|
213
|
+
export function resolveIssueRefsForLand(input: LandedIssueLifecycleInput): { source: LandIssueRefSource; refs: IssueRef[] } {
|
|
143
214
|
const associated = associatedIssueRefs(input.workspace);
|
|
144
215
|
if (associated.length > 0) return { source: "association", refs: associated };
|
|
145
|
-
|
|
216
|
+
const fallbackDefaultRepo = landDefaultRepo(input.workspace);
|
|
217
|
+
const keyworded = parseClosingIssueRefs(input.subject, fallbackDefaultRepo);
|
|
218
|
+
if (keyworded.length > 0) return { source: "commit", refs: keyworded };
|
|
219
|
+
// Bare `(#NNN)` is closing intent in relay-land mode (no PR), but the SAME shape is the
|
|
220
|
+
// PR number GitHub appends to a squash subject — never close that. Suppress on PR lands;
|
|
221
|
+
// those rely on GitHub-native keywords + durable association instead (#482).
|
|
222
|
+
if (input.prLand) return { source: "commit", refs: [] };
|
|
223
|
+
return { source: "backlink", refs: parseBacklinkIssueRefs(input.subject, fallbackDefaultRepo) };
|
|
146
224
|
}
|
|
147
225
|
|
|
148
226
|
function closeAuditComment(input: LandedIssueLifecycleInput): string {
|
|
@@ -152,7 +230,7 @@ function closeAuditComment(input: LandedIssueLifecycleInput): string {
|
|
|
152
230
|
|
|
153
231
|
async function closeIssueRefForLand(
|
|
154
232
|
issueRef: IssueRef,
|
|
155
|
-
source:
|
|
233
|
+
source: LandIssueRefSource,
|
|
156
234
|
input: LandedIssueLifecycleInput,
|
|
157
235
|
opts: IssueLifecycleOptions,
|
|
158
236
|
): Promise<CloseIssueLifecycleResult> {
|
|
@@ -237,7 +315,7 @@ async function reopenIssuesForRevert(input: LandedIssueLifecycleInput, opts: Iss
|
|
|
237
315
|
}
|
|
238
316
|
|
|
239
317
|
export async function handleLandedIssueLifecycle(input: LandedIssueLifecycleInput, opts: IssueLifecycleOptions = {}): Promise<{
|
|
240
|
-
source:
|
|
318
|
+
source: LandIssueRefSource;
|
|
241
319
|
closed: CloseIssueLifecycleResult[];
|
|
242
320
|
reopened: ReopenIssueLifecycleResult[];
|
|
243
321
|
skipped: string[];
|