patchrelay 0.81.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,381 @@
1
+ import { buildFailureContext } from "./idle-reconciliation-helpers.js";
2
+ import { tryParseRunContextValue } from "./run-context.js";
3
+ function parseObservationPayload(observation) {
4
+ if (!observation.payloadJson)
5
+ return undefined;
6
+ try {
7
+ const parsed = JSON.parse(observation.payloadJson);
8
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
9
+ ? parsed
10
+ : undefined;
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ function deriveAuthority(issue, observations) {
17
+ let delegated = issue.delegatedToPatchRelay;
18
+ let epoch = 0;
19
+ let source = "linear";
20
+ let observedAt;
21
+ for (const observation of observations) {
22
+ if (observation.type !== "linear.delegated" && observation.type !== "linear.undelegated" && observation.type !== "operator.authority_changed") {
23
+ continue;
24
+ }
25
+ epoch += 1;
26
+ source = observation.source === "operator" ? "operator" : "linear";
27
+ observedAt = observation.observedAt;
28
+ const payload = parseObservationPayload(observation);
29
+ if (typeof payload?.delegated === "boolean") {
30
+ delegated = payload.delegated;
31
+ continue;
32
+ }
33
+ delegated = observation.type !== "linear.undelegated";
34
+ }
35
+ return {
36
+ delegated,
37
+ epoch,
38
+ source,
39
+ ...(observedAt ? { observedAt } : {}),
40
+ };
41
+ }
42
+ function issueStatus(issue, blockerCount) {
43
+ if (issue.factoryState === "done" || issue.prState === "merged")
44
+ return "done";
45
+ if (issue.factoryState === "failed" || issue.factoryState === "escalated")
46
+ return "failed";
47
+ if (issue.activeRunId !== undefined)
48
+ return "running";
49
+ if (!issue.delegatedToPatchRelay || blockerCount > 0 || issue.factoryState === "awaiting_input")
50
+ return "waiting";
51
+ return "idle";
52
+ }
53
+ function issueArtifacts(issue) {
54
+ const artifacts = [];
55
+ if (issue.branchName) {
56
+ artifacts.push({ type: "branch", ref: issue.branchName });
57
+ }
58
+ if (issue.prNumber !== undefined) {
59
+ artifacts.push({
60
+ type: "pr",
61
+ ref: String(issue.prNumber),
62
+ ...(issue.prState ? { state: issue.prState } : {}),
63
+ metadata: {
64
+ ...(issue.prUrl ? { url: issue.prUrl } : {}),
65
+ ...(issue.prHeadSha ? { headSha: issue.prHeadSha } : {}),
66
+ ...(issue.prReviewState ? { reviewState: issue.prReviewState } : {}),
67
+ ...(issue.prCheckStatus ? { checkStatus: issue.prCheckStatus } : {}),
68
+ },
69
+ });
70
+ }
71
+ if (issue.threadId) {
72
+ artifacts.push({ type: "codex_thread", ref: issue.threadId });
73
+ }
74
+ if (issue.agentSessionId) {
75
+ artifacts.push({ type: "linear_session", ref: issue.agentSessionId });
76
+ }
77
+ return artifacts;
78
+ }
79
+ function parseCiSnapshotContext(raw) {
80
+ const payload = parseObjectJson(raw);
81
+ if (!payload)
82
+ return undefined;
83
+ return tryParseRunContextValue({ ciSnapshot: payload })?.ciSnapshot;
84
+ }
85
+ function latestRequestedChangesContext(observations, blockingHeadSha) {
86
+ for (const observation of [...observations].reverse()) {
87
+ if (observation.source !== "github" || observation.type !== "github.review_changes_requested") {
88
+ continue;
89
+ }
90
+ const payload = parseObservationPayload(observation);
91
+ const rawContext = payload?.requestedChangesContext;
92
+ const context = rawContext && typeof rawContext === "object" && !Array.isArray(rawContext)
93
+ ? tryParseRunContextValue(rawContext)
94
+ : tryParseRunContextValue(payload ?? {});
95
+ if (!context)
96
+ continue;
97
+ if (blockingHeadSha
98
+ && context.requestedChangesHeadSha
99
+ && context.requestedChangesHeadSha !== blockingHeadSha) {
100
+ continue;
101
+ }
102
+ return context;
103
+ }
104
+ return undefined;
105
+ }
106
+ function latestDelegationContext(observations) {
107
+ for (const observation of [...observations].reverse()) {
108
+ if (observation.source !== "linear" || observation.type !== "linear.delegated") {
109
+ continue;
110
+ }
111
+ const payload = parseObservationPayload(observation);
112
+ const context = tryParseRunContextValue({
113
+ ...(typeof payload?.promptContext === "string" ? { promptContext: payload.promptContext } : {}),
114
+ ...(typeof payload?.promptBody === "string" ? { promptBody: payload.promptBody } : {}),
115
+ });
116
+ if (context && Object.keys(context).length > 0) {
117
+ return context;
118
+ }
119
+ }
120
+ return undefined;
121
+ }
122
+ function parseObjectJson(raw) {
123
+ if (!raw)
124
+ return undefined;
125
+ try {
126
+ const parsed = JSON.parse(raw);
127
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
128
+ ? parsed
129
+ : undefined;
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ }
135
+ export function projectWorkflowSnapshot(input) {
136
+ const observations = input.observations ?? [];
137
+ const blockerCount = input.blockerCount ?? 0;
138
+ const childCount = input.childCount ?? 0;
139
+ const openChildCount = input.openChildCount ?? childCount;
140
+ const authority = deriveAuthority(input.issue, observations);
141
+ const failureContext = buildFailureContext(input.issue);
142
+ const ciSnapshot = parseCiSnapshotContext(input.issue.lastGitHubCiSnapshotJson);
143
+ const requestedChangesContext = latestRequestedChangesContext(observations, input.issue.lastBlockingReviewHeadSha);
144
+ const delegationContext = latestDelegationContext(observations);
145
+ const baseSnapshot = {
146
+ id: `${input.issue.projectId}:${input.issue.linearIssueId}`,
147
+ projectId: input.issue.projectId,
148
+ subjectId: input.issue.linearIssueId,
149
+ status: input.activeRun ? "running" : issueStatus({ ...input.issue, delegatedToPatchRelay: authority.delegated }, blockerCount),
150
+ authority,
151
+ context: {
152
+ ...(input.issue.issueKey ? { issueKey: input.issue.issueKey } : {}),
153
+ ...(input.issue.title ? { title: input.issue.title } : {}),
154
+ factoryState: input.issue.factoryState,
155
+ ...(input.issue.lastBlockingReviewHeadSha ? { lastBlockingReviewHeadSha: input.issue.lastBlockingReviewHeadSha } : {}),
156
+ ...(input.issue.lastGitHubFailureSource ? { lastGitHubFailureSource: input.issue.lastGitHubFailureSource } : {}),
157
+ ...(input.issue.lastGitHubFailureHeadSha ? { lastGitHubFailureHeadSha: input.issue.lastGitHubFailureHeadSha } : {}),
158
+ ...(input.issue.lastGitHubFailureSignature ? { lastGitHubFailureSignature: input.issue.lastGitHubFailureSignature } : {}),
159
+ ...(input.issue.lastAttemptedFailureHeadSha ? { lastAttemptedFailureHeadSha: input.issue.lastAttemptedFailureHeadSha } : {}),
160
+ ...(input.issue.lastAttemptedFailureSignature ? { lastAttemptedFailureSignature: input.issue.lastAttemptedFailureSignature } : {}),
161
+ ...(failureContext ? { failureContext } : {}),
162
+ ...(ciSnapshot ? { ciSnapshot } : {}),
163
+ ...(requestedChangesContext ? { requestedChangesContext } : {}),
164
+ ...(delegationContext ? { delegationContext } : {}),
165
+ },
166
+ ...(input.activeRun
167
+ ? { activeRun: input.activeRun }
168
+ : input.issue.activeRunId !== undefined
169
+ ? {
170
+ activeRun: {
171
+ id: input.issue.activeRunId,
172
+ runType: input.issue.pendingRunType ?? "implementation",
173
+ authorityEpoch: authority.epoch,
174
+ status: "running",
175
+ },
176
+ }
177
+ : {}),
178
+ artifacts: issueArtifacts(input.issue),
179
+ blockerCount,
180
+ childCount,
181
+ openChildCount,
182
+ };
183
+ return {
184
+ ...baseSnapshot,
185
+ openTasks: deriveWorkflowTasks(baseSnapshot),
186
+ };
187
+ }
188
+ export function deriveWorkflowTasks(snapshot) {
189
+ const tasks = [];
190
+ if (!snapshot.authority.delegated) {
191
+ return [{
192
+ id: "wait:authority",
193
+ type: "wait",
194
+ reason: "Workflow is waiting for delegated authority",
195
+ }];
196
+ }
197
+ if (snapshot.status === "done") {
198
+ return [];
199
+ }
200
+ if (snapshot.activeRun) {
201
+ return [{
202
+ id: `wait:active-run:${snapshot.activeRun.id}`,
203
+ type: "wait",
204
+ reason: "A run is already active",
205
+ }];
206
+ }
207
+ const issue = snapshot.context;
208
+ const prState = snapshot.artifacts.find((artifact) => artifact.type === "pr")?.state;
209
+ const prHeadSha = snapshot.artifacts.find((artifact) => artifact.type === "pr")?.metadata?.headSha;
210
+ const prReviewState = snapshot.artifacts.find((artifact) => artifact.type === "pr")?.metadata?.reviewState;
211
+ if (issue.factoryState === "awaiting_input") {
212
+ return [{
213
+ id: "wait:input",
214
+ type: "wait",
215
+ reason: "Workflow is waiting for human input",
216
+ }];
217
+ }
218
+ if (snapshot.blockerCount > 0 && prState !== "open") {
219
+ return [{
220
+ id: "wait:blockers",
221
+ type: "wait",
222
+ reason: "Workflow is blocked by unresolved Linear dependencies",
223
+ requirements: { blockerCount: snapshot.blockerCount },
224
+ }];
225
+ }
226
+ if (snapshot.childCount > 0 && prState !== "open") {
227
+ if (snapshot.openChildCount > 0) {
228
+ return [{
229
+ id: "wait:children",
230
+ type: "wait",
231
+ reason: "Workflow is waiting for child workflows to complete",
232
+ requirements: {
233
+ childCount: snapshot.childCount,
234
+ openChildCount: snapshot.openChildCount,
235
+ },
236
+ }];
237
+ }
238
+ return [{
239
+ id: "verify:children_complete",
240
+ type: "verify",
241
+ reason: "Child workflows are complete; parent objective needs verification",
242
+ requirements: { childCount: snapshot.childCount },
243
+ }];
244
+ }
245
+ if (prState === "open" && prReviewState === "changes_requested") {
246
+ tasks.push({
247
+ id: "run:review_fix",
248
+ type: "run",
249
+ runType: "review_fix",
250
+ reason: "PR has requested changes",
251
+ requirements: {
252
+ ...issue.requestedChangesContext,
253
+ prState,
254
+ blockingHeadSha: issue.lastBlockingReviewHeadSha ?? prHeadSha,
255
+ requestedChangesHeadSha: issue.requestedChangesContext?.requestedChangesHeadSha
256
+ ?? issue.lastBlockingReviewHeadSha
257
+ ?? prHeadSha,
258
+ },
259
+ });
260
+ return tasks;
261
+ }
262
+ if (prState === "open" && issue.lastGitHubFailureSource === "queue_eviction") {
263
+ tasks.push({
264
+ id: "run:queue_repair",
265
+ type: "run",
266
+ runType: "queue_repair",
267
+ reason: "Merge queue eviction requires repair",
268
+ requirements: {
269
+ ...issue.failureContext,
270
+ failureSignature: issue.lastGitHubFailureSignature,
271
+ failureHeadSha: issue.lastGitHubFailureHeadSha ?? prHeadSha,
272
+ },
273
+ });
274
+ return tasks;
275
+ }
276
+ const branchFailureMatchesCurrentHead = issue.lastGitHubFailureSource === "branch_ci"
277
+ && typeof issue.lastGitHubFailureSignature === "string"
278
+ && typeof issue.lastGitHubFailureHeadSha === "string"
279
+ && typeof prHeadSha === "string"
280
+ && issue.lastGitHubFailureHeadSha === prHeadSha;
281
+ const branchFailureAlreadyAttempted = branchFailureMatchesCurrentHead
282
+ && issue.lastAttemptedFailureHeadSha === issue.lastGitHubFailureHeadSha
283
+ && issue.lastAttemptedFailureSignature === issue.lastGitHubFailureSignature;
284
+ if (prState === "open" && branchFailureMatchesCurrentHead && !branchFailureAlreadyAttempted) {
285
+ tasks.push({
286
+ id: "run:ci_repair",
287
+ type: "run",
288
+ runType: "ci_repair",
289
+ reason: "Settled branch CI failure requires repair",
290
+ requirements: {
291
+ ...issue.failureContext,
292
+ failureSignature: issue.lastGitHubFailureSignature,
293
+ failureHeadSha: issue.lastGitHubFailureHeadSha ?? prHeadSha,
294
+ ...(issue.ciSnapshot ? { ciSnapshot: issue.ciSnapshot } : {}),
295
+ },
296
+ });
297
+ return tasks;
298
+ }
299
+ if (!snapshot.artifacts.some((artifact) => artifact.type === "pr") && issue.factoryState === "delegated") {
300
+ tasks.push({
301
+ id: "run:implementation",
302
+ type: "run",
303
+ runType: "implementation",
304
+ reason: "Delegated workflow has no PR artifact yet",
305
+ requirements: {
306
+ ...issue.delegationContext,
307
+ blockerCount: snapshot.blockerCount,
308
+ },
309
+ });
310
+ }
311
+ else if (!snapshot.artifacts.some((artifact) => artifact.type === "pr")) {
312
+ tasks.push({
313
+ id: `wait:${issue.factoryState}`,
314
+ type: "wait",
315
+ reason: `Workflow is waiting in ${issue.factoryState}`,
316
+ });
317
+ }
318
+ return tasks;
319
+ }
320
+ export function evaluateTaskStart(snapshot, task) {
321
+ if (!snapshot.authority.delegated) {
322
+ return { action: "wait", reason: "authority_not_delegated" };
323
+ }
324
+ if (snapshot.activeRun) {
325
+ return { action: "wait", reason: "active_run_present" };
326
+ }
327
+ if (task.type !== "run") {
328
+ return { action: "start" };
329
+ }
330
+ if (task.runType === "implementation" && snapshot.blockerCount > 0) {
331
+ return { action: "wait", reason: "blocked" };
332
+ }
333
+ if (task.runType === "review_fix" && typeof task.requirements?.blockingHeadSha !== "string") {
334
+ return {
335
+ action: "ask",
336
+ reason: "missing_blocking_review_head",
337
+ question: "PatchRelay cannot verify the requested-changes repair without a blocking review head SHA.",
338
+ };
339
+ }
340
+ if ((task.runType === "ci_repair" || task.runType === "queue_repair") && typeof task.requirements?.failureHeadSha !== "string") {
341
+ return { action: "wait", reason: "missing_failure_head" };
342
+ }
343
+ return { action: "start" };
344
+ }
345
+ export function evaluateTaskCompletion(snapshot, task) {
346
+ if (!snapshot.authority.delegated) {
347
+ return { action: "wait", reason: "authority_revoked" };
348
+ }
349
+ const pr = snapshot.artifacts.find((artifact) => artifact.type === "pr");
350
+ if (task.runType === "implementation" && (!pr || pr.state !== "open")) {
351
+ return { action: "escalate", reason: "implementation_completed_without_open_pr" };
352
+ }
353
+ if (task.runType === "review_fix") {
354
+ const blockingHeadSha = task.requirements?.blockingHeadSha;
355
+ const currentHeadSha = pr?.metadata?.headSha;
356
+ if (typeof blockingHeadSha !== "string") {
357
+ return { action: "ask", reason: "missing_blocking_review_head", question: "PatchRelay cannot verify the requested-changes repair without the original head SHA." };
358
+ }
359
+ if (currentHeadSha === blockingHeadSha) {
360
+ return { action: "escalate", reason: "same_head_review_handoff_blocked" };
361
+ }
362
+ }
363
+ if (task.runType === "ci_repair" || task.runType === "queue_repair") {
364
+ const failureHeadSha = task.requirements?.failureHeadSha;
365
+ const currentHeadSha = pr?.metadata?.headSha;
366
+ if (typeof failureHeadSha !== "string") {
367
+ return {
368
+ action: "ask",
369
+ reason: "missing_failure_head",
370
+ question: "PatchRelay cannot verify the repair without the failing PR head SHA.",
371
+ };
372
+ }
373
+ if (typeof currentHeadSha !== "string") {
374
+ return { action: "escalate", reason: "repair_completed_without_pr_head" };
375
+ }
376
+ if (currentHeadSha === failureHeadSha) {
377
+ return { action: "escalate", reason: "same_head_repair_handoff_blocked" };
378
+ }
379
+ }
380
+ return { action: "start" };
381
+ }
@@ -0,0 +1,64 @@
1
+ import { evaluateTaskStart, projectWorkflowSnapshot, } from "./workflow-runtime.js";
2
+ function isActiveRun(run) {
3
+ return run.status === "queued" || run.status === "running";
4
+ }
5
+ function resolveActiveRunSnapshot(db, issue) {
6
+ const pinnedRun = issue.activeRunId !== undefined ? db.runs.getRunById(issue.activeRunId) : undefined;
7
+ const run = pinnedRun && isActiveRun(pinnedRun)
8
+ ? pinnedRun
9
+ : db.runs.listRunsForIssue(issue.projectId, issue.linearIssueId)
10
+ .filter(isActiveRun)
11
+ .at(-1);
12
+ if (!run)
13
+ return undefined;
14
+ return {
15
+ id: run.id,
16
+ runType: run.runType,
17
+ authorityEpoch: run.authorityEpoch,
18
+ status: run.status,
19
+ };
20
+ }
21
+ function readinessForTask(snapshot, task) {
22
+ if (task.type === "wait") {
23
+ return { action: "wait", reason: task.reason };
24
+ }
25
+ if (task.type === "ask") {
26
+ return {
27
+ action: "ask",
28
+ reason: task.reason,
29
+ question: typeof task.requirements?.question === "string" ? task.requirements.question : task.reason,
30
+ };
31
+ }
32
+ if (task.type === "escalate") {
33
+ return { action: "escalate", reason: task.reason };
34
+ }
35
+ return evaluateTaskStart(snapshot, task);
36
+ }
37
+ export function buildWorkflowSnapshotForIssue(db, issue) {
38
+ const activeRun = resolveActiveRunSnapshot(db, issue);
39
+ return projectWorkflowSnapshot({
40
+ issue,
41
+ observations: db.workflowObservations.listObservations(issue.projectId, issue.linearIssueId),
42
+ blockerCount: db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
43
+ childCount: db.issues.listCanonicalChildIssues(issue.projectId, issue.linearIssueId).length,
44
+ openChildCount: db.issues.countOpenChildIssues(issue.projectId, issue.linearIssueId),
45
+ ...(activeRun ? { activeRun } : {}),
46
+ });
47
+ }
48
+ export function reconcileWorkflowTasksForIssue(db, issue) {
49
+ const snapshot = buildWorkflowSnapshotForIssue(db, issue);
50
+ const result = db.workflowTasks.reconcileTasks({
51
+ projectId: issue.projectId,
52
+ subjectId: issue.linearIssueId,
53
+ tasks: snapshot.openTasks.map((task) => {
54
+ const decision = readinessForTask(snapshot, task);
55
+ return {
56
+ task,
57
+ authorityEpoch: snapshot.authority.epoch,
58
+ gateAction: decision.action,
59
+ ...("reason" in decision ? { gateReason: decision.reason } : {}),
60
+ };
61
+ }),
62
+ });
63
+ return { snapshot, result };
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.81.0",
3
+ "version": "0.83.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {