patchrelay 0.1.0 → 0.3.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 +147 -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 +17 -63
- 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/issue-projection-store.js +54 -0
- package/dist/db/issue-workflow-coordinator.js +280 -0
- package/dist/db/issue-workflow-store.js +53 -550
- package/dist/db/run-report-store.js +33 -0
- package/dist/db.js +20 -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 -3
- 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
|
+
}
|
|
@@ -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,280 @@
|
|
|
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
|
+
}
|
|
150
|
+
finishStageRun(params) {
|
|
151
|
+
const stageRun = this.issueWorkflows.getStageRun(params.stageRunId);
|
|
152
|
+
if (!stageRun) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.runReports.saveRunReport({
|
|
156
|
+
runLeaseId: params.stageRunId,
|
|
157
|
+
...(params.summaryJson !== undefined ? { summaryJson: params.summaryJson } : {}),
|
|
158
|
+
...(params.reportJson !== undefined ? { reportJson: params.reportJson } : {}),
|
|
159
|
+
});
|
|
160
|
+
this.authoritativeLedger.finishRunLease({
|
|
161
|
+
runLeaseId: params.stageRunId,
|
|
162
|
+
status: params.status === "failed" ? "failed" : "completed",
|
|
163
|
+
threadId: params.threadId,
|
|
164
|
+
...(params.turnId !== undefined ? { turnId: params.turnId } : {}),
|
|
165
|
+
});
|
|
166
|
+
const workspace = this.authoritativeLedger.getWorkspaceOwnership(stageRun.workspaceId);
|
|
167
|
+
if (workspace) {
|
|
168
|
+
this.authoritativeLedger.upsertWorkspaceOwnership({
|
|
169
|
+
projectId: stageRun.projectId,
|
|
170
|
+
linearIssueId: stageRun.linearIssueId,
|
|
171
|
+
branchName: workspace.branchName,
|
|
172
|
+
worktreePath: workspace.worktreePath,
|
|
173
|
+
status: params.status === "completed" ? "active" : "paused",
|
|
174
|
+
currentRunLeaseId: null,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
setIssueDesiredStage(projectId, linearIssueId, desiredStage, options) {
|
|
179
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
180
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
181
|
+
const desiredReceiptId = this.resolveDesiredReceiptId({
|
|
182
|
+
projectId,
|
|
183
|
+
linearIssueId,
|
|
184
|
+
...(options?.desiredWebhookId !== undefined ? { desiredWebhookId: options.desiredWebhookId } : {}),
|
|
185
|
+
...(options?.desiredReceiptId !== undefined ? { desiredReceiptId: options.desiredReceiptId } : {}),
|
|
186
|
+
});
|
|
187
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
188
|
+
projectId,
|
|
189
|
+
linearIssueId,
|
|
190
|
+
...(desiredStage !== undefined ? { desiredStage } : { desiredStage: null }),
|
|
191
|
+
...(desiredReceiptId !== undefined
|
|
192
|
+
? { desiredReceiptId }
|
|
193
|
+
: desiredStage === undefined
|
|
194
|
+
? { desiredReceiptId: null }
|
|
195
|
+
: {}),
|
|
196
|
+
lifecycleStatus: options?.lifecycleStatus ??
|
|
197
|
+
(desiredStage ? "queued" : existingIssueControl?.activeRunLeaseId ? (existing?.lifecycleStatus ?? "idle") : "idle"),
|
|
198
|
+
...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
|
|
199
|
+
...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
|
|
200
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
201
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
setIssueLifecycleStatus(projectId, linearIssueId, lifecycleStatus) {
|
|
205
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
206
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
207
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
208
|
+
projectId,
|
|
209
|
+
linearIssueId,
|
|
210
|
+
lifecycleStatus,
|
|
211
|
+
...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
|
|
212
|
+
...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
|
|
213
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
214
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
215
|
+
...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
|
|
216
|
+
...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
setIssueStatusComment(projectId, linearIssueId, statusCommentId) {
|
|
220
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
221
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
222
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
223
|
+
projectId,
|
|
224
|
+
linearIssueId,
|
|
225
|
+
lifecycleStatus: existing?.lifecycleStatus ?? "idle",
|
|
226
|
+
...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
|
|
227
|
+
...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
|
|
228
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
229
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
230
|
+
serviceOwnedCommentId: statusCommentId ?? null,
|
|
231
|
+
...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
setIssueActiveAgentSession(projectId, linearIssueId, agentSessionId) {
|
|
235
|
+
const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
236
|
+
const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
|
|
237
|
+
this.authoritativeLedger.upsertIssueControl({
|
|
238
|
+
projectId,
|
|
239
|
+
linearIssueId,
|
|
240
|
+
lifecycleStatus: existing?.lifecycleStatus ?? "idle",
|
|
241
|
+
...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
|
|
242
|
+
...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
|
|
243
|
+
...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
|
|
244
|
+
...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
|
|
245
|
+
...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
|
|
246
|
+
activeAgentSessionId: agentSessionId ?? null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
resolveDesiredReceiptId(params) {
|
|
250
|
+
if (params.desiredReceiptId !== undefined) {
|
|
251
|
+
return params.desiredReceiptId;
|
|
252
|
+
}
|
|
253
|
+
if (params.desiredWebhookId === undefined) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
if (params.desiredWebhookId === null) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return this.ensureDesiredReceipt(params.projectId, params.linearIssueId, params.desiredWebhookId);
|
|
260
|
+
}
|
|
261
|
+
ensureDesiredReceipt(projectId, linearIssueId, webhookId) {
|
|
262
|
+
const existing = this.connection
|
|
263
|
+
.prepare("SELECT id FROM event_receipts WHERE external_id = ? ORDER BY id DESC LIMIT 1")
|
|
264
|
+
.get(webhookId);
|
|
265
|
+
if (existing) {
|
|
266
|
+
return Number(existing.id);
|
|
267
|
+
}
|
|
268
|
+
const receipt = this.authoritativeLedger.insertEventReceipt({
|
|
269
|
+
source: "patchrelay-desired-stage",
|
|
270
|
+
externalId: webhookId,
|
|
271
|
+
eventType: "desired_stage",
|
|
272
|
+
receivedAt: isoNow(),
|
|
273
|
+
acceptanceStatus: "accepted",
|
|
274
|
+
projectId,
|
|
275
|
+
linearIssueId,
|
|
276
|
+
});
|
|
277
|
+
this.authoritativeLedger.markEventReceiptProcessed(receipt.id, "processed");
|
|
278
|
+
return receipt.id;
|
|
279
|
+
}
|
|
280
|
+
}
|