patchrelay 0.68.1 → 0.68.2
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/db/issue-store.js +10 -342
- package/dist/db/issue-upsert-columns.js +119 -0
- package/dist/idle-reconciliation-helpers.js +100 -0
- package/dist/idle-reconciliation.js +1 -96
- package/dist/webhooks/delegation-truth.js +52 -0
- package/dist/webhooks/desired-stage-recorder.js +9 -120
- package/dist/webhooks/issue-dependency-sync.js +45 -0
- package/dist/webhooks/linked-pr-adoption.js +41 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/db/issue-store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildInsertBindings, buildUpdateAssignments } from "./issue-upsert-columns.js";
|
|
1
2
|
import { isoNow } from "./shared.js";
|
|
2
3
|
export class IssueStore {
|
|
3
4
|
connection;
|
|
@@ -10,355 +11,22 @@ export class IssueStore {
|
|
|
10
11
|
const now = isoNow();
|
|
11
12
|
const existing = this.getIssue(params.projectId, params.linearIssueId);
|
|
12
13
|
if (existing) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
const { assignments, values } = buildUpdateAssignments(params);
|
|
15
|
+
const sql = `UPDATE issues SET ${["updated_at = @now", ...assignments].join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`;
|
|
16
|
+
this.connection.prepare(sql).run({
|
|
17
|
+
...values,
|
|
15
18
|
now,
|
|
16
19
|
projectId: params.projectId,
|
|
17
20
|
linearIssueId: params.linearIssueId,
|
|
18
|
-
};
|
|
19
|
-
if (params.delegatedToPatchRelay !== undefined) {
|
|
20
|
-
sets.push("delegated_to_patchrelay = @delegatedToPatchRelay");
|
|
21
|
-
values.delegatedToPatchRelay = params.delegatedToPatchRelay ? 1 : 0;
|
|
22
|
-
}
|
|
23
|
-
if (params.issueClass !== undefined) {
|
|
24
|
-
sets.push("issue_class = @issueClass");
|
|
25
|
-
values.issueClass = params.issueClass;
|
|
26
|
-
}
|
|
27
|
-
if (params.issueClassSource !== undefined) {
|
|
28
|
-
sets.push("issue_class_source = @issueClassSource");
|
|
29
|
-
values.issueClassSource = params.issueClassSource;
|
|
30
|
-
}
|
|
31
|
-
if (params.issueTriageHash !== undefined) {
|
|
32
|
-
sets.push("issue_triage_hash = @issueTriageHash");
|
|
33
|
-
values.issueTriageHash = params.issueTriageHash;
|
|
34
|
-
}
|
|
35
|
-
if (params.issueTriageResultJson !== undefined) {
|
|
36
|
-
sets.push("issue_triage_result_json = @issueTriageResultJson");
|
|
37
|
-
values.issueTriageResultJson = params.issueTriageResultJson;
|
|
38
|
-
}
|
|
39
|
-
if (params.parentLinearIssueId !== undefined) {
|
|
40
|
-
sets.push("parent_linear_issue_id = @parentLinearIssueId");
|
|
41
|
-
values.parentLinearIssueId = params.parentLinearIssueId;
|
|
42
|
-
}
|
|
43
|
-
if (params.parentIssueKey !== undefined) {
|
|
44
|
-
sets.push("parent_issue_key = @parentIssueKey");
|
|
45
|
-
values.parentIssueKey = params.parentIssueKey;
|
|
46
|
-
}
|
|
47
|
-
if (params.issueKey !== undefined) {
|
|
48
|
-
sets.push("issue_key = COALESCE(@issueKey, issue_key)");
|
|
49
|
-
values.issueKey = params.issueKey;
|
|
50
|
-
}
|
|
51
|
-
if (params.title !== undefined) {
|
|
52
|
-
sets.push("title = COALESCE(@title, title)");
|
|
53
|
-
values.title = params.title;
|
|
54
|
-
}
|
|
55
|
-
if (params.description !== undefined) {
|
|
56
|
-
sets.push("description = COALESCE(@description, description)");
|
|
57
|
-
values.description = params.description;
|
|
58
|
-
}
|
|
59
|
-
if (params.url !== undefined) {
|
|
60
|
-
sets.push("url = COALESCE(@url, url)");
|
|
61
|
-
values.url = params.url;
|
|
62
|
-
}
|
|
63
|
-
if (params.priority !== undefined) {
|
|
64
|
-
sets.push("priority = @priority");
|
|
65
|
-
values.priority = params.priority;
|
|
66
|
-
}
|
|
67
|
-
if (params.estimate !== undefined) {
|
|
68
|
-
sets.push("estimate = @estimate");
|
|
69
|
-
values.estimate = params.estimate;
|
|
70
|
-
}
|
|
71
|
-
if (params.currentLinearState !== undefined) {
|
|
72
|
-
sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
|
|
73
|
-
values.currentLinearState = params.currentLinearState;
|
|
74
|
-
}
|
|
75
|
-
if (params.currentLinearStateType !== undefined) {
|
|
76
|
-
sets.push("current_linear_state_type = COALESCE(@currentLinearStateType, current_linear_state_type)");
|
|
77
|
-
values.currentLinearStateType = params.currentLinearStateType;
|
|
78
|
-
}
|
|
79
|
-
if (params.factoryState !== undefined) {
|
|
80
|
-
sets.push("factory_state = @factoryState");
|
|
81
|
-
values.factoryState = params.factoryState;
|
|
82
|
-
}
|
|
83
|
-
if (params.pendingRunType !== undefined) {
|
|
84
|
-
sets.push("pending_run_type = @pendingRunType");
|
|
85
|
-
values.pendingRunType = params.pendingRunType;
|
|
86
|
-
}
|
|
87
|
-
if (params.pendingRunContextJson !== undefined) {
|
|
88
|
-
sets.push("pending_run_context_json = @pendingRunContextJson");
|
|
89
|
-
values.pendingRunContextJson = params.pendingRunContextJson;
|
|
90
|
-
}
|
|
91
|
-
if (params.branchName !== undefined) {
|
|
92
|
-
sets.push("branch_name = COALESCE(@branchName, branch_name)");
|
|
93
|
-
values.branchName = params.branchName;
|
|
94
|
-
}
|
|
95
|
-
if (params.worktreePath !== undefined) {
|
|
96
|
-
sets.push("worktree_path = COALESCE(@worktreePath, worktree_path)");
|
|
97
|
-
values.worktreePath = params.worktreePath;
|
|
98
|
-
}
|
|
99
|
-
if (params.threadId !== undefined) {
|
|
100
|
-
sets.push("thread_id = @threadId");
|
|
101
|
-
values.threadId = params.threadId;
|
|
102
|
-
}
|
|
103
|
-
if (params.activeRunId !== undefined) {
|
|
104
|
-
sets.push("active_run_id = @activeRunId");
|
|
105
|
-
values.activeRunId = params.activeRunId;
|
|
106
|
-
}
|
|
107
|
-
if (params.statusCommentId !== undefined) {
|
|
108
|
-
sets.push("status_comment_id = @statusCommentId");
|
|
109
|
-
values.statusCommentId = params.statusCommentId;
|
|
110
|
-
}
|
|
111
|
-
if (params.agentSessionId !== undefined) {
|
|
112
|
-
sets.push("agent_session_id = @agentSessionId");
|
|
113
|
-
values.agentSessionId = params.agentSessionId;
|
|
114
|
-
}
|
|
115
|
-
if (params.lastLinearActivityKey !== undefined) {
|
|
116
|
-
sets.push("last_linear_activity_key = @lastLinearActivityKey");
|
|
117
|
-
values.lastLinearActivityKey = params.lastLinearActivityKey;
|
|
118
|
-
}
|
|
119
|
-
if (params.prNumber !== undefined) {
|
|
120
|
-
sets.push("pr_number = @prNumber");
|
|
121
|
-
values.prNumber = params.prNumber;
|
|
122
|
-
}
|
|
123
|
-
if (params.prUrl !== undefined) {
|
|
124
|
-
sets.push("pr_url = @prUrl");
|
|
125
|
-
values.prUrl = params.prUrl;
|
|
126
|
-
}
|
|
127
|
-
if (params.prState !== undefined) {
|
|
128
|
-
sets.push("pr_state = @prState");
|
|
129
|
-
values.prState = params.prState;
|
|
130
|
-
}
|
|
131
|
-
if (params.prIsDraft !== undefined) {
|
|
132
|
-
sets.push("pr_is_draft = @prIsDraft");
|
|
133
|
-
values.prIsDraft = params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0;
|
|
134
|
-
}
|
|
135
|
-
if (params.prHeadSha !== undefined) {
|
|
136
|
-
sets.push("pr_head_sha = @prHeadSha");
|
|
137
|
-
values.prHeadSha = params.prHeadSha;
|
|
138
|
-
}
|
|
139
|
-
if (params.prAuthorLogin !== undefined) {
|
|
140
|
-
sets.push("pr_author_login = @prAuthorLogin");
|
|
141
|
-
values.prAuthorLogin = params.prAuthorLogin;
|
|
142
|
-
}
|
|
143
|
-
if (params.prReviewState !== undefined) {
|
|
144
|
-
sets.push("pr_review_state = @prReviewState");
|
|
145
|
-
values.prReviewState = params.prReviewState;
|
|
146
|
-
}
|
|
147
|
-
if (params.prCheckStatus !== undefined) {
|
|
148
|
-
sets.push("pr_check_status = @prCheckStatus");
|
|
149
|
-
values.prCheckStatus = params.prCheckStatus;
|
|
150
|
-
}
|
|
151
|
-
if (params.lastBlockingReviewHeadSha !== undefined) {
|
|
152
|
-
sets.push("last_blocking_review_head_sha = @lastBlockingReviewHeadSha");
|
|
153
|
-
values.lastBlockingReviewHeadSha = params.lastBlockingReviewHeadSha;
|
|
154
|
-
}
|
|
155
|
-
if (params.lastGitHubFailureSource !== undefined) {
|
|
156
|
-
sets.push("last_github_failure_source = @lastGitHubFailureSource");
|
|
157
|
-
values.lastGitHubFailureSource = params.lastGitHubFailureSource;
|
|
158
|
-
}
|
|
159
|
-
if (params.lastGitHubFailureHeadSha !== undefined) {
|
|
160
|
-
sets.push("last_github_failure_head_sha = @lastGitHubFailureHeadSha");
|
|
161
|
-
values.lastGitHubFailureHeadSha = params.lastGitHubFailureHeadSha;
|
|
162
|
-
}
|
|
163
|
-
if (params.lastGitHubFailureSignature !== undefined) {
|
|
164
|
-
sets.push("last_github_failure_signature = @lastGitHubFailureSignature");
|
|
165
|
-
values.lastGitHubFailureSignature = params.lastGitHubFailureSignature;
|
|
166
|
-
}
|
|
167
|
-
if (params.lastGitHubFailureCheckName !== undefined) {
|
|
168
|
-
sets.push("last_github_failure_check_name = @lastGitHubFailureCheckName");
|
|
169
|
-
values.lastGitHubFailureCheckName = params.lastGitHubFailureCheckName;
|
|
170
|
-
}
|
|
171
|
-
if (params.lastGitHubFailureCheckUrl !== undefined) {
|
|
172
|
-
sets.push("last_github_failure_check_url = @lastGitHubFailureCheckUrl");
|
|
173
|
-
values.lastGitHubFailureCheckUrl = params.lastGitHubFailureCheckUrl;
|
|
174
|
-
}
|
|
175
|
-
if (params.lastGitHubFailureContextJson !== undefined) {
|
|
176
|
-
sets.push("last_github_failure_context_json = @lastGitHubFailureContextJson");
|
|
177
|
-
values.lastGitHubFailureContextJson = params.lastGitHubFailureContextJson;
|
|
178
|
-
}
|
|
179
|
-
if (params.lastGitHubFailureAt !== undefined) {
|
|
180
|
-
sets.push("last_github_failure_at = @lastGitHubFailureAt");
|
|
181
|
-
values.lastGitHubFailureAt = params.lastGitHubFailureAt;
|
|
182
|
-
}
|
|
183
|
-
if (params.lastGitHubCiSnapshotHeadSha !== undefined) {
|
|
184
|
-
sets.push("last_github_ci_snapshot_head_sha = @lastGitHubCiSnapshotHeadSha");
|
|
185
|
-
values.lastGitHubCiSnapshotHeadSha = params.lastGitHubCiSnapshotHeadSha;
|
|
186
|
-
}
|
|
187
|
-
if (params.lastGitHubCiSnapshotGateCheckName !== undefined) {
|
|
188
|
-
sets.push("last_github_ci_snapshot_gate_check_name = @lastGitHubCiSnapshotGateCheckName");
|
|
189
|
-
values.lastGitHubCiSnapshotGateCheckName = params.lastGitHubCiSnapshotGateCheckName;
|
|
190
|
-
}
|
|
191
|
-
if (params.lastGitHubCiSnapshotGateCheckStatus !== undefined) {
|
|
192
|
-
sets.push("last_github_ci_snapshot_gate_check_status = @lastGitHubCiSnapshotGateCheckStatus");
|
|
193
|
-
values.lastGitHubCiSnapshotGateCheckStatus = params.lastGitHubCiSnapshotGateCheckStatus;
|
|
194
|
-
}
|
|
195
|
-
if (params.lastGitHubCiSnapshotJson !== undefined) {
|
|
196
|
-
sets.push("last_github_ci_snapshot_json = @lastGitHubCiSnapshotJson");
|
|
197
|
-
values.lastGitHubCiSnapshotJson = params.lastGitHubCiSnapshotJson;
|
|
198
|
-
}
|
|
199
|
-
if (params.lastGitHubCiSnapshotSettledAt !== undefined) {
|
|
200
|
-
sets.push("last_github_ci_snapshot_settled_at = @lastGitHubCiSnapshotSettledAt");
|
|
201
|
-
values.lastGitHubCiSnapshotSettledAt = params.lastGitHubCiSnapshotSettledAt;
|
|
202
|
-
}
|
|
203
|
-
if (params.lastQueueSignalAt !== undefined) {
|
|
204
|
-
sets.push("last_queue_signal_at = @lastQueueSignalAt");
|
|
205
|
-
values.lastQueueSignalAt = params.lastQueueSignalAt;
|
|
206
|
-
}
|
|
207
|
-
if (params.lastQueueIncidentJson !== undefined) {
|
|
208
|
-
sets.push("last_queue_incident_json = @lastQueueIncidentJson");
|
|
209
|
-
values.lastQueueIncidentJson = params.lastQueueIncidentJson;
|
|
210
|
-
}
|
|
211
|
-
if (params.lastAttemptedFailureHeadSha !== undefined) {
|
|
212
|
-
sets.push("last_attempted_failure_head_sha = @lastAttemptedFailureHeadSha");
|
|
213
|
-
values.lastAttemptedFailureHeadSha = params.lastAttemptedFailureHeadSha;
|
|
214
|
-
}
|
|
215
|
-
if (params.lastAttemptedFailureSignature !== undefined) {
|
|
216
|
-
sets.push("last_attempted_failure_signature = @lastAttemptedFailureSignature");
|
|
217
|
-
values.lastAttemptedFailureSignature = params.lastAttemptedFailureSignature;
|
|
218
|
-
}
|
|
219
|
-
if (params.lastAttemptedFailureAt !== undefined) {
|
|
220
|
-
sets.push("last_attempted_failure_at = @lastAttemptedFailureAt");
|
|
221
|
-
values.lastAttemptedFailureAt = params.lastAttemptedFailureAt;
|
|
222
|
-
}
|
|
223
|
-
if (params.lastPublishedPatchId !== undefined) {
|
|
224
|
-
sets.push("last_published_patch_id = @lastPublishedPatchId");
|
|
225
|
-
values.lastPublishedPatchId = params.lastPublishedPatchId;
|
|
226
|
-
}
|
|
227
|
-
if (params.lastPublishedIntegrationTreeId !== undefined) {
|
|
228
|
-
sets.push("last_published_integration_tree_id = @lastPublishedIntegrationTreeId");
|
|
229
|
-
values.lastPublishedIntegrationTreeId = params.lastPublishedIntegrationTreeId;
|
|
230
|
-
}
|
|
231
|
-
if (params.lastPublishedHeadSha !== undefined) {
|
|
232
|
-
sets.push("last_published_head_sha = @lastPublishedHeadSha");
|
|
233
|
-
values.lastPublishedHeadSha = params.lastPublishedHeadSha;
|
|
234
|
-
}
|
|
235
|
-
if (params.parentPrBranch !== undefined) {
|
|
236
|
-
sets.push("parent_pr_branch = @parentPrBranch");
|
|
237
|
-
values.parentPrBranch = params.parentPrBranch;
|
|
238
|
-
}
|
|
239
|
-
if (params.ciRepairAttempts !== undefined) {
|
|
240
|
-
sets.push("ci_repair_attempts = @ciRepairAttempts");
|
|
241
|
-
values.ciRepairAttempts = params.ciRepairAttempts;
|
|
242
|
-
}
|
|
243
|
-
if (params.queueRepairAttempts !== undefined) {
|
|
244
|
-
sets.push("queue_repair_attempts = @queueRepairAttempts");
|
|
245
|
-
values.queueRepairAttempts = params.queueRepairAttempts;
|
|
246
|
-
}
|
|
247
|
-
if (params.reviewFixAttempts !== undefined) {
|
|
248
|
-
sets.push("review_fix_attempts = @reviewFixAttempts");
|
|
249
|
-
values.reviewFixAttempts = params.reviewFixAttempts;
|
|
250
|
-
}
|
|
251
|
-
if (params.zombieRecoveryAttempts !== undefined) {
|
|
252
|
-
sets.push("zombie_recovery_attempts = @zombieRecoveryAttempts");
|
|
253
|
-
values.zombieRecoveryAttempts = params.zombieRecoveryAttempts;
|
|
254
|
-
}
|
|
255
|
-
if (params.lastZombieRecoveryAt !== undefined) {
|
|
256
|
-
sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
|
|
257
|
-
values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
|
|
258
|
-
}
|
|
259
|
-
if (params.orchestrationSettleUntil !== undefined) {
|
|
260
|
-
sets.push("orchestration_settle_until = @orchestrationSettleUntil");
|
|
261
|
-
values.orchestrationSettleUntil = params.orchestrationSettleUntil;
|
|
262
|
-
}
|
|
263
|
-
this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
|
|
21
|
+
});
|
|
264
22
|
}
|
|
265
23
|
else {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
271
|
-
branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
|
|
272
|
-
agent_session_id, last_linear_activity_key,
|
|
273
|
-
pr_number, pr_url, pr_state, pr_is_draft, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
|
|
274
|
-
last_github_failure_source, last_github_failure_head_sha, last_github_failure_signature, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_context_json, last_github_failure_at,
|
|
275
|
-
last_github_ci_snapshot_head_sha, last_github_ci_snapshot_gate_check_name, last_github_ci_snapshot_gate_check_status, last_github_ci_snapshot_json, last_github_ci_snapshot_settled_at,
|
|
276
|
-
last_queue_signal_at, last_queue_incident_json,
|
|
277
|
-
last_attempted_failure_head_sha, last_attempted_failure_signature, last_attempted_failure_at,
|
|
278
|
-
last_published_patch_id, last_published_integration_tree_id, last_published_head_sha,
|
|
279
|
-
parent_pr_branch,
|
|
280
|
-
ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at, orchestration_settle_until,
|
|
281
|
-
updated_at
|
|
282
|
-
) VALUES (
|
|
283
|
-
@projectId, @linearIssueId, @delegatedToPatchRelay, @issueClass, @issueClassSource, @issueTriageHash, @issueTriageResultJson, @parentLinearIssueId, @parentIssueKey, @issueKey, @title, @description, @url,
|
|
284
|
-
@priority, @estimate,
|
|
285
|
-
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
286
|
-
@branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
|
|
287
|
-
@agentSessionId, @lastLinearActivityKey,
|
|
288
|
-
@prNumber, @prUrl, @prState, @prIsDraft, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
|
|
289
|
-
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
|
|
290
|
-
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
291
|
-
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
292
|
-
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature, @lastAttemptedFailureAt,
|
|
293
|
-
@lastPublishedPatchId, @lastPublishedIntegrationTreeId, @lastPublishedHeadSha,
|
|
294
|
-
@parentPrBranch,
|
|
295
|
-
@ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt, @orchestrationSettleUntil,
|
|
296
|
-
@now
|
|
297
|
-
)
|
|
298
|
-
`).run({
|
|
24
|
+
const { columns, placeholders, values } = buildInsertBindings(params);
|
|
25
|
+
const sql = `INSERT INTO issues (project_id, linear_issue_id, ${columns.join(", ")}, updated_at) VALUES (@projectId, @linearIssueId, ${placeholders.join(", ")}, @now)`;
|
|
26
|
+
this.connection.prepare(sql).run({
|
|
27
|
+
...values,
|
|
299
28
|
projectId: params.projectId,
|
|
300
29
|
linearIssueId: params.linearIssueId,
|
|
301
|
-
delegatedToPatchRelay: params.delegatedToPatchRelay === false ? 0 : 1,
|
|
302
|
-
issueClass: params.issueClass ?? null,
|
|
303
|
-
issueClassSource: params.issueClassSource ?? null,
|
|
304
|
-
issueTriageHash: params.issueTriageHash ?? null,
|
|
305
|
-
issueTriageResultJson: params.issueTriageResultJson ?? null,
|
|
306
|
-
parentLinearIssueId: params.parentLinearIssueId ?? null,
|
|
307
|
-
parentIssueKey: params.parentIssueKey ?? null,
|
|
308
|
-
issueKey: params.issueKey ?? null,
|
|
309
|
-
title: params.title ?? null,
|
|
310
|
-
description: params.description ?? null,
|
|
311
|
-
url: params.url ?? null,
|
|
312
|
-
priority: params.priority ?? null,
|
|
313
|
-
estimate: params.estimate ?? null,
|
|
314
|
-
currentLinearState: params.currentLinearState ?? null,
|
|
315
|
-
currentLinearStateType: params.currentLinearStateType ?? null,
|
|
316
|
-
factoryState: params.factoryState ?? "delegated",
|
|
317
|
-
pendingRunType: params.pendingRunType ?? null,
|
|
318
|
-
pendingRunContextJson: params.pendingRunContextJson ?? null,
|
|
319
|
-
branchName: params.branchName ?? null,
|
|
320
|
-
worktreePath: params.worktreePath ?? null,
|
|
321
|
-
threadId: params.threadId ?? null,
|
|
322
|
-
activeRunId: params.activeRunId ?? null,
|
|
323
|
-
statusCommentId: params.statusCommentId ?? null,
|
|
324
|
-
agentSessionId: params.agentSessionId ?? null,
|
|
325
|
-
lastLinearActivityKey: params.lastLinearActivityKey ?? null,
|
|
326
|
-
prNumber: params.prNumber ?? null,
|
|
327
|
-
prUrl: params.prUrl ?? null,
|
|
328
|
-
prState: params.prState ?? null,
|
|
329
|
-
prIsDraft: params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0,
|
|
330
|
-
prHeadSha: params.prHeadSha ?? null,
|
|
331
|
-
prAuthorLogin: params.prAuthorLogin ?? null,
|
|
332
|
-
prReviewState: params.prReviewState ?? null,
|
|
333
|
-
prCheckStatus: params.prCheckStatus ?? null,
|
|
334
|
-
lastBlockingReviewHeadSha: params.lastBlockingReviewHeadSha ?? null,
|
|
335
|
-
lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
|
|
336
|
-
lastGitHubFailureHeadSha: params.lastGitHubFailureHeadSha ?? null,
|
|
337
|
-
lastGitHubFailureSignature: params.lastGitHubFailureSignature ?? null,
|
|
338
|
-
lastGitHubFailureCheckName: params.lastGitHubFailureCheckName ?? null,
|
|
339
|
-
lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
|
|
340
|
-
lastGitHubFailureContextJson: params.lastGitHubFailureContextJson ?? null,
|
|
341
|
-
lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
|
|
342
|
-
lastGitHubCiSnapshotHeadSha: params.lastGitHubCiSnapshotHeadSha ?? null,
|
|
343
|
-
lastGitHubCiSnapshotGateCheckName: params.lastGitHubCiSnapshotGateCheckName ?? null,
|
|
344
|
-
lastGitHubCiSnapshotGateCheckStatus: params.lastGitHubCiSnapshotGateCheckStatus ?? null,
|
|
345
|
-
lastGitHubCiSnapshotJson: params.lastGitHubCiSnapshotJson ?? null,
|
|
346
|
-
lastGitHubCiSnapshotSettledAt: params.lastGitHubCiSnapshotSettledAt ?? null,
|
|
347
|
-
lastQueueSignalAt: params.lastQueueSignalAt ?? null,
|
|
348
|
-
lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
|
|
349
|
-
lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
|
|
350
|
-
lastAttemptedFailureSignature: params.lastAttemptedFailureSignature ?? null,
|
|
351
|
-
lastAttemptedFailureAt: params.lastAttemptedFailureAt ?? null,
|
|
352
|
-
lastPublishedPatchId: params.lastPublishedPatchId ?? null,
|
|
353
|
-
lastPublishedIntegrationTreeId: params.lastPublishedIntegrationTreeId ?? null,
|
|
354
|
-
lastPublishedHeadSha: params.lastPublishedHeadSha ?? null,
|
|
355
|
-
parentPrBranch: params.parentPrBranch ?? null,
|
|
356
|
-
ciRepairAttempts: params.ciRepairAttempts ?? 0,
|
|
357
|
-
queueRepairAttempts: params.queueRepairAttempts ?? 0,
|
|
358
|
-
reviewFixAttempts: params.reviewFixAttempts ?? 0,
|
|
359
|
-
zombieRecoveryAttempts: params.zombieRecoveryAttempts ?? 0,
|
|
360
|
-
lastZombieRecoveryAt: params.lastZombieRecoveryAt ?? null,
|
|
361
|
-
orchestrationSettleUntil: params.orchestrationSettleUntil ?? null,
|
|
362
30
|
now,
|
|
363
31
|
});
|
|
364
32
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const booleanToInt = (value) => value === true || value === 1 ? 1 : 0;
|
|
2
|
+
const nullableBooleanToInt = (value) => {
|
|
3
|
+
if (value == null)
|
|
4
|
+
return null;
|
|
5
|
+
return value === true || value === 1 ? 1 : 0;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Source-of-truth column map for `upsertIssue`. Ordered to match the INSERT
|
|
9
|
+
* column list so a quick read shows the table shape end-to-end. Adding a new
|
|
10
|
+
* field is a one-line addition here instead of editing three parallel lists.
|
|
11
|
+
*/
|
|
12
|
+
export const ISSUE_COLUMN_DEFS = {
|
|
13
|
+
delegatedToPatchRelay: { column: "delegated_to_patchrelay", transform: booleanToInt, insertDefault: 1 },
|
|
14
|
+
issueClass: { column: "issue_class" },
|
|
15
|
+
issueClassSource: { column: "issue_class_source" },
|
|
16
|
+
issueTriageHash: { column: "issue_triage_hash" },
|
|
17
|
+
issueTriageResultJson: { column: "issue_triage_result_json" },
|
|
18
|
+
parentLinearIssueId: { column: "parent_linear_issue_id" },
|
|
19
|
+
parentIssueKey: { column: "parent_issue_key" },
|
|
20
|
+
issueKey: { column: "issue_key", coalesce: true },
|
|
21
|
+
title: { column: "title", coalesce: true },
|
|
22
|
+
description: { column: "description", coalesce: true },
|
|
23
|
+
url: { column: "url", coalesce: true },
|
|
24
|
+
priority: { column: "priority" },
|
|
25
|
+
estimate: { column: "estimate" },
|
|
26
|
+
currentLinearState: { column: "current_linear_state", coalesce: true },
|
|
27
|
+
currentLinearStateType: { column: "current_linear_state_type", coalesce: true },
|
|
28
|
+
factoryState: { column: "factory_state", insertDefault: "delegated" },
|
|
29
|
+
pendingRunType: { column: "pending_run_type" },
|
|
30
|
+
pendingRunContextJson: { column: "pending_run_context_json" },
|
|
31
|
+
branchName: { column: "branch_name", coalesce: true },
|
|
32
|
+
worktreePath: { column: "worktree_path", coalesce: true },
|
|
33
|
+
threadId: { column: "thread_id" },
|
|
34
|
+
activeRunId: { column: "active_run_id" },
|
|
35
|
+
statusCommentId: { column: "status_comment_id" },
|
|
36
|
+
agentSessionId: { column: "agent_session_id" },
|
|
37
|
+
lastLinearActivityKey: { column: "last_linear_activity_key" },
|
|
38
|
+
prNumber: { column: "pr_number" },
|
|
39
|
+
prUrl: { column: "pr_url" },
|
|
40
|
+
prState: { column: "pr_state" },
|
|
41
|
+
prIsDraft: { column: "pr_is_draft", transform: nullableBooleanToInt },
|
|
42
|
+
prHeadSha: { column: "pr_head_sha" },
|
|
43
|
+
prAuthorLogin: { column: "pr_author_login" },
|
|
44
|
+
prReviewState: { column: "pr_review_state" },
|
|
45
|
+
prCheckStatus: { column: "pr_check_status" },
|
|
46
|
+
lastBlockingReviewHeadSha: { column: "last_blocking_review_head_sha" },
|
|
47
|
+
lastGitHubFailureSource: { column: "last_github_failure_source" },
|
|
48
|
+
lastGitHubFailureHeadSha: { column: "last_github_failure_head_sha" },
|
|
49
|
+
lastGitHubFailureSignature: { column: "last_github_failure_signature" },
|
|
50
|
+
lastGitHubFailureCheckName: { column: "last_github_failure_check_name" },
|
|
51
|
+
lastGitHubFailureCheckUrl: { column: "last_github_failure_check_url" },
|
|
52
|
+
lastGitHubFailureContextJson: { column: "last_github_failure_context_json" },
|
|
53
|
+
lastGitHubFailureAt: { column: "last_github_failure_at" },
|
|
54
|
+
lastGitHubCiSnapshotHeadSha: { column: "last_github_ci_snapshot_head_sha" },
|
|
55
|
+
lastGitHubCiSnapshotGateCheckName: { column: "last_github_ci_snapshot_gate_check_name" },
|
|
56
|
+
lastGitHubCiSnapshotGateCheckStatus: { column: "last_github_ci_snapshot_gate_check_status" },
|
|
57
|
+
lastGitHubCiSnapshotJson: { column: "last_github_ci_snapshot_json" },
|
|
58
|
+
lastGitHubCiSnapshotSettledAt: { column: "last_github_ci_snapshot_settled_at" },
|
|
59
|
+
lastQueueSignalAt: { column: "last_queue_signal_at" },
|
|
60
|
+
lastQueueIncidentJson: { column: "last_queue_incident_json" },
|
|
61
|
+
lastAttemptedFailureHeadSha: { column: "last_attempted_failure_head_sha" },
|
|
62
|
+
lastAttemptedFailureSignature: { column: "last_attempted_failure_signature" },
|
|
63
|
+
lastAttemptedFailureAt: { column: "last_attempted_failure_at" },
|
|
64
|
+
lastPublishedPatchId: { column: "last_published_patch_id" },
|
|
65
|
+
lastPublishedIntegrationTreeId: { column: "last_published_integration_tree_id" },
|
|
66
|
+
lastPublishedHeadSha: { column: "last_published_head_sha" },
|
|
67
|
+
parentPrBranch: { column: "parent_pr_branch" },
|
|
68
|
+
ciRepairAttempts: { column: "ci_repair_attempts", insertDefault: 0 },
|
|
69
|
+
queueRepairAttempts: { column: "queue_repair_attempts", insertDefault: 0 },
|
|
70
|
+
reviewFixAttempts: { column: "review_fix_attempts", insertDefault: 0 },
|
|
71
|
+
zombieRecoveryAttempts: { column: "zombie_recovery_attempts", insertDefault: 0 },
|
|
72
|
+
lastZombieRecoveryAt: { column: "last_zombie_recovery_at" },
|
|
73
|
+
orchestrationSettleUntil: { column: "orchestration_settle_until" },
|
|
74
|
+
};
|
|
75
|
+
export const ISSUE_COLUMN_KEYS = Object.keys(ISSUE_COLUMN_DEFS);
|
|
76
|
+
/**
|
|
77
|
+
* Builds the `SET col = @param` fragments and the bound-value bag for an
|
|
78
|
+
* UPDATE. Only fields explicitly set in `params` are included so we don't
|
|
79
|
+
* accidentally clobber other columns.
|
|
80
|
+
*/
|
|
81
|
+
export function buildUpdateAssignments(params) {
|
|
82
|
+
const assignments = [];
|
|
83
|
+
const values = {};
|
|
84
|
+
for (const key of ISSUE_COLUMN_KEYS) {
|
|
85
|
+
const value = params[key];
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
continue;
|
|
88
|
+
const def = ISSUE_COLUMN_DEFS[key];
|
|
89
|
+
const bound = def.transform ? def.transform(value) : value;
|
|
90
|
+
assignments.push(def.coalesce
|
|
91
|
+
? `${def.column} = COALESCE(@${key}, ${def.column})`
|
|
92
|
+
: `${def.column} = @${key}`);
|
|
93
|
+
values[key] = bound;
|
|
94
|
+
}
|
|
95
|
+
return { assignments, values };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Builds the columns list, the placeholders list, and the bound-value bag for
|
|
99
|
+
* an INSERT. Every column is emitted (NULL if the param is omitted and no
|
|
100
|
+
* `insertDefault` is set) so the SQL stays stable across calls.
|
|
101
|
+
*/
|
|
102
|
+
export function buildInsertBindings(params) {
|
|
103
|
+
const columns = [];
|
|
104
|
+
const placeholders = [];
|
|
105
|
+
const values = {};
|
|
106
|
+
for (const key of ISSUE_COLUMN_KEYS) {
|
|
107
|
+
const def = ISSUE_COLUMN_DEFS[key];
|
|
108
|
+
columns.push(def.column);
|
|
109
|
+
placeholders.push(`@${key}`);
|
|
110
|
+
const raw = params[key];
|
|
111
|
+
const resolved = raw !== undefined ? raw : def.insertDefault;
|
|
112
|
+
if (resolved === undefined) {
|
|
113
|
+
values[key] = null;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
values[key] = def.transform ? def.transform(resolved) : resolved;
|
|
117
|
+
}
|
|
118
|
+
return { columns, placeholders, values };
|
|
119
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
2
|
+
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
3
|
+
export function isFailingCheckStatus(status) {
|
|
4
|
+
return status === "failed" || status === "failure";
|
|
5
|
+
}
|
|
6
|
+
export function isReviewDecisionApproved(value) {
|
|
7
|
+
return value?.trim().toUpperCase() === "APPROVED";
|
|
8
|
+
}
|
|
9
|
+
export function isReviewDecisionChangesRequested(value) {
|
|
10
|
+
return value?.trim().toUpperCase() === "CHANGES_REQUESTED";
|
|
11
|
+
}
|
|
12
|
+
export function isReviewDecisionReviewRequired(value) {
|
|
13
|
+
return value?.trim().toUpperCase() === "REVIEW_REQUIRED";
|
|
14
|
+
}
|
|
15
|
+
export function buildBranchUpkeepContext(prNumber, baseBranch, mergeStateStatus, headSha) {
|
|
16
|
+
const promptContext = [
|
|
17
|
+
`The requested code change may already be present, but GitHub still reports PR #${prNumber} as ${mergeStateStatus ?? "DIRTY"} against latest ${baseBranch}.`,
|
|
18
|
+
`This turn is branch upkeep on the existing PR branch: update onto latest ${baseBranch}, resolve any conflicts, rerun the narrowest relevant verification, and push a newer head.`,
|
|
19
|
+
"Do not stop just because the requested code change is already present. Review can only move forward after a new pushed head.",
|
|
20
|
+
].join(" ");
|
|
21
|
+
return {
|
|
22
|
+
branchUpkeepRequired: true,
|
|
23
|
+
reviewFixMode: "branch_upkeep",
|
|
24
|
+
wakeReason: "branch_upkeep",
|
|
25
|
+
promptContext,
|
|
26
|
+
...(mergeStateStatus ? { mergeStateStatus } : {}),
|
|
27
|
+
...(headSha ? { failingHeadSha: headSha } : {}),
|
|
28
|
+
baseBranch,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function hasCompletedReviewQuillVerdict(entries) {
|
|
32
|
+
return (entries ?? []).some((entry) => entry.__typename === "CheckRun"
|
|
33
|
+
&& entry.name === "review-quill/verdict"
|
|
34
|
+
&& entry.status === "COMPLETED");
|
|
35
|
+
}
|
|
36
|
+
export function getGateCheckNames(project) {
|
|
37
|
+
const configured = project?.gateChecks?.map((entry) => entry.trim()).filter(Boolean) ?? [];
|
|
38
|
+
return configured.length > 0 ? configured : ["verify"];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A repair attempt is "duplicate" when we have already tried to repair the
|
|
42
|
+
* exact same failure (same signature, same head SHA) AND no newer failure
|
|
43
|
+
* has been observed since that attempt was recorded. For queue evictions
|
|
44
|
+
* the PR head doesn't advance between attempts, so we additionally compare
|
|
45
|
+
* the timestamps: a fresh incident after `main` advances looks identical
|
|
46
|
+
* to a stale one without the timestamp check.
|
|
47
|
+
*/
|
|
48
|
+
export function isDuplicateRepairAttempt(issue, context) {
|
|
49
|
+
const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
|
|
50
|
+
const headSha = typeof context?.failureHeadSha === "string"
|
|
51
|
+
? context.failureHeadSha
|
|
52
|
+
: typeof context?.headSha === "string" ? context.headSha : undefined;
|
|
53
|
+
if (!signature)
|
|
54
|
+
return false;
|
|
55
|
+
if (issue.lastAttemptedFailureSignature !== signature)
|
|
56
|
+
return false;
|
|
57
|
+
if (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha)
|
|
58
|
+
return false;
|
|
59
|
+
if (issue.lastAttemptedFailureAt && issue.lastGitHubFailureAt
|
|
60
|
+
&& issue.lastGitHubFailureAt > issue.lastAttemptedFailureAt) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
export function buildFailureContext(issue) {
|
|
66
|
+
const storedFailureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
67
|
+
const queueRepairContext = issue.lastQueueIncidentJson
|
|
68
|
+
? parseStoredQueueRepairContext(issue.lastQueueIncidentJson)
|
|
69
|
+
: undefined;
|
|
70
|
+
if (!queueRepairContext
|
|
71
|
+
&& !issue.lastGitHubFailureSource
|
|
72
|
+
&& !issue.lastGitHubFailureHeadSha
|
|
73
|
+
&& !issue.lastGitHubFailureSignature
|
|
74
|
+
&& !issue.lastGitHubFailureCheckName
|
|
75
|
+
&& !issue.lastGitHubFailureCheckUrl
|
|
76
|
+
&& !storedFailureContext) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...(issue.lastGitHubFailureSource ? { failureReason: issue.lastGitHubFailureSource } : {}),
|
|
81
|
+
...(issue.lastGitHubFailureHeadSha ? { failureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
|
|
82
|
+
...(issue.lastGitHubFailureSignature ? { failureSignature: issue.lastGitHubFailureSignature } : {}),
|
|
83
|
+
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
84
|
+
...(issue.lastGitHubFailureCheckUrl ? { checkUrl: issue.lastGitHubFailureCheckUrl } : {}),
|
|
85
|
+
...(storedFailureContext ? storedFailureContext : {}),
|
|
86
|
+
...(queueRepairContext ? queueRepairContext : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function hasFailureProvenance(issue) {
|
|
90
|
+
return Boolean(issue.lastGitHubFailureSource
|
|
91
|
+
|| issue.lastGitHubFailureHeadSha
|
|
92
|
+
|| issue.lastGitHubFailureSignature
|
|
93
|
+
|| issue.lastGitHubFailureCheckName
|
|
94
|
+
|| issue.lastGitHubFailureCheckUrl
|
|
95
|
+
|| issue.lastGitHubFailureContextJson
|
|
96
|
+
|| issue.lastGitHubFailureAt
|
|
97
|
+
|| issue.lastQueueIncidentJson
|
|
98
|
+
|| issue.lastAttemptedFailureHeadSha
|
|
99
|
+
|| issue.lastAttemptedFailureSignature);
|
|
100
|
+
}
|
|
@@ -1,107 +1,12 @@
|
|
|
1
|
+
import { buildBranchUpkeepContext, buildFailureContext, getGateCheckNames, hasCompletedReviewQuillVerdict, hasFailureProvenance, isDuplicateRepairAttempt, isFailingCheckStatus, isReviewDecisionApproved, isReviewDecisionChangesRequested, isReviewDecisionReviewRequired, } from "./idle-reconciliation-helpers.js";
|
|
1
2
|
import { isMainRepairIssue } from "./main-repair.js";
|
|
2
3
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
3
|
-
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
4
4
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
5
5
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
6
|
-
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
7
6
|
import { buildClosedPrCleanupFields, resolveClosedPrDisposition } from "./pr-state.js";
|
|
8
7
|
import { getReviewFixBudget } from "./run-budgets.js";
|
|
9
8
|
import { queueSettledOrchestrationIssue } from "./orchestration-parent-wake.js";
|
|
10
9
|
import { execCommand } from "./utils.js";
|
|
11
|
-
function isFailingCheckStatus(status) {
|
|
12
|
-
return status === "failed" || status === "failure";
|
|
13
|
-
}
|
|
14
|
-
function isReviewDecisionApproved(value) {
|
|
15
|
-
return value?.trim().toUpperCase() === "APPROVED";
|
|
16
|
-
}
|
|
17
|
-
function isReviewDecisionChangesRequested(value) {
|
|
18
|
-
return value?.trim().toUpperCase() === "CHANGES_REQUESTED";
|
|
19
|
-
}
|
|
20
|
-
function isReviewDecisionReviewRequired(value) {
|
|
21
|
-
return value?.trim().toUpperCase() === "REVIEW_REQUIRED";
|
|
22
|
-
}
|
|
23
|
-
function buildBranchUpkeepContext(prNumber, baseBranch, mergeStateStatus, headSha) {
|
|
24
|
-
const promptContext = [
|
|
25
|
-
`The requested code change may already be present, but GitHub still reports PR #${prNumber} as ${mergeStateStatus ?? "DIRTY"} against latest ${baseBranch}.`,
|
|
26
|
-
`This turn is branch upkeep on the existing PR branch: update onto latest ${baseBranch}, resolve any conflicts, rerun the narrowest relevant verification, and push a newer head.`,
|
|
27
|
-
"Do not stop just because the requested code change is already present. Review can only move forward after a new pushed head.",
|
|
28
|
-
].join(" ");
|
|
29
|
-
return {
|
|
30
|
-
branchUpkeepRequired: true,
|
|
31
|
-
reviewFixMode: "branch_upkeep",
|
|
32
|
-
wakeReason: "branch_upkeep",
|
|
33
|
-
promptContext,
|
|
34
|
-
...(mergeStateStatus ? { mergeStateStatus } : {}),
|
|
35
|
-
...(headSha ? { failingHeadSha: headSha } : {}),
|
|
36
|
-
baseBranch,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function hasCompletedReviewQuillVerdict(entries) {
|
|
40
|
-
return (entries ?? []).some((entry) => entry.__typename === "CheckRun"
|
|
41
|
-
&& entry.name === "review-quill/verdict"
|
|
42
|
-
&& entry.status === "COMPLETED");
|
|
43
|
-
}
|
|
44
|
-
function getGateCheckNames(project) {
|
|
45
|
-
const configured = project?.gateChecks?.map((entry) => entry.trim()).filter(Boolean) ?? [];
|
|
46
|
-
return configured.length > 0 ? configured : ["verify"];
|
|
47
|
-
}
|
|
48
|
-
function isDuplicateRepairAttempt(issue, context) {
|
|
49
|
-
const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
|
|
50
|
-
const headSha = typeof context?.failureHeadSha === "string"
|
|
51
|
-
? context.failureHeadSha
|
|
52
|
-
: typeof context?.headSha === "string" ? context.headSha : undefined;
|
|
53
|
-
if (!signature)
|
|
54
|
-
return false;
|
|
55
|
-
if (issue.lastAttemptedFailureSignature !== signature)
|
|
56
|
-
return false;
|
|
57
|
-
if (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha)
|
|
58
|
-
return false;
|
|
59
|
-
// A signature+headSha match alone isn't enough: for queue evictions the PR head
|
|
60
|
-
// doesn't advance (we haven't pushed) and the steward's check name is constant,
|
|
61
|
-
// so a fresh incident after main advances looks identical. Treat the attempt as
|
|
62
|
-
// stale if a newer failure has been observed since it was recorded.
|
|
63
|
-
if (issue.lastAttemptedFailureAt && issue.lastGitHubFailureAt
|
|
64
|
-
&& issue.lastGitHubFailureAt > issue.lastAttemptedFailureAt) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
function buildFailureContext(issue) {
|
|
70
|
-
const storedFailureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
71
|
-
const queueRepairContext = issue.lastQueueIncidentJson
|
|
72
|
-
? parseStoredQueueRepairContext(issue.lastQueueIncidentJson)
|
|
73
|
-
: undefined;
|
|
74
|
-
if (!queueRepairContext
|
|
75
|
-
&& !issue.lastGitHubFailureSource
|
|
76
|
-
&& !issue.lastGitHubFailureHeadSha
|
|
77
|
-
&& !issue.lastGitHubFailureSignature
|
|
78
|
-
&& !issue.lastGitHubFailureCheckName
|
|
79
|
-
&& !issue.lastGitHubFailureCheckUrl
|
|
80
|
-
&& !storedFailureContext) {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
...(issue.lastGitHubFailureSource ? { failureReason: issue.lastGitHubFailureSource } : {}),
|
|
85
|
-
...(issue.lastGitHubFailureHeadSha ? { failureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
|
|
86
|
-
...(issue.lastGitHubFailureSignature ? { failureSignature: issue.lastGitHubFailureSignature } : {}),
|
|
87
|
-
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
88
|
-
...(issue.lastGitHubFailureCheckUrl ? { checkUrl: issue.lastGitHubFailureCheckUrl } : {}),
|
|
89
|
-
...(storedFailureContext ? storedFailureContext : {}),
|
|
90
|
-
...(queueRepairContext ? queueRepairContext : {}),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
function hasFailureProvenance(issue) {
|
|
94
|
-
return Boolean(issue.lastGitHubFailureSource
|
|
95
|
-
|| issue.lastGitHubFailureHeadSha
|
|
96
|
-
|| issue.lastGitHubFailureSignature
|
|
97
|
-
|| issue.lastGitHubFailureCheckName
|
|
98
|
-
|| issue.lastGitHubFailureCheckUrl
|
|
99
|
-
|| issue.lastGitHubFailureContextJson
|
|
100
|
-
|| issue.lastGitHubFailureAt
|
|
101
|
-
|| issue.lastQueueIncidentJson
|
|
102
|
-
|| issue.lastAttemptedFailureHeadSha
|
|
103
|
-
|| issue.lastAttemptedFailureSignature);
|
|
104
|
-
}
|
|
105
10
|
export class IdleIssueReconciler {
|
|
106
11
|
db;
|
|
107
12
|
config;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { appendDelegationObservedEvent } from "../delegation-audit.js";
|
|
2
|
+
export function isDelegatedToPatchRelay(db, project, issue) {
|
|
3
|
+
const installation = db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
4
|
+
if (!installation?.actorId)
|
|
5
|
+
return false;
|
|
6
|
+
return issue.delegateId === installation.actorId;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolves whether the issue is currently delegated to PatchRelay, applying
|
|
10
|
+
* the "preserve previous value when the webhook didn't carry a delegate
|
|
11
|
+
* identity" guard so a stale webhook can't accidentally un-delegate the
|
|
12
|
+
* issue. Emits a `delegation_observed` audit entry whenever the resolved
|
|
13
|
+
* value diverges from what we previously stored, so the audit log captures
|
|
14
|
+
* both raw observation and applied decision.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveDelegationTruth(input) {
|
|
17
|
+
const previousDelegated = input.existingIssue?.delegatedToPatchRelay;
|
|
18
|
+
const observedDelegated = isDelegatedToPatchRelay(input.db, input.project, input.hydratedIssue);
|
|
19
|
+
const explicitDelegateSignal = input.triggerEvent === "delegateChanged";
|
|
20
|
+
const hasObservedDelegate = input.hydratedIssue.delegateId !== undefined;
|
|
21
|
+
let delegated = observedDelegated;
|
|
22
|
+
let reason = hasObservedDelegate
|
|
23
|
+
? "delegate_id_present"
|
|
24
|
+
: `missing_delegate_identity_after_${input.hydration}`;
|
|
25
|
+
if (!hasObservedDelegate && !explicitDelegateSignal && previousDelegated !== undefined) {
|
|
26
|
+
delegated = previousDelegated;
|
|
27
|
+
reason = `preserved_previous_delegation_after_${input.hydration}`;
|
|
28
|
+
}
|
|
29
|
+
if (previousDelegated !== delegated
|
|
30
|
+
|| input.hydration === "live_linear_failed"
|
|
31
|
+
|| (!hasObservedDelegate && previousDelegated !== undefined)) {
|
|
32
|
+
appendDelegationObservedEvent(input.db, {
|
|
33
|
+
projectId: input.project.id,
|
|
34
|
+
linearIssueId: input.normalizedIssue.id,
|
|
35
|
+
payload: {
|
|
36
|
+
source: "linear_webhook",
|
|
37
|
+
webhookId: input.webhookId,
|
|
38
|
+
triggerEvent: input.triggerEvent,
|
|
39
|
+
...(input.actorId ? { actorId: input.actorId } : {}),
|
|
40
|
+
...(input.hydratedIssue.delegateId ? { observedDelegateId: input.hydratedIssue.delegateId } : {}),
|
|
41
|
+
...(previousDelegated !== undefined ? { previousDelegatedToPatchRelay: previousDelegated } : {}),
|
|
42
|
+
observedDelegatedToPatchRelay: observedDelegated,
|
|
43
|
+
appliedDelegatedToPatchRelay: delegated,
|
|
44
|
+
hydration: input.hydration,
|
|
45
|
+
...(input.activeRunId !== undefined ? { activeRunId: input.activeRunId } : {}),
|
|
46
|
+
decision: "none",
|
|
47
|
+
reason,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return { delegated };
|
|
52
|
+
}
|
|
@@ -2,12 +2,11 @@ import { classifyIssue } from "../issue-class.js";
|
|
|
2
2
|
import { computeOrchestrationSettleUntil, wakeOrchestrationParentsForChildEvent, } from "../orchestration-parent-wake.js";
|
|
3
3
|
import { triggerEventAllowed } from "../project-resolution.js";
|
|
4
4
|
import { resolveAwaitingInputReason } from "../awaiting-input-reason.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { decideActiveRunRelease, decideAgentSession, decideRunIntent, decideUnDelegation, isResolvedLinearState, isTerminalDelegationState, resolveReDelegationResume, } from "./decision-helpers.js";
|
|
6
|
+
import { isDelegatedToPatchRelay, resolveDelegationTruth } from "./delegation-truth.js";
|
|
7
|
+
import { syncIssueDependencies } from "./issue-dependency-sync.js";
|
|
8
|
+
import { resolveLinkedPrAdoption } from "./linked-pr-adoption.js";
|
|
7
9
|
import { buildOperatorRetryEvent } from "../operator-retry-event.js";
|
|
8
|
-
import { resolveLinkedPullRequest } from "../linear-linked-pr-reconciliation.js";
|
|
9
|
-
import { readRemotePrState } from "../remote-pr-state.js";
|
|
10
|
-
import { deriveLinkedPrAdoptionOutcome } from "../delegation-linked-pr.js";
|
|
11
10
|
export class DesiredStageRecorder {
|
|
12
11
|
db;
|
|
13
12
|
linearProvider;
|
|
@@ -30,12 +29,13 @@ export class DesiredStageRecorder {
|
|
|
30
29
|
const triggerAllowed = triggerEventAllowed(params.project, params.normalized.triggerEvent);
|
|
31
30
|
const incomingAgentSessionId = params.normalized.agentSession?.id;
|
|
32
31
|
const hasPendingWake = this.db.issueSessions.peekIssueSessionWake(params.project.id, normalizedIssue.id) !== undefined;
|
|
33
|
-
if (!existingIssue && !this.
|
|
32
|
+
if (!existingIssue && !isDelegatedToPatchRelay(this.db, params.project, normalizedIssue) && !incomingAgentSessionId) {
|
|
34
33
|
return { issue: undefined, wakeRunType: undefined, delegated: false };
|
|
35
34
|
}
|
|
36
|
-
const syncResult = await this.
|
|
35
|
+
const syncResult = await syncIssueDependencies(this.db, this.linearProvider, params.project.id, normalizedIssue);
|
|
37
36
|
const hydratedIssue = syncResult.issue;
|
|
38
|
-
const delegation =
|
|
37
|
+
const delegation = resolveDelegationTruth({
|
|
38
|
+
db: this.db,
|
|
39
39
|
project: params.project,
|
|
40
40
|
normalizedIssue,
|
|
41
41
|
hydratedIssue,
|
|
@@ -47,7 +47,7 @@ export class DesiredStageRecorder {
|
|
|
47
47
|
activeRunId: activeRun?.id,
|
|
48
48
|
});
|
|
49
49
|
const delegated = delegation.delegated;
|
|
50
|
-
const linkedPrAdoption = await
|
|
50
|
+
const linkedPrAdoption = await resolveLinkedPrAdoption({
|
|
51
51
|
project: params.project,
|
|
52
52
|
issue: hydratedIssue,
|
|
53
53
|
existingIssue,
|
|
@@ -324,115 +324,4 @@ export class DesiredStageRecorder {
|
|
|
324
324
|
delegated,
|
|
325
325
|
};
|
|
326
326
|
}
|
|
327
|
-
isDelegatedToPatchRelay(project, issue) {
|
|
328
|
-
const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
|
|
329
|
-
if (!installation?.actorId)
|
|
330
|
-
return false;
|
|
331
|
-
return issue.delegateId === installation.actorId;
|
|
332
|
-
}
|
|
333
|
-
resolveDelegationTruth(params) {
|
|
334
|
-
const previousDelegated = params.existingIssue?.delegatedToPatchRelay;
|
|
335
|
-
const observedDelegated = this.isDelegatedToPatchRelay(params.project, params.hydratedIssue);
|
|
336
|
-
const explicitDelegateSignal = params.triggerEvent === "delegateChanged";
|
|
337
|
-
const hasObservedDelegate = params.hydratedIssue.delegateId !== undefined;
|
|
338
|
-
let delegated = observedDelegated;
|
|
339
|
-
let reason = hasObservedDelegate
|
|
340
|
-
? "delegate_id_present"
|
|
341
|
-
: `missing_delegate_identity_after_${params.hydration}`;
|
|
342
|
-
if (!hasObservedDelegate && !explicitDelegateSignal && previousDelegated !== undefined) {
|
|
343
|
-
delegated = previousDelegated;
|
|
344
|
-
reason = `preserved_previous_delegation_after_${params.hydration}`;
|
|
345
|
-
}
|
|
346
|
-
if (previousDelegated !== delegated
|
|
347
|
-
|| params.hydration === "live_linear_failed"
|
|
348
|
-
|| (!hasObservedDelegate && previousDelegated !== undefined)) {
|
|
349
|
-
appendDelegationObservedEvent(this.db, {
|
|
350
|
-
projectId: params.project.id,
|
|
351
|
-
linearIssueId: params.normalizedIssue.id,
|
|
352
|
-
payload: {
|
|
353
|
-
source: "linear_webhook",
|
|
354
|
-
webhookId: params.webhookId,
|
|
355
|
-
triggerEvent: params.triggerEvent,
|
|
356
|
-
...(params.actorId ? { actorId: params.actorId } : {}),
|
|
357
|
-
...(params.hydratedIssue.delegateId ? { observedDelegateId: params.hydratedIssue.delegateId } : {}),
|
|
358
|
-
...(previousDelegated !== undefined ? { previousDelegatedToPatchRelay: previousDelegated } : {}),
|
|
359
|
-
observedDelegatedToPatchRelay: observedDelegated,
|
|
360
|
-
appliedDelegatedToPatchRelay: delegated,
|
|
361
|
-
hydration: params.hydration,
|
|
362
|
-
...(params.activeRunId !== undefined ? { activeRunId: params.activeRunId } : {}),
|
|
363
|
-
decision: "none",
|
|
364
|
-
reason,
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
return { delegated };
|
|
369
|
-
}
|
|
370
|
-
async syncIssueDependencies(projectId, issue) {
|
|
371
|
-
let source = issue;
|
|
372
|
-
let hydration = "webhook_only";
|
|
373
|
-
if (!source.relationsKnown) {
|
|
374
|
-
const linear = await this.linearProvider.forProject(projectId);
|
|
375
|
-
if (linear) {
|
|
376
|
-
try {
|
|
377
|
-
source = mergeIssueMetadata(source, await linear.getIssue(issue.id));
|
|
378
|
-
hydration = "live_linear";
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
// Preserve existing dependency rows when webhook relation data is incomplete.
|
|
382
|
-
hydration = "live_linear_failed";
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
if (source.relationsKnown) {
|
|
387
|
-
this.db.issues.replaceIssueDependencies({
|
|
388
|
-
projectId,
|
|
389
|
-
linearIssueId: source.id,
|
|
390
|
-
blockers: source.blockedBy.map((blocker) => ({
|
|
391
|
-
blockerLinearIssueId: blocker.id,
|
|
392
|
-
...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
|
|
393
|
-
...(blocker.title ? { blockerTitle: blocker.title } : {}),
|
|
394
|
-
...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
|
|
395
|
-
...(blocker.stateType ? { blockerCurrentLinearStateType: blocker.stateType } : {}),
|
|
396
|
-
})),
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
this.db.issues.replaceIssueParentLink({
|
|
400
|
-
projectId,
|
|
401
|
-
childLinearIssueId: source.id,
|
|
402
|
-
parentLinearIssueId: source.parentId ?? null,
|
|
403
|
-
});
|
|
404
|
-
return { issue: source, hydration };
|
|
405
|
-
}
|
|
406
|
-
async resolveLinkedPrAdoption(params) {
|
|
407
|
-
if (!params.delegated)
|
|
408
|
-
return undefined;
|
|
409
|
-
if (params.triggerEvent !== "delegateChanged")
|
|
410
|
-
return undefined;
|
|
411
|
-
if (params.existingIssue?.prNumber !== undefined)
|
|
412
|
-
return undefined;
|
|
413
|
-
const resolution = resolveLinkedPullRequest(params.issue.attachments, params.project.github?.repoFullName);
|
|
414
|
-
if (resolution.kind === "none")
|
|
415
|
-
return undefined;
|
|
416
|
-
if (resolution.kind === "ambiguous") {
|
|
417
|
-
return {
|
|
418
|
-
factoryState: "awaiting_input",
|
|
419
|
-
pendingRunType: null,
|
|
420
|
-
pendingRunContext: undefined,
|
|
421
|
-
issueUpdates: {},
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
const remote = await readRemotePrState(resolution.reference.repoFullName, resolution.reference.prNumber);
|
|
425
|
-
if (!remote) {
|
|
426
|
-
return {
|
|
427
|
-
factoryState: "awaiting_input",
|
|
428
|
-
pendingRunType: null,
|
|
429
|
-
pendingRunContext: undefined,
|
|
430
|
-
issueUpdates: {
|
|
431
|
-
prNumber: resolution.reference.prNumber,
|
|
432
|
-
prUrl: resolution.reference.url,
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
return deriveLinkedPrAdoptionOutcome(params.project, resolution.reference.prNumber, remote);
|
|
437
|
-
}
|
|
438
327
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mergeIssueMetadata } from "./decision-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Brings the local dependency / parent-link state for `issue` up to date.
|
|
4
|
+
* If the webhook payload doesn't already include relation data we fetch it
|
|
5
|
+
* from Linear directly so subsequent decisions don't operate on a
|
|
6
|
+
* stale-by-omission snapshot. Returns the resolved `IssueMetadata` plus a
|
|
7
|
+
* label describing where the relation data came from (used by the audit
|
|
8
|
+
* trail).
|
|
9
|
+
*/
|
|
10
|
+
export async function syncIssueDependencies(db, linearProvider, projectId, issue) {
|
|
11
|
+
let source = issue;
|
|
12
|
+
let hydration = "webhook_only";
|
|
13
|
+
if (!source.relationsKnown) {
|
|
14
|
+
const linear = await linearProvider.forProject(projectId);
|
|
15
|
+
if (linear) {
|
|
16
|
+
try {
|
|
17
|
+
source = mergeIssueMetadata(source, await linear.getIssue(issue.id));
|
|
18
|
+
hydration = "live_linear";
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Preserve existing dependency rows when webhook relation data is incomplete.
|
|
22
|
+
hydration = "live_linear_failed";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (source.relationsKnown) {
|
|
27
|
+
db.issues.replaceIssueDependencies({
|
|
28
|
+
projectId,
|
|
29
|
+
linearIssueId: source.id,
|
|
30
|
+
blockers: source.blockedBy.map((blocker) => ({
|
|
31
|
+
blockerLinearIssueId: blocker.id,
|
|
32
|
+
...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
|
|
33
|
+
...(blocker.title ? { blockerTitle: blocker.title } : {}),
|
|
34
|
+
...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
|
|
35
|
+
...(blocker.stateType ? { blockerCurrentLinearStateType: blocker.stateType } : {}),
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
db.issues.replaceIssueParentLink({
|
|
40
|
+
projectId,
|
|
41
|
+
childLinearIssueId: source.id,
|
|
42
|
+
parentLinearIssueId: source.parentId ?? null,
|
|
43
|
+
});
|
|
44
|
+
return { issue: source, hydration };
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveLinkedPullRequest } from "../linear-linked-pr-reconciliation.js";
|
|
2
|
+
import { readRemotePrState } from "../remote-pr-state.js";
|
|
3
|
+
import { deriveLinkedPrAdoptionOutcome } from "../delegation-linked-pr.js";
|
|
4
|
+
/**
|
|
5
|
+
* On `delegateChanged` for a newly-delegated issue with no recorded PR yet,
|
|
6
|
+
* try to adopt any pull request referenced in Linear attachments. Returns
|
|
7
|
+
* the desired stage / pending-run shape, or `undefined` if no adoption
|
|
8
|
+
* applies (wrong trigger, not delegated, PR already tracked, no candidate).
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveLinkedPrAdoption(input) {
|
|
11
|
+
if (!input.delegated)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (input.triggerEvent !== "delegateChanged")
|
|
14
|
+
return undefined;
|
|
15
|
+
if (input.existingIssue?.prNumber !== undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const resolution = resolveLinkedPullRequest(input.issue.attachments, input.project.github?.repoFullName);
|
|
18
|
+
if (resolution.kind === "none")
|
|
19
|
+
return undefined;
|
|
20
|
+
if (resolution.kind === "ambiguous") {
|
|
21
|
+
return {
|
|
22
|
+
factoryState: "awaiting_input",
|
|
23
|
+
pendingRunType: null,
|
|
24
|
+
pendingRunContext: undefined,
|
|
25
|
+
issueUpdates: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const remote = await readRemotePrState(resolution.reference.repoFullName, resolution.reference.prNumber);
|
|
29
|
+
if (!remote) {
|
|
30
|
+
return {
|
|
31
|
+
factoryState: "awaiting_input",
|
|
32
|
+
pendingRunType: null,
|
|
33
|
+
pendingRunContext: undefined,
|
|
34
|
+
issueUpdates: {
|
|
35
|
+
prNumber: resolution.reference.prNumber,
|
|
36
|
+
prUrl: resolution.reference.url,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return deriveLinkedPrAdoptionOutcome(input.project, resolution.reference.prNumber, remote);
|
|
41
|
+
}
|