@a5c-ai/adapters-gateway 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.
Files changed (202) hide show
  1. package/README.md +20 -0
  2. package/dist/auth/bootstrap.d.ts +89 -0
  3. package/dist/auth/bootstrap.d.ts.map +1 -0
  4. package/dist/auth/bootstrap.js +222 -0
  5. package/dist/auth/bootstrap.js.map +1 -0
  6. package/dist/auth/hashing.d.ts +4 -0
  7. package/dist/auth/hashing.d.ts.map +1 -0
  8. package/dist/auth/hashing.js +27 -0
  9. package/dist/auth/hashing.js.map +1 -0
  10. package/dist/auth/middleware.d.ts +3 -0
  11. package/dist/auth/middleware.d.ts.map +1 -0
  12. package/dist/auth/middleware.js +17 -0
  13. package/dist/auth/middleware.js.map +1 -0
  14. package/dist/auth/tokens.d.ts +45 -0
  15. package/dist/auth/tokens.d.ts.map +1 -0
  16. package/dist/auth/tokens.js +186 -0
  17. package/dist/auth/tokens.js.map +1 -0
  18. package/dist/builtin-adapters.d.ts +17 -0
  19. package/dist/builtin-adapters.d.ts.map +1 -0
  20. package/dist/builtin-adapters.js +119 -0
  21. package/dist/builtin-adapters.js.map +1 -0
  22. package/dist/config.d.ts +37 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +97 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/fanout/client-conn.d.ts +20 -0
  27. package/dist/fanout/client-conn.d.ts.map +1 -0
  28. package/dist/fanout/client-conn.js +53 -0
  29. package/dist/fanout/client-conn.js.map +1 -0
  30. package/dist/fanout/subscriber.d.ts +12 -0
  31. package/dist/fanout/subscriber.d.ts.map +1 -0
  32. package/dist/fanout/subscriber.js +40 -0
  33. package/dist/fanout/subscriber.js.map +1 -0
  34. package/dist/index.d.ts +30 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +52 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/kanban/lib/config-loader.d.ts +29 -0
  39. package/dist/kanban/lib/config-loader.d.ts.map +1 -0
  40. package/dist/kanban/lib/config-loader.js +166 -0
  41. package/dist/kanban/lib/config-loader.js.map +1 -0
  42. package/dist/kanban/lib/config.d.ts +3 -0
  43. package/dist/kanban/lib/config.d.ts.map +1 -0
  44. package/dist/kanban/lib/config.js +6 -0
  45. package/dist/kanban/lib/config.js.map +1 -0
  46. package/dist/kanban/lib/create-global-registry.d.ts +28 -0
  47. package/dist/kanban/lib/create-global-registry.d.ts.map +1 -0
  48. package/dist/kanban/lib/create-global-registry.js +53 -0
  49. package/dist/kanban/lib/create-global-registry.js.map +1 -0
  50. package/dist/kanban/lib/dispatch-context-audit.d.ts +12 -0
  51. package/dist/kanban/lib/dispatch-context-audit.d.ts.map +1 -0
  52. package/dist/kanban/lib/dispatch-context-audit.js +44 -0
  53. package/dist/kanban/lib/dispatch-context-audit.js.map +1 -0
  54. package/dist/kanban/lib/error-handler.d.ts +28 -0
  55. package/dist/kanban/lib/error-handler.d.ts.map +1 -0
  56. package/dist/kanban/lib/error-handler.js +61 -0
  57. package/dist/kanban/lib/error-handler.js.map +1 -0
  58. package/dist/kanban/lib/global-registry.d.ts +49 -0
  59. package/dist/kanban/lib/global-registry.d.ts.map +1 -0
  60. package/dist/kanban/lib/global-registry.js +18 -0
  61. package/dist/kanban/lib/global-registry.js.map +1 -0
  62. package/dist/kanban/lib/parser.d.ts +36 -0
  63. package/dist/kanban/lib/parser.d.ts.map +1 -0
  64. package/dist/kanban/lib/parser.js +585 -0
  65. package/dist/kanban/lib/parser.js.map +1 -0
  66. package/dist/kanban/lib/path-resolver.d.ts +2 -0
  67. package/dist/kanban/lib/path-resolver.d.ts.map +1 -0
  68. package/dist/kanban/lib/path-resolver.js +16 -0
  69. package/dist/kanban/lib/path-resolver.js.map +1 -0
  70. package/dist/kanban/lib/review-service.d.ts +63 -0
  71. package/dist/kanban/lib/review-service.d.ts.map +1 -0
  72. package/dist/kanban/lib/review-service.js +571 -0
  73. package/dist/kanban/lib/review-service.js.map +1 -0
  74. package/dist/kanban/lib/run-cache.d.ts +36 -0
  75. package/dist/kanban/lib/run-cache.d.ts.map +1 -0
  76. package/dist/kanban/lib/run-cache.js +313 -0
  77. package/dist/kanban/lib/run-cache.js.map +1 -0
  78. package/dist/kanban/lib/server-init.d.ts +26 -0
  79. package/dist/kanban/lib/server-init.d.ts.map +1 -0
  80. package/dist/kanban/lib/server-init.js +179 -0
  81. package/dist/kanban/lib/server-init.js.map +1 -0
  82. package/dist/kanban/lib/services/automation-rule-service.d.ts +97 -0
  83. package/dist/kanban/lib/services/automation-rule-service.d.ts.map +1 -0
  84. package/dist/kanban/lib/services/automation-rule-service.js +806 -0
  85. package/dist/kanban/lib/services/automation-rule-service.js.map +1 -0
  86. package/dist/kanban/lib/services/automation-webhook-service.d.ts +44 -0
  87. package/dist/kanban/lib/services/automation-webhook-service.d.ts.map +1 -0
  88. package/dist/kanban/lib/services/automation-webhook-service.js +405 -0
  89. package/dist/kanban/lib/services/automation-webhook-service.js.map +1 -0
  90. package/dist/kanban/lib/services/backlog-query-service.d.ts +130 -0
  91. package/dist/kanban/lib/services/backlog-query-service.d.ts.map +1 -0
  92. package/dist/kanban/lib/services/backlog-query-service.js +1972 -0
  93. package/dist/kanban/lib/services/backlog-query-service.js.map +1 -0
  94. package/dist/kanban/lib/services/dispatch-context-label-service.d.ts +39 -0
  95. package/dist/kanban/lib/services/dispatch-context-label-service.d.ts.map +1 -0
  96. package/dist/kanban/lib/services/dispatch-context-label-service.js +160 -0
  97. package/dist/kanban/lib/services/dispatch-context-label-service.js.map +1 -0
  98. package/dist/kanban/lib/services/kanban-storage.d.ts +36 -0
  99. package/dist/kanban/lib/services/kanban-storage.d.ts.map +1 -0
  100. package/dist/kanban/lib/services/kanban-storage.js +26 -0
  101. package/dist/kanban/lib/services/kanban-storage.js.map +1 -0
  102. package/dist/kanban/lib/services/run-query-service.d.ts +79 -0
  103. package/dist/kanban/lib/services/run-query-service.d.ts.map +1 -0
  104. package/dist/kanban/lib/services/run-query-service.js +202 -0
  105. package/dist/kanban/lib/services/run-query-service.js.map +1 -0
  106. package/dist/kanban/lib/services/task-tag-service.d.ts +39 -0
  107. package/dist/kanban/lib/services/task-tag-service.d.ts.map +1 -0
  108. package/dist/kanban/lib/services/task-tag-service.js +145 -0
  109. package/dist/kanban/lib/services/task-tag-service.js.map +1 -0
  110. package/dist/kanban/lib/settings-section-storage.d.ts +13 -0
  111. package/dist/kanban/lib/settings-section-storage.d.ts.map +1 -0
  112. package/dist/kanban/lib/settings-section-storage.js +38 -0
  113. package/dist/kanban/lib/settings-section-storage.js.map +1 -0
  114. package/dist/kanban/lib/source-discovery.d.ts +10 -0
  115. package/dist/kanban/lib/source-discovery.d.ts.map +1 -0
  116. package/dist/kanban/lib/source-discovery.js +201 -0
  117. package/dist/kanban/lib/source-discovery.js.map +1 -0
  118. package/dist/kanban/lib/utils.d.ts +8 -0
  119. package/dist/kanban/lib/utils.d.ts.map +1 -0
  120. package/dist/kanban/lib/utils.js +116 -0
  121. package/dist/kanban/lib/utils.js.map +1 -0
  122. package/dist/kanban/lib/watcher.d.ts +14 -0
  123. package/dist/kanban/lib/watcher.d.ts.map +1 -0
  124. package/dist/kanban/lib/watcher.js +221 -0
  125. package/dist/kanban/lib/watcher.js.map +1 -0
  126. package/dist/kanban/lib/workspace-lifecycle.d.ts +68 -0
  127. package/dist/kanban/lib/workspace-lifecycle.d.ts.map +1 -0
  128. package/dist/kanban/lib/workspace-lifecycle.js +1085 -0
  129. package/dist/kanban/lib/workspace-lifecycle.js.map +1 -0
  130. package/dist/kanban/routes.d.ts +2 -0
  131. package/dist/kanban/routes.d.ts.map +1 -0
  132. package/dist/kanban/routes.js +1358 -0
  133. package/dist/kanban/routes.js.map +1 -0
  134. package/dist/kanban/types/breakpoint.d.ts +13 -0
  135. package/dist/kanban/types/breakpoint.d.ts.map +1 -0
  136. package/dist/kanban/types/breakpoint.js +3 -0
  137. package/dist/kanban/types/breakpoint.js.map +1 -0
  138. package/dist/kanban/types/index.d.ts +173 -0
  139. package/dist/kanban/types/index.d.ts.map +1 -0
  140. package/dist/kanban/types/index.js +3 -0
  141. package/dist/kanban/types/index.js.map +1 -0
  142. package/dist/logging.d.ts +7 -0
  143. package/dist/logging.d.ts.map +1 -0
  144. package/dist/logging.js +22 -0
  145. package/dist/logging.js.map +1 -0
  146. package/dist/notifications/types.d.ts +18 -0
  147. package/dist/notifications/types.d.ts.map +1 -0
  148. package/dist/notifications/types.js +2 -0
  149. package/dist/notifications/types.js.map +1 -0
  150. package/dist/notifications/webhook-out.d.ts +3 -0
  151. package/dist/notifications/webhook-out.d.ts.map +1 -0
  152. package/dist/notifications/webhook-out.js +55 -0
  153. package/dist/notifications/webhook-out.js.map +1 -0
  154. package/dist/pairing/short-code.d.ts +20 -0
  155. package/dist/pairing/short-code.d.ts.map +1 -0
  156. package/dist/pairing/short-code.js +50 -0
  157. package/dist/pairing/short-code.js.map +1 -0
  158. package/dist/protocol/errors.d.ts +10 -0
  159. package/dist/protocol/errors.d.ts.map +1 -0
  160. package/dist/protocol/errors.js +15 -0
  161. package/dist/protocol/errors.js.map +1 -0
  162. package/dist/protocol/frames.d.ts +107 -0
  163. package/dist/protocol/frames.d.ts.map +1 -0
  164. package/dist/protocol/frames.js +146 -0
  165. package/dist/protocol/frames.js.map +1 -0
  166. package/dist/protocol/v1.d.ts +111 -0
  167. package/dist/protocol/v1.d.ts.map +1 -0
  168. package/dist/protocol/v1.js +2 -0
  169. package/dist/protocol/v1.js.map +1 -0
  170. package/dist/runs/event-log-index.d.ts +29 -0
  171. package/dist/runs/event-log-index.d.ts.map +1 -0
  172. package/dist/runs/event-log-index.js +210 -0
  173. package/dist/runs/event-log-index.js.map +1 -0
  174. package/dist/runs/event-log.d.ts +25 -0
  175. package/dist/runs/event-log.d.ts.map +1 -0
  176. package/dist/runs/event-log.js +104 -0
  177. package/dist/runs/event-log.js.map +1 -0
  178. package/dist/runs/hook-broker.d.ts +18 -0
  179. package/dist/runs/hook-broker.d.ts.map +1 -0
  180. package/dist/runs/hook-broker.js +110 -0
  181. package/dist/runs/hook-broker.js.map +1 -0
  182. package/dist/runs/manager.d.ts +57 -0
  183. package/dist/runs/manager.d.ts.map +1 -0
  184. package/dist/runs/manager.js +753 -0
  185. package/dist/runs/manager.js.map +1 -0
  186. package/dist/runs/session-runtime.d.ts +8 -0
  187. package/dist/runs/session-runtime.d.ts.map +1 -0
  188. package/dist/runs/session-runtime.js +291 -0
  189. package/dist/runs/session-runtime.js.map +1 -0
  190. package/dist/runs/types.d.ts +55 -0
  191. package/dist/runs/types.d.ts.map +1 -0
  192. package/dist/runs/types.js +2 -0
  193. package/dist/runs/types.js.map +1 -0
  194. package/dist/server.d.ts +15 -0
  195. package/dist/server.d.ts.map +1 -0
  196. package/dist/server.js +702 -0
  197. package/dist/server.js.map +1 -0
  198. package/dist/static/webui-server.d.ts +2 -0
  199. package/dist/static/webui-server.d.ts.map +1 -0
  200. package/dist/static/webui-server.js +97 -0
  201. package/dist/static/webui-server.js.map +1 -0
  202. package/package.json +68 -0
@@ -0,0 +1,1358 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import crypto from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { createClient, validateProfileData, } from '@a5c-ai/comm-adapter';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { getRunCached, getAllCachedDigests, discoverAndCacheAll } from './lib/run-cache.js';
7
+ import { AppError, normalizeError } from './lib/error-handler.js';
8
+ import { findRunDir } from './lib/path-resolver.js';
9
+ import { parseJournalDir, parseTaskDetail } from './lib/parser.js';
10
+ import { ReviewService } from './lib/review-service.js';
11
+ import { ensureInitialized, serverEvents } from './lib/server-init.js';
12
+ import { AutomationRuleService, isAutomationRuleState, isAutomationTriggerType, } from './lib/services/automation-rule-service.js';
13
+ import { AutomationWebhookService } from './lib/services/automation-webhook-service.js';
14
+ import { BacklogQueryService } from './lib/services/backlog-query-service.js';
15
+ import { DispatchContextLabelService } from './lib/services/dispatch-context-label-service.js';
16
+ import { RunQueryService } from './lib/services/run-query-service.js';
17
+ import { TaskTagService } from './lib/services/task-tag-service.js';
18
+ import { loadSettingsSectionStorage, writeSettingsSectionStorage, } from './lib/settings-section-storage.js';
19
+ import { WorkspaceLifecycleService } from './lib/workspace-lifecycle.js';
20
+ const NO_CACHE_HEADERS = { 'Cache-Control': 'no-cache, no-store' };
21
+ const backlogService = new BacklogQueryService();
22
+ const taskTagService = new TaskTagService();
23
+ const dispatchContextLabelService = new DispatchContextLabelService();
24
+ const reviewService = new ReviewService();
25
+ const automationRuleService = new AutomationRuleService();
26
+ const automationWebhookService = new AutomationWebhookService();
27
+ const workspaceService = new WorkspaceLifecycleService();
28
+ const runQueryService = new RunQueryService();
29
+ const nextId = () => randomUUID();
30
+ function jsonWithHeaders(body, status = 200, headers) {
31
+ return Response.json(body, {
32
+ status,
33
+ headers: {
34
+ ...NO_CACHE_HEADERS,
35
+ ...(headers ?? {}),
36
+ },
37
+ });
38
+ }
39
+ function jsonError(error) {
40
+ const normalized = normalizeError(error);
41
+ return Response.json({ error: normalized.message, code: normalized.code }, { status: normalized.status });
42
+ }
43
+ function isValidId(id) {
44
+ return /^[a-zA-Z0-9_\-]+$/.test(id);
45
+ }
46
+ function isWorkflowState(value) {
47
+ return value === 'todo' || value === 'in-progress' || value === 'review' || value === 'done';
48
+ }
49
+ function isCollaboratorRole(value) {
50
+ return value === 'owner' || value === 'maintainer' || value === 'contributor' || value === 'viewer';
51
+ }
52
+ function isVisibility(value) {
53
+ return value === 'private' || value === 'team' || value === 'workspace-shared';
54
+ }
55
+ function isActivityScope(value) {
56
+ return value === 'project-and-issues' || value === 'all-board-entities';
57
+ }
58
+ function isWorkspaceProvisioning(value) {
59
+ return value === 'owners-maintainers' || value === 'contributors-and-up';
60
+ }
61
+ function readQueryValues(searchParams, name) {
62
+ return Array.from(new Set(searchParams
63
+ .getAll(name)
64
+ .flatMap((value) => value.split(','))
65
+ .map((value) => value.trim())
66
+ .filter(Boolean)));
67
+ }
68
+ function parseAutomationQuery(url) {
69
+ const { searchParams } = url;
70
+ const states = readQueryValues(searchParams, 'state');
71
+ for (const state of states) {
72
+ if (!isAutomationRuleState(state)) {
73
+ throw new AppError(`Invalid state query value: ${state}`, 'BAD_REQUEST', 400);
74
+ }
75
+ }
76
+ const triggerTypes = readQueryValues(searchParams, 'triggerType');
77
+ for (const triggerType of triggerTypes) {
78
+ if (!isAutomationTriggerType(triggerType)) {
79
+ throw new AppError(`Invalid triggerType query value: ${triggerType}`, 'BAD_REQUEST', 400);
80
+ }
81
+ }
82
+ return {
83
+ state: states,
84
+ triggerType: triggerTypes,
85
+ projectId: searchParams.get('projectId')?.trim() || undefined,
86
+ boardProjectId: searchParams.get('boardProjectId')?.trim() || undefined,
87
+ search: searchParams.get('search')?.trim() || undefined,
88
+ includeArchived: searchParams.get('includeArchived') === 'true',
89
+ };
90
+ }
91
+ function readRuntime(value) {
92
+ if (!value || typeof value !== 'object') {
93
+ return undefined;
94
+ }
95
+ return value;
96
+ }
97
+ function readSessions(body) {
98
+ if (!body || typeof body !== 'object' || !Array.isArray(body.sessions)) {
99
+ return [];
100
+ }
101
+ return body.sessions.flatMap((value) => {
102
+ if (!value || typeof value !== 'object') {
103
+ return [];
104
+ }
105
+ const session = value;
106
+ if (typeof session.sessionId !== 'string' || typeof session.agent !== 'string' || typeof session.status !== 'string') {
107
+ return [];
108
+ }
109
+ return [
110
+ {
111
+ sessionId: session.sessionId,
112
+ agent: session.agent,
113
+ status: session.status === 'active' ? 'active' : 'inactive',
114
+ cwd: typeof session.cwd === 'string' ? session.cwd : undefined,
115
+ title: typeof session.title === 'string' ? session.title : undefined,
116
+ updatedAt: typeof session.updatedAt === 'number' ? session.updatedAt : undefined,
117
+ activeRunId: typeof session.activeRunId === 'string' ? session.activeRunId : null,
118
+ latestRunId: typeof session.latestRunId === 'string' ? session.latestRunId : null,
119
+ runtime: readRuntime(session.runtime),
120
+ },
121
+ ];
122
+ });
123
+ }
124
+ function buildReviewByWorkspacePath(artifacts) {
125
+ return new Map(artifacts.map((artifact) => [
126
+ artifact.targetId,
127
+ {
128
+ decision: artifact.decision,
129
+ queueState: artifact.queueState,
130
+ commentCount: artifact.comments.length,
131
+ openCommentCount: artifact.comments.filter((comment) => comment.status === 'open').length,
132
+ latestActivityAt: artifact.updatedAt,
133
+ },
134
+ ]));
135
+ }
136
+ async function buildLinkedIssuesByWorkspacePath() {
137
+ const overview = await backlogService.getOverview();
138
+ const projectById = new Map(overview.snapshot.projects.map((project) => [project.id, project]));
139
+ const map = new Map();
140
+ for (const issue of overview.snapshot.issues) {
141
+ const project = projectById.get(issue.projectId);
142
+ for (const workspaceLink of issue.workspaceLinks ?? []) {
143
+ const current = map.get(workspaceLink.workspacePath) ?? [];
144
+ current.push({
145
+ issueId: issue.id,
146
+ issueKey: issue.key,
147
+ issueTitle: issue.title,
148
+ projectId: issue.projectId,
149
+ projectKey: project?.key ?? issue.projectId,
150
+ projectName: project?.name ?? issue.projectId,
151
+ linkedAt: workspaceLink.linkedAt,
152
+ source: workspaceLink.source,
153
+ });
154
+ map.set(workspaceLink.workspacePath, current);
155
+ }
156
+ }
157
+ return map;
158
+ }
159
+ function extractRunId(runDir) {
160
+ return path.basename(runDir);
161
+ }
162
+ async function loadPreviewFile(runDir, filePath) {
163
+ const candidates = new Set();
164
+ if (path.isAbsolute(filePath)) {
165
+ candidates.add(filePath);
166
+ }
167
+ else {
168
+ candidates.add(path.join(runDir, filePath));
169
+ try {
170
+ const runJson = JSON.parse(await fs.readFile(path.join(runDir, 'run.json'), 'utf8'));
171
+ if (typeof runJson.cwd === 'string' && runJson.cwd.length > 0) {
172
+ candidates.add(path.join(runJson.cwd, filePath));
173
+ }
174
+ }
175
+ catch {
176
+ // Ignore missing/invalid run.json when previewing files.
177
+ }
178
+ }
179
+ for (const candidate of candidates) {
180
+ try {
181
+ return await fs.readFile(candidate, 'utf8');
182
+ }
183
+ catch {
184
+ // Try the next candidate.
185
+ }
186
+ }
187
+ return null;
188
+ }
189
+ async function getNextJournalSeq(journalDir) {
190
+ try {
191
+ const files = await fs.readdir(journalDir);
192
+ let max = 0;
193
+ for (const file of files) {
194
+ const seq = Number(file.split('.')[0]);
195
+ if (Number.isFinite(seq) && seq > max) {
196
+ max = seq;
197
+ }
198
+ }
199
+ return max + 1;
200
+ }
201
+ catch {
202
+ return 1;
203
+ }
204
+ }
205
+ async function appendEffectResolvedJournalEntry(runDir, effectId, now) {
206
+ const journalDir = path.join(runDir, 'journal');
207
+ await fs.mkdir(journalDir, { recursive: true });
208
+ const seq = await getNextJournalSeq(journalDir);
209
+ const ulid = nextId();
210
+ const filename = `${seq.toString().padStart(6, '0')}.${ulid}.json`;
211
+ const eventPayload = {
212
+ type: 'EFFECT_RESOLVED',
213
+ recordedAt: now,
214
+ data: {
215
+ effectId,
216
+ status: 'ok',
217
+ resultRef: `tasks/${effectId}/result.json`,
218
+ startedAt: now,
219
+ finishedAt: now,
220
+ },
221
+ };
222
+ const eventContents = JSON.stringify(eventPayload, null, 2) + '\n';
223
+ const checksum = crypto.createHash('sha256').update(eventContents).digest('hex');
224
+ const payload = JSON.stringify({ ...eventPayload, checksum }, null, 2) + '\n';
225
+ await fs.writeFile(path.join(journalDir, filename), payload, 'utf8');
226
+ }
227
+ async function approveBreakpointEffect(runId, effectId, answer) {
228
+ if (!runId || !effectId || !answer.trim()) {
229
+ throw new AppError('Run ID, effect ID, and answer are required', 'BAD_REQUEST', 400);
230
+ }
231
+ if (!isValidId(runId) || !isValidId(effectId)) {
232
+ throw new AppError('Invalid run ID or effect ID', 'BAD_REQUEST', 400);
233
+ }
234
+ const found = await findRunDir(runId);
235
+ if (!found) {
236
+ throw new AppError(`Run not found: ${runId}`, 'NOT_FOUND', 404);
237
+ }
238
+ const taskDir = path.join(found.runDir, 'tasks', effectId);
239
+ try {
240
+ await fs.access(taskDir);
241
+ }
242
+ catch {
243
+ throw new AppError(`Task directory not found: ${effectId}`, 'NOT_FOUND', 404);
244
+ }
245
+ const now = new Date().toISOString();
246
+ const resultPayload = {
247
+ status: 'ok',
248
+ value: {
249
+ answer: answer.trim(),
250
+ approvedAt: now,
251
+ approvedBy: 'adapters-webui',
252
+ },
253
+ startedAt: now,
254
+ finishedAt: now,
255
+ };
256
+ await fs.writeFile(path.join(taskDir, 'result.json'), JSON.stringify(resultPayload, null, 2), 'utf8');
257
+ await appendEffectResolvedJournalEntry(found.runDir, effectId, now);
258
+ }
259
+ async function buildAgentConfigurationResponse() {
260
+ const client = createClient();
261
+ const storage = await loadSettingsSectionStorage();
262
+ return {
263
+ agents: client.adapters.list().map((adapter) => {
264
+ const stored = storage.agentConfiguration[adapter.agent] ?? {};
265
+ const defaultModel = client.models.defaultModel(adapter.agent)?.modelId ?? '';
266
+ return {
267
+ agent: adapter.agent,
268
+ displayName: adapter.displayName,
269
+ configuredModel: stored.model ?? '',
270
+ configuredProvider: stored.provider ?? '',
271
+ approvalMode: stored.approvalMode ?? 'prompt',
272
+ maxTokens: stored.maxTokens == null ? '' : String(stored.maxTokens),
273
+ availableModels: client.models.catalog(adapter.agent).map((model) => ({
274
+ modelId: model.modelId,
275
+ provider: model.provider,
276
+ isDefault: model.isDefault,
277
+ deprecated: model.deprecated,
278
+ successorModelId: model.successorModelId,
279
+ })),
280
+ defaultModel,
281
+ };
282
+ }),
283
+ };
284
+ }
285
+ function toMcpDraft(server) {
286
+ return {
287
+ name: server.name,
288
+ transport: server.transport,
289
+ command: server.command ?? '',
290
+ url: server.url ?? '',
291
+ argsText: (server.args ?? []).join('\n'),
292
+ envText: Object.entries(server.env ?? {})
293
+ .map(([key, value]) => `${key}=${value}`)
294
+ .join('\n'),
295
+ };
296
+ }
297
+ function parseEnv(envText) {
298
+ const env = {};
299
+ for (const line of envText.split('\n').map((entry) => entry.trim()).filter(Boolean)) {
300
+ const separatorIndex = line.indexOf('=');
301
+ if (separatorIndex <= 0) {
302
+ throw new Error(`Invalid env line "${line}". Use KEY=value.`);
303
+ }
304
+ env[line.slice(0, separatorIndex)] = line.slice(separatorIndex + 1);
305
+ }
306
+ return env;
307
+ }
308
+ function toMcpConfig(draft) {
309
+ return {
310
+ name: draft.name.trim(),
311
+ transport: draft.transport,
312
+ ...(draft.transport === 'stdio' ? { command: draft.command.trim() } : {}),
313
+ ...(draft.transport !== 'stdio' ? { url: draft.url.trim() } : {}),
314
+ ...(draft.argsText.trim()
315
+ ? {
316
+ args: draft.argsText
317
+ .split('\n')
318
+ .map((entry) => entry.trim())
319
+ .filter(Boolean),
320
+ }
321
+ : {}),
322
+ ...(draft.envText.trim() ? { env: parseEnv(draft.envText) } : {}),
323
+ };
324
+ }
325
+ async function buildMcpServerResponse() {
326
+ const client = createClient();
327
+ const storage = await loadSettingsSectionStorage();
328
+ return {
329
+ agents: client.adapters.list().map((adapter) => ({
330
+ agent: adapter.agent,
331
+ displayName: adapter.displayName,
332
+ servers: (storage.mcpServers[adapter.agent] ?? []).map(toMcpDraft),
333
+ })),
334
+ };
335
+ }
336
+ function readLifecycleAction(value) {
337
+ if (value === 'enable' || value === 'pause' || value === 'resume' || value === 'disable') {
338
+ return value;
339
+ }
340
+ throw new AppError('action must be enable, pause, resume, or disable.', 'BAD_REQUEST', 400);
341
+ }
342
+ export function registerKanbanRoutes(app) {
343
+ app.get('/api/backlog', async () => {
344
+ try {
345
+ await ensureInitialized();
346
+ return jsonWithHeaders(await backlogService.getOverview());
347
+ }
348
+ catch (error) {
349
+ return jsonError(error);
350
+ }
351
+ });
352
+ app.post('/api/backlog', async (context) => {
353
+ try {
354
+ await ensureInitialized();
355
+ const body = await context.req.json();
356
+ let overview;
357
+ switch (body.action) {
358
+ case 'move-issue':
359
+ if (typeof body.issueId !== 'string' || !isWorkflowState(body.toState)) {
360
+ throw new AppError('issueId and toState are required.', 'BAD_REQUEST', 400);
361
+ }
362
+ overview = await backlogService.moveIssue({ issueId: body.issueId, toState: body.toState });
363
+ break;
364
+ case 'link-repository':
365
+ if (typeof body.issueId !== 'string' ||
366
+ typeof body.owner !== 'string' ||
367
+ typeof body.name !== 'string' ||
368
+ typeof body.branchName !== 'string') {
369
+ throw new AppError('issueId, owner, name, and branchName are required.', 'BAD_REQUEST', 400);
370
+ }
371
+ overview = await backlogService.linkRepository({
372
+ issueId: body.issueId,
373
+ owner: body.owner,
374
+ name: body.name,
375
+ branchName: body.branchName,
376
+ defaultBranch: typeof body.defaultBranch === 'string' ? body.defaultBranch : undefined,
377
+ provider: body.provider === 'azure-repos' ||
378
+ body.provider === 'gitlab' ||
379
+ body.provider === 'bitbucket' ||
380
+ body.provider === 'local'
381
+ ? body.provider
382
+ : 'github',
383
+ });
384
+ break;
385
+ case 'update-repository-settings':
386
+ if (typeof body.issueId !== 'string') {
387
+ throw new AppError('issueId is required.', 'BAD_REQUEST', 400);
388
+ }
389
+ overview = await backlogService.updateRepositorySettings({
390
+ issueId: body.issueId,
391
+ settings: {
392
+ baseBranch: typeof body.baseBranch === 'string' ? body.baseBranch : undefined,
393
+ ciProvider: typeof body.ciProvider === 'string' ? body.ciProvider : undefined,
394
+ publishTarget: typeof body.publishTarget === 'string' ? body.publishTarget : undefined,
395
+ autoMerge: typeof body.autoMerge === 'boolean' ? body.autoMerge : undefined,
396
+ requiredApprovals: typeof body.requiredApprovals === 'number' ? body.requiredApprovals : undefined,
397
+ },
398
+ });
399
+ break;
400
+ case 'create-pull-request':
401
+ if (typeof body.issueId !== 'string' || typeof body.title !== 'string') {
402
+ throw new AppError('issueId and title are required.', 'BAD_REQUEST', 400);
403
+ }
404
+ overview = await backlogService.createPullRequest({
405
+ issueId: body.issueId,
406
+ title: body.title,
407
+ reviewers: typeof body.reviewers === 'string' ? body.reviewers : undefined,
408
+ });
409
+ break;
410
+ case 'create-issue': {
411
+ if (typeof body.projectId !== 'string' || typeof body.title !== 'string') {
412
+ throw new AppError('projectId and title are required.', 'BAD_REQUEST', 400);
413
+ }
414
+ const created = await backlogService.createIssue({
415
+ projectId: body.projectId,
416
+ title: body.title,
417
+ summary: typeof body.summary === 'string' ? body.summary : undefined,
418
+ description: typeof body.description === 'string' ? body.description : undefined,
419
+ status: body.status === 'backlog' ||
420
+ body.status === 'ready' ||
421
+ body.status === 'in-progress' ||
422
+ body.status === 'blocked' ||
423
+ body.status === 'review' ||
424
+ body.status === 'done'
425
+ ? body.status
426
+ : undefined,
427
+ priority: body.priority === 'critical' ||
428
+ body.priority === 'high' ||
429
+ body.priority === 'medium' ||
430
+ body.priority === 'low'
431
+ ? body.priority
432
+ : undefined,
433
+ labelIds: Array.isArray(body.labelIds) && body.labelIds.every((id) => typeof id === 'string')
434
+ ? body.labelIds
435
+ : undefined,
436
+ assigneeIds: Array.isArray(body.assigneeIds) && body.assigneeIds.every((id) => typeof id === 'string')
437
+ ? body.assigneeIds
438
+ : undefined,
439
+ dependencies: Array.isArray(body.dependencies)
440
+ ? body.dependencies
441
+ .map((dependency) => {
442
+ if (!dependency || typeof dependency !== 'object')
443
+ return null;
444
+ const entry = dependency;
445
+ if (typeof entry.issueId !== 'string')
446
+ return null;
447
+ return {
448
+ issueId: entry.issueId,
449
+ type: entry.type === 'blocks' || entry.type === 'related' || entry.type === 'blocked-by'
450
+ ? entry.type
451
+ : undefined,
452
+ };
453
+ })
454
+ .filter(Boolean)
455
+ : undefined,
456
+ acceptanceCriteria: Array.isArray(body.acceptanceCriteria)
457
+ ? body.acceptanceCriteria
458
+ .map((criterion) => {
459
+ if (!criterion || typeof criterion !== 'object')
460
+ return null;
461
+ const entry = criterion;
462
+ if (typeof entry.title !== 'string')
463
+ return null;
464
+ return {
465
+ id: typeof entry.id === 'string' ? entry.id : undefined,
466
+ title: entry.title,
467
+ satisfied: typeof entry.satisfied === 'boolean' ? entry.satisfied : undefined,
468
+ notes: typeof entry.notes === 'string' ? entry.notes : undefined,
469
+ };
470
+ })
471
+ .filter(Boolean)
472
+ : undefined,
473
+ metadata: body.metadata && typeof body.metadata === 'object'
474
+ ? body.metadata
475
+ : undefined,
476
+ });
477
+ return jsonWithHeaders(created);
478
+ }
479
+ case 'update-project-collaboration':
480
+ if (typeof body.projectId !== 'string') {
481
+ throw new AppError('projectId is required.', 'BAD_REQUEST', 400);
482
+ }
483
+ overview = await backlogService.updateProjectCollaboration({
484
+ projectId: body.projectId,
485
+ teamName: typeof body.teamName === 'string' ? body.teamName : undefined,
486
+ visibility: isVisibility(body.visibility) ? body.visibility : undefined,
487
+ defaultRole: isCollaboratorRole(body.defaultRole) ? body.defaultRole : undefined,
488
+ allowSelfAssign: typeof body.allowSelfAssign === 'boolean' ? body.allowSelfAssign : undefined,
489
+ reviewRequiredForDone: typeof body.reviewRequiredForDone === 'boolean' ? body.reviewRequiredForDone : undefined,
490
+ activityScope: isActivityScope(body.activityScope) ? body.activityScope : undefined,
491
+ workspaceProvisioning: isWorkspaceProvisioning(body.workspaceProvisioning)
492
+ ? body.workspaceProvisioning
493
+ : undefined,
494
+ members: Array.isArray(body.members)
495
+ ? body.members
496
+ .map((member) => {
497
+ if (!member || typeof member !== 'object')
498
+ return null;
499
+ const entry = member;
500
+ if (typeof entry.id !== 'string' || typeof entry.displayName !== 'string')
501
+ return null;
502
+ return {
503
+ id: entry.id,
504
+ displayName: entry.displayName,
505
+ email: typeof entry.email === 'string' ? entry.email : undefined,
506
+ avatarUrl: typeof entry.avatarUrl === 'string' ? entry.avatarUrl : undefined,
507
+ role: isCollaboratorRole(entry.role) ? entry.role : undefined,
508
+ };
509
+ })
510
+ .filter(Boolean)
511
+ : undefined,
512
+ permissions: Array.isArray(body.permissions)
513
+ ? body.permissions
514
+ .map((permission) => {
515
+ if (!permission || typeof permission !== 'object')
516
+ return null;
517
+ const entry = permission;
518
+ if (typeof entry.action !== 'string' || !Array.isArray(entry.roles))
519
+ return null;
520
+ return {
521
+ action: entry.action,
522
+ roles: entry.roles.filter((role) => isCollaboratorRole(role)),
523
+ description: typeof entry.description === 'string' ? entry.description : undefined,
524
+ };
525
+ })
526
+ .filter(Boolean)
527
+ : undefined,
528
+ });
529
+ break;
530
+ case 'update-issue-detail':
531
+ if (typeof body.issueId !== 'string') {
532
+ throw new AppError('issueId is required.', 'BAD_REQUEST', 400);
533
+ }
534
+ overview = await backlogService.updateIssueDetail({
535
+ issueId: body.issueId,
536
+ expectedUpdatedAt: typeof body.expectedUpdatedAt === 'string' ? body.expectedUpdatedAt : undefined,
537
+ title: typeof body.title === 'string' ? body.title : undefined,
538
+ summary: typeof body.summary === 'string' ? body.summary : undefined,
539
+ description: typeof body.description === 'string' ? body.description : undefined,
540
+ status: body.status === 'backlog' ||
541
+ body.status === 'ready' ||
542
+ body.status === 'in-progress' ||
543
+ body.status === 'blocked' ||
544
+ body.status === 'review' ||
545
+ body.status === 'done'
546
+ ? body.status
547
+ : undefined,
548
+ priority: body.priority === 'critical' ||
549
+ body.priority === 'high' ||
550
+ body.priority === 'medium' ||
551
+ body.priority === 'low'
552
+ ? body.priority
553
+ : undefined,
554
+ assigneeIds: Array.isArray(body.assigneeIds) && body.assigneeIds.every((id) => typeof id === 'string')
555
+ ? body.assigneeIds
556
+ : undefined,
557
+ labelIds: Array.isArray(body.labelIds) && body.labelIds.every((id) => typeof id === 'string')
558
+ ? body.labelIds
559
+ : undefined,
560
+ dependencies: Array.isArray(body.dependencies)
561
+ ? body.dependencies
562
+ .map((dependency) => {
563
+ if (!dependency || typeof dependency !== 'object')
564
+ return null;
565
+ const entry = dependency;
566
+ if (typeof entry.issueId !== 'string')
567
+ return null;
568
+ return {
569
+ issueId: entry.issueId,
570
+ type: entry.type === 'blocks' || entry.type === 'related' || entry.type === 'blocked-by'
571
+ ? entry.type
572
+ : undefined,
573
+ };
574
+ })
575
+ .filter(Boolean)
576
+ : undefined,
577
+ acceptanceCriteria: Array.isArray(body.acceptanceCriteria)
578
+ ? body.acceptanceCriteria
579
+ .map((criterion) => {
580
+ if (!criterion || typeof criterion !== 'object')
581
+ return null;
582
+ const entry = criterion;
583
+ if (typeof entry.title !== 'string')
584
+ return null;
585
+ return {
586
+ id: typeof entry.id === 'string' ? entry.id : undefined,
587
+ title: entry.title,
588
+ satisfied: typeof entry.satisfied === 'boolean' ? entry.satisfied : undefined,
589
+ notes: typeof entry.notes === 'string' ? entry.notes : undefined,
590
+ };
591
+ })
592
+ .filter(Boolean)
593
+ : undefined,
594
+ });
595
+ break;
596
+ case 'update-issue-dispatch-context-labels':
597
+ if (typeof body.issueId !== 'string' ||
598
+ !Array.isArray(body.dispatchContextLabelIds) ||
599
+ !body.dispatchContextLabelIds.every((labelId) => typeof labelId === 'string')) {
600
+ throw new AppError('issueId and dispatchContextLabelIds are required.', 'BAD_REQUEST', 400);
601
+ }
602
+ overview = await backlogService.updateIssueDispatchContextLabels({
603
+ issueId: body.issueId,
604
+ dispatchContextLabelIds: body.dispatchContextLabelIds,
605
+ });
606
+ break;
607
+ case 'create-sub-issue': {
608
+ if (typeof body.parentIssueId !== 'string' || typeof body.title !== 'string') {
609
+ throw new AppError('parentIssueId and title are required.', 'BAD_REQUEST', 400);
610
+ }
611
+ overview = (await backlogService.createIssue({
612
+ parentIssueId: body.parentIssueId,
613
+ title: body.title,
614
+ summary: typeof body.summary === 'string' ? body.summary : undefined,
615
+ priority: body.priority === 'critical' ||
616
+ body.priority === 'high' ||
617
+ body.priority === 'medium' ||
618
+ body.priority === 'low'
619
+ ? body.priority
620
+ : undefined,
621
+ status: body.status === 'backlog' ||
622
+ body.status === 'ready' ||
623
+ body.status === 'in-progress' ||
624
+ body.status === 'blocked' ||
625
+ body.status === 'review' ||
626
+ body.status === 'done'
627
+ ? body.status
628
+ : undefined,
629
+ })).overview;
630
+ break;
631
+ }
632
+ case 'link-child-issue':
633
+ if (typeof body.parentIssueId !== 'string' || typeof body.childIssueId !== 'string') {
634
+ throw new AppError('parentIssueId and childIssueId are required.', 'BAD_REQUEST', 400);
635
+ }
636
+ overview = await backlogService.linkChildIssue({
637
+ parentIssueId: body.parentIssueId,
638
+ childIssueId: body.childIssueId,
639
+ });
640
+ break;
641
+ case 'create-issue-workspace': {
642
+ if (typeof body.issueId !== 'string') {
643
+ throw new AppError('issueId is required.', 'BAD_REQUEST', 400);
644
+ }
645
+ const current = await backlogService.getOverview();
646
+ const issue = current.snapshot.issues.find((candidate) => candidate.id === body.issueId);
647
+ if (!issue) {
648
+ throw new AppError(`Issue ${body.issueId} not found.`, 'NOT_FOUND', 404);
649
+ }
650
+ const provisioned = await workspaceService.provisionWorkspaceForIssue({
651
+ issueKey: issue.key,
652
+ issueTitle: issue.title,
653
+ });
654
+ overview = await backlogService.linkIssueWorkspace({
655
+ issueId: issue.id,
656
+ workspacePath: provisioned.workspacePath,
657
+ workspaceName: provisioned.workspaceName,
658
+ branchName: provisioned.branchName,
659
+ source: 'created-from-issue',
660
+ });
661
+ break;
662
+ }
663
+ case 'link-issue-workspace': {
664
+ if (typeof body.issueId !== 'string' || typeof body.workspacePath !== 'string') {
665
+ throw new AppError('issueId and workspacePath are required.', 'BAD_REQUEST', 400);
666
+ }
667
+ const inventory = await workspaceService.listWorkspaces();
668
+ const workspace = inventory.workspaces.find((candidate) => candidate.path === body.workspacePath);
669
+ if (!workspace) {
670
+ throw new AppError(`Workspace ${body.workspacePath} not found.`, 'NOT_FOUND', 404);
671
+ }
672
+ if (workspace.missing) {
673
+ throw new AppError(`Workspace ${body.workspacePath} is missing. Recover it before linking.`, 'BAD_REQUEST', 409);
674
+ }
675
+ overview = await backlogService.linkIssueWorkspace({
676
+ issueId: body.issueId,
677
+ workspacePath: body.workspacePath,
678
+ workspaceName: workspace.name,
679
+ branchName: workspace.git.branch ?? undefined,
680
+ source: 'linked-existing-workspace',
681
+ });
682
+ break;
683
+ }
684
+ case 'link-issue-session': {
685
+ if (typeof body.issueId !== 'string') {
686
+ throw new AppError('issueId is required.', 'BAD_REQUEST', 400);
687
+ }
688
+ overview = await backlogService.linkIssueSession({
689
+ issueId: body.issueId,
690
+ sessionId: typeof body.sessionId === 'string' ? body.sessionId : undefined,
691
+ runId: typeof body.runId === 'string' ? body.runId : undefined,
692
+ });
693
+ break;
694
+ }
695
+ default:
696
+ throw new AppError('Unsupported backlog action.', 'BAD_REQUEST', 400);
697
+ }
698
+ return jsonWithHeaders(overview);
699
+ }
700
+ catch (error) {
701
+ return jsonError(error);
702
+ }
703
+ });
704
+ app.get('/api/task-tags', async () => {
705
+ try {
706
+ await ensureInitialized();
707
+ return jsonWithHeaders({ taskTags: await taskTagService.listTaskTags() });
708
+ }
709
+ catch (error) {
710
+ return jsonError(error);
711
+ }
712
+ });
713
+ app.post('/api/task-tags', async (context) => {
714
+ try {
715
+ await ensureInitialized();
716
+ const body = await context.req.json();
717
+ return jsonWithHeaders(await taskTagService.createTaskTag({
718
+ key: body.key,
719
+ label: body.label,
720
+ content: body.content,
721
+ description: body.description,
722
+ order: body.order,
723
+ }), 201);
724
+ }
725
+ catch (error) {
726
+ return jsonError(error);
727
+ }
728
+ });
729
+ app.patch('/api/task-tags/:taskTagId', async (context) => {
730
+ try {
731
+ await ensureInitialized();
732
+ const body = await context.req.json();
733
+ return jsonWithHeaders(await taskTagService.updateTaskTag(context.req.param('taskTagId'), {
734
+ key: typeof body.key === 'string' ? body.key : undefined,
735
+ label: typeof body.label === 'string' ? body.label : undefined,
736
+ content: typeof body.content === 'string' ? body.content : undefined,
737
+ description: typeof body.description === 'string' ? body.description : undefined,
738
+ order: typeof body.order === 'number' ? body.order : undefined,
739
+ }));
740
+ }
741
+ catch (error) {
742
+ return jsonError(error);
743
+ }
744
+ });
745
+ app.delete('/api/task-tags/:taskTagId', async (context) => {
746
+ try {
747
+ await ensureInitialized();
748
+ return jsonWithHeaders(await taskTagService.deleteTaskTag(context.req.param('taskTagId')));
749
+ }
750
+ catch (error) {
751
+ return jsonError(error);
752
+ }
753
+ });
754
+ app.get('/api/dispatch-context-labels', async () => {
755
+ try {
756
+ await ensureInitialized();
757
+ return jsonWithHeaders({
758
+ dispatchContextLabels: await dispatchContextLabelService.listDispatchContextLabels(),
759
+ });
760
+ }
761
+ catch (error) {
762
+ return jsonError(error);
763
+ }
764
+ });
765
+ app.post('/api/dispatch-context-labels', async (context) => {
766
+ try {
767
+ await ensureInitialized();
768
+ const body = await context.req.json();
769
+ return jsonWithHeaders(await dispatchContextLabelService.createDispatchContextLabel({
770
+ key: body.key,
771
+ label: body.label,
772
+ instruction: body.instruction,
773
+ description: body.description,
774
+ order: body.order,
775
+ }), 201);
776
+ }
777
+ catch (error) {
778
+ return jsonError(error);
779
+ }
780
+ });
781
+ app.patch('/api/dispatch-context-labels/:dispatchContextLabelId', async (context) => {
782
+ try {
783
+ await ensureInitialized();
784
+ const body = await context.req.json();
785
+ return jsonWithHeaders(await dispatchContextLabelService.updateDispatchContextLabel(context.req.param('dispatchContextLabelId'), {
786
+ key: typeof body.key === 'string' ? body.key : undefined,
787
+ label: typeof body.label === 'string' ? body.label : undefined,
788
+ instruction: typeof body.instruction === 'string' ? body.instruction : undefined,
789
+ description: typeof body.description === 'string' ? body.description : undefined,
790
+ order: typeof body.order === 'number' ? body.order : undefined,
791
+ }));
792
+ }
793
+ catch (error) {
794
+ return jsonError(error);
795
+ }
796
+ });
797
+ app.delete('/api/dispatch-context-labels/:dispatchContextLabelId', async (context) => {
798
+ try {
799
+ await ensureInitialized();
800
+ return jsonWithHeaders(await dispatchContextLabelService.deleteDispatchContextLabel(context.req.param('dispatchContextLabelId')));
801
+ }
802
+ catch (error) {
803
+ return jsonError(error);
804
+ }
805
+ });
806
+ app.get('/api/reviews', async (context) => {
807
+ try {
808
+ const url = new URL(context.req.url);
809
+ const targetType = url.searchParams.get('targetType');
810
+ const targetId = url.searchParams.get('targetId');
811
+ return jsonWithHeaders(await reviewService.listReviews({
812
+ targetType: targetType === 'issue' || targetType === 'workspace' ? targetType : undefined,
813
+ targetId: targetId?.trim() || undefined,
814
+ }));
815
+ }
816
+ catch (error) {
817
+ return jsonError(error);
818
+ }
819
+ });
820
+ app.post('/api/reviews', async (context) => {
821
+ try {
822
+ const body = await context.req.json();
823
+ return jsonWithHeaders(await reviewService.applyAction(body));
824
+ }
825
+ catch (error) {
826
+ return jsonError(error);
827
+ }
828
+ });
829
+ app.get('/api/automations', async (context) => {
830
+ try {
831
+ await ensureInitialized();
832
+ return jsonWithHeaders(await automationRuleService.listRules(parseAutomationQuery(new URL(context.req.url))));
833
+ }
834
+ catch (error) {
835
+ return jsonError(error);
836
+ }
837
+ });
838
+ app.post('/api/automations', async (context) => {
839
+ try {
840
+ await ensureInitialized();
841
+ return jsonWithHeaders(await automationRuleService.createRule(await context.req.json()), 201);
842
+ }
843
+ catch (error) {
844
+ return jsonError(error);
845
+ }
846
+ });
847
+ app.get('/api/automations/:ruleId', async (context) => {
848
+ try {
849
+ await ensureInitialized();
850
+ return jsonWithHeaders(await automationRuleService.getRule(context.req.param('ruleId')));
851
+ }
852
+ catch (error) {
853
+ return jsonError(error);
854
+ }
855
+ });
856
+ app.patch('/api/automations/:ruleId', async (context) => {
857
+ try {
858
+ await ensureInitialized();
859
+ return jsonWithHeaders(await automationRuleService.updateRule(context.req.param('ruleId'), await context.req.json()));
860
+ }
861
+ catch (error) {
862
+ return jsonError(error);
863
+ }
864
+ });
865
+ app.delete('/api/automations/:ruleId', async (context) => {
866
+ try {
867
+ await ensureInitialized();
868
+ return jsonWithHeaders(await automationRuleService.deleteRule(context.req.param('ruleId')));
869
+ }
870
+ catch (error) {
871
+ return jsonError(error);
872
+ }
873
+ });
874
+ app.post('/api/automations/:ruleId/lifecycle', async (context) => {
875
+ try {
876
+ await ensureInitialized();
877
+ const body = await context.req.json();
878
+ return jsonWithHeaders(await automationRuleService.transitionRule(context.req.param('ruleId'), readLifecycleAction(body.action), typeof body.updatedBy === 'string' ? body.updatedBy : undefined));
879
+ }
880
+ catch (error) {
881
+ return jsonError(error);
882
+ }
883
+ });
884
+ app.post('/api/automations/webhooks/:ruleId', async (context) => {
885
+ try {
886
+ await ensureInitialized();
887
+ const payload = await automationWebhookService.deliver({
888
+ ruleId: context.req.param('ruleId'),
889
+ requestPath: new URL(context.req.url).pathname,
890
+ requestMethod: context.req.method,
891
+ headers: context.req.raw.headers,
892
+ rawBody: await context.req.text(),
893
+ });
894
+ const status = payload.outcome === 'created'
895
+ ? 201
896
+ : payload.code === 'AUTOMATION_WEBHOOK_UNAUTHORIZED'
897
+ ? 401
898
+ : payload.code === 'AUTOMATION_RULE_NOT_ACTIVE'
899
+ ? 409
900
+ : payload.outcome === 'rejected'
901
+ ? 400
902
+ : 200;
903
+ return jsonWithHeaders(payload, status);
904
+ }
905
+ catch (error) {
906
+ return jsonError(error);
907
+ }
908
+ });
909
+ app.get('/api/settings/agent-configuration', async () => {
910
+ try {
911
+ return jsonWithHeaders(await buildAgentConfigurationResponse());
912
+ }
913
+ catch (error) {
914
+ return jsonError(error);
915
+ }
916
+ });
917
+ app.post('/api/settings/agent-configuration', async (context) => {
918
+ try {
919
+ const body = await context.req.json();
920
+ const client = createClient();
921
+ if (typeof body.agent !== 'string' || !body.agent.trim()) {
922
+ return Response.json({ error: 'agent is required' }, { status: 400 });
923
+ }
924
+ const approvalMode = body.approvalMode === 'yolo' || body.approvalMode === 'deny' ? body.approvalMode : 'prompt';
925
+ const model = typeof body.model === 'string' ? body.model.trim() : '';
926
+ const provider = typeof body.provider === 'string' ? body.provider.trim() : '';
927
+ const maxTokensRaw = typeof body.maxTokens === 'string' ? body.maxTokens.trim() : '';
928
+ try {
929
+ validateProfileData({
930
+ approvalMode,
931
+ ...(maxTokensRaw ? { maxTokens: Number.parseInt(maxTokensRaw, 10) } : {}),
932
+ });
933
+ }
934
+ catch (error) {
935
+ return Response.json({ error: error instanceof Error ? error.message : 'Invalid configuration.' }, { status: 400 });
936
+ }
937
+ if (model) {
938
+ const result = client.models.validate(body.agent, model);
939
+ if (!result.valid) {
940
+ return Response.json({
941
+ error: result.suggestions && result.suggestions.length > 0
942
+ ? `${result.message}. Suggestions: ${result.suggestions.join(', ')}`
943
+ : result.message,
944
+ }, { status: 400 });
945
+ }
946
+ }
947
+ const storage = await loadSettingsSectionStorage();
948
+ storage.agentConfiguration[body.agent] = {
949
+ model: model || undefined,
950
+ provider: provider || undefined,
951
+ approvalMode,
952
+ ...(maxTokensRaw ? { maxTokens: Number.parseInt(maxTokensRaw, 10) } : {}),
953
+ };
954
+ await writeSettingsSectionStorage(storage);
955
+ return jsonWithHeaders(await buildAgentConfigurationResponse());
956
+ }
957
+ catch (error) {
958
+ return jsonError(error);
959
+ }
960
+ });
961
+ app.get('/api/settings/mcp-servers', async () => {
962
+ try {
963
+ return jsonWithHeaders(await buildMcpServerResponse());
964
+ }
965
+ catch (error) {
966
+ return jsonError(error);
967
+ }
968
+ });
969
+ app.post('/api/settings/mcp-servers', async (context) => {
970
+ try {
971
+ const client = createClient();
972
+ const body = await context.req.json();
973
+ if (typeof body.agent !== 'string' || !body.agent.trim()) {
974
+ return Response.json({ error: 'agent is required' }, { status: 400 });
975
+ }
976
+ if (!Array.isArray(body.servers)) {
977
+ return Response.json({ error: 'servers must be an array' }, { status: 400 });
978
+ }
979
+ let servers;
980
+ try {
981
+ servers = body.servers.map((server) => {
982
+ const draft = server;
983
+ return toMcpConfig({
984
+ name: typeof draft.name === 'string' ? draft.name : '',
985
+ transport: draft.transport === 'sse' || draft.transport === 'streamable-http'
986
+ ? draft.transport
987
+ : 'stdio',
988
+ command: typeof draft.command === 'string' ? draft.command : '',
989
+ url: typeof draft.url === 'string' ? draft.url : '',
990
+ argsText: typeof draft.argsText === 'string' ? draft.argsText : '',
991
+ envText: typeof draft.envText === 'string' ? draft.envText : '',
992
+ });
993
+ });
994
+ validateProfileData({ mcpServers: servers });
995
+ }
996
+ catch (error) {
997
+ return Response.json({ error: error instanceof Error ? error.message : 'Invalid MCP server configuration.' }, { status: 400 });
998
+ }
999
+ const storage = await loadSettingsSectionStorage();
1000
+ storage.mcpServers[body.agent] = servers;
1001
+ await writeSettingsSectionStorage(storage);
1002
+ return jsonWithHeaders(await buildMcpServerResponse());
1003
+ }
1004
+ catch (error) {
1005
+ return jsonError(error);
1006
+ }
1007
+ });
1008
+ app.get('/api/workspaces', async (context) => {
1009
+ try {
1010
+ const reviews = await reviewService.listReviews({ targetType: 'workspace' });
1011
+ const focusWorkspacePath = context.req.query('workspace')?.trim() || undefined;
1012
+ return jsonWithHeaders(await workspaceService.listWorkspaces({
1013
+ focusWorkspacePath,
1014
+ reviewByWorkspacePath: buildReviewByWorkspacePath(reviews.artifacts),
1015
+ linkedIssuesByWorkspacePath: await buildLinkedIssuesByWorkspacePath(),
1016
+ }));
1017
+ }
1018
+ catch (error) {
1019
+ return jsonError(error);
1020
+ }
1021
+ });
1022
+ app.post('/api/workspaces', async (context) => {
1023
+ try {
1024
+ const body = await context.req.json();
1025
+ const sessions = readSessions(body);
1026
+ const focusWorkspacePath = typeof body.focusWorkspacePath === 'string' && body.focusWorkspacePath.trim().length > 0
1027
+ ? body.focusWorkspacePath.trim()
1028
+ : undefined;
1029
+ const reviews = await reviewService.listReviews({ targetType: 'workspace' });
1030
+ const reviewByWorkspacePath = buildReviewByWorkspacePath(reviews.artifacts);
1031
+ const linkedIssuesByWorkspacePath = await buildLinkedIssuesByWorkspacePath();
1032
+ if (body.action === 'provision' ||
1033
+ body.action === 'pin' ||
1034
+ body.action === 'unpin' ||
1035
+ body.action === 'archive' ||
1036
+ body.action === 'cleanup' ||
1037
+ body.action === 'recover' ||
1038
+ body.action === 'notes-save' ||
1039
+ body.action === 'rebase-start' ||
1040
+ body.action === 'rebase-auto-resolve' ||
1041
+ body.action === 'rebase-open-in-editor' ||
1042
+ body.action === 'rebase-mark-resolved' ||
1043
+ body.action === 'rebase-abort') {
1044
+ if (body.action === 'provision') {
1045
+ const current = await backlogService.getOverview();
1046
+ const projectById = new Map(current.snapshot.projects.map((project) => [project.id, project]));
1047
+ const scope = body.scope === 'issue' || body.scope === 'project' || body.scope === 'host'
1048
+ ? body.scope
1049
+ : null;
1050
+ const workspaceName = typeof body.workspaceName === 'string' ? body.workspaceName.trim() : '';
1051
+ if (!scope || !workspaceName) {
1052
+ return Response.json({ error: 'scope and workspaceName are required', code: 'BAD_REQUEST' }, { status: 400 });
1053
+ }
1054
+ const projectId = typeof body.projectId === 'string' ? body.projectId : '';
1055
+ const project = projectById.get(projectId);
1056
+ if (!project) {
1057
+ return Response.json({ error: `Project ${projectId} not found`, code: 'NOT_FOUND' }, { status: 404 });
1058
+ }
1059
+ const selectedIntegration = typeof body.hostProvider === 'string'
1060
+ ? project.integrations.find((integration) => integration.provider === body.hostProvider)
1061
+ : undefined;
1062
+ let provisioned;
1063
+ if (scope === 'issue') {
1064
+ const issueId = typeof body.issueId === 'string' ? body.issueId : '';
1065
+ const issue = current.snapshot.issues.find((candidate) => candidate.id === issueId);
1066
+ if (!issue || issue.projectId !== project.id) {
1067
+ return Response.json({ error: `Issue ${issueId} not found in project ${project.id}`, code: 'NOT_FOUND' }, { status: 404 });
1068
+ }
1069
+ provisioned = await workspaceService.provisionWorkspaceForIssue({
1070
+ issueKey: workspaceName,
1071
+ issueTitle: issue.title,
1072
+ ownership: {
1073
+ source: 'created-from-issue',
1074
+ project: {
1075
+ projectId: project.id,
1076
+ projectKey: project.key,
1077
+ projectName: project.name,
1078
+ },
1079
+ issue: {
1080
+ issueId: issue.id,
1081
+ issueKey: issue.key,
1082
+ issueTitle: issue.title,
1083
+ },
1084
+ host: selectedIntegration
1085
+ ? {
1086
+ provider: selectedIntegration.provider,
1087
+ label: selectedIntegration.label,
1088
+ accountLabel: selectedIntegration.accountLabel,
1089
+ }
1090
+ : undefined,
1091
+ },
1092
+ });
1093
+ await backlogService.linkIssueWorkspace({
1094
+ issueId: issue.id,
1095
+ workspacePath: provisioned.workspacePath,
1096
+ workspaceName: provisioned.workspaceName,
1097
+ branchName: provisioned.branchName,
1098
+ source: 'created-from-issue',
1099
+ });
1100
+ }
1101
+ else {
1102
+ provisioned = await workspaceService.provisionWorkspace({
1103
+ workspaceName,
1104
+ ownership: {
1105
+ source: scope === 'host' ? 'created-from-host' : 'created-from-project',
1106
+ project: {
1107
+ projectId: project.id,
1108
+ projectKey: project.key,
1109
+ projectName: project.name,
1110
+ },
1111
+ host: selectedIntegration
1112
+ ? {
1113
+ provider: selectedIntegration.provider,
1114
+ label: selectedIntegration.label,
1115
+ accountLabel: selectedIntegration.accountLabel,
1116
+ }
1117
+ : undefined,
1118
+ },
1119
+ });
1120
+ }
1121
+ return jsonWithHeaders({
1122
+ workspace: provisioned,
1123
+ ...(await workspaceService.listWorkspaces({
1124
+ focusWorkspacePath,
1125
+ sessions,
1126
+ reviewByWorkspacePath,
1127
+ linkedIssuesByWorkspacePath: await buildLinkedIssuesByWorkspacePath(),
1128
+ })),
1129
+ });
1130
+ }
1131
+ const workspacePath = typeof body.workspacePath === 'string' ? body.workspacePath : '';
1132
+ if (!workspacePath) {
1133
+ return Response.json({ error: 'workspacePath is required', code: 'BAD_REQUEST' }, { status: 400 });
1134
+ }
1135
+ const result = await workspaceService.applyAction({
1136
+ action: body.action,
1137
+ workspacePath,
1138
+ note: typeof body.note === 'string' ? body.note : undefined,
1139
+ });
1140
+ return jsonWithHeaders({
1141
+ result,
1142
+ ...(await workspaceService.listWorkspaces({
1143
+ focusWorkspacePath,
1144
+ sessions,
1145
+ reviewByWorkspacePath,
1146
+ linkedIssuesByWorkspacePath,
1147
+ })),
1148
+ });
1149
+ }
1150
+ return jsonWithHeaders(await workspaceService.listWorkspaces({
1151
+ focusWorkspacePath,
1152
+ sessions,
1153
+ reviewByWorkspacePath,
1154
+ linkedIssuesByWorkspacePath,
1155
+ }));
1156
+ }
1157
+ catch (error) {
1158
+ return jsonError(error);
1159
+ }
1160
+ });
1161
+ app.get('/api/digest', async () => {
1162
+ try {
1163
+ try {
1164
+ await ensureInitialized();
1165
+ }
1166
+ catch {
1167
+ return Response.json({ error: 'Server initialization failed. Check kanban configuration and watch sources.', code: 'INIT_FAILED' }, { status: 500 });
1168
+ }
1169
+ await discoverAndCacheAll();
1170
+ const runs = getAllCachedDigests();
1171
+ runs.sort((a, b) => {
1172
+ const cmp = (b.updatedAt || '').localeCompare(a.updatedAt || '');
1173
+ if (cmp !== 0)
1174
+ return cmp;
1175
+ return a.runId.localeCompare(b.runId);
1176
+ });
1177
+ return jsonWithHeaders({ runs });
1178
+ }
1179
+ catch (error) {
1180
+ return jsonError(error);
1181
+ }
1182
+ });
1183
+ app.get('/api/runs', async (context) => {
1184
+ try {
1185
+ await ensureInitialized();
1186
+ const url = new URL(context.req.url);
1187
+ const mode = url.searchParams.get('mode');
1188
+ const project = url.searchParams.get('project');
1189
+ const limit = parseInt(url.searchParams.get('limit') || '0', 10);
1190
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
1191
+ const search = url.searchParams.get('search') || '';
1192
+ const status = url.searchParams.get('status') || '';
1193
+ const sort = (url.searchParams.get('sort') || 'status');
1194
+ if (mode === 'projects') {
1195
+ return jsonWithHeaders(await runQueryService.listProjects());
1196
+ }
1197
+ if (project) {
1198
+ return jsonWithHeaders(await runQueryService.listProjectRuns({ project, limit, offset, search, status, sort }));
1199
+ }
1200
+ return jsonWithHeaders(await runQueryService.listAllRuns({ limit, offset, search, status, sort }));
1201
+ }
1202
+ catch (error) {
1203
+ return jsonError(error);
1204
+ }
1205
+ });
1206
+ app.get('/api/runs/:runId', async (context) => {
1207
+ try {
1208
+ await ensureInitialized();
1209
+ const runId = context.req.param('runId');
1210
+ if (!isValidId(runId)) {
1211
+ return Response.json({ error: 'Invalid run ID' }, { status: 400 });
1212
+ }
1213
+ const found = await findRunDir(runId);
1214
+ if (!found) {
1215
+ return Response.json({ error: 'Run not found' }, { status: 404 });
1216
+ }
1217
+ const run = await getRunCached(found.runDir, found.source, found.projectName);
1218
+ const maxEvents = parseInt(new URL(context.req.url).searchParams.get('maxEvents') || '50', 10);
1219
+ const totalEvents = run.events.length;
1220
+ const limitedRun = totalEvents > maxEvents
1221
+ ? { ...run, events: run.events.slice(-maxEvents), totalEvents }
1222
+ : { ...run, totalEvents };
1223
+ return jsonWithHeaders({ run: limitedRun }, 200, { 'Cache-Control': 'no-cache' });
1224
+ }
1225
+ catch (error) {
1226
+ return jsonError(error);
1227
+ }
1228
+ });
1229
+ app.get('/api/runs/:runId/events', async (context) => {
1230
+ try {
1231
+ const runId = context.req.param('runId');
1232
+ if (!isValidId(runId)) {
1233
+ return Response.json({ error: 'Invalid run ID' }, { status: 400 });
1234
+ }
1235
+ const found = await findRunDir(runId);
1236
+ if (!found) {
1237
+ return Response.json({ error: 'Run not found' }, { status: 404 });
1238
+ }
1239
+ const journalPath = path.join(found.runDir, 'journal');
1240
+ const url = new URL(context.req.url);
1241
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
1242
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
1243
+ if (Number.isNaN(limit) || Number.isNaN(offset) || limit < 0 || offset < 0) {
1244
+ return Response.json({ error: 'Invalid pagination parameters', code: 'INVALID_INPUT' }, { status: 400 });
1245
+ }
1246
+ const allEvents = await parseJournalDir(journalPath);
1247
+ return jsonWithHeaders({
1248
+ events: allEvents.slice(offset, offset + limit),
1249
+ total: allEvents.length,
1250
+ });
1251
+ }
1252
+ catch (error) {
1253
+ return jsonError(error);
1254
+ }
1255
+ });
1256
+ app.get('/api/runs/:runId/tasks/:effectId', async (context) => {
1257
+ try {
1258
+ const runId = context.req.param('runId');
1259
+ const effectId = context.req.param('effectId');
1260
+ if (!isValidId(runId) || !isValidId(effectId)) {
1261
+ return Response.json({ error: 'Invalid ID' }, { status: 400 });
1262
+ }
1263
+ const found = await findRunDir(runId);
1264
+ if (!found) {
1265
+ return Response.json({ error: 'Run not found' }, { status: 404 });
1266
+ }
1267
+ const filePath = new URL(context.req.url).searchParams.get('file');
1268
+ if (filePath) {
1269
+ const content = await loadPreviewFile(found.runDir, filePath);
1270
+ if (content == null) {
1271
+ return Response.json({ error: 'File not found' }, { status: 404 });
1272
+ }
1273
+ return jsonWithHeaders({ content });
1274
+ }
1275
+ const task = await parseTaskDetail(found.runDir, effectId);
1276
+ if (!task) {
1277
+ return Response.json({ error: 'Task not found' }, { status: 404 });
1278
+ }
1279
+ return jsonWithHeaders({ task });
1280
+ }
1281
+ catch (error) {
1282
+ return jsonError(error);
1283
+ }
1284
+ });
1285
+ app.post('/api/runs/:runId/tasks/:effectId/approve', async (context) => {
1286
+ try {
1287
+ const runId = context.req.param('runId');
1288
+ const effectId = context.req.param('effectId');
1289
+ const body = await context.req.json().catch((e) => { process.stderr.write(`[gateway] response parse failed: ${e instanceof Error ? e.message : String(e)}\n`); return {}; });
1290
+ const answer = typeof body.answer === 'string'
1291
+ ? body.answer
1292
+ : '';
1293
+ await approveBreakpointEffect(runId, effectId, answer);
1294
+ return jsonWithHeaders({ success: true });
1295
+ }
1296
+ catch (error) {
1297
+ return jsonError(error);
1298
+ }
1299
+ });
1300
+ app.get('/api/stream', async () => {
1301
+ try {
1302
+ await ensureInitialized();
1303
+ let cleanup = null;
1304
+ const stream = new ReadableStream({
1305
+ start(controller) {
1306
+ const encoder = new TextEncoder();
1307
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'connected', timestamp: new Date().toISOString() })}\n\n`));
1308
+ const runChangedListener = (event) => {
1309
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({
1310
+ type: 'update',
1311
+ runIds: event.runIds,
1312
+ runId: event.runIds[0],
1313
+ timestamp: new Date().toISOString(),
1314
+ })}\n\n`));
1315
+ };
1316
+ const newRunListener = (event) => {
1317
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({
1318
+ type: 'new-run',
1319
+ runId: extractRunId(event.runDir),
1320
+ runDir: event.runDir,
1321
+ timestamp: new Date().toISOString(),
1322
+ })}\n\n`));
1323
+ };
1324
+ const errorListener = (event) => {
1325
+ console.warn('Watcher error (suppressed from SSE):', event.error?.message ?? 'unknown', event.runDir);
1326
+ };
1327
+ serverEvents.on('run-changed', runChangedListener);
1328
+ serverEvents.on('new-run', newRunListener);
1329
+ serverEvents.on('watcher-error', errorListener);
1330
+ const pingInterval = setInterval(() => {
1331
+ controller.enqueue(encoder.encode(': ping\n\n'));
1332
+ }, 15_000);
1333
+ cleanup = () => {
1334
+ clearInterval(pingInterval);
1335
+ serverEvents.off('run-changed', runChangedListener);
1336
+ serverEvents.off('new-run', newRunListener);
1337
+ serverEvents.off('watcher-error', errorListener);
1338
+ };
1339
+ },
1340
+ cancel() {
1341
+ cleanup?.();
1342
+ cleanup = null;
1343
+ },
1344
+ });
1345
+ return new Response(stream, {
1346
+ headers: {
1347
+ 'Content-Type': 'text/event-stream',
1348
+ 'Cache-Control': 'no-cache, no-store',
1349
+ Connection: 'keep-alive',
1350
+ },
1351
+ });
1352
+ }
1353
+ catch {
1354
+ return Response.json({ error: 'Failed to initialize stream' }, { status: 500 });
1355
+ }
1356
+ });
1357
+ }
1358
+ //# sourceMappingURL=routes.js.map