@a5c-ai/tasks-adapter 5.1.1-staging.52898ebfc24f

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,797 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { generateBreakpointId, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, BREAKPOINTS_DIR, BreakpointSchema, BreakpointPublicAnswerSchema, ProvenBreakpointAnswerSchema, isProvenBreakpointAnswer, validateBreakpointTransition, } from "../types.js";
4
+ import { signAnswer, signAnswerWithKeyRecord } from "../proven/sign.js";
5
+ import { verifyAnswer as verifyProvenAnswer } from "../proven/verify.js";
6
+ import { selectBreakpointAnswer as selectPublicBreakpointAnswer } from "../backend.js";
7
+ export class GitNativeBackend {
8
+ name = "git-native";
9
+ breakpointsDir;
10
+ defaultPollIntervalMs;
11
+ defaultTimeoutMs;
12
+ signingKeyPath;
13
+ constructor(options) {
14
+ this.breakpointsDir = options?.breakpointsDir
15
+ ?? path.resolve(process.cwd(), BREAKPOINTS_DIR);
16
+ this.defaultPollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
17
+ this.defaultTimeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
18
+ this.signingKeyPath = options?.signingKeyPath;
19
+ }
20
+ breakpointPath(id) {
21
+ return path.join(this.breakpointsDir, `${id}.json`);
22
+ }
23
+ answerPath(id) {
24
+ return path.join(this.breakpointsDir, `${id}.answer.json`);
25
+ }
26
+ provenPath(id) {
27
+ return path.join(this.breakpointsDir, `${id}.proven.json`);
28
+ }
29
+ capabilities() {
30
+ return {
31
+ search: true,
32
+ bulkOperations: true,
33
+ assignment: true,
34
+ comments: true,
35
+ history: true,
36
+ metrics: true,
37
+ export: true,
38
+ forms: true,
39
+ notifications: false,
40
+ escalation: true,
41
+ };
42
+ }
43
+ /**
44
+ * Load the signing key from the configured signingKeyPath.
45
+ * Returns null if no signing key is configured or the file cannot be read.
46
+ */
47
+ async loadSigningKey() {
48
+ if (!this.signingKeyPath)
49
+ return null;
50
+ try {
51
+ const raw = await fs.readFile(this.signingKeyPath, "utf-8");
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /**
59
+ * Load a proven answer file for a breakpoint, if it exists.
60
+ */
61
+ async loadProvenAnswer(id) {
62
+ try {
63
+ const raw = await fs.readFile(this.provenPath(id), "utf-8");
64
+ return ProvenBreakpointAnswerSchema.parse(JSON.parse(raw));
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ async loadStoredAnswer(id) {
71
+ try {
72
+ const raw = await fs.readFile(this.answerPath(id), "utf-8");
73
+ return BreakpointPublicAnswerSchema.parse(JSON.parse(raw));
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ async loadPublicAnswer(id) {
80
+ const [storedAnswer, provenAnswer] = await Promise.all([
81
+ this.loadStoredAnswer(id),
82
+ this.loadProvenAnswer(id),
83
+ ]);
84
+ return provenAnswer ?? storedAnswer;
85
+ }
86
+ async writeBreakpoint(breakpoint) {
87
+ await fs.writeFile(this.breakpointPath(breakpoint.id), JSON.stringify(breakpoint, null, 2) + "\n", "utf-8");
88
+ }
89
+ historyEntry(args) {
90
+ return {
91
+ id: generateBreakpointId(),
92
+ at: new Date().toISOString(),
93
+ ...args,
94
+ };
95
+ }
96
+ auditEntry(args) {
97
+ return {
98
+ id: generateBreakpointId(),
99
+ at: new Date().toISOString(),
100
+ redacted: args.redacted ?? false,
101
+ action: args.action,
102
+ actorId: args.actorId,
103
+ metadata: args.metadata,
104
+ };
105
+ }
106
+ async listAllBreakpoints() {
107
+ let files;
108
+ try {
109
+ files = await fs.readdir(this.breakpointsDir);
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ const breakpoints = [];
115
+ for (const file of files) {
116
+ if (!file.endsWith(".json") || file.includes(".answer.") || file.includes(".proven.")) {
117
+ continue;
118
+ }
119
+ try {
120
+ const raw = await fs.readFile(path.join(this.breakpointsDir, file), "utf-8");
121
+ breakpoints.push(BreakpointSchema.parse(JSON.parse(raw)));
122
+ }
123
+ catch {
124
+ // Preserve existing malformed-file tolerance.
125
+ }
126
+ }
127
+ return breakpoints;
128
+ }
129
+ async assertBlockingDependenciesSatisfied(breakpoint) {
130
+ const blockingDependencies = breakpoint.dependsOn.filter((dependency) => dependency.blocking !== false);
131
+ if (blockingDependencies.length === 0)
132
+ return;
133
+ const unmet = [];
134
+ for (const dependency of blockingDependencies) {
135
+ const requiredStatus = dependency.requiredStatus ?? "completed";
136
+ try {
137
+ const dependencyBreakpoint = await this.getBreakpoint(dependency.id);
138
+ if (dependencyBreakpoint.status !== requiredStatus) {
139
+ unmet.push(`${dependency.id} is ${dependencyBreakpoint.status}, requires ${requiredStatus}`);
140
+ }
141
+ }
142
+ catch {
143
+ unmet.push(`${dependency.id} is missing, requires ${requiredStatus}`);
144
+ }
145
+ }
146
+ if (unmet.length > 0) {
147
+ throw new Error(`Invalid breakpoint status transition: blocking dependencies are not satisfied (${unmet.join("; ")})`);
148
+ }
149
+ }
150
+ async submitBreakpoint(params) {
151
+ await fs.mkdir(this.breakpointsDir, { recursive: true });
152
+ const id = generateBreakpointId();
153
+ const now = new Date().toISOString();
154
+ const timeoutMs = params.routing.timeoutMs || this.defaultTimeoutMs;
155
+ const breakpoint = {
156
+ id,
157
+ text: params.text,
158
+ context: params.context,
159
+ status: "pending",
160
+ priority: params.priority,
161
+ dependsOn: params.dependsOn ?? [],
162
+ routing: params.routing,
163
+ answers: [],
164
+ projectId: params.projectId,
165
+ repoId: params.repoId,
166
+ comments: [],
167
+ history: [
168
+ {
169
+ id: generateBreakpointId(),
170
+ type: "created",
171
+ at: now,
172
+ toStatus: "pending",
173
+ message: "Breakpoint created",
174
+ },
175
+ ],
176
+ auditLog: [
177
+ {
178
+ id: generateBreakpointId(),
179
+ action: "breakpoint.created",
180
+ at: now,
181
+ redacted: false,
182
+ },
183
+ ],
184
+ forms: [],
185
+ formSubmissions: [],
186
+ notifications: [],
187
+ metrics: {
188
+ answerCount: 0,
189
+ commentCount: 0,
190
+ },
191
+ createdAt: now,
192
+ updatedAt: now,
193
+ expiresAt: new Date(Date.now() + timeoutMs).toISOString(),
194
+ };
195
+ // Validate before writing
196
+ BreakpointSchema.parse(breakpoint);
197
+ await this.writeBreakpoint(BreakpointSchema.parse(breakpoint));
198
+ return breakpoint;
199
+ }
200
+ async getBreakpoint(id) {
201
+ const raw = await fs.readFile(this.breakpointPath(id), "utf-8");
202
+ const breakpoint = BreakpointSchema.parse(JSON.parse(raw));
203
+ const answer = await this.loadPublicAnswer(id);
204
+ if (answer) {
205
+ const existingIndex = breakpoint.answers.findIndex((candidate) => candidate.id === answer.id);
206
+ if (existingIndex === -1) {
207
+ breakpoint.answers.push(answer);
208
+ }
209
+ else {
210
+ breakpoint.answers[existingIndex] = answer;
211
+ }
212
+ if (breakpoint.status === "pending" || breakpoint.status === "claimed") {
213
+ breakpoint.status = "answered";
214
+ }
215
+ }
216
+ const selectedAnswer = selectPublicBreakpointAnswer(breakpoint);
217
+ if (selectedAnswer && isProvenBreakpointAnswer(selectedAnswer)) {
218
+ const verification = await this.verifyProvenFile(selectedAnswer);
219
+ // Attach verification result as metadata on the breakpoint
220
+ breakpoint
221
+ .provenVerification = verification;
222
+ }
223
+ return breakpoint;
224
+ }
225
+ async waitForAnswer(id, options) {
226
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
227
+ const pollIntervalMs = options?.pollIntervalMs ?? this.defaultPollIntervalMs;
228
+ const signal = options?.signal;
229
+ const startTime = Date.now();
230
+ while (true) {
231
+ if (signal?.aborted) {
232
+ const breakpoint = await this.getBreakpoint(id);
233
+ return {
234
+ answered: false,
235
+ breakpoint,
236
+ allAnswers: breakpoint.answers,
237
+ resolution: "aborted",
238
+ elapsedMs: Date.now() - startTime,
239
+ };
240
+ }
241
+ // Check for answer file
242
+ try {
243
+ await fs.access(this.answerPath(id));
244
+ const breakpoint = await this.getBreakpoint(id);
245
+ const answer = selectPublicBreakpointAnswer(breakpoint);
246
+ return {
247
+ answered: true,
248
+ breakpoint: { ...breakpoint, status: "answered" },
249
+ answer,
250
+ allAnswers: breakpoint.answers,
251
+ resolution: "answered",
252
+ elapsedMs: Date.now() - startTime,
253
+ };
254
+ }
255
+ catch {
256
+ // No answer yet
257
+ }
258
+ // Check cancellation status
259
+ const breakpoint = await this.getBreakpoint(id);
260
+ if (breakpoint.status === "cancelled") {
261
+ return {
262
+ answered: false,
263
+ breakpoint,
264
+ allAnswers: breakpoint.answers,
265
+ resolution: "cancelled",
266
+ elapsedMs: Date.now() - startTime,
267
+ };
268
+ }
269
+ if (Date.now() - startTime >= timeoutMs) {
270
+ return {
271
+ answered: false,
272
+ breakpoint,
273
+ allAnswers: breakpoint.answers,
274
+ resolution: "timeout",
275
+ elapsedMs: Date.now() - startTime,
276
+ };
277
+ }
278
+ // Wait before next poll
279
+ await new Promise((resolve) => {
280
+ const timer = setTimeout(resolve, pollIntervalMs);
281
+ if (signal) {
282
+ const onAbort = () => {
283
+ clearTimeout(timer);
284
+ resolve();
285
+ };
286
+ signal.addEventListener("abort", onAbort, { once: true });
287
+ }
288
+ });
289
+ }
290
+ }
291
+ async listPendingBreakpoints(responderId) {
292
+ let files;
293
+ try {
294
+ files = await fs.readdir(this.breakpointsDir);
295
+ }
296
+ catch {
297
+ return [];
298
+ }
299
+ const pending = [];
300
+ for (const file of files) {
301
+ if (!file.endsWith(".json") || file.includes(".answer.") || file.includes(".proven.")) {
302
+ continue;
303
+ }
304
+ try {
305
+ const raw = await fs.readFile(path.join(this.breakpointsDir, file), "utf-8");
306
+ const bp = BreakpointSchema.parse(JSON.parse(raw));
307
+ if (bp.status !== "pending" && bp.status !== "routed")
308
+ continue;
309
+ // Check expiration
310
+ if (new Date(bp.expiresAt) < new Date())
311
+ continue;
312
+ // Filter by responder if specified
313
+ if (responderId && bp.routing.targetResponders.length > 0) {
314
+ if (!bp.routing.targetResponders.includes(responderId))
315
+ continue;
316
+ }
317
+ // Check if answer already exists
318
+ try {
319
+ await fs.access(this.answerPath(bp.id));
320
+ continue; // Already answered
321
+ }
322
+ catch {
323
+ // No answer, include it
324
+ }
325
+ pending.push(bp);
326
+ }
327
+ catch {
328
+ // Skip malformed files
329
+ }
330
+ }
331
+ return pending;
332
+ }
333
+ async answerBreakpoint(id, answer) {
334
+ // Verify breakpoint exists
335
+ const existing = await this.getBreakpoint(id);
336
+ const validation = validateBreakpointTransition(existing.status, "answered");
337
+ if (!validation.valid) {
338
+ throw new Error(validation.reason);
339
+ }
340
+ await this.assertBlockingDependenciesSatisfied(existing);
341
+ const fromStatus = existing.status;
342
+ const answerId = generateBreakpointId();
343
+ const now = new Date().toISOString();
344
+ const breakpointAnswer = {
345
+ id: answerId,
346
+ breakpointId: id,
347
+ responderId: answer.responderId,
348
+ responderName: answer.responderName,
349
+ text: answer.text,
350
+ approved: answer.approved,
351
+ confidence: answer.confidence ?? 80,
352
+ references: answer.references ?? [],
353
+ followUpQuestions: answer.followUpQuestions ?? [],
354
+ answeredAt: now,
355
+ decisionMemory: answer.decisionMemory
356
+ ? { ...answer.decisionMemory, savedAt: now }
357
+ : undefined,
358
+ };
359
+ if (answer.sign === false && answer.keyFingerprint) {
360
+ throw new Error("keyFingerprint cannot be used when sign=false");
361
+ }
362
+ let publicAnswer = breakpointAnswer;
363
+ if (answer.keyFingerprint) {
364
+ publicAnswer = await signAnswer(breakpointAnswer, answer.keyFingerprint, this.breakpointsDir);
365
+ }
366
+ else if (answer.sign === true) {
367
+ const signingKey = await this.loadSigningKey();
368
+ if (!signingKey) {
369
+ throw new Error("answer_breakpoint.sign requested, but no signing key is configured");
370
+ }
371
+ publicAnswer = signAnswerWithKeyRecord(breakpointAnswer, signingKey);
372
+ }
373
+ else if (answer.sign !== false) {
374
+ const signingKey = await this.loadSigningKey();
375
+ if (signingKey) {
376
+ publicAnswer = signAnswerWithKeyRecord(breakpointAnswer, signingKey);
377
+ }
378
+ }
379
+ BreakpointPublicAnswerSchema.parse(publicAnswer);
380
+ await fs.writeFile(this.answerPath(id), JSON.stringify(publicAnswer, null, 2) + "\n", "utf-8");
381
+ if (isProvenBreakpointAnswer(publicAnswer)) {
382
+ await fs.writeFile(this.provenPath(id), JSON.stringify(publicAnswer, null, 2) + "\n", "utf-8");
383
+ }
384
+ else {
385
+ await fs.rm(this.provenPath(id), { force: true });
386
+ }
387
+ // Update the breakpoint status
388
+ const breakpoint = await this.getBreakpoint(id);
389
+ delete breakpoint.provenVerification;
390
+ breakpoint.status = "answered";
391
+ breakpoint.updatedAt = now;
392
+ breakpoint.history.push(this.historyEntry({
393
+ type: "answer",
394
+ actorId: answer.responderId,
395
+ fromStatus,
396
+ toStatus: "answered",
397
+ message: "Breakpoint answered",
398
+ }));
399
+ breakpoint.auditLog.push(this.auditEntry({
400
+ action: "breakpoint.answered",
401
+ actorId: answer.responderId,
402
+ }));
403
+ breakpoint.metrics = {
404
+ ...breakpoint.metrics,
405
+ answerCount: breakpoint.answers.length,
406
+ responseTimeMs: Date.parse(now) - Date.parse(breakpoint.createdAt),
407
+ };
408
+ await this.writeBreakpoint(BreakpointSchema.parse(breakpoint));
409
+ return publicAnswer;
410
+ }
411
+ async cancelBreakpoint(id) {
412
+ const breakpoint = await this.getBreakpoint(id);
413
+ delete breakpoint.provenVerification;
414
+ const validation = validateBreakpointTransition(breakpoint.status, "cancelled");
415
+ if (!validation.valid) {
416
+ throw new Error(validation.reason);
417
+ }
418
+ const fromStatus = breakpoint.status;
419
+ breakpoint.status = "cancelled";
420
+ breakpoint.updatedAt = new Date().toISOString();
421
+ breakpoint.history.push(this.historyEntry({
422
+ type: "status",
423
+ fromStatus,
424
+ toStatus: "cancelled",
425
+ message: "Breakpoint cancelled",
426
+ }));
427
+ breakpoint.auditLog.push(this.auditEntry({ action: "breakpoint.cancelled" }));
428
+ await this.writeBreakpoint(BreakpointSchema.parse(breakpoint));
429
+ }
430
+ async claimBreakpoint(id, responderId) {
431
+ const breakpoint = await this.getBreakpoint(id);
432
+ delete breakpoint.provenVerification;
433
+ const validation = validateBreakpointTransition(breakpoint.status, "claimed");
434
+ if (!validation.valid) {
435
+ throw new Error(validation.reason);
436
+ }
437
+ const fromStatus = breakpoint.status;
438
+ breakpoint.status = "claimed";
439
+ breakpoint.claimedByResponderId = responderId;
440
+ breakpoint.updatedAt = new Date().toISOString();
441
+ breakpoint.history.push(this.historyEntry({
442
+ type: "status",
443
+ actorId: responderId,
444
+ fromStatus,
445
+ toStatus: "claimed",
446
+ message: "Breakpoint claimed",
447
+ }));
448
+ breakpoint.auditLog.push(this.auditEntry({
449
+ action: "breakpoint.claimed",
450
+ actorId: responderId,
451
+ }));
452
+ await this.writeBreakpoint(BreakpointSchema.parse(breakpoint));
453
+ return breakpoint;
454
+ }
455
+ async assignBreakpoint(id, params) {
456
+ const breakpoint = await this.getBreakpoint(id);
457
+ delete breakpoint.provenVerification;
458
+ const validation = validateBreakpointTransition(breakpoint.status, "assigned");
459
+ if (!validation.valid) {
460
+ throw new Error(validation.reason);
461
+ }
462
+ const fromStatus = breakpoint.status;
463
+ breakpoint.status = "assigned";
464
+ breakpoint.assigneeId = params.assigneeId;
465
+ breakpoint.assigneeName = params.assigneeName;
466
+ breakpoint.updatedAt = new Date().toISOString();
467
+ breakpoint.history.push(this.historyEntry({
468
+ type: "assigned",
469
+ actorId: params.actorId,
470
+ fromStatus,
471
+ toStatus: "assigned",
472
+ message: `Assigned to ${params.assigneeName ?? params.assigneeId}`,
473
+ }));
474
+ breakpoint.auditLog.push(this.auditEntry({
475
+ action: "breakpoint.assigned",
476
+ actorId: params.actorId,
477
+ metadata: { assigneeId: params.assigneeId },
478
+ }));
479
+ const parsed = BreakpointSchema.parse(breakpoint);
480
+ await this.writeBreakpoint(parsed);
481
+ return parsed;
482
+ }
483
+ async transitionBreakpoint(id, params) {
484
+ const breakpoint = await this.getBreakpoint(id);
485
+ delete breakpoint.provenVerification;
486
+ const validation = validateBreakpointTransition(breakpoint.status, params.status);
487
+ if (!validation.valid) {
488
+ throw new Error(validation.reason);
489
+ }
490
+ if (params.status === "answered" || params.status === "completed") {
491
+ await this.assertBlockingDependenciesSatisfied(breakpoint);
492
+ }
493
+ const fromStatus = breakpoint.status;
494
+ breakpoint.status = params.status;
495
+ breakpoint.updatedAt = new Date().toISOString();
496
+ breakpoint.history.push(this.historyEntry({
497
+ type: "status",
498
+ actorId: params.actorId,
499
+ fromStatus,
500
+ toStatus: params.status,
501
+ message: params.message,
502
+ metadata: params.metadata,
503
+ }));
504
+ breakpoint.auditLog.push(this.auditEntry({
505
+ action: "status.changed",
506
+ actorId: params.actorId,
507
+ metadata: { fromStatus, toStatus: params.status },
508
+ }));
509
+ if (params.status === "completed") {
510
+ breakpoint.metrics = {
511
+ ...breakpoint.metrics,
512
+ completionTimeMs: Date.parse(breakpoint.updatedAt) - Date.parse(breakpoint.createdAt),
513
+ };
514
+ }
515
+ const parsed = BreakpointSchema.parse(breakpoint);
516
+ await this.writeBreakpoint(parsed);
517
+ return parsed;
518
+ }
519
+ async addBreakpointComment(id, params) {
520
+ const breakpoint = await this.getBreakpoint(id);
521
+ delete breakpoint.provenVerification;
522
+ const comment = {
523
+ id: generateBreakpointId(),
524
+ authorId: params.authorId,
525
+ authorName: params.authorName,
526
+ text: params.text,
527
+ createdAt: new Date().toISOString(),
528
+ metadata: params.metadata,
529
+ };
530
+ breakpoint.comments.push(comment);
531
+ breakpoint.updatedAt = comment.createdAt;
532
+ breakpoint.history.push(this.historyEntry({
533
+ type: "comment",
534
+ actorId: params.authorId,
535
+ message: "Comment added",
536
+ metadata: { commentId: comment.id },
537
+ }));
538
+ breakpoint.auditLog.push(this.auditEntry({
539
+ action: "comment.added",
540
+ actorId: params.authorId,
541
+ metadata: { commentId: comment.id },
542
+ }));
543
+ breakpoint.metrics = {
544
+ ...breakpoint.metrics,
545
+ commentCount: breakpoint.comments.length,
546
+ };
547
+ await this.writeBreakpoint(BreakpointSchema.parse(breakpoint));
548
+ return comment;
549
+ }
550
+ async searchBreakpoints(query) {
551
+ const all = await this.listAllBreakpoints();
552
+ const filtered = all.filter((breakpoint) => matchesSearchQuery(breakpoint, query));
553
+ const sorted = filtered.sort((left, right) => compareBreakpoints(left, right, query));
554
+ const offset = query.offset ?? 0;
555
+ const limit = query.limit ?? sorted.length;
556
+ return {
557
+ items: sorted.slice(offset, offset + limit),
558
+ total: sorted.length,
559
+ offset,
560
+ limit,
561
+ };
562
+ }
563
+ async bulkUpdateBreakpoints(params) {
564
+ const items = [];
565
+ for (const id of params.ids) {
566
+ try {
567
+ let breakpoint;
568
+ if (params.action === "reassign") {
569
+ if (!params.assigneeId)
570
+ throw new Error("assigneeId is required for reassign");
571
+ breakpoint = await this.assignBreakpoint(id, {
572
+ assigneeId: params.assigneeId,
573
+ assigneeName: params.assigneeName,
574
+ actorId: params.actorId,
575
+ });
576
+ }
577
+ else if (params.action === "cancel") {
578
+ await this.cancelBreakpoint(id);
579
+ breakpoint = await this.getBreakpoint(id);
580
+ }
581
+ else if (params.action === "close") {
582
+ breakpoint = await this.transitionBreakpoint(id, {
583
+ status: "completed",
584
+ actorId: params.actorId,
585
+ message: params.message,
586
+ });
587
+ }
588
+ else if (params.action === "transition") {
589
+ if (!params.status)
590
+ throw new Error("status is required for transition");
591
+ breakpoint = await this.transitionBreakpoint(id, {
592
+ status: params.status,
593
+ actorId: params.actorId,
594
+ message: params.message,
595
+ });
596
+ }
597
+ else {
598
+ if (!params.answer)
599
+ throw new Error("answer is required for approve");
600
+ await this.answerBreakpoint(id, { ...params.answer, approved: true });
601
+ breakpoint = await this.getBreakpoint(id);
602
+ }
603
+ items.push({ id, ok: true, breakpoint });
604
+ }
605
+ catch (error) {
606
+ items.push({
607
+ id,
608
+ ok: false,
609
+ errorCode: isNotFoundError(error) ? "not_found" : isInvalidTransitionError(error) ? "invalid_transition" : "error",
610
+ error: error instanceof Error ? error.message : String(error),
611
+ });
612
+ }
613
+ }
614
+ const succeeded = items.filter((item) => item.ok).length;
615
+ return {
616
+ total: params.ids.length,
617
+ succeeded,
618
+ failed: params.ids.length - succeeded,
619
+ items,
620
+ };
621
+ }
622
+ async getBreakpointMetrics(query = {}) {
623
+ const result = await this.searchBreakpoints(query);
624
+ const byStatus = {};
625
+ const byPriority = {};
626
+ const responseTimes = [];
627
+ const completionTimes = [];
628
+ for (const breakpoint of result.items) {
629
+ byStatus[breakpoint.status] = (byStatus[breakpoint.status] ?? 0) + 1;
630
+ const priority = breakpoint.priority ?? "medium";
631
+ byPriority[priority] = (byPriority[priority] ?? 0) + 1;
632
+ if (typeof breakpoint.metrics?.responseTimeMs === "number") {
633
+ responseTimes.push(breakpoint.metrics.responseTimeMs);
634
+ }
635
+ if (typeof breakpoint.metrics?.completionTimeMs === "number") {
636
+ completionTimes.push(breakpoint.metrics.completionTimeMs);
637
+ }
638
+ }
639
+ return {
640
+ total: result.total,
641
+ byStatus,
642
+ byPriority,
643
+ responseTimeAverageMs: average(responseTimes),
644
+ completionTimeAverageMs: average(completionTimes),
645
+ };
646
+ }
647
+ async exportBreakpoints(query = {}) {
648
+ const result = await this.searchBreakpoints(query);
649
+ return {
650
+ schemaVersion: 1,
651
+ exportedAt: new Date().toISOString(),
652
+ total: result.total,
653
+ items: result.items.map(redactBreakpointForExport),
654
+ };
655
+ }
656
+ /**
657
+ * Verify the selected public answer against trusted public keys.
658
+ */
659
+ async verifyAnswer(id) {
660
+ const breakpoint = await this.getBreakpoint(id);
661
+ const answer = selectPublicBreakpointAnswer(breakpoint);
662
+ if (!answer || !isProvenBreakpointAnswer(answer)) {
663
+ return {
664
+ valid: false,
665
+ reason: "No signed answer found",
666
+ verifiedAt: new Date().toISOString(),
667
+ };
668
+ }
669
+ return this.verifyProvenFile(answer);
670
+ }
671
+ /**
672
+ * Verify a loaded ProvenBreakpointAnswer against trusted keys in the
673
+ * breakpoints directory.
674
+ */
675
+ async verifyProvenFile(provenAnswer) {
676
+ // The proven/verify module's loadTrustedPublicKeys uses baseDir/.keys/trusted/
677
+ // Our breakpointsDir IS the .breakpoints directory, so we pass it as baseDir.
678
+ return verifyProvenAnswer(provenAnswer, this.breakpointsDir);
679
+ }
680
+ }
681
+ const PRIORITY_WEIGHT = {
682
+ low: 0,
683
+ medium: 1,
684
+ high: 2,
685
+ critical: 3,
686
+ };
687
+ function matchesSearchQuery(breakpoint, query) {
688
+ if (query.status && !query.status.includes(breakpoint.status))
689
+ return false;
690
+ if (query.priority && !query.priority.includes(breakpoint.priority ?? "medium"))
691
+ return false;
692
+ if (query.assigneeId && breakpoint.assigneeId !== query.assigneeId)
693
+ return false;
694
+ if (query.responderId && !matchesResponder(breakpoint, query.responderId))
695
+ return false;
696
+ if (query.domain && !breakpoint.context.domain?.toLowerCase().includes(query.domain.toLowerCase()))
697
+ return false;
698
+ if (query.tags && query.tags.length > 0) {
699
+ const tags = new Set(breakpoint.context.tags.map((tag) => tag.toLowerCase()));
700
+ if (!query.tags.every((tag) => tags.has(tag.toLowerCase())))
701
+ return false;
702
+ }
703
+ if (query.createdAfter && Date.parse(breakpoint.createdAt) < Date.parse(query.createdAfter))
704
+ return false;
705
+ if (query.createdBefore && Date.parse(breakpoint.createdAt) > Date.parse(query.createdBefore))
706
+ return false;
707
+ if (query.updatedAfter && Date.parse(breakpoint.updatedAt) < Date.parse(query.updatedAfter))
708
+ return false;
709
+ if (query.updatedBefore && Date.parse(breakpoint.updatedAt) > Date.parse(query.updatedBefore))
710
+ return false;
711
+ if (query.query && !searchText(breakpoint).includes(query.query.toLowerCase()))
712
+ return false;
713
+ return true;
714
+ }
715
+ function matchesResponder(breakpoint, responderId) {
716
+ return breakpoint.routing.targetResponders.includes(responderId) ||
717
+ breakpoint.claimedByResponderId === responderId ||
718
+ breakpoint.assigneeId === responderId ||
719
+ breakpoint.answers.some((answer) => answer.responderId === responderId);
720
+ }
721
+ function searchText(breakpoint) {
722
+ return [
723
+ breakpoint.id,
724
+ breakpoint.text,
725
+ breakpoint.assigneeId,
726
+ breakpoint.assigneeName,
727
+ breakpoint.context.title,
728
+ breakpoint.context.summary,
729
+ breakpoint.context.description,
730
+ breakpoint.context.domain,
731
+ ...breakpoint.context.tags,
732
+ ...breakpoint.comments.map((comment) => comment.text),
733
+ ].filter(Boolean).join("\n").toLowerCase();
734
+ }
735
+ function compareBreakpoints(left, right, query) {
736
+ const direction = query.sortDirection === "asc" ? 1 : -1;
737
+ const sortBy = query.sortBy ?? "createdAt";
738
+ if (sortBy === "priority") {
739
+ return (PRIORITY_WEIGHT[left.priority ?? "medium"] - PRIORITY_WEIGHT[right.priority ?? "medium"]) * direction;
740
+ }
741
+ const leftValue = sortBy === "status" ? left.status : left[sortBy];
742
+ const rightValue = sortBy === "status" ? right.status : right[sortBy];
743
+ return String(leftValue).localeCompare(String(rightValue)) * direction;
744
+ }
745
+ function average(values) {
746
+ if (values.length === 0)
747
+ return undefined;
748
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
749
+ }
750
+ function isNotFoundError(error) {
751
+ return error instanceof Error && ("code" in error && error.code === "ENOENT" ||
752
+ /no such file|not found/i.test(error.message));
753
+ }
754
+ function isInvalidTransitionError(error) {
755
+ return error instanceof Error && /transition|terminal|dependenc/i.test(error.message);
756
+ }
757
+ function redactBreakpointForExport(breakpoint) {
758
+ return BreakpointSchema.parse({
759
+ ...breakpoint,
760
+ context: {
761
+ ...breakpoint.context,
762
+ metadata: redactSecrets(breakpoint.context.metadata),
763
+ },
764
+ comments: breakpoint.comments.map((comment) => ({
765
+ ...comment,
766
+ metadata: redactSecrets(comment.metadata),
767
+ })),
768
+ auditLog: breakpoint.auditLog.map((entry) => ({
769
+ ...entry,
770
+ metadata: redactSecrets(entry.metadata),
771
+ redacted: entry.metadata ? true : entry.redacted,
772
+ })),
773
+ notifications: breakpoint.notifications.map((notification) => ({
774
+ ...notification,
775
+ target: notification.target ? "[redacted]" : undefined,
776
+ secretEnv: notification.secretEnv ? "[redacted]" : undefined,
777
+ })),
778
+ });
779
+ }
780
+ function redactSecrets(value) {
781
+ if (Array.isArray(value)) {
782
+ return value.map((item) => redactSecrets(item));
783
+ }
784
+ if (!value || typeof value !== "object") {
785
+ return value;
786
+ }
787
+ const redacted = {};
788
+ for (const [key, nested] of Object.entries(value)) {
789
+ if (/token|secret|password|authorization|apiKey|apiToken/i.test(key)) {
790
+ redacted[key] = "[redacted]";
791
+ }
792
+ else {
793
+ redacted[key] = redactSecrets(nested);
794
+ }
795
+ }
796
+ return redacted;
797
+ }