patchrelay 0.35.11 → 0.35.12
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 +41 -9
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +0 -1
- package/dist/cli/commands/issues.js +2 -56
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/data.js +110 -47
- package/dist/cli/formatters/text.js +6 -90
- package/dist/cli/help.js +3 -8
- package/dist/cli/index.js +0 -48
- package/dist/cli/operator-client.js +0 -82
- package/dist/cli/watch/App.js +1 -12
- package/dist/cli/watch/HelpBar.js +2 -2
- package/dist/cli/watch/IssueDetailView.js +57 -26
- package/dist/cli/watch/IssueRow.js +71 -27
- package/dist/cli/watch/StatusBar.js +7 -4
- package/dist/cli/watch/state-visualization.js +48 -23
- package/dist/cli/watch/timeline-builder.js +2 -1
- package/dist/cli/watch/use-detail-stream.js +10 -104
- package/dist/cli/watch/use-watch-stream.js +11 -102
- package/dist/cli/watch/watch-state.js +18 -50
- package/dist/codex-thread-utils.js +3 -0
- package/dist/db/migrations.js +239 -2
- package/dist/db.js +628 -39
- package/dist/github-app-token.js +7 -0
- package/dist/github-failure-context.js +44 -1
- package/dist/github-rollup.js +47 -0
- package/dist/github-webhook-handler.js +248 -51
- package/dist/github-webhooks.js +5 -0
- package/dist/http.js +12 -264
- package/dist/idle-reconciliation.js +268 -76
- package/dist/issue-query-service.js +221 -129
- package/dist/issue-session-events.js +151 -0
- package/dist/issue-session.js +99 -0
- package/dist/linear-client.js +39 -25
- package/dist/linear-session-reporting.js +12 -0
- package/dist/linear-session-sync.js +253 -24
- package/dist/linear-workflow.js +33 -0
- package/dist/merge-queue-protocol.js +0 -51
- package/dist/preflight.js +1 -4
- package/dist/queue-health-monitor.js +11 -7
- package/dist/run-orchestrator.js +1295 -146
- package/dist/run-reporting.js +5 -3
- package/dist/service.js +279 -102
- package/dist/status-note.js +56 -0
- package/dist/waiting-reason.js +65 -0
- package/dist/webhook-handler.js +270 -79
- package/package.json +1 -1
- package/dist/cli/commands/feed.js +0 -60
- package/dist/cli/watch/FeedView.js +0 -28
- package/dist/cli/watch/use-feed-stream.js +0 -92
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
2
2
|
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
3
|
+
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
4
|
+
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
3
5
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
4
6
|
import { execCommand } from "./utils.js";
|
|
7
|
+
function isFailingCheckStatus(status) {
|
|
8
|
+
return status === "failed" || status === "failure";
|
|
9
|
+
}
|
|
10
|
+
function getGateCheckNames(project) {
|
|
11
|
+
const configured = project?.gateChecks?.map((entry) => entry.trim()).filter(Boolean) ?? [];
|
|
12
|
+
return configured.length > 0 ? configured : ["verify"];
|
|
13
|
+
}
|
|
5
14
|
function isDuplicateRepairAttempt(issue, context) {
|
|
6
15
|
const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
|
|
7
16
|
const headSha = typeof context?.failureHeadSha === "string"
|
|
@@ -36,11 +45,23 @@ function buildFailureContext(issue) {
|
|
|
36
45
|
...(queueRepairContext ? queueRepairContext : {}),
|
|
37
46
|
};
|
|
38
47
|
}
|
|
48
|
+
function hasFailureProvenance(issue) {
|
|
49
|
+
return Boolean(issue.lastGitHubFailureSource
|
|
50
|
+
|| issue.lastGitHubFailureHeadSha
|
|
51
|
+
|| issue.lastGitHubFailureSignature
|
|
52
|
+
|| issue.lastGitHubFailureCheckName
|
|
53
|
+
|| issue.lastGitHubFailureCheckUrl
|
|
54
|
+
|| issue.lastGitHubFailureContextJson
|
|
55
|
+
|| issue.lastGitHubFailureAt
|
|
56
|
+
|| issue.lastQueueIncidentJson
|
|
57
|
+
|| issue.lastAttemptedFailureHeadSha
|
|
58
|
+
|| issue.lastAttemptedFailureSignature);
|
|
59
|
+
}
|
|
39
60
|
export function resolveBranchOwnerForStateTransition(newState, pendingRunType) {
|
|
40
61
|
if (pendingRunType)
|
|
41
62
|
return "patchrelay";
|
|
42
63
|
if (newState === "awaiting_queue")
|
|
43
|
-
return "
|
|
64
|
+
return "patchrelay";
|
|
44
65
|
if (newState === "repairing_ci" || newState === "repairing_queue")
|
|
45
66
|
return "patchrelay";
|
|
46
67
|
return undefined;
|
|
@@ -64,16 +85,27 @@ export class IdleIssueReconciler {
|
|
|
64
85
|
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
65
86
|
continue;
|
|
66
87
|
}
|
|
67
|
-
if (issue.
|
|
68
|
-
|
|
88
|
+
if (issue.lastGitHubFailureSource === "queue_eviction") {
|
|
89
|
+
await this.routeFailedIssue(issue);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (issue.lastGitHubFailureSource === "branch_ci") {
|
|
93
|
+
await this.routeFailedIssue(issue);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (issue.prReviewState === "approved" && !isFailingCheckStatus(issue.prCheckStatus)) {
|
|
97
|
+
if (issue.prNumber) {
|
|
98
|
+
await this.reconcileFromGitHub(issue);
|
|
99
|
+
}
|
|
100
|
+
else if (issue.factoryState !== "awaiting_queue") {
|
|
69
101
|
this.advanceIdleIssue(issue, "awaiting_queue", { clearFailureProvenance: true });
|
|
70
102
|
}
|
|
71
|
-
else if (
|
|
72
|
-
|
|
103
|
+
else if (hasFailureProvenance(issue)) {
|
|
104
|
+
this.advanceIdleIssue(issue, "awaiting_queue", { clearFailureProvenance: true });
|
|
73
105
|
}
|
|
74
106
|
continue;
|
|
75
107
|
}
|
|
76
|
-
if (issue.prCheckStatus
|
|
108
|
+
if (isFailingCheckStatus(issue.prCheckStatus)) {
|
|
77
109
|
await this.routeFailedIssue(issue);
|
|
78
110
|
continue;
|
|
79
111
|
}
|
|
@@ -86,12 +118,15 @@ export class IdleIssueReconciler {
|
|
|
86
118
|
for (const issue of this.db.listBlockedDelegatedIssues()) {
|
|
87
119
|
const unresolved = this.db.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
88
120
|
if (unresolved === 0) {
|
|
89
|
-
this.db.
|
|
121
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
90
122
|
projectId: issue.projectId,
|
|
91
123
|
linearIssueId: issue.linearIssueId,
|
|
92
|
-
|
|
124
|
+
eventType: "delegated",
|
|
125
|
+
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
93
126
|
});
|
|
94
|
-
this.
|
|
127
|
+
if (this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
128
|
+
this.deps.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
129
|
+
}
|
|
95
130
|
}
|
|
96
131
|
}
|
|
97
132
|
}
|
|
@@ -100,18 +135,16 @@ export class IdleIssueReconciler {
|
|
|
100
135
|
return;
|
|
101
136
|
}
|
|
102
137
|
this.logger.info({ issueKey: issue.issueKey, from: issue.factoryState, to: newState, pendingRunType: options?.pendingRunType }, "Reconciliation: advancing idle issue");
|
|
103
|
-
const resetQueueLabel = newState === "awaiting_queue" || issue.factoryState === "awaiting_queue";
|
|
104
138
|
this.db.upsertIssue({
|
|
105
139
|
projectId: issue.projectId,
|
|
106
140
|
linearIssueId: issue.linearIssueId,
|
|
107
141
|
factoryState: newState,
|
|
108
|
-
...(options?.pendingRunType
|
|
109
|
-
...(options?.pendingRunType
|
|
142
|
+
...((options?.pendingRunType || newState === "awaiting_queue" || newState === "delegated" || newState === "done")
|
|
110
143
|
? {
|
|
111
|
-
|
|
144
|
+
pendingRunType: null,
|
|
145
|
+
pendingRunContextJson: null,
|
|
112
146
|
}
|
|
113
147
|
: {}),
|
|
114
|
-
...(resetQueueLabel ? { queueLabelApplied: false } : {}),
|
|
115
148
|
...(options?.clearFailureProvenance
|
|
116
149
|
? {
|
|
117
150
|
lastGitHubFailureSource: null,
|
|
@@ -131,6 +164,9 @@ export class IdleIssueReconciler {
|
|
|
131
164
|
if (branchOwner) {
|
|
132
165
|
this.db.setBranchOwner(issue.projectId, issue.linearIssueId, branchOwner);
|
|
133
166
|
}
|
|
167
|
+
if (options?.pendingRunType) {
|
|
168
|
+
this.appendWakeEvent(issue, options.pendingRunType, options.pendingRunContext, "idle_reconciliation");
|
|
169
|
+
}
|
|
134
170
|
this.feed?.publish({
|
|
135
171
|
level: "info",
|
|
136
172
|
kind: "stage",
|
|
@@ -140,57 +176,52 @@ export class IdleIssueReconciler {
|
|
|
140
176
|
status: "reconciled",
|
|
141
177
|
summary: `Reconciliation: ${issue.factoryState} \u2192 ${newState}`,
|
|
142
178
|
});
|
|
143
|
-
if (
|
|
144
|
-
void this.deps.requestMergeQueueAdmission(issue, issue.projectId);
|
|
145
|
-
}
|
|
146
|
-
if (options?.pendingRunType) {
|
|
179
|
+
if (options?.pendingRunType && this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
147
180
|
this.deps.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
148
181
|
}
|
|
149
182
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
this.advanceIdleIssue(issue, "repairing_queue", {
|
|
158
|
-
pendingRunType: "queue_repair",
|
|
159
|
-
...(pendingRunContext ? { pendingRunContext } : {}),
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
return;
|
|
183
|
+
appendWakeEvent(issue, runType, context, dedupeScope = "idle_reconciliation") {
|
|
184
|
+
let eventType;
|
|
185
|
+
let dedupeKey;
|
|
186
|
+
if (runType === "queue_repair") {
|
|
187
|
+
eventType = "merge_steward_incident";
|
|
188
|
+
dedupeKey = `${dedupeScope}:queue_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown"}`;
|
|
163
189
|
}
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.advanceIdleIssue(issue, "repairing_ci");
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
this.advanceIdleIssue(issue, "repairing_ci", {
|
|
171
|
-
pendingRunType: "ci_repair",
|
|
172
|
-
...(pendingRunContext ? { pendingRunContext } : {}),
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
return;
|
|
190
|
+
else if (runType === "ci_repair") {
|
|
191
|
+
eventType = "settled_red_ci";
|
|
192
|
+
dedupeKey = `${dedupeScope}:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown"}`;
|
|
176
193
|
}
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
else if (runType === "review_fix") {
|
|
195
|
+
eventType = "review_changes_requested";
|
|
196
|
+
dedupeKey = `${dedupeScope}:review_fix:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown"}`;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
eventType = "delegated";
|
|
200
|
+
dedupeKey = `${dedupeScope}:implementation:${issue.linearIssueId}`;
|
|
201
|
+
}
|
|
202
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
203
|
+
projectId: issue.projectId,
|
|
204
|
+
linearIssueId: issue.linearIssueId,
|
|
205
|
+
eventType,
|
|
206
|
+
...(context ? { eventJson: JSON.stringify(context) } : {}),
|
|
207
|
+
dedupeKey,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async routeFailedIssue(issue) {
|
|
211
|
+
issue = await this.refreshMissingFailureProvenance(issue);
|
|
212
|
+
issue = await this.reclassifyStaleBranchFailure(issue);
|
|
213
|
+
const latestRun = this.db.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
214
|
+
const ignoreDuplicateAttempt = latestRun?.status === "failed"
|
|
215
|
+
&& latestRun.failureReason === "Codex turn was interrupted";
|
|
216
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
217
|
+
prNumber: issue.prNumber,
|
|
218
|
+
prState: issue.prState,
|
|
219
|
+
prReviewState: issue.prReviewState,
|
|
220
|
+
prCheckStatus: issue.prCheckStatus,
|
|
221
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
222
|
+
});
|
|
223
|
+
if (!reactiveIntent && issue.factoryState === "awaiting_queue") {
|
|
224
|
+
const inferred = await this.inferFailureSourceFromGitHub(issue) ?? "branch_ci";
|
|
194
225
|
const inferRunType = inferred === "queue_eviction" ? "queue_repair" : "ci_repair";
|
|
195
226
|
const inferState = inferred === "queue_eviction" ? "repairing_queue" : "repairing_ci";
|
|
196
227
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, inferred }, "Inferred failure provenance for awaiting_queue issue");
|
|
@@ -201,17 +232,126 @@ export class IdleIssueReconciler {
|
|
|
201
232
|
});
|
|
202
233
|
return;
|
|
203
234
|
}
|
|
235
|
+
if (!reactiveIntent) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
204
238
|
const pendingRunContext = buildFailureContext(issue);
|
|
205
|
-
|
|
206
|
-
|
|
239
|
+
const duplicateRepair = reactiveIntent.runType !== "review_fix"
|
|
240
|
+
&& !ignoreDuplicateAttempt
|
|
241
|
+
&& isDuplicateRepairAttempt(issue, pendingRunContext);
|
|
242
|
+
if (duplicateRepair) {
|
|
243
|
+
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState);
|
|
207
244
|
}
|
|
208
245
|
else {
|
|
209
|
-
this.advanceIdleIssue(issue,
|
|
210
|
-
pendingRunType:
|
|
246
|
+
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
247
|
+
pendingRunType: reactiveIntent.runType,
|
|
211
248
|
...(pendingRunContext ? { pendingRunContext } : {}),
|
|
212
249
|
});
|
|
213
250
|
}
|
|
214
251
|
}
|
|
252
|
+
async refreshMissingFailureProvenance(issue) {
|
|
253
|
+
if (issue.lastGitHubFailureSource || !issue.prNumber || !isFailingCheckStatus(issue.prCheckStatus)) {
|
|
254
|
+
return issue;
|
|
255
|
+
}
|
|
256
|
+
const inferred = await this.inferFailureSourceFromGitHub(issue);
|
|
257
|
+
if (!inferred)
|
|
258
|
+
return issue;
|
|
259
|
+
const protocol = this.getIssueProtocol(issue);
|
|
260
|
+
const failureHeadSha = issue.lastGitHubFailureHeadSha ?? issue.lastGitHubCiSnapshotHeadSha ?? issue.prHeadSha ?? null;
|
|
261
|
+
const checkName = inferred === "queue_eviction"
|
|
262
|
+
? issue.lastGitHubFailureCheckName ?? protocol.evictionCheckName
|
|
263
|
+
: issue.lastGitHubFailureCheckName ?? null;
|
|
264
|
+
const failureSignature = issue.lastGitHubFailureSignature
|
|
265
|
+
?? (inferred === "queue_eviction" && failureHeadSha && checkName
|
|
266
|
+
? ["queue_eviction", failureHeadSha, checkName].join("::")
|
|
267
|
+
: null);
|
|
268
|
+
this.db.upsertIssue({
|
|
269
|
+
projectId: issue.projectId,
|
|
270
|
+
linearIssueId: issue.linearIssueId,
|
|
271
|
+
lastGitHubFailureSource: inferred,
|
|
272
|
+
...(failureHeadSha ? { lastGitHubFailureHeadSha: failureHeadSha } : {}),
|
|
273
|
+
...(checkName ? { lastGitHubFailureCheckName: checkName } : {}),
|
|
274
|
+
...(failureSignature ? { lastGitHubFailureSignature: failureSignature } : {}),
|
|
275
|
+
});
|
|
276
|
+
const refreshed = this.db.getIssue(issue.projectId, issue.linearIssueId);
|
|
277
|
+
if (!refreshed)
|
|
278
|
+
return issue;
|
|
279
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, inferred, factoryState: issue.factoryState }, "Recovered missing failure provenance from GitHub state");
|
|
280
|
+
return refreshed;
|
|
281
|
+
}
|
|
282
|
+
async reclassifyStaleBranchFailure(issue) {
|
|
283
|
+
const downstreamOwned = issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved";
|
|
284
|
+
if (issue.lastGitHubFailureSource !== "branch_ci" || !downstreamOwned) {
|
|
285
|
+
return issue;
|
|
286
|
+
}
|
|
287
|
+
const inferred = await this.inferFailureSourceFromGitHub(issue);
|
|
288
|
+
if (inferred !== "queue_eviction") {
|
|
289
|
+
return issue;
|
|
290
|
+
}
|
|
291
|
+
const protocol = this.getIssueProtocol(issue);
|
|
292
|
+
const failureHeadSha = issue.lastGitHubFailureHeadSha ?? issue.lastGitHubCiSnapshotHeadSha ?? issue.prHeadSha ?? null;
|
|
293
|
+
const checkName = issue.lastGitHubFailureCheckName ?? protocol.evictionCheckName;
|
|
294
|
+
const failureSignature = issue.lastGitHubFailureSignature
|
|
295
|
+
?? (failureHeadSha && checkName ? ["queue_eviction", failureHeadSha, checkName].join("::") : null);
|
|
296
|
+
this.db.upsertIssue({
|
|
297
|
+
projectId: issue.projectId,
|
|
298
|
+
linearIssueId: issue.linearIssueId,
|
|
299
|
+
lastGitHubFailureSource: "queue_eviction",
|
|
300
|
+
...(failureHeadSha ? { lastGitHubFailureHeadSha: failureHeadSha } : {}),
|
|
301
|
+
...(checkName ? { lastGitHubFailureCheckName: checkName } : {}),
|
|
302
|
+
...(failureSignature ? { lastGitHubFailureSignature: failureSignature } : {}),
|
|
303
|
+
});
|
|
304
|
+
const refreshed = this.db.getIssue(issue.projectId, issue.linearIssueId);
|
|
305
|
+
if (!refreshed)
|
|
306
|
+
return issue;
|
|
307
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reclassified stale branch failure as queue repair from GitHub state");
|
|
308
|
+
return refreshed;
|
|
309
|
+
}
|
|
310
|
+
async inferFailureSourceFromGitHub(issue) {
|
|
311
|
+
const project = this.config.projects.find((candidate) => candidate.id === issue.projectId);
|
|
312
|
+
const repoFullName = project?.github?.repoFullName;
|
|
313
|
+
const probeSha = issue.lastGitHubFailureHeadSha ?? issue.lastGitHubCiSnapshotHeadSha ?? issue.prHeadSha;
|
|
314
|
+
if (!repoFullName || !issue.prNumber || !probeSha)
|
|
315
|
+
return undefined;
|
|
316
|
+
const protocol = this.getIssueProtocol(issue);
|
|
317
|
+
try {
|
|
318
|
+
const { stdout } = await execCommand("gh", [
|
|
319
|
+
"api",
|
|
320
|
+
`repos/${repoFullName}/commits/${probeSha}/check-runs`,
|
|
321
|
+
"--jq", `.check_runs[] | select(.name == "${protocol.evictionCheckName}" and .conclusion == "failure") | .name`,
|
|
322
|
+
], { timeoutMs: 10_000 });
|
|
323
|
+
if (stdout.trim().length > 0)
|
|
324
|
+
return "queue_eviction";
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Fall through to a PR-level probe. Preemptive conflicts can require
|
|
328
|
+
// queue repair even when no merge-steward eviction check-run exists yet.
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const { stdout } = await execCommand("gh", [
|
|
332
|
+
"pr", "view", String(issue.prNumber),
|
|
333
|
+
"--repo", repoFullName,
|
|
334
|
+
"--json", "mergeable,mergeStateStatus,labels",
|
|
335
|
+
], { timeoutMs: 10_000 });
|
|
336
|
+
const pr = JSON.parse(stdout);
|
|
337
|
+
const downstreamOwned = issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved";
|
|
338
|
+
if ((pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY")
|
|
339
|
+
&& downstreamOwned) {
|
|
340
|
+
return "queue_eviction";
|
|
341
|
+
}
|
|
342
|
+
if (pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY") {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved" ? "branch_ci" : undefined;
|
|
348
|
+
}
|
|
349
|
+
return "branch_ci";
|
|
350
|
+
}
|
|
351
|
+
getIssueProtocol(issue) {
|
|
352
|
+
const project = this.config.projects.find((candidate) => candidate.id === issue.projectId);
|
|
353
|
+
return resolveMergeQueueProtocol(project);
|
|
354
|
+
}
|
|
215
355
|
async reconcileFromGitHub(issue) {
|
|
216
356
|
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
217
357
|
if (!project?.github?.repoFullName || !issue.prNumber)
|
|
@@ -220,9 +360,31 @@ export class IdleIssueReconciler {
|
|
|
220
360
|
const { stdout } = await execCommand("gh", [
|
|
221
361
|
"pr", "view", String(issue.prNumber),
|
|
222
362
|
"--repo", project.github.repoFullName,
|
|
223
|
-
"--json", "state,reviewDecision,mergeable,mergeStateStatus",
|
|
363
|
+
"--json", "headRefOid,state,reviewDecision,mergeable,mergeStateStatus,statusCheckRollup",
|
|
224
364
|
], { timeoutMs: 10_000 });
|
|
225
365
|
const pr = JSON.parse(stdout);
|
|
366
|
+
const gateCheckNames = getGateCheckNames(project);
|
|
367
|
+
const gateCheckStatus = deriveGateCheckStatusFromRollup(pr.statusCheckRollup, gateCheckNames);
|
|
368
|
+
this.db.upsertIssue({
|
|
369
|
+
projectId: issue.projectId,
|
|
370
|
+
linearIssueId: issue.linearIssueId,
|
|
371
|
+
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
372
|
+
...(pr.state === "OPEN" ? { prState: "open" } : {}),
|
|
373
|
+
...(pr.reviewDecision === "APPROVED"
|
|
374
|
+
? { prReviewState: "approved" }
|
|
375
|
+
: pr.reviewDecision === "CHANGES_REQUESTED"
|
|
376
|
+
? { prReviewState: "changes_requested" }
|
|
377
|
+
: {}),
|
|
378
|
+
...(gateCheckStatus ? { prCheckStatus: gateCheckStatus } : {}),
|
|
379
|
+
...(pr.headRefOid && gateCheckStatus
|
|
380
|
+
? {
|
|
381
|
+
lastGitHubCiSnapshotHeadSha: pr.headRefOid,
|
|
382
|
+
lastGitHubCiSnapshotGateCheckName: gateCheckNames[0] ?? "verify",
|
|
383
|
+
lastGitHubCiSnapshotGateCheckStatus: gateCheckStatus,
|
|
384
|
+
lastGitHubCiSnapshotSettledAt: gateCheckStatus === "pending" ? null : new Date().toISOString(),
|
|
385
|
+
}
|
|
386
|
+
: {}),
|
|
387
|
+
});
|
|
226
388
|
if (pr.state === "MERGED") {
|
|
227
389
|
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
|
|
228
390
|
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
@@ -237,16 +399,22 @@ export class IdleIssueReconciler {
|
|
|
237
399
|
});
|
|
238
400
|
return;
|
|
239
401
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
402
|
+
const downstreamOwned = issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved" || pr.reviewDecision === "APPROVED";
|
|
403
|
+
const mergeConflictDetected = pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
|
|
404
|
+
const refreshedIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
405
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
406
|
+
prNumber: refreshedIssue.prNumber,
|
|
407
|
+
prState: refreshedIssue.prState,
|
|
408
|
+
prReviewState: refreshedIssue.prReviewState,
|
|
409
|
+
prCheckStatus: refreshedIssue.prCheckStatus,
|
|
410
|
+
latestFailureSource: refreshedIssue.lastGitHubFailureSource,
|
|
411
|
+
mergeConflictDetected,
|
|
412
|
+
downstreamOwned,
|
|
413
|
+
});
|
|
414
|
+
if (reactiveIntent?.runType === "queue_repair" && mergeConflictDetected) {
|
|
415
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable }, "Reconciliation: PR needs queue repair from fresh GitHub truth");
|
|
416
|
+
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
417
|
+
pendingRunType: reactiveIntent.runType,
|
|
250
418
|
pendingRunContext: {
|
|
251
419
|
source: "idle_reconciliation",
|
|
252
420
|
failureReason: "merge_conflict_detected",
|
|
@@ -258,14 +426,38 @@ export class IdleIssueReconciler {
|
|
|
258
426
|
kind: "github",
|
|
259
427
|
issueKey: issue.issueKey,
|
|
260
428
|
projectId: issue.projectId,
|
|
261
|
-
stage:
|
|
429
|
+
stage: reactiveIntent.compatibilityFactoryState,
|
|
262
430
|
status: "conflict_detected",
|
|
263
431
|
summary: `PR #${issue.prNumber} has merge conflicts with main, dispatching rebase`,
|
|
264
432
|
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (pr.reviewDecision === "APPROVED") {
|
|
436
|
+
this.db.upsertIssue({
|
|
437
|
+
projectId: issue.projectId,
|
|
438
|
+
linearIssueId: issue.linearIssueId,
|
|
439
|
+
prReviewState: "approved",
|
|
440
|
+
});
|
|
441
|
+
if (issue.factoryState !== "awaiting_queue" || hasFailureProvenance(issue)) {
|
|
442
|
+
this.advanceIdleIssue(issue, "awaiting_queue", {
|
|
443
|
+
...(hasFailureProvenance(issue) ? { clearFailureProvenance: true } : {}),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (mergeConflictDetected) {
|
|
449
|
+
this.logger.debug({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable, mergeStateStatus: pr.mergeStateStatus }, "Reconciliation: PR is dirty but not yet queue-admitted; leaving PatchRelay in review state");
|
|
265
450
|
}
|
|
266
451
|
}
|
|
267
452
|
catch (error) {
|
|
268
453
|
this.logger.debug({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to query GitHub PR state during reconciliation");
|
|
454
|
+
if (issue.prReviewState === "approved") {
|
|
455
|
+
if (issue.factoryState !== "awaiting_queue" || hasFailureProvenance(issue)) {
|
|
456
|
+
this.advanceIdleIssue(issue, "awaiting_queue", {
|
|
457
|
+
...(hasFailureProvenance(issue) ? { clearFailureProvenance: true } : {}),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
269
461
|
}
|
|
270
462
|
}
|
|
271
463
|
}
|