patchrelay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +271 -0
  3. package/config/patchrelay.example.json +5 -0
  4. package/dist/build-info.js +29 -0
  5. package/dist/build-info.json +6 -0
  6. package/dist/cli/data.js +461 -0
  7. package/dist/cli/formatters/json.js +3 -0
  8. package/dist/cli/formatters/text.js +119 -0
  9. package/dist/cli/index.js +761 -0
  10. package/dist/codex-app-server.js +353 -0
  11. package/dist/codex-types.js +1 -0
  12. package/dist/config-types.js +1 -0
  13. package/dist/config.js +494 -0
  14. package/dist/db/authoritative-ledger-store.js +437 -0
  15. package/dist/db/issue-workflow-store.js +690 -0
  16. package/dist/db/linear-installation-store.js +184 -0
  17. package/dist/db/migrations.js +183 -0
  18. package/dist/db/shared.js +101 -0
  19. package/dist/db/stage-event-store.js +33 -0
  20. package/dist/db/webhook-event-store.js +46 -0
  21. package/dist/db-ports.js +5 -0
  22. package/dist/db-types.js +1 -0
  23. package/dist/db.js +40 -0
  24. package/dist/file-permissions.js +40 -0
  25. package/dist/http.js +321 -0
  26. package/dist/index.js +69 -0
  27. package/dist/install.js +302 -0
  28. package/dist/installation-ports.js +1 -0
  29. package/dist/issue-query-service.js +68 -0
  30. package/dist/ledger-ports.js +1 -0
  31. package/dist/linear-client.js +338 -0
  32. package/dist/linear-oauth-service.js +131 -0
  33. package/dist/linear-oauth.js +154 -0
  34. package/dist/linear-types.js +1 -0
  35. package/dist/linear-workflow.js +78 -0
  36. package/dist/logging.js +62 -0
  37. package/dist/preflight.js +227 -0
  38. package/dist/project-resolution.js +51 -0
  39. package/dist/reconciliation-action-applier.js +55 -0
  40. package/dist/reconciliation-actions.js +1 -0
  41. package/dist/reconciliation-engine.js +312 -0
  42. package/dist/reconciliation-snapshot-builder.js +96 -0
  43. package/dist/reconciliation-types.js +1 -0
  44. package/dist/runtime-paths.js +89 -0
  45. package/dist/service-queue.js +49 -0
  46. package/dist/service-runtime.js +96 -0
  47. package/dist/service-stage-finalizer.js +348 -0
  48. package/dist/service-stage-runner.js +233 -0
  49. package/dist/service-webhook-processor.js +181 -0
  50. package/dist/service-webhooks.js +148 -0
  51. package/dist/service.js +139 -0
  52. package/dist/stage-agent-activity-publisher.js +33 -0
  53. package/dist/stage-event-ports.js +1 -0
  54. package/dist/stage-failure.js +92 -0
  55. package/dist/stage-launch.js +54 -0
  56. package/dist/stage-lifecycle-publisher.js +213 -0
  57. package/dist/stage-reporting.js +153 -0
  58. package/dist/stage-turn-input-dispatcher.js +102 -0
  59. package/dist/token-crypto.js +21 -0
  60. package/dist/types.js +5 -0
  61. package/dist/utils.js +163 -0
  62. package/dist/webhook-agent-session-handler.js +157 -0
  63. package/dist/webhook-archive.js +24 -0
  64. package/dist/webhook-comment-handler.js +89 -0
  65. package/dist/webhook-desired-stage-recorder.js +150 -0
  66. package/dist/webhook-event-ports.js +1 -0
  67. package/dist/webhook-installation-handler.js +57 -0
  68. package/dist/webhooks.js +301 -0
  69. package/dist/workflow-policy.js +42 -0
  70. package/dist/workflow-ports.js +1 -0
  71. package/dist/workflow-types.js +1 -0
  72. package/dist/worktree-manager.js +66 -0
  73. package/infra/patchrelay-reload.service +6 -0
  74. package/infra/patchrelay.path +11 -0
  75. package/infra/patchrelay.service +28 -0
  76. package/package.json +55 -0
  77. package/runtime.env.example +8 -0
  78. package/service.env.example +7 -0
@@ -0,0 +1,690 @@
1
+ import { isoNow } from "./shared.js";
2
+ export class IssueWorkflowStore {
3
+ connection;
4
+ constructor(connection) {
5
+ this.connection = connection;
6
+ }
7
+ upsertTrackedIssue(params) {
8
+ this.upsertIssueProjection({
9
+ projectId: params.projectId,
10
+ linearIssueId: params.linearIssueId,
11
+ ...(params.issueKey ? { issueKey: params.issueKey } : {}),
12
+ ...(params.title ? { title: params.title } : {}),
13
+ ...(params.issueUrl ? { issueUrl: params.issueUrl } : {}),
14
+ ...(params.currentLinearState ? { currentLinearState: params.currentLinearState } : {}),
15
+ ...(params.lastWebhookAt ? { lastWebhookAt: params.lastWebhookAt } : {}),
16
+ });
17
+ const desiredReceiptId = params.desiredWebhookId === undefined
18
+ ? undefined
19
+ : params.desiredWebhookId === null
20
+ ? null
21
+ : this.ensureDesiredReceipt(params.projectId, params.linearIssueId, params.desiredWebhookId);
22
+ this.upsertIssueControl({
23
+ projectId: params.projectId,
24
+ linearIssueId: params.linearIssueId,
25
+ ...(params.desiredStage !== undefined ? { desiredStage: params.desiredStage } : {}),
26
+ ...(desiredReceiptId !== undefined ? { desiredReceiptId } : {}),
27
+ ...(params.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: params.activeWorkspaceId } : {}),
28
+ ...(params.activeStageRunId !== undefined ? { activeRunLeaseId: params.activeStageRunId } : {}),
29
+ ...(params.statusCommentId !== undefined ? { serviceOwnedCommentId: params.statusCommentId } : {}),
30
+ ...(params.activeAgentSessionId !== undefined ? { activeAgentSessionId: params.activeAgentSessionId } : {}),
31
+ lifecycleStatus: params.lifecycleStatus,
32
+ });
33
+ return this.getTrackedIssue(params.projectId, params.linearIssueId);
34
+ }
35
+ getTrackedIssue(projectId, linearIssueId) {
36
+ const issueControl = this.getIssueControlRow(projectId, linearIssueId);
37
+ const projection = this.getIssueProjectionRow(projectId, linearIssueId);
38
+ if (!issueControl && !projection) {
39
+ return undefined;
40
+ }
41
+ return this.buildTrackedIssue(issueControl, projection);
42
+ }
43
+ getTrackedIssueByKey(issueKey) {
44
+ const projection = this.connection
45
+ .prepare("SELECT * FROM issue_projection WHERE issue_key = ? ORDER BY updated_at DESC LIMIT 1")
46
+ .get(issueKey);
47
+ if (!projection) {
48
+ return undefined;
49
+ }
50
+ return this.getTrackedIssue(String(projection.project_id), String(projection.linear_issue_id));
51
+ }
52
+ getTrackedIssueByLinearIssueId(linearIssueId) {
53
+ const projection = this.connection
54
+ .prepare("SELECT * FROM issue_projection WHERE linear_issue_id = ? ORDER BY updated_at DESC LIMIT 1")
55
+ .get(linearIssueId);
56
+ if (!projection) {
57
+ return undefined;
58
+ }
59
+ return this.getTrackedIssue(String(projection.project_id), linearIssueId);
60
+ }
61
+ recordDesiredStage(params) {
62
+ const existing = this.getTrackedIssue(params.projectId, params.linearIssueId);
63
+ this.upsertIssueProjection({
64
+ projectId: params.projectId,
65
+ linearIssueId: params.linearIssueId,
66
+ ...(params.issueKey ? { issueKey: params.issueKey } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
67
+ ...(params.title ? { title: params.title } : existing?.title ? { title: existing.title } : {}),
68
+ ...(params.issueUrl ? { issueUrl: params.issueUrl } : existing?.issueUrl ? { issueUrl: existing.issueUrl } : {}),
69
+ ...(params.currentLinearState
70
+ ? { currentLinearState: params.currentLinearState }
71
+ : existing?.currentLinearState
72
+ ? { currentLinearState: existing.currentLinearState }
73
+ : {}),
74
+ lastWebhookAt: params.lastWebhookAt,
75
+ });
76
+ const existingIssueControl = this.getIssueControlRow(params.projectId, params.linearIssueId);
77
+ const lifecycleStatus = existingIssueControl?.active_run_lease_id || params.desiredStage
78
+ ? existing?.lifecycleStatus ?? "queued"
79
+ : existing?.lifecycleStatus ?? "idle";
80
+ const desiredReceiptId = params.desiredWebhookId === undefined ? undefined : this.ensureDesiredReceipt(params.projectId, params.linearIssueId, params.desiredWebhookId);
81
+ this.upsertIssueControl({
82
+ projectId: params.projectId,
83
+ linearIssueId: params.linearIssueId,
84
+ ...(params.desiredStage !== undefined ? { desiredStage: params.desiredStage } : {}),
85
+ ...(desiredReceiptId !== undefined ? { desiredReceiptId } : {}),
86
+ lifecycleStatus,
87
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
88
+ ...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
89
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
90
+ ...(existingIssueControl?.active_run_lease_id !== undefined && existingIssueControl.active_run_lease_id !== null
91
+ ? { activeRunLeaseId: Number(existingIssueControl.active_run_lease_id) }
92
+ : {}),
93
+ });
94
+ return this.getTrackedIssue(params.projectId, params.linearIssueId);
95
+ }
96
+ listIssuesReadyForExecution() {
97
+ const rows = this.connection
98
+ .prepare("SELECT project_id, linear_issue_id FROM issue_control WHERE desired_stage IS NOT NULL AND active_run_lease_id IS NULL ORDER BY id")
99
+ .all();
100
+ return rows.map((row) => ({
101
+ projectId: String(row.project_id),
102
+ linearIssueId: String(row.linear_issue_id),
103
+ }));
104
+ }
105
+ listActiveStageRuns() {
106
+ return this.listRunLeaseRows({ status: "running" }).map((row) => this.buildStageRun(row));
107
+ }
108
+ claimStageRun(params) {
109
+ const transaction = this.connection.transaction(() => {
110
+ const issue = this.getTrackedIssue(params.projectId, params.linearIssueId);
111
+ const issueControlRow = this.getIssueControlRow(params.projectId, params.linearIssueId);
112
+ const issueControl = issueControlRow ? mapIssueControl(issueControlRow) : undefined;
113
+ if (!issue || !issueControl || issueControl.activeRunLeaseId !== undefined || issue.desiredStage !== params.stage || issue.desiredWebhookId !== params.triggerWebhookId) {
114
+ return undefined;
115
+ }
116
+ const workspaceOwnership = this.upsertWorkspaceOwnership({
117
+ projectId: params.projectId,
118
+ linearIssueId: params.linearIssueId,
119
+ branchName: params.branchName,
120
+ worktreePath: params.worktreePath,
121
+ status: "active",
122
+ });
123
+ const runLease = this.insertRunLease({
124
+ issueControlId: issueControl.id,
125
+ projectId: params.projectId,
126
+ linearIssueId: params.linearIssueId,
127
+ workspaceOwnershipId: workspaceOwnership.id,
128
+ stage: params.stage,
129
+ status: "running",
130
+ workflowFile: params.workflowFile,
131
+ promptText: params.promptText,
132
+ triggerReceiptId: issueControl.desiredReceiptId ?? null,
133
+ });
134
+ this.upsertWorkspaceOwnership({
135
+ projectId: params.projectId,
136
+ linearIssueId: params.linearIssueId,
137
+ branchName: params.branchName,
138
+ worktreePath: params.worktreePath,
139
+ status: "active",
140
+ currentRunLeaseId: runLease.id,
141
+ });
142
+ this.upsertIssueControl({
143
+ projectId: params.projectId,
144
+ linearIssueId: params.linearIssueId,
145
+ desiredStage: null,
146
+ desiredReceiptId: null,
147
+ activeWorkspaceOwnershipId: workspaceOwnership.id,
148
+ activeRunLeaseId: runLease.id,
149
+ lifecycleStatus: "running",
150
+ ...(issue.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
151
+ ...(issue.activeAgentSessionId ? { activeAgentSessionId: issue.activeAgentSessionId } : {}),
152
+ });
153
+ const refreshedIssue = this.getTrackedIssue(params.projectId, params.linearIssueId);
154
+ const workspace = this.getWorkspace(workspaceOwnership.id);
155
+ const stageRun = this.getStageRun(runLease.id);
156
+ const pipeline = this.getPipelineRun(runLease.id);
157
+ return { issue: refreshedIssue, workspace, pipeline, stageRun };
158
+ });
159
+ return transaction();
160
+ }
161
+ getWorkspace(id) {
162
+ const row = this.connection.prepare("SELECT * FROM workspace_ownership WHERE id = ?").get(id);
163
+ if (!row) {
164
+ return undefined;
165
+ }
166
+ return this.buildWorkspace(mapWorkspaceOwnership(row));
167
+ }
168
+ getActiveWorkspaceForIssue(projectId, linearIssueId) {
169
+ const row = this.connection
170
+ .prepare("SELECT * FROM workspace_ownership WHERE project_id = ? AND linear_issue_id = ?")
171
+ .get(projectId, linearIssueId);
172
+ return row ? this.buildWorkspace(mapWorkspaceOwnership(row)) : undefined;
173
+ }
174
+ getPipelineRun(id) {
175
+ const runLease = this.getRunLeaseRowById(id);
176
+ if (!runLease) {
177
+ return undefined;
178
+ }
179
+ const issueControl = this.getIssueControlRow(String(runLease.project_id), String(runLease.linear_issue_id));
180
+ const status = resolvePipelineStatus(runLease.status, issueControl?.lifecycle_status);
181
+ return {
182
+ id: Number(runLease.id),
183
+ projectId: String(runLease.project_id),
184
+ linearIssueId: String(runLease.linear_issue_id),
185
+ workspaceId: Number(runLease.workspace_ownership_id),
186
+ status,
187
+ currentStage: runLease.stage,
188
+ startedAt: String(runLease.started_at),
189
+ ...(runLease.ended_at === null ? {} : { endedAt: String(runLease.ended_at) }),
190
+ };
191
+ }
192
+ getStageRun(id) {
193
+ const row = this.getRunLeaseRowById(id);
194
+ return row ? this.buildStageRun(row) : undefined;
195
+ }
196
+ getStageRunByThreadId(threadId) {
197
+ const row = this.connection
198
+ .prepare("SELECT * FROM run_leases WHERE thread_id = ? ORDER BY id DESC LIMIT 1")
199
+ .get(threadId);
200
+ return row ? this.buildStageRun(row) : undefined;
201
+ }
202
+ listStageRunsForIssue(projectId, linearIssueId) {
203
+ return this.listRunLeaseRows({ projectId, linearIssueId }).map((row) => this.buildStageRun(row));
204
+ }
205
+ updateStageRunThread(params) {
206
+ this.connection
207
+ .prepare(`
208
+ UPDATE run_leases
209
+ SET thread_id = ?,
210
+ parent_thread_id = COALESCE(?, parent_thread_id),
211
+ turn_id = COALESCE(?, turn_id)
212
+ WHERE id = ?
213
+ `)
214
+ .run(params.threadId, params.parentThreadId ?? null, params.turnId ?? null, params.stageRunId);
215
+ }
216
+ finishStageRun(params) {
217
+ const stageRun = this.getStageRun(params.stageRunId);
218
+ if (!stageRun) {
219
+ return;
220
+ }
221
+ const now = isoNow();
222
+ this.connection
223
+ .prepare(`
224
+ INSERT INTO run_reports (run_lease_id, summary_json, report_json, created_at, updated_at)
225
+ VALUES (?, ?, ?, ?, ?)
226
+ ON CONFLICT(run_lease_id) DO UPDATE SET
227
+ summary_json = excluded.summary_json,
228
+ report_json = excluded.report_json,
229
+ updated_at = excluded.updated_at
230
+ `)
231
+ .run(params.stageRunId, params.summaryJson ?? null, params.reportJson ?? null, now, now);
232
+ this.connection
233
+ .prepare(`
234
+ UPDATE run_leases
235
+ SET status = ?,
236
+ thread_id = ?,
237
+ turn_id = COALESCE(?, turn_id),
238
+ ended_at = CASE WHEN ? IN ('completed', 'failed') THEN ? ELSE ended_at END
239
+ WHERE id = ?
240
+ `)
241
+ .run(params.status === "failed" ? "failed" : "completed", params.threadId, params.turnId ?? null, params.status === "failed" ? "failed" : "completed", now, params.stageRunId);
242
+ const workspace = this.getWorkspaceOwnershipRowById(stageRun.workspaceId);
243
+ if (workspace) {
244
+ this.upsertWorkspaceOwnership({
245
+ projectId: stageRun.projectId,
246
+ linearIssueId: stageRun.linearIssueId,
247
+ branchName: String(workspace.branch_name),
248
+ worktreePath: String(workspace.worktree_path),
249
+ status: params.status === "completed" ? "active" : "paused",
250
+ currentRunLeaseId: null,
251
+ });
252
+ }
253
+ }
254
+ setIssueDesiredStage(projectId, linearIssueId, desiredStage, desiredWebhookId) {
255
+ const existing = this.getTrackedIssue(projectId, linearIssueId);
256
+ const existingIssueControl = this.getIssueControlRow(projectId, linearIssueId);
257
+ this.upsertIssueControl({
258
+ projectId,
259
+ linearIssueId,
260
+ ...(desiredStage !== undefined ? { desiredStage } : { desiredStage: null }),
261
+ ...(desiredWebhookId !== undefined
262
+ ? { desiredReceiptId: this.ensureDesiredReceipt(projectId, linearIssueId, desiredWebhookId) }
263
+ : desiredStage === undefined
264
+ ? { desiredReceiptId: null }
265
+ : {}),
266
+ lifecycleStatus: desiredStage ? "queued" : existingIssueControl?.active_run_lease_id ? (existing?.lifecycleStatus ?? "idle") : "idle",
267
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
268
+ ...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
269
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
270
+ ...(existingIssueControl?.active_run_lease_id !== undefined && existingIssueControl.active_run_lease_id !== null
271
+ ? { activeRunLeaseId: Number(existingIssueControl.active_run_lease_id) }
272
+ : {}),
273
+ });
274
+ }
275
+ setIssueLifecycleStatus(projectId, linearIssueId, lifecycleStatus) {
276
+ const existing = this.getTrackedIssue(projectId, linearIssueId);
277
+ const existingIssueControl = this.getIssueControlRow(projectId, linearIssueId);
278
+ this.upsertIssueControl({
279
+ projectId,
280
+ linearIssueId,
281
+ lifecycleStatus,
282
+ ...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
283
+ ...(existing?.desiredWebhookId ? { desiredReceiptId: this.ensureDesiredReceipt(projectId, linearIssueId, existing.desiredWebhookId) } : {}),
284
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
285
+ ...(existingIssueControl?.active_run_lease_id !== undefined && existingIssueControl.active_run_lease_id !== null
286
+ ? { activeRunLeaseId: Number(existingIssueControl.active_run_lease_id) }
287
+ : {}),
288
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
289
+ ...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
290
+ });
291
+ }
292
+ setIssueStatusComment(projectId, linearIssueId, statusCommentId) {
293
+ const existing = this.getTrackedIssue(projectId, linearIssueId);
294
+ const existingIssueControl = this.getIssueControlRow(projectId, linearIssueId);
295
+ this.upsertIssueControl({
296
+ projectId,
297
+ linearIssueId,
298
+ lifecycleStatus: existing?.lifecycleStatus ?? "idle",
299
+ ...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
300
+ ...(existing?.desiredWebhookId ? { desiredReceiptId: this.ensureDesiredReceipt(projectId, linearIssueId, existing.desiredWebhookId) } : {}),
301
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
302
+ ...(existingIssueControl?.active_run_lease_id !== undefined && existingIssueControl.active_run_lease_id !== null
303
+ ? { activeRunLeaseId: Number(existingIssueControl.active_run_lease_id) }
304
+ : {}),
305
+ serviceOwnedCommentId: statusCommentId ?? null,
306
+ ...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
307
+ });
308
+ }
309
+ setIssueActiveAgentSession(projectId, linearIssueId, agentSessionId) {
310
+ const existing = this.getTrackedIssue(projectId, linearIssueId);
311
+ const existingIssueControl = this.getIssueControlRow(projectId, linearIssueId);
312
+ this.upsertIssueControl({
313
+ projectId,
314
+ linearIssueId,
315
+ lifecycleStatus: existing?.lifecycleStatus ?? "idle",
316
+ ...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
317
+ ...(existing?.desiredWebhookId ? { desiredReceiptId: this.ensureDesiredReceipt(projectId, linearIssueId, existing.desiredWebhookId) } : {}),
318
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
319
+ ...(existingIssueControl?.active_run_lease_id !== undefined && existingIssueControl.active_run_lease_id !== null
320
+ ? { activeRunLeaseId: Number(existingIssueControl.active_run_lease_id) }
321
+ : {}),
322
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
323
+ activeAgentSessionId: agentSessionId ?? null,
324
+ });
325
+ }
326
+ getLatestStageRunForIssue(projectId, linearIssueId) {
327
+ const row = this.connection
328
+ .prepare("SELECT * FROM run_leases WHERE project_id = ? AND linear_issue_id = ? ORDER BY id DESC LIMIT 1")
329
+ .get(projectId, linearIssueId);
330
+ return row ? this.buildStageRun(row) : undefined;
331
+ }
332
+ getIssueOverview(issueKey) {
333
+ const issue = this.getTrackedIssueByKey(issueKey);
334
+ if (!issue) {
335
+ return undefined;
336
+ }
337
+ const issueControl = this.getIssueControlRow(issue.projectId, issue.linearIssueId);
338
+ const activeWorkspaceOwnershipId = issueControl?.active_workspace_ownership_id === null || issueControl?.active_workspace_ownership_id === undefined
339
+ ? undefined
340
+ : Number(issueControl.active_workspace_ownership_id);
341
+ const activeRunLeaseId = issueControl?.active_run_lease_id === null || issueControl?.active_run_lease_id === undefined
342
+ ? undefined
343
+ : Number(issueControl.active_run_lease_id);
344
+ const workspace = activeWorkspaceOwnershipId ? this.getWorkspace(activeWorkspaceOwnershipId) : this.getActiveWorkspaceForIssue(issue.projectId, issue.linearIssueId);
345
+ const pipeline = activeRunLeaseId ? this.getPipelineRun(activeRunLeaseId) : issue.activePipelineRunId ? this.getPipelineRun(issue.activePipelineRunId) : undefined;
346
+ const activeStageRun = activeRunLeaseId === undefined ? undefined : this.getStageRun(activeRunLeaseId);
347
+ return {
348
+ issue,
349
+ ...(workspace ? { workspace } : {}),
350
+ ...(pipeline ? { pipeline } : {}),
351
+ ...(activeStageRun ? { activeStageRun } : {}),
352
+ };
353
+ }
354
+ upsertIssueProjection(params) {
355
+ this.connection
356
+ .prepare(`
357
+ INSERT INTO issue_projection (
358
+ project_id, linear_issue_id, issue_key, title, issue_url, current_linear_state, last_webhook_at, updated_at
359
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
360
+ ON CONFLICT(project_id, linear_issue_id) DO UPDATE SET
361
+ issue_key = COALESCE(excluded.issue_key, issue_projection.issue_key),
362
+ title = COALESCE(excluded.title, issue_projection.title),
363
+ issue_url = COALESCE(excluded.issue_url, issue_projection.issue_url),
364
+ current_linear_state = COALESCE(excluded.current_linear_state, issue_projection.current_linear_state),
365
+ last_webhook_at = COALESCE(excluded.last_webhook_at, issue_projection.last_webhook_at),
366
+ updated_at = excluded.updated_at
367
+ `)
368
+ .run(params.projectId, params.linearIssueId, params.issueKey ?? null, params.title ?? null, params.issueUrl ?? null, params.currentLinearState ?? null, params.lastWebhookAt ?? null, isoNow());
369
+ }
370
+ upsertIssueControl(params) {
371
+ const now = isoNow();
372
+ this.connection
373
+ .prepare(`
374
+ INSERT INTO issue_control (
375
+ project_id, linear_issue_id, desired_stage, desired_receipt_id, active_workspace_ownership_id,
376
+ active_run_lease_id, service_owned_comment_id, active_agent_session_id, lifecycle_status, updated_at
377
+ ) VALUES (
378
+ @projectId, @linearIssueId, @desiredStage, @desiredReceiptId, @activeWorkspaceOwnershipId,
379
+ @activeRunLeaseId, @serviceOwnedCommentId, @activeAgentSessionId, @lifecycleStatus, @updatedAt
380
+ )
381
+ ON CONFLICT(project_id, linear_issue_id) DO UPDATE SET
382
+ desired_stage = CASE WHEN @setDesiredStage = 1 THEN @desiredStage ELSE issue_control.desired_stage END,
383
+ desired_receipt_id = CASE WHEN @setDesiredReceiptId = 1 THEN @desiredReceiptId ELSE issue_control.desired_receipt_id END,
384
+ active_workspace_ownership_id = CASE WHEN @setActiveWorkspaceOwnershipId = 1 THEN @activeWorkspaceOwnershipId ELSE issue_control.active_workspace_ownership_id END,
385
+ active_run_lease_id = CASE WHEN @setActiveRunLeaseId = 1 THEN @activeRunLeaseId ELSE issue_control.active_run_lease_id END,
386
+ service_owned_comment_id = CASE WHEN @setServiceOwnedCommentId = 1 THEN @serviceOwnedCommentId ELSE issue_control.service_owned_comment_id END,
387
+ active_agent_session_id = CASE WHEN @setActiveAgentSessionId = 1 THEN @activeAgentSessionId ELSE issue_control.active_agent_session_id END,
388
+ lifecycle_status = @lifecycleStatus,
389
+ updated_at = @updatedAt
390
+ `)
391
+ .run({
392
+ projectId: params.projectId,
393
+ linearIssueId: params.linearIssueId,
394
+ desiredStage: params.desiredStage ?? null,
395
+ desiredReceiptId: params.desiredReceiptId ?? null,
396
+ activeWorkspaceOwnershipId: params.activeWorkspaceOwnershipId ?? null,
397
+ activeRunLeaseId: params.activeRunLeaseId ?? null,
398
+ serviceOwnedCommentId: params.serviceOwnedCommentId ?? null,
399
+ activeAgentSessionId: params.activeAgentSessionId ?? null,
400
+ lifecycleStatus: params.lifecycleStatus,
401
+ updatedAt: now,
402
+ setDesiredStage: Number("desiredStage" in params),
403
+ setDesiredReceiptId: Number("desiredReceiptId" in params),
404
+ setActiveWorkspaceOwnershipId: Number("activeWorkspaceOwnershipId" in params),
405
+ setActiveRunLeaseId: Number("activeRunLeaseId" in params),
406
+ setServiceOwnedCommentId: Number("serviceOwnedCommentId" in params),
407
+ setActiveAgentSessionId: Number("activeAgentSessionId" in params),
408
+ });
409
+ return mapIssueControl(this.connection
410
+ .prepare("SELECT * FROM issue_control WHERE project_id = ? AND linear_issue_id = ?")
411
+ .get(params.projectId, params.linearIssueId));
412
+ }
413
+ upsertWorkspaceOwnership(params) {
414
+ const now = isoNow();
415
+ this.connection
416
+ .prepare(`
417
+ INSERT INTO workspace_ownership (
418
+ project_id, linear_issue_id, branch_name, worktree_path, status, current_run_lease_id, created_at, updated_at
419
+ ) VALUES (@projectId, @linearIssueId, @branchName, @worktreePath, @status, @currentRunLeaseId, @createdAt, @updatedAt)
420
+ ON CONFLICT(project_id, linear_issue_id) DO UPDATE SET
421
+ branch_name = @branchName,
422
+ worktree_path = @worktreePath,
423
+ status = @status,
424
+ current_run_lease_id = CASE WHEN @setCurrentRunLeaseId = 1 THEN @currentRunLeaseId ELSE workspace_ownership.current_run_lease_id END,
425
+ updated_at = @updatedAt
426
+ `)
427
+ .run({
428
+ projectId: params.projectId,
429
+ linearIssueId: params.linearIssueId,
430
+ branchName: params.branchName,
431
+ worktreePath: params.worktreePath,
432
+ status: params.status,
433
+ currentRunLeaseId: params.currentRunLeaseId ?? null,
434
+ createdAt: now,
435
+ updatedAt: now,
436
+ setCurrentRunLeaseId: Number("currentRunLeaseId" in params),
437
+ });
438
+ return mapWorkspaceOwnership(this.connection
439
+ .prepare("SELECT * FROM workspace_ownership WHERE project_id = ? AND linear_issue_id = ?")
440
+ .get(params.projectId, params.linearIssueId));
441
+ }
442
+ insertRunLease(params) {
443
+ const result = this.connection
444
+ .prepare(`
445
+ INSERT INTO run_leases (
446
+ issue_control_id, project_id, linear_issue_id, workspace_ownership_id, stage, status, trigger_receipt_id, workflow_file, prompt_text, started_at
447
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
448
+ `)
449
+ .run(params.issueControlId, params.projectId, params.linearIssueId, params.workspaceOwnershipId, params.stage, params.status, params.triggerReceiptId ?? null, params.workflowFile, params.promptText, isoNow());
450
+ return this.buildRunLease(this.connection.prepare("SELECT * FROM run_leases WHERE id = ?").get(Number(result.lastInsertRowid)));
451
+ }
452
+ buildTrackedIssue(issueControlRow, projectionRow) {
453
+ const issueControl = issueControlRow ? mapIssueControl(issueControlRow) : undefined;
454
+ const projection = projectionRow ? mapIssueProjection(projectionRow) : undefined;
455
+ const projectId = issueControl?.projectId ?? projection?.projectId;
456
+ const linearIssueId = issueControl?.linearIssueId ?? projection?.linearIssueId;
457
+ if (!projectId || !linearIssueId) {
458
+ throw new Error("Cannot synthesize tracked issue without an issue identity");
459
+ }
460
+ const latestRun = this.getLatestRunLeaseForIssue(projectId, linearIssueId);
461
+ const activeRun = issueControl?.activeRunLeaseId ? this.getStageRun(issueControl.activeRunLeaseId) : undefined;
462
+ return {
463
+ id: issueControl?.id ?? projection?.id ?? 0,
464
+ projectId,
465
+ linearIssueId,
466
+ ...(projection?.issueKey ? { issueKey: projection.issueKey } : {}),
467
+ ...(projection?.title ? { title: projection.title } : {}),
468
+ ...(projection?.issueUrl ? { issueUrl: projection.issueUrl } : {}),
469
+ ...(projection?.currentLinearState ? { currentLinearState: projection.currentLinearState } : {}),
470
+ ...(issueControl?.desiredStage ? { desiredStage: issueControl.desiredStage } : {}),
471
+ ...(() => {
472
+ if (!issueControl?.desiredReceiptId) {
473
+ return {};
474
+ }
475
+ const receipt = this.getEventReceiptById(issueControl.desiredReceiptId);
476
+ return receipt?.externalId ? { desiredWebhookId: receipt.externalId } : {};
477
+ })(),
478
+ ...(issueControl?.activeWorkspaceOwnershipId !== undefined ? { activeWorkspaceId: issueControl.activeWorkspaceOwnershipId } : {}),
479
+ ...(latestRun ? { activePipelineRunId: latestRun.id } : {}),
480
+ ...(issueControl?.activeRunLeaseId !== undefined ? { activeStageRunId: issueControl.activeRunLeaseId } : {}),
481
+ ...(activeRun?.threadId ? { latestThreadId: activeRun.threadId } : latestRun?.threadId ? { latestThreadId: latestRun.threadId } : {}),
482
+ ...(issueControl?.serviceOwnedCommentId ? { statusCommentId: issueControl.serviceOwnedCommentId } : {}),
483
+ ...(issueControl?.activeAgentSessionId ? { activeAgentSessionId: issueControl.activeAgentSessionId } : {}),
484
+ lifecycleStatus: issueControl?.lifecycleStatus ?? "idle",
485
+ ...(projection?.lastWebhookAt ? { lastWebhookAt: projection.lastWebhookAt } : {}),
486
+ updatedAt: issueControl?.updatedAt ?? projection?.updatedAt ?? isoNow(),
487
+ };
488
+ }
489
+ buildWorkspace(workspaceOwnership) {
490
+ const stageRuns = this.listStageRunsForIssue(workspaceOwnership.projectId, workspaceOwnership.linearIssueId);
491
+ const latestStageRun = stageRuns.findLast((stageRun) => stageRun.status !== "running") ?? stageRuns.at(-1);
492
+ return {
493
+ id: workspaceOwnership.id,
494
+ projectId: workspaceOwnership.projectId,
495
+ linearIssueId: workspaceOwnership.linearIssueId,
496
+ branchName: workspaceOwnership.branchName,
497
+ worktreePath: workspaceOwnership.worktreePath,
498
+ status: workspaceOwnership.status === "released" ? "closed" : workspaceOwnership.status,
499
+ ...(latestStageRun ? { lastStage: latestStageRun.stage } : {}),
500
+ ...(latestStageRun?.threadId ? { lastThreadId: latestStageRun.threadId } : {}),
501
+ createdAt: workspaceOwnership.createdAt,
502
+ updatedAt: workspaceOwnership.updatedAt,
503
+ };
504
+ }
505
+ buildStageRun(runLeaseRow) {
506
+ const runLease = this.buildRunLease(runLeaseRow);
507
+ const report = this.getRunReport(runLease.id);
508
+ const triggerWebhookId = runLease.triggerReceiptId ? this.getEventReceiptById(runLease.triggerReceiptId)?.externalId ?? `run-lease:${runLease.id}` : `run-lease:${runLease.id}`;
509
+ return {
510
+ id: runLease.id,
511
+ pipelineRunId: runLease.id,
512
+ projectId: runLease.projectId,
513
+ linearIssueId: runLease.linearIssueId,
514
+ workspaceId: runLease.workspaceOwnershipId,
515
+ stage: runLease.stage,
516
+ status: runLease.status === "failed" ? "failed" : runLease.status === "completed" || runLease.status === "released" || runLease.status === "paused" ? "completed" : "running",
517
+ triggerWebhookId,
518
+ workflowFile: runLease.workflowFile,
519
+ promptText: runLease.promptText,
520
+ ...(runLease.threadId ? { threadId: runLease.threadId } : {}),
521
+ ...(runLease.parentThreadId ? { parentThreadId: runLease.parentThreadId } : {}),
522
+ ...(runLease.turnId ? { turnId: runLease.turnId } : {}),
523
+ ...(report?.summaryJson ? { summaryJson: report.summaryJson } : {}),
524
+ ...(report?.reportJson ? { reportJson: report.reportJson } : {}),
525
+ startedAt: runLease.startedAt,
526
+ ...(runLease.endedAt ? { endedAt: runLease.endedAt } : {}),
527
+ };
528
+ }
529
+ buildRunLease(row) {
530
+ return {
531
+ id: Number(row.id),
532
+ issueControlId: Number(row.issue_control_id),
533
+ projectId: String(row.project_id),
534
+ linearIssueId: String(row.linear_issue_id),
535
+ workspaceOwnershipId: Number(row.workspace_ownership_id),
536
+ stage: row.stage,
537
+ status: row.status,
538
+ ...(row.trigger_receipt_id === null ? {} : { triggerReceiptId: Number(row.trigger_receipt_id) }),
539
+ workflowFile: String(row.workflow_file ?? ""),
540
+ promptText: String(row.prompt_text ?? ""),
541
+ ...(row.thread_id === null ? {} : { threadId: String(row.thread_id) }),
542
+ ...(row.parent_thread_id === null ? {} : { parentThreadId: String(row.parent_thread_id) }),
543
+ ...(row.turn_id === null ? {} : { turnId: String(row.turn_id) }),
544
+ startedAt: String(row.started_at),
545
+ ...(row.ended_at === null ? {} : { endedAt: String(row.ended_at) }),
546
+ ...(row.failure_reason === null ? {} : { failureReason: String(row.failure_reason) }),
547
+ };
548
+ }
549
+ getIssueProjectionRow(projectId, linearIssueId) {
550
+ return this.connection
551
+ .prepare("SELECT * FROM issue_projection WHERE project_id = ? AND linear_issue_id = ?")
552
+ .get(projectId, linearIssueId);
553
+ }
554
+ getIssueControlRow(projectId, linearIssueId) {
555
+ return this.connection
556
+ .prepare("SELECT * FROM issue_control WHERE project_id = ? AND linear_issue_id = ?")
557
+ .get(projectId, linearIssueId);
558
+ }
559
+ getWorkspaceOwnershipRowById(id) {
560
+ return this.connection.prepare("SELECT * FROM workspace_ownership WHERE id = ?").get(id);
561
+ }
562
+ getRunLeaseRowById(id) {
563
+ return this.connection.prepare("SELECT * FROM run_leases WHERE id = ?").get(id);
564
+ }
565
+ listRunLeaseRows(params) {
566
+ const clauses = [];
567
+ const values = [];
568
+ if (params?.projectId) {
569
+ clauses.push("project_id = ?");
570
+ values.push(params.projectId);
571
+ }
572
+ if (params?.linearIssueId) {
573
+ clauses.push("linear_issue_id = ?");
574
+ values.push(params.linearIssueId);
575
+ }
576
+ if (params?.status) {
577
+ clauses.push("status = ?");
578
+ values.push(params.status);
579
+ }
580
+ const sql = `SELECT * FROM run_leases${clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY id`;
581
+ return this.connection.prepare(sql).all(...values);
582
+ }
583
+ getLatestRunLeaseForIssue(projectId, linearIssueId) {
584
+ const row = this.connection
585
+ .prepare("SELECT * FROM run_leases WHERE project_id = ? AND linear_issue_id = ? ORDER BY id DESC LIMIT 1")
586
+ .get(projectId, linearIssueId);
587
+ return row ? this.buildRunLease(row) : undefined;
588
+ }
589
+ getRunReport(runLeaseId) {
590
+ const row = this.connection.prepare("SELECT * FROM run_reports WHERE run_lease_id = ?").get(runLeaseId);
591
+ return row ? mapRunReport(row) : undefined;
592
+ }
593
+ getEventReceiptById(id) {
594
+ const row = this.connection.prepare("SELECT * FROM event_receipts WHERE id = ?").get(id);
595
+ return row ? mapEventReceipt(row) : undefined;
596
+ }
597
+ ensureDesiredReceipt(projectId, linearIssueId, webhookId) {
598
+ const existing = this.connection
599
+ .prepare("SELECT * FROM event_receipts WHERE external_id = ? ORDER BY id DESC LIMIT 1")
600
+ .get(webhookId);
601
+ if (existing) {
602
+ return Number(existing.id);
603
+ }
604
+ const result = this.connection
605
+ .prepare(`
606
+ INSERT INTO event_receipts (
607
+ source, external_id, event_type, received_at, acceptance_status, processing_status, project_id, linear_issue_id
608
+ ) VALUES (?, ?, ?, ?, 'accepted', 'processed', ?, ?)
609
+ `)
610
+ .run("patchrelay-desired-stage", webhookId, "desired_stage", isoNow(), projectId, linearIssueId);
611
+ return Number(result.lastInsertRowid);
612
+ }
613
+ }
614
+ function resolvePipelineStatus(runStatus, lifecycleStatus) {
615
+ if (lifecycleStatus === "paused") {
616
+ return "paused";
617
+ }
618
+ if (runStatus === "failed" || lifecycleStatus === "failed") {
619
+ return "failed";
620
+ }
621
+ if (runStatus === "completed" || runStatus === "released" || lifecycleStatus === "completed") {
622
+ return "completed";
623
+ }
624
+ return "active";
625
+ }
626
+ function mapIssueProjection(row) {
627
+ return {
628
+ id: Number(row.id),
629
+ projectId: String(row.project_id),
630
+ linearIssueId: String(row.linear_issue_id),
631
+ ...(row.issue_key === null ? {} : { issueKey: String(row.issue_key) }),
632
+ ...(row.title === null ? {} : { title: String(row.title) }),
633
+ ...(row.issue_url === null ? {} : { issueUrl: String(row.issue_url) }),
634
+ ...(row.current_linear_state === null ? {} : { currentLinearState: String(row.current_linear_state) }),
635
+ ...(row.last_webhook_at === null ? {} : { lastWebhookAt: String(row.last_webhook_at) }),
636
+ updatedAt: String(row.updated_at),
637
+ };
638
+ }
639
+ function mapIssueControl(row) {
640
+ return {
641
+ id: Number(row.id),
642
+ projectId: String(row.project_id),
643
+ linearIssueId: String(row.linear_issue_id),
644
+ ...(row.desired_stage === null ? {} : { desiredStage: row.desired_stage }),
645
+ ...(row.desired_receipt_id === null ? {} : { desiredReceiptId: Number(row.desired_receipt_id) }),
646
+ ...(row.active_run_lease_id === null ? {} : { activeRunLeaseId: Number(row.active_run_lease_id) }),
647
+ ...(row.active_workspace_ownership_id === null ? {} : { activeWorkspaceOwnershipId: Number(row.active_workspace_ownership_id) }),
648
+ ...(row.service_owned_comment_id === null ? {} : { serviceOwnedCommentId: String(row.service_owned_comment_id) }),
649
+ ...(row.active_agent_session_id === null ? {} : { activeAgentSessionId: String(row.active_agent_session_id) }),
650
+ lifecycleStatus: row.lifecycle_status,
651
+ updatedAt: String(row.updated_at),
652
+ };
653
+ }
654
+ function mapWorkspaceOwnership(row) {
655
+ return {
656
+ id: Number(row.id),
657
+ projectId: String(row.project_id),
658
+ linearIssueId: String(row.linear_issue_id),
659
+ branchName: String(row.branch_name),
660
+ worktreePath: String(row.worktree_path),
661
+ status: row.status,
662
+ ...(row.current_run_lease_id === null ? {} : { currentRunLeaseId: Number(row.current_run_lease_id) }),
663
+ createdAt: String(row.created_at),
664
+ updatedAt: String(row.updated_at),
665
+ };
666
+ }
667
+ function mapRunReport(row) {
668
+ return {
669
+ runLeaseId: Number(row.run_lease_id),
670
+ ...(row.summary_json === null ? {} : { summaryJson: String(row.summary_json) }),
671
+ ...(row.report_json === null ? {} : { reportJson: String(row.report_json) }),
672
+ createdAt: String(row.created_at),
673
+ updatedAt: String(row.updated_at),
674
+ };
675
+ }
676
+ function mapEventReceipt(row) {
677
+ return {
678
+ id: Number(row.id),
679
+ source: String(row.source),
680
+ externalId: String(row.external_id),
681
+ eventType: String(row.event_type),
682
+ receivedAt: String(row.received_at),
683
+ acceptanceStatus: row.acceptance_status,
684
+ processingStatus: row.processing_status,
685
+ ...(row.project_id === null ? {} : { projectId: String(row.project_id) }),
686
+ ...(row.linear_issue_id === null ? {} : { linearIssueId: String(row.linear_issue_id) }),
687
+ ...(row.headers_json === null ? {} : { headersJson: String(row.headers_json) }),
688
+ ...(row.payload_json === null ? {} : { payloadJson: String(row.payload_json) }),
689
+ };
690
+ }