@a5c-ai/tasks-adapter 5.1.1-staging.00ceebd28cf2
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/README.md +125 -0
- package/dist/auth/forge-interface.d.ts +67 -0
- package/dist/auth/forge-interface.d.ts.map +1 -0
- package/dist/auth/forge-interface.js +69 -0
- package/dist/auth/github-app.d.ts +64 -0
- package/dist/auth/github-app.d.ts.map +1 -0
- package/dist/auth/github-app.js +141 -0
- package/dist/auth/github-oauth.d.ts +27 -0
- package/dist/auth/github-oauth.d.ts.map +1 -0
- package/dist/auth/github-oauth.js +89 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/jwt.d.ts +24 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/jwt.js +43 -0
- package/dist/auth/middleware.d.ts +22 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +36 -0
- package/dist/auth/ssh-keys.d.ts +21 -0
- package/dist/auth/ssh-keys.d.ts.map +1 -0
- package/dist/auth/ssh-keys.js +59 -0
- package/dist/auth/types.d.ts +165 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +53 -0
- package/dist/backend.d.ts +248 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +40 -0
- package/dist/backends/adapters.d.ts +99 -0
- package/dist/backends/adapters.d.ts.map +1 -0
- package/dist/backends/adapters.js +308 -0
- package/dist/backends/external-tracker.d.ts +133 -0
- package/dist/backends/external-tracker.d.ts.map +1 -0
- package/dist/backends/external-tracker.js +731 -0
- package/dist/backends/git-native.d.ts +69 -0
- package/dist/backends/git-native.d.ts.map +1 -0
- package/dist/backends/git-native.js +797 -0
- package/dist/backends/github-issues.d.ts +78 -0
- package/dist/backends/github-issues.d.ts.map +1 -0
- package/dist/backends/github-issues.js +806 -0
- package/dist/backends/index.d.ts +52 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +151 -0
- package/dist/backends/server.d.ts +42 -0
- package/dist/backends/server.d.ts.map +1 -0
- package/dist/backends/server.js +305 -0
- package/dist/cli/auth-store.d.ts +49 -0
- package/dist/cli/auth-store.d.ts.map +1 -0
- package/dist/cli/auth-store.js +150 -0
- package/dist/cli/client-config.d.ts +10 -0
- package/dist/cli/client-config.d.ts.map +1 -0
- package/dist/cli/client-config.js +87 -0
- package/dist/cli/commands/ask.d.ts +3 -0
- package/dist/cli/commands/ask.d.ts.map +1 -0
- package/dist/cli/commands/ask.js +171 -0
- package/dist/cli/commands/auth.d.ts +3 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +510 -0
- package/dist/cli/commands/breakpoints.d.ts +3 -0
- package/dist/cli/commands/breakpoints.d.ts.map +1 -0
- package/dist/cli/commands/breakpoints.js +311 -0
- package/dist/cli/commands/responder-loop.d.ts +3 -0
- package/dist/cli/commands/responder-loop.d.ts.map +1 -0
- package/dist/cli/commands/responder-loop.js +78 -0
- package/dist/cli/commands/responders.d.ts +3 -0
- package/dist/cli/commands/responders.d.ts.map +1 -0
- package/dist/cli/commands/responders.js +157 -0
- package/dist/cli/commands/rules.d.ts +3 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +105 -0
- package/dist/cli/commands/server.d.ts +3 -0
- package/dist/cli/commands/server.d.ts.map +1 -0
- package/dist/cli/commands/server.js +34 -0
- package/dist/cli/commands/tasks.d.ts +3 -0
- package/dist/cli/commands/tasks.d.ts.map +1 -0
- package/dist/cli/commands/tasks.js +281 -0
- package/dist/cli/commands/templates.d.ts +3 -0
- package/dist/cli/commands/templates.d.ts.map +1 -0
- package/dist/cli/commands/templates.js +100 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/output.d.ts +26 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +143 -0
- package/dist/cli/program.d.ts +6 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +38 -0
- package/dist/cli/tasks-adapter.d.ts +3 -0
- package/dist/cli/tasks-adapter.d.ts.map +1 -0
- package/dist/cli/tasks-adapter.js +4 -0
- package/dist/client/answer-poller.d.ts +52 -0
- package/dist/client/answer-poller.d.ts.map +1 -0
- package/dist/client/answer-poller.js +200 -0
- package/dist/client/auth-client.d.ts +200 -0
- package/dist/client/auth-client.d.ts.map +1 -0
- package/dist/client/auth-client.js +309 -0
- package/dist/client/breakpoint-router.d.ts +45 -0
- package/dist/client/breakpoint-router.d.ts.map +1 -0
- package/dist/client/breakpoint-router.js +45 -0
- package/dist/client/index.d.ts +17 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +16 -0
- package/dist/client/profile-validator.d.ts +34 -0
- package/dist/client/profile-validator.d.ts.map +1 -0
- package/dist/client/profile-validator.js +89 -0
- package/dist/client/responder-client.d.ts +39 -0
- package/dist/client/responder-client.d.ts.map +1 -0
- package/dist/client/responder-client.js +72 -0
- package/dist/client/responder-matcher.d.ts +49 -0
- package/dist/client/responder-matcher.d.ts.map +1 -0
- package/dist/client/responder-matcher.js +226 -0
- package/dist/client/server-client.d.ts +124 -0
- package/dist/client/server-client.d.ts.map +1 -0
- package/dist/client/server-client.js +266 -0
- package/dist/client/timeout-manager.d.ts +47 -0
- package/dist/client/timeout-manager.d.ts.map +1 -0
- package/dist/client/timeout-manager.js +77 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +93 -0
- package/dist/harness/index.d.ts +4 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +2 -0
- package/dist/harness/interaction-provider.d.ts +71 -0
- package/dist/harness/interaction-provider.d.ts.map +1 -0
- package/dist/harness/interaction-provider.js +124 -0
- package/dist/harness/routing-rules.d.ts +7 -0
- package/dist/harness/routing-rules.d.ts.map +1 -0
- package/dist/harness/routing-rules.js +37 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/mcp/backend-resolver.d.ts +43 -0
- package/dist/mcp/backend-resolver.d.ts.map +1 -0
- package/dist/mcp/backend-resolver.js +111 -0
- package/dist/mcp/http-transport.d.ts +37 -0
- package/dist/mcp/http-transport.d.ts.map +1 -0
- package/dist/mcp/http-transport.js +103 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +12 -0
- package/dist/mcp/server.d.ts +20 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +259 -0
- package/dist/mcp/tools/answer-breakpoint.d.ts +32 -0
- package/dist/mcp/tools/answer-breakpoint.d.ts.map +1 -0
- package/dist/mcp/tools/answer-breakpoint.js +45 -0
- package/dist/mcp/tools/ask-breakpoint.d.ts +58 -0
- package/dist/mcp/tools/ask-breakpoint.d.ts.map +1 -0
- package/dist/mcp/tools/ask-breakpoint.js +78 -0
- package/dist/mcp/tools/check-status.d.ts +16 -0
- package/dist/mcp/tools/check-status.d.ts.map +1 -0
- package/dist/mcp/tools/check-status.js +18 -0
- package/dist/mcp/tools/claim-breakpoint.d.ts +18 -0
- package/dist/mcp/tools/claim-breakpoint.d.ts.map +1 -0
- package/dist/mcp/tools/claim-breakpoint.js +28 -0
- package/dist/mcp/tools/list-breakpoints.d.ts +16 -0
- package/dist/mcp/tools/list-breakpoints.d.ts.map +1 -0
- package/dist/mcp/tools/list-breakpoints.js +14 -0
- package/dist/mcp/tools/list-responders.d.ts +18 -0
- package/dist/mcp/tools/list-responders.d.ts.map +1 -0
- package/dist/mcp/tools/list-responders.js +37 -0
- package/dist/mcp/tools/native-tasks.d.ts +270 -0
- package/dist/mcp/tools/native-tasks.d.ts.map +1 -0
- package/dist/mcp/tools/native-tasks.js +481 -0
- package/dist/mcp/tools/poll-breakpoints.d.ts +18 -0
- package/dist/mcp/tools/poll-breakpoints.d.ts.map +1 -0
- package/dist/mcp/tools/poll-breakpoints.js +36 -0
- package/dist/mcp/tools/verify-answer.d.ts +16 -0
- package/dist/mcp/tools/verify-answer.d.ts.map +1 -0
- package/dist/mcp/tools/verify-answer.js +38 -0
- package/dist/proven/index.d.ts +5 -0
- package/dist/proven/index.d.ts.map +1 -0
- package/dist/proven/index.js +3 -0
- package/dist/proven/keys.d.ts +33 -0
- package/dist/proven/keys.d.ts.map +1 -0
- package/dist/proven/keys.js +117 -0
- package/dist/proven/sign.d.ts +16 -0
- package/dist/proven/sign.d.ts.map +1 -0
- package/dist/proven/sign.js +60 -0
- package/dist/proven/types.d.ts +26 -0
- package/dist/proven/types.d.ts.map +1 -0
- package/dist/proven/types.js +5 -0
- package/dist/proven/verify.d.ts +6 -0
- package/dist/proven/verify.d.ts.map +1 -0
- package/dist/proven/verify.js +58 -0
- package/dist/responders/types.d.ts +38 -0
- package/dist/responders/types.d.ts.map +1 -0
- package/dist/responders/types.js +1 -0
- package/dist/router.d.ts +51 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +200 -0
- package/dist/types.d.ts +7711 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +479 -0
- package/package.json +96 -0
- package/responder/README.md +42 -0
- package/responder/backend-responder.json +9 -0
- package/responder/devops-responder.json +9 -0
- package/responder/frontend-responder.json +9 -0
- package/responder/schema.json +89 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { selectBreakpointAnswer, unsupportedBackendFeatureMessage, unsupportedBreakpointBackendCapabilities, } from "../backend.js";
|
|
2
|
+
import { GitHubIssuesBackend } from "./github-issues.js";
|
|
3
|
+
import { BreakpointSchema, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, } from "../types.js";
|
|
4
|
+
const TRACKER_ANSWER_MARKER = "tasks-adapter:tracker-answer:v1";
|
|
5
|
+
const SECRET_KEYS = [
|
|
6
|
+
"token",
|
|
7
|
+
"authorization",
|
|
8
|
+
"auth",
|
|
9
|
+
"secret",
|
|
10
|
+
"password",
|
|
11
|
+
"apiKey",
|
|
12
|
+
"apiToken",
|
|
13
|
+
"webhookSecret",
|
|
14
|
+
];
|
|
15
|
+
function nowIso() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function encodeRef(provider, externalId) {
|
|
19
|
+
return `tracker-${provider}-${Buffer.from(externalId, "utf8").toString("base64url")}`;
|
|
20
|
+
}
|
|
21
|
+
function decodeRef(id) {
|
|
22
|
+
const match = /^tracker-(github-issues|jira|linear|generic-rest)-(.+)$/.exec(id);
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error(`Unknown external tracker breakpoint ID: ${id}`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
provider: match[1],
|
|
28
|
+
externalId: Buffer.from(match[2], "base64url").toString("utf8"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function readString(value) {
|
|
32
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function readStringArray(value) {
|
|
35
|
+
if (!Array.isArray(value))
|
|
36
|
+
return [];
|
|
37
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
38
|
+
}
|
|
39
|
+
function getPath(source, path) {
|
|
40
|
+
if (!path)
|
|
41
|
+
return undefined;
|
|
42
|
+
let current = source;
|
|
43
|
+
for (const part of path.split(".")) {
|
|
44
|
+
if (current === null || typeof current !== "object")
|
|
45
|
+
return undefined;
|
|
46
|
+
current = current[part];
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
function env(name) {
|
|
51
|
+
if (!name)
|
|
52
|
+
return undefined;
|
|
53
|
+
const value = process.env[name];
|
|
54
|
+
return value && value.length > 0 ? value : undefined;
|
|
55
|
+
}
|
|
56
|
+
function asRecord(value) {
|
|
57
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
58
|
+
}
|
|
59
|
+
function getTrackerString(config, key) {
|
|
60
|
+
return readString(asRecord(config.tracker)[key]);
|
|
61
|
+
}
|
|
62
|
+
function getTrackerNumber(config, key) {
|
|
63
|
+
const value = asRecord(config.tracker)[key];
|
|
64
|
+
return typeof value === "number" ? value : undefined;
|
|
65
|
+
}
|
|
66
|
+
function normalizeStatus(rawStatus, config) {
|
|
67
|
+
if (!rawStatus)
|
|
68
|
+
return "open";
|
|
69
|
+
const mapped = config.statusMapping?.[rawStatus] ?? config.statusMapping?.[rawStatus.toLowerCase()];
|
|
70
|
+
if (mapped)
|
|
71
|
+
return mapped;
|
|
72
|
+
const normalized = rawStatus.toLowerCase();
|
|
73
|
+
if (normalized === "done" || normalized === "closed" || normalized === "resolved")
|
|
74
|
+
return "completed";
|
|
75
|
+
if (normalized === "cancelled" || normalized === "canceled")
|
|
76
|
+
return "cancelled";
|
|
77
|
+
if (normalized === "answered")
|
|
78
|
+
return "answered";
|
|
79
|
+
if (normalized === "claimed" || normalized === "in progress")
|
|
80
|
+
return "claimed";
|
|
81
|
+
return "open";
|
|
82
|
+
}
|
|
83
|
+
export function redactExternalTrackerSecrets(value) {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.map((item) => redactExternalTrackerSecrets(item));
|
|
86
|
+
}
|
|
87
|
+
if (!value || typeof value !== "object") {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
const redacted = {};
|
|
91
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
92
|
+
const isSecret = SECRET_KEYS.some((secretKey) => key.toLowerCase().includes(secretKey.toLowerCase()));
|
|
93
|
+
redacted[key] = isSecret ? "[REDACTED]" : redactExternalTrackerSecrets(nested);
|
|
94
|
+
}
|
|
95
|
+
return redacted;
|
|
96
|
+
}
|
|
97
|
+
function buildAnswerBlock(answer) {
|
|
98
|
+
const payload = {
|
|
99
|
+
version: 1,
|
|
100
|
+
schema: "tasks-adapter:tracker-answer",
|
|
101
|
+
responderId: answer.responderId,
|
|
102
|
+
responderName: answer.responderName,
|
|
103
|
+
text: answer.text,
|
|
104
|
+
confidence: answer.confidence,
|
|
105
|
+
references: answer.references,
|
|
106
|
+
};
|
|
107
|
+
return [
|
|
108
|
+
"## Answer",
|
|
109
|
+
"",
|
|
110
|
+
answer.text,
|
|
111
|
+
"",
|
|
112
|
+
`<!-- ${TRACKER_ANSWER_MARKER}`,
|
|
113
|
+
JSON.stringify(payload, null, 2),
|
|
114
|
+
"-->",
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
function answerFromComment(comment, breakpointId) {
|
|
118
|
+
const marker = `<!-- ${TRACKER_ANSWER_MARKER}`;
|
|
119
|
+
const markerIndex = comment.body.indexOf(marker);
|
|
120
|
+
let text = comment.body.trim();
|
|
121
|
+
let confidence;
|
|
122
|
+
let references;
|
|
123
|
+
if (markerIndex >= 0) {
|
|
124
|
+
text = comment.body.slice(0, markerIndex).replace(/^## Answer\s*/m, "").trim();
|
|
125
|
+
const jsonStart = comment.body.indexOf("\n", markerIndex);
|
|
126
|
+
const jsonEnd = comment.body.indexOf("-->", jsonStart);
|
|
127
|
+
if (jsonStart >= 0 && jsonEnd >= 0) {
|
|
128
|
+
try {
|
|
129
|
+
const payload = JSON.parse(comment.body.slice(jsonStart, jsonEnd).trim());
|
|
130
|
+
text = readString(payload.text) ?? text;
|
|
131
|
+
confidence = typeof payload.confidence === "number" ? payload.confidence : undefined;
|
|
132
|
+
references = readStringArray(payload.references);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Ignore malformed provider comments and fall back to visible text.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (!comment.body.includes("## Answer")) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
text = comment.body.replace(/^## Answer\s*/m, "").trim();
|
|
144
|
+
}
|
|
145
|
+
if (text.length === 0)
|
|
146
|
+
return null;
|
|
147
|
+
return {
|
|
148
|
+
id: `tracker-comment-${comment.id}`,
|
|
149
|
+
breakpointId,
|
|
150
|
+
responderId: comment.authorId,
|
|
151
|
+
responderName: comment.authorName,
|
|
152
|
+
text,
|
|
153
|
+
confidence: confidence ?? 80,
|
|
154
|
+
references: references ?? [],
|
|
155
|
+
followUpQuestions: [],
|
|
156
|
+
answeredAt: comment.createdAt,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
class HttpTrackerAdapter {
|
|
160
|
+
config;
|
|
161
|
+
constructor(config) {
|
|
162
|
+
this.config = config;
|
|
163
|
+
}
|
|
164
|
+
async requestJson(pathOrUrl, init = {}) {
|
|
165
|
+
const baseUrl = getTrackerString(this.config, "baseUrl") ?? "";
|
|
166
|
+
const url = pathOrUrl.startsWith("http") ? pathOrUrl : `${baseUrl}${pathOrUrl}`;
|
|
167
|
+
const headers = {
|
|
168
|
+
Accept: "application/json",
|
|
169
|
+
...init.headers,
|
|
170
|
+
};
|
|
171
|
+
if (init.body)
|
|
172
|
+
headers["Content-Type"] = "application/json";
|
|
173
|
+
this.applyAuth(headers);
|
|
174
|
+
const response = await fetch(url, { ...init, headers });
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
const body = await response.text();
|
|
177
|
+
throw new Error(`${this.provider} tracker request failed (${response.status}): ${body}`);
|
|
178
|
+
}
|
|
179
|
+
if (response.status === 204)
|
|
180
|
+
return {};
|
|
181
|
+
const text = await response.text();
|
|
182
|
+
return text.length > 0 ? JSON.parse(text) : {};
|
|
183
|
+
}
|
|
184
|
+
applyAuth(headers) {
|
|
185
|
+
const token = env(this.config.auth?.tokenEnv) ?? env(this.config.auth?.apiTokenEnv);
|
|
186
|
+
if (token)
|
|
187
|
+
headers.Authorization = `Bearer ${token}`;
|
|
188
|
+
}
|
|
189
|
+
mapIssue(raw, fallbackId) {
|
|
190
|
+
const mapping = this.config.fieldMapping;
|
|
191
|
+
const record = asRecord(raw);
|
|
192
|
+
const id = readString(getPath(record, mapping?.externalId)) ?? readString(record.id) ?? fallbackId ?? "";
|
|
193
|
+
const key = readString(getPath(record, mapping?.externalKey)) ?? readString(record.key);
|
|
194
|
+
const title = readString(getPath(record, mapping?.title)) ?? readString(record.title) ?? readString(record.summary) ?? key ?? id;
|
|
195
|
+
const description = readString(getPath(record, mapping?.description)) ?? readString(record.description);
|
|
196
|
+
const status = normalizeStatus(readString(getPath(record, mapping?.status)) ?? readString(record.status), this.config);
|
|
197
|
+
const labels = readStringArray(getPath(record, mapping?.labels) ?? record.labels);
|
|
198
|
+
const assignees = readStringArray(getPath(record, mapping?.assignee) ?? record.assignees);
|
|
199
|
+
const createdAt = readString(record.createdAt) ?? readString(record.created_at) ?? nowIso();
|
|
200
|
+
const updatedAt = readString(getPath(record, mapping?.updatedAt)) ?? readString(record.updatedAt) ?? readString(record.updated_at) ?? createdAt;
|
|
201
|
+
const url = readString(getPath(record, mapping?.url)) ?? readString(record.url);
|
|
202
|
+
return {
|
|
203
|
+
ref: { provider: this.provider, id, key, url },
|
|
204
|
+
title,
|
|
205
|
+
description,
|
|
206
|
+
status,
|
|
207
|
+
labels,
|
|
208
|
+
assignees,
|
|
209
|
+
createdAt,
|
|
210
|
+
updatedAt,
|
|
211
|
+
comments: this.mapComments(record.comments),
|
|
212
|
+
metadata: redactExternalTrackerSecrets(record),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
mapComments(raw) {
|
|
216
|
+
if (!Array.isArray(raw))
|
|
217
|
+
return [];
|
|
218
|
+
return raw.map((item, index) => {
|
|
219
|
+
const record = asRecord(item);
|
|
220
|
+
const author = asRecord(record.author ?? record.user);
|
|
221
|
+
return {
|
|
222
|
+
id: readString(record.id) ?? String(index),
|
|
223
|
+
authorId: readString(record.authorId) ?? readString(author.id) ?? readString(author.login) ?? "unknown",
|
|
224
|
+
authorName: readString(record.authorName) ?? readString(author.name) ?? readString(author.login) ?? "unknown",
|
|
225
|
+
body: readString(record.body) ?? readString(record.text) ?? "",
|
|
226
|
+
createdAt: readString(record.createdAt) ?? readString(record.created_at) ?? nowIso(),
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
normalizeWebhook(payload) {
|
|
231
|
+
const record = asRecord(payload);
|
|
232
|
+
const issueSource = record.issue ?? record.data ?? record;
|
|
233
|
+
const issue = this.mapIssue(issueSource);
|
|
234
|
+
if (!issue.ref.id)
|
|
235
|
+
return null;
|
|
236
|
+
const comment = record.comment ? this.mapComments([record.comment])[0] : undefined;
|
|
237
|
+
return {
|
|
238
|
+
provider: this.provider,
|
|
239
|
+
eventId: readString(record.eventId) ?? readString(record.id) ?? `${issue.ref.id}:${issue.updatedAt}:${comment?.id ?? "issue"}`,
|
|
240
|
+
action: comment ? "comment.created" : "issue.updated",
|
|
241
|
+
issue,
|
|
242
|
+
comment,
|
|
243
|
+
receivedAt: nowIso(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export class JiraTrackerAdapter extends HttpTrackerAdapter {
|
|
248
|
+
provider = "jira";
|
|
249
|
+
async createIssue(input) {
|
|
250
|
+
const projectKey = getTrackerString(this.config, "projectKey");
|
|
251
|
+
const issueType = getTrackerString(this.config, "issueType") ?? "Task";
|
|
252
|
+
const fields = {
|
|
253
|
+
project: projectKey ? { key: projectKey } : undefined,
|
|
254
|
+
issuetype: { name: issueType },
|
|
255
|
+
summary: input.title,
|
|
256
|
+
description: input.description,
|
|
257
|
+
labels: input.labels,
|
|
258
|
+
...input.fields,
|
|
259
|
+
};
|
|
260
|
+
const raw = await this.requestJson("/rest/api/3/issue", {
|
|
261
|
+
method: "POST",
|
|
262
|
+
body: JSON.stringify({ fields: redactExternalTrackerSecrets(fields) }),
|
|
263
|
+
});
|
|
264
|
+
return this.mapIssue(raw);
|
|
265
|
+
}
|
|
266
|
+
async getIssue(ref) {
|
|
267
|
+
const key = encodeURIComponent(ref.key ?? ref.id);
|
|
268
|
+
const raw = await this.requestJson(`/rest/api/3/issue/${key}?expand=renderedFields,comments`);
|
|
269
|
+
return this.mapIssue(raw, ref.id);
|
|
270
|
+
}
|
|
271
|
+
async listIssues() {
|
|
272
|
+
const jql = getTrackerString(this.config, "jql") ?? "";
|
|
273
|
+
const maxResults = getTrackerNumber(this.config, "maxResults") ?? 50;
|
|
274
|
+
const raw = await this.requestJson(`/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}`);
|
|
275
|
+
const issues = asRecord(raw).issues;
|
|
276
|
+
return Array.isArray(issues) ? issues.map((issue) => this.mapIssue(issue)) : [];
|
|
277
|
+
}
|
|
278
|
+
async addComment(ref, answer) {
|
|
279
|
+
const key = encodeURIComponent(ref.key ?? ref.id);
|
|
280
|
+
const raw = await this.requestJson(`/rest/api/3/issue/${key}/comment`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: JSON.stringify({ body: buildAnswerBlock(answer) }),
|
|
283
|
+
});
|
|
284
|
+
return this.mapComments([raw])[0];
|
|
285
|
+
}
|
|
286
|
+
async transitionIssue(ref, status) {
|
|
287
|
+
const transitions = asRecord(asRecord(this.config.tracker).transitions);
|
|
288
|
+
const transitionId = readString(transitions[status]);
|
|
289
|
+
if (!transitionId)
|
|
290
|
+
return;
|
|
291
|
+
const key = encodeURIComponent(ref.key ?? ref.id);
|
|
292
|
+
await this.requestJson(`/rest/api/3/issue/${key}/transitions`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
body: JSON.stringify({ transition: { id: transitionId } }),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export class LinearTrackerAdapter extends HttpTrackerAdapter {
|
|
299
|
+
provider = "linear";
|
|
300
|
+
applyAuth(headers) {
|
|
301
|
+
const token = env(this.config.auth?.tokenEnv) ?? env(this.config.auth?.apiTokenEnv);
|
|
302
|
+
if (token)
|
|
303
|
+
headers.Authorization = token;
|
|
304
|
+
}
|
|
305
|
+
async graphql(query, variables) {
|
|
306
|
+
const raw = await this.requestJson("/graphql", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
body: JSON.stringify({ query, variables: redactExternalTrackerSecrets(variables) }),
|
|
309
|
+
});
|
|
310
|
+
return asRecord(asRecord(raw).data);
|
|
311
|
+
}
|
|
312
|
+
async createIssue(input) {
|
|
313
|
+
const teamId = getTrackerString(this.config, "teamId");
|
|
314
|
+
const data = await this.graphql(`mutation CreateIssue($input: IssueCreateInput!) {
|
|
315
|
+
issueCreate(input: $input) { success issue { id identifier title url state { name } createdAt updatedAt } }
|
|
316
|
+
}`, { input: { teamId, title: input.title, description: input.description, labelIds: input.labels, ...input.fields } });
|
|
317
|
+
return this.mapIssue(asRecord(asRecord(data.issueCreate).issue));
|
|
318
|
+
}
|
|
319
|
+
async getIssue(ref) {
|
|
320
|
+
const data = await this.graphql(`query Issue($id: String!) {
|
|
321
|
+
issue(id: $id) { id identifier title description url createdAt updatedAt state { name } comments { nodes { id body createdAt user { id name } } } }
|
|
322
|
+
}`, { id: ref.id });
|
|
323
|
+
const issue = asRecord(data.issue);
|
|
324
|
+
const comments = asRecord(issue.comments).nodes;
|
|
325
|
+
return this.mapIssue({ ...issue, comments: Array.isArray(comments) ? comments : [] }, ref.id);
|
|
326
|
+
}
|
|
327
|
+
async listIssues() {
|
|
328
|
+
const teamId = getTrackerString(this.config, "teamId");
|
|
329
|
+
const data = await this.graphql(`query Issues($filter: IssueFilter) {
|
|
330
|
+
issues(filter: $filter, first: 50) { nodes { id identifier title description url createdAt updatedAt state { name } } }
|
|
331
|
+
}`, { filter: teamId ? { team: { id: { eq: teamId } } } : undefined });
|
|
332
|
+
const nodes = asRecord(data.issues).nodes;
|
|
333
|
+
return Array.isArray(nodes) ? nodes.map((issue) => this.mapIssue(issue)) : [];
|
|
334
|
+
}
|
|
335
|
+
async addComment(ref, answer) {
|
|
336
|
+
const data = await this.graphql(`mutation Comment($input: CommentCreateInput!) {
|
|
337
|
+
commentCreate(input: $input) { success comment { id body createdAt user { id name } } }
|
|
338
|
+
}`, { input: { issueId: ref.id, body: buildAnswerBlock(answer) } });
|
|
339
|
+
return this.mapComments([asRecord(asRecord(data.commentCreate).comment)])[0];
|
|
340
|
+
}
|
|
341
|
+
async transitionIssue(ref, status) {
|
|
342
|
+
const stateIds = asRecord(asRecord(this.config.tracker).workflowStateIds);
|
|
343
|
+
const stateId = readString(stateIds[status]);
|
|
344
|
+
if (!stateId)
|
|
345
|
+
return;
|
|
346
|
+
await this.graphql(`mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
347
|
+
issueUpdate(id: $id, input: $input) { success }
|
|
348
|
+
}`, { id: ref.id, input: { stateId } });
|
|
349
|
+
}
|
|
350
|
+
mapIssue(raw, fallbackId) {
|
|
351
|
+
const record = asRecord(raw);
|
|
352
|
+
const state = asRecord(record.state);
|
|
353
|
+
return super.mapIssue({
|
|
354
|
+
...record,
|
|
355
|
+
key: record.identifier,
|
|
356
|
+
status: readString(state.name),
|
|
357
|
+
}, fallbackId);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
export class GenericRestTrackerAdapter extends HttpTrackerAdapter {
|
|
361
|
+
provider = "generic-rest";
|
|
362
|
+
async createIssue(input) {
|
|
363
|
+
const path = getTrackerString(this.config, "createPath") ?? "/issues";
|
|
364
|
+
const raw = await this.requestJson(path, {
|
|
365
|
+
method: getTrackerString(this.config, "createMethod") ?? "POST",
|
|
366
|
+
body: JSON.stringify(redactExternalTrackerSecrets({ ...input, fields: input.fields })),
|
|
367
|
+
});
|
|
368
|
+
return this.mapIssue(raw);
|
|
369
|
+
}
|
|
370
|
+
async getIssue(ref) {
|
|
371
|
+
const template = getTrackerString(this.config, "getPath") ?? "/issues/{id}";
|
|
372
|
+
const raw = await this.requestJson(template.replace("{id}", encodeURIComponent(ref.id)));
|
|
373
|
+
return this.mapIssue(raw, ref.id);
|
|
374
|
+
}
|
|
375
|
+
async listIssues() {
|
|
376
|
+
const path = getTrackerString(this.config, "listPath") ?? "/issues";
|
|
377
|
+
const raw = await this.requestJson(path);
|
|
378
|
+
const items = Array.isArray(raw) ? raw : asRecord(raw).items;
|
|
379
|
+
return Array.isArray(items) ? items.map((item) => this.mapIssue(item)) : [];
|
|
380
|
+
}
|
|
381
|
+
async addComment(ref, answer) {
|
|
382
|
+
const template = getTrackerString(this.config, "commentPath") ?? "/issues/{id}/comments";
|
|
383
|
+
const raw = await this.requestJson(template.replace("{id}", encodeURIComponent(ref.id)), {
|
|
384
|
+
method: getTrackerString(this.config, "commentMethod") ?? "POST",
|
|
385
|
+
body: JSON.stringify({ body: buildAnswerBlock(answer), answer: redactExternalTrackerSecrets(answer) }),
|
|
386
|
+
});
|
|
387
|
+
return this.mapComments([raw])[0];
|
|
388
|
+
}
|
|
389
|
+
async transitionIssue(ref, status) {
|
|
390
|
+
const template = getTrackerString(this.config, "transitionPath");
|
|
391
|
+
if (!template)
|
|
392
|
+
return;
|
|
393
|
+
await this.requestJson(template.replace("{id}", encodeURIComponent(ref.id)), {
|
|
394
|
+
method: getTrackerString(this.config, "transitionMethod") ?? "PATCH",
|
|
395
|
+
body: JSON.stringify({ status }),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async claimIssue(ref, responderId) {
|
|
399
|
+
const template = getTrackerString(this.config, "claimPath");
|
|
400
|
+
if (!template)
|
|
401
|
+
return;
|
|
402
|
+
await this.requestJson(template.replace("{id}", encodeURIComponent(ref.id)), {
|
|
403
|
+
method: getTrackerString(this.config, "claimMethod") ?? "PATCH",
|
|
404
|
+
body: JSON.stringify({ assignee: responderId }),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
export class ExternalTrackerBackend {
|
|
409
|
+
config;
|
|
410
|
+
name = "external-tracker";
|
|
411
|
+
adapter;
|
|
412
|
+
delegatedGitHub;
|
|
413
|
+
refsByBreakpointId = new Map();
|
|
414
|
+
seenWebhookEvents = new Set();
|
|
415
|
+
defaultPollIntervalMs;
|
|
416
|
+
defaultTimeoutMs;
|
|
417
|
+
constructor(config) {
|
|
418
|
+
this.config = config;
|
|
419
|
+
this.defaultPollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
420
|
+
this.defaultTimeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
421
|
+
if (config.provider === "github-issues") {
|
|
422
|
+
const tracker = asRecord(config.tracker);
|
|
423
|
+
const owner = readString(tracker.owner);
|
|
424
|
+
const repo = readString(tracker.repo);
|
|
425
|
+
if (!owner || !repo) {
|
|
426
|
+
throw new Error('external-tracker provider "github-issues" requires tracker.owner and tracker.repo.');
|
|
427
|
+
}
|
|
428
|
+
this.delegatedGitHub = new GitHubIssuesBackend({
|
|
429
|
+
type: "github-issues",
|
|
430
|
+
owner,
|
|
431
|
+
repo,
|
|
432
|
+
labels: readStringArray(tracker.labels),
|
|
433
|
+
assignees: readStringArray(tracker.assignees),
|
|
434
|
+
pollIntervalMs: config.pollIntervalMs,
|
|
435
|
+
timeoutMs: config.timeoutMs,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
this.adapter = createExternalTrackerAdapter(config);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
capabilities() {
|
|
443
|
+
return {
|
|
444
|
+
...unsupportedBreakpointBackendCapabilities,
|
|
445
|
+
assignment: true,
|
|
446
|
+
comments: true,
|
|
447
|
+
history: true,
|
|
448
|
+
export: this.config.provider === "github-issues",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async submitBreakpoint(params) {
|
|
452
|
+
if (this.delegatedGitHub)
|
|
453
|
+
return this.delegatedGitHub.submitBreakpoint(params);
|
|
454
|
+
if (params.proven) {
|
|
455
|
+
throw new Error(unsupportedBackendFeatureMessage(this.name, "ask_breakpoint.proven"));
|
|
456
|
+
}
|
|
457
|
+
const adapter = this.requireAdapter();
|
|
458
|
+
const issue = await adapter.createIssue({
|
|
459
|
+
title: params.context.title ?? params.text,
|
|
460
|
+
description: params.context.markdown ?? params.context.description,
|
|
461
|
+
labels: params.context.tags,
|
|
462
|
+
assignees: params.routing.targetResponders,
|
|
463
|
+
fields: this.buildMappedFields(params, this.config.fieldMapping),
|
|
464
|
+
metadata: { projectId: params.projectId, repoId: params.repoId },
|
|
465
|
+
});
|
|
466
|
+
const breakpoint = this.issueToBreakpoint(issue, params);
|
|
467
|
+
this.refsByBreakpointId.set(breakpoint.id, issue.ref);
|
|
468
|
+
return breakpoint;
|
|
469
|
+
}
|
|
470
|
+
async getBreakpoint(id) {
|
|
471
|
+
if (this.delegatedGitHub)
|
|
472
|
+
return this.delegatedGitHub.getBreakpoint(id);
|
|
473
|
+
const ref = this.resolveRef(id);
|
|
474
|
+
const issue = await this.requireAdapter().getIssue(ref);
|
|
475
|
+
const breakpoint = this.issueToBreakpoint(issue);
|
|
476
|
+
this.refsByBreakpointId.set(breakpoint.id, issue.ref);
|
|
477
|
+
return breakpoint;
|
|
478
|
+
}
|
|
479
|
+
async waitForAnswer(id, options) {
|
|
480
|
+
if (this.delegatedGitHub)
|
|
481
|
+
return this.delegatedGitHub.waitForAnswer(id, options);
|
|
482
|
+
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
|
483
|
+
const pollIntervalMs = options?.pollIntervalMs ?? this.defaultPollIntervalMs;
|
|
484
|
+
const startTime = Date.now();
|
|
485
|
+
while (true) {
|
|
486
|
+
const breakpoint = await this.getBreakpoint(id);
|
|
487
|
+
const answer = selectBreakpointAnswer(breakpoint);
|
|
488
|
+
if (answer) {
|
|
489
|
+
return {
|
|
490
|
+
answered: true,
|
|
491
|
+
breakpoint,
|
|
492
|
+
answer,
|
|
493
|
+
allAnswers: breakpoint.answers,
|
|
494
|
+
elapsedMs: Date.now() - startTime,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (breakpoint.status === "completed" || breakpoint.status === "cancelled") {
|
|
498
|
+
return {
|
|
499
|
+
answered: false,
|
|
500
|
+
breakpoint,
|
|
501
|
+
allAnswers: breakpoint.answers,
|
|
502
|
+
resolution: breakpoint.status,
|
|
503
|
+
elapsedMs: Date.now() - startTime,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (options?.signal?.aborted) {
|
|
507
|
+
return {
|
|
508
|
+
answered: false,
|
|
509
|
+
breakpoint,
|
|
510
|
+
allAnswers: breakpoint.answers,
|
|
511
|
+
resolution: "aborted",
|
|
512
|
+
elapsedMs: Date.now() - startTime,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
516
|
+
return {
|
|
517
|
+
answered: false,
|
|
518
|
+
breakpoint,
|
|
519
|
+
allAnswers: breakpoint.answers,
|
|
520
|
+
resolution: "timeout",
|
|
521
|
+
elapsedMs: Date.now() - startTime,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async listPendingBreakpoints(responderId) {
|
|
528
|
+
if (this.delegatedGitHub)
|
|
529
|
+
return this.delegatedGitHub.listPendingBreakpoints(responderId);
|
|
530
|
+
const issues = await this.requireAdapter().listIssues();
|
|
531
|
+
return issues
|
|
532
|
+
.filter((issue) => issue.status === "open" || issue.status === "claimed")
|
|
533
|
+
.filter((issue) => !responderId || issue.assignees.includes(responderId))
|
|
534
|
+
.map((issue) => {
|
|
535
|
+
const breakpoint = this.issueToBreakpoint(issue);
|
|
536
|
+
this.refsByBreakpointId.set(breakpoint.id, issue.ref);
|
|
537
|
+
return breakpoint;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
async answerBreakpoint(id, answer) {
|
|
541
|
+
if (this.delegatedGitHub)
|
|
542
|
+
return this.delegatedGitHub.answerBreakpoint(id, answer);
|
|
543
|
+
if (answer.sign || answer.keyFingerprint) {
|
|
544
|
+
throw new Error(unsupportedBackendFeatureMessage(this.name, "answer signing"));
|
|
545
|
+
}
|
|
546
|
+
const ref = this.resolveRef(id);
|
|
547
|
+
const comment = await this.requireAdapter().addComment(ref, answer);
|
|
548
|
+
await this.requireAdapter().transitionIssue(ref, "answered");
|
|
549
|
+
return answerFromComment(comment, id) ?? {
|
|
550
|
+
id: `tracker-comment-${comment.id}`,
|
|
551
|
+
breakpointId: id,
|
|
552
|
+
responderId: answer.responderId,
|
|
553
|
+
responderName: answer.responderName,
|
|
554
|
+
text: answer.text,
|
|
555
|
+
confidence: answer.confidence ?? 80,
|
|
556
|
+
references: answer.references ?? [],
|
|
557
|
+
followUpQuestions: answer.followUpQuestions ?? [],
|
|
558
|
+
answeredAt: comment.createdAt,
|
|
559
|
+
decisionMemory: answer.decisionMemory
|
|
560
|
+
? { ...answer.decisionMemory, savedAt: nowIso() }
|
|
561
|
+
: undefined,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
async cancelBreakpoint(id) {
|
|
565
|
+
if (this.delegatedGitHub)
|
|
566
|
+
return this.delegatedGitHub.cancelBreakpoint(id);
|
|
567
|
+
await this.requireAdapter().transitionIssue(this.resolveRef(id), "cancelled");
|
|
568
|
+
}
|
|
569
|
+
async listResponders(_params) {
|
|
570
|
+
if (this.delegatedGitHub)
|
|
571
|
+
return this.delegatedGitHub.listResponders();
|
|
572
|
+
const responders = readStringArray(asRecord(this.config.tracker).responders);
|
|
573
|
+
return responders.map((id) => ({
|
|
574
|
+
id,
|
|
575
|
+
type: "tracker",
|
|
576
|
+
name: id,
|
|
577
|
+
title: `${this.config.provider} tracker responder`,
|
|
578
|
+
capabilities: ["tracking", "issue", this.config.provider],
|
|
579
|
+
domains: [],
|
|
580
|
+
tags: [this.config.provider, "tracker"],
|
|
581
|
+
availability: true,
|
|
582
|
+
responseTimeSla: this.defaultTimeoutMs / 1000,
|
|
583
|
+
trackerBackend: this.config.provider,
|
|
584
|
+
trackerConfig: redactExternalTrackerSecrets(asRecord(this.config.tracker)),
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
async claimBreakpoint(id, responderId) {
|
|
588
|
+
if (this.delegatedGitHub)
|
|
589
|
+
return this.delegatedGitHub.claimBreakpoint(id, responderId);
|
|
590
|
+
const ref = this.resolveRef(id);
|
|
591
|
+
await this.requireAdapter().claimIssue?.(ref, responderId);
|
|
592
|
+
await this.requireAdapter().transitionIssue(ref, "claimed");
|
|
593
|
+
return this.getBreakpoint(id);
|
|
594
|
+
}
|
|
595
|
+
handleWebhook(payload, headers) {
|
|
596
|
+
if (!this.adapter?.normalizeWebhook) {
|
|
597
|
+
return { accepted: false, duplicate: false };
|
|
598
|
+
}
|
|
599
|
+
const event = this.adapter.normalizeWebhook(payload, headers);
|
|
600
|
+
if (!event)
|
|
601
|
+
return { accepted: false, duplicate: false };
|
|
602
|
+
const eventKey = `${event.provider}:${event.eventId}`;
|
|
603
|
+
if (this.seenWebhookEvents.has(eventKey)) {
|
|
604
|
+
return { accepted: true, duplicate: true, event };
|
|
605
|
+
}
|
|
606
|
+
this.seenWebhookEvents.add(eventKey);
|
|
607
|
+
const breakpoint = this.issueToBreakpoint(event.issue);
|
|
608
|
+
this.refsByBreakpointId.set(breakpoint.id, event.issue.ref);
|
|
609
|
+
const answer = event.comment ? answerFromComment(event.comment, breakpoint.id) ?? undefined : undefined;
|
|
610
|
+
return { accepted: true, duplicate: false, event, breakpoint, answer };
|
|
611
|
+
}
|
|
612
|
+
requireAdapter() {
|
|
613
|
+
if (!this.adapter) {
|
|
614
|
+
throw new Error(`External tracker provider "${this.config.provider}" is delegated, not adapter-backed.`);
|
|
615
|
+
}
|
|
616
|
+
return this.adapter;
|
|
617
|
+
}
|
|
618
|
+
resolveRef(id) {
|
|
619
|
+
const cached = this.refsByBreakpointId.get(id);
|
|
620
|
+
if (cached)
|
|
621
|
+
return cached;
|
|
622
|
+
const parsed = decodeRef(id);
|
|
623
|
+
if (parsed.provider !== this.config.provider) {
|
|
624
|
+
throw new Error(`Breakpoint ${id} belongs to ${parsed.provider}, not ${this.config.provider}.`);
|
|
625
|
+
}
|
|
626
|
+
const ref = { provider: parsed.provider, id: parsed.externalId };
|
|
627
|
+
this.refsByBreakpointId.set(id, ref);
|
|
628
|
+
return ref;
|
|
629
|
+
}
|
|
630
|
+
issueToBreakpoint(issue, submitted) {
|
|
631
|
+
const id = encodeRef(issue.ref.provider, issue.ref.id);
|
|
632
|
+
const status = this.mapStatusToBreakpoint(issue.status);
|
|
633
|
+
const answers = issue.comments
|
|
634
|
+
.map((comment) => answerFromComment(comment, id))
|
|
635
|
+
.filter((answer) => answer !== null);
|
|
636
|
+
return BreakpointSchema.parse({
|
|
637
|
+
id,
|
|
638
|
+
text: issue.title,
|
|
639
|
+
context: submitted
|
|
640
|
+
? {
|
|
641
|
+
...submitted.context,
|
|
642
|
+
metadata: {
|
|
643
|
+
...submitted.context.metadata,
|
|
644
|
+
externalTracker: redactExternalTrackerSecrets({
|
|
645
|
+
provider: issue.ref.provider,
|
|
646
|
+
id: issue.ref.id,
|
|
647
|
+
key: issue.ref.key,
|
|
648
|
+
url: issue.ref.url,
|
|
649
|
+
}),
|
|
650
|
+
},
|
|
651
|
+
}
|
|
652
|
+
: {
|
|
653
|
+
description: issue.description ?? issue.title,
|
|
654
|
+
codeSnippets: [],
|
|
655
|
+
fileReferences: [],
|
|
656
|
+
tags: issue.labels,
|
|
657
|
+
title: issue.title,
|
|
658
|
+
links: issue.ref.url ? [{ label: issue.ref.key ?? issue.ref.id, url: issue.ref.url, kind: "external" }] : undefined,
|
|
659
|
+
metadata: {
|
|
660
|
+
externalTracker: redactExternalTrackerSecrets({
|
|
661
|
+
provider: issue.ref.provider,
|
|
662
|
+
id: issue.ref.id,
|
|
663
|
+
key: issue.ref.key,
|
|
664
|
+
url: issue.ref.url,
|
|
665
|
+
}),
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
status: answers.length > 0 && status === "claimed" ? "answered" : status,
|
|
669
|
+
routing: submitted?.routing ?? {
|
|
670
|
+
strategy: "single",
|
|
671
|
+
targetResponders: issue.assignees,
|
|
672
|
+
timeoutMs: this.defaultTimeoutMs,
|
|
673
|
+
presentToUser: false,
|
|
674
|
+
},
|
|
675
|
+
answers,
|
|
676
|
+
selectedAnswer: answers[0]?.id,
|
|
677
|
+
projectId: submitted?.projectId,
|
|
678
|
+
repoId: submitted?.repoId,
|
|
679
|
+
createdAt: issue.createdAt,
|
|
680
|
+
updatedAt: issue.updatedAt,
|
|
681
|
+
expiresAt: new Date(new Date(issue.createdAt).getTime() + (submitted?.routing.timeoutMs ?? this.defaultTimeoutMs)).toISOString(),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
mapStatusToBreakpoint(status) {
|
|
685
|
+
switch (status) {
|
|
686
|
+
case "claimed":
|
|
687
|
+
return "claimed";
|
|
688
|
+
case "answered":
|
|
689
|
+
return "answered";
|
|
690
|
+
case "completed":
|
|
691
|
+
return "completed";
|
|
692
|
+
case "cancelled":
|
|
693
|
+
return "cancelled";
|
|
694
|
+
case "open":
|
|
695
|
+
default:
|
|
696
|
+
return "pending";
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
buildMappedFields(params, mapping) {
|
|
700
|
+
const fields = {};
|
|
701
|
+
if (mapping?.title)
|
|
702
|
+
fields[mapping.title] = params.context.title ?? params.text;
|
|
703
|
+
if (mapping?.description)
|
|
704
|
+
fields[mapping.description] = params.context.markdown ?? params.context.description;
|
|
705
|
+
if (mapping?.labels)
|
|
706
|
+
fields[mapping.labels] = params.context.tags;
|
|
707
|
+
if (mapping?.assignee)
|
|
708
|
+
fields[mapping.assignee] = params.routing.targetResponders;
|
|
709
|
+
if (mapping?.priority && params.context.urgency)
|
|
710
|
+
fields[mapping.priority] = params.context.urgency;
|
|
711
|
+
if (mapping?.metadata) {
|
|
712
|
+
for (const [source, destination] of Object.entries(mapping.metadata)) {
|
|
713
|
+
fields[destination] = getPath(params.context.metadata, source);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return redactExternalTrackerSecrets(fields);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
export function createExternalTrackerAdapter(config) {
|
|
720
|
+
switch (config.provider) {
|
|
721
|
+
case "jira":
|
|
722
|
+
return new JiraTrackerAdapter(config);
|
|
723
|
+
case "linear":
|
|
724
|
+
return new LinearTrackerAdapter(config);
|
|
725
|
+
case "generic-rest":
|
|
726
|
+
return new GenericRestTrackerAdapter(config);
|
|
727
|
+
case "github-issues":
|
|
728
|
+
throw new Error('Use ExternalTrackerBackend delegation for provider "github-issues".');
|
|
729
|
+
}
|
|
730
|
+
throw new Error(`Unsupported external tracker provider: ${config.provider ?? "unknown"}`);
|
|
731
|
+
}
|