patchrelay 0.8.9 → 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.
- package/README.md +64 -62
- package/dist/agent-session-plan.js +17 -17
- package/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +12 -12
- package/dist/cli/data.js +109 -298
- package/dist/cli/formatters/text.js +22 -28
- package/dist/config.js +13 -166
- package/dist/db/migrations.js +46 -154
- package/dist/db.js +369 -45
- package/dist/factory-state.js +55 -0
- package/dist/github-webhook-handler.js +199 -0
- package/dist/github-webhooks.js +166 -0
- package/dist/hook-runner.js +28 -0
- package/dist/http.js +48 -22
- package/dist/issue-query-service.js +33 -38
- package/dist/linear-workflow.js +5 -118
- package/dist/preflight.js +1 -6
- package/dist/project-resolution.js +12 -1
- package/dist/run-orchestrator.js +446 -0
- package/dist/{stage-reporting.js → run-reporting.js} +11 -13
- package/dist/service-runtime.js +12 -61
- package/dist/service-webhooks.js +7 -52
- package/dist/service.js +39 -61
- package/dist/webhook-handler.js +387 -0
- package/dist/webhook-installation-handler.js +3 -8
- package/package.json +2 -1
- package/dist/db/authoritative-ledger-store.js +0 -536
- package/dist/db/issue-projection-store.js +0 -54
- package/dist/db/issue-workflow-coordinator.js +0 -320
- package/dist/db/issue-workflow-store.js +0 -194
- package/dist/db/run-report-store.js +0 -33
- package/dist/db/stage-event-store.js +0 -33
- package/dist/db/webhook-event-store.js +0 -59
- package/dist/db-ports.js +0 -5
- package/dist/ledger-ports.js +0 -1
- package/dist/reconciliation-action-applier.js +0 -68
- package/dist/reconciliation-actions.js +0 -1
- package/dist/reconciliation-engine.js +0 -350
- package/dist/reconciliation-snapshot-builder.js +0 -135
- package/dist/reconciliation-types.js +0 -1
- package/dist/service-stage-finalizer.js +0 -753
- package/dist/service-stage-runner.js +0 -336
- package/dist/service-webhook-processor.js +0 -411
- package/dist/stage-agent-activity-publisher.js +0 -59
- package/dist/stage-event-ports.js +0 -1
- package/dist/stage-failure.js +0 -92
- package/dist/stage-handoff.js +0 -107
- package/dist/stage-launch.js +0 -84
- package/dist/stage-lifecycle-publisher.js +0 -284
- package/dist/stage-turn-input-dispatcher.js +0 -104
- package/dist/webhook-agent-session-handler.js +0 -228
- package/dist/webhook-comment-handler.js +0 -141
- package/dist/webhook-desired-stage-recorder.js +0 -122
- package/dist/webhook-event-ports.js +0 -1
- package/dist/workflow-policy.js +0 -149
- package/dist/workflow-ports.js +0 -1
- /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 {
|
|
9
|
-
import {
|
|
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
|
+
}
|