patchrelay 0.8.9 → 0.9.1

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 (60) hide show
  1. package/README.md +64 -62
  2. package/dist/agent-session-plan.js +17 -17
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli/args.js +1 -1
  5. package/dist/cli/commands/issues.js +18 -18
  6. package/dist/cli/data.js +109 -298
  7. package/dist/cli/formatters/text.js +22 -28
  8. package/dist/cli/help.js +7 -7
  9. package/dist/cli/index.js +3 -3
  10. package/dist/config.js +13 -166
  11. package/dist/db/migrations.js +46 -154
  12. package/dist/db.js +369 -45
  13. package/dist/factory-state.js +55 -0
  14. package/dist/github-webhook-handler.js +199 -0
  15. package/dist/github-webhooks.js +166 -0
  16. package/dist/hook-runner.js +28 -0
  17. package/dist/http.js +48 -22
  18. package/dist/issue-query-service.js +33 -38
  19. package/dist/linear-workflow.js +5 -118
  20. package/dist/preflight.js +1 -6
  21. package/dist/project-resolution.js +12 -1
  22. package/dist/run-orchestrator.js +446 -0
  23. package/dist/{stage-reporting.js → run-reporting.js} +11 -13
  24. package/dist/service-runtime.js +12 -61
  25. package/dist/service-webhooks.js +7 -52
  26. package/dist/service.js +39 -61
  27. package/dist/webhook-handler.js +387 -0
  28. package/dist/webhook-installation-handler.js +3 -8
  29. package/package.json +2 -1
  30. package/dist/db/authoritative-ledger-store.js +0 -536
  31. package/dist/db/issue-projection-store.js +0 -54
  32. package/dist/db/issue-workflow-coordinator.js +0 -320
  33. package/dist/db/issue-workflow-store.js +0 -194
  34. package/dist/db/run-report-store.js +0 -33
  35. package/dist/db/stage-event-store.js +0 -33
  36. package/dist/db/webhook-event-store.js +0 -59
  37. package/dist/db-ports.js +0 -5
  38. package/dist/ledger-ports.js +0 -1
  39. package/dist/reconciliation-action-applier.js +0 -68
  40. package/dist/reconciliation-actions.js +0 -1
  41. package/dist/reconciliation-engine.js +0 -350
  42. package/dist/reconciliation-snapshot-builder.js +0 -135
  43. package/dist/reconciliation-types.js +0 -1
  44. package/dist/service-stage-finalizer.js +0 -753
  45. package/dist/service-stage-runner.js +0 -336
  46. package/dist/service-webhook-processor.js +0 -411
  47. package/dist/stage-agent-activity-publisher.js +0 -59
  48. package/dist/stage-event-ports.js +0 -1
  49. package/dist/stage-failure.js +0 -92
  50. package/dist/stage-handoff.js +0 -107
  51. package/dist/stage-launch.js +0 -84
  52. package/dist/stage-lifecycle-publisher.js +0 -284
  53. package/dist/stage-turn-input-dispatcher.js +0 -104
  54. package/dist/webhook-agent-session-handler.js +0 -228
  55. package/dist/webhook-comment-handler.js +0 -141
  56. package/dist/webhook-desired-stage-recorder.js +0 -122
  57. package/dist/webhook-event-ports.js +0 -1
  58. package/dist/workflow-policy.js +0 -149
  59. package/dist/workflow-ports.js +0 -1
  60. /package/dist/{installation-ports.js → github-types.js} +0 -0
package/dist/db.js CHANGED
@@ -1,29 +1,9 @@
1
- import { AuthoritativeLedgerStore } from "./db/authoritative-ledger-store.js";
2
- import { IssueProjectionStore } from "./db/issue-projection-store.js";
3
- import { IssueWorkflowCoordinator } from "./db/issue-workflow-coordinator.js";
4
- import { IssueWorkflowStore } from "./db/issue-workflow-store.js";
5
1
  import { LinearInstallationStore } from "./db/linear-installation-store.js";
6
- import { runPatchRelayMigrations } from "./db/migrations.js";
7
2
  import { OperatorFeedStore } from "./db/operator-feed-store.js";
8
- import { RunReportStore } from "./db/run-report-store.js";
9
- import { StageEventStore } from "./db/stage-event-store.js";
10
- import { SqliteConnection } from "./db/shared.js";
11
- import { WebhookEventStore } from "./db/webhook-event-store.js";
3
+ import { runPatchRelayMigrations } from "./db/migrations.js";
4
+ import { SqliteConnection, isoNow } from "./db/shared.js";
12
5
  export class PatchRelayDatabase {
13
6
  connection;
14
- authoritativeLedger;
15
- eventReceipts;
16
- issueControl;
17
- workspaceOwnership;
18
- issueSessions;
19
- runLeases;
20
- obligations;
21
- webhookEvents;
22
- issueProjections;
23
- issueWorkflows;
24
- workflowCoordinator;
25
- runReports;
26
- stageEvents;
27
7
  linearInstallations;
28
8
  operatorFeed;
29
9
  constructor(databasePath, wal) {
@@ -32,33 +12,377 @@ export class PatchRelayDatabase {
32
12
  if (wal) {
33
13
  this.connection.pragma("journal_mode = WAL");
34
14
  }
35
- this.authoritativeLedger = new AuthoritativeLedgerStore(this.connection);
36
- this.eventReceipts = this.authoritativeLedger;
37
- this.issueControl = this.authoritativeLedger;
38
- this.workspaceOwnership = this.authoritativeLedger;
39
- this.issueSessions = this.authoritativeLedger;
40
- this.runLeases = this.authoritativeLedger;
41
- this.obligations = this.authoritativeLedger;
42
- this.webhookEvents = new WebhookEventStore(this.connection);
43
- this.issueProjections = new IssueProjectionStore(this.connection);
44
- this.runReports = new RunReportStore(this.connection);
45
- this.issueWorkflows = new IssueWorkflowStore({
46
- authoritativeLedger: this.authoritativeLedger,
47
- issueProjections: this.issueProjections,
48
- runReports: this.runReports,
49
- });
50
- this.workflowCoordinator = new IssueWorkflowCoordinator({
51
- connection: this.connection,
52
- authoritativeLedger: this.authoritativeLedger,
53
- issueProjections: this.issueProjections,
54
- issueWorkflows: this.issueWorkflows,
55
- runReports: this.runReports,
56
- });
57
- this.stageEvents = new StageEventStore(this.connection);
58
15
  this.linearInstallations = new LinearInstallationStore(this.connection);
59
16
  this.operatorFeed = new OperatorFeedStore(this.connection);
60
17
  }
61
18
  runMigrations() {
62
19
  runPatchRelayMigrations(this.connection);
63
20
  }
21
+ transaction(fn) {
22
+ return this.connection.transaction(fn)();
23
+ }
24
+ // ─── Webhook Events ───────────────────────────────────────────────
25
+ insertWebhookEvent(webhookId, receivedAt) {
26
+ const existing = this.connection
27
+ .prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
28
+ .get(webhookId);
29
+ if (existing) {
30
+ return { id: existing.id, duplicate: true };
31
+ }
32
+ const result = this.connection
33
+ .prepare("INSERT INTO webhook_events (webhook_id, received_at) VALUES (?, ?)")
34
+ .run(webhookId, receivedAt);
35
+ return { id: Number(result.lastInsertRowid), duplicate: false };
36
+ }
37
+ insertFullWebhookEvent(params) {
38
+ const existing = this.connection
39
+ .prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
40
+ .get(params.webhookId);
41
+ if (existing) {
42
+ return { id: existing.id, dedupeStatus: "duplicate" };
43
+ }
44
+ const result = this.connection
45
+ .prepare("INSERT INTO webhook_events (webhook_id, received_at, payload_json) VALUES (?, ?, ?)")
46
+ .run(params.webhookId, params.receivedAt, params.payloadJson);
47
+ return { id: Number(result.lastInsertRowid), dedupeStatus: "accepted" };
48
+ }
49
+ getWebhookPayload(id) {
50
+ const row = this.connection.prepare("SELECT webhook_id, payload_json FROM webhook_events WHERE id = ?").get(id);
51
+ if (!row || !row.payload_json)
52
+ return undefined;
53
+ return { webhookId: String(row.webhook_id), payloadJson: String(row.payload_json) };
54
+ }
55
+ isWebhookDuplicate(webhookId) {
56
+ return this.connection.prepare("SELECT 1 FROM webhook_events WHERE webhook_id = ?").get(webhookId) !== undefined;
57
+ }
58
+ markWebhookProcessed(id, status) {
59
+ this.connection.prepare("UPDATE webhook_events SET processing_status = ? WHERE id = ?").run(status, id);
60
+ }
61
+ assignWebhookProject(id, projectId) {
62
+ this.connection.prepare("UPDATE webhook_events SET project_id = ? WHERE id = ?").run(projectId, id);
63
+ }
64
+ // ─── Issues ───────────────────────────────────────────────────────
65
+ upsertIssue(params) {
66
+ const now = isoNow();
67
+ const existing = this.getIssue(params.projectId, params.linearIssueId);
68
+ if (existing) {
69
+ // Build dynamic SET clauses for nullable fields
70
+ const sets = ["updated_at = @now"];
71
+ const values = {
72
+ now,
73
+ projectId: params.projectId,
74
+ linearIssueId: params.linearIssueId,
75
+ };
76
+ if (params.issueKey !== undefined) {
77
+ sets.push("issue_key = COALESCE(@issueKey, issue_key)");
78
+ values.issueKey = params.issueKey;
79
+ }
80
+ if (params.title !== undefined) {
81
+ sets.push("title = COALESCE(@title, title)");
82
+ values.title = params.title;
83
+ }
84
+ if (params.url !== undefined) {
85
+ sets.push("url = COALESCE(@url, url)");
86
+ values.url = params.url;
87
+ }
88
+ if (params.currentLinearState !== undefined) {
89
+ sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
90
+ values.currentLinearState = params.currentLinearState;
91
+ }
92
+ if (params.factoryState !== undefined) {
93
+ sets.push("factory_state = @factoryState");
94
+ values.factoryState = params.factoryState;
95
+ }
96
+ if (params.pendingRunType !== undefined) {
97
+ sets.push("pending_run_type = @pendingRunType");
98
+ values.pendingRunType = params.pendingRunType;
99
+ }
100
+ if (params.pendingRunContextJson !== undefined) {
101
+ sets.push("pending_run_context_json = @pendingRunContextJson");
102
+ values.pendingRunContextJson = params.pendingRunContextJson;
103
+ }
104
+ if (params.branchName !== undefined) {
105
+ sets.push("branch_name = COALESCE(@branchName, branch_name)");
106
+ values.branchName = params.branchName;
107
+ }
108
+ if (params.worktreePath !== undefined) {
109
+ sets.push("worktree_path = COALESCE(@worktreePath, worktree_path)");
110
+ values.worktreePath = params.worktreePath;
111
+ }
112
+ if (params.threadId !== undefined) {
113
+ sets.push("thread_id = @threadId");
114
+ values.threadId = params.threadId;
115
+ }
116
+ if (params.activeRunId !== undefined) {
117
+ sets.push("active_run_id = @activeRunId");
118
+ values.activeRunId = params.activeRunId;
119
+ }
120
+ if (params.agentSessionId !== undefined) {
121
+ sets.push("agent_session_id = @agentSessionId");
122
+ values.agentSessionId = params.agentSessionId;
123
+ }
124
+ if (params.prNumber !== undefined) {
125
+ sets.push("pr_number = @prNumber");
126
+ values.prNumber = params.prNumber;
127
+ }
128
+ if (params.prUrl !== undefined) {
129
+ sets.push("pr_url = @prUrl");
130
+ values.prUrl = params.prUrl;
131
+ }
132
+ if (params.prState !== undefined) {
133
+ sets.push("pr_state = @prState");
134
+ values.prState = params.prState;
135
+ }
136
+ if (params.prReviewState !== undefined) {
137
+ sets.push("pr_review_state = @prReviewState");
138
+ values.prReviewState = params.prReviewState;
139
+ }
140
+ if (params.prCheckStatus !== undefined) {
141
+ sets.push("pr_check_status = @prCheckStatus");
142
+ values.prCheckStatus = params.prCheckStatus;
143
+ }
144
+ if (params.ciRepairAttempts !== undefined) {
145
+ sets.push("ci_repair_attempts = @ciRepairAttempts");
146
+ values.ciRepairAttempts = params.ciRepairAttempts;
147
+ }
148
+ if (params.queueRepairAttempts !== undefined) {
149
+ sets.push("queue_repair_attempts = @queueRepairAttempts");
150
+ values.queueRepairAttempts = params.queueRepairAttempts;
151
+ }
152
+ this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
153
+ }
154
+ else {
155
+ this.connection.prepare(`
156
+ INSERT INTO issues (
157
+ project_id, linear_issue_id, issue_key, title, url,
158
+ current_linear_state, factory_state, pending_run_type, pending_run_context_json,
159
+ branch_name, worktree_path, thread_id, active_run_id,
160
+ agent_session_id,
161
+ updated_at
162
+ ) VALUES (
163
+ @projectId, @linearIssueId, @issueKey, @title, @url,
164
+ @currentLinearState, @factoryState, @pendingRunType, @pendingRunContextJson,
165
+ @branchName, @worktreePath, @threadId, @activeRunId,
166
+ @agentSessionId,
167
+ @now
168
+ )
169
+ `).run({
170
+ projectId: params.projectId,
171
+ linearIssueId: params.linearIssueId,
172
+ issueKey: params.issueKey ?? null,
173
+ title: params.title ?? null,
174
+ url: params.url ?? null,
175
+ currentLinearState: params.currentLinearState ?? null,
176
+ factoryState: params.factoryState ?? "delegated",
177
+ pendingRunType: params.pendingRunType ?? null,
178
+ pendingRunContextJson: params.pendingRunContextJson ?? null,
179
+ branchName: params.branchName ?? null,
180
+ worktreePath: params.worktreePath ?? null,
181
+ threadId: params.threadId ?? null,
182
+ activeRunId: params.activeRunId ?? null,
183
+ agentSessionId: params.agentSessionId ?? null,
184
+ now,
185
+ });
186
+ }
187
+ return this.getIssue(params.projectId, params.linearIssueId);
188
+ }
189
+ getIssue(projectId, linearIssueId) {
190
+ const row = this.connection
191
+ .prepare("SELECT * FROM issues WHERE project_id = ? AND linear_issue_id = ?")
192
+ .get(projectId, linearIssueId);
193
+ return row ? mapIssueRow(row) : undefined;
194
+ }
195
+ getIssueById(id) {
196
+ const row = this.connection.prepare("SELECT * FROM issues WHERE id = ?").get(id);
197
+ return row ? mapIssueRow(row) : undefined;
198
+ }
199
+ getIssueByKey(issueKey) {
200
+ const row = this.connection.prepare("SELECT * FROM issues WHERE issue_key = ?").get(issueKey);
201
+ return row ? mapIssueRow(row) : undefined;
202
+ }
203
+ getIssueByBranch(branchName) {
204
+ const row = this.connection.prepare("SELECT * FROM issues WHERE branch_name = ?").get(branchName);
205
+ return row ? mapIssueRow(row) : undefined;
206
+ }
207
+ listIssuesReadyForExecution() {
208
+ const rows = this.connection
209
+ .prepare("SELECT project_id, linear_issue_id FROM issues WHERE pending_run_type IS NOT NULL AND active_run_id IS NULL")
210
+ .all();
211
+ return rows.map((row) => ({
212
+ projectId: String(row.project_id),
213
+ linearIssueId: String(row.linear_issue_id),
214
+ }));
215
+ }
216
+ // ─── Runs ─────────────────────────────────────────────────────────
217
+ createRun(params) {
218
+ const now = isoNow();
219
+ const result = this.connection.prepare(`
220
+ INSERT INTO runs (issue_id, project_id, linear_issue_id, run_type, status, prompt_text, started_at)
221
+ VALUES (?, ?, ?, ?, 'queued', ?, ?)
222
+ `).run(params.issueId, params.projectId, params.linearIssueId, params.runType, params.promptText ?? null, now);
223
+ return this.getRun(Number(result.lastInsertRowid));
224
+ }
225
+ getRun(id) {
226
+ const row = this.connection.prepare("SELECT * FROM runs WHERE id = ?").get(id);
227
+ return row ? mapRunRow(row) : undefined;
228
+ }
229
+ getRunByThreadId(threadId) {
230
+ const row = this.connection.prepare("SELECT * FROM runs WHERE thread_id = ?").get(threadId);
231
+ return row ? mapRunRow(row) : undefined;
232
+ }
233
+ listRunsForIssue(projectId, linearIssueId) {
234
+ const rows = this.connection
235
+ .prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id")
236
+ .all(projectId, linearIssueId);
237
+ return rows.map(mapRunRow);
238
+ }
239
+ getLatestRunForIssue(projectId, linearIssueId) {
240
+ const row = this.connection
241
+ .prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id DESC LIMIT 1")
242
+ .get(projectId, linearIssueId);
243
+ return row ? mapRunRow(row) : undefined;
244
+ }
245
+ listActiveRuns() {
246
+ const rows = this.connection
247
+ .prepare("SELECT * FROM runs WHERE status IN ('queued', 'running')")
248
+ .all();
249
+ return rows.map(mapRunRow);
250
+ }
251
+ listRunningRuns() {
252
+ const rows = this.connection
253
+ .prepare("SELECT * FROM runs WHERE status = 'running'")
254
+ .all();
255
+ return rows.map(mapRunRow);
256
+ }
257
+ updateRunThread(runId, params) {
258
+ this.connection.prepare(`
259
+ UPDATE runs SET
260
+ thread_id = ?,
261
+ parent_thread_id = COALESCE(?, parent_thread_id),
262
+ turn_id = COALESCE(?, turn_id),
263
+ status = 'running'
264
+ WHERE id = ?
265
+ `).run(params.threadId, params.parentThreadId ?? null, params.turnId ?? null, runId);
266
+ }
267
+ updateRunTurnId(runId, turnId) {
268
+ this.connection.prepare("UPDATE runs SET turn_id = ? WHERE id = ?").run(turnId, runId);
269
+ }
270
+ finishRun(runId, params) {
271
+ const now = isoNow();
272
+ this.connection.prepare(`
273
+ UPDATE runs SET
274
+ status = ?,
275
+ thread_id = COALESCE(?, thread_id),
276
+ turn_id = COALESCE(?, turn_id),
277
+ failure_reason = COALESCE(?, failure_reason),
278
+ summary_json = COALESCE(?, summary_json),
279
+ report_json = COALESCE(?, report_json),
280
+ ended_at = ?
281
+ WHERE id = ?
282
+ `).run(params.status, params.threadId ?? null, params.turnId ?? null, params.failureReason ?? null, params.summaryJson ?? null, params.reportJson ?? null, now, runId);
283
+ }
284
+ // ─── Thread Events (kept for extended history) ────────────────────
285
+ saveThreadEvent(params) {
286
+ this.connection.prepare(`
287
+ INSERT INTO run_thread_events (run_id, thread_id, turn_id, method, event_json, created_at)
288
+ VALUES (?, ?, ?, ?, ?, ?)
289
+ `).run(params.runId, params.threadId, params.turnId ?? null, params.method, params.eventJson, isoNow());
290
+ }
291
+ listThreadEvents(runId) {
292
+ const rows = this.connection
293
+ .prepare("SELECT * FROM run_thread_events WHERE run_id = ? ORDER BY id")
294
+ .all(runId);
295
+ return rows.map((row) => ({
296
+ id: Number(row.id),
297
+ runId: Number(row.run_id),
298
+ threadId: String(row.thread_id),
299
+ ...(row.turn_id !== null ? { turnId: String(row.turn_id) } : {}),
300
+ method: String(row.method),
301
+ eventJson: String(row.event_json),
302
+ createdAt: String(row.created_at),
303
+ }));
304
+ }
305
+ // ─── View builders ──────────────────────────────────────────────
306
+ issueToTrackedIssue(issue) {
307
+ return {
308
+ id: issue.id,
309
+ projectId: issue.projectId,
310
+ linearIssueId: issue.linearIssueId,
311
+ ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
312
+ ...(issue.title ? { title: issue.title } : {}),
313
+ ...(issue.url ? { issueUrl: issue.url } : {}),
314
+ ...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
315
+ factoryState: issue.factoryState,
316
+ ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
317
+ ...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
318
+ updatedAt: issue.updatedAt,
319
+ };
320
+ }
321
+ getTrackedIssue(projectId, linearIssueId) {
322
+ const issue = this.getIssue(projectId, linearIssueId);
323
+ return issue ? this.issueToTrackedIssue(issue) : undefined;
324
+ }
325
+ getTrackedIssueByKey(issueKey) {
326
+ const issue = this.getIssueByKey(issueKey);
327
+ return issue ? this.issueToTrackedIssue(issue) : undefined;
328
+ }
329
+ // ─── Issue overview for query service ─────────────────────────────
330
+ getIssueOverview(issueKey) {
331
+ const issue = this.getIssueByKey(issueKey);
332
+ if (!issue)
333
+ return undefined;
334
+ const tracked = this.issueToTrackedIssue(issue);
335
+ const activeRun = issue.activeRunId ? this.getRun(issue.activeRunId) : undefined;
336
+ return {
337
+ issue: tracked,
338
+ ...(activeRun ? { activeRun } : {}),
339
+ };
340
+ }
341
+ }
342
+ // ─── Row mappers ──────────────────────────────────────────────────
343
+ function mapIssueRow(row) {
344
+ return {
345
+ id: Number(row.id),
346
+ projectId: String(row.project_id),
347
+ linearIssueId: String(row.linear_issue_id),
348
+ ...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
349
+ ...(row.title !== null ? { title: String(row.title) } : {}),
350
+ ...(row.url !== null ? { url: String(row.url) } : {}),
351
+ ...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
352
+ factoryState: String(row.factory_state ?? "delegated"),
353
+ ...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
354
+ ...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
355
+ ...(row.branch_name !== null ? { branchName: String(row.branch_name) } : {}),
356
+ ...(row.worktree_path !== null ? { worktreePath: String(row.worktree_path) } : {}),
357
+ ...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
358
+ ...(row.active_run_id !== null ? { activeRunId: Number(row.active_run_id) } : {}),
359
+ ...(row.agent_session_id !== null ? { agentSessionId: String(row.agent_session_id) } : {}),
360
+ updatedAt: String(row.updated_at),
361
+ ...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
362
+ ...(row.pr_url !== null && row.pr_url !== undefined ? { prUrl: String(row.pr_url) } : {}),
363
+ ...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
364
+ ...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
365
+ ...(row.pr_check_status !== null && row.pr_check_status !== undefined ? { prCheckStatus: String(row.pr_check_status) } : {}),
366
+ ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
367
+ queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
368
+ };
369
+ }
370
+ function mapRunRow(row) {
371
+ return {
372
+ id: Number(row.id),
373
+ issueId: Number(row.issue_id),
374
+ projectId: String(row.project_id),
375
+ linearIssueId: String(row.linear_issue_id),
376
+ runType: String(row.run_type ?? "implementation"),
377
+ status: String(row.status),
378
+ ...(row.prompt_text !== null ? { promptText: String(row.prompt_text) } : {}),
379
+ ...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
380
+ ...(row.turn_id !== null ? { turnId: String(row.turn_id) } : {}),
381
+ ...(row.parent_thread_id !== null ? { parentThreadId: String(row.parent_thread_id) } : {}),
382
+ ...(row.summary_json !== null ? { summaryJson: String(row.summary_json) } : {}),
383
+ ...(row.report_json !== null ? { reportJson: String(row.report_json) } : {}),
384
+ ...(row.failure_reason !== null ? { failureReason: String(row.failure_reason) } : {}),
385
+ startedAt: String(row.started_at),
386
+ ...(row.ended_at !== null ? { endedAt: String(row.ended_at) } : {}),
387
+ };
64
388
  }
@@ -0,0 +1,55 @@
1
+ /** Which factory states involve an active Codex run. */
2
+ export const ACTIVE_RUN_STATES = new Set([
3
+ "implementing",
4
+ "repairing_ci",
5
+ "changes_requested",
6
+ "repairing_queue",
7
+ ]);
8
+ /** Which factory states are terminal (no further transitions possible). */
9
+ export const TERMINAL_STATES = new Set([
10
+ "done",
11
+ "escalated",
12
+ ]);
13
+ export const ALLOWED_TRANSITIONS = {
14
+ delegated: ["preparing", "failed"],
15
+ preparing: ["implementing", "failed"],
16
+ implementing: ["pr_open", "awaiting_input", "failed", "escalated"],
17
+ pr_open: ["awaiting_review", "repairing_ci", "failed"],
18
+ awaiting_review: ["changes_requested", "awaiting_queue", "repairing_ci"],
19
+ changes_requested: ["implementing", "awaiting_input", "escalated"],
20
+ repairing_ci: ["pr_open", "awaiting_review", "escalated", "failed"],
21
+ awaiting_queue: ["done", "repairing_queue", "repairing_ci"],
22
+ repairing_queue: ["pr_open", "awaiting_review", "escalated", "failed"],
23
+ awaiting_input: ["implementing", "delegated", "escalated"],
24
+ escalated: [],
25
+ done: [],
26
+ failed: ["delegated"],
27
+ };
28
+ export function resolveFactoryStateFromGitHub(triggerEvent, current) {
29
+ switch (triggerEvent) {
30
+ case "pr_opened":
31
+ return current === "implementing" ? "pr_open" : undefined;
32
+ case "pr_synchronize":
33
+ return undefined; // just resets repair counters, no state change
34
+ case "review_approved":
35
+ return current === "awaiting_review" || current === "pr_open" ? "awaiting_queue" : undefined;
36
+ case "review_changes_requested":
37
+ return current === "awaiting_review" || current === "pr_open" ? "changes_requested" : undefined;
38
+ case "review_commented":
39
+ return undefined; // informational only
40
+ case "check_passed":
41
+ return current === "repairing_ci" ? "pr_open" : undefined;
42
+ case "check_failed":
43
+ return current === "pr_open" || current === "awaiting_review" ? "repairing_ci" : undefined;
44
+ case "pr_merged":
45
+ return "done";
46
+ case "pr_closed":
47
+ return "failed";
48
+ case "merge_group_passed":
49
+ return undefined; // merge event will follow
50
+ case "merge_group_failed":
51
+ return current === "awaiting_queue" ? "repairing_queue" : undefined;
52
+ default:
53
+ return undefined;
54
+ }
55
+ }