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,33 @@
|
|
|
1
|
+
import { isoNow } from "./shared.js";
|
|
2
|
+
export class RunReportStore {
|
|
3
|
+
connection;
|
|
4
|
+
constructor(connection) {
|
|
5
|
+
this.connection = connection;
|
|
6
|
+
}
|
|
7
|
+
saveRunReport(params) {
|
|
8
|
+
const now = isoNow();
|
|
9
|
+
this.connection
|
|
10
|
+
.prepare(`
|
|
11
|
+
INSERT INTO run_reports (run_lease_id, summary_json, report_json, created_at, updated_at)
|
|
12
|
+
VALUES (?, ?, ?, ?, ?)
|
|
13
|
+
ON CONFLICT(run_lease_id) DO UPDATE SET
|
|
14
|
+
summary_json = excluded.summary_json,
|
|
15
|
+
report_json = excluded.report_json,
|
|
16
|
+
updated_at = excluded.updated_at
|
|
17
|
+
`)
|
|
18
|
+
.run(params.runLeaseId, params.summaryJson ?? null, params.reportJson ?? null, now, now);
|
|
19
|
+
}
|
|
20
|
+
getRunReport(runLeaseId) {
|
|
21
|
+
const row = this.connection.prepare("SELECT * FROM run_reports WHERE run_lease_id = ?").get(runLeaseId);
|
|
22
|
+
return row ? mapRunReport(row) : undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function mapRunReport(row) {
|
|
26
|
+
return {
|
|
27
|
+
runLeaseId: Number(row.run_lease_id),
|
|
28
|
+
...(row.summary_json === null ? {} : { summaryJson: String(row.summary_json) }),
|
|
29
|
+
...(row.report_json === null ? {} : { reportJson: String(row.report_json) }),
|
|
30
|
+
createdAt: String(row.created_at),
|
|
31
|
+
updatedAt: String(row.updated_at),
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/db.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
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";
|
|
2
4
|
import { IssueWorkflowStore } from "./db/issue-workflow-store.js";
|
|
3
5
|
import { LinearInstallationStore } from "./db/linear-installation-store.js";
|
|
4
6
|
import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
7
|
+
import { RunReportStore } from "./db/run-report-store.js";
|
|
5
8
|
import { StageEventStore } from "./db/stage-event-store.js";
|
|
6
9
|
import { SqliteConnection } from "./db/shared.js";
|
|
7
10
|
import { WebhookEventStore } from "./db/webhook-event-store.js";
|
|
@@ -14,7 +17,10 @@ export class PatchRelayDatabase {
|
|
|
14
17
|
runLeases;
|
|
15
18
|
obligations;
|
|
16
19
|
webhookEvents;
|
|
20
|
+
issueProjections;
|
|
17
21
|
issueWorkflows;
|
|
22
|
+
workflowCoordinator;
|
|
23
|
+
runReports;
|
|
18
24
|
stageEvents;
|
|
19
25
|
linearInstallations;
|
|
20
26
|
constructor(databasePath, wal) {
|
|
@@ -30,7 +36,20 @@ export class PatchRelayDatabase {
|
|
|
30
36
|
this.runLeases = this.authoritativeLedger;
|
|
31
37
|
this.obligations = this.authoritativeLedger;
|
|
32
38
|
this.webhookEvents = new WebhookEventStore(this.connection);
|
|
33
|
-
this.
|
|
39
|
+
this.issueProjections = new IssueProjectionStore(this.connection);
|
|
40
|
+
this.runReports = new RunReportStore(this.connection);
|
|
41
|
+
this.issueWorkflows = new IssueWorkflowStore({
|
|
42
|
+
authoritativeLedger: this.authoritativeLedger,
|
|
43
|
+
issueProjections: this.issueProjections,
|
|
44
|
+
runReports: this.runReports,
|
|
45
|
+
});
|
|
46
|
+
this.workflowCoordinator = new IssueWorkflowCoordinator({
|
|
47
|
+
connection: this.connection,
|
|
48
|
+
authoritativeLedger: this.authoritativeLedger,
|
|
49
|
+
issueProjections: this.issueProjections,
|
|
50
|
+
issueWorkflows: this.issueWorkflows,
|
|
51
|
+
runReports: this.runReports,
|
|
52
|
+
});
|
|
34
53
|
this.stageEvents = new StageEventStore(this.connection);
|
|
35
54
|
this.linearInstallations = new LinearInstallationStore(this.connection);
|
|
36
55
|
}
|
package/dist/index.js
CHANGED
|
@@ -42,10 +42,19 @@ async function main() {
|
|
|
42
42
|
const service = new PatchRelayService(config, db, codex, linearProvider, logger);
|
|
43
43
|
await service.start();
|
|
44
44
|
const app = await buildHttpServer(config, service, logger);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
try {
|
|
46
|
+
await app.listen({
|
|
47
|
+
host: config.server.bind,
|
|
48
|
+
port: config.server.port,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
|
|
53
|
+
throw new Error(`Port ${config.server.port} on ${config.server.bind} is already in use. ` +
|
|
54
|
+
`Another patchrelay process may be running. Check with: ss -tlnp | grep ${config.server.port}`, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
49
58
|
logger.info({
|
|
50
59
|
bind: config.server.bind,
|
|
51
60
|
port: config.server.port,
|
package/dist/install.js
CHANGED
|
@@ -193,11 +193,12 @@ export async function upsertProjectInConfig(options) {
|
|
|
193
193
|
const original = await readFile(configPath, "utf8");
|
|
194
194
|
const parsed = parseConfigObject(original, configPath);
|
|
195
195
|
const existingProjects = Array.isArray(parsed.projects) ? parsed.projects : [];
|
|
196
|
-
const existingIndex = existingProjects.findIndex((project) => String(project.id ?? "") === projectId);
|
|
196
|
+
const existingIndex = existingProjects.findIndex((project) => String(project.id ?? "").toLowerCase() === projectId.toLowerCase());
|
|
197
197
|
const existingProject = existingIndex >= 0 ? existingProjects[existingIndex] : undefined;
|
|
198
|
+
const resolvedProjectId = existingProject ? String(existingProject.id ?? projectId) : projectId;
|
|
198
199
|
const nextProject = {
|
|
199
200
|
...(existingProject ?? {}),
|
|
200
|
-
id:
|
|
201
|
+
id: resolvedProjectId,
|
|
201
202
|
repo_path: repoPath,
|
|
202
203
|
workflows: Array.isArray(existingProject?.workflows) && existingProject.workflows.length > 0
|
|
203
204
|
? existingProject.workflows
|
|
@@ -293,7 +294,7 @@ export async function upsertProjectInConfig(options) {
|
|
|
293
294
|
configPath,
|
|
294
295
|
status,
|
|
295
296
|
project: {
|
|
296
|
-
id:
|
|
297
|
+
id: resolvedProjectId,
|
|
297
298
|
repoPath,
|
|
298
299
|
issueKeyPrefixes,
|
|
299
300
|
linearTeamIds,
|
package/dist/linear-oauth.js
CHANGED
|
@@ -20,19 +20,20 @@ export async function exchangeLinearOAuthCode(config, params) {
|
|
|
20
20
|
const response = await fetch(DEFAULT_LINEAR_TOKEN_URL, {
|
|
21
21
|
method: "POST",
|
|
22
22
|
headers: {
|
|
23
|
-
"content-type": "application/
|
|
23
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
24
24
|
},
|
|
25
|
-
body:
|
|
25
|
+
body: new URLSearchParams({
|
|
26
26
|
grant_type: "authorization_code",
|
|
27
27
|
code: params.code,
|
|
28
28
|
client_id: config.linear.oauth.clientId,
|
|
29
29
|
client_secret: config.linear.oauth.clientSecret,
|
|
30
30
|
redirect_uri: params.redirectUri,
|
|
31
|
-
}),
|
|
31
|
+
}).toString(),
|
|
32
32
|
});
|
|
33
33
|
const payload = (await response.json().catch(() => undefined));
|
|
34
34
|
if (!response.ok || !payload) {
|
|
35
|
-
|
|
35
|
+
const detail = payload?.error ? `: ${String(payload.error)}` : "";
|
|
36
|
+
throw new Error(`Linear OAuth code exchange failed with HTTP ${response.status}${detail}`);
|
|
36
37
|
}
|
|
37
38
|
const accessToken = typeof payload.access_token === "string" ? payload.access_token : undefined;
|
|
38
39
|
if (!accessToken) {
|
|
@@ -53,14 +54,14 @@ export async function refreshLinearOAuthToken(config, refreshToken) {
|
|
|
53
54
|
const response = await fetch(DEFAULT_LINEAR_TOKEN_URL, {
|
|
54
55
|
method: "POST",
|
|
55
56
|
headers: {
|
|
56
|
-
"content-type": "application/
|
|
57
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
57
58
|
},
|
|
58
|
-
body:
|
|
59
|
+
body: new URLSearchParams({
|
|
59
60
|
grant_type: "refresh_token",
|
|
60
61
|
refresh_token: refreshToken,
|
|
61
62
|
client_id: config.linear.oauth.clientId,
|
|
62
63
|
client_secret: config.linear.oauth.clientSecret,
|
|
63
|
-
}),
|
|
64
|
+
}).toString(),
|
|
64
65
|
});
|
|
65
66
|
const payload = (await response.json().catch(() => undefined));
|
|
66
67
|
if (!response.ok || !payload) {
|
|
@@ -5,7 +5,6 @@ import { syncFailedStageToLinear } from "./stage-failure.js";
|
|
|
5
5
|
import { buildFailedStageReport, buildPendingMaterializationThread, buildStageReport, countEventMethods, extractStageSummary, extractTurnId, resolveStageRunStatus, summarizeCurrentThread, } from "./stage-reporting.js";
|
|
6
6
|
import { StageLifecyclePublisher } from "./stage-lifecycle-publisher.js";
|
|
7
7
|
import { StageTurnInputDispatcher } from "./stage-turn-input-dispatcher.js";
|
|
8
|
-
import { safeJsonParse } from "./utils.js";
|
|
9
8
|
export class ServiceStageFinalizer {
|
|
10
9
|
config;
|
|
11
10
|
stores;
|
|
@@ -115,7 +114,7 @@ export class ServiceStageFinalizer {
|
|
|
115
114
|
...(params.turnId ? { turnId: params.turnId } : {}),
|
|
116
115
|
nextLifecycleStatus: params.nextLifecycleStatus ?? (issue.desiredStage ? "queued" : "completed"),
|
|
117
116
|
});
|
|
118
|
-
this.stores.
|
|
117
|
+
this.stores.workflowCoordinator.finishStageRun({
|
|
119
118
|
stageRunId: stageRun.id,
|
|
120
119
|
status,
|
|
121
120
|
threadId: params.threadId,
|
|
@@ -134,7 +133,7 @@ export class ServiceStageFinalizer {
|
|
|
134
133
|
failureReason: message,
|
|
135
134
|
nextLifecycleStatus: "failed",
|
|
136
135
|
});
|
|
137
|
-
this.stores.
|
|
136
|
+
this.stores.workflowCoordinator.finishStageRun({
|
|
138
137
|
stageRunId: stageRun.id,
|
|
139
138
|
status: "failed",
|
|
140
139
|
threadId,
|
|
@@ -45,7 +45,7 @@ export class ServiceStageRunner {
|
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
const plan = buildStageLaunchPlan(project, issue, desiredStage);
|
|
48
|
-
const claim = this.stores.
|
|
48
|
+
const claim = this.stores.workflowCoordinator.claimStageRun({
|
|
49
49
|
projectId: item.projectId,
|
|
50
50
|
linearIssueId: item.issueId,
|
|
51
51
|
stage: desiredStage,
|
|
@@ -83,7 +83,7 @@ export class ServiceStageRunner {
|
|
|
83
83
|
}, "Failed to launch Codex stage run");
|
|
84
84
|
throw err;
|
|
85
85
|
}
|
|
86
|
-
this.stores.
|
|
86
|
+
this.stores.workflowCoordinator.updateStageRunThread({
|
|
87
87
|
stageRunId: claim.stageRun.id,
|
|
88
88
|
threadId: threadLaunch.threadId,
|
|
89
89
|
...(threadLaunch.parentThreadId ? { parentThreadId: threadLaunch.parentThreadId } : {}),
|
|
@@ -121,7 +121,7 @@ export class ServiceStageRunner {
|
|
|
121
121
|
.forProject(project.id)
|
|
122
122
|
.then((linear) => linear?.getIssue(linearIssueId))
|
|
123
123
|
.catch(() => undefined);
|
|
124
|
-
return this.stores.
|
|
124
|
+
return this.stores.workflowCoordinator.recordDesiredStage({
|
|
125
125
|
projectId: project.id,
|
|
126
126
|
linearIssueId,
|
|
127
127
|
...(liveIssue?.identifier ? { issueKey: liveIssue.identifier } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
|
|
@@ -168,7 +168,7 @@ export class ServiceStageRunner {
|
|
|
168
168
|
async markLaunchFailed(project, issue, stageRun, message, threadId) {
|
|
169
169
|
const failureThreadId = threadId ?? `launch-failed-${stageRun.id}`;
|
|
170
170
|
this.runAtomically(() => {
|
|
171
|
-
this.stores.
|
|
171
|
+
this.stores.workflowCoordinator.finishStageRun({
|
|
172
172
|
stageRunId: stageRun.id,
|
|
173
173
|
status: "failed",
|
|
174
174
|
threadId: failureThreadId,
|
package/dist/service.js
CHANGED
|
@@ -13,6 +13,7 @@ function createServiceStores(db) {
|
|
|
13
13
|
workspaceOwnership: db.workspaceOwnership,
|
|
14
14
|
runLeases: db.runLeases,
|
|
15
15
|
obligations: db.obligations,
|
|
16
|
+
workflowCoordinator: db.workflowCoordinator,
|
|
16
17
|
issueWorkflows: db.issueWorkflows,
|
|
17
18
|
stageEvents: db.stageEvents,
|
|
18
19
|
linearInstallations: db.linearInstallations,
|
package/dist/stage-failure.js
CHANGED
|
@@ -39,21 +39,14 @@ export async function syncFailedStageToLinear(params) {
|
|
|
39
39
|
}
|
|
40
40
|
if (fallbackState) {
|
|
41
41
|
await linear.setIssueState(params.stageRun.linearIssueId, fallbackState).catch(() => undefined);
|
|
42
|
-
params.stores.
|
|
43
|
-
params.stores.issueWorkflows.upsertTrackedIssue({
|
|
42
|
+
params.stores.workflowCoordinator.upsertTrackedIssue({
|
|
44
43
|
projectId: params.stageRun.projectId,
|
|
45
44
|
linearIssueId: params.stageRun.linearIssueId,
|
|
46
45
|
currentLinearState: fallbackState,
|
|
47
46
|
statusCommentId: params.issue.statusCommentId ?? null,
|
|
47
|
+
activeAgentSessionId: params.issue.activeAgentSessionId ?? null,
|
|
48
48
|
lifecycleStatus: "failed",
|
|
49
49
|
});
|
|
50
|
-
params.stores.issueControl.upsertIssueControl({
|
|
51
|
-
projectId: params.stageRun.projectId,
|
|
52
|
-
linearIssueId: params.stageRun.linearIssueId,
|
|
53
|
-
lifecycleStatus: "failed",
|
|
54
|
-
...(params.issue.statusCommentId ? { serviceOwnedCommentId: params.issue.statusCommentId } : {}),
|
|
55
|
-
...(params.issue.activeAgentSessionId ? { activeAgentSessionId: params.issue.activeAgentSessionId } : {}),
|
|
56
|
-
});
|
|
57
50
|
}
|
|
58
51
|
const result = await linear
|
|
59
52
|
.upsertIssueComment({
|
|
@@ -69,14 +62,7 @@ export async function syncFailedStageToLinear(params) {
|
|
|
69
62
|
})
|
|
70
63
|
.catch(() => undefined);
|
|
71
64
|
if (result) {
|
|
72
|
-
params.stores.
|
|
73
|
-
params.stores.issueControl.upsertIssueControl({
|
|
74
|
-
projectId: params.stageRun.projectId,
|
|
75
|
-
linearIssueId: params.stageRun.linearIssueId,
|
|
76
|
-
serviceOwnedCommentId: result.id,
|
|
77
|
-
lifecycleStatus: "failed",
|
|
78
|
-
...(params.issue.activeAgentSessionId ? { activeAgentSessionId: params.issue.activeAgentSessionId } : {}),
|
|
79
|
-
});
|
|
65
|
+
params.stores.workflowCoordinator.setIssueStatusComment(params.stageRun.projectId, params.stageRun.linearIssueId, result.id);
|
|
80
66
|
}
|
|
81
67
|
if (params.issue.activeAgentSessionId) {
|
|
82
68
|
await linear
|
|
@@ -26,18 +26,12 @@ export class StageLifecyclePublisher {
|
|
|
26
26
|
...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
this.stores.
|
|
29
|
+
this.stores.workflowCoordinator.upsertTrackedIssue({
|
|
30
30
|
projectId: stageRun.projectId,
|
|
31
31
|
linearIssueId: stageRun.linearIssueId,
|
|
32
32
|
currentLinearState: activeState,
|
|
33
33
|
statusCommentId: issue.statusCommentId ?? null,
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
this.stores.issueControl.upsertIssueControl({
|
|
37
|
-
projectId: stageRun.projectId,
|
|
38
|
-
linearIssueId: stageRun.linearIssueId,
|
|
39
|
-
...(issue.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
|
|
40
|
-
...(issue.activeAgentSessionId ? { activeAgentSessionId: issue.activeAgentSessionId } : {}),
|
|
34
|
+
activeAgentSessionId: issue.activeAgentSessionId ?? null,
|
|
41
35
|
lifecycleStatus: "running",
|
|
42
36
|
});
|
|
43
37
|
}
|
|
@@ -62,13 +56,7 @@ export class StageLifecyclePublisher {
|
|
|
62
56
|
branchName: workspace.branchName,
|
|
63
57
|
}),
|
|
64
58
|
});
|
|
65
|
-
this.stores.
|
|
66
|
-
this.stores.issueControl.upsertIssueControl({
|
|
67
|
-
projectId,
|
|
68
|
-
linearIssueId: issueId,
|
|
69
|
-
serviceOwnedCommentId: result.id,
|
|
70
|
-
lifecycleStatus: issue.lifecycleStatus,
|
|
71
|
-
});
|
|
59
|
+
this.stores.workflowCoordinator.setIssueStatusComment(projectId, issueId, result.id);
|
|
72
60
|
}
|
|
73
61
|
catch (error) {
|
|
74
62
|
this.logger.warn({
|
|
@@ -133,12 +121,7 @@ export class StageLifecyclePublisher {
|
|
|
133
121
|
...(labels.remove.length > 0 ? { removeNames: labels.remove } : {}),
|
|
134
122
|
});
|
|
135
123
|
}
|
|
136
|
-
this.stores.
|
|
137
|
-
this.stores.issueControl.upsertIssueControl({
|
|
138
|
-
projectId: stageRun.projectId,
|
|
139
|
-
linearIssueId: stageRun.linearIssueId,
|
|
140
|
-
lifecycleStatus: "paused",
|
|
141
|
-
});
|
|
124
|
+
this.stores.workflowCoordinator.setIssueLifecycleStatus(stageRun.projectId, stageRun.linearIssueId, "paused");
|
|
142
125
|
const finalStageRun = this.stores.issueWorkflows.getStageRun(stageRun.id) ?? stageRun;
|
|
143
126
|
const result = await linear.upsertIssueComment({
|
|
144
127
|
issueId: stageRun.linearIssueId,
|
|
@@ -149,13 +132,7 @@ export class StageLifecyclePublisher {
|
|
|
149
132
|
activeState,
|
|
150
133
|
}),
|
|
151
134
|
});
|
|
152
|
-
this.stores.
|
|
153
|
-
this.stores.issueControl.upsertIssueControl({
|
|
154
|
-
projectId: stageRun.projectId,
|
|
155
|
-
linearIssueId: stageRun.linearIssueId,
|
|
156
|
-
serviceOwnedCommentId: result.id,
|
|
157
|
-
lifecycleStatus: "paused",
|
|
158
|
-
});
|
|
135
|
+
this.stores.workflowCoordinator.setIssueStatusComment(stageRun.projectId, stageRun.linearIssueId, result.id);
|
|
159
136
|
await this.publishAgentCompletion(refreshedIssue, {
|
|
160
137
|
type: "elicitation",
|
|
161
138
|
body: `PatchRelay finished the ${stageRun.stage} workflow. Move the issue to its next workflow state or leave a follow-up prompt to continue.`,
|
|
@@ -27,19 +27,18 @@ export class WebhookDesiredStageRecorder {
|
|
|
27
27
|
const stageAllowed = triggerEventAllowed(project, normalized.triggerEvent);
|
|
28
28
|
const desiredStage = this.resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay);
|
|
29
29
|
const launchInput = this.resolveLaunchInput(normalized.agentSession);
|
|
30
|
-
this.
|
|
31
|
-
this.stores.issueWorkflows.recordDesiredStage({
|
|
30
|
+
const refreshedIssue = this.stores.workflowCoordinator.recordDesiredStage({
|
|
32
31
|
projectId: project.id,
|
|
33
32
|
linearIssueId: normalizedIssue.id,
|
|
34
33
|
...(normalizedIssue.identifier ? { issueKey: normalizedIssue.identifier } : {}),
|
|
35
34
|
...(normalizedIssue.title ? { title: normalizedIssue.title } : {}),
|
|
36
35
|
...(normalizedIssue.url ? { issueUrl: normalizedIssue.url } : {}),
|
|
37
36
|
...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
|
|
37
|
+
...(desiredStage ? { desiredStage } : {}),
|
|
38
|
+
...(options?.eventReceiptId !== undefined ? { desiredReceiptId: options.eventReceiptId } : {}),
|
|
39
|
+
...(normalized.agentSession?.id ? { activeAgentSessionId: normalized.agentSession.id } : {}),
|
|
38
40
|
lastWebhookAt: new Date().toISOString(),
|
|
39
41
|
});
|
|
40
|
-
if (normalized.agentSession?.id) {
|
|
41
|
-
this.stores.issueWorkflows.setIssueActiveAgentSession(project.id, normalizedIssue.id, normalized.agentSession.id);
|
|
42
|
-
}
|
|
43
42
|
if (launchInput && !activeStageRun && delegatedToPatchRelay && stageAllowed) {
|
|
44
43
|
this.stores.obligations.enqueueObligation({
|
|
45
44
|
projectId: project.id,
|
|
@@ -51,8 +50,6 @@ export class WebhookDesiredStageRecorder {
|
|
|
51
50
|
}),
|
|
52
51
|
});
|
|
53
52
|
}
|
|
54
|
-
const refreshedIssue = this.stores.issueWorkflows.getTrackedIssue(project.id, normalizedIssue.id);
|
|
55
|
-
this.syncIssueControl(project.id, normalizedIssue.id, refreshedIssue, desiredStage, normalized.agentSession?.id, options?.eventReceiptId);
|
|
56
53
|
return {
|
|
57
54
|
issue: refreshedIssue ?? issue,
|
|
58
55
|
activeStageRun,
|
|
@@ -119,32 +116,4 @@ export class WebhookDesiredStageRecorder {
|
|
|
119
116
|
}
|
|
120
117
|
return undefined;
|
|
121
118
|
}
|
|
122
|
-
persistIssueControlFirst(projectId, linearIssueId, issue, activeStageRun, desiredStage, activeAgentSessionId, eventReceiptId) {
|
|
123
|
-
if (!desiredStage) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const lifecycleStatus = issue?.lifecycleStatus ?? "queued";
|
|
127
|
-
this.stores.issueControl.upsertIssueControl({
|
|
128
|
-
projectId,
|
|
129
|
-
linearIssueId,
|
|
130
|
-
desiredStage,
|
|
131
|
-
...(eventReceiptId !== undefined ? { desiredReceiptId: eventReceiptId } : {}),
|
|
132
|
-
...(issue?.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
|
|
133
|
-
...(activeAgentSessionId ? { activeAgentSessionId } : {}),
|
|
134
|
-
lifecycleStatus,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
syncIssueControl(projectId, linearIssueId, issue, desiredStage, activeAgentSessionId, eventReceiptId) {
|
|
138
|
-
if (!issue) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
this.stores.issueControl.upsertIssueControl({
|
|
142
|
-
projectId,
|
|
143
|
-
linearIssueId,
|
|
144
|
-
...(desiredStage ? { desiredStage } : {}),
|
|
145
|
-
...(eventReceiptId !== undefined && desiredStage ? { desiredReceiptId: eventReceiptId } : {}),
|
|
146
|
-
...(activeAgentSessionId ? { activeAgentSessionId } : {}),
|
|
147
|
-
lifecycleStatus: issue.lifecycleStatus,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
119
|
}
|
package/infra/patchrelay.path
CHANGED
|
@@ -6,6 +6,8 @@ Unit=patchrelay-reload.service
|
|
|
6
6
|
PathChanged=/home/your-user/.config/patchrelay/runtime.env
|
|
7
7
|
PathChanged=/home/your-user/.config/patchrelay/service.env
|
|
8
8
|
PathChanged=/home/your-user/.config/patchrelay/patchrelay.json
|
|
9
|
+
TriggerLimitIntervalSec=5
|
|
10
|
+
TriggerLimitBurst=1
|
|
9
11
|
|
|
10
12
|
[Install]
|
|
11
13
|
WantedBy=default.target
|
package/infra/patchrelay.service
CHANGED