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.
@@ -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
+ }