@a5c-ai/adapters-gateway 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.
- package/README.md +20 -0
- package/dist/auth/bootstrap.d.ts +89 -0
- package/dist/auth/bootstrap.d.ts.map +1 -0
- package/dist/auth/bootstrap.js +222 -0
- package/dist/auth/bootstrap.js.map +1 -0
- package/dist/auth/hashing.d.ts +4 -0
- package/dist/auth/hashing.d.ts.map +1 -0
- package/dist/auth/hashing.js +27 -0
- package/dist/auth/hashing.js.map +1 -0
- package/dist/auth/middleware.d.ts +3 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +17 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/tokens.d.ts +45 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +186 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/builtin-adapters.d.ts +17 -0
- package/dist/builtin-adapters.d.ts.map +1 -0
- package/dist/builtin-adapters.js +119 -0
- package/dist/builtin-adapters.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/fanout/client-conn.d.ts +20 -0
- package/dist/fanout/client-conn.d.ts.map +1 -0
- package/dist/fanout/client-conn.js +53 -0
- package/dist/fanout/client-conn.js.map +1 -0
- package/dist/fanout/subscriber.d.ts +12 -0
- package/dist/fanout/subscriber.d.ts.map +1 -0
- package/dist/fanout/subscriber.js +40 -0
- package/dist/fanout/subscriber.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/kanban/lib/config-loader.d.ts +29 -0
- package/dist/kanban/lib/config-loader.d.ts.map +1 -0
- package/dist/kanban/lib/config-loader.js +166 -0
- package/dist/kanban/lib/config-loader.js.map +1 -0
- package/dist/kanban/lib/config.d.ts +3 -0
- package/dist/kanban/lib/config.d.ts.map +1 -0
- package/dist/kanban/lib/config.js +6 -0
- package/dist/kanban/lib/config.js.map +1 -0
- package/dist/kanban/lib/create-global-registry.d.ts +28 -0
- package/dist/kanban/lib/create-global-registry.d.ts.map +1 -0
- package/dist/kanban/lib/create-global-registry.js +53 -0
- package/dist/kanban/lib/create-global-registry.js.map +1 -0
- package/dist/kanban/lib/dispatch-context-audit.d.ts +12 -0
- package/dist/kanban/lib/dispatch-context-audit.d.ts.map +1 -0
- package/dist/kanban/lib/dispatch-context-audit.js +44 -0
- package/dist/kanban/lib/dispatch-context-audit.js.map +1 -0
- package/dist/kanban/lib/error-handler.d.ts +28 -0
- package/dist/kanban/lib/error-handler.d.ts.map +1 -0
- package/dist/kanban/lib/error-handler.js +61 -0
- package/dist/kanban/lib/error-handler.js.map +1 -0
- package/dist/kanban/lib/global-registry.d.ts +49 -0
- package/dist/kanban/lib/global-registry.d.ts.map +1 -0
- package/dist/kanban/lib/global-registry.js +18 -0
- package/dist/kanban/lib/global-registry.js.map +1 -0
- package/dist/kanban/lib/parser.d.ts +36 -0
- package/dist/kanban/lib/parser.d.ts.map +1 -0
- package/dist/kanban/lib/parser.js +585 -0
- package/dist/kanban/lib/parser.js.map +1 -0
- package/dist/kanban/lib/path-resolver.d.ts +2 -0
- package/dist/kanban/lib/path-resolver.d.ts.map +1 -0
- package/dist/kanban/lib/path-resolver.js +16 -0
- package/dist/kanban/lib/path-resolver.js.map +1 -0
- package/dist/kanban/lib/review-service.d.ts +63 -0
- package/dist/kanban/lib/review-service.d.ts.map +1 -0
- package/dist/kanban/lib/review-service.js +571 -0
- package/dist/kanban/lib/review-service.js.map +1 -0
- package/dist/kanban/lib/run-cache.d.ts +36 -0
- package/dist/kanban/lib/run-cache.d.ts.map +1 -0
- package/dist/kanban/lib/run-cache.js +313 -0
- package/dist/kanban/lib/run-cache.js.map +1 -0
- package/dist/kanban/lib/server-init.d.ts +26 -0
- package/dist/kanban/lib/server-init.d.ts.map +1 -0
- package/dist/kanban/lib/server-init.js +179 -0
- package/dist/kanban/lib/server-init.js.map +1 -0
- package/dist/kanban/lib/services/automation-rule-service.d.ts +97 -0
- package/dist/kanban/lib/services/automation-rule-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/automation-rule-service.js +806 -0
- package/dist/kanban/lib/services/automation-rule-service.js.map +1 -0
- package/dist/kanban/lib/services/automation-webhook-service.d.ts +44 -0
- package/dist/kanban/lib/services/automation-webhook-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/automation-webhook-service.js +405 -0
- package/dist/kanban/lib/services/automation-webhook-service.js.map +1 -0
- package/dist/kanban/lib/services/backlog-query-service.d.ts +130 -0
- package/dist/kanban/lib/services/backlog-query-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/backlog-query-service.js +1972 -0
- package/dist/kanban/lib/services/backlog-query-service.js.map +1 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.d.ts +39 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.js +160 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.js.map +1 -0
- package/dist/kanban/lib/services/kanban-storage.d.ts +36 -0
- package/dist/kanban/lib/services/kanban-storage.d.ts.map +1 -0
- package/dist/kanban/lib/services/kanban-storage.js +26 -0
- package/dist/kanban/lib/services/kanban-storage.js.map +1 -0
- package/dist/kanban/lib/services/run-query-service.d.ts +79 -0
- package/dist/kanban/lib/services/run-query-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/run-query-service.js +202 -0
- package/dist/kanban/lib/services/run-query-service.js.map +1 -0
- package/dist/kanban/lib/services/task-tag-service.d.ts +39 -0
- package/dist/kanban/lib/services/task-tag-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/task-tag-service.js +145 -0
- package/dist/kanban/lib/services/task-tag-service.js.map +1 -0
- package/dist/kanban/lib/settings-section-storage.d.ts +13 -0
- package/dist/kanban/lib/settings-section-storage.d.ts.map +1 -0
- package/dist/kanban/lib/settings-section-storage.js +38 -0
- package/dist/kanban/lib/settings-section-storage.js.map +1 -0
- package/dist/kanban/lib/source-discovery.d.ts +10 -0
- package/dist/kanban/lib/source-discovery.d.ts.map +1 -0
- package/dist/kanban/lib/source-discovery.js +201 -0
- package/dist/kanban/lib/source-discovery.js.map +1 -0
- package/dist/kanban/lib/utils.d.ts +8 -0
- package/dist/kanban/lib/utils.d.ts.map +1 -0
- package/dist/kanban/lib/utils.js +116 -0
- package/dist/kanban/lib/utils.js.map +1 -0
- package/dist/kanban/lib/watcher.d.ts +14 -0
- package/dist/kanban/lib/watcher.d.ts.map +1 -0
- package/dist/kanban/lib/watcher.js +221 -0
- package/dist/kanban/lib/watcher.js.map +1 -0
- package/dist/kanban/lib/workspace-lifecycle.d.ts +68 -0
- package/dist/kanban/lib/workspace-lifecycle.d.ts.map +1 -0
- package/dist/kanban/lib/workspace-lifecycle.js +1085 -0
- package/dist/kanban/lib/workspace-lifecycle.js.map +1 -0
- package/dist/kanban/routes.d.ts +2 -0
- package/dist/kanban/routes.d.ts.map +1 -0
- package/dist/kanban/routes.js +1358 -0
- package/dist/kanban/routes.js.map +1 -0
- package/dist/kanban/types/breakpoint.d.ts +13 -0
- package/dist/kanban/types/breakpoint.d.ts.map +1 -0
- package/dist/kanban/types/breakpoint.js +3 -0
- package/dist/kanban/types/breakpoint.js.map +1 -0
- package/dist/kanban/types/index.d.ts +173 -0
- package/dist/kanban/types/index.d.ts.map +1 -0
- package/dist/kanban/types/index.js +3 -0
- package/dist/kanban/types/index.js.map +1 -0
- package/dist/logging.d.ts +7 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +22 -0
- package/dist/logging.js.map +1 -0
- package/dist/notifications/types.d.ts +18 -0
- package/dist/notifications/types.d.ts.map +1 -0
- package/dist/notifications/types.js +2 -0
- package/dist/notifications/types.js.map +1 -0
- package/dist/notifications/webhook-out.d.ts +3 -0
- package/dist/notifications/webhook-out.d.ts.map +1 -0
- package/dist/notifications/webhook-out.js +55 -0
- package/dist/notifications/webhook-out.js.map +1 -0
- package/dist/pairing/short-code.d.ts +20 -0
- package/dist/pairing/short-code.d.ts.map +1 -0
- package/dist/pairing/short-code.js +50 -0
- package/dist/pairing/short-code.js.map +1 -0
- package/dist/protocol/errors.d.ts +10 -0
- package/dist/protocol/errors.d.ts.map +1 -0
- package/dist/protocol/errors.js +15 -0
- package/dist/protocol/errors.js.map +1 -0
- package/dist/protocol/frames.d.ts +107 -0
- package/dist/protocol/frames.d.ts.map +1 -0
- package/dist/protocol/frames.js +146 -0
- package/dist/protocol/frames.js.map +1 -0
- package/dist/protocol/v1.d.ts +111 -0
- package/dist/protocol/v1.d.ts.map +1 -0
- package/dist/protocol/v1.js +2 -0
- package/dist/protocol/v1.js.map +1 -0
- package/dist/runs/event-log-index.d.ts +29 -0
- package/dist/runs/event-log-index.d.ts.map +1 -0
- package/dist/runs/event-log-index.js +210 -0
- package/dist/runs/event-log-index.js.map +1 -0
- package/dist/runs/event-log.d.ts +25 -0
- package/dist/runs/event-log.d.ts.map +1 -0
- package/dist/runs/event-log.js +104 -0
- package/dist/runs/event-log.js.map +1 -0
- package/dist/runs/hook-broker.d.ts +18 -0
- package/dist/runs/hook-broker.d.ts.map +1 -0
- package/dist/runs/hook-broker.js +110 -0
- package/dist/runs/hook-broker.js.map +1 -0
- package/dist/runs/manager.d.ts +57 -0
- package/dist/runs/manager.d.ts.map +1 -0
- package/dist/runs/manager.js +757 -0
- package/dist/runs/manager.js.map +1 -0
- package/dist/runs/session-runtime.d.ts +8 -0
- package/dist/runs/session-runtime.d.ts.map +1 -0
- package/dist/runs/session-runtime.js +291 -0
- package/dist/runs/session-runtime.js.map +1 -0
- package/dist/runs/types.d.ts +55 -0
- package/dist/runs/types.d.ts.map +1 -0
- package/dist/runs/types.js +2 -0
- package/dist/runs/types.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +702 -0
- package/dist/server.js.map +1 -0
- package/dist/static/webui-server.d.ts +2 -0
- package/dist/static/webui-server.d.ts.map +1 -0
- package/dist/static/webui-server.js +97 -0
- package/dist/static/webui-server.js.map +1 -0
- 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
|