@paperclipai/server 0.2.2
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/dist/adapters/codex-models.d.ts +4 -0
- package/dist/adapters/codex-models.d.ts.map +1 -0
- package/dist/adapters/codex-models.js +98 -0
- package/dist/adapters/codex-models.js.map +1 -0
- package/dist/adapters/http/execute.d.ts +3 -0
- package/dist/adapters/http/execute.d.ts.map +1 -0
- package/dist/adapters/http/execute.js +39 -0
- package/dist/adapters/http/execute.js.map +1 -0
- package/dist/adapters/http/index.d.ts +3 -0
- package/dist/adapters/http/index.d.ts.map +1 -0
- package/dist/adapters/http/index.js +20 -0
- package/dist/adapters/http/index.js.map +1 -0
- package/dist/adapters/http/test.d.ts +3 -0
- package/dist/adapters/http/test.d.ts.map +1 -0
- package/dist/adapters/http/test.js +106 -0
- package/dist/adapters/http/test.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/process/execute.d.ts +3 -0
- package/dist/adapters/process/execute.d.ts.map +1 -0
- package/dist/adapters/process/execute.js +63 -0
- package/dist/adapters/process/execute.js.map +1 -0
- package/dist/adapters/process/index.d.ts +3 -0
- package/dist/adapters/process/index.d.ts.map +1 -0
- package/dist/adapters/process/index.js +23 -0
- package/dist/adapters/process/index.js.map +1 -0
- package/dist/adapters/process/test.d.ts +3 -0
- package/dist/adapters/process/test.d.ts.map +1 -0
- package/dist/adapters/process/test.js +77 -0
- package/dist/adapters/process/test.js.map +1 -0
- package/dist/adapters/registry.d.ts +9 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +63 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/adapters/utils.d.ts +10 -0
- package/dist/adapters/utils.d.ts.map +1 -0
- package/dist/adapters/utils.js +14 -0
- package/dist/adapters/utils.js.map +1 -0
- package/dist/agent-auth-jwt.d.ts +14 -0
- package/dist/agent-auth-jwt.d.ts.map +1 -0
- package/dist/agent-auth-jwt.js +117 -0
- package/dist/agent-auth-jwt.js.map +1 -0
- package/dist/app.d.ts +20 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +127 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/better-auth.d.ts +23 -0
- package/dist/auth/better-auth.d.ts.map +1 -0
- package/dist/auth/better-auth.js +80 -0
- package/dist/auth/better-auth.js.map +1 -0
- package/dist/board-claim.d.ts +23 -0
- package/dist/board-claim.d.ts.map +1 -0
- package/dist/board-claim.js +115 -0
- package/dist/board-claim.js.map +1 -0
- package/dist/config-file.d.ts +3 -0
- package/dist/config-file.d.ts.map +1 -0
- package/dist/config-file.js +16 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +114 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/home-paths.d.ts +11 -0
- package/dist/home-paths.d.ts.map +1 -0
- package/dist/home-paths.js +54 -0
- package/dist/home-paths.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +439 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +12 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +124 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/board-mutation-guard.d.ts +3 -0
- package/dist/middleware/board-mutation-guard.d.ts.map +1 -0
- package/dist/middleware/board-mutation-guard.js +60 -0
- package/dist/middleware/board-mutation-guard.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +3 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +22 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +4 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +37 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/private-hostname-guard.d.ts +11 -0
- package/dist/middleware/private-hostname-guard.d.ts.map +1 -0
- package/dist/middleware/private-hostname-guard.js +78 -0
- package/dist/middleware/private-hostname-guard.js.map +1 -0
- package/dist/middleware/validate.d.ts +4 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +7 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +31 -0
- package/dist/paths.js.map +1 -0
- package/dist/realtime/live-events-ws.d.ts +10 -0
- package/dist/realtime/live-events-ws.d.ts.map +1 -0
- package/dist/realtime/live-events-ws.js +185 -0
- package/dist/realtime/live-events-ws.js.map +1 -0
- package/dist/redaction.d.ts +4 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +63 -0
- package/dist/redaction.js.map +1 -0
- package/dist/routes/access.d.ts +9 -0
- package/dist/routes/access.d.ts.map +1 -0
- package/dist/routes/access.js +887 -0
- package/dist/routes/access.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +87 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/agents.d.ts +3 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +1132 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/approvals.d.ts +3 -0
- package/dist/routes/approvals.d.ts.map +1 -0
- package/dist/routes/approvals.js +271 -0
- package/dist/routes/approvals.js.map +1 -0
- package/dist/routes/assets.d.ts +4 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +138 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/authz.d.ts +15 -0
- package/dist/routes/authz.d.ts.map +1 -0
- package/dist/routes/authz.js +40 -0
- package/dist/routes/authz.js.map +1 -0
- package/dist/routes/companies.d.ts +3 -0
- package/dist/routes/companies.d.ts.map +1 -0
- package/dist/routes/companies.js +159 -0
- package/dist/routes/companies.js.map +1 -0
- package/dist/routes/costs.d.ts +3 -0
- package/dist/routes/costs.d.ts.map +1 -0
- package/dist/routes/costs.js +113 -0
- package/dist/routes/costs.js.map +1 -0
- package/dist/routes/dashboard.d.ts +3 -0
- package/dist/routes/dashboard.d.ts.map +1 -0
- package/dist/routes/dashboard.js +15 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/goals.d.ts +3 -0
- package/dist/routes/goals.d.ts.map +1 -0
- package/dist/routes/goals.js +95 -0
- package/dist/routes/goals.js.map +1 -0
- package/dist/routes/health.d.ts +9 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +38 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +15 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +15 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/issues.d.ts +4 -0
- package/dist/routes/issues.d.ts.map +1 -0
- package/dist/routes/issues.js +973 -0
- package/dist/routes/issues.js.map +1 -0
- package/dist/routes/llms.d.ts +3 -0
- package/dist/routes/llms.d.ts.map +1 -0
- package/dist/routes/llms.js +78 -0
- package/dist/routes/llms.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +253 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/secrets.d.ts +3 -0
- package/dist/routes/secrets.d.ts.map +1 -0
- package/dist/routes/secrets.js +128 -0
- package/dist/routes/secrets.js.map +1 -0
- package/dist/routes/sidebar-badges.d.ts +3 -0
- package/dist/routes/sidebar-badges.d.ts.map +1 -0
- package/dist/routes/sidebar-badges.js +47 -0
- package/dist/routes/sidebar-badges.js.map +1 -0
- package/dist/secrets/external-stub-providers.d.ts +5 -0
- package/dist/secrets/external-stub-providers.d.ts.map +1 -0
- package/dist/secrets/external-stub-providers.js +21 -0
- package/dist/secrets/external-stub-providers.js.map +1 -0
- package/dist/secrets/local-encrypted-provider.d.ts +3 -0
- package/dist/secrets/local-encrypted-provider.d.ts.map +1 -0
- package/dist/secrets/local-encrypted-provider.js +116 -0
- package/dist/secrets/local-encrypted-provider.js.map +1 -0
- package/dist/secrets/provider-registry.d.ts +5 -0
- package/dist/secrets/provider-registry.d.ts.map +1 -0
- package/dist/secrets/provider-registry.js +20 -0
- package/dist/secrets/provider-registry.js.map +1 -0
- package/dist/secrets/types.d.ts +21 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +2 -0
- package/dist/secrets/types.js.map +1 -0
- package/dist/services/access.d.ts +81 -0
- package/dist/services/access.d.ts.map +1 -0
- package/dist/services/access.js +187 -0
- package/dist/services/access.js.map +1 -0
- package/dist/services/activity-log.d.ts +14 -0
- package/dist/services/activity-log.d.ts.map +1 -0
- package/dist/services/activity-log.js +32 -0
- package/dist/services/activity-log.js.map +1 -0
- package/dist/services/activity.d.ts +764 -0
- package/dist/services/activity.d.ts.map +1 -0
- package/dist/services/activity.js +105 -0
- package/dist/services/activity.js.map +1 -0
- package/dist/services/agent-permissions.d.ts +6 -0
- package/dist/services/agent-permissions.d.ts.map +1 -0
- package/dist/services/agent-permissions.js +18 -0
- package/dist/services/agent-permissions.js.map +1 -0
- package/dist/services/agents.d.ts +1494 -0
- package/dist/services/agents.d.ts.map +1 -0
- package/dist/services/agents.js +454 -0
- package/dist/services/agents.js.map +1 -0
- package/dist/services/approvals.d.ts +540 -0
- package/dist/services/approvals.d.ts.map +1 -0
- package/dist/services/approvals.js +173 -0
- package/dist/services/approvals.js.map +1 -0
- package/dist/services/assets.d.ts +33 -0
- package/dist/services/assets.d.ts.map +1 -0
- package/dist/services/assets.js +17 -0
- package/dist/services/assets.js.map +1 -0
- package/dist/services/companies.d.ts +503 -0
- package/dist/services/companies.d.ts.map +1 -0
- package/dist/services/companies.js +120 -0
- package/dist/services/companies.js.map +1 -0
- package/dist/services/company-portability.d.ts +8 -0
- package/dist/services/company-portability.d.ts.map +1 -0
- package/dist/services/company-portability.js +851 -0
- package/dist/services/company-portability.js.map +1 -0
- package/dist/services/costs.d.ts +50 -0
- package/dist/services/costs.d.ts.map +1 -0
- package/dist/services/costs.js +166 -0
- package/dist/services/costs.js.map +1 -0
- package/dist/services/dashboard.d.ts +21 -0
- package/dist/services/dashboard.d.ts.map +1 -0
- package/dist/services/dashboard.js +96 -0
- package/dist/services/dashboard.js.map +1 -0
- package/dist/services/goals.d.ts +407 -0
- package/dist/services/goals.d.ts.map +1 -0
- package/dist/services/goals.js +29 -0
- package/dist/services/goals.js.map +1 -0
- package/dist/services/heartbeat.d.ts +1666 -0
- package/dist/services/heartbeat.d.ts.map +1 -0
- package/dist/services/heartbeat.js +1752 -0
- package/dist/services/heartbeat.js.map +1 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +20 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/issue-approvals.d.ts +56 -0
- package/dist/services/issue-approvals.d.ts.map +1 -0
- package/dist/services/issue-approvals.js +153 -0
- package/dist/services/issue-approvals.js.map +1 -0
- package/dist/services/issues.d.ts +756 -0
- package/dist/services/issues.d.ts.map +1 -0
- package/dist/services/issues.js +917 -0
- package/dist/services/issues.js.map +1 -0
- package/dist/services/live-events.d.ts +12 -0
- package/dist/services/live-events.d.ts.map +1 -0
- package/dist/services/live-events.js +24 -0
- package/dist/services/live-events.js.map +1 -0
- package/dist/services/projects.d.ts +66 -0
- package/dist/services/projects.d.ts.map +1 -0
- package/dist/services/projects.js +472 -0
- package/dist/services/projects.js.map +1 -0
- package/dist/services/run-log-store.d.ts +34 -0
- package/dist/services/run-log-store.d.ts.map +1 -0
- package/dist/services/run-log-store.js +112 -0
- package/dist/services/run-log-store.js.map +1 -0
- package/dist/services/secrets.d.ts +506 -0
- package/dist/services/secrets.d.ts.map +1 -0
- package/dist/services/secrets.js +284 -0
- package/dist/services/secrets.js.map +1 -0
- package/dist/services/sidebar-badges.d.ts +9 -0
- package/dist/services/sidebar-badges.d.ts.map +1 -0
- package/dist/services/sidebar-badges.js +33 -0
- package/dist/services/sidebar-badges.js.map +1 -0
- package/dist/startup-banner.d.ts +27 -0
- package/dist/startup-banner.d.ts.map +1 -0
- package/dist/startup-banner.js +112 -0
- package/dist/startup-banner.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +29 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local-disk-provider.d.ts +3 -0
- package/dist/storage/local-disk-provider.d.ts.map +1 -0
- package/dist/storage/local-disk-provider.js +79 -0
- package/dist/storage/local-disk-provider.js.map +1 -0
- package/dist/storage/provider-registry.d.ts +4 -0
- package/dist/storage/provider-registry.d.ts.map +1 -0
- package/dist/storage/provider-registry.js +15 -0
- package/dist/storage/provider-registry.js.map +1 -0
- package/dist/storage/s3-provider.d.ts +11 -0
- package/dist/storage/s3-provider.d.ts.map +1 -0
- package/dist/storage/s3-provider.js +123 -0
- package/dist/storage/s3-provider.js.map +1 -0
- package/dist/storage/service.d.ts +3 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +120 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/types.d.ts +55 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
import { and, asc, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
2
|
+
import { agents, assets, companies, companyMemberships, goals, heartbeatRuns, issueAttachments, issueLabels, issueComments, issues, labels, projectWorkspaces, projects, } from "@paperclipai/db";
|
|
3
|
+
import { extractProjectMentionIds } from "@paperclipai/shared";
|
|
4
|
+
import { conflict, notFound, unprocessable } from "../errors.js";
|
|
5
|
+
const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
|
|
6
|
+
function assertTransition(from, to) {
|
|
7
|
+
if (from === to)
|
|
8
|
+
return;
|
|
9
|
+
if (!ALL_ISSUE_STATUSES.includes(to)) {
|
|
10
|
+
throw conflict(`Unknown issue status: ${to}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function applyStatusSideEffects(status, patch) {
|
|
14
|
+
if (!status)
|
|
15
|
+
return patch;
|
|
16
|
+
if (status === "in_progress" && !patch.startedAt) {
|
|
17
|
+
patch.startedAt = new Date();
|
|
18
|
+
}
|
|
19
|
+
if (status === "done") {
|
|
20
|
+
patch.completedAt = new Date();
|
|
21
|
+
}
|
|
22
|
+
if (status === "cancelled") {
|
|
23
|
+
patch.cancelledAt = new Date();
|
|
24
|
+
}
|
|
25
|
+
return patch;
|
|
26
|
+
}
|
|
27
|
+
function sameRunLock(checkoutRunId, actorRunId) {
|
|
28
|
+
if (actorRunId)
|
|
29
|
+
return checkoutRunId === actorRunId;
|
|
30
|
+
return checkoutRunId == null;
|
|
31
|
+
}
|
|
32
|
+
const TERMINAL_HEARTBEAT_RUN_STATUSES = new Set(["succeeded", "failed", "cancelled", "timed_out"]);
|
|
33
|
+
function escapeLikePattern(value) {
|
|
34
|
+
return value.replace(/[\\%_]/g, "\\$&");
|
|
35
|
+
}
|
|
36
|
+
async function labelMapForIssues(dbOrTx, issueIds) {
|
|
37
|
+
const map = new Map();
|
|
38
|
+
if (issueIds.length === 0)
|
|
39
|
+
return map;
|
|
40
|
+
const rows = await dbOrTx
|
|
41
|
+
.select({
|
|
42
|
+
issueId: issueLabels.issueId,
|
|
43
|
+
label: labels,
|
|
44
|
+
})
|
|
45
|
+
.from(issueLabels)
|
|
46
|
+
.innerJoin(labels, eq(issueLabels.labelId, labels.id))
|
|
47
|
+
.where(inArray(issueLabels.issueId, issueIds))
|
|
48
|
+
.orderBy(asc(labels.name), asc(labels.id));
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
const existing = map.get(row.issueId);
|
|
51
|
+
if (existing)
|
|
52
|
+
existing.push(row.label);
|
|
53
|
+
else
|
|
54
|
+
map.set(row.issueId, [row.label]);
|
|
55
|
+
}
|
|
56
|
+
return map;
|
|
57
|
+
}
|
|
58
|
+
async function withIssueLabels(dbOrTx, rows) {
|
|
59
|
+
if (rows.length === 0)
|
|
60
|
+
return [];
|
|
61
|
+
const labelsByIssueId = await labelMapForIssues(dbOrTx, rows.map((row) => row.id));
|
|
62
|
+
return rows.map((row) => {
|
|
63
|
+
const issueLabels = labelsByIssueId.get(row.id) ?? [];
|
|
64
|
+
return {
|
|
65
|
+
...row,
|
|
66
|
+
labels: issueLabels,
|
|
67
|
+
labelIds: issueLabels.map((label) => label.id),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const ACTIVE_RUN_STATUSES = ["queued", "running"];
|
|
72
|
+
async function activeRunMapForIssues(dbOrTx, issueRows) {
|
|
73
|
+
const map = new Map();
|
|
74
|
+
const runIds = issueRows
|
|
75
|
+
.map((row) => row.executionRunId)
|
|
76
|
+
.filter((id) => id != null);
|
|
77
|
+
if (runIds.length === 0)
|
|
78
|
+
return map;
|
|
79
|
+
const rows = await dbOrTx
|
|
80
|
+
.select({
|
|
81
|
+
id: heartbeatRuns.id,
|
|
82
|
+
status: heartbeatRuns.status,
|
|
83
|
+
agentId: heartbeatRuns.agentId,
|
|
84
|
+
invocationSource: heartbeatRuns.invocationSource,
|
|
85
|
+
triggerDetail: heartbeatRuns.triggerDetail,
|
|
86
|
+
startedAt: heartbeatRuns.startedAt,
|
|
87
|
+
finishedAt: heartbeatRuns.finishedAt,
|
|
88
|
+
createdAt: heartbeatRuns.createdAt,
|
|
89
|
+
})
|
|
90
|
+
.from(heartbeatRuns)
|
|
91
|
+
.where(and(inArray(heartbeatRuns.id, runIds), inArray(heartbeatRuns.status, ACTIVE_RUN_STATUSES)));
|
|
92
|
+
for (const row of rows) {
|
|
93
|
+
map.set(row.id, row);
|
|
94
|
+
}
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
97
|
+
function withActiveRuns(issueRows, runMap) {
|
|
98
|
+
return issueRows.map((row) => ({
|
|
99
|
+
...row,
|
|
100
|
+
activeRun: row.executionRunId ? (runMap.get(row.executionRunId) ?? null) : null,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
export function issueService(db) {
|
|
104
|
+
async function assertAssignableAgent(companyId, agentId) {
|
|
105
|
+
const assignee = await db
|
|
106
|
+
.select({
|
|
107
|
+
id: agents.id,
|
|
108
|
+
companyId: agents.companyId,
|
|
109
|
+
status: agents.status,
|
|
110
|
+
})
|
|
111
|
+
.from(agents)
|
|
112
|
+
.where(eq(agents.id, agentId))
|
|
113
|
+
.then((rows) => rows[0] ?? null);
|
|
114
|
+
if (!assignee)
|
|
115
|
+
throw notFound("Assignee agent not found");
|
|
116
|
+
if (assignee.companyId !== companyId) {
|
|
117
|
+
throw unprocessable("Assignee must belong to same company");
|
|
118
|
+
}
|
|
119
|
+
if (assignee.status === "pending_approval") {
|
|
120
|
+
throw conflict("Cannot assign work to pending approval agents");
|
|
121
|
+
}
|
|
122
|
+
if (assignee.status === "terminated") {
|
|
123
|
+
throw conflict("Cannot assign work to terminated agents");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function assertAssignableUser(companyId, userId) {
|
|
127
|
+
const membership = await db
|
|
128
|
+
.select({ id: companyMemberships.id })
|
|
129
|
+
.from(companyMemberships)
|
|
130
|
+
.where(and(eq(companyMemberships.companyId, companyId), eq(companyMemberships.principalType, "user"), eq(companyMemberships.principalId, userId), eq(companyMemberships.status, "active")))
|
|
131
|
+
.then((rows) => rows[0] ?? null);
|
|
132
|
+
if (!membership) {
|
|
133
|
+
throw notFound("Assignee user not found");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function assertValidLabelIds(companyId, labelIds, dbOrTx = db) {
|
|
137
|
+
if (labelIds.length === 0)
|
|
138
|
+
return;
|
|
139
|
+
const existing = await dbOrTx
|
|
140
|
+
.select({ id: labels.id })
|
|
141
|
+
.from(labels)
|
|
142
|
+
.where(and(eq(labels.companyId, companyId), inArray(labels.id, labelIds)));
|
|
143
|
+
if (existing.length !== new Set(labelIds).size) {
|
|
144
|
+
throw unprocessable("One or more labels are invalid for this company");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function syncIssueLabels(issueId, companyId, labelIds, dbOrTx = db) {
|
|
148
|
+
const deduped = [...new Set(labelIds)];
|
|
149
|
+
await assertValidLabelIds(companyId, deduped, dbOrTx);
|
|
150
|
+
await dbOrTx.delete(issueLabels).where(eq(issueLabels.issueId, issueId));
|
|
151
|
+
if (deduped.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
await dbOrTx.insert(issueLabels).values(deduped.map((labelId) => ({
|
|
154
|
+
issueId,
|
|
155
|
+
labelId,
|
|
156
|
+
companyId,
|
|
157
|
+
})));
|
|
158
|
+
}
|
|
159
|
+
async function isTerminalOrMissingHeartbeatRun(runId) {
|
|
160
|
+
const run = await db
|
|
161
|
+
.select({ status: heartbeatRuns.status })
|
|
162
|
+
.from(heartbeatRuns)
|
|
163
|
+
.where(eq(heartbeatRuns.id, runId))
|
|
164
|
+
.then((rows) => rows[0] ?? null);
|
|
165
|
+
if (!run)
|
|
166
|
+
return true;
|
|
167
|
+
return TERMINAL_HEARTBEAT_RUN_STATUSES.has(run.status);
|
|
168
|
+
}
|
|
169
|
+
async function adoptStaleCheckoutRun(input) {
|
|
170
|
+
const stale = await isTerminalOrMissingHeartbeatRun(input.expectedCheckoutRunId);
|
|
171
|
+
if (!stale)
|
|
172
|
+
return null;
|
|
173
|
+
const now = new Date();
|
|
174
|
+
const adopted = await db
|
|
175
|
+
.update(issues)
|
|
176
|
+
.set({
|
|
177
|
+
checkoutRunId: input.actorRunId,
|
|
178
|
+
executionRunId: input.actorRunId,
|
|
179
|
+
executionLockedAt: now,
|
|
180
|
+
updatedAt: now,
|
|
181
|
+
})
|
|
182
|
+
.where(and(eq(issues.id, input.issueId), eq(issues.status, "in_progress"), eq(issues.assigneeAgentId, input.actorAgentId), eq(issues.checkoutRunId, input.expectedCheckoutRunId)))
|
|
183
|
+
.returning({
|
|
184
|
+
id: issues.id,
|
|
185
|
+
status: issues.status,
|
|
186
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
187
|
+
checkoutRunId: issues.checkoutRunId,
|
|
188
|
+
executionRunId: issues.executionRunId,
|
|
189
|
+
})
|
|
190
|
+
.then((rows) => rows[0] ?? null);
|
|
191
|
+
return adopted;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
list: async (companyId, filters) => {
|
|
195
|
+
const conditions = [eq(issues.companyId, companyId)];
|
|
196
|
+
const rawSearch = filters?.q?.trim() ?? "";
|
|
197
|
+
const hasSearch = rawSearch.length > 0;
|
|
198
|
+
const escapedSearch = hasSearch ? escapeLikePattern(rawSearch) : "";
|
|
199
|
+
const startsWithPattern = `${escapedSearch}%`;
|
|
200
|
+
const containsPattern = `%${escapedSearch}%`;
|
|
201
|
+
const titleStartsWithMatch = sql `${issues.title} ILIKE ${startsWithPattern} ESCAPE '\\'`;
|
|
202
|
+
const titleContainsMatch = sql `${issues.title} ILIKE ${containsPattern} ESCAPE '\\'`;
|
|
203
|
+
const identifierStartsWithMatch = sql `${issues.identifier} ILIKE ${startsWithPattern} ESCAPE '\\'`;
|
|
204
|
+
const identifierContainsMatch = sql `${issues.identifier} ILIKE ${containsPattern} ESCAPE '\\'`;
|
|
205
|
+
const descriptionContainsMatch = sql `${issues.description} ILIKE ${containsPattern} ESCAPE '\\'`;
|
|
206
|
+
const commentContainsMatch = sql `
|
|
207
|
+
EXISTS (
|
|
208
|
+
SELECT 1
|
|
209
|
+
FROM ${issueComments}
|
|
210
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
211
|
+
AND ${issueComments.companyId} = ${companyId}
|
|
212
|
+
AND ${issueComments.body} ILIKE ${containsPattern} ESCAPE '\\'
|
|
213
|
+
)
|
|
214
|
+
`;
|
|
215
|
+
if (filters?.status) {
|
|
216
|
+
const statuses = filters.status.split(",").map((s) => s.trim());
|
|
217
|
+
conditions.push(statuses.length === 1 ? eq(issues.status, statuses[0]) : inArray(issues.status, statuses));
|
|
218
|
+
}
|
|
219
|
+
if (filters?.assigneeAgentId) {
|
|
220
|
+
conditions.push(eq(issues.assigneeAgentId, filters.assigneeAgentId));
|
|
221
|
+
}
|
|
222
|
+
if (filters?.assigneeUserId) {
|
|
223
|
+
conditions.push(eq(issues.assigneeUserId, filters.assigneeUserId));
|
|
224
|
+
}
|
|
225
|
+
if (filters?.projectId)
|
|
226
|
+
conditions.push(eq(issues.projectId, filters.projectId));
|
|
227
|
+
if (filters?.labelId) {
|
|
228
|
+
const labeledIssueIds = await db
|
|
229
|
+
.select({ issueId: issueLabels.issueId })
|
|
230
|
+
.from(issueLabels)
|
|
231
|
+
.where(and(eq(issueLabels.companyId, companyId), eq(issueLabels.labelId, filters.labelId)));
|
|
232
|
+
if (labeledIssueIds.length === 0)
|
|
233
|
+
return [];
|
|
234
|
+
conditions.push(inArray(issues.id, labeledIssueIds.map((row) => row.issueId)));
|
|
235
|
+
}
|
|
236
|
+
if (hasSearch) {
|
|
237
|
+
conditions.push(or(titleContainsMatch, identifierContainsMatch, descriptionContainsMatch, commentContainsMatch));
|
|
238
|
+
}
|
|
239
|
+
conditions.push(isNull(issues.hiddenAt));
|
|
240
|
+
const priorityOrder = sql `CASE ${issues.priority} WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END`;
|
|
241
|
+
const searchOrder = sql `
|
|
242
|
+
CASE
|
|
243
|
+
WHEN ${titleStartsWithMatch} THEN 0
|
|
244
|
+
WHEN ${titleContainsMatch} THEN 1
|
|
245
|
+
WHEN ${identifierStartsWithMatch} THEN 2
|
|
246
|
+
WHEN ${identifierContainsMatch} THEN 3
|
|
247
|
+
WHEN ${descriptionContainsMatch} THEN 4
|
|
248
|
+
WHEN ${commentContainsMatch} THEN 5
|
|
249
|
+
ELSE 6
|
|
250
|
+
END
|
|
251
|
+
`;
|
|
252
|
+
const rows = await db
|
|
253
|
+
.select()
|
|
254
|
+
.from(issues)
|
|
255
|
+
.where(and(...conditions))
|
|
256
|
+
.orderBy(hasSearch ? asc(searchOrder) : asc(priorityOrder), asc(priorityOrder), desc(issues.updatedAt));
|
|
257
|
+
const withLabels = await withIssueLabels(db, rows);
|
|
258
|
+
const runMap = await activeRunMapForIssues(db, withLabels);
|
|
259
|
+
return withActiveRuns(withLabels, runMap);
|
|
260
|
+
},
|
|
261
|
+
getById: async (id) => {
|
|
262
|
+
const row = await db
|
|
263
|
+
.select()
|
|
264
|
+
.from(issues)
|
|
265
|
+
.where(eq(issues.id, id))
|
|
266
|
+
.then((rows) => rows[0] ?? null);
|
|
267
|
+
if (!row)
|
|
268
|
+
return null;
|
|
269
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
270
|
+
return enriched;
|
|
271
|
+
},
|
|
272
|
+
getByIdentifier: async (identifier) => {
|
|
273
|
+
const row = await db
|
|
274
|
+
.select()
|
|
275
|
+
.from(issues)
|
|
276
|
+
.where(eq(issues.identifier, identifier.toUpperCase()))
|
|
277
|
+
.then((rows) => rows[0] ?? null);
|
|
278
|
+
if (!row)
|
|
279
|
+
return null;
|
|
280
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
281
|
+
return enriched;
|
|
282
|
+
},
|
|
283
|
+
create: async (companyId, data) => {
|
|
284
|
+
const { labelIds: inputLabelIds, ...issueData } = data;
|
|
285
|
+
if (data.assigneeAgentId && data.assigneeUserId) {
|
|
286
|
+
throw unprocessable("Issue can only have one assignee");
|
|
287
|
+
}
|
|
288
|
+
if (data.assigneeAgentId) {
|
|
289
|
+
await assertAssignableAgent(companyId, data.assigneeAgentId);
|
|
290
|
+
}
|
|
291
|
+
if (data.assigneeUserId) {
|
|
292
|
+
await assertAssignableUser(companyId, data.assigneeUserId);
|
|
293
|
+
}
|
|
294
|
+
if (data.status === "in_progress" && !data.assigneeAgentId && !data.assigneeUserId) {
|
|
295
|
+
throw unprocessable("in_progress issues require an assignee");
|
|
296
|
+
}
|
|
297
|
+
return db.transaction(async (tx) => {
|
|
298
|
+
const [company] = await tx
|
|
299
|
+
.update(companies)
|
|
300
|
+
.set({ issueCounter: sql `${companies.issueCounter} + 1` })
|
|
301
|
+
.where(eq(companies.id, companyId))
|
|
302
|
+
.returning({ issueCounter: companies.issueCounter, issuePrefix: companies.issuePrefix });
|
|
303
|
+
const issueNumber = company.issueCounter;
|
|
304
|
+
const identifier = `${company.issuePrefix}-${issueNumber}`;
|
|
305
|
+
const values = { ...issueData, companyId, issueNumber, identifier };
|
|
306
|
+
if (values.status === "in_progress" && !values.startedAt) {
|
|
307
|
+
values.startedAt = new Date();
|
|
308
|
+
}
|
|
309
|
+
if (values.status === "done") {
|
|
310
|
+
values.completedAt = new Date();
|
|
311
|
+
}
|
|
312
|
+
if (values.status === "cancelled") {
|
|
313
|
+
values.cancelledAt = new Date();
|
|
314
|
+
}
|
|
315
|
+
const [issue] = await tx.insert(issues).values(values).returning();
|
|
316
|
+
if (inputLabelIds) {
|
|
317
|
+
await syncIssueLabels(issue.id, companyId, inputLabelIds, tx);
|
|
318
|
+
}
|
|
319
|
+
const [enriched] = await withIssueLabels(tx, [issue]);
|
|
320
|
+
return enriched;
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
update: async (id, data) => {
|
|
324
|
+
const existing = await db
|
|
325
|
+
.select()
|
|
326
|
+
.from(issues)
|
|
327
|
+
.where(eq(issues.id, id))
|
|
328
|
+
.then((rows) => rows[0] ?? null);
|
|
329
|
+
if (!existing)
|
|
330
|
+
return null;
|
|
331
|
+
const { labelIds: nextLabelIds, ...issueData } = data;
|
|
332
|
+
if (issueData.status) {
|
|
333
|
+
assertTransition(existing.status, issueData.status);
|
|
334
|
+
}
|
|
335
|
+
const patch = {
|
|
336
|
+
...issueData,
|
|
337
|
+
updatedAt: new Date(),
|
|
338
|
+
};
|
|
339
|
+
const nextAssigneeAgentId = issueData.assigneeAgentId !== undefined ? issueData.assigneeAgentId : existing.assigneeAgentId;
|
|
340
|
+
const nextAssigneeUserId = issueData.assigneeUserId !== undefined ? issueData.assigneeUserId : existing.assigneeUserId;
|
|
341
|
+
if (nextAssigneeAgentId && nextAssigneeUserId) {
|
|
342
|
+
throw unprocessable("Issue can only have one assignee");
|
|
343
|
+
}
|
|
344
|
+
if (patch.status === "in_progress" && !nextAssigneeAgentId && !nextAssigneeUserId) {
|
|
345
|
+
throw unprocessable("in_progress issues require an assignee");
|
|
346
|
+
}
|
|
347
|
+
if (issueData.assigneeAgentId) {
|
|
348
|
+
await assertAssignableAgent(existing.companyId, issueData.assigneeAgentId);
|
|
349
|
+
}
|
|
350
|
+
if (issueData.assigneeUserId) {
|
|
351
|
+
await assertAssignableUser(existing.companyId, issueData.assigneeUserId);
|
|
352
|
+
}
|
|
353
|
+
applyStatusSideEffects(issueData.status, patch);
|
|
354
|
+
if (issueData.status && issueData.status !== "done") {
|
|
355
|
+
patch.completedAt = null;
|
|
356
|
+
}
|
|
357
|
+
if (issueData.status && issueData.status !== "cancelled") {
|
|
358
|
+
patch.cancelledAt = null;
|
|
359
|
+
}
|
|
360
|
+
if (issueData.status && issueData.status !== "in_progress") {
|
|
361
|
+
patch.checkoutRunId = null;
|
|
362
|
+
}
|
|
363
|
+
if ((issueData.assigneeAgentId !== undefined && issueData.assigneeAgentId !== existing.assigneeAgentId) ||
|
|
364
|
+
(issueData.assigneeUserId !== undefined && issueData.assigneeUserId !== existing.assigneeUserId)) {
|
|
365
|
+
patch.checkoutRunId = null;
|
|
366
|
+
}
|
|
367
|
+
return db.transaction(async (tx) => {
|
|
368
|
+
const updated = await tx
|
|
369
|
+
.update(issues)
|
|
370
|
+
.set(patch)
|
|
371
|
+
.where(eq(issues.id, id))
|
|
372
|
+
.returning()
|
|
373
|
+
.then((rows) => rows[0] ?? null);
|
|
374
|
+
if (!updated)
|
|
375
|
+
return null;
|
|
376
|
+
if (nextLabelIds !== undefined) {
|
|
377
|
+
await syncIssueLabels(updated.id, existing.companyId, nextLabelIds, tx);
|
|
378
|
+
}
|
|
379
|
+
const [enriched] = await withIssueLabels(tx, [updated]);
|
|
380
|
+
return enriched;
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
remove: (id) => db.transaction(async (tx) => {
|
|
384
|
+
const attachmentAssetIds = await tx
|
|
385
|
+
.select({ assetId: issueAttachments.assetId })
|
|
386
|
+
.from(issueAttachments)
|
|
387
|
+
.where(eq(issueAttachments.issueId, id));
|
|
388
|
+
const removedIssue = await tx
|
|
389
|
+
.delete(issues)
|
|
390
|
+
.where(eq(issues.id, id))
|
|
391
|
+
.returning()
|
|
392
|
+
.then((rows) => rows[0] ?? null);
|
|
393
|
+
if (removedIssue && attachmentAssetIds.length > 0) {
|
|
394
|
+
await tx
|
|
395
|
+
.delete(assets)
|
|
396
|
+
.where(inArray(assets.id, attachmentAssetIds.map((row) => row.assetId)));
|
|
397
|
+
}
|
|
398
|
+
if (!removedIssue)
|
|
399
|
+
return null;
|
|
400
|
+
const [enriched] = await withIssueLabels(tx, [removedIssue]);
|
|
401
|
+
return enriched;
|
|
402
|
+
}),
|
|
403
|
+
checkout: async (id, agentId, expectedStatuses, checkoutRunId) => {
|
|
404
|
+
const issueCompany = await db
|
|
405
|
+
.select({ companyId: issues.companyId })
|
|
406
|
+
.from(issues)
|
|
407
|
+
.where(eq(issues.id, id))
|
|
408
|
+
.then((rows) => rows[0] ?? null);
|
|
409
|
+
if (!issueCompany)
|
|
410
|
+
throw notFound("Issue not found");
|
|
411
|
+
await assertAssignableAgent(issueCompany.companyId, agentId);
|
|
412
|
+
const now = new Date();
|
|
413
|
+
const sameRunAssigneeCondition = checkoutRunId
|
|
414
|
+
? and(eq(issues.assigneeAgentId, agentId), or(isNull(issues.checkoutRunId), eq(issues.checkoutRunId, checkoutRunId)))
|
|
415
|
+
: and(eq(issues.assigneeAgentId, agentId), isNull(issues.checkoutRunId));
|
|
416
|
+
const executionLockCondition = checkoutRunId
|
|
417
|
+
? or(isNull(issues.executionRunId), eq(issues.executionRunId, checkoutRunId))
|
|
418
|
+
: isNull(issues.executionRunId);
|
|
419
|
+
const updated = await db
|
|
420
|
+
.update(issues)
|
|
421
|
+
.set({
|
|
422
|
+
assigneeAgentId: agentId,
|
|
423
|
+
assigneeUserId: null,
|
|
424
|
+
checkoutRunId,
|
|
425
|
+
executionRunId: checkoutRunId,
|
|
426
|
+
status: "in_progress",
|
|
427
|
+
startedAt: now,
|
|
428
|
+
updatedAt: now,
|
|
429
|
+
})
|
|
430
|
+
.where(and(eq(issues.id, id), inArray(issues.status, expectedStatuses), or(isNull(issues.assigneeAgentId), sameRunAssigneeCondition), executionLockCondition))
|
|
431
|
+
.returning()
|
|
432
|
+
.then((rows) => rows[0] ?? null);
|
|
433
|
+
if (updated) {
|
|
434
|
+
const [enriched] = await withIssueLabels(db, [updated]);
|
|
435
|
+
return enriched;
|
|
436
|
+
}
|
|
437
|
+
const current = await db
|
|
438
|
+
.select({
|
|
439
|
+
id: issues.id,
|
|
440
|
+
status: issues.status,
|
|
441
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
442
|
+
checkoutRunId: issues.checkoutRunId,
|
|
443
|
+
executionRunId: issues.executionRunId,
|
|
444
|
+
})
|
|
445
|
+
.from(issues)
|
|
446
|
+
.where(eq(issues.id, id))
|
|
447
|
+
.then((rows) => rows[0] ?? null);
|
|
448
|
+
if (!current)
|
|
449
|
+
throw notFound("Issue not found");
|
|
450
|
+
if (current.assigneeAgentId === agentId &&
|
|
451
|
+
current.status === "in_progress" &&
|
|
452
|
+
current.checkoutRunId == null &&
|
|
453
|
+
(current.executionRunId == null || current.executionRunId === checkoutRunId) &&
|
|
454
|
+
checkoutRunId) {
|
|
455
|
+
const adopted = await db
|
|
456
|
+
.update(issues)
|
|
457
|
+
.set({
|
|
458
|
+
checkoutRunId,
|
|
459
|
+
executionRunId: checkoutRunId,
|
|
460
|
+
updatedAt: new Date(),
|
|
461
|
+
})
|
|
462
|
+
.where(and(eq(issues.id, id), eq(issues.status, "in_progress"), eq(issues.assigneeAgentId, agentId), isNull(issues.checkoutRunId), or(isNull(issues.executionRunId), eq(issues.executionRunId, checkoutRunId))))
|
|
463
|
+
.returning()
|
|
464
|
+
.then((rows) => rows[0] ?? null);
|
|
465
|
+
if (adopted)
|
|
466
|
+
return adopted;
|
|
467
|
+
}
|
|
468
|
+
if (checkoutRunId &&
|
|
469
|
+
current.assigneeAgentId === agentId &&
|
|
470
|
+
current.status === "in_progress" &&
|
|
471
|
+
current.checkoutRunId &&
|
|
472
|
+
current.checkoutRunId !== checkoutRunId) {
|
|
473
|
+
const adopted = await adoptStaleCheckoutRun({
|
|
474
|
+
issueId: id,
|
|
475
|
+
actorAgentId: agentId,
|
|
476
|
+
actorRunId: checkoutRunId,
|
|
477
|
+
expectedCheckoutRunId: current.checkoutRunId,
|
|
478
|
+
});
|
|
479
|
+
if (adopted) {
|
|
480
|
+
const row = await db.select().from(issues).where(eq(issues.id, id)).then((rows) => rows[0]);
|
|
481
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
482
|
+
return enriched;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// If this run already owns it and it's in_progress, return it (no self-409)
|
|
486
|
+
if (current.assigneeAgentId === agentId &&
|
|
487
|
+
current.status === "in_progress" &&
|
|
488
|
+
sameRunLock(current.checkoutRunId, checkoutRunId)) {
|
|
489
|
+
const row = await db.select().from(issues).where(eq(issues.id, id)).then((rows) => rows[0]);
|
|
490
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
491
|
+
return enriched;
|
|
492
|
+
}
|
|
493
|
+
throw conflict("Issue checkout conflict", {
|
|
494
|
+
issueId: current.id,
|
|
495
|
+
status: current.status,
|
|
496
|
+
assigneeAgentId: current.assigneeAgentId,
|
|
497
|
+
checkoutRunId: current.checkoutRunId,
|
|
498
|
+
executionRunId: current.executionRunId,
|
|
499
|
+
});
|
|
500
|
+
},
|
|
501
|
+
assertCheckoutOwner: async (id, actorAgentId, actorRunId) => {
|
|
502
|
+
const current = await db
|
|
503
|
+
.select({
|
|
504
|
+
id: issues.id,
|
|
505
|
+
status: issues.status,
|
|
506
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
507
|
+
checkoutRunId: issues.checkoutRunId,
|
|
508
|
+
})
|
|
509
|
+
.from(issues)
|
|
510
|
+
.where(eq(issues.id, id))
|
|
511
|
+
.then((rows) => rows[0] ?? null);
|
|
512
|
+
if (!current)
|
|
513
|
+
throw notFound("Issue not found");
|
|
514
|
+
if (current.status === "in_progress" &&
|
|
515
|
+
current.assigneeAgentId === actorAgentId &&
|
|
516
|
+
sameRunLock(current.checkoutRunId, actorRunId)) {
|
|
517
|
+
return { ...current, adoptedFromRunId: null };
|
|
518
|
+
}
|
|
519
|
+
if (actorRunId &&
|
|
520
|
+
current.status === "in_progress" &&
|
|
521
|
+
current.assigneeAgentId === actorAgentId &&
|
|
522
|
+
current.checkoutRunId &&
|
|
523
|
+
current.checkoutRunId !== actorRunId) {
|
|
524
|
+
const adopted = await adoptStaleCheckoutRun({
|
|
525
|
+
issueId: id,
|
|
526
|
+
actorAgentId,
|
|
527
|
+
actorRunId,
|
|
528
|
+
expectedCheckoutRunId: current.checkoutRunId,
|
|
529
|
+
});
|
|
530
|
+
if (adopted) {
|
|
531
|
+
return {
|
|
532
|
+
...adopted,
|
|
533
|
+
adoptedFromRunId: current.checkoutRunId,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
throw conflict("Issue run ownership conflict", {
|
|
538
|
+
issueId: current.id,
|
|
539
|
+
status: current.status,
|
|
540
|
+
assigneeAgentId: current.assigneeAgentId,
|
|
541
|
+
checkoutRunId: current.checkoutRunId,
|
|
542
|
+
actorAgentId,
|
|
543
|
+
actorRunId,
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
release: async (id, actorAgentId, actorRunId) => {
|
|
547
|
+
const existing = await db
|
|
548
|
+
.select()
|
|
549
|
+
.from(issues)
|
|
550
|
+
.where(eq(issues.id, id))
|
|
551
|
+
.then((rows) => rows[0] ?? null);
|
|
552
|
+
if (!existing)
|
|
553
|
+
return null;
|
|
554
|
+
if (actorAgentId && existing.assigneeAgentId && existing.assigneeAgentId !== actorAgentId) {
|
|
555
|
+
throw conflict("Only assignee can release issue");
|
|
556
|
+
}
|
|
557
|
+
if (actorAgentId &&
|
|
558
|
+
existing.status === "in_progress" &&
|
|
559
|
+
existing.assigneeAgentId === actorAgentId &&
|
|
560
|
+
existing.checkoutRunId &&
|
|
561
|
+
!sameRunLock(existing.checkoutRunId, actorRunId ?? null)) {
|
|
562
|
+
throw conflict("Only checkout run can release issue", {
|
|
563
|
+
issueId: existing.id,
|
|
564
|
+
assigneeAgentId: existing.assigneeAgentId,
|
|
565
|
+
checkoutRunId: existing.checkoutRunId,
|
|
566
|
+
actorRunId: actorRunId ?? null,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const updated = await db
|
|
570
|
+
.update(issues)
|
|
571
|
+
.set({
|
|
572
|
+
status: "todo",
|
|
573
|
+
assigneeAgentId: null,
|
|
574
|
+
checkoutRunId: null,
|
|
575
|
+
updatedAt: new Date(),
|
|
576
|
+
})
|
|
577
|
+
.where(eq(issues.id, id))
|
|
578
|
+
.returning()
|
|
579
|
+
.then((rows) => rows[0] ?? null);
|
|
580
|
+
if (!updated)
|
|
581
|
+
return null;
|
|
582
|
+
const [enriched] = await withIssueLabels(db, [updated]);
|
|
583
|
+
return enriched;
|
|
584
|
+
},
|
|
585
|
+
listLabels: (companyId) => db.select().from(labels).where(eq(labels.companyId, companyId)).orderBy(asc(labels.name), asc(labels.id)),
|
|
586
|
+
getLabelById: (id) => db
|
|
587
|
+
.select()
|
|
588
|
+
.from(labels)
|
|
589
|
+
.where(eq(labels.id, id))
|
|
590
|
+
.then((rows) => rows[0] ?? null),
|
|
591
|
+
createLabel: async (companyId, data) => {
|
|
592
|
+
const [created] = await db
|
|
593
|
+
.insert(labels)
|
|
594
|
+
.values({
|
|
595
|
+
companyId,
|
|
596
|
+
name: data.name.trim(),
|
|
597
|
+
color: data.color,
|
|
598
|
+
})
|
|
599
|
+
.returning();
|
|
600
|
+
return created;
|
|
601
|
+
},
|
|
602
|
+
deleteLabel: async (id) => db
|
|
603
|
+
.delete(labels)
|
|
604
|
+
.where(eq(labels.id, id))
|
|
605
|
+
.returning()
|
|
606
|
+
.then((rows) => rows[0] ?? null),
|
|
607
|
+
listComments: (issueId) => db
|
|
608
|
+
.select()
|
|
609
|
+
.from(issueComments)
|
|
610
|
+
.where(eq(issueComments.issueId, issueId))
|
|
611
|
+
.orderBy(desc(issueComments.createdAt)),
|
|
612
|
+
addComment: async (issueId, body, actor) => {
|
|
613
|
+
const issue = await db
|
|
614
|
+
.select({ companyId: issues.companyId })
|
|
615
|
+
.from(issues)
|
|
616
|
+
.where(eq(issues.id, issueId))
|
|
617
|
+
.then((rows) => rows[0] ?? null);
|
|
618
|
+
if (!issue)
|
|
619
|
+
throw notFound("Issue not found");
|
|
620
|
+
const [comment] = await db
|
|
621
|
+
.insert(issueComments)
|
|
622
|
+
.values({
|
|
623
|
+
companyId: issue.companyId,
|
|
624
|
+
issueId,
|
|
625
|
+
authorAgentId: actor.agentId ?? null,
|
|
626
|
+
authorUserId: actor.userId ?? null,
|
|
627
|
+
body,
|
|
628
|
+
})
|
|
629
|
+
.returning();
|
|
630
|
+
// Update issue's updatedAt so comment activity is reflected in recency sorting
|
|
631
|
+
await db
|
|
632
|
+
.update(issues)
|
|
633
|
+
.set({ updatedAt: new Date() })
|
|
634
|
+
.where(eq(issues.id, issueId));
|
|
635
|
+
return comment;
|
|
636
|
+
},
|
|
637
|
+
createAttachment: async (input) => {
|
|
638
|
+
const issue = await db
|
|
639
|
+
.select({ id: issues.id, companyId: issues.companyId })
|
|
640
|
+
.from(issues)
|
|
641
|
+
.where(eq(issues.id, input.issueId))
|
|
642
|
+
.then((rows) => rows[0] ?? null);
|
|
643
|
+
if (!issue)
|
|
644
|
+
throw notFound("Issue not found");
|
|
645
|
+
if (input.issueCommentId) {
|
|
646
|
+
const comment = await db
|
|
647
|
+
.select({ id: issueComments.id, companyId: issueComments.companyId, issueId: issueComments.issueId })
|
|
648
|
+
.from(issueComments)
|
|
649
|
+
.where(eq(issueComments.id, input.issueCommentId))
|
|
650
|
+
.then((rows) => rows[0] ?? null);
|
|
651
|
+
if (!comment)
|
|
652
|
+
throw notFound("Issue comment not found");
|
|
653
|
+
if (comment.companyId !== issue.companyId || comment.issueId !== issue.id) {
|
|
654
|
+
throw unprocessable("Attachment comment must belong to same issue and company");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return db.transaction(async (tx) => {
|
|
658
|
+
const [asset] = await tx
|
|
659
|
+
.insert(assets)
|
|
660
|
+
.values({
|
|
661
|
+
companyId: issue.companyId,
|
|
662
|
+
provider: input.provider,
|
|
663
|
+
objectKey: input.objectKey,
|
|
664
|
+
contentType: input.contentType,
|
|
665
|
+
byteSize: input.byteSize,
|
|
666
|
+
sha256: input.sha256,
|
|
667
|
+
originalFilename: input.originalFilename ?? null,
|
|
668
|
+
createdByAgentId: input.createdByAgentId ?? null,
|
|
669
|
+
createdByUserId: input.createdByUserId ?? null,
|
|
670
|
+
})
|
|
671
|
+
.returning();
|
|
672
|
+
const [attachment] = await tx
|
|
673
|
+
.insert(issueAttachments)
|
|
674
|
+
.values({
|
|
675
|
+
companyId: issue.companyId,
|
|
676
|
+
issueId: issue.id,
|
|
677
|
+
assetId: asset.id,
|
|
678
|
+
issueCommentId: input.issueCommentId ?? null,
|
|
679
|
+
})
|
|
680
|
+
.returning();
|
|
681
|
+
return {
|
|
682
|
+
id: attachment.id,
|
|
683
|
+
companyId: attachment.companyId,
|
|
684
|
+
issueId: attachment.issueId,
|
|
685
|
+
issueCommentId: attachment.issueCommentId,
|
|
686
|
+
assetId: attachment.assetId,
|
|
687
|
+
provider: asset.provider,
|
|
688
|
+
objectKey: asset.objectKey,
|
|
689
|
+
contentType: asset.contentType,
|
|
690
|
+
byteSize: asset.byteSize,
|
|
691
|
+
sha256: asset.sha256,
|
|
692
|
+
originalFilename: asset.originalFilename,
|
|
693
|
+
createdByAgentId: asset.createdByAgentId,
|
|
694
|
+
createdByUserId: asset.createdByUserId,
|
|
695
|
+
createdAt: attachment.createdAt,
|
|
696
|
+
updatedAt: attachment.updatedAt,
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
},
|
|
700
|
+
listAttachments: async (issueId) => db
|
|
701
|
+
.select({
|
|
702
|
+
id: issueAttachments.id,
|
|
703
|
+
companyId: issueAttachments.companyId,
|
|
704
|
+
issueId: issueAttachments.issueId,
|
|
705
|
+
issueCommentId: issueAttachments.issueCommentId,
|
|
706
|
+
assetId: issueAttachments.assetId,
|
|
707
|
+
provider: assets.provider,
|
|
708
|
+
objectKey: assets.objectKey,
|
|
709
|
+
contentType: assets.contentType,
|
|
710
|
+
byteSize: assets.byteSize,
|
|
711
|
+
sha256: assets.sha256,
|
|
712
|
+
originalFilename: assets.originalFilename,
|
|
713
|
+
createdByAgentId: assets.createdByAgentId,
|
|
714
|
+
createdByUserId: assets.createdByUserId,
|
|
715
|
+
createdAt: issueAttachments.createdAt,
|
|
716
|
+
updatedAt: issueAttachments.updatedAt,
|
|
717
|
+
})
|
|
718
|
+
.from(issueAttachments)
|
|
719
|
+
.innerJoin(assets, eq(issueAttachments.assetId, assets.id))
|
|
720
|
+
.where(eq(issueAttachments.issueId, issueId))
|
|
721
|
+
.orderBy(desc(issueAttachments.createdAt)),
|
|
722
|
+
getAttachmentById: async (id) => db
|
|
723
|
+
.select({
|
|
724
|
+
id: issueAttachments.id,
|
|
725
|
+
companyId: issueAttachments.companyId,
|
|
726
|
+
issueId: issueAttachments.issueId,
|
|
727
|
+
issueCommentId: issueAttachments.issueCommentId,
|
|
728
|
+
assetId: issueAttachments.assetId,
|
|
729
|
+
provider: assets.provider,
|
|
730
|
+
objectKey: assets.objectKey,
|
|
731
|
+
contentType: assets.contentType,
|
|
732
|
+
byteSize: assets.byteSize,
|
|
733
|
+
sha256: assets.sha256,
|
|
734
|
+
originalFilename: assets.originalFilename,
|
|
735
|
+
createdByAgentId: assets.createdByAgentId,
|
|
736
|
+
createdByUserId: assets.createdByUserId,
|
|
737
|
+
createdAt: issueAttachments.createdAt,
|
|
738
|
+
updatedAt: issueAttachments.updatedAt,
|
|
739
|
+
})
|
|
740
|
+
.from(issueAttachments)
|
|
741
|
+
.innerJoin(assets, eq(issueAttachments.assetId, assets.id))
|
|
742
|
+
.where(eq(issueAttachments.id, id))
|
|
743
|
+
.then((rows) => rows[0] ?? null),
|
|
744
|
+
removeAttachment: async (id) => db.transaction(async (tx) => {
|
|
745
|
+
const existing = await tx
|
|
746
|
+
.select({
|
|
747
|
+
id: issueAttachments.id,
|
|
748
|
+
companyId: issueAttachments.companyId,
|
|
749
|
+
issueId: issueAttachments.issueId,
|
|
750
|
+
issueCommentId: issueAttachments.issueCommentId,
|
|
751
|
+
assetId: issueAttachments.assetId,
|
|
752
|
+
provider: assets.provider,
|
|
753
|
+
objectKey: assets.objectKey,
|
|
754
|
+
contentType: assets.contentType,
|
|
755
|
+
byteSize: assets.byteSize,
|
|
756
|
+
sha256: assets.sha256,
|
|
757
|
+
originalFilename: assets.originalFilename,
|
|
758
|
+
createdByAgentId: assets.createdByAgentId,
|
|
759
|
+
createdByUserId: assets.createdByUserId,
|
|
760
|
+
createdAt: issueAttachments.createdAt,
|
|
761
|
+
updatedAt: issueAttachments.updatedAt,
|
|
762
|
+
})
|
|
763
|
+
.from(issueAttachments)
|
|
764
|
+
.innerJoin(assets, eq(issueAttachments.assetId, assets.id))
|
|
765
|
+
.where(eq(issueAttachments.id, id))
|
|
766
|
+
.then((rows) => rows[0] ?? null);
|
|
767
|
+
if (!existing)
|
|
768
|
+
return null;
|
|
769
|
+
await tx.delete(issueAttachments).where(eq(issueAttachments.id, id));
|
|
770
|
+
await tx.delete(assets).where(eq(assets.id, existing.assetId));
|
|
771
|
+
return existing;
|
|
772
|
+
}),
|
|
773
|
+
findMentionedAgents: async (companyId, body) => {
|
|
774
|
+
const re = /\B@([^\s@,!?.]+)/g;
|
|
775
|
+
const tokens = new Set();
|
|
776
|
+
let m;
|
|
777
|
+
while ((m = re.exec(body)) !== null)
|
|
778
|
+
tokens.add(m[1].toLowerCase());
|
|
779
|
+
if (tokens.size === 0)
|
|
780
|
+
return [];
|
|
781
|
+
const rows = await db.select({ id: agents.id, name: agents.name })
|
|
782
|
+
.from(agents).where(eq(agents.companyId, companyId));
|
|
783
|
+
return rows.filter(a => tokens.has(a.name.toLowerCase())).map(a => a.id);
|
|
784
|
+
},
|
|
785
|
+
findMentionedProjectIds: async (issueId) => {
|
|
786
|
+
const issue = await db
|
|
787
|
+
.select({
|
|
788
|
+
companyId: issues.companyId,
|
|
789
|
+
title: issues.title,
|
|
790
|
+
description: issues.description,
|
|
791
|
+
})
|
|
792
|
+
.from(issues)
|
|
793
|
+
.where(eq(issues.id, issueId))
|
|
794
|
+
.then((rows) => rows[0] ?? null);
|
|
795
|
+
if (!issue)
|
|
796
|
+
return [];
|
|
797
|
+
const comments = await db
|
|
798
|
+
.select({ body: issueComments.body })
|
|
799
|
+
.from(issueComments)
|
|
800
|
+
.where(eq(issueComments.issueId, issueId));
|
|
801
|
+
const mentionedIds = new Set();
|
|
802
|
+
for (const source of [
|
|
803
|
+
issue.title,
|
|
804
|
+
issue.description ?? "",
|
|
805
|
+
...comments.map((comment) => comment.body),
|
|
806
|
+
]) {
|
|
807
|
+
for (const projectId of extractProjectMentionIds(source)) {
|
|
808
|
+
mentionedIds.add(projectId);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (mentionedIds.size === 0)
|
|
812
|
+
return [];
|
|
813
|
+
const rows = await db
|
|
814
|
+
.select({ id: projects.id })
|
|
815
|
+
.from(projects)
|
|
816
|
+
.where(and(eq(projects.companyId, issue.companyId), inArray(projects.id, [...mentionedIds])));
|
|
817
|
+
const valid = new Set(rows.map((row) => row.id));
|
|
818
|
+
return [...mentionedIds].filter((projectId) => valid.has(projectId));
|
|
819
|
+
},
|
|
820
|
+
getAncestors: async (issueId) => {
|
|
821
|
+
const raw = [];
|
|
822
|
+
const visited = new Set([issueId]);
|
|
823
|
+
const start = await db.select().from(issues).where(eq(issues.id, issueId)).then(r => r[0] ?? null);
|
|
824
|
+
let currentId = start?.parentId ?? null;
|
|
825
|
+
while (currentId && !visited.has(currentId) && raw.length < 50) {
|
|
826
|
+
visited.add(currentId);
|
|
827
|
+
const parent = await db.select({
|
|
828
|
+
id: issues.id, identifier: issues.identifier, title: issues.title, description: issues.description,
|
|
829
|
+
status: issues.status, priority: issues.priority,
|
|
830
|
+
assigneeAgentId: issues.assigneeAgentId, projectId: issues.projectId,
|
|
831
|
+
goalId: issues.goalId, parentId: issues.parentId,
|
|
832
|
+
}).from(issues).where(eq(issues.id, currentId)).then(r => r[0] ?? null);
|
|
833
|
+
if (!parent)
|
|
834
|
+
break;
|
|
835
|
+
raw.push({
|
|
836
|
+
id: parent.id, identifier: parent.identifier ?? null, title: parent.title, description: parent.description ?? null,
|
|
837
|
+
status: parent.status, priority: parent.priority,
|
|
838
|
+
assigneeAgentId: parent.assigneeAgentId ?? null,
|
|
839
|
+
projectId: parent.projectId ?? null, goalId: parent.goalId ?? null,
|
|
840
|
+
});
|
|
841
|
+
currentId = parent.parentId ?? null;
|
|
842
|
+
}
|
|
843
|
+
// Batch-fetch referenced projects and goals
|
|
844
|
+
const projectIds = [...new Set(raw.map(a => a.projectId).filter((id) => id != null))];
|
|
845
|
+
const goalIds = [...new Set(raw.map(a => a.goalId).filter((id) => id != null))];
|
|
846
|
+
const projectMap = new Map();
|
|
847
|
+
const goalMap = new Map();
|
|
848
|
+
if (projectIds.length > 0) {
|
|
849
|
+
const workspaceRows = await db
|
|
850
|
+
.select()
|
|
851
|
+
.from(projectWorkspaces)
|
|
852
|
+
.where(inArray(projectWorkspaces.projectId, projectIds))
|
|
853
|
+
.orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id));
|
|
854
|
+
const workspaceMap = new Map();
|
|
855
|
+
for (const workspace of workspaceRows) {
|
|
856
|
+
const existing = workspaceMap.get(workspace.projectId);
|
|
857
|
+
if (existing)
|
|
858
|
+
existing.push(workspace);
|
|
859
|
+
else
|
|
860
|
+
workspaceMap.set(workspace.projectId, [workspace]);
|
|
861
|
+
}
|
|
862
|
+
const rows = await db.select({
|
|
863
|
+
id: projects.id, name: projects.name, description: projects.description,
|
|
864
|
+
status: projects.status, goalId: projects.goalId,
|
|
865
|
+
}).from(projects).where(inArray(projects.id, projectIds));
|
|
866
|
+
for (const r of rows) {
|
|
867
|
+
const projectWorkspaceRows = workspaceMap.get(r.id) ?? [];
|
|
868
|
+
const workspaces = projectWorkspaceRows.map((workspace) => ({
|
|
869
|
+
id: workspace.id,
|
|
870
|
+
companyId: workspace.companyId,
|
|
871
|
+
projectId: workspace.projectId,
|
|
872
|
+
name: workspace.name,
|
|
873
|
+
cwd: workspace.cwd,
|
|
874
|
+
repoUrl: workspace.repoUrl ?? null,
|
|
875
|
+
repoRef: workspace.repoRef ?? null,
|
|
876
|
+
metadata: workspace.metadata ?? null,
|
|
877
|
+
isPrimary: workspace.isPrimary,
|
|
878
|
+
createdAt: workspace.createdAt,
|
|
879
|
+
updatedAt: workspace.updatedAt,
|
|
880
|
+
}));
|
|
881
|
+
const primaryWorkspace = workspaces.find((workspace) => workspace.isPrimary) ?? workspaces[0] ?? null;
|
|
882
|
+
projectMap.set(r.id, {
|
|
883
|
+
...r,
|
|
884
|
+
workspaces,
|
|
885
|
+
primaryWorkspace,
|
|
886
|
+
});
|
|
887
|
+
// Also collect goalIds from projects
|
|
888
|
+
if (r.goalId && !goalIds.includes(r.goalId))
|
|
889
|
+
goalIds.push(r.goalId);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (goalIds.length > 0) {
|
|
893
|
+
const rows = await db.select({
|
|
894
|
+
id: goals.id, title: goals.title, description: goals.description,
|
|
895
|
+
level: goals.level, status: goals.status,
|
|
896
|
+
}).from(goals).where(inArray(goals.id, goalIds));
|
|
897
|
+
for (const r of rows)
|
|
898
|
+
goalMap.set(r.id, r);
|
|
899
|
+
}
|
|
900
|
+
return raw.map(a => ({
|
|
901
|
+
...a,
|
|
902
|
+
project: a.projectId ? projectMap.get(a.projectId) ?? null : null,
|
|
903
|
+
goal: a.goalId ? goalMap.get(a.goalId) ?? null : null,
|
|
904
|
+
}));
|
|
905
|
+
},
|
|
906
|
+
staleCount: async (companyId, minutes = 60) => {
|
|
907
|
+
const cutoff = new Date(Date.now() - minutes * 60 * 1000);
|
|
908
|
+
const result = await db
|
|
909
|
+
.select({ count: sql `count(*)` })
|
|
910
|
+
.from(issues)
|
|
911
|
+
.where(and(eq(issues.companyId, companyId), eq(issues.status, "in_progress"), isNull(issues.hiddenAt), sql `${issues.startedAt} < ${cutoff.toISOString()}`))
|
|
912
|
+
.then((rows) => rows[0]);
|
|
913
|
+
return Number(result?.count ?? 0);
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
//# sourceMappingURL=issues.js.map
|