patchrelay 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/IssueDetailView.js +1 -1
- package/dist/cli/watch/use-detail-stream.js +5 -0
- package/dist/cli/watch/watch-state.js +14 -0
- package/dist/db/migrations.js +9 -0
- package/dist/db.js +82 -14
- package/dist/github-failure-context.js +205 -0
- package/dist/github-webhook-handler.js +140 -22
- package/dist/issue-query-service.js +6 -0
- package/dist/linear-client.js +2 -0
- package/dist/run-orchestrator.js +155 -9
- package/dist/service.js +28 -3
- package/dist/webhook-handler.js +20 -14
- package/dist/webhooks.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveFactoryStateFromGitHub, TERMINAL_STATES } from "./factory-state.js";
|
|
2
|
+
import { createGitHubFailureContextResolver, summarizeGitHubFailureContext, } from "./github-failure-context.js";
|
|
2
3
|
import { normalizeGitHubWebhook, verifyGitHubWebhookSignature } from "./github-webhooks.js";
|
|
3
4
|
import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
|
|
4
5
|
import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
|
|
@@ -28,7 +29,8 @@ export class GitHubWebhookHandler {
|
|
|
28
29
|
logger;
|
|
29
30
|
codex;
|
|
30
31
|
feed;
|
|
31
|
-
|
|
32
|
+
failureContextResolver;
|
|
33
|
+
constructor(config, db, linearProvider, enqueueIssue, logger, codex, feed, failureContextResolver = createGitHubFailureContextResolver()) {
|
|
32
34
|
this.config = config;
|
|
33
35
|
this.db = db;
|
|
34
36
|
this.linearProvider = linearProvider;
|
|
@@ -36,6 +38,7 @@ export class GitHubWebhookHandler {
|
|
|
36
38
|
this.logger = logger;
|
|
37
39
|
this.codex = codex;
|
|
38
40
|
this.feed = feed;
|
|
41
|
+
this.failureContextResolver = failureContextResolver;
|
|
39
42
|
}
|
|
40
43
|
async acceptGitHubWebhook(params) {
|
|
41
44
|
// Deduplicate
|
|
@@ -114,6 +117,7 @@ export class GitHubWebhookHandler {
|
|
|
114
117
|
this.logger.debug({ branchName: event.branchName, triggerEvent: event.triggerEvent }, "GitHub webhook: no matching issue for branch");
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
120
|
+
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
117
121
|
// Update PR state on the issue
|
|
118
122
|
this.db.upsertIssue({
|
|
119
123
|
projectId: issue.projectId,
|
|
@@ -124,7 +128,7 @@ export class GitHubWebhookHandler {
|
|
|
124
128
|
...(event.reviewState !== undefined ? { prReviewState: event.reviewState } : {}),
|
|
125
129
|
...(event.checkStatus !== undefined ? { prCheckStatus: event.checkStatus } : {}),
|
|
126
130
|
});
|
|
127
|
-
this.updateFailureProvenance(issue, event);
|
|
131
|
+
await this.updateFailureProvenance(issue, event, project);
|
|
128
132
|
if (!isMetadataOnlyCheckEvent(event)) {
|
|
129
133
|
// Re-read issue after PR metadata upsert so guards see fresh prReviewState
|
|
130
134
|
const afterMetadata = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
@@ -168,10 +172,15 @@ export class GitHubWebhookHandler {
|
|
|
168
172
|
ciRepairAttempts: 0,
|
|
169
173
|
queueRepairAttempts: 0,
|
|
170
174
|
lastGitHubFailureSource: null,
|
|
175
|
+
lastGitHubFailureHeadSha: null,
|
|
176
|
+
lastGitHubFailureSignature: null,
|
|
171
177
|
lastGitHubFailureCheckName: null,
|
|
172
178
|
lastGitHubFailureCheckUrl: null,
|
|
179
|
+
lastGitHubFailureContextJson: null,
|
|
173
180
|
lastGitHubFailureAt: null,
|
|
174
181
|
lastQueueIncidentJson: null,
|
|
182
|
+
lastAttemptedFailureHeadSha: null,
|
|
183
|
+
lastAttemptedFailureSignature: null,
|
|
175
184
|
});
|
|
176
185
|
}
|
|
177
186
|
this.logger.info({ issueKey: issue.issueKey, branchName: event.branchName, triggerEvent: event.triggerEvent, prNumber: event.prNumber }, "GitHub webhook: updated issue PR state");
|
|
@@ -188,16 +197,15 @@ export class GitHubWebhookHandler {
|
|
|
188
197
|
// Queue eviction check runs bypass the metadata-only filter because
|
|
189
198
|
// they're individual check_run events (not check_suite), but they
|
|
190
199
|
// must drive state transitions.
|
|
191
|
-
const project = this.config.projects.find((p) => p.id === freshIssue.projectId);
|
|
192
200
|
const protocol = resolveMergeQueueProtocol(project);
|
|
193
201
|
if (event.eventSource === "check_run" && event.checkName === protocol.evictionCheckName) {
|
|
194
|
-
this.maybeEnqueueReactiveRun(freshIssue, event, project);
|
|
202
|
+
await this.maybeEnqueueReactiveRun(freshIssue, event, project);
|
|
195
203
|
}
|
|
196
204
|
else if (!isMetadataOnlyCheckEvent(event)) {
|
|
197
|
-
this.maybeEnqueueReactiveRun(freshIssue, event, project);
|
|
205
|
+
await this.maybeEnqueueReactiveRun(freshIssue, event, project);
|
|
198
206
|
}
|
|
199
207
|
}
|
|
200
|
-
maybeEnqueueReactiveRun(issue, event, project) {
|
|
208
|
+
async maybeEnqueueReactiveRun(issue, event, project) {
|
|
201
209
|
// Don't trigger if there's already an active run
|
|
202
210
|
if (issue.activeRunId !== undefined)
|
|
203
211
|
return;
|
|
@@ -213,14 +221,24 @@ export class GitHubWebhookHandler {
|
|
|
213
221
|
if (issue.factoryState === "awaiting_queue"
|
|
214
222
|
&& event.checkName === queueCheckName) {
|
|
215
223
|
const queueRepairContext = buildQueueRepairContextFromEvent(event);
|
|
224
|
+
const failureContext = this.buildQueueFailureContext(issue, event, queueRepairContext);
|
|
225
|
+
if (this.hasDuplicatePendingReactiveRun(issue, "queue_repair", failureContext)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
216
228
|
this.db.upsertIssue({
|
|
217
229
|
projectId: issue.projectId,
|
|
218
230
|
linearIssueId: issue.linearIssueId,
|
|
219
231
|
pendingRunType: "queue_repair",
|
|
220
|
-
pendingRunContextJson: JSON.stringify(
|
|
232
|
+
pendingRunContextJson: JSON.stringify({
|
|
233
|
+
...queueRepairContext,
|
|
234
|
+
...failureContext,
|
|
235
|
+
}),
|
|
221
236
|
lastGitHubFailureSource: "queue_eviction",
|
|
237
|
+
lastGitHubFailureHeadSha: failureContext.failureHeadSha ?? null,
|
|
238
|
+
lastGitHubFailureSignature: failureContext.failureSignature ?? null,
|
|
222
239
|
lastGitHubFailureCheckName: event.checkName ?? null,
|
|
223
240
|
lastGitHubFailureCheckUrl: event.checkUrl ?? null,
|
|
241
|
+
lastGitHubFailureContextJson: JSON.stringify(failureContext),
|
|
224
242
|
lastGitHubFailureAt: new Date().toISOString(),
|
|
225
243
|
lastQueueSignalAt: new Date().toISOString(),
|
|
226
244
|
lastQueueIncidentJson: JSON.stringify(queueRepairContext),
|
|
@@ -239,23 +257,39 @@ export class GitHubWebhookHandler {
|
|
|
239
257
|
});
|
|
240
258
|
}
|
|
241
259
|
else {
|
|
260
|
+
const failureContext = await this.resolveBranchFailureContext(issue, event, project);
|
|
261
|
+
if (this.hasDuplicatePendingReactiveRun(issue, "ci_repair", failureContext)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
242
264
|
this.db.upsertIssue({
|
|
243
265
|
projectId: issue.projectId,
|
|
244
266
|
linearIssueId: issue.linearIssueId,
|
|
245
267
|
pendingRunType: "ci_repair",
|
|
246
268
|
pendingRunContextJson: JSON.stringify({
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
checkClass: resolveCheckClass(event.checkName, project),
|
|
269
|
+
...failureContext,
|
|
270
|
+
checkClass: resolveCheckClass(failureContext.checkName ?? event.checkName, project),
|
|
250
271
|
}),
|
|
251
272
|
lastGitHubFailureSource: "branch_ci",
|
|
252
|
-
|
|
253
|
-
|
|
273
|
+
lastGitHubFailureHeadSha: failureContext.failureHeadSha ?? null,
|
|
274
|
+
lastGitHubFailureSignature: failureContext.failureSignature ?? null,
|
|
275
|
+
lastGitHubFailureCheckName: failureContext.checkName ?? event.checkName ?? null,
|
|
276
|
+
lastGitHubFailureCheckUrl: failureContext.checkUrl ?? event.checkUrl ?? null,
|
|
277
|
+
lastGitHubFailureContextJson: JSON.stringify(failureContext),
|
|
254
278
|
lastGitHubFailureAt: new Date().toISOString(),
|
|
255
279
|
lastQueueIncidentJson: null,
|
|
256
280
|
});
|
|
257
281
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
258
|
-
this.logger.info({ issueKey: issue.issueKey, checkName: event.checkName }, "Enqueued CI repair run");
|
|
282
|
+
this.logger.info({ issueKey: issue.issueKey, checkName: failureContext.checkName ?? event.checkName }, "Enqueued CI repair run");
|
|
283
|
+
this.feed?.publish({
|
|
284
|
+
level: "warn",
|
|
285
|
+
kind: "github",
|
|
286
|
+
issueKey: issue.issueKey,
|
|
287
|
+
projectId: issue.projectId,
|
|
288
|
+
stage: "repairing_ci",
|
|
289
|
+
status: "ci_repair_queued",
|
|
290
|
+
summary: `CI repair queued for ${failureContext.jobName ?? failureContext.checkName ?? "failed check"}`,
|
|
291
|
+
detail: summarizeGitHubFailureContext(failureContext),
|
|
292
|
+
});
|
|
259
293
|
}
|
|
260
294
|
}
|
|
261
295
|
if (event.triggerEvent === "review_changes_requested") {
|
|
@@ -272,23 +306,25 @@ export class GitHubWebhookHandler {
|
|
|
272
306
|
this.logger.info({ issueKey: issue.issueKey, reviewerName: event.reviewerName }, "Enqueued review fix run");
|
|
273
307
|
}
|
|
274
308
|
}
|
|
275
|
-
updateFailureProvenance(issue, event) {
|
|
276
|
-
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
309
|
+
async updateFailureProvenance(issue, event, project) {
|
|
277
310
|
const protocol = resolveMergeQueueProtocol(project);
|
|
278
311
|
const isQueueEvictionCheck = event.eventSource === "check_run" && event.checkName === protocol.evictionCheckName;
|
|
279
312
|
if (event.triggerEvent === "check_failed" && issue.prState === "open") {
|
|
280
|
-
if (isMetadataOnlyCheckEvent(event) && !isQueueEvictionCheck) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
313
|
const source = issue.factoryState === "awaiting_queue" && isQueueEvictionCheck
|
|
284
314
|
? "queue_eviction"
|
|
285
315
|
: "branch_ci";
|
|
316
|
+
const failureContext = source === "queue_eviction"
|
|
317
|
+
? this.buildQueueFailureContext(issue, event)
|
|
318
|
+
: await this.resolveBranchFailureContext(issue, event, project);
|
|
286
319
|
this.db.upsertIssue({
|
|
287
320
|
projectId: issue.projectId,
|
|
288
321
|
linearIssueId: issue.linearIssueId,
|
|
289
322
|
lastGitHubFailureSource: source,
|
|
290
|
-
|
|
291
|
-
|
|
323
|
+
lastGitHubFailureHeadSha: failureContext.failureHeadSha ?? event.headSha ?? null,
|
|
324
|
+
lastGitHubFailureSignature: failureContext.failureSignature ?? null,
|
|
325
|
+
lastGitHubFailureCheckName: failureContext.checkName ?? event.checkName ?? null,
|
|
326
|
+
lastGitHubFailureCheckUrl: failureContext.checkUrl ?? event.checkUrl ?? null,
|
|
327
|
+
lastGitHubFailureContextJson: JSON.stringify(failureContext),
|
|
292
328
|
lastGitHubFailureAt: new Date().toISOString(),
|
|
293
329
|
...(source === "queue_eviction"
|
|
294
330
|
? {
|
|
@@ -308,12 +344,94 @@ export class GitHubWebhookHandler {
|
|
|
308
344
|
projectId: issue.projectId,
|
|
309
345
|
linearIssueId: issue.linearIssueId,
|
|
310
346
|
lastGitHubFailureSource: null,
|
|
347
|
+
lastGitHubFailureHeadSha: null,
|
|
348
|
+
lastGitHubFailureSignature: null,
|
|
311
349
|
lastGitHubFailureCheckName: null,
|
|
312
350
|
lastGitHubFailureCheckUrl: null,
|
|
351
|
+
lastGitHubFailureContextJson: null,
|
|
313
352
|
lastGitHubFailureAt: null,
|
|
314
353
|
lastQueueIncidentJson: null,
|
|
354
|
+
lastAttemptedFailureHeadSha: null,
|
|
355
|
+
lastAttemptedFailureSignature: null,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async resolveBranchFailureContext(issue, event, project) {
|
|
360
|
+
const repoFullName = project?.github?.repoFullName ?? event.repoFullName;
|
|
361
|
+
const context = await this.failureContextResolver.resolve({
|
|
362
|
+
source: "branch_ci",
|
|
363
|
+
repoFullName,
|
|
364
|
+
event,
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
...(context ? context : {}),
|
|
368
|
+
...(context?.headSha || event.headSha ? { failureHeadSha: context?.headSha ?? event.headSha } : {}),
|
|
369
|
+
...(context?.failureSignature ? { failureSignature: context.failureSignature } : {}),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
buildQueueFailureContext(issue, event, queueRepairContext) {
|
|
373
|
+
const repoFullName = event.repoFullName || this.config.projects.find((p) => p.id === issue.projectId)?.github?.repoFullName || "";
|
|
374
|
+
const incident = queueRepairContext && typeof queueRepairContext === "object"
|
|
375
|
+
? queueRepairContext
|
|
376
|
+
: undefined;
|
|
377
|
+
const summary = typeof incident?.incidentSummary === "string"
|
|
378
|
+
? incident.incidentSummary
|
|
379
|
+
: event.checkOutputSummary ?? event.checkOutputTitle;
|
|
380
|
+
const failureHeadSha = event.headSha;
|
|
381
|
+
const failureSignature = [
|
|
382
|
+
"queue_eviction",
|
|
383
|
+
failureHeadSha ?? "unknown-sha",
|
|
384
|
+
event.checkName ?? "merge-steward/queue",
|
|
385
|
+
].join("::");
|
|
386
|
+
return {
|
|
387
|
+
source: "queue_eviction",
|
|
388
|
+
repoFullName,
|
|
389
|
+
capturedAt: new Date().toISOString(),
|
|
390
|
+
...(failureHeadSha ? { headSha: failureHeadSha, failureHeadSha } : {}),
|
|
391
|
+
...(event.checkName ? { checkName: event.checkName } : {}),
|
|
392
|
+
...(event.checkUrl ? { checkUrl: event.checkUrl } : {}),
|
|
393
|
+
...(event.checkDetailsUrl ? { checkDetailsUrl: event.checkDetailsUrl } : {}),
|
|
394
|
+
...(summary ? { summary } : {}),
|
|
395
|
+
failureSignature,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
hasDuplicatePendingReactiveRun(issue, runType, failureContext) {
|
|
399
|
+
const signature = typeof failureContext.failureSignature === "string" ? failureContext.failureSignature : undefined;
|
|
400
|
+
const headSha = typeof failureContext.failureHeadSha === "string"
|
|
401
|
+
? failureContext.failureHeadSha
|
|
402
|
+
: typeof failureContext.headSha === "string" ? failureContext.headSha : undefined;
|
|
403
|
+
if (!signature)
|
|
404
|
+
return false;
|
|
405
|
+
if (issue.pendingRunType === runType && issue.pendingRunContextJson) {
|
|
406
|
+
const existing = safeJsonParse(issue.pendingRunContextJson);
|
|
407
|
+
if (existing?.failureSignature === signature
|
|
408
|
+
&& (headSha === undefined || existing.failureHeadSha === headSha || existing.headSha === headSha)) {
|
|
409
|
+
this.feed?.publish({
|
|
410
|
+
level: "info",
|
|
411
|
+
kind: "github",
|
|
412
|
+
issueKey: issue.issueKey,
|
|
413
|
+
projectId: issue.projectId,
|
|
414
|
+
stage: issue.factoryState,
|
|
415
|
+
status: "repair_deduped",
|
|
416
|
+
summary: `Skipped duplicate ${runType} for ${signature}`,
|
|
417
|
+
});
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (issue.lastAttemptedFailureSignature === signature
|
|
422
|
+
&& (headSha === undefined || issue.lastAttemptedFailureHeadSha === headSha)) {
|
|
423
|
+
this.feed?.publish({
|
|
424
|
+
level: "info",
|
|
425
|
+
kind: "github",
|
|
426
|
+
issueKey: issue.issueKey,
|
|
427
|
+
projectId: issue.projectId,
|
|
428
|
+
stage: issue.factoryState,
|
|
429
|
+
status: "repair_deduped",
|
|
430
|
+
summary: `Already attempted ${runType} for this failing PR head`,
|
|
315
431
|
});
|
|
432
|
+
return true;
|
|
316
433
|
}
|
|
434
|
+
return false;
|
|
317
435
|
}
|
|
318
436
|
async emitLinearActivity(issue, newState, event) {
|
|
319
437
|
if (!issue.agentSessionId)
|
|
@@ -421,9 +539,9 @@ export class GitHubWebhookHandler {
|
|
|
421
539
|
function resolveCheckClass(checkName, project) {
|
|
422
540
|
if (!checkName || !project)
|
|
423
541
|
return "code";
|
|
424
|
-
if (project.reviewChecks.some((name) => checkName.includes(name)))
|
|
542
|
+
if ((project.reviewChecks ?? []).some((name) => checkName.includes(name)))
|
|
425
543
|
return "review";
|
|
426
|
-
if (project.gateChecks.some((name) => checkName.includes(name)))
|
|
544
|
+
if ((project.gateChecks ?? []).some((name) => checkName.includes(name)))
|
|
427
545
|
return "gate";
|
|
428
546
|
return "code";
|
|
429
547
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseGitHubFailureContext, summarizeGitHubFailureContext } from "./github-failure-context.js";
|
|
1
2
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
2
3
|
import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
|
|
3
4
|
import { extractStageSummary, summarizeCurrentThread } from "./run-reporting.js";
|
|
@@ -149,6 +150,7 @@ export class IssueQueryService {
|
|
|
149
150
|
buildQueueProtocol(projectId, issue) {
|
|
150
151
|
const project = this.config.projects.find((entry) => entry.id === projectId);
|
|
151
152
|
const protocol = resolveMergeQueueProtocol(project);
|
|
153
|
+
const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
152
154
|
const queueIncident = issue.lastQueueIncidentJson
|
|
153
155
|
? parseStoredQueueRepairContext(issue.lastQueueIncidentJson)
|
|
154
156
|
: undefined;
|
|
@@ -159,8 +161,12 @@ export class IssueQueryService {
|
|
|
159
161
|
evictionCheckName: protocol.evictionCheckName,
|
|
160
162
|
prNumber: issue.prNumber ?? null,
|
|
161
163
|
lastFailureSource: issue.lastGitHubFailureSource ?? null,
|
|
164
|
+
lastFailureHeadSha: issue.lastGitHubFailureHeadSha ?? failureContext?.headSha ?? null,
|
|
165
|
+
lastFailureSignature: issue.lastGitHubFailureSignature ?? failureContext?.failureSignature ?? null,
|
|
162
166
|
lastFailureCheckName: issue.lastGitHubFailureCheckName ?? null,
|
|
163
167
|
lastFailureCheckUrl: issue.lastGitHubFailureCheckUrl ?? null,
|
|
168
|
+
lastFailureStepName: failureContext?.stepName ?? null,
|
|
169
|
+
lastFailureSummary: summarizeGitHubFailureContext(failureContext) ?? null,
|
|
164
170
|
lastFailureAt: issue.lastGitHubFailureAt ?? null,
|
|
165
171
|
lastQueueSignalAt: issue.lastQueueSignalAt ?? null,
|
|
166
172
|
lastIncidentId: queueIncident?.incidentId ?? null,
|
package/dist/linear-client.js
CHANGED
|
@@ -15,6 +15,7 @@ const LINEAR_ISSUE_SELECTION = `
|
|
|
15
15
|
state {
|
|
16
16
|
id
|
|
17
17
|
name
|
|
18
|
+
type
|
|
18
19
|
}
|
|
19
20
|
labels {
|
|
20
21
|
nodes {
|
|
@@ -317,6 +318,7 @@ export class LinearGraphqlClient {
|
|
|
317
318
|
...(issue.estimate != null ? { estimate: issue.estimate } : {}),
|
|
318
319
|
...(issue.state?.id ? { stateId: issue.state.id } : {}),
|
|
319
320
|
...(issue.state?.name ? { stateName: issue.state.name } : {}),
|
|
321
|
+
...(issue.state?.type ? { stateType: issue.state.type } : {}),
|
|
320
322
|
...(issue.team?.id ? { teamId: issue.team.id } : {}),
|
|
321
323
|
...(issue.team?.key ? { teamKey: issue.team.key } : {}),
|
|
322
324
|
...(issue.delegate?.id ? { delegateId: issue.delegate.id } : {}),
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ACTIVE_RUN_STATES, TERMINAL_STATES } from "./factory-state.js";
|
|
4
|
+
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
4
5
|
import { buildHookEnv, runProjectHook } from "./hook-runner.js";
|
|
5
6
|
import { buildAgentSessionPlanForIssue, } from "./agent-session-plan.js";
|
|
6
7
|
import { buildStageReport, countEventMethods, extractTurnId, resolveRunCompletionStatus, summarizeCurrentThread, } from "./run-reporting.js";
|
|
@@ -57,7 +58,9 @@ function buildRunPrompt(issue, runType, repoPath, context) {
|
|
|
57
58
|
// Add run-type-specific context for reactive runs
|
|
58
59
|
switch (runType) {
|
|
59
60
|
case "ci_repair":
|
|
60
|
-
lines.push("## CI Repair", "", "A CI check has failed on your PR. Fix the failure and push.", context?.
|
|
61
|
+
lines.push("## CI Repair", "", "A CI check has failed on your PR. Fix the failure and push.", context?.failureHeadSha ? `Failing head SHA: ${String(context.failureHeadSha)}` : "", context?.checkName ? `Failed check: ${String(context.checkName)}` : "", context?.jobName && context?.jobName !== context?.checkName ? `Failed job: ${String(context.jobName)}` : "", context?.stepName ? `Failed step: ${String(context.stepName)}` : "", context?.summary ? `Failure summary: ${String(context.summary)}` : "", context?.checkUrl ? `Check URL: ${String(context.checkUrl)}` : "", Array.isArray(context?.annotations) && context.annotations.length > 0
|
|
62
|
+
? `Annotations:\n${context.annotations.map((entry) => `- ${String(entry)}`).join("\n")}`
|
|
63
|
+
: "", "", "Read the CI failure logs, fix the code issue, run verification, commit and push.", "Do not change test expectations unless the test is genuinely wrong.", "");
|
|
61
64
|
break;
|
|
62
65
|
case "review_fix":
|
|
63
66
|
lines.push("## Review Changes Requested", "", "A reviewer has requested changes on your PR. Address the feedback and push.", context?.reviewerName ? `Reviewer: ${String(context.reviewerName)}` : "", context?.reviewBody ? `\n## Review comment\n\n${String(context.reviewBody)}` : "", "", "Steps:", "1. Read the review feedback and PR comments (`gh pr view --comments`).", "2. Check the current diff (`git diff origin/main`) — a prior rebase may have already resolved some concerns (e.g., scope-bundling from stale commits).", "3. For each review point: if already resolved, note why. If not, fix it.", "4. Run verification, commit and push.", "5. If you believe all concerns are resolved, request a re-review: `gh pr edit <PR#> --add-reviewer <reviewer>`.", " Do NOT just post a comment saying \"resolved\" — the reviewer must re-review to dismiss the CHANGES_REQUESTED state.", "");
|
|
@@ -155,6 +158,10 @@ export class RunOrchestrator {
|
|
|
155
158
|
runType,
|
|
156
159
|
promptText: prompt,
|
|
157
160
|
});
|
|
161
|
+
const failureHeadSha = typeof context?.failureHeadSha === "string"
|
|
162
|
+
? context.failureHeadSha
|
|
163
|
+
: typeof context?.headSha === "string" ? context.headSha : undefined;
|
|
164
|
+
const failureSignature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
|
|
158
165
|
this.db.upsertIssue({
|
|
159
166
|
projectId: item.projectId,
|
|
160
167
|
linearIssueId: item.issueId,
|
|
@@ -168,6 +175,12 @@ export class RunOrchestrator {
|
|
|
168
175
|
: runType === "review_fix" ? "changes_requested"
|
|
169
176
|
: runType === "queue_repair" ? "repairing_queue"
|
|
170
177
|
: "implementing",
|
|
178
|
+
...((runType === "ci_repair" || runType === "queue_repair") && failureSignature
|
|
179
|
+
? {
|
|
180
|
+
lastAttemptedFailureSignature: failureSignature,
|
|
181
|
+
lastAttemptedFailureHeadSha: failureHeadSha ?? null,
|
|
182
|
+
}
|
|
183
|
+
: {}),
|
|
171
184
|
});
|
|
172
185
|
return created;
|
|
173
186
|
});
|
|
@@ -402,6 +415,26 @@ export class RunOrchestrator {
|
|
|
402
415
|
const report = buildStageReport(run, trackedIssue, thread, countEventMethods(this.db.listThreadEvents(run.id)));
|
|
403
416
|
// Determine post-run state based on current PR metadata.
|
|
404
417
|
const freshIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
418
|
+
const verifiedRepairError = await this.verifyReactiveRunAdvancedBranch(run, freshIssue);
|
|
419
|
+
if (verifiedRepairError) {
|
|
420
|
+
const holdState = resolveRecoverablePostRunState(freshIssue) ?? "failed";
|
|
421
|
+
this.failRunAndClear(run, verifiedRepairError, holdState);
|
|
422
|
+
const heldIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
423
|
+
this.feed?.publish({
|
|
424
|
+
level: "warn",
|
|
425
|
+
kind: "turn",
|
|
426
|
+
issueKey: freshIssue.issueKey,
|
|
427
|
+
projectId: run.projectId,
|
|
428
|
+
stage: run.runType,
|
|
429
|
+
status: "branch_not_advanced",
|
|
430
|
+
summary: verifiedRepairError,
|
|
431
|
+
});
|
|
432
|
+
void this.emitLinearActivity(heldIssue, buildRunFailureActivity(run.runType, verifiedRepairError));
|
|
433
|
+
void this.syncLinearSession(heldIssue, { activeRunType: run.runType });
|
|
434
|
+
this.progressThrottle.delete(run.id);
|
|
435
|
+
this.activeThreadId = undefined;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
405
438
|
const postRunState = resolvePostRunState(freshIssue);
|
|
406
439
|
this.db.transaction(() => {
|
|
407
440
|
this.db.finishRun(run.id, {
|
|
@@ -419,10 +452,15 @@ export class RunOrchestrator {
|
|
|
419
452
|
...(postRunState === "awaiting_queue" || postRunState === "done"
|
|
420
453
|
? {
|
|
421
454
|
lastGitHubFailureSource: null,
|
|
455
|
+
lastGitHubFailureHeadSha: null,
|
|
456
|
+
lastGitHubFailureSignature: null,
|
|
422
457
|
lastGitHubFailureCheckName: null,
|
|
423
458
|
lastGitHubFailureCheckUrl: null,
|
|
459
|
+
lastGitHubFailureContextJson: null,
|
|
424
460
|
lastGitHubFailureAt: null,
|
|
425
461
|
lastQueueIncidentJson: null,
|
|
462
|
+
lastAttemptedFailureHeadSha: null,
|
|
463
|
+
lastAttemptedFailureSignature: null,
|
|
426
464
|
}
|
|
427
465
|
: {}),
|
|
428
466
|
});
|
|
@@ -534,8 +572,11 @@ export class RunOrchestrator {
|
|
|
534
572
|
// Checks failed + idle — route based on durable GitHub failure provenance.
|
|
535
573
|
if (issue.prCheckStatus === "failed") {
|
|
536
574
|
if (issue.lastGitHubFailureSource === "queue_eviction") {
|
|
537
|
-
|
|
538
|
-
|
|
575
|
+
const pendingRunContext = buildFailureContext(issue);
|
|
576
|
+
if (isDuplicateRepairAttempt(issue, pendingRunContext)) {
|
|
577
|
+
this.advanceIdleIssue(issue, "repairing_queue");
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
539
580
|
this.advanceIdleIssue(issue, "repairing_queue", {
|
|
540
581
|
pendingRunType: "queue_repair",
|
|
541
582
|
...(pendingRunContext ? { pendingRunContext } : {}),
|
|
@@ -544,8 +585,11 @@ export class RunOrchestrator {
|
|
|
544
585
|
continue;
|
|
545
586
|
}
|
|
546
587
|
if (issue.lastGitHubFailureSource === "branch_ci") {
|
|
547
|
-
|
|
548
|
-
|
|
588
|
+
const pendingRunContext = buildFailureContext(issue);
|
|
589
|
+
if (isDuplicateRepairAttempt(issue, pendingRunContext)) {
|
|
590
|
+
this.advanceIdleIssue(issue, "repairing_ci");
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
549
593
|
this.advanceIdleIssue(issue, "repairing_ci", {
|
|
550
594
|
pendingRunType: "ci_repair",
|
|
551
595
|
...(pendingRunContext ? { pendingRunContext } : {}),
|
|
@@ -566,8 +610,11 @@ export class RunOrchestrator {
|
|
|
566
610
|
});
|
|
567
611
|
continue;
|
|
568
612
|
}
|
|
569
|
-
|
|
570
|
-
|
|
613
|
+
const pendingRunContext = buildFailureContext(issue);
|
|
614
|
+
if (isDuplicateRepairAttempt(issue, pendingRunContext)) {
|
|
615
|
+
this.advanceIdleIssue(issue, "repairing_ci");
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
571
618
|
this.advanceIdleIssue(issue, "repairing_ci", {
|
|
572
619
|
pendingRunType: "ci_repair",
|
|
573
620
|
...(pendingRunContext ? { pendingRunContext } : {}),
|
|
@@ -606,6 +653,9 @@ export class RunOrchestrator {
|
|
|
606
653
|
}
|
|
607
654
|
}
|
|
608
655
|
advanceIdleIssue(issue, newState, options) {
|
|
656
|
+
if (issue.factoryState === newState && !options?.pendingRunType && !options?.clearFailureProvenance) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
609
659
|
this.logger.info({ issueKey: issue.issueKey, from: issue.factoryState, to: newState, pendingRunType: options?.pendingRunType }, "Reconciliation: advancing idle issue");
|
|
610
660
|
this.db.upsertIssue({
|
|
611
661
|
projectId: issue.projectId,
|
|
@@ -620,10 +670,15 @@ export class RunOrchestrator {
|
|
|
620
670
|
...(options?.clearFailureProvenance
|
|
621
671
|
? {
|
|
622
672
|
lastGitHubFailureSource: null,
|
|
673
|
+
lastGitHubFailureHeadSha: null,
|
|
674
|
+
lastGitHubFailureSignature: null,
|
|
623
675
|
lastGitHubFailureCheckName: null,
|
|
624
676
|
lastGitHubFailureCheckUrl: null,
|
|
677
|
+
lastGitHubFailureContextJson: null,
|
|
625
678
|
lastGitHubFailureAt: null,
|
|
626
679
|
lastQueueIncidentJson: null,
|
|
680
|
+
lastAttemptedFailureHeadSha: null,
|
|
681
|
+
lastAttemptedFailureSignature: null,
|
|
627
682
|
}
|
|
628
683
|
: {}),
|
|
629
684
|
});
|
|
@@ -790,7 +845,7 @@ export class RunOrchestrator {
|
|
|
790
845
|
else if (run.runType === "review_fix" && issue.reviewFixAttempts > 0) {
|
|
791
846
|
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, reviewFixAttempts: issue.reviewFixAttempts - 1 });
|
|
792
847
|
}
|
|
793
|
-
const recoveredState =
|
|
848
|
+
const recoveredState = resolveRecoverablePostRunState(this.db.getIssue(run.projectId, run.linearIssueId) ?? issue);
|
|
794
849
|
this.failRunAndClear(run, "Codex turn was interrupted", recoveredState);
|
|
795
850
|
const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
796
851
|
if (recoveredState) {
|
|
@@ -815,6 +870,21 @@ export class RunOrchestrator {
|
|
|
815
870
|
const trackedIssue = this.db.issueToTrackedIssue(issue);
|
|
816
871
|
const report = buildStageReport(run, trackedIssue, thread, countEventMethods(this.db.listThreadEvents(run.id)));
|
|
817
872
|
const freshIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
873
|
+
const verifiedRepairError = await this.verifyReactiveRunAdvancedBranch(run, freshIssue);
|
|
874
|
+
if (verifiedRepairError) {
|
|
875
|
+
const holdState = resolveRecoverablePostRunState(freshIssue) ?? "failed";
|
|
876
|
+
this.failRunAndClear(run, verifiedRepairError, holdState);
|
|
877
|
+
this.feed?.publish({
|
|
878
|
+
level: "warn",
|
|
879
|
+
kind: "turn",
|
|
880
|
+
issueKey: issue.issueKey,
|
|
881
|
+
projectId: run.projectId,
|
|
882
|
+
stage: run.runType,
|
|
883
|
+
status: "branch_not_advanced",
|
|
884
|
+
summary: verifiedRepairError,
|
|
885
|
+
});
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
818
888
|
const postRunState = resolvePostRunState(freshIssue);
|
|
819
889
|
this.db.transaction(() => {
|
|
820
890
|
this.db.finishRun(run.id, {
|
|
@@ -832,10 +902,15 @@ export class RunOrchestrator {
|
|
|
832
902
|
...(postRunState === "awaiting_queue" || postRunState === "done"
|
|
833
903
|
? {
|
|
834
904
|
lastGitHubFailureSource: null,
|
|
905
|
+
lastGitHubFailureHeadSha: null,
|
|
906
|
+
lastGitHubFailureSignature: null,
|
|
835
907
|
lastGitHubFailureCheckName: null,
|
|
836
908
|
lastGitHubFailureCheckUrl: null,
|
|
909
|
+
lastGitHubFailureContextJson: null,
|
|
837
910
|
lastGitHubFailureAt: null,
|
|
838
911
|
lastQueueIncidentJson: null,
|
|
912
|
+
lastAttemptedFailureHeadSha: null,
|
|
913
|
+
lastAttemptedFailureSignature: null,
|
|
839
914
|
}
|
|
840
915
|
: {}),
|
|
841
916
|
});
|
|
@@ -908,6 +983,41 @@ export class RunOrchestrator {
|
|
|
908
983
|
});
|
|
909
984
|
});
|
|
910
985
|
}
|
|
986
|
+
async verifyReactiveRunAdvancedBranch(run, issue) {
|
|
987
|
+
if (run.runType !== "ci_repair" && run.runType !== "queue_repair") {
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
if (!issue.prNumber || issue.prState !== "open" || !issue.lastGitHubFailureHeadSha) {
|
|
991
|
+
return undefined;
|
|
992
|
+
}
|
|
993
|
+
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
994
|
+
if (!project?.github?.repoFullName) {
|
|
995
|
+
return undefined;
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
const { stdout, exitCode } = await execCommand("gh", [
|
|
999
|
+
"pr", "view", String(issue.prNumber),
|
|
1000
|
+
"--repo", project.github.repoFullName,
|
|
1001
|
+
"--json", "headRefOid,state",
|
|
1002
|
+
], { timeoutMs: 10_000 });
|
|
1003
|
+
if (exitCode !== 0)
|
|
1004
|
+
return undefined;
|
|
1005
|
+
const pr = JSON.parse(stdout);
|
|
1006
|
+
if (pr.state?.toUpperCase() !== "OPEN")
|
|
1007
|
+
return undefined;
|
|
1008
|
+
if (!pr.headRefOid || pr.headRefOid !== issue.lastGitHubFailureHeadSha)
|
|
1009
|
+
return undefined;
|
|
1010
|
+
return `Repair finished but PR #${issue.prNumber} is still on failing head ${issue.lastGitHubFailureHeadSha.slice(0, 8)}`;
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
this.logger.debug({
|
|
1014
|
+
issueKey: issue.issueKey,
|
|
1015
|
+
prNumber: issue.prNumber,
|
|
1016
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1017
|
+
}, "Failed to verify PR head advancement after repair");
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
911
1021
|
async emitLinearActivity(issue, content, options) {
|
|
912
1022
|
if (!issue.agentSessionId)
|
|
913
1023
|
return;
|
|
@@ -1025,23 +1135,59 @@ function resolvePostRunState(issue) {
|
|
|
1025
1135
|
}
|
|
1026
1136
|
return undefined;
|
|
1027
1137
|
}
|
|
1138
|
+
function resolveRecoverablePostRunState(issue) {
|
|
1139
|
+
if (!issue.prNumber) {
|
|
1140
|
+
return resolvePostRunState(issue);
|
|
1141
|
+
}
|
|
1142
|
+
if (issue.prState === "merged")
|
|
1143
|
+
return "done";
|
|
1144
|
+
if (issue.prState === "open") {
|
|
1145
|
+
if (issue.lastGitHubFailureSource === "queue_eviction")
|
|
1146
|
+
return "repairing_queue";
|
|
1147
|
+
if (issue.prCheckStatus === "failed" || issue.lastGitHubFailureSource === "branch_ci")
|
|
1148
|
+
return "repairing_ci";
|
|
1149
|
+
if (issue.prReviewState === "changes_requested")
|
|
1150
|
+
return "changes_requested";
|
|
1151
|
+
if (issue.prReviewState === "approved")
|
|
1152
|
+
return "awaiting_queue";
|
|
1153
|
+
return "pr_open";
|
|
1154
|
+
}
|
|
1155
|
+
return resolvePostRunState(issue);
|
|
1156
|
+
}
|
|
1028
1157
|
function buildFailureContext(issue) {
|
|
1158
|
+
const storedFailureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
1029
1159
|
const queueRepairContext = issue.lastQueueIncidentJson
|
|
1030
1160
|
? parseStoredQueueRepairContext(issue.lastQueueIncidentJson)
|
|
1031
1161
|
: undefined;
|
|
1032
1162
|
if (!queueRepairContext
|
|
1033
1163
|
&& !issue.lastGitHubFailureSource
|
|
1164
|
+
&& !issue.lastGitHubFailureHeadSha
|
|
1165
|
+
&& !issue.lastGitHubFailureSignature
|
|
1034
1166
|
&& !issue.lastGitHubFailureCheckName
|
|
1035
|
-
&& !issue.lastGitHubFailureCheckUrl
|
|
1167
|
+
&& !issue.lastGitHubFailureCheckUrl
|
|
1168
|
+
&& !storedFailureContext) {
|
|
1036
1169
|
return undefined;
|
|
1037
1170
|
}
|
|
1038
1171
|
return {
|
|
1039
1172
|
...(issue.lastGitHubFailureSource ? { failureReason: issue.lastGitHubFailureSource } : {}),
|
|
1173
|
+
...(issue.lastGitHubFailureHeadSha ? { failureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
|
|
1174
|
+
...(issue.lastGitHubFailureSignature ? { failureSignature: issue.lastGitHubFailureSignature } : {}),
|
|
1040
1175
|
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
1041
1176
|
...(issue.lastGitHubFailureCheckUrl ? { checkUrl: issue.lastGitHubFailureCheckUrl } : {}),
|
|
1177
|
+
...(storedFailureContext ? storedFailureContext : {}),
|
|
1042
1178
|
...(queueRepairContext ? queueRepairContext : {}),
|
|
1043
1179
|
};
|
|
1044
1180
|
}
|
|
1181
|
+
function isDuplicateRepairAttempt(issue, context) {
|
|
1182
|
+
const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
|
|
1183
|
+
const headSha = typeof context?.failureHeadSha === "string"
|
|
1184
|
+
? context.failureHeadSha
|
|
1185
|
+
: typeof context?.headSha === "string" ? context.headSha : undefined;
|
|
1186
|
+
if (!signature)
|
|
1187
|
+
return false;
|
|
1188
|
+
return issue.lastAttemptedFailureSignature === signature
|
|
1189
|
+
&& (headSha === undefined || issue.lastAttemptedFailureHeadSha === headSha);
|
|
1190
|
+
}
|
|
1045
1191
|
function appendQueueRepairContext(lines, context) {
|
|
1046
1192
|
const incidentTitle = typeof context?.incidentTitle === "string" ? context.incidentTitle.trim() : "";
|
|
1047
1193
|
const incidentSummary = typeof context?.incidentSummary === "string" ? context.incidentSummary.trim() : "";
|