patchrelay 0.35.10 → 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 +275 -74
- 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,24 +360,61 @@ 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 });
|
|
229
391
|
return;
|
|
230
392
|
}
|
|
231
|
-
if (pr.
|
|
232
|
-
this.
|
|
233
|
-
this.
|
|
393
|
+
if (pr.state === "CLOSED") {
|
|
394
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed, re-delegating for implementation");
|
|
395
|
+
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "closed" });
|
|
396
|
+
this.advanceIdleIssue(issue, "delegated", {
|
|
397
|
+
pendingRunType: "implementation",
|
|
398
|
+
clearFailureProvenance: true,
|
|
399
|
+
});
|
|
234
400
|
return;
|
|
235
401
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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,
|
|
241
418
|
pendingRunContext: {
|
|
242
419
|
source: "idle_reconciliation",
|
|
243
420
|
failureReason: "merge_conflict_detected",
|
|
@@ -249,14 +426,38 @@ export class IdleIssueReconciler {
|
|
|
249
426
|
kind: "github",
|
|
250
427
|
issueKey: issue.issueKey,
|
|
251
428
|
projectId: issue.projectId,
|
|
252
|
-
stage:
|
|
429
|
+
stage: reactiveIntent.compatibilityFactoryState,
|
|
253
430
|
status: "conflict_detected",
|
|
254
431
|
summary: `PR #${issue.prNumber} has merge conflicts with main, dispatching rebase`,
|
|
255
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");
|
|
256
450
|
}
|
|
257
451
|
}
|
|
258
452
|
catch (error) {
|
|
259
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
|
+
}
|
|
260
461
|
}
|
|
261
462
|
}
|
|
262
463
|
}
|