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,437 @@
1
+ import { isoNow } from "./shared.js";
2
+ export class AuthoritativeLedgerStore {
3
+ connection;
4
+ constructor(connection) {
5
+ this.connection = connection;
6
+ }
7
+ insertEventReceipt(params) {
8
+ const inserted = this.connection
9
+ .prepare(`
10
+ INSERT OR IGNORE INTO event_receipts (
11
+ source, external_id, event_type, received_at, acceptance_status, processing_status,
12
+ project_id, linear_issue_id, headers_json, payload_json
13
+ ) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?)
14
+ `)
15
+ .run(params.source, params.externalId, params.eventType, params.receivedAt, params.acceptanceStatus, params.projectId ?? null, params.linearIssueId ?? null, params.headersJson ?? null, params.payloadJson ?? null);
16
+ const row = this.connection
17
+ .prepare("SELECT * FROM event_receipts WHERE source = ? AND external_id = ?")
18
+ .get(params.source, params.externalId);
19
+ if (!row) {
20
+ throw new Error(`Failed to load event receipt for ${params.source}:${params.externalId}`);
21
+ }
22
+ if (!inserted.changes) {
23
+ this.connection
24
+ .prepare("UPDATE event_receipts SET acceptance_status = 'duplicate' WHERE id = ? AND acceptance_status = 'accepted'")
25
+ .run(row.id);
26
+ }
27
+ return { id: Number(row.id), inserted: Boolean(inserted.changes) };
28
+ }
29
+ markEventReceiptProcessed(id, status) {
30
+ this.connection.prepare("UPDATE event_receipts SET processing_status = ? WHERE id = ?").run(status, id);
31
+ }
32
+ assignEventReceiptContext(id, params) {
33
+ this.connection
34
+ .prepare(`
35
+ UPDATE event_receipts
36
+ SET project_id = COALESCE(?, project_id),
37
+ linear_issue_id = COALESCE(?, linear_issue_id)
38
+ WHERE id = ?
39
+ `)
40
+ .run(params.projectId ?? null, params.linearIssueId ?? null, id);
41
+ }
42
+ getEventReceipt(id) {
43
+ const row = this.connection.prepare("SELECT * FROM event_receipts WHERE id = ?").get(id);
44
+ return row ? mapEventReceipt(row) : undefined;
45
+ }
46
+ getEventReceiptBySourceExternalId(source, externalId) {
47
+ const row = this.connection
48
+ .prepare("SELECT * FROM event_receipts WHERE source = ? AND external_id = ?")
49
+ .get(source, externalId);
50
+ return row ? mapEventReceipt(row) : undefined;
51
+ }
52
+ upsertIssueControl(params) {
53
+ const now = isoNow();
54
+ this.connection
55
+ .prepare(`
56
+ INSERT INTO issue_control (
57
+ project_id, linear_issue_id, desired_stage, desired_receipt_id, active_workspace_ownership_id,
58
+ active_run_lease_id, service_owned_comment_id, active_agent_session_id, lifecycle_status, updated_at
59
+ ) VALUES (
60
+ @projectId, @linearIssueId, @desiredStage, @desiredReceiptId, @activeWorkspaceOwnershipId,
61
+ @activeRunLeaseId, @serviceOwnedCommentId, @activeAgentSessionId, @lifecycleStatus, @updatedAt
62
+ )
63
+ ON CONFLICT(project_id, linear_issue_id) DO UPDATE SET
64
+ desired_stage = CASE WHEN @setDesiredStage = 1 THEN @desiredStage ELSE issue_control.desired_stage END,
65
+ desired_receipt_id = CASE WHEN @setDesiredReceiptId = 1 THEN @desiredReceiptId ELSE issue_control.desired_receipt_id END,
66
+ active_workspace_ownership_id = CASE WHEN @setActiveWorkspaceOwnershipId = 1 THEN @activeWorkspaceOwnershipId ELSE issue_control.active_workspace_ownership_id END,
67
+ active_run_lease_id = CASE WHEN @setActiveRunLeaseId = 1 THEN @activeRunLeaseId ELSE issue_control.active_run_lease_id END,
68
+ service_owned_comment_id = CASE WHEN @setServiceOwnedCommentId = 1 THEN @serviceOwnedCommentId ELSE issue_control.service_owned_comment_id END,
69
+ active_agent_session_id = CASE WHEN @setActiveAgentSessionId = 1 THEN @activeAgentSessionId ELSE issue_control.active_agent_session_id END,
70
+ lifecycle_status = @lifecycleStatus,
71
+ updated_at = @updatedAt
72
+ `)
73
+ .run({
74
+ projectId: params.projectId,
75
+ linearIssueId: params.linearIssueId,
76
+ desiredStage: params.desiredStage ?? null,
77
+ desiredReceiptId: params.desiredReceiptId ?? null,
78
+ activeWorkspaceOwnershipId: params.activeWorkspaceOwnershipId ?? null,
79
+ activeRunLeaseId: params.activeRunLeaseId ?? null,
80
+ serviceOwnedCommentId: params.serviceOwnedCommentId ?? null,
81
+ activeAgentSessionId: params.activeAgentSessionId ?? null,
82
+ lifecycleStatus: params.lifecycleStatus,
83
+ updatedAt: now,
84
+ setDesiredStage: Number("desiredStage" in params),
85
+ setDesiredReceiptId: Number("desiredReceiptId" in params),
86
+ setActiveWorkspaceOwnershipId: Number("activeWorkspaceOwnershipId" in params),
87
+ setActiveRunLeaseId: Number("activeRunLeaseId" in params),
88
+ setServiceOwnedCommentId: Number("serviceOwnedCommentId" in params),
89
+ setActiveAgentSessionId: Number("activeAgentSessionId" in params),
90
+ });
91
+ return this.getIssueControl(params.projectId, params.linearIssueId);
92
+ }
93
+ getIssueControl(projectId, linearIssueId) {
94
+ const row = this.connection
95
+ .prepare("SELECT * FROM issue_control WHERE project_id = ? AND linear_issue_id = ?")
96
+ .get(projectId, linearIssueId);
97
+ return row ? mapIssueControl(row) : undefined;
98
+ }
99
+ listIssueControlsReadyForLaunch() {
100
+ const rows = this.connection
101
+ .prepare("SELECT * FROM issue_control WHERE desired_stage IS NOT NULL AND active_run_lease_id IS NULL ORDER BY id")
102
+ .all();
103
+ return rows.map((row) => mapIssueControl(row));
104
+ }
105
+ upsertWorkspaceOwnership(params) {
106
+ const now = isoNow();
107
+ this.connection
108
+ .prepare(`
109
+ INSERT INTO workspace_ownership (
110
+ project_id, linear_issue_id, branch_name, worktree_path, status, current_run_lease_id, created_at, updated_at
111
+ ) VALUES (@projectId, @linearIssueId, @branchName, @worktreePath, @status, @currentRunLeaseId, @createdAt, @updatedAt)
112
+ ON CONFLICT(project_id, linear_issue_id) DO UPDATE SET
113
+ branch_name = @branchName,
114
+ worktree_path = @worktreePath,
115
+ status = @status,
116
+ current_run_lease_id = CASE WHEN @setCurrentRunLeaseId = 1 THEN @currentRunLeaseId ELSE workspace_ownership.current_run_lease_id END,
117
+ updated_at = @updatedAt
118
+ `)
119
+ .run({
120
+ projectId: params.projectId,
121
+ linearIssueId: params.linearIssueId,
122
+ branchName: params.branchName,
123
+ worktreePath: params.worktreePath,
124
+ status: params.status,
125
+ currentRunLeaseId: params.currentRunLeaseId ?? null,
126
+ createdAt: now,
127
+ updatedAt: now,
128
+ setCurrentRunLeaseId: Number("currentRunLeaseId" in params),
129
+ });
130
+ return this.getWorkspaceOwnershipForIssue(params.projectId, params.linearIssueId);
131
+ }
132
+ getWorkspaceOwnership(id) {
133
+ const row = this.connection
134
+ .prepare("SELECT * FROM workspace_ownership WHERE id = ?")
135
+ .get(id);
136
+ return row ? mapWorkspaceOwnership(row) : undefined;
137
+ }
138
+ getWorkspaceOwnershipForIssue(projectId, linearIssueId) {
139
+ const row = this.connection
140
+ .prepare("SELECT * FROM workspace_ownership WHERE project_id = ? AND linear_issue_id = ?")
141
+ .get(projectId, linearIssueId);
142
+ return row ? mapWorkspaceOwnership(row) : undefined;
143
+ }
144
+ createRunLease(params) {
145
+ const result = this.connection
146
+ .prepare(`
147
+ INSERT INTO run_leases (
148
+ issue_control_id, project_id, linear_issue_id, workspace_ownership_id, stage, status, trigger_receipt_id, workflow_file, prompt_text, started_at
149
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
150
+ `)
151
+ .run(params.issueControlId, params.projectId, params.linearIssueId, params.workspaceOwnershipId, params.stage, params.status ?? "queued", params.triggerReceiptId ?? null, params.workflowFile ?? "", params.promptText ?? "", isoNow());
152
+ return this.getRunLease(Number(result.lastInsertRowid));
153
+ }
154
+ getRunLease(id) {
155
+ const row = this.connection.prepare("SELECT * FROM run_leases WHERE id = ?").get(id);
156
+ return row ? mapRunLease(row) : undefined;
157
+ }
158
+ getRunLeaseByThreadId(threadId) {
159
+ const row = this.connection
160
+ .prepare("SELECT * FROM run_leases WHERE thread_id = ? ORDER BY id DESC LIMIT 1")
161
+ .get(threadId);
162
+ return row ? mapRunLease(row) : undefined;
163
+ }
164
+ listActiveRunLeases() {
165
+ const rows = this.connection
166
+ .prepare("SELECT * FROM run_leases WHERE status IN ('queued', 'running', 'paused') ORDER BY id")
167
+ .all();
168
+ return rows.map((row) => mapRunLease(row));
169
+ }
170
+ listRunLeasesForIssue(projectId, linearIssueId) {
171
+ const rows = this.connection
172
+ .prepare("SELECT * FROM run_leases WHERE project_id = ? AND linear_issue_id = ? ORDER BY id")
173
+ .all(projectId, linearIssueId);
174
+ return rows.map((row) => mapRunLease(row));
175
+ }
176
+ updateRunLeaseThread(params) {
177
+ this.connection
178
+ .prepare(`
179
+ UPDATE run_leases
180
+ SET thread_id = CASE WHEN @setThreadId = 1 THEN @threadId ELSE thread_id END,
181
+ parent_thread_id = CASE WHEN @setParentThreadId = 1 THEN @parentThreadId ELSE parent_thread_id END,
182
+ turn_id = CASE WHEN @setTurnId = 1 THEN @turnId ELSE turn_id END
183
+ WHERE id = @runLeaseId
184
+ `)
185
+ .run({
186
+ runLeaseId: params.runLeaseId,
187
+ threadId: params.threadId ?? null,
188
+ parentThreadId: params.parentThreadId ?? null,
189
+ turnId: params.turnId ?? null,
190
+ setThreadId: Number("threadId" in params),
191
+ setParentThreadId: Number("parentThreadId" in params),
192
+ setTurnId: Number("turnId" in params),
193
+ });
194
+ }
195
+ finishRunLease(params) {
196
+ const now = isoNow();
197
+ this.connection
198
+ .prepare(`
199
+ UPDATE run_leases
200
+ SET status = @status,
201
+ thread_id = CASE WHEN @setThreadId = 1 THEN @threadId ELSE thread_id END,
202
+ turn_id = CASE WHEN @setTurnId = 1 THEN @turnId ELSE turn_id END,
203
+ failure_reason = CASE WHEN @setFailureReason = 1 THEN @failureReason ELSE failure_reason END,
204
+ ended_at = CASE WHEN @status IN ('completed', 'failed', 'released') THEN @endedAt ELSE ended_at END
205
+ WHERE id = @runLeaseId
206
+ `)
207
+ .run({
208
+ runLeaseId: params.runLeaseId,
209
+ status: params.status,
210
+ threadId: params.threadId ?? null,
211
+ turnId: params.turnId ?? null,
212
+ failureReason: params.failureReason ?? null,
213
+ endedAt: now,
214
+ setThreadId: Number("threadId" in params),
215
+ setTurnId: Number("turnId" in params),
216
+ setFailureReason: Number("failureReason" in params),
217
+ });
218
+ }
219
+ enqueueObligation(params) {
220
+ const now = isoNow();
221
+ const result = this.connection
222
+ .prepare(`
223
+ INSERT OR IGNORE INTO obligations (
224
+ project_id, linear_issue_id, kind, status, source, payload_json, run_lease_id, thread_id, turn_id, dedupe_key, created_at, updated_at
225
+ ) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?)
226
+ `)
227
+ .run(params.projectId, params.linearIssueId, params.kind, params.source, params.payloadJson, params.runLeaseId ?? null, params.threadId ?? null, params.turnId ?? null, params.dedupeKey ?? null, now, now);
228
+ if (result.changes) {
229
+ return this.getObligation(Number(result.lastInsertRowid));
230
+ }
231
+ if (params.dedupeKey) {
232
+ const existing = params.runLeaseId === undefined || params.runLeaseId === null
233
+ ? undefined
234
+ : this.getObligationByDedupeKey({
235
+ runLeaseId: params.runLeaseId,
236
+ kind: params.kind,
237
+ dedupeKey: params.dedupeKey,
238
+ });
239
+ if (existing) {
240
+ return existing;
241
+ }
242
+ }
243
+ throw new Error(`Failed to persist obligation for ${params.projectId}:${params.linearIssueId}:${params.kind}`);
244
+ }
245
+ listPendingObligations(params) {
246
+ const statuses = params?.includeInProgress ? ["pending", "in_progress"] : ["pending"];
247
+ const clauses = [`status IN (${statuses.map(() => "?").join(", ")})`];
248
+ const values = [...statuses];
249
+ if (params?.runLeaseId !== undefined) {
250
+ clauses.push("run_lease_id = ?");
251
+ values.push(params.runLeaseId);
252
+ }
253
+ if (params?.kind) {
254
+ clauses.push("kind = ?");
255
+ values.push(params.kind);
256
+ }
257
+ const rows = this.connection
258
+ .prepare(`SELECT * FROM obligations WHERE ${clauses.join(" AND ")} ORDER BY id`)
259
+ .all(...values);
260
+ return rows.map((row) => mapObligation(row));
261
+ }
262
+ claimPendingObligation(id, params) {
263
+ const result = this.connection
264
+ .prepare(`
265
+ UPDATE obligations
266
+ SET status = 'in_progress',
267
+ run_lease_id = CASE WHEN @setRunLeaseId = 1 THEN @runLeaseId ELSE run_lease_id END,
268
+ thread_id = CASE WHEN @setThreadId = 1 THEN @threadId ELSE thread_id END,
269
+ turn_id = CASE WHEN @setTurnId = 1 THEN @turnId ELSE turn_id END,
270
+ last_error = NULL,
271
+ updated_at = @updatedAt
272
+ WHERE id = @id
273
+ AND status = 'pending'
274
+ `)
275
+ .run({
276
+ id,
277
+ runLeaseId: params?.runLeaseId ?? null,
278
+ threadId: params?.threadId ?? null,
279
+ turnId: params?.turnId ?? null,
280
+ updatedAt: isoNow(),
281
+ setRunLeaseId: Number("runLeaseId" in (params ?? {})),
282
+ setThreadId: Number("threadId" in (params ?? {})),
283
+ setTurnId: Number("turnId" in (params ?? {})),
284
+ });
285
+ return result.changes > 0;
286
+ }
287
+ updateObligationPayloadJson(id, payloadJson) {
288
+ this.connection
289
+ .prepare(`
290
+ UPDATE obligations
291
+ SET payload_json = ?,
292
+ updated_at = ?
293
+ WHERE id = ?
294
+ `)
295
+ .run(payloadJson, isoNow(), id);
296
+ }
297
+ updateObligationRouting(id, params) {
298
+ this.connection
299
+ .prepare(`
300
+ UPDATE obligations
301
+ SET run_lease_id = CASE WHEN @setRunLeaseId = 1 THEN @runLeaseId ELSE run_lease_id END,
302
+ thread_id = CASE WHEN @setThreadId = 1 THEN @threadId ELSE thread_id END,
303
+ turn_id = CASE WHEN @setTurnId = 1 THEN @turnId ELSE turn_id END,
304
+ updated_at = @updatedAt
305
+ WHERE id = @id
306
+ `)
307
+ .run({
308
+ id,
309
+ runLeaseId: params.runLeaseId ?? null,
310
+ threadId: params.threadId ?? null,
311
+ turnId: params.turnId ?? null,
312
+ updatedAt: isoNow(),
313
+ setRunLeaseId: Number("runLeaseId" in params),
314
+ setThreadId: Number("threadId" in params),
315
+ setTurnId: Number("turnId" in params),
316
+ });
317
+ }
318
+ markObligationStatus(id, status, lastError) {
319
+ const now = isoNow();
320
+ this.connection
321
+ .prepare(`
322
+ UPDATE obligations
323
+ SET status = @status,
324
+ last_error = CASE WHEN @setLastError = 1 THEN @lastError ELSE last_error END,
325
+ updated_at = @updatedAt,
326
+ completed_at = CASE WHEN @status = 'completed' THEN @completedAt ELSE completed_at END
327
+ WHERE id = @id
328
+ `)
329
+ .run({
330
+ id,
331
+ status,
332
+ lastError: lastError ?? null,
333
+ updatedAt: now,
334
+ completedAt: now,
335
+ setLastError: Number(lastError !== undefined),
336
+ });
337
+ }
338
+ getObligation(id) {
339
+ const row = this.connection.prepare("SELECT * FROM obligations WHERE id = ?").get(id);
340
+ return row ? mapObligation(row) : undefined;
341
+ }
342
+ getObligationByDedupeKey(params) {
343
+ const row = this.connection
344
+ .prepare(`
345
+ SELECT * FROM obligations
346
+ WHERE run_lease_id IS ?
347
+ AND kind = ?
348
+ AND dedupe_key = ?
349
+ ORDER BY id DESC
350
+ LIMIT 1
351
+ `)
352
+ .get(params.runLeaseId, params.kind, params.dedupeKey);
353
+ return row ? mapObligation(row) : undefined;
354
+ }
355
+ }
356
+ function mapEventReceipt(row) {
357
+ return {
358
+ id: Number(row.id),
359
+ source: String(row.source),
360
+ externalId: String(row.external_id),
361
+ eventType: String(row.event_type),
362
+ receivedAt: String(row.received_at),
363
+ acceptanceStatus: row.acceptance_status,
364
+ processingStatus: row.processing_status,
365
+ ...(row.project_id === null ? {} : { projectId: String(row.project_id) }),
366
+ ...(row.linear_issue_id === null ? {} : { linearIssueId: String(row.linear_issue_id) }),
367
+ ...(row.headers_json === null ? {} : { headersJson: String(row.headers_json) }),
368
+ ...(row.payload_json === null ? {} : { payloadJson: String(row.payload_json) }),
369
+ };
370
+ }
371
+ function mapIssueControl(row) {
372
+ return {
373
+ id: Number(row.id),
374
+ projectId: String(row.project_id),
375
+ linearIssueId: String(row.linear_issue_id),
376
+ ...(row.desired_stage === null ? {} : { desiredStage: row.desired_stage }),
377
+ ...(row.desired_receipt_id === null ? {} : { desiredReceiptId: Number(row.desired_receipt_id) }),
378
+ ...(row.active_run_lease_id === null ? {} : { activeRunLeaseId: Number(row.active_run_lease_id) }),
379
+ ...(row.active_workspace_ownership_id === null ? {} : { activeWorkspaceOwnershipId: Number(row.active_workspace_ownership_id) }),
380
+ ...(row.service_owned_comment_id === null ? {} : { serviceOwnedCommentId: String(row.service_owned_comment_id) }),
381
+ ...(row.active_agent_session_id === null ? {} : { activeAgentSessionId: String(row.active_agent_session_id) }),
382
+ lifecycleStatus: row.lifecycle_status,
383
+ updatedAt: String(row.updated_at),
384
+ };
385
+ }
386
+ function mapWorkspaceOwnership(row) {
387
+ return {
388
+ id: Number(row.id),
389
+ projectId: String(row.project_id),
390
+ linearIssueId: String(row.linear_issue_id),
391
+ branchName: String(row.branch_name),
392
+ worktreePath: String(row.worktree_path),
393
+ status: row.status,
394
+ ...(row.current_run_lease_id === null ? {} : { currentRunLeaseId: Number(row.current_run_lease_id) }),
395
+ createdAt: String(row.created_at),
396
+ updatedAt: String(row.updated_at),
397
+ };
398
+ }
399
+ function mapRunLease(row) {
400
+ return {
401
+ id: Number(row.id),
402
+ issueControlId: Number(row.issue_control_id),
403
+ projectId: String(row.project_id),
404
+ linearIssueId: String(row.linear_issue_id),
405
+ workspaceOwnershipId: Number(row.workspace_ownership_id),
406
+ stage: row.stage,
407
+ status: row.status,
408
+ ...(row.trigger_receipt_id === null ? {} : { triggerReceiptId: Number(row.trigger_receipt_id) }),
409
+ workflowFile: String(row.workflow_file ?? ""),
410
+ promptText: String(row.prompt_text ?? ""),
411
+ ...(row.thread_id === null ? {} : { threadId: String(row.thread_id) }),
412
+ ...(row.parent_thread_id === null ? {} : { parentThreadId: String(row.parent_thread_id) }),
413
+ ...(row.turn_id === null ? {} : { turnId: String(row.turn_id) }),
414
+ startedAt: String(row.started_at),
415
+ ...(row.ended_at === null ? {} : { endedAt: String(row.ended_at) }),
416
+ ...(row.failure_reason === null ? {} : { failureReason: String(row.failure_reason) }),
417
+ };
418
+ }
419
+ function mapObligation(row) {
420
+ return {
421
+ id: Number(row.id),
422
+ projectId: String(row.project_id),
423
+ linearIssueId: String(row.linear_issue_id),
424
+ kind: String(row.kind),
425
+ status: row.status,
426
+ source: String(row.source),
427
+ payloadJson: String(row.payload_json),
428
+ ...(row.run_lease_id === null ? {} : { runLeaseId: Number(row.run_lease_id) }),
429
+ ...(row.thread_id === null ? {} : { threadId: String(row.thread_id) }),
430
+ ...(row.turn_id === null ? {} : { turnId: String(row.turn_id) }),
431
+ ...(row.dedupe_key === null ? {} : { dedupeKey: String(row.dedupe_key) }),
432
+ ...(row.last_error === null ? {} : { lastError: String(row.last_error) }),
433
+ createdAt: String(row.created_at),
434
+ updatedAt: String(row.updated_at),
435
+ ...(row.completed_at === null ? {} : { completedAt: String(row.completed_at) }),
436
+ };
437
+ }