patchrelay 0.8.8 → 0.9.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/README.md +64 -62
- package/dist/agent-session-plan.js +17 -17
- package/dist/build-info.json +3 -3
- package/dist/cli/commands/issues.js +12 -12
- package/dist/cli/data.js +109 -298
- package/dist/cli/formatters/text.js +22 -28
- 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 +21 -54
- 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,228 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { buildPreparingSessionPlan, buildRunningSessionPlan } from "./agent-session-plan.js";
|
|
3
|
-
import { triggerEventAllowed } from "./project-resolution.js";
|
|
4
|
-
import { listRunnableStates, resolveWorkflowStage } from "./workflow-policy.js";
|
|
5
|
-
function trimPrompt(value) {
|
|
6
|
-
const trimmed = value?.trim();
|
|
7
|
-
return trimmed ? trimmed : undefined;
|
|
8
|
-
}
|
|
9
|
-
function buildSessionUpdateParams(projectId, agentSessionId, issueKey, plan) {
|
|
10
|
-
return {
|
|
11
|
-
projectId,
|
|
12
|
-
agentSessionId,
|
|
13
|
-
...(issueKey ? { issueKey } : {}),
|
|
14
|
-
plan,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
export class AgentSessionWebhookHandler {
|
|
18
|
-
stores;
|
|
19
|
-
turnInputDispatcher;
|
|
20
|
-
agentActivity;
|
|
21
|
-
feed;
|
|
22
|
-
constructor(stores, turnInputDispatcher, agentActivity, feed) {
|
|
23
|
-
this.stores = stores;
|
|
24
|
-
this.turnInputDispatcher = turnInputDispatcher;
|
|
25
|
-
this.agentActivity = agentActivity;
|
|
26
|
-
this.feed = feed;
|
|
27
|
-
}
|
|
28
|
-
async handle(params) {
|
|
29
|
-
const { normalized, project, issue, desiredStage, delegatedToPatchRelay } = params;
|
|
30
|
-
if (!normalized.agentSession?.id) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const promptBody = trimPrompt(normalized.agentSession.promptBody);
|
|
34
|
-
const promptContext = trimPrompt(normalized.agentSession.promptContext);
|
|
35
|
-
const issueControl = normalized.issue ? this.stores.issueControl.getIssueControl(project.id, normalized.issue.id) : undefined;
|
|
36
|
-
const activeRunLease = issueControl?.activeRunLeaseId !== undefined ? this.stores.runLeases.getRunLease(issueControl.activeRunLeaseId) : undefined;
|
|
37
|
-
const activeStage = activeRunLease?.stage;
|
|
38
|
-
const runnableWorkflow = normalized.issue?.stateName
|
|
39
|
-
? resolveWorkflowStage(project, normalized.issue.stateName, {
|
|
40
|
-
...(issue?.selectedWorkflowId
|
|
41
|
-
? { workflowDefinitionId: issue.selectedWorkflowId }
|
|
42
|
-
: normalized.issue
|
|
43
|
-
? { issue: normalized.issue }
|
|
44
|
-
: {}),
|
|
45
|
-
})
|
|
46
|
-
: undefined;
|
|
47
|
-
if (normalized.triggerEvent === "agentSessionCreated") {
|
|
48
|
-
if (!delegatedToPatchRelay) {
|
|
49
|
-
if (activeStage) {
|
|
50
|
-
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildRunningSessionPlan(activeStage)));
|
|
51
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
52
|
-
type: "thought",
|
|
53
|
-
body: `PatchRelay is already running the ${activeStage} workflow for this issue. Delegate it to PatchRelay if you want automation to own the workflow, or keep replying here to steer the active run.`,
|
|
54
|
-
});
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const body = runnableWorkflow
|
|
58
|
-
? `PatchRelay received your mention. Delegate the issue to PatchRelay to start the ${runnableWorkflow} workflow from the current \`${normalized.issue?.stateName}\` state.`
|
|
59
|
-
: `PatchRelay received your mention, but the issue is not in a runnable workflow state yet. Move it to one of: ${listRunnableStates(project).join(", ")}, then delegate it to PatchRelay.`;
|
|
60
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
61
|
-
type: "elicitation",
|
|
62
|
-
body,
|
|
63
|
-
});
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (!desiredStage && !activeStage) {
|
|
67
|
-
const runnableStates = listRunnableStates(project).join(", ");
|
|
68
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
69
|
-
type: "elicitation",
|
|
70
|
-
body: `PatchRelay is delegated, but the issue is not in a runnable workflow state. Move it to one of: ${runnableStates}.`,
|
|
71
|
-
});
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (desiredStage) {
|
|
75
|
-
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
76
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
77
|
-
type: "response",
|
|
78
|
-
body: `PatchRelay started working on the ${desiredStage} workflow and is preparing the workspace.`,
|
|
79
|
-
}, { ephemeral: false });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (activeStage) {
|
|
83
|
-
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildRunningSessionPlan(activeStage)));
|
|
84
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
85
|
-
type: "response",
|
|
86
|
-
body: `PatchRelay is already running the ${activeStage} workflow for this issue.`,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (normalized.triggerEvent !== "agentPrompted") {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (!triggerEventAllowed(project, normalized.triggerEvent)) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (activeRunLease && promptBody) {
|
|
98
|
-
const dedupeKey = buildPromptDedupeKey(normalized.agentSession.id, promptBody);
|
|
99
|
-
if (issueControl?.activeRunLeaseId !== undefined &&
|
|
100
|
-
this.stores.obligations.getObligationByDedupeKey({
|
|
101
|
-
runLeaseId: issueControl.activeRunLeaseId,
|
|
102
|
-
kind: "deliver_turn_input",
|
|
103
|
-
dedupeKey,
|
|
104
|
-
})) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const promptInput = ["New Linear agent prompt received while you are working.", "", promptBody].join("\n");
|
|
108
|
-
const source = `linear-agent-prompt:${normalized.agentSession.id}:${normalized.webhookId}`;
|
|
109
|
-
const obligationId = this.enqueueObligation(project.id, normalized.issue.id, activeRunLease.threadId, activeRunLease.turnId, source, promptInput, dedupeKey);
|
|
110
|
-
const flushResult = await this.turnInputDispatcher.flush({
|
|
111
|
-
id: issueControl?.activeRunLeaseId ?? 0,
|
|
112
|
-
projectId: project.id,
|
|
113
|
-
linearIssueId: normalized.issue.id,
|
|
114
|
-
...(activeRunLease.threadId ? { threadId: activeRunLease.threadId } : {}),
|
|
115
|
-
...(activeRunLease.turnId ? { turnId: activeRunLease.turnId } : {}),
|
|
116
|
-
}, {
|
|
117
|
-
...(issue?.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
118
|
-
failureMessage: "Failed to deliver queued Linear agent prompt to active Codex turn",
|
|
119
|
-
});
|
|
120
|
-
this.publishPromptDeliveryEvent({
|
|
121
|
-
projectId: project.id,
|
|
122
|
-
issueKey: issue?.issueKey ?? normalized.issue?.identifier,
|
|
123
|
-
stage: activeRunLease.stage,
|
|
124
|
-
obligationId,
|
|
125
|
-
flushResult,
|
|
126
|
-
});
|
|
127
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
128
|
-
type: "thought",
|
|
129
|
-
body: obligationId !== undefined && flushResult.deliveredObligationIds.includes(obligationId)
|
|
130
|
-
? `PatchRelay routed your follow-up instructions into the active ${activeRunLease.stage} workflow.`
|
|
131
|
-
: `PatchRelay queued your follow-up instructions for delivery into the active ${activeRunLease.stage} workflow.`,
|
|
132
|
-
});
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (!delegatedToPatchRelay && (promptBody || promptContext)) {
|
|
136
|
-
const body = runnableWorkflow
|
|
137
|
-
? `PatchRelay received your prompt. Delegate the issue to PatchRelay to start the ${runnableWorkflow} workflow from the current \`${normalized.issue?.stateName}\` state.`
|
|
138
|
-
: `PatchRelay received your prompt, but the issue is not in a runnable workflow state yet. Move it to one of: ${listRunnableStates(project).join(", ")}, then delegate it to PatchRelay.`;
|
|
139
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
140
|
-
type: "elicitation",
|
|
141
|
-
body,
|
|
142
|
-
});
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (!activeRunLease && desiredStage) {
|
|
146
|
-
await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
|
|
147
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
148
|
-
type: "response",
|
|
149
|
-
body: `PatchRelay is preparing the ${desiredStage} workflow from your latest prompt.`,
|
|
150
|
-
}, { ephemeral: false });
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (!activeRunLease && !desiredStage && (promptBody || promptContext)) {
|
|
154
|
-
const runnableStates = listRunnableStates(project).join(", ");
|
|
155
|
-
await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
|
|
156
|
-
type: "elicitation",
|
|
157
|
-
body: `PatchRelay received your prompt, but the issue is not in a runnable workflow state yet. Move it to one of: ${runnableStates}.`,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
enqueueObligation(projectId, linearIssueId, threadId, turnId, source, promptBody, dedupeKey) {
|
|
162
|
-
const activeRunLeaseId = this.stores.issueControl.getIssueControl(projectId, linearIssueId)?.activeRunLeaseId;
|
|
163
|
-
if (activeRunLeaseId === undefined) {
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
const obligation = this.stores.obligations.enqueueObligation({
|
|
167
|
-
projectId,
|
|
168
|
-
linearIssueId,
|
|
169
|
-
kind: "deliver_turn_input",
|
|
170
|
-
source,
|
|
171
|
-
payloadJson: JSON.stringify({
|
|
172
|
-
body: promptBody,
|
|
173
|
-
}),
|
|
174
|
-
runLeaseId: activeRunLeaseId,
|
|
175
|
-
...(threadId ? { threadId } : {}),
|
|
176
|
-
...(turnId ? { turnId } : {}),
|
|
177
|
-
dedupeKey,
|
|
178
|
-
});
|
|
179
|
-
return obligation.id;
|
|
180
|
-
}
|
|
181
|
-
publishPromptDeliveryEvent(params) {
|
|
182
|
-
if (params.obligationId === undefined) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (params.flushResult.deliveredObligationIds.includes(params.obligationId)) {
|
|
186
|
-
this.feed?.publish({
|
|
187
|
-
level: "info",
|
|
188
|
-
kind: "agent",
|
|
189
|
-
projectId: params.projectId,
|
|
190
|
-
issueKey: params.issueKey,
|
|
191
|
-
stage: params.stage,
|
|
192
|
-
status: "delivered",
|
|
193
|
-
summary: `Delivered follow-up prompt to active ${params.stage} workflow`,
|
|
194
|
-
detail: "The active Linear agent session was routed into the running Codex turn.",
|
|
195
|
-
});
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (params.flushResult.failedObligationIds.includes(params.obligationId)) {
|
|
199
|
-
this.feed?.publish({
|
|
200
|
-
level: "warn",
|
|
201
|
-
kind: "agent",
|
|
202
|
-
projectId: params.projectId,
|
|
203
|
-
issueKey: params.issueKey,
|
|
204
|
-
stage: params.stage,
|
|
205
|
-
status: "delivery_failed",
|
|
206
|
-
summary: `Could not deliver follow-up prompt to active ${params.stage} workflow`,
|
|
207
|
-
detail: "PatchRelay kept the prompt queued and will retry delivery on the next active turn.",
|
|
208
|
-
});
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
this.feed?.publish({
|
|
212
|
-
level: "info",
|
|
213
|
-
kind: "agent",
|
|
214
|
-
projectId: params.projectId,
|
|
215
|
-
issueKey: params.issueKey,
|
|
216
|
-
stage: params.stage,
|
|
217
|
-
status: "queued",
|
|
218
|
-
summary: `Queued follow-up prompt for active ${params.stage} workflow`,
|
|
219
|
-
detail: "PatchRelay saved the prompt for the next delivery opportunity.",
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function buildPromptDedupeKey(agentSessionId, promptBody) {
|
|
224
|
-
return `linear-agent-prompt:${agentSessionId}:${hashBody(promptBody)}`;
|
|
225
|
-
}
|
|
226
|
-
function hashBody(value) {
|
|
227
|
-
return createHash("sha256").update(value).digest("hex");
|
|
228
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { isPatchRelayStatusComment } from "./linear-workflow.js";
|
|
3
|
-
import { triggerEventAllowed } from "./project-resolution.js";
|
|
4
|
-
export class CommentWebhookHandler {
|
|
5
|
-
stores;
|
|
6
|
-
turnInputDispatcher;
|
|
7
|
-
feed;
|
|
8
|
-
constructor(stores, turnInputDispatcher, feed) {
|
|
9
|
-
this.stores = stores;
|
|
10
|
-
this.turnInputDispatcher = turnInputDispatcher;
|
|
11
|
-
this.feed = feed;
|
|
12
|
-
}
|
|
13
|
-
async handle(normalized, project) {
|
|
14
|
-
if ((normalized.triggerEvent !== "commentCreated" && normalized.triggerEvent !== "commentUpdated") || !normalized.comment?.body) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (!triggerEventAllowed(project, normalized.triggerEvent)) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
const normalizedIssue = normalized.issue;
|
|
21
|
-
if (!normalizedIssue) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const issue = this.stores.issueWorkflows.getTrackedIssue(project.id, normalizedIssue.id);
|
|
25
|
-
const issueControl = this.stores.issueControl.getIssueControl(project.id, normalizedIssue.id);
|
|
26
|
-
if (!issueControl?.activeRunLeaseId) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (isPatchRelayStatusComment(normalized.comment.id, normalized.comment.body, issueControl.serviceOwnedCommentId ?? issue?.statusCommentId)) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const runLease = this.stores.runLeases.getRunLease(issueControl.activeRunLeaseId);
|
|
33
|
-
if (!runLease) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const body = [
|
|
37
|
-
"New Linear comment received while you are working.",
|
|
38
|
-
normalized.comment.userName ? `Author: ${normalized.comment.userName}` : undefined,
|
|
39
|
-
"",
|
|
40
|
-
normalized.comment.body.trim(),
|
|
41
|
-
]
|
|
42
|
-
.filter(Boolean)
|
|
43
|
-
.join("\n");
|
|
44
|
-
const dedupeKey = buildCommentDedupeKey(normalized.comment.id, body);
|
|
45
|
-
if (issueControl.activeRunLeaseId !== undefined &&
|
|
46
|
-
this.stores.obligations.getObligationByDedupeKey({
|
|
47
|
-
runLeaseId: issueControl.activeRunLeaseId,
|
|
48
|
-
kind: "deliver_turn_input",
|
|
49
|
-
dedupeKey,
|
|
50
|
-
})) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const obligationId = this.enqueueObligation(project.id, normalizedIssue.id, runLease.threadId, runLease.turnId, normalized.comment.id, body, dedupeKey);
|
|
54
|
-
const flushResult = await this.turnInputDispatcher.flush({
|
|
55
|
-
id: issueControl.activeRunLeaseId,
|
|
56
|
-
projectId: project.id,
|
|
57
|
-
linearIssueId: normalizedIssue.id,
|
|
58
|
-
...(runLease.threadId ? { threadId: runLease.threadId } : {}),
|
|
59
|
-
...(runLease.turnId ? { turnId: runLease.turnId } : {}),
|
|
60
|
-
}, {
|
|
61
|
-
...(issue?.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
62
|
-
failureMessage: "Failed to deliver queued Linear comment to active Codex turn",
|
|
63
|
-
});
|
|
64
|
-
this.publishCommentDeliveryEvent({
|
|
65
|
-
projectId: project.id,
|
|
66
|
-
issueKey: issue?.issueKey ?? normalizedIssue.identifier,
|
|
67
|
-
stage: runLease.stage,
|
|
68
|
-
obligationId,
|
|
69
|
-
authorName: normalized.comment.userName,
|
|
70
|
-
flushResult,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
enqueueObligation(projectId, linearIssueId, threadId, turnId, commentId, body, dedupeKey) {
|
|
74
|
-
const activeRunLeaseId = this.stores.issueControl.getIssueControl(projectId, linearIssueId)?.activeRunLeaseId;
|
|
75
|
-
if (activeRunLeaseId === undefined) {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
const obligation = this.stores.obligations.enqueueObligation({
|
|
79
|
-
projectId,
|
|
80
|
-
linearIssueId,
|
|
81
|
-
kind: "deliver_turn_input",
|
|
82
|
-
source: `linear-comment:${commentId}`,
|
|
83
|
-
payloadJson: JSON.stringify({
|
|
84
|
-
body,
|
|
85
|
-
}),
|
|
86
|
-
runLeaseId: activeRunLeaseId,
|
|
87
|
-
...(threadId ? { threadId } : {}),
|
|
88
|
-
...(turnId ? { turnId } : {}),
|
|
89
|
-
dedupeKey,
|
|
90
|
-
});
|
|
91
|
-
return obligation.id;
|
|
92
|
-
}
|
|
93
|
-
publishCommentDeliveryEvent(params) {
|
|
94
|
-
if (params.obligationId === undefined) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const authorDetail = params.authorName ? `Author: ${params.authorName}.` : undefined;
|
|
98
|
-
if (params.flushResult.deliveredObligationIds.includes(params.obligationId)) {
|
|
99
|
-
this.feed?.publish({
|
|
100
|
-
level: "info",
|
|
101
|
-
kind: "comment",
|
|
102
|
-
projectId: params.projectId,
|
|
103
|
-
issueKey: params.issueKey,
|
|
104
|
-
stage: params.stage,
|
|
105
|
-
status: "delivered",
|
|
106
|
-
summary: `Delivered follow-up comment to active ${params.stage} workflow`,
|
|
107
|
-
detail: authorDetail ?? "The comment was routed into the running Codex turn.",
|
|
108
|
-
});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (params.flushResult.failedObligationIds.includes(params.obligationId)) {
|
|
112
|
-
this.feed?.publish({
|
|
113
|
-
level: "warn",
|
|
114
|
-
kind: "comment",
|
|
115
|
-
projectId: params.projectId,
|
|
116
|
-
issueKey: params.issueKey,
|
|
117
|
-
stage: params.stage,
|
|
118
|
-
status: "delivery_failed",
|
|
119
|
-
summary: `Could not deliver follow-up comment to active ${params.stage} workflow`,
|
|
120
|
-
detail: authorDetail ?? "PatchRelay kept the comment queued and will retry delivery.",
|
|
121
|
-
});
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
this.feed?.publish({
|
|
125
|
-
level: "info",
|
|
126
|
-
kind: "comment",
|
|
127
|
-
projectId: params.projectId,
|
|
128
|
-
issueKey: params.issueKey,
|
|
129
|
-
stage: params.stage,
|
|
130
|
-
status: "queued",
|
|
131
|
-
summary: `Queued follow-up comment for active ${params.stage} workflow`,
|
|
132
|
-
detail: authorDetail ?? "PatchRelay saved the comment for the next delivery opportunity.",
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function buildCommentDedupeKey(commentId, body) {
|
|
137
|
-
return `linear-comment:${commentId}:${hashBody(body)}`;
|
|
138
|
-
}
|
|
139
|
-
function hashBody(value) {
|
|
140
|
-
return createHash("sha256").update(value).digest("hex");
|
|
141
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { triggerEventAllowed } from "./project-resolution.js";
|
|
2
|
-
import { resolveWorkflowStage, selectWorkflowDefinition } from "./workflow-policy.js";
|
|
3
|
-
function trimPrompt(value) {
|
|
4
|
-
const trimmed = value?.trim();
|
|
5
|
-
return trimmed ? trimmed : undefined;
|
|
6
|
-
}
|
|
7
|
-
export class WebhookDesiredStageRecorder {
|
|
8
|
-
stores;
|
|
9
|
-
constructor(stores) {
|
|
10
|
-
this.stores = stores;
|
|
11
|
-
}
|
|
12
|
-
record(project, normalized, options) {
|
|
13
|
-
const normalizedIssue = normalized.issue;
|
|
14
|
-
if (!normalizedIssue) {
|
|
15
|
-
return {
|
|
16
|
-
issue: undefined,
|
|
17
|
-
activeStageRun: undefined,
|
|
18
|
-
desiredStage: undefined,
|
|
19
|
-
delegatedToPatchRelay: false,
|
|
20
|
-
launchInput: undefined,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
const issue = this.stores.issueWorkflows.getTrackedIssue(project.id, normalizedIssue.id);
|
|
24
|
-
const issueControl = this.stores.issueControl.getIssueControl(project.id, normalizedIssue.id);
|
|
25
|
-
const activeStageRun = issueControl?.activeRunLeaseId !== undefined ? this.stores.issueWorkflows.getStageRun(issueControl.activeRunLeaseId) : undefined;
|
|
26
|
-
const delegatedToPatchRelay = this.isDelegatedToPatchRelay(project, normalized);
|
|
27
|
-
const stageAllowed = triggerEventAllowed(project, normalized.triggerEvent);
|
|
28
|
-
const selectedWorkflowId = this.resolveSelectedWorkflowId(project, normalized, issue, activeStageRun, delegatedToPatchRelay);
|
|
29
|
-
const desiredStage = this.resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay, selectedWorkflowId);
|
|
30
|
-
const launchInput = this.resolveLaunchInput(normalized.agentSession);
|
|
31
|
-
const activeAgentSessionId = normalized.agentSession?.id ??
|
|
32
|
-
(!activeStageRun && (desiredStage || (normalized.triggerEvent === "delegateChanged" && !delegatedToPatchRelay)) ? null : undefined);
|
|
33
|
-
const refreshedIssue = this.stores.workflowCoordinator.recordDesiredStage({
|
|
34
|
-
projectId: project.id,
|
|
35
|
-
linearIssueId: normalizedIssue.id,
|
|
36
|
-
...(normalizedIssue.identifier ? { issueKey: normalizedIssue.identifier } : {}),
|
|
37
|
-
...(normalizedIssue.title ? { title: normalizedIssue.title } : {}),
|
|
38
|
-
...(normalizedIssue.url ? { issueUrl: normalizedIssue.url } : {}),
|
|
39
|
-
...(normalizedIssue.stateName ? { currentLinearState: normalizedIssue.stateName } : {}),
|
|
40
|
-
...(selectedWorkflowId !== undefined ? { selectedWorkflowId } : {}),
|
|
41
|
-
...(desiredStage ? { desiredStage } : {}),
|
|
42
|
-
...(desiredStage && options?.eventReceiptId !== undefined ? { desiredReceiptId: options.eventReceiptId } : {}),
|
|
43
|
-
...(activeAgentSessionId !== undefined ? { activeAgentSessionId } : {}),
|
|
44
|
-
lastWebhookAt: new Date().toISOString(),
|
|
45
|
-
});
|
|
46
|
-
if (launchInput && !activeStageRun && delegatedToPatchRelay && stageAllowed) {
|
|
47
|
-
this.stores.obligations.enqueueObligation({
|
|
48
|
-
projectId: project.id,
|
|
49
|
-
linearIssueId: normalizedIssue.id,
|
|
50
|
-
kind: "deliver_turn_input",
|
|
51
|
-
source: `linear-agent-launch:${normalized.agentSession?.id ?? normalized.webhookId}`,
|
|
52
|
-
payloadJson: JSON.stringify({
|
|
53
|
-
body: launchInput,
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
issue: refreshedIssue ?? issue,
|
|
59
|
-
activeStageRun,
|
|
60
|
-
desiredStage,
|
|
61
|
-
delegatedToPatchRelay,
|
|
62
|
-
launchInput,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
isDelegatedToPatchRelay(project, normalized) {
|
|
66
|
-
const normalizedIssue = normalized.issue;
|
|
67
|
-
if (!normalizedIssue) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
const installation = this.stores.linearInstallations.getLinearInstallationForProject(project.id);
|
|
71
|
-
if (!installation?.actorId) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
return normalizedIssue.delegateId === installation.actorId;
|
|
75
|
-
}
|
|
76
|
-
resolveDesiredStage(project, normalized, issue, activeStageRun, delegatedToPatchRelay, selectedWorkflowId) {
|
|
77
|
-
const normalizedIssue = normalized.issue;
|
|
78
|
-
if (!normalizedIssue) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
if (!delegatedToPatchRelay || !triggerEventAllowed(project, normalized.triggerEvent)) {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
const desiredStage = resolveWorkflowStage(project, normalizedIssue.stateName, {
|
|
85
|
-
...(selectedWorkflowId ? { workflowDefinitionId: selectedWorkflowId } : {}),
|
|
86
|
-
});
|
|
87
|
-
if (!desiredStage) {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
if (activeStageRun && desiredStage === activeStageRun.stage) {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
if (issue?.desiredStage && desiredStage === issue.desiredStage) {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
return desiredStage;
|
|
97
|
-
}
|
|
98
|
-
resolveSelectedWorkflowId(project, normalized, issue, activeStageRun, delegatedToPatchRelay) {
|
|
99
|
-
if (activeStageRun) {
|
|
100
|
-
return issue?.selectedWorkflowId;
|
|
101
|
-
}
|
|
102
|
-
if (!delegatedToPatchRelay || !triggerEventAllowed(project, normalized.triggerEvent) || !normalized.issue) {
|
|
103
|
-
return issue?.selectedWorkflowId;
|
|
104
|
-
}
|
|
105
|
-
const selectedWorkflow = selectWorkflowDefinition(project, normalized.issue);
|
|
106
|
-
if (selectedWorkflow) {
|
|
107
|
-
return selectedWorkflow.id;
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
resolveLaunchInput(agentSession) {
|
|
112
|
-
const promptBody = trimPrompt(agentSession?.promptBody);
|
|
113
|
-
if (promptBody) {
|
|
114
|
-
return ["New Linear agent input received.", "", promptBody].join("\n");
|
|
115
|
-
}
|
|
116
|
-
const promptContext = trimPrompt(agentSession?.promptContext);
|
|
117
|
-
if (promptContext) {
|
|
118
|
-
return ["Linear provided this initial agent context.", "", promptContext].join("\n");
|
|
119
|
-
}
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/workflow-policy.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
function normalize(value) {
|
|
2
|
-
const trimmed = value?.trim();
|
|
3
|
-
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
4
|
-
}
|
|
5
|
-
function normalizeWorkflowLabel(value) {
|
|
6
|
-
return normalize(value)?.replace(/[\s_-]+/g, "");
|
|
7
|
-
}
|
|
8
|
-
function extractIssuePrefix(identifier) {
|
|
9
|
-
const value = identifier?.trim();
|
|
10
|
-
if (!value) {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
const [prefix] = value.split("-", 1);
|
|
14
|
-
return prefix ? prefix.toUpperCase() : undefined;
|
|
15
|
-
}
|
|
16
|
-
function withWorkflowDefinitionId(workflowDefinitionId) {
|
|
17
|
-
return workflowDefinitionId ? { workflowDefinitionId } : undefined;
|
|
18
|
-
}
|
|
19
|
-
export function listProjectWorkflowDefinitions(project) {
|
|
20
|
-
if (project.workflowDefinitions && project.workflowDefinitions.length > 0) {
|
|
21
|
-
return project.workflowDefinitions;
|
|
22
|
-
}
|
|
23
|
-
return [
|
|
24
|
-
{
|
|
25
|
-
id: project.workflowSelection?.defaultWorkflowId ?? "default",
|
|
26
|
-
stages: project.workflows,
|
|
27
|
-
},
|
|
28
|
-
];
|
|
29
|
-
}
|
|
30
|
-
export function resolveWorkflowDefinitionById(project, workflowDefinitionId) {
|
|
31
|
-
const normalized = normalize(workflowDefinitionId);
|
|
32
|
-
if (!normalized) {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
return listProjectWorkflowDefinitions(project).find((definition) => normalize(definition.id) === normalized);
|
|
36
|
-
}
|
|
37
|
-
export function selectWorkflowDefinition(project, issue) {
|
|
38
|
-
const workflowDefinitions = listProjectWorkflowDefinitions(project);
|
|
39
|
-
if (workflowDefinitions.length === 0) {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
const labelNames = new Set((issue?.labelNames ?? []).map((label) => label.trim().toLowerCase()).filter(Boolean));
|
|
43
|
-
const matchedWorkflowIds = new Set();
|
|
44
|
-
for (const rule of project.workflowSelection?.byLabel ?? []) {
|
|
45
|
-
if (labelNames.has(rule.label.trim().toLowerCase())) {
|
|
46
|
-
matchedWorkflowIds.add(rule.workflowId);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (matchedWorkflowIds.size === 1) {
|
|
50
|
-
const [workflowId] = [...matchedWorkflowIds];
|
|
51
|
-
return resolveWorkflowDefinitionById(project, workflowId);
|
|
52
|
-
}
|
|
53
|
-
if (matchedWorkflowIds.size > 1) {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
if (project.workflowSelection?.defaultWorkflowId) {
|
|
57
|
-
return resolveWorkflowDefinitionById(project, project.workflowSelection.defaultWorkflowId);
|
|
58
|
-
}
|
|
59
|
-
return workflowDefinitions[0];
|
|
60
|
-
}
|
|
61
|
-
function resolveStageList(project, options) {
|
|
62
|
-
if (options?.workflowDefinitionId) {
|
|
63
|
-
return resolveWorkflowDefinitionById(project, options.workflowDefinitionId)?.stages ?? [];
|
|
64
|
-
}
|
|
65
|
-
if (options?.issue) {
|
|
66
|
-
return selectWorkflowDefinition(project, options.issue)?.stages ?? [];
|
|
67
|
-
}
|
|
68
|
-
return project.workflows;
|
|
69
|
-
}
|
|
70
|
-
export function resolveWorkflow(project, stateName, options) {
|
|
71
|
-
const normalized = normalize(stateName);
|
|
72
|
-
if (!normalized) {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
return resolveStageList(project, options).find((workflow) => normalize(workflow.whenState) === normalized);
|
|
76
|
-
}
|
|
77
|
-
export function resolveWorkflowStage(project, stateName, options) {
|
|
78
|
-
return resolveWorkflow(project, stateName, options)?.id;
|
|
79
|
-
}
|
|
80
|
-
export function resolveWorkflowStageConfig(project, workflowId, workflowDefinitionId) {
|
|
81
|
-
const normalized = normalize(workflowId);
|
|
82
|
-
if (!normalized) {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).find((workflow) => normalize(workflow.id) === normalized);
|
|
86
|
-
}
|
|
87
|
-
export function listRunnableStates(project, options) {
|
|
88
|
-
return [...new Set(resolveStageList(project, options).map((workflow) => workflow.whenState))];
|
|
89
|
-
}
|
|
90
|
-
export function listWorkflowStageIds(project, workflowDefinitionId) {
|
|
91
|
-
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).map((workflow) => workflow.id);
|
|
92
|
-
}
|
|
93
|
-
export function resolveWorkflowIndex(project, workflowId, workflowDefinitionId) {
|
|
94
|
-
if (!workflowId) {
|
|
95
|
-
return -1;
|
|
96
|
-
}
|
|
97
|
-
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).findIndex((workflow) => workflow.id === workflowId);
|
|
98
|
-
}
|
|
99
|
-
export function resolveDefaultTransitionTarget(project, currentStage, workflowDefinitionId) {
|
|
100
|
-
const stages = resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId));
|
|
101
|
-
const currentIndex = resolveWorkflowIndex(project, currentStage, workflowDefinitionId);
|
|
102
|
-
if (currentIndex < 0) {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
const nextStage = stages[currentIndex + 1]?.id;
|
|
106
|
-
return nextStage ?? "done";
|
|
107
|
-
}
|
|
108
|
-
export function listAllowedTransitionTargets(project, currentStage, workflowDefinitionId) {
|
|
109
|
-
const stages = resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId));
|
|
110
|
-
const currentIndex = resolveWorkflowIndex(project, currentStage, workflowDefinitionId);
|
|
111
|
-
if (currentIndex < 0) {
|
|
112
|
-
return ["human_needed"];
|
|
113
|
-
}
|
|
114
|
-
const targets = new Set(["human_needed"]);
|
|
115
|
-
const defaultTarget = resolveDefaultTransitionTarget(project, currentStage, workflowDefinitionId);
|
|
116
|
-
if (defaultTarget) {
|
|
117
|
-
targets.add(defaultTarget);
|
|
118
|
-
}
|
|
119
|
-
if (currentIndex > 0) {
|
|
120
|
-
targets.add(stages[currentIndex - 1].id);
|
|
121
|
-
}
|
|
122
|
-
if (currentIndex > 1) {
|
|
123
|
-
targets.add(stages[0].id);
|
|
124
|
-
}
|
|
125
|
-
return [...targets];
|
|
126
|
-
}
|
|
127
|
-
export function transitionTargetAllowed(project, currentStage, nextTarget, workflowDefinitionId) {
|
|
128
|
-
return listAllowedTransitionTargets(project, currentStage, workflowDefinitionId).includes(nextTarget);
|
|
129
|
-
}
|
|
130
|
-
export function resolveWorkflowStageCandidate(project, value, workflowDefinitionId) {
|
|
131
|
-
const normalized = normalizeWorkflowLabel(value);
|
|
132
|
-
if (!normalized) {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
return resolveStageList(project, withWorkflowDefinitionId(workflowDefinitionId)).find((workflow) => {
|
|
136
|
-
const candidates = [workflow.id, workflow.whenState, workflow.activeState];
|
|
137
|
-
return candidates.some((candidate) => normalizeWorkflowLabel(candidate) === normalized);
|
|
138
|
-
})?.id;
|
|
139
|
-
}
|
|
140
|
-
export function matchesProject(issue, project) {
|
|
141
|
-
const issuePrefix = extractIssuePrefix(issue.identifier);
|
|
142
|
-
const teamCandidates = [issue.teamId, issue.teamKey].filter((value) => Boolean(value));
|
|
143
|
-
const labelNames = new Set(issue.labelNames.map((label) => label.toLowerCase()));
|
|
144
|
-
const matchesPrefix = project.issueKeyPrefixes.length === 0 ||
|
|
145
|
-
(issuePrefix ? project.issueKeyPrefixes.map((value) => value.toUpperCase()).includes(issuePrefix) : false);
|
|
146
|
-
const matchesTeam = project.linearTeamIds.length === 0 || teamCandidates.some((candidate) => project.linearTeamIds.includes(candidate));
|
|
147
|
-
const matchesLabel = project.allowLabels.length === 0 || project.allowLabels.some((label) => labelNames.has(label.toLowerCase()));
|
|
148
|
-
return matchesPrefix && matchesTeam && matchesLabel;
|
|
149
|
-
}
|
package/dist/workflow-ports.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
File without changes
|