patchrelay 0.2.0 → 0.4.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/dist/build-info.json +3 -3
- package/dist/cli/args.js +73 -0
- package/dist/cli/command-types.js +1 -0
- package/dist/cli/commands/connect.js +28 -0
- package/dist/cli/commands/issues.js +155 -0
- package/dist/cli/commands/project.js +140 -0
- package/dist/cli/commands/setup.js +140 -0
- package/dist/cli/connect-flow.js +52 -0
- package/dist/cli/data.js +124 -67
- package/dist/cli/index.js +59 -615
- package/dist/cli/interactive.js +48 -0
- package/dist/cli/output.js +13 -0
- package/dist/cli/service-commands.js +31 -0
- package/dist/db/authoritative-ledger-store.js +95 -0
- package/dist/db/issue-projection-store.js +54 -0
- package/dist/db/issue-workflow-coordinator.js +309 -0
- package/dist/db/issue-workflow-store.js +53 -550
- package/dist/db/migrations.js +19 -0
- package/dist/db/run-report-store.js +33 -0
- package/dist/db.js +22 -1
- package/dist/index.js +13 -4
- package/dist/install.js +4 -3
- package/dist/linear-oauth.js +8 -7
- package/dist/service-stage-finalizer.js +2 -2
- package/dist/service-stage-runner.js +4 -4
- package/dist/service.js +1 -0
- package/dist/stage-failure.js +3 -17
- package/dist/stage-lifecycle-publisher.js +5 -28
- package/dist/webhook-desired-stage-recorder.js +4 -35
- package/infra/patchrelay.path +2 -0
- package/infra/patchrelay.service +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export function buildOpenCommand(config, worktreePath, resumeThreadId) {
|
|
3
|
+
const args = ["--dangerously-bypass-approvals-and-sandbox"];
|
|
4
|
+
if (resumeThreadId) {
|
|
5
|
+
args.push("resume", "-C", worktreePath, resumeThreadId);
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
args.push("-C", worktreePath);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
command: config.runner.codex.bin,
|
|
12
|
+
args,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export async function runInteractiveCommand(command, args) {
|
|
16
|
+
return await new Promise((resolve, reject) => {
|
|
17
|
+
const child = spawn(command, args, {
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
});
|
|
20
|
+
child.on("error", reject);
|
|
21
|
+
child.on("exit", (code, signal) => {
|
|
22
|
+
if (signal) {
|
|
23
|
+
resolve(1);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolve(code ?? 0);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function openExternalUrl(url) {
|
|
31
|
+
const candidates = process.platform === "darwin"
|
|
32
|
+
? [{ command: "open", args: [url] }]
|
|
33
|
+
: process.platform === "win32"
|
|
34
|
+
? [{ command: "cmd", args: ["/c", "start", "", url] }]
|
|
35
|
+
: [{ command: "xdg-open", args: [url] }];
|
|
36
|
+
for (const candidate of candidates) {
|
|
37
|
+
try {
|
|
38
|
+
const exitCode = await runInteractiveCommand(candidate.command, candidate.args);
|
|
39
|
+
if (exitCode === 0) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Try the next opener.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function writeOutput(stream, text) {
|
|
2
|
+
stream.write(text);
|
|
3
|
+
}
|
|
4
|
+
export function formatDoctor(report) {
|
|
5
|
+
const lines = ["PatchRelay doctor", ""];
|
|
6
|
+
for (const check of report.checks) {
|
|
7
|
+
const marker = check.status === "pass" ? "PASS" : check.status === "warn" ? "WARN" : "FAIL";
|
|
8
|
+
lines.push(`${marker} [${check.scope}] ${check.message}`);
|
|
9
|
+
}
|
|
10
|
+
lines.push("");
|
|
11
|
+
lines.push(report.ok ? "Doctor result: ready" : "Doctor result: not ready");
|
|
12
|
+
return `${lines.join("\n")}\n`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export async function runServiceCommands(runner, commands) {
|
|
2
|
+
for (const entry of commands) {
|
|
3
|
+
const exitCode = await runner(entry.command, entry.args);
|
|
4
|
+
if (exitCode !== 0) {
|
|
5
|
+
throw new Error(`Command failed with exit code ${exitCode}: ${entry.command} ${entry.args.join(" ")}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function tryManageService(runner, commands) {
|
|
10
|
+
try {
|
|
11
|
+
await runServiceCommands(runner, commands);
|
|
12
|
+
return { ok: true };
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function installServiceCommands() {
|
|
19
|
+
return [
|
|
20
|
+
{ command: "systemctl", args: ["--user", "daemon-reload"] },
|
|
21
|
+
{ command: "systemctl", args: ["--user", "enable", "--now", "patchrelay.path"] },
|
|
22
|
+
{ command: "systemctl", args: ["--user", "enable", "patchrelay.service"] },
|
|
23
|
+
{ command: "systemctl", args: ["--user", "reload-or-restart", "patchrelay.service"] },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
export function restartServiceCommands() {
|
|
27
|
+
return [
|
|
28
|
+
{ command: "systemctl", args: ["--user", "daemon-reload"] },
|
|
29
|
+
{ command: "systemctl", args: ["--user", "reload-or-restart", "patchrelay.service"] },
|
|
30
|
+
];
|
|
31
|
+
}
|
|
@@ -141,6 +141,85 @@ export class AuthoritativeLedgerStore {
|
|
|
141
141
|
.get(projectId, linearIssueId);
|
|
142
142
|
return row ? mapWorkspaceOwnership(row) : undefined;
|
|
143
143
|
}
|
|
144
|
+
upsertIssueSession(params) {
|
|
145
|
+
const now = isoNow();
|
|
146
|
+
this.connection
|
|
147
|
+
.prepare(`
|
|
148
|
+
INSERT INTO issue_sessions (
|
|
149
|
+
project_id, linear_issue_id, workspace_ownership_id, run_lease_id, thread_id, parent_thread_id,
|
|
150
|
+
source, linked_agent_session_id, created_at, updated_at, last_opened_at
|
|
151
|
+
) VALUES (
|
|
152
|
+
@projectId, @linearIssueId, @workspaceOwnershipId, @runLeaseId, @threadId, @parentThreadId,
|
|
153
|
+
@source, @linkedAgentSessionId, @createdAt, @updatedAt, NULL
|
|
154
|
+
)
|
|
155
|
+
ON CONFLICT(thread_id) DO UPDATE SET
|
|
156
|
+
project_id = @projectId,
|
|
157
|
+
linear_issue_id = @linearIssueId,
|
|
158
|
+
workspace_ownership_id = @workspaceOwnershipId,
|
|
159
|
+
run_lease_id = CASE WHEN @setRunLeaseId = 1 THEN @runLeaseId ELSE issue_sessions.run_lease_id END,
|
|
160
|
+
parent_thread_id = CASE WHEN @setParentThreadId = 1 THEN @parentThreadId ELSE issue_sessions.parent_thread_id END,
|
|
161
|
+
source = @source,
|
|
162
|
+
linked_agent_session_id = CASE
|
|
163
|
+
WHEN @setLinkedAgentSessionId = 1 THEN @linkedAgentSessionId
|
|
164
|
+
ELSE issue_sessions.linked_agent_session_id
|
|
165
|
+
END,
|
|
166
|
+
updated_at = @updatedAt
|
|
167
|
+
`)
|
|
168
|
+
.run({
|
|
169
|
+
projectId: params.projectId,
|
|
170
|
+
linearIssueId: params.linearIssueId,
|
|
171
|
+
workspaceOwnershipId: params.workspaceOwnershipId,
|
|
172
|
+
runLeaseId: params.runLeaseId ?? null,
|
|
173
|
+
threadId: params.threadId,
|
|
174
|
+
parentThreadId: params.parentThreadId ?? null,
|
|
175
|
+
source: params.source,
|
|
176
|
+
linkedAgentSessionId: params.linkedAgentSessionId ?? null,
|
|
177
|
+
createdAt: now,
|
|
178
|
+
updatedAt: now,
|
|
179
|
+
setRunLeaseId: Number("runLeaseId" in params),
|
|
180
|
+
setParentThreadId: Number("parentThreadId" in params),
|
|
181
|
+
setLinkedAgentSessionId: Number("linkedAgentSessionId" in params),
|
|
182
|
+
});
|
|
183
|
+
return this.getIssueSessionByThreadId(params.threadId);
|
|
184
|
+
}
|
|
185
|
+
getIssueSessionByThreadId(threadId) {
|
|
186
|
+
const row = this.connection
|
|
187
|
+
.prepare("SELECT * FROM issue_sessions WHERE thread_id = ?")
|
|
188
|
+
.get(threadId);
|
|
189
|
+
return row ? mapIssueSession(row) : undefined;
|
|
190
|
+
}
|
|
191
|
+
listIssueSessionsForIssue(projectId, linearIssueId) {
|
|
192
|
+
const rows = this.connection
|
|
193
|
+
.prepare(`
|
|
194
|
+
SELECT * FROM issue_sessions
|
|
195
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
196
|
+
ORDER BY
|
|
197
|
+
CASE WHEN last_opened_at IS NULL THEN 1 ELSE 0 END,
|
|
198
|
+
last_opened_at DESC,
|
|
199
|
+
id DESC
|
|
200
|
+
`)
|
|
201
|
+
.all(projectId, linearIssueId);
|
|
202
|
+
return rows.map((row) => mapIssueSession(row));
|
|
203
|
+
}
|
|
204
|
+
touchIssueSession(threadId) {
|
|
205
|
+
const now = isoNow();
|
|
206
|
+
const result = this.connection
|
|
207
|
+
.prepare(`
|
|
208
|
+
UPDATE issue_sessions
|
|
209
|
+
SET updated_at = @updatedAt,
|
|
210
|
+
last_opened_at = @lastOpenedAt
|
|
211
|
+
WHERE thread_id = @threadId
|
|
212
|
+
`)
|
|
213
|
+
.run({
|
|
214
|
+
threadId,
|
|
215
|
+
updatedAt: now,
|
|
216
|
+
lastOpenedAt: now,
|
|
217
|
+
});
|
|
218
|
+
if (result.changes < 1) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
return this.getIssueSessionByThreadId(threadId);
|
|
222
|
+
}
|
|
144
223
|
createRunLease(params) {
|
|
145
224
|
const result = this.connection
|
|
146
225
|
.prepare(`
|
|
@@ -416,6 +495,22 @@ function mapRunLease(row) {
|
|
|
416
495
|
...(row.failure_reason === null ? {} : { failureReason: String(row.failure_reason) }),
|
|
417
496
|
};
|
|
418
497
|
}
|
|
498
|
+
function mapIssueSession(row) {
|
|
499
|
+
return {
|
|
500
|
+
id: Number(row.id),
|
|
501
|
+
projectId: String(row.project_id),
|
|
502
|
+
linearIssueId: String(row.linear_issue_id),
|
|
503
|
+
workspaceOwnershipId: Number(row.workspace_ownership_id),
|
|
504
|
+
threadId: String(row.thread_id),
|
|
505
|
+
source: row.source,
|
|
506
|
+
...(row.run_lease_id === null ? {} : { runLeaseId: Number(row.run_lease_id) }),
|
|
507
|
+
...(row.parent_thread_id === null ? {} : { parentThreadId: String(row.parent_thread_id) }),
|
|
508
|
+
...(row.linked_agent_session_id === null ? {} : { linkedAgentSessionId: String(row.linked_agent_session_id) }),
|
|
509
|
+
createdAt: String(row.created_at),
|
|
510
|
+
updatedAt: String(row.updated_at),
|
|
511
|
+
...(row.last_opened_at === null ? {} : { lastOpenedAt: String(row.last_opened_at) }),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
419
514
|
function mapObligation(row) {
|
|
420
515
|
return {
|
|
421
516
|
id: Number(row.id),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isoNow } from "./shared.js";
|
|
2
|
+
export class IssueProjectionStore {
|
|
3
|
+
connection;
|
|
4
|
+
constructor(connection) {
|
|
5
|
+
this.connection = connection;
|
|
6
|
+
}
|
|
7
|
+
upsertIssueProjection(params) {
|
|
8
|
+
this.connection
|
|
9
|
+
.prepare(`
|
|
10
|
+
INSERT INTO issue_projection (
|
|
11
|
+
project_id, linear_issue_id, issue_key, title, issue_url, current_linear_state, last_webhook_at, updated_at
|
|
12
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
13
|
+
ON CONFLICT(project_id, linear_issue_id) DO UPDATE SET
|
|
14
|
+
issue_key = COALESCE(excluded.issue_key, issue_projection.issue_key),
|
|
15
|
+
title = COALESCE(excluded.title, issue_projection.title),
|
|
16
|
+
issue_url = COALESCE(excluded.issue_url, issue_projection.issue_url),
|
|
17
|
+
current_linear_state = COALESCE(excluded.current_linear_state, issue_projection.current_linear_state),
|
|
18
|
+
last_webhook_at = COALESCE(excluded.last_webhook_at, issue_projection.last_webhook_at),
|
|
19
|
+
updated_at = excluded.updated_at
|
|
20
|
+
`)
|
|
21
|
+
.run(params.projectId, params.linearIssueId, params.issueKey ?? null, params.title ?? null, params.issueUrl ?? null, params.currentLinearState ?? null, params.lastWebhookAt ?? null, isoNow());
|
|
22
|
+
}
|
|
23
|
+
getIssueProjection(projectId, linearIssueId) {
|
|
24
|
+
const row = this.connection
|
|
25
|
+
.prepare("SELECT * FROM issue_projection WHERE project_id = ? AND linear_issue_id = ?")
|
|
26
|
+
.get(projectId, linearIssueId);
|
|
27
|
+
return row ? mapIssueProjection(row) : undefined;
|
|
28
|
+
}
|
|
29
|
+
getIssueProjectionByKey(issueKey) {
|
|
30
|
+
const row = this.connection
|
|
31
|
+
.prepare("SELECT * FROM issue_projection WHERE issue_key = ? ORDER BY updated_at DESC LIMIT 1")
|
|
32
|
+
.get(issueKey);
|
|
33
|
+
return row ? mapIssueProjection(row) : undefined;
|
|
34
|
+
}
|
|
35
|
+
getIssueProjectionByLinearIssueId(linearIssueId) {
|
|
36
|
+
const row = this.connection
|
|
37
|
+
.prepare("SELECT * FROM issue_projection WHERE linear_issue_id = ? ORDER BY updated_at DESC LIMIT 1")
|
|
38
|
+
.get(linearIssueId);
|
|
39
|
+
return row ? mapIssueProjection(row) : undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function mapIssueProjection(row) {
|
|
43
|
+
return {
|
|
44
|
+
id: Number(row.id),
|
|
45
|
+
projectId: String(row.project_id),
|
|
46
|
+
linearIssueId: String(row.linear_issue_id),
|
|
47
|
+
...(row.issue_key === null ? {} : { issueKey: String(row.issue_key) }),
|
|
48
|
+
...(row.title === null ? {} : { title: String(row.title) }),
|
|
49
|
+
...(row.issue_url === null ? {} : { issueUrl: String(row.issue_url) }),
|
|
50
|
+
...(row.current_linear_state === null ? {} : { currentLinearState: String(row.current_linear_state) }),
|
|
51
|
+
...(row.last_webhook_at === null ? {} : { lastWebhookAt: String(row.last_webhook_at) }),
|
|
52
|
+
updatedAt: String(row.updated_at),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { isoNow } from "./shared.js";
|
|
2
|
+
export class IssueWorkflowCoordinator {
|
|
3
|
+
connection;
|
|
4
|
+
authoritativeLedger;
|
|
5
|
+
issueProjections;
|
|
6
|
+
issueWorkflows;
|
|
7
|
+
runReports;
|
|
8
|
+
constructor(dependencies) {
|
|
9
|
+
this.connection = dependencies.connection;
|
|
10
|
+
this.authoritativeLedger = dependencies.authoritativeLedger;
|
|
11
|
+
this.issueProjections = dependencies.issueProjections;
|
|
12
|
+
this.issueWorkflows = dependencies.issueWorkflows;
|
|
13
|
+
this.runReports = dependencies.runReports;
|
|
14
|
+
}
|
|
15
|
+
upsertTrackedIssue(params) {
|
|
16
|
+
this.issueProjections.upsertIssueProjection({
|
|
17
|
+
projectId: params.projectId,
|
|
18
|
+
linearIssueId: params.linearIssueId,
|
|
19
|
+
...(params.issueKey ? { issueKey: params.issueKey } : {}),
|
|
20
|
+
...(params.title ? { title: params.title } : {}),
|
|
21
|
+
...(params.issueUrl ? { issueUrl: params.issueUrl } : {}),
|
|
22
|
+
...(params.currentLinearState ? { currentLinearState: params.currentLinearState } : {}),
|
|
23
|
+
...(params.lastWebhookAt ? { lastWebhookAt: params.lastWebhookAt } : {}),
|
|
24
|
+
});
|
|
25
|
+
const desiredReceiptId = this.resolveDesiredReceiptId({
|
|
26
|
+
projectId: params.projectId,
|
|
27
|
+
linearIssueId: params.linearIssueId,
|
|
28
|
+
...(params.desiredWebhookId !== undefined ? { desiredWebhookId: params.desiredWebhookId } : {}),
|
|
29
|
+
...(params.desiredReceiptId !== undefined ? { desiredReceiptId: params.desiredReceiptId } : {}),
|
|
30
|
+
});
|
|
31
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
32
|
+
projectId: params.projectId,
|
|
33
|
+
linearIssueId: params.linearIssueId,
|
|
34
|
+
...(params.desiredStage !== undefined ? { desiredStage: params.desiredStage } : {}),
|
|
35
|
+
...(desiredReceiptId !== undefined ? { desiredReceiptId } : {}),
|
|
36
|
+
...(params.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: params.activeWorkspaceId } : {}),
|
|
37
|
+
...(params.activeStageRunId !== undefined ? { activeRunLeaseId: params.activeStageRunId } : {}),
|
|
38
|
+
...(params.statusCommentId !== undefined ? { serviceOwnedCommentId: params.statusCommentId } : {}),
|
|
39
|
+
...(params.activeAgentSessionId !== undefined ? { activeAgentSessionId: params.activeAgentSessionId } : {}),
|
|
40
|
+
lifecycleStatus: params.lifecycleStatus,
|
|
41
|
+
});
|
|
42
|
+
return this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
|
|
43
|
+
}
|
|
44
|
+
recordDesiredStage(params) {
|
|
45
|
+
const existing = this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
|
|
46
|
+
this.issueProjections.upsertIssueProjection({
|
|
47
|
+
projectId: params.projectId,
|
|
48
|
+
linearIssueId: params.linearIssueId,
|
|
49
|
+
...(params.issueKey ? { issueKey: params.issueKey } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
|
|
50
|
+
...(params.title ? { title: params.title } : existing?.title ? { title: existing.title } : {}),
|
|
51
|
+
...(params.issueUrl ? { issueUrl: params.issueUrl } : existing?.issueUrl ? { issueUrl: existing.issueUrl } : {}),
|
|
52
|
+
...(params.currentLinearState
|
|
53
|
+
? { currentLinearState: params.currentLinearState }
|
|
54
|
+
: existing?.currentLinearState
|
|
55
|
+
? { currentLinearState: existing.currentLinearState }
|
|
56
|
+
: {}),
|
|
57
|
+
lastWebhookAt: params.lastWebhookAt,
|
|
58
|
+
});
|
|
59
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(params.projectId, params.linearIssueId);
|
|
60
|
+
const lifecycleStatus = existingIssueControl?.activeRunLeaseId || params.desiredStage
|
|
61
|
+
? existing?.lifecycleStatus ?? "queued"
|
|
62
|
+
: existing?.lifecycleStatus ?? "idle";
|
|
63
|
+
const desiredReceiptId = this.resolveDesiredReceiptId({
|
|
64
|
+
projectId: params.projectId,
|
|
65
|
+
linearIssueId: params.linearIssueId,
|
|
66
|
+
...(params.desiredWebhookId !== undefined ? { desiredWebhookId: params.desiredWebhookId } : {}),
|
|
67
|
+
...(params.desiredReceiptId !== undefined ? { desiredReceiptId: params.desiredReceiptId } : {}),
|
|
68
|
+
});
|
|
69
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
70
|
+
projectId: params.projectId,
|
|
71
|
+
linearIssueId: params.linearIssueId,
|
|
72
|
+
...(params.desiredStage !== undefined ? { desiredStage: params.desiredStage } : {}),
|
|
73
|
+
...(desiredReceiptId !== undefined ? { desiredReceiptId } : {}),
|
|
74
|
+
lifecycleStatus,
|
|
75
|
+
...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
|
|
76
|
+
...(params.activeAgentSessionId !== undefined
|
|
77
|
+
? { activeAgentSessionId: params.activeAgentSessionId }
|
|
78
|
+
: existing?.activeAgentSessionId
|
|
79
|
+
? { activeAgentSessionId: existing.activeAgentSessionId }
|
|
80
|
+
: {}),
|
|
81
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
82
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
83
|
+
});
|
|
84
|
+
return this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
|
|
85
|
+
}
|
|
86
|
+
claimStageRun(params) {
|
|
87
|
+
const transaction = this.connection.transaction(() => {
|
|
88
|
+
const issue = this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
|
|
89
|
+
const issueControl = this.authoritativeLedger.getIssueControl(params.projectId, params.linearIssueId);
|
|
90
|
+
if (!issue ||
|
|
91
|
+
!issueControl ||
|
|
92
|
+
issueControl.activeRunLeaseId !== undefined ||
|
|
93
|
+
issue.desiredStage !== params.stage ||
|
|
94
|
+
issue.desiredWebhookId !== params.triggerWebhookId) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const workspaceOwnership = this.authoritativeLedger.upsertWorkspaceOwnership({
|
|
98
|
+
projectId: params.projectId,
|
|
99
|
+
linearIssueId: params.linearIssueId,
|
|
100
|
+
branchName: params.branchName,
|
|
101
|
+
worktreePath: params.worktreePath,
|
|
102
|
+
status: "active",
|
|
103
|
+
});
|
|
104
|
+
const runLease = this.authoritativeLedger.createRunLease({
|
|
105
|
+
issueControlId: issueControl.id,
|
|
106
|
+
projectId: params.projectId,
|
|
107
|
+
linearIssueId: params.linearIssueId,
|
|
108
|
+
workspaceOwnershipId: workspaceOwnership.id,
|
|
109
|
+
stage: params.stage,
|
|
110
|
+
status: "running",
|
|
111
|
+
workflowFile: params.workflowFile,
|
|
112
|
+
promptText: params.promptText,
|
|
113
|
+
triggerReceiptId: issueControl.desiredReceiptId ?? null,
|
|
114
|
+
});
|
|
115
|
+
this.authoritativeLedger.upsertWorkspaceOwnership({
|
|
116
|
+
projectId: params.projectId,
|
|
117
|
+
linearIssueId: params.linearIssueId,
|
|
118
|
+
branchName: params.branchName,
|
|
119
|
+
worktreePath: params.worktreePath,
|
|
120
|
+
status: "active",
|
|
121
|
+
currentRunLeaseId: runLease.id,
|
|
122
|
+
});
|
|
123
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
124
|
+
projectId: params.projectId,
|
|
125
|
+
linearIssueId: params.linearIssueId,
|
|
126
|
+
desiredStage: null,
|
|
127
|
+
desiredReceiptId: null,
|
|
128
|
+
activeWorkspaceOwnershipId: workspaceOwnership.id,
|
|
129
|
+
activeRunLeaseId: runLease.id,
|
|
130
|
+
lifecycleStatus: "running",
|
|
131
|
+
...(issue.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
|
|
132
|
+
...(issue.activeAgentSessionId ? { activeAgentSessionId: issue.activeAgentSessionId } : {}),
|
|
133
|
+
});
|
|
134
|
+
const refreshedIssue = this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
|
|
135
|
+
const workspace = this.issueWorkflows.getWorkspace(workspaceOwnership.id);
|
|
136
|
+
const stageRun = this.issueWorkflows.getStageRun(runLease.id);
|
|
137
|
+
const pipeline = this.issueWorkflows.getPipelineRun(runLease.id);
|
|
138
|
+
return { issue: refreshedIssue, workspace, pipeline, stageRun };
|
|
139
|
+
});
|
|
140
|
+
return transaction();
|
|
141
|
+
}
|
|
142
|
+
updateStageRunThread(params) {
|
|
143
|
+
this.authoritativeLedger.updateRunLeaseThread({
|
|
144
|
+
runLeaseId: params.stageRunId,
|
|
145
|
+
threadId: params.threadId,
|
|
146
|
+
...(params.parentThreadId !== undefined ? { parentThreadId: params.parentThreadId } : {}),
|
|
147
|
+
...(params.turnId !== undefined ? { turnId: params.turnId } : {}),
|
|
148
|
+
});
|
|
149
|
+
const stageRun = this.issueWorkflows.getStageRun(params.stageRunId);
|
|
150
|
+
if (!stageRun) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const issue = this.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
154
|
+
this.authoritativeLedger.upsertIssueSession({
|
|
155
|
+
projectId: stageRun.projectId,
|
|
156
|
+
linearIssueId: stageRun.linearIssueId,
|
|
157
|
+
workspaceOwnershipId: stageRun.workspaceId,
|
|
158
|
+
runLeaseId: stageRun.id,
|
|
159
|
+
threadId: params.threadId,
|
|
160
|
+
...(params.parentThreadId !== undefined ? { parentThreadId: params.parentThreadId } : {}),
|
|
161
|
+
...(issue?.activeAgentSessionId ? { linkedAgentSessionId: issue.activeAgentSessionId } : {}),
|
|
162
|
+
source: "stage_run",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
finishStageRun(params) {
|
|
166
|
+
const stageRun = this.issueWorkflows.getStageRun(params.stageRunId);
|
|
167
|
+
if (!stageRun) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.runReports.saveRunReport({
|
|
171
|
+
runLeaseId: params.stageRunId,
|
|
172
|
+
...(params.summaryJson !== undefined ? { summaryJson: params.summaryJson } : {}),
|
|
173
|
+
...(params.reportJson !== undefined ? { reportJson: params.reportJson } : {}),
|
|
174
|
+
});
|
|
175
|
+
this.authoritativeLedger.finishRunLease({
|
|
176
|
+
runLeaseId: params.stageRunId,
|
|
177
|
+
status: params.status === "failed" ? "failed" : "completed",
|
|
178
|
+
threadId: params.threadId,
|
|
179
|
+
...(params.turnId !== undefined ? { turnId: params.turnId } : {}),
|
|
180
|
+
});
|
|
181
|
+
const runLease = this.authoritativeLedger.getRunLease(params.stageRunId);
|
|
182
|
+
if (runLease?.workspaceOwnershipId !== undefined) {
|
|
183
|
+
const issue = this.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
184
|
+
this.authoritativeLedger.upsertIssueSession({
|
|
185
|
+
projectId: stageRun.projectId,
|
|
186
|
+
linearIssueId: stageRun.linearIssueId,
|
|
187
|
+
workspaceOwnershipId: runLease.workspaceOwnershipId,
|
|
188
|
+
runLeaseId: params.stageRunId,
|
|
189
|
+
threadId: params.threadId,
|
|
190
|
+
...(runLease.parentThreadId ? { parentThreadId: runLease.parentThreadId } : {}),
|
|
191
|
+
...(issue?.activeAgentSessionId ? { linkedAgentSessionId: issue.activeAgentSessionId } : {}),
|
|
192
|
+
source: "stage_run",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const workspace = this.authoritativeLedger.getWorkspaceOwnership(stageRun.workspaceId);
|
|
196
|
+
if (workspace) {
|
|
197
|
+
this.authoritativeLedger.upsertWorkspaceOwnership({
|
|
198
|
+
projectId: stageRun.projectId,
|
|
199
|
+
linearIssueId: stageRun.linearIssueId,
|
|
200
|
+
branchName: workspace.branchName,
|
|
201
|
+
worktreePath: workspace.worktreePath,
|
|
202
|
+
status: params.status === "completed" ? "active" : "paused",
|
|
203
|
+
currentRunLeaseId: null,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
setIssueDesiredStage(projectId, linearIssueId, desiredStage, options) {
|
|
208
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
209
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
210
|
+
const desiredReceiptId = this.resolveDesiredReceiptId({
|
|
211
|
+
projectId,
|
|
212
|
+
linearIssueId,
|
|
213
|
+
...(options?.desiredWebhookId !== undefined ? { desiredWebhookId: options.desiredWebhookId } : {}),
|
|
214
|
+
...(options?.desiredReceiptId !== undefined ? { desiredReceiptId: options.desiredReceiptId } : {}),
|
|
215
|
+
});
|
|
216
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
217
|
+
projectId,
|
|
218
|
+
linearIssueId,
|
|
219
|
+
...(desiredStage !== undefined ? { desiredStage } : { desiredStage: null }),
|
|
220
|
+
...(desiredReceiptId !== undefined
|
|
221
|
+
? { desiredReceiptId }
|
|
222
|
+
: desiredStage === undefined
|
|
223
|
+
? { desiredReceiptId: null }
|
|
224
|
+
: {}),
|
|
225
|
+
lifecycleStatus: options?.lifecycleStatus ??
|
|
226
|
+
(desiredStage ? "queued" : existingIssueControl?.activeRunLeaseId ? (existing?.lifecycleStatus ?? "idle") : "idle"),
|
|
227
|
+
...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
|
|
228
|
+
...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
|
|
229
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
230
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
setIssueLifecycleStatus(projectId, linearIssueId, lifecycleStatus) {
|
|
234
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
235
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
236
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
237
|
+
projectId,
|
|
238
|
+
linearIssueId,
|
|
239
|
+
lifecycleStatus,
|
|
240
|
+
...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
|
|
241
|
+
...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
|
|
242
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
243
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
244
|
+
...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
|
|
245
|
+
...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
setIssueStatusComment(projectId, linearIssueId, statusCommentId) {
|
|
249
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
250
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
251
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
252
|
+
projectId,
|
|
253
|
+
linearIssueId,
|
|
254
|
+
lifecycleStatus: existing?.lifecycleStatus ?? "idle",
|
|
255
|
+
...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
|
|
256
|
+
...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
|
|
257
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
258
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
259
|
+
serviceOwnedCommentId: statusCommentId ?? null,
|
|
260
|
+
...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
setIssueActiveAgentSession(projectId, linearIssueId, agentSessionId) {
|
|
264
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
265
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
266
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
267
|
+
projectId,
|
|
268
|
+
linearIssueId,
|
|
269
|
+
lifecycleStatus: existing?.lifecycleStatus ?? "idle",
|
|
270
|
+
...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
|
|
271
|
+
...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
|
|
272
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
273
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
274
|
+
...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
|
|
275
|
+
activeAgentSessionId: agentSessionId ?? null,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
resolveDesiredReceiptId(params) {
|
|
279
|
+
if (params.desiredReceiptId !== undefined) {
|
|
280
|
+
return params.desiredReceiptId;
|
|
281
|
+
}
|
|
282
|
+
if (params.desiredWebhookId === undefined) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
if (params.desiredWebhookId === null) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return this.ensureDesiredReceipt(params.projectId, params.linearIssueId, params.desiredWebhookId);
|
|
289
|
+
}
|
|
290
|
+
ensureDesiredReceipt(projectId, linearIssueId, webhookId) {
|
|
291
|
+
const existing = this.connection
|
|
292
|
+
.prepare("SELECT id FROM event_receipts WHERE external_id = ? ORDER BY id DESC LIMIT 1")
|
|
293
|
+
.get(webhookId);
|
|
294
|
+
if (existing) {
|
|
295
|
+
return Number(existing.id);
|
|
296
|
+
}
|
|
297
|
+
const receipt = this.authoritativeLedger.insertEventReceipt({
|
|
298
|
+
source: "patchrelay-desired-stage",
|
|
299
|
+
externalId: webhookId,
|
|
300
|
+
eventType: "desired_stage",
|
|
301
|
+
receivedAt: isoNow(),
|
|
302
|
+
acceptanceStatus: "accepted",
|
|
303
|
+
projectId,
|
|
304
|
+
linearIssueId,
|
|
305
|
+
});
|
|
306
|
+
this.authoritativeLedger.markEventReceiptProcessed(receipt.id, "processed");
|
|
307
|
+
return receipt.id;
|
|
308
|
+
}
|
|
309
|
+
}
|