patchrelay 0.8.9 → 0.9.1
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/README.md +64 -62
- package/dist/agent-session-plan.js +17 -17
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +1 -1
- package/dist/cli/commands/issues.js +18 -18
- package/dist/cli/data.js +109 -298
- package/dist/cli/formatters/text.js +22 -28
- package/dist/cli/help.js +7 -7
- package/dist/cli/index.js +3 -3
- package/dist/config.js +13 -166
- package/dist/db/migrations.js +46 -154
- package/dist/db.js +369 -45
- package/dist/factory-state.js +55 -0
- package/dist/github-webhook-handler.js +199 -0
- package/dist/github-webhooks.js +166 -0
- package/dist/hook-runner.js +28 -0
- package/dist/http.js +48 -22
- package/dist/issue-query-service.js +33 -38
- package/dist/linear-workflow.js +5 -118
- package/dist/preflight.js +1 -6
- package/dist/project-resolution.js +12 -1
- package/dist/run-orchestrator.js +446 -0
- package/dist/{stage-reporting.js → run-reporting.js} +11 -13
- package/dist/service-runtime.js +12 -61
- package/dist/service-webhooks.js +7 -52
- package/dist/service.js +39 -61
- package/dist/webhook-handler.js +387 -0
- package/dist/webhook-installation-handler.js +3 -8
- package/package.json +2 -1
- package/dist/db/authoritative-ledger-store.js +0 -536
- package/dist/db/issue-projection-store.js +0 -54
- package/dist/db/issue-workflow-coordinator.js +0 -320
- package/dist/db/issue-workflow-store.js +0 -194
- package/dist/db/run-report-store.js +0 -33
- package/dist/db/stage-event-store.js +0 -33
- package/dist/db/webhook-event-store.js +0 -59
- package/dist/db-ports.js +0 -5
- package/dist/ledger-ports.js +0 -1
- package/dist/reconciliation-action-applier.js +0 -68
- package/dist/reconciliation-actions.js +0 -1
- package/dist/reconciliation-engine.js +0 -350
- package/dist/reconciliation-snapshot-builder.js +0 -135
- package/dist/reconciliation-types.js +0 -1
- package/dist/service-stage-finalizer.js +0 -753
- package/dist/service-stage-runner.js +0 -336
- package/dist/service-webhook-processor.js +0 -411
- package/dist/stage-agent-activity-publisher.js +0 -59
- package/dist/stage-event-ports.js +0 -1
- package/dist/stage-failure.js +0 -92
- package/dist/stage-handoff.js +0 -107
- package/dist/stage-launch.js +0 -84
- package/dist/stage-lifecycle-publisher.js +0 -284
- package/dist/stage-turn-input-dispatcher.js +0 -104
- package/dist/webhook-agent-session-handler.js +0 -228
- package/dist/webhook-comment-handler.js +0 -141
- package/dist/webhook-desired-stage-recorder.js +0 -122
- package/dist/webhook-event-ports.js +0 -1
- package/dist/workflow-policy.js +0 -149
- package/dist/workflow-ports.js +0 -1
- /package/dist/{installation-ports.js → github-types.js} +0 -0
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
import { resolveProject, trustedActorAllowed } from "./project-resolution.js";
|
|
2
|
-
import { StageAgentActivityPublisher } from "./stage-agent-activity-publisher.js";
|
|
3
|
-
import { StageTurnInputDispatcher } from "./stage-turn-input-dispatcher.js";
|
|
4
|
-
import { safeJsonParse, sanitizeDiagnosticText } from "./utils.js";
|
|
5
|
-
import { AgentSessionWebhookHandler } from "./webhook-agent-session-handler.js";
|
|
6
|
-
import { CommentWebhookHandler } from "./webhook-comment-handler.js";
|
|
7
|
-
import { WebhookDesiredStageRecorder } from "./webhook-desired-stage-recorder.js";
|
|
8
|
-
import { InstallationWebhookHandler } from "./webhook-installation-handler.js";
|
|
9
|
-
import { normalizeWebhook } from "./webhooks.js";
|
|
10
|
-
export class ServiceWebhookProcessor {
|
|
11
|
-
config;
|
|
12
|
-
stores;
|
|
13
|
-
linearProvider;
|
|
14
|
-
enqueueIssue;
|
|
15
|
-
logger;
|
|
16
|
-
feed;
|
|
17
|
-
desiredStageRecorder;
|
|
18
|
-
agentSessionHandler;
|
|
19
|
-
commentHandler;
|
|
20
|
-
installationHandler;
|
|
21
|
-
constructor(config, stores, linearProvider, codex, enqueueIssue, logger, feed) {
|
|
22
|
-
this.config = config;
|
|
23
|
-
this.stores = stores;
|
|
24
|
-
this.linearProvider = linearProvider;
|
|
25
|
-
this.enqueueIssue = enqueueIssue;
|
|
26
|
-
this.logger = logger;
|
|
27
|
-
this.feed = feed;
|
|
28
|
-
const turnInputDispatcher = new StageTurnInputDispatcher(stores, codex, logger);
|
|
29
|
-
const agentActivity = new StageAgentActivityPublisher(config, linearProvider, logger);
|
|
30
|
-
this.desiredStageRecorder = new WebhookDesiredStageRecorder(stores);
|
|
31
|
-
this.agentSessionHandler = new AgentSessionWebhookHandler(stores, turnInputDispatcher, agentActivity, feed);
|
|
32
|
-
this.commentHandler = new CommentWebhookHandler(stores, turnInputDispatcher, feed);
|
|
33
|
-
this.installationHandler = new InstallationWebhookHandler(config, stores, logger);
|
|
34
|
-
}
|
|
35
|
-
async processWebhookEvent(webhookEventId) {
|
|
36
|
-
const event = this.stores.webhookEvents.getWebhookEvent(webhookEventId);
|
|
37
|
-
if (!event) {
|
|
38
|
-
this.logger.warn({ webhookEventId }, "Webhook event was not found during processing");
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
const payload = safeJsonParse(event.payloadJson);
|
|
43
|
-
if (!payload) {
|
|
44
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "failed");
|
|
45
|
-
this.markEventReceiptProcessed(event.webhookId, "failed");
|
|
46
|
-
throw new Error(`Stored webhook payload is invalid JSON: event ${webhookEventId}`);
|
|
47
|
-
}
|
|
48
|
-
let normalized = normalizeWebhook({
|
|
49
|
-
webhookId: event.webhookId,
|
|
50
|
-
payload,
|
|
51
|
-
});
|
|
52
|
-
this.logger.info({
|
|
53
|
-
webhookEventId,
|
|
54
|
-
webhookId: event.webhookId,
|
|
55
|
-
eventType: normalized.eventType,
|
|
56
|
-
triggerEvent: normalized.triggerEvent,
|
|
57
|
-
issueKey: normalized.issue?.identifier,
|
|
58
|
-
issueId: normalized.issue?.id,
|
|
59
|
-
}, "Processing stored webhook event");
|
|
60
|
-
if (!normalized.issue) {
|
|
61
|
-
this.feed?.publish({
|
|
62
|
-
level: "info",
|
|
63
|
-
kind: "webhook",
|
|
64
|
-
status: normalized.triggerEvent,
|
|
65
|
-
summary: `Received ${normalized.triggerEvent} webhook`,
|
|
66
|
-
});
|
|
67
|
-
this.installationHandler.handle(normalized);
|
|
68
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
69
|
-
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
let project = resolveProject(this.config, normalized.issue);
|
|
73
|
-
if (!project) {
|
|
74
|
-
const routed = await this.tryHydrateProjectRoute(normalized);
|
|
75
|
-
if (routed) {
|
|
76
|
-
normalized = routed.normalized;
|
|
77
|
-
project = routed.project;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!project) {
|
|
81
|
-
const unresolvedIssue = normalized.issue;
|
|
82
|
-
if (!unresolvedIssue) {
|
|
83
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "failed");
|
|
84
|
-
this.markEventReceiptProcessed(event.webhookId, "failed");
|
|
85
|
-
throw new Error(`Normalized issue context disappeared before routing webhook ${event.webhookId}`);
|
|
86
|
-
}
|
|
87
|
-
this.feed?.publish({
|
|
88
|
-
level: "warn",
|
|
89
|
-
kind: "webhook",
|
|
90
|
-
issueKey: unresolvedIssue.identifier,
|
|
91
|
-
status: "ignored",
|
|
92
|
-
summary: "Ignored webhook with no matching project route",
|
|
93
|
-
detail: normalized.triggerEvent,
|
|
94
|
-
});
|
|
95
|
-
this.logger.info({
|
|
96
|
-
webhookEventId,
|
|
97
|
-
webhookId: event.webhookId,
|
|
98
|
-
issueKey: unresolvedIssue.identifier,
|
|
99
|
-
issueId: unresolvedIssue.id,
|
|
100
|
-
teamId: unresolvedIssue.teamId,
|
|
101
|
-
teamKey: unresolvedIssue.teamKey,
|
|
102
|
-
triggerEvent: normalized.triggerEvent,
|
|
103
|
-
}, "Ignoring webhook because no project route matched the Linear issue");
|
|
104
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
105
|
-
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const routedIssue = normalized.issue;
|
|
109
|
-
if (!routedIssue) {
|
|
110
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "failed");
|
|
111
|
-
this.markEventReceiptProcessed(event.webhookId, "failed");
|
|
112
|
-
throw new Error(`Normalized issue context disappeared while routing webhook ${event.webhookId}`);
|
|
113
|
-
}
|
|
114
|
-
if (!trustedActorAllowed(project, normalized.actor)) {
|
|
115
|
-
this.feed?.publish({
|
|
116
|
-
level: "warn",
|
|
117
|
-
kind: "webhook",
|
|
118
|
-
issueKey: routedIssue.identifier,
|
|
119
|
-
projectId: project.id,
|
|
120
|
-
status: "ignored",
|
|
121
|
-
summary: "Ignored webhook from an untrusted actor",
|
|
122
|
-
detail: normalized.actor?.name ?? normalized.actor?.email ?? normalized.triggerEvent,
|
|
123
|
-
});
|
|
124
|
-
this.logger.info({
|
|
125
|
-
webhookId: normalized.webhookId,
|
|
126
|
-
projectId: project.id,
|
|
127
|
-
triggerEvent: normalized.triggerEvent,
|
|
128
|
-
actorId: normalized.actor?.id,
|
|
129
|
-
actorName: normalized.actor?.name,
|
|
130
|
-
actorEmail: normalized.actor?.email,
|
|
131
|
-
}, "Ignoring webhook from untrusted Linear actor");
|
|
132
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
133
|
-
this.assignEventReceiptContext(event.webhookId, project.id, routedIssue.id);
|
|
134
|
-
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
this.stores.webhookEvents.assignWebhookProject(webhookEventId, project.id);
|
|
138
|
-
const receipt = this.ensureEventReceipt(event, project.id, routedIssue.id);
|
|
139
|
-
const hydrated = await this.hydrateIssueContext(project.id, normalized);
|
|
140
|
-
const hydratedIssue = hydrated.issue ?? routedIssue;
|
|
141
|
-
const priorIssue = this.stores.issueWorkflows.getTrackedIssue(project.id, hydratedIssue.id);
|
|
142
|
-
const issueState = this.desiredStageRecorder.record(project, hydrated, receipt ? { eventReceiptId: receipt.id } : undefined);
|
|
143
|
-
const observation = describeWebhookObservation(hydrated, issueState.delegatedToPatchRelay);
|
|
144
|
-
if (observation) {
|
|
145
|
-
this.feed?.publish({
|
|
146
|
-
level: "info",
|
|
147
|
-
kind: observation.kind,
|
|
148
|
-
issueKey: hydratedIssue.identifier,
|
|
149
|
-
projectId: project.id,
|
|
150
|
-
...(issueState.issue?.selectedWorkflowId ? { workflowId: issueState.issue.selectedWorkflowId } : {}),
|
|
151
|
-
...(observation.status ? { status: observation.status } : {}),
|
|
152
|
-
summary: observation.summary,
|
|
153
|
-
...(observation.detail ? { detail: observation.detail } : {}),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
if (issueState.issue?.selectedWorkflowId &&
|
|
157
|
-
issueState.issue.selectedWorkflowId !== priorIssue?.selectedWorkflowId) {
|
|
158
|
-
this.feed?.publish({
|
|
159
|
-
level: "info",
|
|
160
|
-
kind: "workflow",
|
|
161
|
-
issueKey: hydratedIssue.identifier,
|
|
162
|
-
projectId: project.id,
|
|
163
|
-
workflowId: issueState.issue.selectedWorkflowId,
|
|
164
|
-
...(issueState.desiredStage ? { nextStage: issueState.desiredStage } : {}),
|
|
165
|
-
status: "selected",
|
|
166
|
-
summary: `Selected ${issueState.issue.selectedWorkflowId} workflow`,
|
|
167
|
-
detail: issueState.desiredStage
|
|
168
|
-
? `PatchRelay will start with ${issueState.desiredStage} from ${hydratedIssue.stateName ?? "the current Linear state"}.`
|
|
169
|
-
: undefined,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
await this.agentSessionHandler.handle({
|
|
173
|
-
normalized: hydrated,
|
|
174
|
-
project,
|
|
175
|
-
issue: issueState.issue,
|
|
176
|
-
desiredStage: issueState.desiredStage,
|
|
177
|
-
delegatedToPatchRelay: issueState.delegatedToPatchRelay,
|
|
178
|
-
});
|
|
179
|
-
await this.commentHandler.handle(hydrated, project);
|
|
180
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
|
|
181
|
-
this.markEventReceiptProcessed(event.webhookId, "processed");
|
|
182
|
-
if (issueState.desiredStage) {
|
|
183
|
-
this.feed?.publish({
|
|
184
|
-
level: "info",
|
|
185
|
-
kind: "stage",
|
|
186
|
-
issueKey: hydratedIssue.identifier,
|
|
187
|
-
projectId: project.id,
|
|
188
|
-
stage: issueState.desiredStage,
|
|
189
|
-
...(issueState.issue?.selectedWorkflowId ? { workflowId: issueState.issue.selectedWorkflowId } : {}),
|
|
190
|
-
status: "queued",
|
|
191
|
-
summary: `Queued ${issueState.desiredStage} workflow`,
|
|
192
|
-
detail: `Triggered by ${hydrated.triggerEvent}${hydratedIssue.stateName ? ` from ${hydratedIssue.stateName}` : ""}.`,
|
|
193
|
-
});
|
|
194
|
-
this.logger.info({
|
|
195
|
-
webhookEventId,
|
|
196
|
-
webhookId: event.webhookId,
|
|
197
|
-
projectId: project.id,
|
|
198
|
-
issueKey: hydratedIssue.identifier,
|
|
199
|
-
issueId: hydratedIssue.id,
|
|
200
|
-
desiredStage: issueState.desiredStage,
|
|
201
|
-
delegatedToPatchRelay: issueState.delegatedToPatchRelay,
|
|
202
|
-
}, "Recorded desired stage from webhook and enqueued issue execution");
|
|
203
|
-
this.enqueueIssue(project.id, hydratedIssue.id);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
this.logger.info({
|
|
207
|
-
webhookEventId,
|
|
208
|
-
webhookId: event.webhookId,
|
|
209
|
-
projectId: project.id,
|
|
210
|
-
issueKey: hydratedIssue.identifier,
|
|
211
|
-
issueId: hydratedIssue.id,
|
|
212
|
-
triggerEvent: hydrated.triggerEvent,
|
|
213
|
-
delegatedToPatchRelay: issueState.delegatedToPatchRelay,
|
|
214
|
-
}, "Processed webhook without enqueuing a new stage run");
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "failed");
|
|
218
|
-
this.markEventReceiptProcessed(event.webhookId, "failed");
|
|
219
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
220
|
-
this.feed?.publish({
|
|
221
|
-
level: "error",
|
|
222
|
-
kind: "webhook",
|
|
223
|
-
projectId: event.projectId ?? undefined,
|
|
224
|
-
status: "failed",
|
|
225
|
-
summary: "Failed to process webhook",
|
|
226
|
-
detail: sanitizeDiagnosticText(err.message),
|
|
227
|
-
});
|
|
228
|
-
this.logger.error({
|
|
229
|
-
webhookEventId,
|
|
230
|
-
webhookId: event.webhookId,
|
|
231
|
-
issueId: event.issueId,
|
|
232
|
-
projectId: event.projectId,
|
|
233
|
-
error: sanitizeDiagnosticText(err.message),
|
|
234
|
-
stack: err.stack,
|
|
235
|
-
}, "Failed to process Linear webhook event");
|
|
236
|
-
throw err;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
async hydrateIssueContext(projectId, normalized) {
|
|
240
|
-
if (!normalized.issue) {
|
|
241
|
-
return normalized;
|
|
242
|
-
}
|
|
243
|
-
if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted") {
|
|
244
|
-
return normalized;
|
|
245
|
-
}
|
|
246
|
-
if (hasCompleteIssueContext(normalized.issue)) {
|
|
247
|
-
return normalized;
|
|
248
|
-
}
|
|
249
|
-
const linear = await this.linearProvider.forProject(projectId);
|
|
250
|
-
if (!linear) {
|
|
251
|
-
return normalized;
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
const liveIssue = await linear.getIssue(normalized.issue.id);
|
|
255
|
-
return {
|
|
256
|
-
...normalized,
|
|
257
|
-
issue: mergeIssueMetadata(normalized.issue, liveIssue),
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
this.logger.warn({
|
|
262
|
-
projectId,
|
|
263
|
-
issueId: normalized.issue.id,
|
|
264
|
-
triggerEvent: normalized.triggerEvent,
|
|
265
|
-
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
266
|
-
}, "Failed to hydrate sparse Linear issue context for agent session webhook");
|
|
267
|
-
return normalized;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
async tryHydrateProjectRoute(normalized) {
|
|
271
|
-
if (!normalized.issue) {
|
|
272
|
-
return undefined;
|
|
273
|
-
}
|
|
274
|
-
if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted") {
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
for (const candidate of this.config.projects) {
|
|
278
|
-
const linear = await this.linearProvider.forProject(candidate.id);
|
|
279
|
-
if (!linear) {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
try {
|
|
283
|
-
const liveIssue = await linear.getIssue(normalized.issue.id);
|
|
284
|
-
const hydrated = {
|
|
285
|
-
...normalized,
|
|
286
|
-
issue: mergeIssueMetadata(normalized.issue, liveIssue),
|
|
287
|
-
};
|
|
288
|
-
const resolved = resolveProject(this.config, hydrated.issue);
|
|
289
|
-
if (resolved) {
|
|
290
|
-
return {
|
|
291
|
-
project: resolved,
|
|
292
|
-
normalized: hydrated,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
this.logger.debug({
|
|
298
|
-
candidateProjectId: candidate.id,
|
|
299
|
-
issueId: normalized.issue.id,
|
|
300
|
-
triggerEvent: normalized.triggerEvent,
|
|
301
|
-
error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
|
|
302
|
-
}, "Failed to hydrate Linear issue context while resolving project route");
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return undefined;
|
|
306
|
-
}
|
|
307
|
-
assignEventReceiptContext(webhookId, projectId, linearIssueId) {
|
|
308
|
-
const receipt = this.lookupEventReceipt(webhookId);
|
|
309
|
-
if (!receipt) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
this.stores.eventReceipts.assignEventReceiptContext(receipt.id, {
|
|
313
|
-
...(projectId ? { projectId } : {}),
|
|
314
|
-
...(linearIssueId ? { linearIssueId } : {}),
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
markEventReceiptProcessed(webhookId, status) {
|
|
318
|
-
const receipt = this.lookupEventReceipt(webhookId);
|
|
319
|
-
if (!receipt) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
this.stores.eventReceipts.markEventReceiptProcessed(receipt.id, status);
|
|
323
|
-
}
|
|
324
|
-
lookupEventReceipt(webhookId) {
|
|
325
|
-
return this.stores.eventReceipts.getEventReceiptBySourceExternalId("linear-webhook", webhookId);
|
|
326
|
-
}
|
|
327
|
-
ensureEventReceipt(event, projectId, linearIssueId) {
|
|
328
|
-
const existing = this.lookupEventReceipt(event.webhookId);
|
|
329
|
-
if (existing) {
|
|
330
|
-
this.assignEventReceiptContext(event.webhookId, projectId, linearIssueId);
|
|
331
|
-
return existing;
|
|
332
|
-
}
|
|
333
|
-
const inserted = this.stores.eventReceipts.insertEventReceipt({
|
|
334
|
-
source: "linear-webhook",
|
|
335
|
-
externalId: event.webhookId,
|
|
336
|
-
eventType: event.eventType,
|
|
337
|
-
receivedAt: event.receivedAt,
|
|
338
|
-
acceptanceStatus: "accepted",
|
|
339
|
-
...(projectId ? { projectId } : {}),
|
|
340
|
-
...(linearIssueId ? { linearIssueId } : {}),
|
|
341
|
-
headersJson: event.headersJson,
|
|
342
|
-
payloadJson: event.payloadJson,
|
|
343
|
-
});
|
|
344
|
-
return this.stores.eventReceipts.getEventReceipt(inserted.id);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
function hasCompleteIssueContext(issue) {
|
|
348
|
-
return Boolean(issue.stateName && issue.delegateId && issue.teamId && issue.teamKey);
|
|
349
|
-
}
|
|
350
|
-
function mergeIssueMetadata(issue, liveIssue) {
|
|
351
|
-
return {
|
|
352
|
-
...issue,
|
|
353
|
-
...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
|
|
354
|
-
...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
|
|
355
|
-
...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
|
|
356
|
-
...(issue.teamId ? {} : liveIssue.teamId ? { teamId: liveIssue.teamId } : {}),
|
|
357
|
-
...(issue.teamKey ? {} : liveIssue.teamKey ? { teamKey: liveIssue.teamKey } : {}),
|
|
358
|
-
...(issue.stateId ? {} : liveIssue.stateId ? { stateId: liveIssue.stateId } : {}),
|
|
359
|
-
...(issue.stateName ? {} : liveIssue.stateName ? { stateName: liveIssue.stateName } : {}),
|
|
360
|
-
...(issue.delegateId ? {} : liveIssue.delegateId ? { delegateId: liveIssue.delegateId } : {}),
|
|
361
|
-
...(issue.delegateName ? {} : liveIssue.delegateName ? { delegateName: liveIssue.delegateName } : {}),
|
|
362
|
-
labelNames: issue.labelNames.length > 0 ? issue.labelNames : (liveIssue.labels ?? []).map((label) => label.name),
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
function describeWebhookObservation(normalized, delegatedToPatchRelay) {
|
|
366
|
-
switch (normalized.triggerEvent) {
|
|
367
|
-
case "delegateChanged":
|
|
368
|
-
return delegatedToPatchRelay
|
|
369
|
-
? {
|
|
370
|
-
kind: "agent",
|
|
371
|
-
status: "delegated",
|
|
372
|
-
summary: "Delegated to PatchRelay",
|
|
373
|
-
detail: normalized.issue?.stateName ? `Current Linear state: ${normalized.issue.stateName}.` : undefined,
|
|
374
|
-
}
|
|
375
|
-
: {
|
|
376
|
-
kind: "agent",
|
|
377
|
-
status: "undelegated",
|
|
378
|
-
summary: "Delegation moved away from PatchRelay",
|
|
379
|
-
};
|
|
380
|
-
case "agentSessionCreated":
|
|
381
|
-
return {
|
|
382
|
-
kind: "agent",
|
|
383
|
-
status: delegatedToPatchRelay ? "session" : "mention",
|
|
384
|
-
summary: delegatedToPatchRelay ? "Opened a delegated agent session" : "Mentioned PatchRelay in Linear",
|
|
385
|
-
detail: normalized.agentSession?.promptBody ?? normalized.agentSession?.promptContext,
|
|
386
|
-
};
|
|
387
|
-
case "agentPrompted":
|
|
388
|
-
return {
|
|
389
|
-
kind: "agent",
|
|
390
|
-
status: "prompted",
|
|
391
|
-
summary: "Received follow-up agent instructions",
|
|
392
|
-
detail: normalized.agentSession?.promptBody ?? normalized.agentSession?.promptContext,
|
|
393
|
-
};
|
|
394
|
-
case "commentCreated":
|
|
395
|
-
case "commentUpdated":
|
|
396
|
-
return {
|
|
397
|
-
kind: "comment",
|
|
398
|
-
status: "received",
|
|
399
|
-
summary: "Received a Linear comment",
|
|
400
|
-
detail: normalized.comment?.userName ?? normalized.comment?.body,
|
|
401
|
-
};
|
|
402
|
-
case "statusChanged":
|
|
403
|
-
return {
|
|
404
|
-
kind: "webhook",
|
|
405
|
-
status: "status_changed",
|
|
406
|
-
summary: normalized.issue?.stateName ? `Linear state changed to ${normalized.issue.stateName}` : "Linear state changed",
|
|
407
|
-
};
|
|
408
|
-
default:
|
|
409
|
-
return undefined;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
2
|
-
export class StageAgentActivityPublisher {
|
|
3
|
-
config;
|
|
4
|
-
linearProvider;
|
|
5
|
-
logger;
|
|
6
|
-
constructor(config, linearProvider, logger) {
|
|
7
|
-
this.config = config;
|
|
8
|
-
this.linearProvider = linearProvider;
|
|
9
|
-
this.logger = logger;
|
|
10
|
-
}
|
|
11
|
-
async publishForSession(projectId, agentSessionId, content, options) {
|
|
12
|
-
const linear = await this.linearProvider.forProject(projectId);
|
|
13
|
-
if (!linear) {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
try {
|
|
17
|
-
await linear.createAgentActivity({
|
|
18
|
-
agentSessionId,
|
|
19
|
-
content,
|
|
20
|
-
ephemeral: options?.ephemeral ?? (content.type === "thought" || content.type === "action"),
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
this.logger.warn({
|
|
25
|
-
agentSessionId,
|
|
26
|
-
error: error instanceof Error ? error.message : String(error),
|
|
27
|
-
}, "Failed to publish Linear agent activity");
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async publishForIssue(issue, content, options) {
|
|
31
|
-
if (!issue.activeAgentSessionId) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
await this.publishForSession(issue.projectId, issue.activeAgentSessionId, content, options);
|
|
35
|
-
}
|
|
36
|
-
async updateSession(params) {
|
|
37
|
-
const linear = await this.linearProvider.forProject(params.projectId);
|
|
38
|
-
if (!linear?.updateAgentSession) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const externalUrls = buildAgentSessionExternalUrls(this.config, params.issueKey);
|
|
42
|
-
if (!externalUrls && !params.plan) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
await linear.updateAgentSession({
|
|
47
|
-
agentSessionId: params.agentSessionId,
|
|
48
|
-
...(externalUrls ? { externalUrls } : {}),
|
|
49
|
-
...(params.plan ? { plan: params.plan } : {}),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
this.logger.warn({
|
|
54
|
-
agentSessionId: params.agentSessionId,
|
|
55
|
-
error: error instanceof Error ? error.message : String(error),
|
|
56
|
-
}, "Failed to update Linear agent session");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/stage-failure.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { buildFailedSessionPlan } from "./agent-session-plan.js";
|
|
2
|
-
import { buildStageFailedComment, resolveActiveLinearState, resolveFallbackLinearState, resolveWorkflowLabelCleanup, } from "./linear-workflow.js";
|
|
3
|
-
function normalizeStateName(value) {
|
|
4
|
-
const trimmed = value?.trim();
|
|
5
|
-
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
6
|
-
}
|
|
7
|
-
export async function syncFailedStageToLinear(params) {
|
|
8
|
-
const linear = await params.linearProvider.forProject(params.stageRun.projectId);
|
|
9
|
-
if (!linear) {
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
const fallbackState = resolveFallbackLinearState(params.project, params.stageRun.stage, params.issue.selectedWorkflowId);
|
|
13
|
-
let shouldWriteFailureState = true;
|
|
14
|
-
if (params.requireActiveLinearStateMatch) {
|
|
15
|
-
const activeState = resolveActiveLinearState(params.project, params.stageRun.stage, params.issue.selectedWorkflowId);
|
|
16
|
-
if (!activeState) {
|
|
17
|
-
shouldWriteFailureState = false;
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
try {
|
|
21
|
-
const linearIssue = await linear.getIssue(params.stageRun.linearIssueId);
|
|
22
|
-
shouldWriteFailureState = normalizeStateName(linearIssue.stateName) === normalizeStateName(activeState);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
shouldWriteFailureState = false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const cleanup = resolveWorkflowLabelCleanup(params.project);
|
|
30
|
-
if (cleanup.remove.length > 0) {
|
|
31
|
-
await linear
|
|
32
|
-
.updateIssueLabels({
|
|
33
|
-
issueId: params.stageRun.linearIssueId,
|
|
34
|
-
removeNames: cleanup.remove,
|
|
35
|
-
})
|
|
36
|
-
.catch(() => undefined);
|
|
37
|
-
}
|
|
38
|
-
if (!shouldWriteFailureState) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
if (fallbackState) {
|
|
42
|
-
await linear.setIssueState(params.stageRun.linearIssueId, fallbackState).catch(() => undefined);
|
|
43
|
-
params.stores.workflowCoordinator.upsertTrackedIssue({
|
|
44
|
-
projectId: params.stageRun.projectId,
|
|
45
|
-
linearIssueId: params.stageRun.linearIssueId,
|
|
46
|
-
currentLinearState: fallbackState,
|
|
47
|
-
statusCommentId: params.issue.statusCommentId ?? null,
|
|
48
|
-
activeAgentSessionId: params.issue.activeAgentSessionId ?? null,
|
|
49
|
-
lifecycleStatus: "failed",
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
let deliveredToSession = false;
|
|
53
|
-
if (params.issue.activeAgentSessionId) {
|
|
54
|
-
deliveredToSession =
|
|
55
|
-
(await linear
|
|
56
|
-
.updateAgentSession?.({
|
|
57
|
-
agentSessionId: params.issue.activeAgentSessionId,
|
|
58
|
-
plan: buildFailedSessionPlan(params.stageRun.stage, params.stageRun),
|
|
59
|
-
})
|
|
60
|
-
.then(() => true)
|
|
61
|
-
.catch(() => false)) ?? false;
|
|
62
|
-
deliveredToSession =
|
|
63
|
-
(await linear
|
|
64
|
-
.createAgentActivity({
|
|
65
|
-
agentSessionId: params.issue.activeAgentSessionId,
|
|
66
|
-
content: {
|
|
67
|
-
type: "error",
|
|
68
|
-
body: `PatchRelay could not complete the ${params.stageRun.stage} workflow: ${params.message}`,
|
|
69
|
-
},
|
|
70
|
-
})
|
|
71
|
-
.then(() => true)
|
|
72
|
-
.catch(() => false)) || deliveredToSession;
|
|
73
|
-
}
|
|
74
|
-
if (!deliveredToSession && !params.issue.activeAgentSessionId) {
|
|
75
|
-
const result = await linear
|
|
76
|
-
.upsertIssueComment({
|
|
77
|
-
issueId: params.stageRun.linearIssueId,
|
|
78
|
-
...(params.issue.statusCommentId ? { commentId: params.issue.statusCommentId } : {}),
|
|
79
|
-
body: buildStageFailedComment({
|
|
80
|
-
issue: params.issue,
|
|
81
|
-
stageRun: params.stageRun,
|
|
82
|
-
message: params.message,
|
|
83
|
-
...(fallbackState ? { fallbackState } : {}),
|
|
84
|
-
...(params.mode ? { mode: params.mode } : {}),
|
|
85
|
-
}),
|
|
86
|
-
})
|
|
87
|
-
.catch(() => undefined);
|
|
88
|
-
if (result) {
|
|
89
|
-
params.stores.workflowCoordinator.setIssueStatusComment(params.stageRun.projectId, params.stageRun.linearIssueId, result.id);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
package/dist/stage-handoff.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { safeJsonParse } from "./utils.js";
|
|
2
|
-
import { listAllowedTransitionTargets, listWorkflowStageIds, resolveWorkflowStageCandidate } from "./workflow-policy.js";
|
|
3
|
-
function normalize(value) {
|
|
4
|
-
const trimmed = value?.trim();
|
|
5
|
-
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
6
|
-
}
|
|
7
|
-
function stripListPrefix(value) {
|
|
8
|
-
return value.replace(/^[-*•]\s+/, "").replace(/^\d+\.\s+/, "").trim();
|
|
9
|
-
}
|
|
10
|
-
function resolveTerminalTarget(value) {
|
|
11
|
-
const normalized = normalize(value)?.replace(/[\s_-]+/g, "");
|
|
12
|
-
if (!normalized) {
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
if (["done", "complete", "completed", "shipped", "ship"].includes(normalized)) {
|
|
16
|
-
return "done";
|
|
17
|
-
}
|
|
18
|
-
if (["humanneeded", "humaninput", "needsinput", "unclear", "unknown", "blocked", "ambiguous"].includes(normalized)) {
|
|
19
|
-
return "human_needed";
|
|
20
|
-
}
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
export function resolveWorkflowTarget(project, value) {
|
|
24
|
-
return resolveWorkflowTargetForDefinition(project, value);
|
|
25
|
-
}
|
|
26
|
-
export function resolveWorkflowTargetForDefinition(project, value, workflowDefinitionId) {
|
|
27
|
-
return resolveWorkflowStageCandidate(project, value, workflowDefinitionId) ?? resolveTerminalTarget(value);
|
|
28
|
-
}
|
|
29
|
-
function summarizeSignalsHumanNeeded(lines) {
|
|
30
|
-
const joined = normalize(lines.join(" "));
|
|
31
|
-
if (!joined) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return ["blocked", "unclear", "ambiguous", "human input", "human needed", "need human", "cannot determine"].some((token) => joined.includes(token));
|
|
35
|
-
}
|
|
36
|
-
export function parseStageHandoff(project, assistantMessages, workflowDefinitionId) {
|
|
37
|
-
const latestMessage = [...assistantMessages].reverse().find((message) => typeof message === "string" && message.trim().length > 0);
|
|
38
|
-
if (!latestMessage) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
const lines = latestMessage
|
|
42
|
-
.split(/\r?\n/)
|
|
43
|
-
.map((line) => line.trimEnd());
|
|
44
|
-
const markerIndex = lines.findIndex((line) => /^stage result\s*:?\s*$/i.test(line.trim()));
|
|
45
|
-
const relevantLines = (markerIndex >= 0 ? lines.slice(markerIndex + 1) : lines)
|
|
46
|
-
.map((line) => stripListPrefix(line.trim()))
|
|
47
|
-
.filter(Boolean);
|
|
48
|
-
if (relevantLines.length === 0) {
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
const summaryLines = [];
|
|
52
|
-
let nextLikelyStageText;
|
|
53
|
-
let nextAttention;
|
|
54
|
-
for (const line of relevantLines) {
|
|
55
|
-
const nextStageMatch = line.match(/^next likely stage\s*:\s*(.+)$/i);
|
|
56
|
-
if (nextStageMatch) {
|
|
57
|
-
nextLikelyStageText = nextStageMatch[1]?.trim();
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const attentionMatch = line.match(/^(next attention|what to watch|watch carefully|pay attention|human attention)\s*:\s*(.+)$/i);
|
|
61
|
-
if (attentionMatch) {
|
|
62
|
-
nextAttention = attentionMatch[2]?.trim();
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
summaryLines.push(line);
|
|
66
|
-
}
|
|
67
|
-
const resolvedNextStage = resolveWorkflowTargetForDefinition(project, nextLikelyStageText, workflowDefinitionId);
|
|
68
|
-
return {
|
|
69
|
-
sourceText: latestMessage,
|
|
70
|
-
summaryLines,
|
|
71
|
-
...(nextLikelyStageText ? { nextLikelyStageText } : {}),
|
|
72
|
-
...(nextAttention ? { nextAttention } : {}),
|
|
73
|
-
suggestsHumanNeeded: summarizeSignalsHumanNeeded(summaryLines),
|
|
74
|
-
...(resolvedNextStage ? { resolvedNextStage } : {}),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
export function extractPriorStageHandoff(project, stageRun, workflowDefinitionId) {
|
|
78
|
-
if (!stageRun?.reportJson) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
const report = safeJsonParse(stageRun.reportJson);
|
|
82
|
-
return report ? parseStageHandoff(project, report.assistantMessages, workflowDefinitionId) : undefined;
|
|
83
|
-
}
|
|
84
|
-
export function buildCarryForwardPrompt(params) {
|
|
85
|
-
const availableStages = listWorkflowStageIds(params.project, params.workflowDefinitionId);
|
|
86
|
-
const attemptNumber = params.stageHistory.filter((stageRun) => stageRun.stage === params.currentStage).length + 1;
|
|
87
|
-
const recentHistory = params.stageHistory.slice(-4).map((stageRun) => stageRun.stage);
|
|
88
|
-
const previousHandoff = extractPriorStageHandoff(params.project, params.previousStageRun, params.workflowDefinitionId);
|
|
89
|
-
const lines = [
|
|
90
|
-
`Workflow stage ids: ${availableStages.join(", ")}`,
|
|
91
|
-
`Allowed next targets from ${params.currentStage}: ${listAllowedTransitionTargets(params.project, params.currentStage, params.workflowDefinitionId).join(", ")}`,
|
|
92
|
-
`This is attempt ${attemptNumber} for the ${params.currentStage} stage.`,
|
|
93
|
-
recentHistory.length > 0 ? `Recent workflow history: ${recentHistory.join(" -> ")}` : undefined,
|
|
94
|
-
params.workspace?.branchName ? `Branch: ${params.workspace.branchName}` : undefined,
|
|
95
|
-
params.workspace?.worktreePath ? `Worktree: ${params.workspace.worktreePath}` : undefined,
|
|
96
|
-
params.previousStageRun ? "" : undefined,
|
|
97
|
-
params.previousStageRun ? "Carry-forward from the previous stage:" : undefined,
|
|
98
|
-
params.previousStageRun ? `- Prior stage: ${params.previousStageRun.stage}` : undefined,
|
|
99
|
-
previousHandoff?.summaryLines[0] ? `- Outcome: ${previousHandoff.summaryLines[0]}` : undefined,
|
|
100
|
-
previousHandoff && previousHandoff.summaryLines.length > 1
|
|
101
|
-
? `- Key facts: ${previousHandoff.summaryLines.slice(1, 3).join(" ")}`
|
|
102
|
-
: undefined,
|
|
103
|
-
previousHandoff?.nextAttention ? `- Watch next: ${previousHandoff.nextAttention}` : undefined,
|
|
104
|
-
params.previousStageRun?.threadId ? `- Prior thread: ${params.previousStageRun.threadId}` : undefined,
|
|
105
|
-
].filter((value) => Boolean(value));
|
|
106
|
-
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
107
|
-
}
|