@a5c-ai/tasks-adapter 5.1.1-staging.0ad6ac75ae4a

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.
Files changed (202) hide show
  1. package/README.md +125 -0
  2. package/dist/auth/forge-interface.d.ts +67 -0
  3. package/dist/auth/forge-interface.d.ts.map +1 -0
  4. package/dist/auth/forge-interface.js +69 -0
  5. package/dist/auth/github-app.d.ts +64 -0
  6. package/dist/auth/github-app.d.ts.map +1 -0
  7. package/dist/auth/github-app.js +141 -0
  8. package/dist/auth/github-oauth.d.ts +27 -0
  9. package/dist/auth/github-oauth.d.ts.map +1 -0
  10. package/dist/auth/github-oauth.js +89 -0
  11. package/dist/auth/index.d.ts +8 -0
  12. package/dist/auth/index.d.ts.map +1 -0
  13. package/dist/auth/index.js +14 -0
  14. package/dist/auth/jwt.d.ts +24 -0
  15. package/dist/auth/jwt.d.ts.map +1 -0
  16. package/dist/auth/jwt.js +43 -0
  17. package/dist/auth/middleware.d.ts +22 -0
  18. package/dist/auth/middleware.d.ts.map +1 -0
  19. package/dist/auth/middleware.js +36 -0
  20. package/dist/auth/ssh-keys.d.ts +21 -0
  21. package/dist/auth/ssh-keys.d.ts.map +1 -0
  22. package/dist/auth/ssh-keys.js +59 -0
  23. package/dist/auth/types.d.ts +165 -0
  24. package/dist/auth/types.d.ts.map +1 -0
  25. package/dist/auth/types.js +53 -0
  26. package/dist/backend.d.ts +248 -0
  27. package/dist/backend.d.ts.map +1 -0
  28. package/dist/backend.js +40 -0
  29. package/dist/backends/adapters.d.ts +99 -0
  30. package/dist/backends/adapters.d.ts.map +1 -0
  31. package/dist/backends/adapters.js +308 -0
  32. package/dist/backends/external-tracker.d.ts +133 -0
  33. package/dist/backends/external-tracker.d.ts.map +1 -0
  34. package/dist/backends/external-tracker.js +731 -0
  35. package/dist/backends/git-native.d.ts +69 -0
  36. package/dist/backends/git-native.d.ts.map +1 -0
  37. package/dist/backends/git-native.js +797 -0
  38. package/dist/backends/github-issues.d.ts +78 -0
  39. package/dist/backends/github-issues.d.ts.map +1 -0
  40. package/dist/backends/github-issues.js +806 -0
  41. package/dist/backends/index.d.ts +52 -0
  42. package/dist/backends/index.d.ts.map +1 -0
  43. package/dist/backends/index.js +151 -0
  44. package/dist/backends/server.d.ts +42 -0
  45. package/dist/backends/server.d.ts.map +1 -0
  46. package/dist/backends/server.js +305 -0
  47. package/dist/cli/auth-store.d.ts +49 -0
  48. package/dist/cli/auth-store.d.ts.map +1 -0
  49. package/dist/cli/auth-store.js +150 -0
  50. package/dist/cli/client-config.d.ts +10 -0
  51. package/dist/cli/client-config.d.ts.map +1 -0
  52. package/dist/cli/client-config.js +87 -0
  53. package/dist/cli/commands/ask.d.ts +3 -0
  54. package/dist/cli/commands/ask.d.ts.map +1 -0
  55. package/dist/cli/commands/ask.js +171 -0
  56. package/dist/cli/commands/auth.d.ts +3 -0
  57. package/dist/cli/commands/auth.d.ts.map +1 -0
  58. package/dist/cli/commands/auth.js +510 -0
  59. package/dist/cli/commands/breakpoints.d.ts +3 -0
  60. package/dist/cli/commands/breakpoints.d.ts.map +1 -0
  61. package/dist/cli/commands/breakpoints.js +311 -0
  62. package/dist/cli/commands/responder-loop.d.ts +3 -0
  63. package/dist/cli/commands/responder-loop.d.ts.map +1 -0
  64. package/dist/cli/commands/responder-loop.js +78 -0
  65. package/dist/cli/commands/responders.d.ts +3 -0
  66. package/dist/cli/commands/responders.d.ts.map +1 -0
  67. package/dist/cli/commands/responders.js +157 -0
  68. package/dist/cli/commands/rules.d.ts +3 -0
  69. package/dist/cli/commands/rules.d.ts.map +1 -0
  70. package/dist/cli/commands/rules.js +105 -0
  71. package/dist/cli/commands/server.d.ts +3 -0
  72. package/dist/cli/commands/server.d.ts.map +1 -0
  73. package/dist/cli/commands/server.js +34 -0
  74. package/dist/cli/commands/tasks.d.ts +3 -0
  75. package/dist/cli/commands/tasks.d.ts.map +1 -0
  76. package/dist/cli/commands/tasks.js +281 -0
  77. package/dist/cli/commands/templates.d.ts +3 -0
  78. package/dist/cli/commands/templates.d.ts.map +1 -0
  79. package/dist/cli/commands/templates.js +100 -0
  80. package/dist/cli/index.d.ts +4 -0
  81. package/dist/cli/index.d.ts.map +1 -0
  82. package/dist/cli/index.js +9 -0
  83. package/dist/cli/output.d.ts +26 -0
  84. package/dist/cli/output.d.ts.map +1 -0
  85. package/dist/cli/output.js +143 -0
  86. package/dist/cli/program.d.ts +6 -0
  87. package/dist/cli/program.d.ts.map +1 -0
  88. package/dist/cli/program.js +38 -0
  89. package/dist/cli/tasks-adapter.d.ts +3 -0
  90. package/dist/cli/tasks-adapter.d.ts.map +1 -0
  91. package/dist/cli/tasks-adapter.js +4 -0
  92. package/dist/client/answer-poller.d.ts +52 -0
  93. package/dist/client/answer-poller.d.ts.map +1 -0
  94. package/dist/client/answer-poller.js +200 -0
  95. package/dist/client/auth-client.d.ts +200 -0
  96. package/dist/client/auth-client.d.ts.map +1 -0
  97. package/dist/client/auth-client.js +309 -0
  98. package/dist/client/breakpoint-router.d.ts +45 -0
  99. package/dist/client/breakpoint-router.d.ts.map +1 -0
  100. package/dist/client/breakpoint-router.js +45 -0
  101. package/dist/client/index.d.ts +17 -0
  102. package/dist/client/index.d.ts.map +1 -0
  103. package/dist/client/index.js +16 -0
  104. package/dist/client/profile-validator.d.ts +34 -0
  105. package/dist/client/profile-validator.d.ts.map +1 -0
  106. package/dist/client/profile-validator.js +89 -0
  107. package/dist/client/responder-client.d.ts +39 -0
  108. package/dist/client/responder-client.d.ts.map +1 -0
  109. package/dist/client/responder-client.js +72 -0
  110. package/dist/client/responder-matcher.d.ts +49 -0
  111. package/dist/client/responder-matcher.d.ts.map +1 -0
  112. package/dist/client/responder-matcher.js +226 -0
  113. package/dist/client/server-client.d.ts +124 -0
  114. package/dist/client/server-client.d.ts.map +1 -0
  115. package/dist/client/server-client.js +266 -0
  116. package/dist/client/timeout-manager.d.ts +47 -0
  117. package/dist/client/timeout-manager.d.ts.map +1 -0
  118. package/dist/client/timeout-manager.js +77 -0
  119. package/dist/config.d.ts +20 -0
  120. package/dist/config.d.ts.map +1 -0
  121. package/dist/config.js +93 -0
  122. package/dist/harness/index.d.ts +4 -0
  123. package/dist/harness/index.d.ts.map +1 -0
  124. package/dist/harness/index.js +2 -0
  125. package/dist/harness/interaction-provider.d.ts +71 -0
  126. package/dist/harness/interaction-provider.d.ts.map +1 -0
  127. package/dist/harness/interaction-provider.js +124 -0
  128. package/dist/harness/routing-rules.d.ts +7 -0
  129. package/dist/harness/routing-rules.d.ts.map +1 -0
  130. package/dist/harness/routing-rules.js +37 -0
  131. package/dist/index.d.ts +29 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +33 -0
  134. package/dist/mcp/backend-resolver.d.ts +43 -0
  135. package/dist/mcp/backend-resolver.d.ts.map +1 -0
  136. package/dist/mcp/backend-resolver.js +111 -0
  137. package/dist/mcp/http-transport.d.ts +37 -0
  138. package/dist/mcp/http-transport.d.ts.map +1 -0
  139. package/dist/mcp/http-transport.js +103 -0
  140. package/dist/mcp/index.d.ts +16 -0
  141. package/dist/mcp/index.d.ts.map +1 -0
  142. package/dist/mcp/index.js +12 -0
  143. package/dist/mcp/server.d.ts +20 -0
  144. package/dist/mcp/server.d.ts.map +1 -0
  145. package/dist/mcp/server.js +259 -0
  146. package/dist/mcp/tools/answer-breakpoint.d.ts +32 -0
  147. package/dist/mcp/tools/answer-breakpoint.d.ts.map +1 -0
  148. package/dist/mcp/tools/answer-breakpoint.js +45 -0
  149. package/dist/mcp/tools/ask-breakpoint.d.ts +58 -0
  150. package/dist/mcp/tools/ask-breakpoint.d.ts.map +1 -0
  151. package/dist/mcp/tools/ask-breakpoint.js +78 -0
  152. package/dist/mcp/tools/check-status.d.ts +16 -0
  153. package/dist/mcp/tools/check-status.d.ts.map +1 -0
  154. package/dist/mcp/tools/check-status.js +18 -0
  155. package/dist/mcp/tools/claim-breakpoint.d.ts +18 -0
  156. package/dist/mcp/tools/claim-breakpoint.d.ts.map +1 -0
  157. package/dist/mcp/tools/claim-breakpoint.js +28 -0
  158. package/dist/mcp/tools/list-breakpoints.d.ts +16 -0
  159. package/dist/mcp/tools/list-breakpoints.d.ts.map +1 -0
  160. package/dist/mcp/tools/list-breakpoints.js +14 -0
  161. package/dist/mcp/tools/list-responders.d.ts +18 -0
  162. package/dist/mcp/tools/list-responders.d.ts.map +1 -0
  163. package/dist/mcp/tools/list-responders.js +37 -0
  164. package/dist/mcp/tools/native-tasks.d.ts +270 -0
  165. package/dist/mcp/tools/native-tasks.d.ts.map +1 -0
  166. package/dist/mcp/tools/native-tasks.js +481 -0
  167. package/dist/mcp/tools/poll-breakpoints.d.ts +18 -0
  168. package/dist/mcp/tools/poll-breakpoints.d.ts.map +1 -0
  169. package/dist/mcp/tools/poll-breakpoints.js +36 -0
  170. package/dist/mcp/tools/verify-answer.d.ts +16 -0
  171. package/dist/mcp/tools/verify-answer.d.ts.map +1 -0
  172. package/dist/mcp/tools/verify-answer.js +38 -0
  173. package/dist/proven/index.d.ts +5 -0
  174. package/dist/proven/index.d.ts.map +1 -0
  175. package/dist/proven/index.js +3 -0
  176. package/dist/proven/keys.d.ts +33 -0
  177. package/dist/proven/keys.d.ts.map +1 -0
  178. package/dist/proven/keys.js +117 -0
  179. package/dist/proven/sign.d.ts +16 -0
  180. package/dist/proven/sign.d.ts.map +1 -0
  181. package/dist/proven/sign.js +60 -0
  182. package/dist/proven/types.d.ts +26 -0
  183. package/dist/proven/types.d.ts.map +1 -0
  184. package/dist/proven/types.js +5 -0
  185. package/dist/proven/verify.d.ts +6 -0
  186. package/dist/proven/verify.d.ts.map +1 -0
  187. package/dist/proven/verify.js +58 -0
  188. package/dist/responders/types.d.ts +38 -0
  189. package/dist/responders/types.d.ts.map +1 -0
  190. package/dist/responders/types.js +1 -0
  191. package/dist/router.d.ts +51 -0
  192. package/dist/router.d.ts.map +1 -0
  193. package/dist/router.js +200 -0
  194. package/dist/types.d.ts +7711 -0
  195. package/dist/types.d.ts.map +1 -0
  196. package/dist/types.js +479 -0
  197. package/package.json +96 -0
  198. package/responder/README.md +42 -0
  199. package/responder/backend-responder.json +9 -0
  200. package/responder/devops-responder.json +9 -0
  201. package/responder/frontend-responder.json +9 -0
  202. package/responder/schema.json +89 -0
@@ -0,0 +1,806 @@
1
+ import { execSync } from "node:child_process";
2
+ import { unsupportedBackendFeatureMessage, unsupportedBreakpointBackendCapabilities, } from "../backend.js";
3
+ import { BreakpointSchema, BreakpointContextSchema, BreakpointRoutingSchema, BreakpointSubmitterSchema, DEFAULT_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, } from "../types.js";
4
+ const GITHUB_API_BASE = "https://api.github.com";
5
+ const GITHUB_API_VERSION = "2022-11-28";
6
+ const ISSUE_PAYLOAD_MARKER = "tasks-adapter:issue:v1";
7
+ const ANSWER_PAYLOAD_MARKER = "tasks-adapter:answer:v1";
8
+ function escapeRegExp(value) {
9
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
+ }
11
+ function buildPayloadBlock(marker, payload) {
12
+ return `<!-- ${marker}\n${JSON.stringify(payload, null, 2)}\n-->`;
13
+ }
14
+ function extractPayloadBlock(body, marker) {
15
+ if (!body)
16
+ return { cleanedBody: "" };
17
+ const regex = new RegExp(`<!--\\s*${escapeRegExp(marker)}\\s*([\\s\\S]*?)\\s*-->`, "m");
18
+ const match = body.match(regex);
19
+ if (!match)
20
+ return { cleanedBody: body };
21
+ let payload;
22
+ const raw = match[1].trim();
23
+ if (raw.length > 0) {
24
+ try {
25
+ payload = JSON.parse(raw);
26
+ }
27
+ catch {
28
+ payload = undefined;
29
+ }
30
+ }
31
+ const cleanedBody = body.replace(match[0], "").trim();
32
+ return { payload, cleanedBody };
33
+ }
34
+ function parseIssuePayload(raw) {
35
+ if (!raw || typeof raw !== "object")
36
+ return null;
37
+ const payload = raw;
38
+ if (payload.version !== 1 || payload.schema !== "tasks-adapter:issue") {
39
+ return null;
40
+ }
41
+ if (typeof payload.text !== "string")
42
+ return null;
43
+ const contextParse = BreakpointContextSchema.safeParse(payload.context);
44
+ if (!contextParse.success)
45
+ return null;
46
+ const routingParse = BreakpointRoutingSchema.safeParse(payload.routing);
47
+ if (!routingParse.success)
48
+ return null;
49
+ if (payload.createdBy) {
50
+ const createdByParse = BreakpointSubmitterSchema.safeParse(payload.createdBy);
51
+ if (!createdByParse.success)
52
+ return null;
53
+ }
54
+ return {
55
+ version: 1,
56
+ schema: "tasks-adapter:issue",
57
+ text: payload.text,
58
+ context: contextParse.data,
59
+ routing: routingParse.data,
60
+ projectId: payload.projectId,
61
+ repoId: payload.repoId,
62
+ createdBy: payload.createdBy,
63
+ };
64
+ }
65
+ function parseAnswerPayload(raw) {
66
+ if (!raw || typeof raw !== "object")
67
+ return null;
68
+ const payload = raw;
69
+ if (payload.version !== 1 || payload.schema !== "tasks-adapter:answer") {
70
+ return null;
71
+ }
72
+ if (typeof payload.text !== "string")
73
+ return null;
74
+ return {
75
+ text: payload.text,
76
+ confidence: typeof payload.confidence === "number" ? payload.confidence : undefined,
77
+ references: Array.isArray(payload.references)
78
+ ? payload.references.filter((item) => typeof item === "string")
79
+ : undefined,
80
+ responderId: typeof payload.responderId === "string" ? payload.responderId : undefined,
81
+ responderName: typeof payload.responderName === "string" ? payload.responderName : undefined,
82
+ breakpointId: typeof payload.breakpointId === "string" ? payload.breakpointId : undefined,
83
+ answeredAt: typeof payload.answeredAt === "string" ? payload.answeredAt : undefined,
84
+ };
85
+ }
86
+ function stripLegacyFooter(body) {
87
+ const footerRegex = /\*Project:\s*([^|]+)\|\s*Repo:\s*([^*]+)\*/i;
88
+ const match = footerRegex.exec(body);
89
+ const projectId = match ? match[1].trim() : undefined;
90
+ const repoId = match ? match[2].trim() : undefined;
91
+ const cleanedBody = body.replace(footerRegex, "").replace(/\n---\n?$/, "").trim();
92
+ return { cleanedBody, projectId, repoId };
93
+ }
94
+ function sliceSection(body, heading) {
95
+ const regex = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, "m");
96
+ const match = regex.exec(body);
97
+ if (!match)
98
+ return null;
99
+ const startIndex = match.index + match[0].length;
100
+ const rest = body.slice(startIndex);
101
+ const nextHeadingIndex = rest.search(/^##\s+/m);
102
+ const section = nextHeadingIndex === -1 ? rest : rest.slice(0, nextHeadingIndex);
103
+ return section.trim();
104
+ }
105
+ function parseLegacyFileReferences(body) {
106
+ const section = sliceSection(body, "File References");
107
+ if (!section)
108
+ return [];
109
+ const lines = section.split(/\r?\n/);
110
+ const files = [];
111
+ for (const line of lines) {
112
+ const match = /`([^`]+)`/.exec(line);
113
+ if (match) {
114
+ files.push(match[1].trim());
115
+ continue;
116
+ }
117
+ const trimmed = line.replace(/^-/, "").trim();
118
+ if (trimmed.length > 0) {
119
+ files.push(trimmed);
120
+ }
121
+ }
122
+ return files;
123
+ }
124
+ function parseLegacyCodeSnippets(body) {
125
+ const section = sliceSection(body, "Code Snippets");
126
+ if (!section)
127
+ return [];
128
+ const snippets = [];
129
+ const regex = /```([^\n]*)\n([\s\S]*?)```/g;
130
+ let match;
131
+ while ((match = regex.exec(section)) !== null) {
132
+ const language = match[1]?.trim();
133
+ const code = match[2].trim();
134
+ const before = section.slice(0, match.index);
135
+ const lines = before.trim().split(/\r?\n/);
136
+ const lastLine = lines[lines.length - 1] ?? "";
137
+ const filenameMatch = /^###\s+(.+)/.exec(lastLine.trim());
138
+ if (filenameMatch) {
139
+ snippets.push({
140
+ filename: filenameMatch[1].trim(),
141
+ code,
142
+ language: language || undefined,
143
+ });
144
+ }
145
+ else {
146
+ snippets.push(code);
147
+ }
148
+ }
149
+ return snippets;
150
+ }
151
+ function parseLegacyTags(body, labels) {
152
+ const tagMatch = /\*\*Tags:\*\*\s*(.+)/i.exec(body);
153
+ if (tagMatch) {
154
+ return tagMatch[1]
155
+ .split(",")
156
+ .map((tag) => tag.trim())
157
+ .filter((tag) => tag.length > 0);
158
+ }
159
+ return labels.map((label) => label.name);
160
+ }
161
+ function parseLegacyUrgency(body, labels) {
162
+ const urgencyMatch = /\*\*Urgency:\*\*\s*(\w+)/i.exec(body);
163
+ if (urgencyMatch) {
164
+ const normalized = urgencyMatch[1].trim().toLowerCase();
165
+ if (normalized === "low" || normalized === "medium" || normalized === "high") {
166
+ return normalized;
167
+ }
168
+ }
169
+ const labelMatch = labels.find((label) => label.name.startsWith("urgency:"));
170
+ if (labelMatch) {
171
+ const normalized = labelMatch.name.replace(/^urgency:/, "").toLowerCase();
172
+ if (normalized === "low" || normalized === "medium" || normalized === "high") {
173
+ return normalized;
174
+ }
175
+ }
176
+ return undefined;
177
+ }
178
+ function extractLegacyDescription(body) {
179
+ const trimmed = body.trim();
180
+ if (!trimmed)
181
+ return "";
182
+ const nextHeadingIndex = trimmed.search(/^##\s+/m);
183
+ const description = nextHeadingIndex === -1 ? trimmed : trimmed.slice(0, nextHeadingIndex);
184
+ return description.trim();
185
+ }
186
+ /**
187
+ * Resolve a GitHub token by trying `gh auth token` first,
188
+ * then falling back to the GITHUB_TOKEN environment variable.
189
+ */
190
+ export function getGitHubToken() {
191
+ try {
192
+ const token = execSync("gh auth token", {
193
+ encoding: "utf-8",
194
+ stdio: ["pipe", "pipe", "pipe"],
195
+ }).trim();
196
+ if (token)
197
+ return token;
198
+ }
199
+ catch {
200
+ // gh CLI not available or not authenticated
201
+ }
202
+ const envToken = process.env.GITHUB_TOKEN;
203
+ if (envToken)
204
+ return envToken;
205
+ throw new Error("No GitHub token found. Install the GitHub CLI and run `gh auth login`, or set the GITHUB_TOKEN environment variable.");
206
+ }
207
+ /**
208
+ * Minimum character length for a plain-text comment to be treated
209
+ * as an answer from an assigned responder (heuristic).
210
+ */
211
+ const PLAIN_TEXT_ANSWER_MIN_LENGTH = 80;
212
+ /**
213
+ * Parse an answer from a GitHub issue comment body.
214
+ * Looks for a hidden JSON payload block, a "## Answer" heading anywhere
215
+ * in the body, a JSON code block with an "answer" field, or a sufficiently
216
+ * long plain-text comment (for assigned responders handled upstream).
217
+ */
218
+ export function parseAnswerFromComment(body, options) {
219
+ const { payload } = extractPayloadBlock(body, ANSWER_PAYLOAD_MARKER);
220
+ const payloadAnswer = parseAnswerPayload(payload);
221
+ if (payloadAnswer) {
222
+ return payloadAnswer;
223
+ }
224
+ // Try "## Answer" marker anywhere in the body
225
+ const markerIndex = body.indexOf("## Answer");
226
+ if (markerIndex !== -1) {
227
+ const afterMarker = body.slice(markerIndex + "## Answer".length).trim();
228
+ if (afterMarker.length > 0) {
229
+ // Split off metadata footer (after --- separator)
230
+ const hrIndex = afterMarker.indexOf("\n---\n");
231
+ const answerText = hrIndex !== -1 ? afterMarker.slice(0, hrIndex).trim() : afterMarker;
232
+ let confidence;
233
+ let references;
234
+ if (hrIndex !== -1) {
235
+ const footer = afterMarker.slice(hrIndex + 5);
236
+ const confMatch = /\*\*Confidence:\*\*\s*(\d+)\/100/.exec(footer);
237
+ if (confMatch)
238
+ confidence = parseInt(confMatch[1], 10);
239
+ const refMatch = /\*\*References:\*\*\s*(.+)/.exec(footer);
240
+ if (refMatch)
241
+ references = refMatch[1].split(",").map((s) => s.trim());
242
+ }
243
+ return { text: answerText, confidence, references };
244
+ }
245
+ }
246
+ // Try JSON code block with "answer" field
247
+ const jsonBlockMatch = body.match(/```(?:json)?\s*\n(\{[\s\S]*?\})\s*\n```/);
248
+ if (jsonBlockMatch) {
249
+ try {
250
+ const parsed = JSON.parse(jsonBlockMatch[1]);
251
+ if (typeof parsed.answer === "string") {
252
+ return {
253
+ text: parsed.answer,
254
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : undefined,
255
+ references: Array.isArray(parsed.references) ? parsed.references : undefined,
256
+ };
257
+ }
258
+ }
259
+ catch {
260
+ // Not valid JSON, skip
261
+ }
262
+ }
263
+ // Plain-text fallback for assigned responders with long comments
264
+ if (options?.isAssignedResponder && body.trim().length >= PLAIN_TEXT_ANSWER_MIN_LENGTH) {
265
+ return { text: body.trim() };
266
+ }
267
+ return null;
268
+ }
269
+ /**
270
+ * BreakpointBackend implementation backed by GitHub Issues.
271
+ *
272
+ * Each breakpoint maps to a GitHub issue; answers are detected
273
+ * from issue comments containing payload blocks, "## Answer" markers, or JSON blocks.
274
+ */
275
+ export class GitHubIssuesBackend {
276
+ name = "github-issues";
277
+ owner;
278
+ repo;
279
+ labels;
280
+ assignees;
281
+ defaultPollIntervalMs;
282
+ defaultTimeoutMs;
283
+ /**
284
+ * Maps breakpointId -> GitHub issue number.
285
+ *
286
+ * Used for legacy non-`gh-{number}` IDs within a single process.
287
+ * New breakpoints always use `gh-{number}` IDs and remain durable
288
+ * across backend restarts without this map.
289
+ */
290
+ issueMap = new Map();
291
+ tokenOverride;
292
+ /**
293
+ * Resolve the GitHub issue number from a breakpoint ID.
294
+ * Checks the in-memory map first, then parses the `gh-{number}` format
295
+ * so that IDs survive across backend instances.
296
+ */
297
+ resolveIssueNumber(breakpointId) {
298
+ const mapped = this.issueMap.get(breakpointId);
299
+ if (mapped !== undefined)
300
+ return mapped;
301
+ const match = /^gh-(\d+)$/.exec(breakpointId);
302
+ if (match) {
303
+ const num = parseInt(match[1], 10);
304
+ this.issueMap.set(breakpointId, num);
305
+ return num;
306
+ }
307
+ throw new Error(`Unknown breakpoint ID: ${breakpointId}`);
308
+ }
309
+ constructor(config) {
310
+ this.owner = config.owner;
311
+ this.repo = config.repo;
312
+ this.labels = config.labels ?? [];
313
+ this.assignees = config.assignees ?? [];
314
+ this.defaultPollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
315
+ this.defaultTimeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
316
+ }
317
+ capabilities() {
318
+ return {
319
+ ...unsupportedBreakpointBackendCapabilities,
320
+ comments: true,
321
+ history: true,
322
+ export: true,
323
+ };
324
+ }
325
+ /**
326
+ * Override the token resolution for testing.
327
+ * @internal
328
+ */
329
+ setToken(token) {
330
+ this.tokenOverride = token;
331
+ }
332
+ getToken() {
333
+ if (this.tokenOverride)
334
+ return this.tokenOverride;
335
+ return getGitHubToken();
336
+ }
337
+ async githubFetch(path, options = {}, retryCount = 0) {
338
+ const maxRetries = 3;
339
+ const token = this.getToken();
340
+ const url = `${GITHUB_API_BASE}${path}`;
341
+ const headers = {
342
+ "Authorization": `Bearer ${token}`,
343
+ "Accept": "application/vnd.github+json",
344
+ "X-GitHub-Api-Version": GITHUB_API_VERSION,
345
+ ...options.headers,
346
+ };
347
+ if (options.body) {
348
+ headers["Content-Type"] = "application/json";
349
+ }
350
+ const response = await fetch(url, { ...options, headers });
351
+ // Handle rate limiting
352
+ if (response.status === 403 || response.status === 429) {
353
+ const retryAfter = response.headers.get("Retry-After");
354
+ if (retryAfter) {
355
+ if (retryCount >= maxRetries) {
356
+ throw new Error(`GitHub API rate limit exceeded after ${maxRetries} retries`);
357
+ }
358
+ const waitMs = parseInt(retryAfter, 10) * 1000;
359
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
360
+ return this.githubFetch(path, options, retryCount + 1);
361
+ }
362
+ // 403 without Retry-After is likely an auth failure or forbidden repo
363
+ if (response.status === 403) {
364
+ const errorBody = await response.text();
365
+ throw new Error(`GitHub API forbidden (403): ${errorBody}`);
366
+ }
367
+ }
368
+ return response;
369
+ }
370
+ repoPath() {
371
+ return `/repos/${this.owner}/${this.repo}`;
372
+ }
373
+ mapIssueToBreakpoint(issue) {
374
+ const breakpointId = `gh-${issue.number}`;
375
+ const { payload, cleanedBody } = extractPayloadBlock(issue.body, ISSUE_PAYLOAD_MARKER);
376
+ const parsedPayload = parseIssuePayload(payload);
377
+ const { cleanedBody: legacyBody, projectId: legacyProjectId, repoId: legacyRepoId } = stripLegacyFooter(cleanedBody);
378
+ const legacyTags = parseLegacyTags(legacyBody, issue.labels);
379
+ const legacyUrgency = parseLegacyUrgency(legacyBody, issue.labels);
380
+ const legacyContext = {
381
+ description: extractLegacyDescription(legacyBody),
382
+ codeSnippets: parseLegacyCodeSnippets(legacyBody),
383
+ fileReferences: parseLegacyFileReferences(legacyBody),
384
+ tags: legacyTags,
385
+ urgency: legacyUrgency,
386
+ };
387
+ const routing = parsedPayload?.routing ?? {
388
+ strategy: "single",
389
+ targetResponders: issue.assignees.map((a) => a.login),
390
+ timeoutMs: this.defaultTimeoutMs,
391
+ presentToUser: false,
392
+ };
393
+ const hasAssignee = issue.assignees.length > 0;
394
+ let status;
395
+ if (issue.state === "closed") {
396
+ status = "completed";
397
+ }
398
+ else if (hasAssignee) {
399
+ status = "claimed";
400
+ }
401
+ else {
402
+ status = "pending";
403
+ }
404
+ return BreakpointSchema.parse({
405
+ id: breakpointId,
406
+ text: parsedPayload?.text ?? issue.title,
407
+ context: parsedPayload?.context ?? legacyContext,
408
+ status,
409
+ routing,
410
+ answers: [],
411
+ projectId: parsedPayload?.projectId ?? legacyProjectId,
412
+ repoId: parsedPayload?.repoId ?? legacyRepoId,
413
+ createdBy: parsedPayload?.createdBy,
414
+ createdAt: issue.created_at,
415
+ updatedAt: issue.updated_at,
416
+ expiresAt: new Date(new Date(issue.created_at).getTime() + routing.timeoutMs).toISOString(),
417
+ });
418
+ }
419
+ buildAnswerFromComment(comment, breakpointId, isAssignedResponder) {
420
+ if (!comment.body)
421
+ return null;
422
+ const parsed = parseAnswerFromComment(comment.body, { isAssignedResponder });
423
+ if (!parsed)
424
+ return null;
425
+ const responderId = parsed.responderId ?? comment.user?.login ?? "unknown";
426
+ const responderName = parsed.responderName ?? responderId;
427
+ return {
428
+ id: `comment-${comment.id}`,
429
+ breakpointId: parsed.breakpointId === breakpointId ? parsed.breakpointId : breakpointId,
430
+ responderId,
431
+ responderName,
432
+ text: parsed.text,
433
+ confidence: parsed.confidence ?? 80,
434
+ references: parsed.references ?? [],
435
+ followUpQuestions: [],
436
+ answeredAt: parsed.answeredAt ?? comment.created_at,
437
+ };
438
+ }
439
+ formatIssueBody(params) {
440
+ const parts = [];
441
+ const context = params.context;
442
+ const createdBy = params.createdBy;
443
+ if (context.title && context.title !== params.text) {
444
+ parts.push(`**Title:** ${context.title}`);
445
+ }
446
+ if (context.summary) {
447
+ parts.push(`**Summary:** ${context.summary}`);
448
+ }
449
+ if (context.description) {
450
+ parts.push(context.description);
451
+ }
452
+ if (context.markdown) {
453
+ parts.push("## Details");
454
+ parts.push(context.markdown);
455
+ }
456
+ if (context.codeSnippets.length > 0) {
457
+ parts.push("## Code Snippets");
458
+ for (const snippet of context.codeSnippets) {
459
+ if (typeof snippet === "string") {
460
+ parts.push("```\n" + snippet + "\n```");
461
+ }
462
+ else {
463
+ parts.push(`### ${snippet.filename}` +
464
+ "\n```" + (snippet.language ?? "") + "\n" +
465
+ snippet.code +
466
+ "\n```");
467
+ }
468
+ }
469
+ }
470
+ if (context.fileReferences.length > 0) {
471
+ parts.push("## File References");
472
+ parts.push(context.fileReferences.map((f) => `- \`${f}\``).join("\n"));
473
+ }
474
+ if (context.tags.length > 0) {
475
+ parts.push(`**Tags:** ${context.tags.join(", ")}`);
476
+ }
477
+ if (context.domain) {
478
+ parts.push(`**Domain:** ${context.domain}`);
479
+ }
480
+ if (context.interactionKind) {
481
+ parts.push(`**Interaction Kind:** ${context.interactionKind}`);
482
+ }
483
+ if (context.urgency) {
484
+ parts.push(`**Urgency:** ${context.urgency}`);
485
+ }
486
+ if (context.links && context.links.length > 0) {
487
+ parts.push("## Links");
488
+ parts.push(context.links
489
+ .map((link) => `- ${link.label}: ${link.url}`)
490
+ .join("\n"));
491
+ }
492
+ if (context.sections && context.sections.length > 0) {
493
+ parts.push("## Sections");
494
+ for (const section of context.sections) {
495
+ parts.push(`### ${section.title}`);
496
+ parts.push(section.markdown);
497
+ }
498
+ }
499
+ if (context.artifacts && context.artifacts.length > 0) {
500
+ parts.push("## Artifacts");
501
+ parts.push(context.artifacts
502
+ .map((artifact) => `- ${artifact.label}: ${artifact.url}`)
503
+ .join("\n"));
504
+ }
505
+ if (context.metadata && Object.keys(context.metadata).length > 0) {
506
+ parts.push("## Metadata");
507
+ parts.push(`\`\`\`json\n${JSON.stringify(context.metadata, null, 2)}\n\`\`\``);
508
+ }
509
+ parts.push(`\n---\n*Project: ${params.projectId ?? "unknown"} | Repo: ${params.repoId ?? "unknown"}*`);
510
+ const payload = {
511
+ version: 1,
512
+ schema: "tasks-adapter:issue",
513
+ text: params.text,
514
+ context: {
515
+ description: context.description,
516
+ codeSnippets: context.codeSnippets,
517
+ fileReferences: context.fileReferences,
518
+ tags: context.tags,
519
+ title: context.title,
520
+ summary: context.summary,
521
+ markdown: context.markdown,
522
+ domain: context.domain,
523
+ urgency: context.urgency,
524
+ interactionKind: context.interactionKind,
525
+ links: context.links,
526
+ sections: context.sections,
527
+ artifacts: context.artifacts,
528
+ metadata: context.metadata,
529
+ },
530
+ routing: {
531
+ strategy: params.routing.strategy,
532
+ targetResponders: params.routing.targetResponders,
533
+ timeoutMs: params.routing.timeoutMs,
534
+ presentToUser: params.routing.presentToUser,
535
+ autoApproveAfterN: params.routing.autoApproveAfterN,
536
+ breakpointId: params.routing.breakpointId,
537
+ },
538
+ projectId: params.projectId,
539
+ repoId: params.repoId,
540
+ createdBy,
541
+ };
542
+ parts.push(buildPayloadBlock(ISSUE_PAYLOAD_MARKER, payload));
543
+ return parts.join("\n\n");
544
+ }
545
+ async submitBreakpoint(params) {
546
+ if (params.proven) {
547
+ throw new Error(unsupportedBackendFeatureMessage(this.name, "ask_breakpoint.proven"));
548
+ }
549
+ const urgency = params.context.urgency;
550
+ const issueLabels = [...this.labels];
551
+ if (urgency) {
552
+ issueLabels.push(`urgency:${urgency}`);
553
+ }
554
+ const response = await this.githubFetch(`${this.repoPath()}/issues`, {
555
+ method: "POST",
556
+ body: JSON.stringify({
557
+ title: params.text,
558
+ body: this.formatIssueBody(params),
559
+ labels: issueLabels,
560
+ assignees: this.assignees,
561
+ }),
562
+ });
563
+ if (!response.ok) {
564
+ const errorBody = await response.text();
565
+ throw new Error(`GitHub API error (${response.status}): ${errorBody}`);
566
+ }
567
+ const issue = (await response.json());
568
+ const breakpointId = `gh-${issue.number}`;
569
+ this.issueMap.set(breakpointId, issue.number);
570
+ return this.mapIssueToBreakpoint(issue);
571
+ }
572
+ async getBreakpoint(id) {
573
+ const issueNumber = this.resolveIssueNumber(id);
574
+ const response = await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}`);
575
+ if (!response.ok) {
576
+ const errorBody = await response.text();
577
+ throw new Error(`GitHub API error (${response.status}): ${errorBody}`);
578
+ }
579
+ const issue = (await response.json());
580
+ const breakpoint = this.mapIssueToBreakpoint(issue);
581
+ // Fetch comments to detect answers
582
+ const assigneeLogins = new Set(issue.assignees.map((a) => a.login));
583
+ const commentsResponse = await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}/comments`);
584
+ if (commentsResponse.ok) {
585
+ const comments = (await commentsResponse.json());
586
+ for (const comment of comments) {
587
+ if (!comment.body)
588
+ continue;
589
+ const commenterLogin = comment.user?.login ?? "";
590
+ const isAssignedResponder = assigneeLogins.has(commenterLogin);
591
+ const answer = this.buildAnswerFromComment(comment, breakpoint.id, isAssignedResponder);
592
+ if (answer) {
593
+ breakpoint.answers.push(answer);
594
+ }
595
+ }
596
+ if (breakpoint.answers.length > 0 && breakpoint.status === "claimed") {
597
+ breakpoint.status = "answered";
598
+ }
599
+ }
600
+ return breakpoint;
601
+ }
602
+ async waitForAnswer(id, options) {
603
+ const issueNumber = this.resolveIssueNumber(id);
604
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
605
+ const pollIntervalMs = options?.pollIntervalMs ?? this.defaultPollIntervalMs;
606
+ const signal = options?.signal;
607
+ const startTime = Date.now();
608
+ while (true) {
609
+ if (signal?.aborted) {
610
+ const breakpoint = await this.getBreakpoint(id);
611
+ return {
612
+ answered: false,
613
+ breakpoint,
614
+ allAnswers: [],
615
+ resolution: "aborted",
616
+ elapsedMs: Date.now() - startTime,
617
+ };
618
+ }
619
+ // Fetch issue metadata for assignees and state
620
+ const issueMetaResponse = await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}`);
621
+ const assigneeLogins = new Set();
622
+ let issueMeta = null;
623
+ if (issueMetaResponse.ok) {
624
+ issueMeta = (await issueMetaResponse.json());
625
+ for (const a of issueMeta.assignees) {
626
+ assigneeLogins.add(a.login);
627
+ }
628
+ }
629
+ // Check for comments with answers
630
+ const commentsResponse = await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}/comments`);
631
+ if (!commentsResponse.ok) {
632
+ const errorBody = await commentsResponse.text();
633
+ throw new Error(`Failed to fetch comments for issue #${issueNumber} (${commentsResponse.status}): ${errorBody}`);
634
+ }
635
+ const comments = (await commentsResponse.json());
636
+ for (const comment of comments) {
637
+ if (!comment.body)
638
+ continue;
639
+ const commenterLogin = comment.user?.login ?? "";
640
+ const isAssignedResponder = assigneeLogins.has(commenterLogin);
641
+ const answer = this.buildAnswerFromComment(comment, `gh-${issueNumber}`, isAssignedResponder);
642
+ if (!answer)
643
+ continue;
644
+ const breakpoint = await this.getBreakpoint(id);
645
+ return {
646
+ answered: true,
647
+ breakpoint: { ...breakpoint, status: "answered", answers: [answer] },
648
+ answer,
649
+ allAnswers: [answer],
650
+ elapsedMs: Date.now() - startTime,
651
+ };
652
+ }
653
+ // Check if issue was closed (reuse metadata already fetched)
654
+ if (issueMeta && issueMeta.state === "closed") {
655
+ const breakpoint = this.mapIssueToBreakpoint(issueMeta);
656
+ return {
657
+ answered: false,
658
+ breakpoint,
659
+ allAnswers: [],
660
+ resolution: "closed",
661
+ elapsedMs: Date.now() - startTime,
662
+ };
663
+ }
664
+ // Check timeout
665
+ if (Date.now() - startTime >= timeoutMs) {
666
+ const breakpoint = await this.getBreakpoint(id);
667
+ return {
668
+ answered: false,
669
+ breakpoint,
670
+ allAnswers: [],
671
+ resolution: "timeout",
672
+ elapsedMs: Date.now() - startTime,
673
+ };
674
+ }
675
+ // Wait before next poll
676
+ await new Promise((resolve) => {
677
+ const timer = setTimeout(resolve, pollIntervalMs);
678
+ if (signal) {
679
+ const onAbort = () => {
680
+ clearTimeout(timer);
681
+ resolve();
682
+ };
683
+ signal.addEventListener("abort", onAbort, { once: true });
684
+ }
685
+ });
686
+ }
687
+ }
688
+ async listResponders(_params) {
689
+ return this.assignees.map((login) => ({
690
+ id: login,
691
+ type: "human",
692
+ name: login,
693
+ title: "",
694
+ capabilities: ["review", "approval", "text"],
695
+ domains: [],
696
+ tags: [],
697
+ availability: true,
698
+ responseTimeSla: this.defaultTimeoutMs / 1000,
699
+ }));
700
+ }
701
+ async claimBreakpoint(id, responderId) {
702
+ const issueNumber = this.resolveIssueNumber(id);
703
+ // Post claim comment
704
+ await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}/comments`, {
705
+ method: "POST",
706
+ body: JSON.stringify({
707
+ body: `Claimed by ${responderId}`,
708
+ }),
709
+ });
710
+ // Actually assign the responder on the GitHub issue
711
+ await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}/assignees`, {
712
+ method: "POST",
713
+ body: JSON.stringify({
714
+ assignees: [responderId],
715
+ }),
716
+ });
717
+ return this.getBreakpoint(id);
718
+ }
719
+ async answerBreakpoint(id, answer) {
720
+ if (answer.sign || answer.keyFingerprint) {
721
+ throw new Error(unsupportedBackendFeatureMessage(this.name, "answer signing"));
722
+ }
723
+ const issueNumber = this.resolveIssueNumber(id);
724
+ const metadataLines = [];
725
+ if (answer.confidence !== undefined) {
726
+ metadataLines.push(`**Confidence:** ${answer.confidence}/100`);
727
+ }
728
+ if (answer.references && answer.references.length > 0) {
729
+ metadataLines.push(`**References:** ${answer.references.join(", ")}`);
730
+ }
731
+ const metadataSection = metadataLines.length > 0
732
+ ? `\n\n---\n${metadataLines.join("\n")}`
733
+ : "";
734
+ const breakpointId = `gh-${issueNumber}`;
735
+ const payload = {
736
+ version: 1,
737
+ schema: "tasks-adapter:answer",
738
+ text: answer.text,
739
+ confidence: answer.confidence,
740
+ references: answer.references,
741
+ responderId: answer.responderId,
742
+ responderName: answer.responderName,
743
+ breakpointId,
744
+ };
745
+ const commentBody = `## Answer\n\n${answer.text}${metadataSection}\n\n${buildPayloadBlock(ANSWER_PAYLOAD_MARKER, payload)}`;
746
+ const response = await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}/comments`, {
747
+ method: "POST",
748
+ body: JSON.stringify({ body: commentBody }),
749
+ });
750
+ if (!response.ok) {
751
+ const errorBody = await response.text();
752
+ throw new Error(`GitHub API error (${response.status}): ${errorBody}`);
753
+ }
754
+ const comment = (await response.json());
755
+ // Close the issue now that it's answered
756
+ await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}`, {
757
+ method: "PATCH",
758
+ body: JSON.stringify({ state: "closed" }),
759
+ });
760
+ return {
761
+ id: `comment-${comment.id}`,
762
+ breakpointId,
763
+ responderId: answer.responderId,
764
+ responderName: answer.responderName,
765
+ text: answer.text,
766
+ confidence: answer.confidence ?? 80,
767
+ references: answer.references ?? [],
768
+ followUpQuestions: answer.followUpQuestions ?? [],
769
+ answeredAt: comment.created_at,
770
+ decisionMemory: answer.decisionMemory
771
+ ? { ...answer.decisionMemory, savedAt: new Date().toISOString() }
772
+ : undefined,
773
+ };
774
+ }
775
+ async listPendingBreakpoints(responderId) {
776
+ const params = new URLSearchParams({
777
+ state: "open",
778
+ assignee: responderId ?? "",
779
+ });
780
+ for (const label of this.labels) {
781
+ params.append("labels", label);
782
+ }
783
+ const response = await this.githubFetch(`${this.repoPath()}/issues?${params.toString()}`);
784
+ if (!response.ok) {
785
+ const errorBody = await response.text();
786
+ throw new Error(`GitHub API error (${response.status}): ${errorBody}`);
787
+ }
788
+ const issues = await response.json();
789
+ return issues.map((issue) => {
790
+ const breakpointId = `gh-${issue.number}`;
791
+ this.issueMap.set(breakpointId, issue.number);
792
+ return this.mapIssueToBreakpoint(issue);
793
+ });
794
+ }
795
+ async cancelBreakpoint(id) {
796
+ const issueNumber = this.resolveIssueNumber(id);
797
+ const response = await this.githubFetch(`${this.repoPath()}/issues/${issueNumber}`, {
798
+ method: "PATCH",
799
+ body: JSON.stringify({ state: "closed" }),
800
+ });
801
+ if (!response.ok) {
802
+ const errorBody = await response.text();
803
+ throw new Error(`GitHub API error (${response.status}): ${errorBody}`);
804
+ }
805
+ }
806
+ }