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
package/dist/db/migrations.js
CHANGED
|
@@ -91,6 +91,23 @@ CREATE TABLE IF NOT EXISTS run_leases (
|
|
|
91
91
|
FOREIGN KEY(trigger_receipt_id) REFERENCES event_receipts(id) ON DELETE SET NULL
|
|
92
92
|
);
|
|
93
93
|
|
|
94
|
+
CREATE TABLE IF NOT EXISTS issue_sessions (
|
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
project_id TEXT NOT NULL,
|
|
97
|
+
linear_issue_id TEXT NOT NULL,
|
|
98
|
+
workspace_ownership_id INTEGER NOT NULL,
|
|
99
|
+
run_lease_id INTEGER,
|
|
100
|
+
thread_id TEXT NOT NULL UNIQUE,
|
|
101
|
+
parent_thread_id TEXT,
|
|
102
|
+
source TEXT NOT NULL,
|
|
103
|
+
linked_agent_session_id TEXT,
|
|
104
|
+
created_at TEXT NOT NULL,
|
|
105
|
+
updated_at TEXT NOT NULL,
|
|
106
|
+
last_opened_at TEXT,
|
|
107
|
+
FOREIGN KEY(workspace_ownership_id) REFERENCES workspace_ownership(id) ON DELETE CASCADE,
|
|
108
|
+
FOREIGN KEY(run_lease_id) REFERENCES run_leases(id) ON DELETE SET NULL
|
|
109
|
+
);
|
|
110
|
+
|
|
94
111
|
CREATE TABLE IF NOT EXISTS run_reports (
|
|
95
112
|
run_lease_id INTEGER PRIMARY KEY,
|
|
96
113
|
summary_json TEXT,
|
|
@@ -170,6 +187,8 @@ CREATE TABLE IF NOT EXISTS oauth_states (
|
|
|
170
187
|
CREATE INDEX IF NOT EXISTS idx_event_receipts_project_issue ON event_receipts(project_id, linear_issue_id);
|
|
171
188
|
CREATE INDEX IF NOT EXISTS idx_issue_control_ready ON issue_control(desired_stage, active_run_lease_id);
|
|
172
189
|
CREATE INDEX IF NOT EXISTS idx_issue_projection_issue_key ON issue_projection(issue_key);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_issue_sessions_issue ON issue_sessions(project_id, linear_issue_id, id DESC);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_issue_sessions_last_opened ON issue_sessions(project_id, linear_issue_id, last_opened_at DESC, id DESC);
|
|
173
192
|
CREATE INDEX IF NOT EXISTS idx_run_leases_active ON run_leases(status, project_id, linear_issue_id);
|
|
174
193
|
CREATE INDEX IF NOT EXISTS idx_run_leases_thread ON run_leases(thread_id);
|
|
175
194
|
CREATE INDEX IF NOT EXISTS idx_run_thread_events_run ON run_thread_events(run_lease_id, id);
|
|
@@ -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";
|
|
@@ -11,10 +14,14 @@ export class PatchRelayDatabase {
|
|
|
11
14
|
eventReceipts;
|
|
12
15
|
issueControl;
|
|
13
16
|
workspaceOwnership;
|
|
17
|
+
issueSessions;
|
|
14
18
|
runLeases;
|
|
15
19
|
obligations;
|
|
16
20
|
webhookEvents;
|
|
21
|
+
issueProjections;
|
|
17
22
|
issueWorkflows;
|
|
23
|
+
workflowCoordinator;
|
|
24
|
+
runReports;
|
|
18
25
|
stageEvents;
|
|
19
26
|
linearInstallations;
|
|
20
27
|
constructor(databasePath, wal) {
|
|
@@ -27,10 +34,24 @@ export class PatchRelayDatabase {
|
|
|
27
34
|
this.eventReceipts = this.authoritativeLedger;
|
|
28
35
|
this.issueControl = this.authoritativeLedger;
|
|
29
36
|
this.workspaceOwnership = this.authoritativeLedger;
|
|
37
|
+
this.issueSessions = this.authoritativeLedger;
|
|
30
38
|
this.runLeases = this.authoritativeLedger;
|
|
31
39
|
this.obligations = this.authoritativeLedger;
|
|
32
40
|
this.webhookEvents = new WebhookEventStore(this.connection);
|
|
33
|
-
this.
|
|
41
|
+
this.issueProjections = new IssueProjectionStore(this.connection);
|
|
42
|
+
this.runReports = new RunReportStore(this.connection);
|
|
43
|
+
this.issueWorkflows = new IssueWorkflowStore({
|
|
44
|
+
authoritativeLedger: this.authoritativeLedger,
|
|
45
|
+
issueProjections: this.issueProjections,
|
|
46
|
+
runReports: this.runReports,
|
|
47
|
+
});
|
|
48
|
+
this.workflowCoordinator = new IssueWorkflowCoordinator({
|
|
49
|
+
connection: this.connection,
|
|
50
|
+
authoritativeLedger: this.authoritativeLedger,
|
|
51
|
+
issueProjections: this.issueProjections,
|
|
52
|
+
issueWorkflows: this.issueWorkflows,
|
|
53
|
+
runReports: this.runReports,
|
|
54
|
+
});
|
|
34
55
|
this.stageEvents = new StageEventStore(this.connection);
|
|
35
56
|
this.linearInstallations = new LinearInstallationStore(this.connection);
|
|
36
57
|
}
|
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) {
|
|
@@ -114,7 +114,7 @@ export class ServiceStageFinalizer {
|
|
|
114
114
|
...(params.turnId ? { turnId: params.turnId } : {}),
|
|
115
115
|
nextLifecycleStatus: params.nextLifecycleStatus ?? (issue.desiredStage ? "queued" : "completed"),
|
|
116
116
|
});
|
|
117
|
-
this.stores.
|
|
117
|
+
this.stores.workflowCoordinator.finishStageRun({
|
|
118
118
|
stageRunId: stageRun.id,
|
|
119
119
|
status,
|
|
120
120
|
threadId: params.threadId,
|
|
@@ -133,7 +133,7 @@ export class ServiceStageFinalizer {
|
|
|
133
133
|
failureReason: message,
|
|
134
134
|
nextLifecycleStatus: "failed",
|
|
135
135
|
});
|
|
136
|
-
this.stores.
|
|
136
|
+
this.stores.workflowCoordinator.finishStageRun({
|
|
137
137
|
stageRunId: stageRun.id,
|
|
138
138
|
status: "failed",
|
|
139
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