patchrelay 0.8.8 → 0.9.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 (57) 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/commands/issues.js +12 -12
  5. package/dist/cli/data.js +109 -298
  6. package/dist/cli/formatters/text.js +22 -28
  7. package/dist/config.js +13 -166
  8. package/dist/db/migrations.js +46 -154
  9. package/dist/db.js +369 -45
  10. package/dist/factory-state.js +55 -0
  11. package/dist/github-webhook-handler.js +199 -0
  12. package/dist/github-webhooks.js +166 -0
  13. package/dist/hook-runner.js +28 -0
  14. package/dist/http.js +48 -22
  15. package/dist/issue-query-service.js +33 -38
  16. package/dist/linear-workflow.js +5 -118
  17. package/dist/preflight.js +1 -6
  18. package/dist/project-resolution.js +12 -1
  19. package/dist/run-orchestrator.js +446 -0
  20. package/dist/{stage-reporting.js → run-reporting.js} +11 -13
  21. package/dist/service-runtime.js +21 -54
  22. package/dist/service-webhooks.js +7 -52
  23. package/dist/service.js +39 -61
  24. package/dist/webhook-handler.js +387 -0
  25. package/dist/webhook-installation-handler.js +3 -8
  26. package/package.json +2 -1
  27. package/dist/db/authoritative-ledger-store.js +0 -536
  28. package/dist/db/issue-projection-store.js +0 -54
  29. package/dist/db/issue-workflow-coordinator.js +0 -320
  30. package/dist/db/issue-workflow-store.js +0 -194
  31. package/dist/db/run-report-store.js +0 -33
  32. package/dist/db/stage-event-store.js +0 -33
  33. package/dist/db/webhook-event-store.js +0 -59
  34. package/dist/db-ports.js +0 -5
  35. package/dist/ledger-ports.js +0 -1
  36. package/dist/reconciliation-action-applier.js +0 -68
  37. package/dist/reconciliation-actions.js +0 -1
  38. package/dist/reconciliation-engine.js +0 -350
  39. package/dist/reconciliation-snapshot-builder.js +0 -135
  40. package/dist/reconciliation-types.js +0 -1
  41. package/dist/service-stage-finalizer.js +0 -753
  42. package/dist/service-stage-runner.js +0 -336
  43. package/dist/service-webhook-processor.js +0 -411
  44. package/dist/stage-agent-activity-publisher.js +0 -59
  45. package/dist/stage-event-ports.js +0 -1
  46. package/dist/stage-failure.js +0 -92
  47. package/dist/stage-handoff.js +0 -107
  48. package/dist/stage-launch.js +0 -84
  49. package/dist/stage-lifecycle-publisher.js +0 -284
  50. package/dist/stage-turn-input-dispatcher.js +0 -104
  51. package/dist/webhook-agent-session-handler.js +0 -228
  52. package/dist/webhook-comment-handler.js +0 -141
  53. package/dist/webhook-desired-stage-recorder.js +0 -122
  54. package/dist/webhook-event-ports.js +0 -1
  55. package/dist/workflow-policy.js +0 -149
  56. package/dist/workflow-ports.js +0 -1
  57. /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
+ }