agent-relay-server 0.61.1 → 0.62.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/docs/openapi.json +479 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-CX63ZV4X.js → MessageBubble-BP0Ym3xb.js} +2 -2
- package/public/assets/{MessageBubble-CX63ZV4X.js.map → MessageBubble-BP0Ym3xb.js.map} +1 -1
- package/public/assets/PlanGraphPanel-DKc3gdou.js +2 -0
- package/public/assets/PlanGraphPanel-DKc3gdou.js.map +1 -0
- package/public/assets/{activity-2xPOScu_.js → activity-CRLG6ICe.js} +2 -2
- package/public/assets/{activity-2xPOScu_.js.map → activity-CRLG6ICe.js.map} +1 -1
- package/public/assets/{agent-profiles-BfGTTbeY.js → agent-profiles-DItyEkXP.js} +2 -2
- package/public/assets/{agent-profiles-BfGTTbeY.js.map → agent-profiles-DItyEkXP.js.map} +1 -1
- package/public/assets/{agents-Bg3WOBRV.js → agents-5mKiZ1rz.js} +2 -2
- package/public/assets/{agents-Bg3WOBRV.js.map → agents-5mKiZ1rz.js.map} +1 -1
- package/public/assets/{analytics-De0cEZGx.js → analytics-J5pPcS-v.js} +3 -3
- package/public/assets/{analytics-De0cEZGx.js.map → analytics-J5pPcS-v.js.map} +1 -1
- package/public/assets/{automation-CHxhzq2t.js → automation-BG2HWK5N.js} +2 -2
- package/public/assets/{automation-CHxhzq2t.js.map → automation-BG2HWK5N.js.map} +1 -1
- package/public/assets/{badge-DhLyCPag.js → badge-CZhIdlTJ.js} +2 -2
- package/public/assets/{badge-DhLyCPag.js.map → badge-CZhIdlTJ.js.map} +1 -1
- package/public/assets/{branch-state-badge-DSyWKluD.js → branch-state-badge-BZMoxs65.js} +2 -2
- package/public/assets/{branch-state-badge-DSyWKluD.js.map → branch-state-badge-BZMoxs65.js.map} +1 -1
- package/public/assets/{button-K20wAZG1.js → button-2taQeNHW.js} +2 -2
- package/public/assets/{button-K20wAZG1.js.map → button-2taQeNHW.js.map} +1 -1
- package/public/assets/card-DzIsAI41.js +2 -0
- package/public/assets/{card-CcZ9hLI7.js.map → card-DzIsAI41.js.map} +1 -1
- package/public/assets/channels-B2PeqiRT.js +2 -0
- package/public/assets/{channels-BG0Nib7y.js.map → channels-B2PeqiRT.js.map} +1 -1
- package/public/assets/chat-CLRJVagA.js +2 -0
- package/public/assets/{chat-CjP_crSl.js.map → chat-CLRJVagA.js.map} +1 -1
- package/public/assets/{connectors-DWh7MwV_.js → connectors-Bzvkxsgo.js} +2 -2
- package/public/assets/{connectors-DWh7MwV_.js.map → connectors-Bzvkxsgo.js.map} +1 -1
- package/public/assets/copy-button-HEkwi6xX.js +2 -0
- package/public/assets/{copy-button-eF3cKp4Q.js.map → copy-button-HEkwi6xX.js.map} +1 -1
- package/public/assets/display-DzNvOu1j.js.map +1 -1
- package/public/assets/dist-1rXN5FN9.js +2 -0
- package/public/assets/{dist-YpHfnnIA.js.map → dist-1rXN5FN9.js.map} +1 -1
- package/public/assets/{dist-1dhfdhm9.js → dist-AbEQuFoK.js} +2 -2
- package/public/assets/{dist-1dhfdhm9.js.map → dist-AbEQuFoK.js.map} +1 -1
- package/public/assets/{dist-COQuunEv.js → dist-B7p1zkvf.js} +2 -2
- package/public/assets/{dist-COQuunEv.js.map → dist-B7p1zkvf.js.map} +1 -1
- package/public/assets/{dist-Ci5Y37nj.js → dist-CG10hM1-.js} +2 -2
- package/public/assets/{dist-Ci5Y37nj.js.map → dist-CG10hM1-.js.map} +1 -1
- package/public/assets/dist-Cfm5_zPB.js +2 -0
- package/public/assets/{dist-qd198ib9.js.map → dist-Cfm5_zPB.js.map} +1 -1
- package/public/assets/{dist-BrQsqBbc.js → dist-Cgw85UcE.js} +2 -2
- package/public/assets/{dist-BrQsqBbc.js.map → dist-Cgw85UcE.js.map} +1 -1
- package/public/assets/{dist-CoROafNg.js → dist-D3_TJKTC.js} +2 -2
- package/public/assets/{dist-CoROafNg.js.map → dist-D3_TJKTC.js.map} +1 -1
- package/public/assets/{dist-I-MFUv51.js → dist-DQkLq0hs.js} +2 -2
- package/public/assets/{dist-I-MFUv51.js.map → dist-DQkLq0hs.js.map} +1 -1
- package/public/assets/{dist-UQGKwqZH.js → dist-D_sl7u_f.js} +2 -2
- package/public/assets/{dist-UQGKwqZH.js.map → dist-D_sl7u_f.js.map} +1 -1
- package/public/assets/{dist-yGOq6sB3.js → dist-Ilb3r1im.js} +2 -2
- package/public/assets/{dist-yGOq6sB3.js.map → dist-Ilb3r1im.js.map} +1 -1
- package/public/assets/dist-Pf_3Gf38.js +2 -0
- package/public/assets/{dist-BLBF7qqr.js.map → dist-Pf_3Gf38.js.map} +1 -1
- package/public/assets/{es2015-CRxz9lOj.js → es2015-GSepNaIS.js} +2 -2
- package/public/assets/{es2015-CRxz9lOj.js.map → es2015-GSepNaIS.js.map} +1 -1
- package/public/assets/{formatted-body-impl-u0W_hWYA.js → formatted-body-impl-QqHxdMGP.js} +2 -2
- package/public/assets/{formatted-body-impl-u0W_hWYA.js.map → formatted-body-impl-QqHxdMGP.js.map} +1 -1
- package/public/assets/index-CCp1V35c.js +22 -0
- package/public/assets/index-CCp1V35c.js.map +1 -0
- package/public/assets/index-DiR-fqy_.css +2 -0
- package/public/assets/{input-BHYNpXAG.js → input-BsNqlONJ.js} +2 -2
- package/public/assets/{input-BHYNpXAG.js.map → input-BsNqlONJ.js.map} +1 -1
- package/public/assets/{insights-CAF9Hi5D.js → insights-D-EyDfoZ.js} +2 -2
- package/public/assets/{insights-CAF9Hi5D.js.map → insights-D-EyDfoZ.js.map} +1 -1
- package/public/assets/{integrations-CG9qyHUf.js → integrations-D1kc-yNi.js} +2 -2
- package/public/assets/{integrations-CG9qyHUf.js.map → integrations-D1kc-yNi.js.map} +1 -1
- package/public/assets/{lib-Df4aph5u.js → lib-bXPu6xCL.js} +2 -2
- package/public/assets/{lib-Df4aph5u.js.map → lib-bXPu6xCL.js.map} +1 -1
- package/public/assets/lucide-react-DWYjU-AF.js +9 -0
- package/public/assets/{lucide-react-L2UxFDic.js.map → lucide-react-DWYjU-AF.js.map} +1 -1
- package/public/assets/{maintenance-DgHA35ve.js → maintenance-COqID7aq.js} +2 -2
- package/public/assets/{maintenance-DgHA35ve.js.map → maintenance-COqID7aq.js.map} +1 -1
- package/public/assets/{managed-agents-Rk6ghKR9.js → managed-agents-b4B8qewQ.js} +2 -2
- package/public/assets/{managed-agents-Rk6ghKR9.js.map → managed-agents-b4B8qewQ.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-NWMtJdYz.js → markdown-preview-impl-D7o8jS07.js} +2 -2
- package/public/assets/{markdown-preview-impl-NWMtJdYz.js.map → markdown-preview-impl-D7o8jS07.js.map} +1 -1
- package/public/assets/memory-Q0qF7_i3.js +2 -0
- package/public/assets/{memory-DamwWa9A.js.map → memory-Q0qF7_i3.js.map} +1 -1
- package/public/assets/{messages--NU-YIL6.js → messages-m4EpkK6s.js} +2 -2
- package/public/assets/{messages--NU-YIL6.js.map → messages-m4EpkK6s.js.map} +1 -1
- package/public/assets/{orchestrators-DQnIn6qx.js → orchestrators-CnAtGfUN.js} +2 -2
- package/public/assets/{orchestrators-DQnIn6qx.js.map → orchestrators-CnAtGfUN.js.map} +1 -1
- package/public/assets/{overview-DCmNgXDf.js → overview-D7CF88ik.js} +2 -2
- package/public/assets/{overview-DCmNgXDf.js.map → overview-D7CF88ik.js.map} +1 -1
- package/public/assets/pairs-Q6PlFJtH.js +2 -0
- package/public/assets/{pairs-DNELeC9P.js.map → pairs-Q6PlFJtH.js.map} +1 -1
- package/public/assets/{react-dom-CrP_AtcL.js → react-dom-CPghDLpB.js} +2 -2
- package/public/assets/{react-dom-CrP_AtcL.js.map → react-dom-CPghDLpB.js.map} +1 -1
- package/public/assets/{security-Dd3Kh0oC.js → security-BeV-6SdA.js} +2 -2
- package/public/assets/{security-Dd3Kh0oC.js.map → security-BeV-6SdA.js.map} +1 -1
- package/public/assets/{select-DQbeJZX0.js → select-Dchc0_Jg.js} +2 -2
- package/public/assets/{select-DQbeJZX0.js.map → select-Dchc0_Jg.js.map} +1 -1
- package/public/assets/{settings-BMAJrBIH.js → settings-hPwL-31u.js} +3 -3
- package/public/assets/{settings-BMAJrBIH.js.map → settings-hPwL-31u.js.map} +1 -1
- package/public/assets/store-Buwkqv_7.js +9 -0
- package/public/assets/store-Buwkqv_7.js.map +1 -0
- package/public/assets/tasks-DKUMPEgk.js +2 -0
- package/public/assets/{tasks-BYR4L9GP.js.map → tasks-DKUMPEgk.js.map} +1 -1
- package/public/assets/teams-T5fFsdOX.js +2 -0
- package/public/assets/teams-T5fFsdOX.js.map +1 -0
- package/public/assets/{terminal-viewer-impl-B1YzfpE6.js → terminal-viewer-impl-Bk0u_Mav.js} +3 -3
- package/public/assets/{terminal-viewer-impl-B1YzfpE6.js.map → terminal-viewer-impl-Bk0u_Mav.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js → use-keyboard-viewport-NKw1O5gC.js} +2 -2
- package/public/assets/{use-keyboard-viewport-DrRH_QzG.js.map → use-keyboard-viewport-NKw1O5gC.js.map} +1 -1
- package/public/assets/{work-queue-x57vlgyZ.js → work-queue-D26jok09.js} +2 -2
- package/public/assets/{work-queue-x57vlgyZ.js.map → work-queue-D26jok09.js.map} +1 -1
- package/public/assets/{workspaces-CYm5bLg9.js → workspaces-BMj6zx23.js} +3 -3
- package/public/assets/{workspaces-CYm5bLg9.js.map → workspaces-BMj6zx23.js.map} +1 -1
- package/public/index.html +21 -21
- package/public/sw.js +32 -8
- package/src/automations.ts +40 -7
- package/src/branch-landed.ts +5 -0
- package/src/bus.ts +3 -0
- package/src/cli/workspace.ts +9 -17
- package/src/db/activity.ts +191 -3
- package/src/db/integrations.ts +1 -0
- package/src/db/issue-associations.ts +128 -0
- package/src/db/issue-lifecycle.ts +187 -0
- package/src/db/issue-refs.ts +254 -0
- package/src/db/mappers.ts +2 -0
- package/src/db/migrations.ts +47 -0
- package/src/db/schema.ts +2 -2
- package/src/issue-tracker/adapter.ts +24 -0
- package/src/issue-tracker/github.ts +136 -0
- package/src/issue-tracker/index.ts +13 -0
- package/src/issue-tracker/registry.ts +26 -0
- package/src/maintenance.ts +22 -169
- package/src/mcp-issue-activity-tools.ts +114 -0
- package/src/mcp-issue-association-tools.ts +130 -0
- package/src/mcp-issue-lifecycle-tools.ts +55 -0
- package/src/mcp-issue-ref-tools.ts +174 -0
- package/src/mcp-team-tools.ts +1 -1
- package/src/mcp.ts +3 -3
- package/src/provider-config-store.ts +30 -8
- package/src/routes/activity.ts +6 -1
- package/src/routes/commands.ts +29 -0
- package/src/routes/index.ts +19 -0
- package/src/routes/issue-activity.ts +78 -0
- package/src/routes/issue-associations.ts +76 -0
- package/src/routes/issue-lifecycle.ts +34 -0
- package/src/routes/issue-refs.ts +137 -0
- package/src/routes/orchestrator.ts +42 -1
- package/src/security.ts +8 -0
- package/src/services/issue-activity.ts +30 -0
- package/src/services/issue-associations.ts +72 -0
- package/src/services/issue-lifecycle.ts +308 -0
- package/src/services/issue-refs.ts +32 -0
- package/src/services/plan-graph.ts +40 -2
- package/src/services/team.ts +4 -0
- package/src/token-db.ts +3 -3
- package/src/workspace-actions.ts +8 -1
- package/src/workspace-auto-merge.ts +228 -0
- package/src/workspace-probe.ts +125 -0
- package/public/assets/PlanGraphPanel-Dby7wq9c.js +0 -2
- package/public/assets/PlanGraphPanel-Dby7wq9c.js.map +0 -1
- package/public/assets/card-CcZ9hLI7.js +0 -2
- package/public/assets/channels-BG0Nib7y.js +0 -2
- package/public/assets/chat-CjP_crSl.js +0 -2
- package/public/assets/copy-button-eF3cKp4Q.js +0 -2
- package/public/assets/dist-BLBF7qqr.js +0 -2
- package/public/assets/dist-YpHfnnIA.js +0 -2
- package/public/assets/dist-qd198ib9.js +0 -2
- package/public/assets/index-BwfVOzUG.js +0 -21
- package/public/assets/index-BwfVOzUG.js.map +0 -1
- package/public/assets/index-Dn_GDXbi.css +0 -2
- package/public/assets/lucide-react-L2UxFDic.js +0 -9
- package/public/assets/memory-DamwWa9A.js +0 -2
- package/public/assets/pairs-DNELeC9P.js +0 -2
- package/public/assets/store-B8xP6z6T.js +0 -9
- package/public/assets/store-B8xP6z6T.js.map +0 -1
- package/public/assets/tasks-BYR4L9GP.js +0 -2
- package/public/assets/teams-CoTifrq_.js +0 -2
- package/public/assets/teams-CoTifrq_.js.map +0 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { deleteIssueRef, getIssueRef, listIssueRefs, resolveIssueRefByExternalKey, resolveIssueRefFromString, updateIssueRef, upsertIssueRef } from "./db/issue-refs";
|
|
2
|
+
import { McpNotFoundError } from "./mcp-errors";
|
|
3
|
+
import { cleanEnum, cleanString, optionalEnum } from "./validation";
|
|
4
|
+
import { ISSUE_PROVIDERS, ISSUE_REF_STATUSES, isRecord, type IssueProvider, type IssueRefStatus } from "./types";
|
|
5
|
+
import { ValidationError } from "./db";
|
|
6
|
+
|
|
7
|
+
export const ISSUE_REF_TOOLS = [
|
|
8
|
+
{
|
|
9
|
+
name: "relay_issue_ref_upsert",
|
|
10
|
+
description: "Create or update the canonical IssueRef for provider/repo/externalKey. Idempotent for the same external issue.",
|
|
11
|
+
requiredScopes: ["issue:write"],
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
provider: { type: "string", enum: ISSUE_PROVIDERS },
|
|
16
|
+
repo: { type: "string" },
|
|
17
|
+
externalKey: { type: "string" },
|
|
18
|
+
url: { type: "string" },
|
|
19
|
+
title: { type: "string" },
|
|
20
|
+
status: { type: "string", enum: ISSUE_REF_STATUSES },
|
|
21
|
+
statusSyncedAt: { type: "integer", minimum: 0 },
|
|
22
|
+
},
|
|
23
|
+
required: ["provider", "repo", "externalKey", "url"],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "relay_issue_ref_resolve",
|
|
29
|
+
description: "Resolve a raw issue string (`#466`, `edimuj/agent-relay#466`, GitHub issue URL, or `issue_<uuid>`) or provider/repo/externalKey to the canonical IssueRef.",
|
|
30
|
+
requiredScopes: ["issue:write"],
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
ref: { type: "string" },
|
|
35
|
+
defaultRepo: { type: "string" },
|
|
36
|
+
provider: { type: "string", enum: ISSUE_PROVIDERS },
|
|
37
|
+
repo: { type: "string" },
|
|
38
|
+
externalKey: { type: "string" },
|
|
39
|
+
url: { type: "string" },
|
|
40
|
+
title: { type: "string" },
|
|
41
|
+
status: { type: "string", enum: ISSUE_REF_STATUSES },
|
|
42
|
+
statusSyncedAt: { type: "integer", minimum: 0 },
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "relay_issue_ref_get",
|
|
49
|
+
description: "Fetch one IssueRef by its opaque internal id.",
|
|
50
|
+
requiredScopes: ["issue:read"],
|
|
51
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "relay_issue_ref_list",
|
|
55
|
+
description: "List IssueRefs, optionally filtered by provider, repo, or cached status.",
|
|
56
|
+
requiredScopes: ["issue:read"],
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
provider: { type: "string", enum: ISSUE_PROVIDERS },
|
|
61
|
+
repo: { type: "string" },
|
|
62
|
+
status: { type: "string", enum: ISSUE_REF_STATUSES },
|
|
63
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "relay_issue_ref_update",
|
|
70
|
+
description: "Update cached IssueRef display fields without changing provider/repo/externalKey.",
|
|
71
|
+
requiredScopes: ["issue:write"],
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
id: { type: "string" },
|
|
76
|
+
url: { type: "string" },
|
|
77
|
+
title: { type: "string" },
|
|
78
|
+
status: { type: "string", enum: ISSUE_REF_STATUSES },
|
|
79
|
+
statusSyncedAt: { type: "integer", minimum: 0 },
|
|
80
|
+
},
|
|
81
|
+
required: ["id"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "relay_issue_ref_delete",
|
|
87
|
+
description: "Delete an IssueRef mapping row. Foundation-only cleanup; lifecycle automation lives elsewhere.",
|
|
88
|
+
requiredScopes: ["issue:write"],
|
|
89
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false },
|
|
90
|
+
},
|
|
91
|
+
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown> }>;
|
|
92
|
+
|
|
93
|
+
type IssueRefToolAuth = { kind: string; actor: string; scopes?: string[] };
|
|
94
|
+
|
|
95
|
+
function providerArg(value: unknown): IssueProvider {
|
|
96
|
+
return cleanEnum(value, "provider", ISSUE_PROVIDERS) as IssueProvider;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function statusArg(value: unknown): IssueRefStatus | undefined {
|
|
100
|
+
return optionalEnum(value, "status", ISSUE_REF_STATUSES) as IssueRefStatus | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function timestampArg(value: unknown): number | undefined {
|
|
104
|
+
if (value === undefined || value === null) return undefined;
|
|
105
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new ValidationError("statusSyncedAt must be a non-negative integer");
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function relayIssueRefTool(_auth: IssueRefToolAuth, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
110
|
+
if (name === "relay_issue_ref_upsert") {
|
|
111
|
+
return upsertIssueRef({
|
|
112
|
+
provider: providerArg(args.provider),
|
|
113
|
+
repo: cleanString(args.repo, "repo", { required: true, max: 500 })!,
|
|
114
|
+
externalKey: cleanString(args.externalKey, "externalKey", { required: true, max: 240 })!,
|
|
115
|
+
url: cleanString(args.url, "url", { required: true, max: 2000 })!,
|
|
116
|
+
title: cleanString(args.title, "title", { max: 500 }),
|
|
117
|
+
status: statusArg(args.status),
|
|
118
|
+
statusSyncedAt: timestampArg(args.statusSyncedAt),
|
|
119
|
+
}) as unknown as Record<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
if (name === "relay_issue_ref_resolve") {
|
|
122
|
+
const ref = cleanString(args.ref, "ref", { max: 1000 });
|
|
123
|
+
return (ref
|
|
124
|
+
? resolveIssueRefFromString({
|
|
125
|
+
ref,
|
|
126
|
+
defaultRepo: cleanString(args.defaultRepo ?? args.repo, "defaultRepo", { max: 500 }),
|
|
127
|
+
provider: optionalEnum(args.provider, "provider", ISSUE_PROVIDERS, "github") as IssueProvider,
|
|
128
|
+
title: cleanString(args.title, "title", { max: 500 }),
|
|
129
|
+
status: statusArg(args.status),
|
|
130
|
+
statusSyncedAt: timestampArg(args.statusSyncedAt),
|
|
131
|
+
})
|
|
132
|
+
: resolveIssueRefByExternalKey({
|
|
133
|
+
provider: providerArg(args.provider),
|
|
134
|
+
repo: cleanString(args.repo, "repo", { required: true, max: 500 })!,
|
|
135
|
+
externalKey: cleanString(args.externalKey, "externalKey", { required: true, max: 240 })!,
|
|
136
|
+
url: cleanString(args.url, "url", { max: 2000 }),
|
|
137
|
+
title: cleanString(args.title, "title", { max: 500 }),
|
|
138
|
+
status: statusArg(args.status),
|
|
139
|
+
statusSyncedAt: timestampArg(args.statusSyncedAt),
|
|
140
|
+
})) as unknown as Record<string, unknown>;
|
|
141
|
+
}
|
|
142
|
+
if (name === "relay_issue_ref_get") {
|
|
143
|
+
const issueRef = getIssueRef(cleanString(args.id, "id", { required: true, max: 120 })!);
|
|
144
|
+
if (!issueRef) throw new McpNotFoundError("issue ref not found");
|
|
145
|
+
return { issueRef };
|
|
146
|
+
}
|
|
147
|
+
if (name === "relay_issue_ref_list") {
|
|
148
|
+
return {
|
|
149
|
+
issueRefs: listIssueRefs({
|
|
150
|
+
provider: optionalEnum(args.provider, "provider", ISSUE_PROVIDERS) as IssueProvider | undefined,
|
|
151
|
+
repo: cleanString(args.repo, "repo", { max: 500 }),
|
|
152
|
+
status: statusArg(args.status),
|
|
153
|
+
limit: typeof args.limit === "number" ? args.limit : undefined,
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (name === "relay_issue_ref_update") {
|
|
158
|
+
const issueRef = updateIssueRef(cleanString(args.id, "id", { required: true, max: 120 })!, {
|
|
159
|
+
url: cleanString(args.url, "url", { max: 2000 }),
|
|
160
|
+
title: cleanString(args.title, "title", { max: 500 }),
|
|
161
|
+
status: statusArg(args.status),
|
|
162
|
+
statusSyncedAt: timestampArg(args.statusSyncedAt),
|
|
163
|
+
});
|
|
164
|
+
if (!issueRef) throw new McpNotFoundError("issue ref not found");
|
|
165
|
+
return { issueRef };
|
|
166
|
+
}
|
|
167
|
+
if (name === "relay_issue_ref_delete") {
|
|
168
|
+
const deleted = deleteIssueRef(cleanString(args.id, "id", { required: true, max: 120 })!);
|
|
169
|
+
if (!deleted) throw new McpNotFoundError("issue ref not found");
|
|
170
|
+
return { deleted };
|
|
171
|
+
}
|
|
172
|
+
if (!isRecord(args)) throw new ValidationError("arguments must be an object");
|
|
173
|
+
throw new ValidationError(`unknown tool: ${name}`);
|
|
174
|
+
}
|
package/src/mcp-team-tools.ts
CHANGED
|
@@ -11,7 +11,7 @@ const OUTCOME_STATUSES = ["success", "partial", "failed"] as const;
|
|
|
11
11
|
export const TEAM_TOOLS = [
|
|
12
12
|
{
|
|
13
13
|
name: "relay_team_get",
|
|
14
|
-
description: "Return the caller's team row, live roster, shared brief/definition of done, and any bound plan.",
|
|
14
|
+
description: "Return the caller's team row, live roster, shared brief/definition of done, associated issues, and any bound plan.",
|
|
15
15
|
requiredScopes: ["teams:read"],
|
|
16
16
|
inputSchema: {
|
|
17
17
|
type: "object",
|
package/src/mcp.ts
CHANGED
|
@@ -49,7 +49,7 @@ import { ServiceAuthError } from "./services/errors";
|
|
|
49
49
|
import { PLAN_TOOLS, relayPlanTool } from "./mcp-plan-tools";
|
|
50
50
|
import { TEAM_TOOLS, relayTeamTool } from "./mcp-team-tools";
|
|
51
51
|
import { BLACKBOARD_TOOLS, relayBlackboardTool } from "./mcp-blackboard-tools";
|
|
52
|
-
import { PROJECT_TOOLS, relayProjectTool } from "./mcp-project-tools";
|
|
52
|
+
import { PROJECT_TOOLS, relayProjectTool } from "./mcp-project-tools"; import { ISSUE_REF_TOOLS, relayIssueRefTool } from "./mcp-issue-ref-tools"; import { ISSUE_ASSOCIATION_TOOLS, relayIssueAssociationTool } from "./mcp-issue-association-tools"; import { ISSUE_ACTIVITY_TOOLS, relayIssueActivityTool } from "./mcp-issue-activity-tools"; import { ISSUE_LIFECYCLE_TOOLS, relayIssueLifecycleTool } from "./mcp-issue-lifecycle-tools";
|
|
53
53
|
import { NOTIFICATION_SUBSCRIPTION_TOOLS, relayNotificationSubscriptionTool } from "./mcp-notification-subscription-tools"; import { MERGE_TOOLS, relayMergeTool } from "./mcp-merge-tools";
|
|
54
54
|
import { PROJECT_INSTRUCTION_POLICY_NAMES, parseProjectInstructions } from "./project-instructions";
|
|
55
55
|
import { AGENT_ACTION_TOOL, relayAgentAction } from "./mcp-agent-action"; import { providerCatalogToolInputSchema } from "./mcp-provider-catalog";
|
|
@@ -461,7 +461,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
461
461
|
},
|
|
462
462
|
},
|
|
463
463
|
...MERGE_TOOLS, ...TEAM_TOOLS,
|
|
464
|
-
...PLAN_TOOLS,
|
|
464
|
+
...PLAN_TOOLS, ...ISSUE_REF_TOOLS, ...ISSUE_ASSOCIATION_TOOLS, ...ISSUE_ACTIVITY_TOOLS, ...ISSUE_LIFECYCLE_TOOLS,
|
|
465
465
|
...BLACKBOARD_TOOLS,
|
|
466
466
|
...PROJECT_TOOLS,
|
|
467
467
|
...NOTIFICATION_SUBSCRIPTION_TOOLS,
|
|
@@ -580,7 +580,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
580
580
|
else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
|
|
581
581
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
582
582
|
else if (name.startsWith("relay_blackboard_")) result = relayBlackboardTool(auth, name, args);
|
|
583
|
-
else if (name.startsWith("relay_project_")) result = relayProjectTool(auth, name, args);
|
|
583
|
+
else if (name.startsWith("relay_project_")) result = relayProjectTool(auth, name, args); else if (name.startsWith("relay_issue_ref_")) result = relayIssueRefTool(auth, name, args); else if (name.startsWith("relay_issue_association_")) result = relayIssueAssociationTool(auth, name, args); else if (name.startsWith("relay_issue_activity_")) result = relayIssueActivityTool(auth, name, args); else if (name.startsWith("relay_issue_lifecycle_")) result = await relayIssueLifecycleTool(auth, name, args);
|
|
584
584
|
else if (name === "relay_subscribe" || name === "relay_unsubscribe" || name === "relay_list_subscriptions") result = relayNotificationSubscriptionTool(auth, name, args);
|
|
585
585
|
else throw new ValidationError(`unknown tool: ${name}`);
|
|
586
586
|
|
|
@@ -123,21 +123,43 @@ export function providerConfigPublic(entry: ProviderConfigEntry, opts: { maskEnv
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Idempotently seed central provider-config rows for `host` from raw per-provider
|
|
128
|
+
* config objects (#465). A real stored row (version >= 1) always wins — a stale
|
|
129
|
+
* local file never clobbers it. Unknown providers and malformed bodies are skipped.
|
|
130
|
+
* This is the server-authority write path: callers are in-process (local migration
|
|
131
|
+
* or the orchestrator command-bus result handler), so no HTTP scope is required.
|
|
132
|
+
*/
|
|
133
|
+
export function seedProviderConfigs(host: string, rawByProvider: Record<string, unknown>): ProviderConfigEntry[] {
|
|
134
|
+
const seeded: ProviderConfigEntry[] = [];
|
|
135
|
+
for (const [provider, raw] of Object.entries(rawByProvider)) {
|
|
136
|
+
if (!VALID_PROVIDER_CONFIGS.includes(provider as SpawnProvider)) continue;
|
|
137
|
+
const key = providerConfigKey(host, provider);
|
|
138
|
+
if (getConfig(PROVIDER_CONFIG_NAMESPACE, key)) continue;
|
|
139
|
+
// Shallow-merge defaults so a partial local file (e.g. missing `command`)
|
|
140
|
+
// still validates; the relay owns the schema, the reporter stays dumb.
|
|
141
|
+
const value = { ...providerConfigDefaults(provider), ...(isRecord(raw) ? raw : {}) };
|
|
142
|
+
try {
|
|
143
|
+
const entry = setProviderConfig(provider, value, { host, updatedBy: `migration:${host}` });
|
|
144
|
+
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
145
|
+
seeded.push(entry);
|
|
146
|
+
} catch {
|
|
147
|
+
// A single invalid provider config shouldn't abort seeding the rest.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return seeded;
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
export function migrateLocalProviderConfigs(home = agentRelayHome(), host = defaultProviderConfigHost()): ProviderConfigEntry[] {
|
|
127
154
|
const dir = join(home, "providers");
|
|
128
155
|
if (!existsSync(dir)) return [];
|
|
129
|
-
const
|
|
156
|
+
const raw: Record<string, unknown> = {};
|
|
130
157
|
for (const file of readdirSync(dir)) {
|
|
131
158
|
const provider = file.endsWith(".json") ? file.slice(0, -".json".length) : "";
|
|
132
159
|
if (!VALID_PROVIDER_CONFIGS.includes(provider as SpawnProvider)) continue;
|
|
133
|
-
|
|
134
|
-
if (getConfig(PROVIDER_CONFIG_NAMESPACE, key)) continue;
|
|
135
|
-
const local = loadLocalProviderConfig(provider, home);
|
|
136
|
-
const entry = setProviderConfig(provider, local, { host, updatedBy: `migration:${host}` });
|
|
137
|
-
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
138
|
-
migrated.push(entry);
|
|
160
|
+
raw[provider] = loadLocalProviderConfig(provider, home);
|
|
139
161
|
}
|
|
140
|
-
return
|
|
162
|
+
return seedProviderConfigs(host, raw);
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
function normalizeProviderConfigHost(host: string): string {
|
package/src/routes/activity.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { emitActivityEvent } from "../sse";
|
|
|
5
5
|
import { error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
|
|
6
6
|
import { isRecord } from "agent-relay-sdk";
|
|
7
7
|
import { type ActivityEventInput, type ActivityKind } from "../types";
|
|
8
|
+
import { ISSUE_ACTIVITY_ACTIONS } from "../services/issue-activity";
|
|
8
9
|
|
|
9
|
-
const VALID_ACTIVITY_KINDS = ["message", "reply", "question", "operator", "pair", "task", "state"] as const;
|
|
10
|
+
const VALID_ACTIVITY_KINDS = ["message", "reply", "question", "operator", "pair", "task", "state", "issue"] as const;
|
|
10
11
|
|
|
11
12
|
function normalizeActivityInput(body: unknown): ActivityEventInput {
|
|
12
13
|
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
@@ -24,6 +25,8 @@ function normalizeActivityInput(body: unknown): ActivityEventInput {
|
|
|
24
25
|
pairId: cleanString(body.pairId, "pairId", { max: 120 }),
|
|
25
26
|
taskId: cleanPositiveId(body.taskId, "taskId"),
|
|
26
27
|
agentId: cleanString(body.agentId, "agentId", { max: 200 }),
|
|
28
|
+
issueRefId: cleanString(body.issueRefId, "issueRefId", { max: 120 }),
|
|
29
|
+
issueAction: optionalEnum(body.issueAction, "issueAction", ISSUE_ACTIVITY_ACTIONS),
|
|
27
30
|
metadata: cleanMeta(body.metadata),
|
|
28
31
|
};
|
|
29
32
|
}
|
|
@@ -38,6 +41,8 @@ export const getActivityEvents: Handler = (req) => {
|
|
|
38
41
|
return json(listActivityEvents({
|
|
39
42
|
operatorId: cleanString(url.searchParams.get("operatorId") ?? undefined, "operatorId", { max: 200 }),
|
|
40
43
|
agentId: cleanString(url.searchParams.get("agentId") ?? undefined, "agentId", { max: 200 }),
|
|
44
|
+
issueRefId: cleanString(url.searchParams.get("issueRefId") ?? undefined, "issueRefId", { max: 120 }),
|
|
45
|
+
issueAction: optionalEnum(url.searchParams.get("issueAction") ?? undefined, "issueAction", ISSUE_ACTIVITY_ACTIONS),
|
|
41
46
|
limit: limitRaw ?? 200,
|
|
42
47
|
since: sinceRaw ?? undefined,
|
|
43
48
|
}));
|
package/src/routes/commands.ts
CHANGED
|
@@ -13,8 +13,10 @@ import { authContextFromRequest } from "../services/auth-context";
|
|
|
13
13
|
import { commandAuthorizationResource as dispatchCommandAuthorizationResource, dispatchCommand } from "../services/dispatch-command";
|
|
14
14
|
import { ServiceAuthError } from "../services/errors";
|
|
15
15
|
import { notifyBranchLanded } from "../branch-landed";
|
|
16
|
+
import { seedProviderConfigs } from "../provider-config-store";
|
|
16
17
|
import { notifyAgentSpawnFailed } from "../agent-lifecycle-events";
|
|
17
18
|
import { handleReviewerMergeResult } from "../reviewer-pipeline";
|
|
19
|
+
import { scheduleRepoMerge } from "../workspace-auto-merge";
|
|
18
20
|
import { enqueueContinuationInjectionAfterClear } from "../self-resume";
|
|
19
21
|
import { type Command, type CommandStatus, type CreateCommandInput, type WorkspaceStatus } from "../types";
|
|
20
22
|
|
|
@@ -189,6 +191,12 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
189
191
|
// base merge can proceed (issue #157).
|
|
190
192
|
if (command.status === "succeeded" || command.status === "failed") {
|
|
191
193
|
releaseMergeLease({ commandId: command.id });
|
|
194
|
+
// #237: the lease just freed — drain the next ready/review_requested row for
|
|
195
|
+
// this repo right now instead of waiting for the backstop sweep. This is the
|
|
196
|
+
// on-settle link of the sequential per-repo drain chain (no-op if nothing else
|
|
197
|
+
// is queued, or if AUTO_MERGE is disabled).
|
|
198
|
+
const settledRepoRoot = typeof command.params.repoRoot === "string" ? command.params.repoRoot : undefined;
|
|
199
|
+
scheduleRepoMerge(settledRepoRoot);
|
|
192
200
|
}
|
|
193
201
|
if (command.status === "succeeded" && isRecord(command.result)) {
|
|
194
202
|
if (handleReviewerMergeResult(command, command.result)) {
|
|
@@ -357,6 +365,27 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
357
365
|
});
|
|
358
366
|
}
|
|
359
367
|
}
|
|
368
|
+
if (command.type === "orchestrator.migrate-provider-config" && command.status === "succeeded" && isRecord(command.result)) {
|
|
369
|
+
// The orchestrator reported its host-local provider config files; seed the
|
|
370
|
+
// central rows with server authority (#465). Idempotent — a real stored row wins.
|
|
371
|
+
const host = cleanString(command.result.host, "result.host", { max: 120 });
|
|
372
|
+
const configs = isRecord(command.result.configs) ? command.result.configs : undefined;
|
|
373
|
+
if (host && configs) {
|
|
374
|
+
const seeded = seedProviderConfigs(host, configs);
|
|
375
|
+
if (seeded.length) {
|
|
376
|
+
auditEvent({
|
|
377
|
+
clientId: `server-provider-config-migrated-${host}-${command.id}`,
|
|
378
|
+
kind: "state",
|
|
379
|
+
title: "Provider config migrated",
|
|
380
|
+
body: `${host}: ${seeded.map((entry) => entry.key.split("/").at(-1)).join(", ")}`,
|
|
381
|
+
meta: host,
|
|
382
|
+
icon: "ti-arrow-up-circle",
|
|
383
|
+
view: "orchestrators",
|
|
384
|
+
metadata: { host, commandId: command.id, providers: seeded.map((entry) => entry.key) },
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
360
389
|
settleFailedOrchestratorUpgrade(command);
|
|
361
390
|
emitCommand(command);
|
|
362
391
|
auditCommandOutcome(command);
|
package/src/routes/index.ts
CHANGED
|
@@ -28,6 +28,10 @@ import { getOrchestratorDirectories, getOrchestratorFileRead, getOrchestratorFil
|
|
|
28
28
|
import { getPairById, getPairs, postAcceptPair, postHangupPair, postPair, postPairMessage, postRejectPair } from "./pairs";
|
|
29
29
|
import { getPlanByIdRoute, getPlansRoute } from "./plans";
|
|
30
30
|
import { getBlackboardEntryRoute, getBlackboardRoute } from "./blackboard";
|
|
31
|
+
import { deleteIssueAssociationRoute, getIssueAssociationEntityIssuesRoute, getIssueAssociationIssueEntitiesRoute, postIssueAssociationRoute } from "./issue-associations";
|
|
32
|
+
import { getAgentIssueActivityRoute, getIssueActivityAgentsRoute, postIssueActivityRoute } from "./issue-activity";
|
|
33
|
+
import { postIssueLifecycleReopenRoute } from "./issue-lifecycle";
|
|
34
|
+
import { deleteIssueRefRoute, getIssueRefRoute, listIssueRefsRoute, patchIssueRefRoute, postIssueRefRoute, resolveIssueRefRoute } from "./issue-refs";
|
|
31
35
|
import { getProjectEntriesRoute, getProjectEntryRoute, getProjectRoute, getProjectsRoute } from "./projects";
|
|
32
36
|
import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, getRecipeInstances, getRecipes, postRecipeInstanceArtifacts, postRecipeStart, postRecipeStop } from "./recipes";
|
|
33
37
|
import { getSettingRoute, getSettingsNamespaceRoute, getSettingsRoute, putSettingRoute, putSettingsDefaultRoute } from "./settings";
|
|
@@ -298,6 +302,21 @@ const routes: Route[] = [
|
|
|
298
302
|
route("GET", "/api/plans/:id", getPlanByIdRoute),
|
|
299
303
|
route("GET", "/api/blackboard", getBlackboardRoute),
|
|
300
304
|
route("GET", "/api/blackboard/:id", getBlackboardEntryRoute),
|
|
305
|
+
route("POST", "/api/issue-associations", postIssueAssociationRoute),
|
|
306
|
+
route("DELETE", "/api/issue-associations", deleteIssueAssociationRoute),
|
|
307
|
+
route("GET", "/api/issue-associations/entities/:entityType/:entityId/issues", getIssueAssociationEntityIssuesRoute),
|
|
308
|
+
route("GET", "/api/issue-associations/issues/:issueRefId/entities", getIssueAssociationIssueEntitiesRoute),
|
|
309
|
+
route("POST", "/api/issue-activity", postIssueActivityRoute),
|
|
310
|
+
route("GET", "/api/issue-activity/issues", getIssueActivityAgentsRoute),
|
|
311
|
+
route("GET", "/api/issue-activity/agents/:id", getAgentIssueActivityRoute),
|
|
312
|
+
route("POST", "/api/issue-lifecycle/reopen", postIssueLifecycleReopenRoute),
|
|
313
|
+
route("POST", "/api/issue-refs", postIssueRefRoute),
|
|
314
|
+
route("GET", "/api/issue-refs", listIssueRefsRoute),
|
|
315
|
+
route("GET", "/api/issue-refs/resolve", resolveIssueRefRoute),
|
|
316
|
+
route("POST", "/api/issue-refs/resolve", resolveIssueRefRoute),
|
|
317
|
+
route("GET", "/api/issue-refs/:id", getIssueRefRoute),
|
|
318
|
+
route("PATCH", "/api/issue-refs/:id", patchIssueRefRoute),
|
|
319
|
+
route("DELETE", "/api/issue-refs/:id", deleteIssueRefRoute),
|
|
301
320
|
route("GET", "/api/projects", getProjectsRoute),
|
|
302
321
|
route("GET", "/api/projects/:id", getProjectRoute),
|
|
303
322
|
route("GET", "/api/projects/:id/entries", getProjectEntriesRoute),
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ValidationError } from "../db";
|
|
2
|
+
import { getIssueRef } from "../services/issue-refs";
|
|
3
|
+
import {
|
|
4
|
+
ISSUE_ACTIVITY_ACTIONS,
|
|
5
|
+
listAgentIssueActivity,
|
|
6
|
+
listIssueActivityAgents,
|
|
7
|
+
recordAndEmitIssueActivity,
|
|
8
|
+
type IssueActivityAction,
|
|
9
|
+
} from "../services/issue-activity";
|
|
10
|
+
import { cleanEnum, cleanMeta, cleanString, optionalEnum } from "../validation";
|
|
11
|
+
import { error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
|
|
12
|
+
import { ISSUE_PROVIDERS, isRecord, type IssueProvider } from "../types";
|
|
13
|
+
|
|
14
|
+
function cleanProvider(value: unknown): IssueProvider | undefined {
|
|
15
|
+
return optionalEnum(value, "provider", ISSUE_PROVIDERS) as IssueProvider | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cleanAction(value: unknown): IssueActivityAction {
|
|
19
|
+
return cleanEnum(value, "action", ISSUE_ACTIVITY_ACTIONS) as IssueActivityAction;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanLimit(req: Request): number | undefined | Response {
|
|
23
|
+
const url = new URL(req.url);
|
|
24
|
+
const rawLimit = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500, clamp: true });
|
|
25
|
+
if (Number.isNaN(rawLimit)) return error("limit must be an integer");
|
|
26
|
+
return rawLimit ?? undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const postIssueActivityRoute: Handler = async (req) => {
|
|
30
|
+
const parsed = await parseBody<unknown>(req);
|
|
31
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
32
|
+
try {
|
|
33
|
+
if (!isRecord(parsed.body)) throw new ValidationError("issue activity body required");
|
|
34
|
+
const event = recordAndEmitIssueActivity({
|
|
35
|
+
agentId: cleanString(parsed.body.agentId, "agentId", { required: true, max: 200 })!,
|
|
36
|
+
action: cleanAction(parsed.body.action),
|
|
37
|
+
issueRefId: cleanString(parsed.body.issueRefId, "issueRefId", { max: 120 }),
|
|
38
|
+
issueRef: cleanString(parsed.body.issueRef ?? parsed.body.ref, "issueRef", { max: 1000 }),
|
|
39
|
+
defaultRepo: cleanString(parsed.body.defaultRepo ?? parsed.body.repo, "defaultRepo", { max: 500 }),
|
|
40
|
+
provider: cleanProvider(parsed.body.provider),
|
|
41
|
+
clientId: cleanString(parsed.body.clientId, "clientId", { max: 240 }),
|
|
42
|
+
title: cleanString(parsed.body.title, "title", { max: 200 }),
|
|
43
|
+
body: cleanString(parsed.body.body, "body", { max: 1000 }),
|
|
44
|
+
metadata: cleanMeta(parsed.body.metadata),
|
|
45
|
+
});
|
|
46
|
+
return json({ event }, 201);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err instanceof ValidationError) return error(err.message, 400);
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const getIssueActivityAgentsRoute: Handler = (req) => {
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(req.url);
|
|
56
|
+
const limit = cleanLimit(req);
|
|
57
|
+
if (limit instanceof Response) return limit;
|
|
58
|
+
const issueRefId = cleanString(url.searchParams.get("issueRefId") ?? undefined, "issueRefId", { required: true, max: 120 })!;
|
|
59
|
+
const issueRef = getIssueRef(issueRefId);
|
|
60
|
+
if (!issueRef) return error("issue ref not found", 404);
|
|
61
|
+
return json({ issueRef, agents: listIssueActivityAgents(issueRef.id, { limit }) });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err instanceof ValidationError) return error(err.message, 400);
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const getAgentIssueActivityRoute: Handler = (req, params) => {
|
|
69
|
+
try {
|
|
70
|
+
const limit = cleanLimit(req);
|
|
71
|
+
if (limit instanceof Response) return limit;
|
|
72
|
+
const agentId = cleanString(params.id, "agentId", { required: true, max: 200 })!;
|
|
73
|
+
return json({ agentId, issues: listAgentIssueActivity(agentId, { limit }) });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err instanceof ValidationError) return error(err.message, 400);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ISSUE_ASSOCIATION_ENTITY_TYPES, ISSUE_PROVIDERS, isRecord, type IssueAssociationEntityType, type IssueProvider } from "../types";
|
|
2
|
+
import { associateIssueRef, listIssueEntities, listIssueRefsForEntity, removeIssueAssociation, resolveAssociationIssueRef } from "../services/issue-associations";
|
|
3
|
+
import { cleanEnum, cleanString, optionalEnum } from "../validation";
|
|
4
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
5
|
+
|
|
6
|
+
function cleanEntityType(value: unknown): IssueAssociationEntityType {
|
|
7
|
+
return cleanEnum(value, "entityType", ISSUE_ASSOCIATION_ENTITY_TYPES) as IssueAssociationEntityType;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cleanIssueInput(value: Record<string, unknown>) {
|
|
11
|
+
return {
|
|
12
|
+
issueRefId: cleanString(value.issueRefId, "issueRefId", { max: 120 }),
|
|
13
|
+
ref: cleanString(value.ref, "ref", { max: 1000 }),
|
|
14
|
+
defaultRepo: cleanString(value.defaultRepo, "defaultRepo", { max: 500 }),
|
|
15
|
+
provider: optionalEnum(value.provider, "provider", ISSUE_PROVIDERS) as IssueProvider | undefined,
|
|
16
|
+
repo: cleanString(value.repo, "repo", { max: 500 }),
|
|
17
|
+
externalKey: cleanString(value.externalKey, "externalKey", { max: 240 }),
|
|
18
|
+
url: cleanString(value.url, "url", { max: 2000 }),
|
|
19
|
+
title: cleanString(value.title, "title", { max: 500 }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanAssociationBody(body: unknown) {
|
|
24
|
+
if (!isRecord(body)) throw new Error("issue association body required");
|
|
25
|
+
return {
|
|
26
|
+
...cleanIssueInput(body),
|
|
27
|
+
entityType: cleanEntityType(body.entityType),
|
|
28
|
+
entityId: cleanString(body.entityId, "entityId", { required: true, max: 240 })!,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function routeError(err: unknown): Response {
|
|
33
|
+
return error(err instanceof Error ? err.message : "issue associations route failed");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const postIssueAssociationRoute: Handler = async (req) => {
|
|
37
|
+
const parsed = await parseBody<unknown>(req);
|
|
38
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
39
|
+
try {
|
|
40
|
+
return json(associateIssueRef(cleanAssociationBody(parsed.body)));
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return routeError(err);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const getIssueAssociationEntityIssuesRoute: Handler = (_req, params) => {
|
|
47
|
+
try {
|
|
48
|
+
return json({
|
|
49
|
+
issues: listIssueRefsForEntity(
|
|
50
|
+
cleanEntityType(params.entityType),
|
|
51
|
+
cleanString(params.entityId, "entityId", { required: true, max: 240 })!,
|
|
52
|
+
),
|
|
53
|
+
});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return routeError(err);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getIssueAssociationIssueEntitiesRoute: Handler = (_req, params) => {
|
|
60
|
+
try {
|
|
61
|
+
const issueRef = resolveAssociationIssueRef({ issueRefId: cleanString(params.issueRefId, "issueRefId", { required: true, max: 120 })! });
|
|
62
|
+
return json({ issueRef, entities: listIssueEntities(issueRef.id) });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return routeError(err);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const deleteIssueAssociationRoute: Handler = async (req) => {
|
|
69
|
+
const parsed = await parseBody<unknown>(req);
|
|
70
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
71
|
+
try {
|
|
72
|
+
return json(removeIssueAssociation(cleanAssociationBody(parsed.body)));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return routeError(err);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { reopenIssueLifecycle } from "../services/issue-lifecycle";
|
|
2
|
+
import { cleanEnum, cleanString, optionalEnum } from "../validation";
|
|
3
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
4
|
+
import { ISSUE_ASSOCIATION_ENTITY_TYPES, ISSUE_PROVIDERS, isRecord, type IssueAssociationEntityType, type IssueProvider } from "../types";
|
|
5
|
+
|
|
6
|
+
function cleanReopenBody(body: unknown) {
|
|
7
|
+
if (!isRecord(body)) throw new Error("issue lifecycle reopen body required");
|
|
8
|
+
return {
|
|
9
|
+
issueRefId: cleanString(body.issueRefId, "issueRefId", { max: 120 }),
|
|
10
|
+
ref: cleanString(body.ref, "ref", { max: 1000 }),
|
|
11
|
+
defaultRepo: cleanString(body.defaultRepo ?? body.repo, "defaultRepo", { max: 500 }),
|
|
12
|
+
provider: optionalEnum(body.provider, "provider", ISSUE_PROVIDERS) as IssueProvider | undefined,
|
|
13
|
+
repo: cleanString(body.repo, "repo", { max: 500 }),
|
|
14
|
+
externalKey: cleanString(body.externalKey, "externalKey", { max: 240 }),
|
|
15
|
+
url: cleanString(body.url, "url", { max: 2000 }),
|
|
16
|
+
title: cleanString(body.title, "title", { max: 500 }),
|
|
17
|
+
comment: cleanString(body.comment, "comment", { max: 2000 }),
|
|
18
|
+
agentId: cleanString(body.agentId, "agentId", { max: 200 }),
|
|
19
|
+
entityType: body.entityType === undefined ? undefined : cleanEnum(body.entityType, "entityType", ISSUE_ASSOCIATION_ENTITY_TYPES) as IssueAssociationEntityType,
|
|
20
|
+
entityId: cleanString(body.entityId, "entityId", { max: 240 }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const postIssueLifecycleReopenRoute: Handler = async (req) => {
|
|
25
|
+
const parsed = await parseBody<unknown>(req);
|
|
26
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
27
|
+
try {
|
|
28
|
+
const result = await reopenIssueLifecycle(cleanReopenBody(parsed.body));
|
|
29
|
+
if (result.action === "failed") return error(result.error, 502);
|
|
30
|
+
return json(result);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return error(err instanceof Error ? err.message : "issue lifecycle route failed");
|
|
33
|
+
}
|
|
34
|
+
};
|