patchrelay 0.37.0 → 0.38.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/README.md +47 -9
- package/dist/awaiting-input-reason.js +9 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +64 -5
- package/dist/cli/data.js +1 -0
- package/dist/cli/formatters/text.js +5 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/output.js +2 -0
- package/dist/cli/watch/IssueRow.js +4 -3
- package/dist/cli/watch/StatusBar.js +2 -1
- package/dist/cli/watch/detail-rows.js +4 -3
- package/dist/cli/watch/pr-status.js +2 -1
- package/dist/cli/watch/state-visualization.js +5 -1
- package/dist/db/issue-session-store.js +0 -14
- package/dist/db/issue-store.js +8 -16
- package/dist/db/migrations.js +6 -13
- package/dist/db.js +1 -3
- package/dist/factory-state.js +1 -1
- package/dist/github-webhook-handler.js +95 -54
- package/dist/github-webhooks.js +4 -0
- package/dist/idle-reconciliation.js +38 -22
- package/dist/implementation-outcome-policy.js +3 -1
- package/dist/issue-overview-query.js +8 -0
- package/dist/issue-session-projector.js +1 -0
- package/dist/issue-session.js +8 -0
- package/dist/linear-session-reporting.js +43 -5
- package/dist/linear-session-sync.js +9 -1
- package/dist/linear-status-comment-sync.js +47 -2
- package/dist/linear-workflow-state-sync.js +2 -2
- package/dist/operator-retry-event.js +15 -12
- package/dist/paused-issue-state.js +24 -0
- package/dist/pr-state.js +49 -0
- package/dist/run-launcher.js +0 -1
- package/dist/run-orchestrator.js +2 -5
- package/dist/run-reconciler.js +10 -0
- package/dist/run-recovery-service.js +1 -10
- package/dist/service-issue-actions.js +10 -4
- package/dist/service-startup-recovery.js +9 -6
- package/dist/service.js +0 -1
- package/dist/tracked-issue-list-query.js +6 -2
- package/dist/tracked-issue-projector.js +8 -0
- package/dist/waiting-reason.js +13 -2
- package/dist/webhooks/agent-session-handler.js +9 -1
- package/dist/webhooks/comment-wake-handler.js +12 -0
- package/dist/webhooks/decision-helpers.js +44 -3
- package/dist/webhooks/dependency-readiness-handler.js +1 -0
- package/dist/webhooks/desired-stage-recorder.js +40 -10
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveFactoryStateFromGitHub
|
|
1
|
+
import { resolveFactoryStateFromGitHub } from "./factory-state.js";
|
|
2
2
|
import { createGitHubCiSnapshotResolver, createGitHubFailureContextResolver, summarizeGitHubFailureContext, } from "./github-failure-context.js";
|
|
3
3
|
import { normalizeGitHubWebhook, verifyGitHubWebhookSignature } from "./github-webhooks.js";
|
|
4
4
|
import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
|
|
@@ -7,6 +7,7 @@ import { buildGitHubStateActivity } from "./linear-session-reporting.js";
|
|
|
7
7
|
import { resolveMergeQueueProtocol, } from "./merge-queue-protocol.js";
|
|
8
8
|
import { buildQueueRepairContextFromEvent } from "./merge-queue-incident.js";
|
|
9
9
|
import { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
|
|
10
|
+
import { buildClosedPrCleanupFields, isIssueTerminal, resolveClosedPrFactoryState, resolveClosedPrDisposition, } from "./pr-state.js";
|
|
10
11
|
import { resolveSecret } from "./resolve-secret.js";
|
|
11
12
|
import { safeJsonParse } from "./utils.js";
|
|
12
13
|
/**
|
|
@@ -33,7 +34,6 @@ export class GitHubWebhookHandler {
|
|
|
33
34
|
failureContextResolver;
|
|
34
35
|
ciSnapshotResolver;
|
|
35
36
|
fetchImpl;
|
|
36
|
-
patchRelayAuthorLogins = new Set();
|
|
37
37
|
constructor(config, db, linearProvider, enqueueIssue, logger, codex, feed, failureContextResolver = createGitHubFailureContextResolver(), ciSnapshotResolver = createGitHubCiSnapshotResolver(), fetchImpl = fetch) {
|
|
38
38
|
this.config = config;
|
|
39
39
|
this.db = db;
|
|
@@ -45,18 +45,6 @@ export class GitHubWebhookHandler {
|
|
|
45
45
|
this.failureContextResolver = failureContextResolver;
|
|
46
46
|
this.ciSnapshotResolver = ciSnapshotResolver;
|
|
47
47
|
this.fetchImpl = fetchImpl;
|
|
48
|
-
for (const login of resolvePatchRelayAuthorLoginsFromEnv()) {
|
|
49
|
-
this.patchRelayAuthorLogins.add(login);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
setPatchRelayAuthorLogins(logins) {
|
|
53
|
-
this.patchRelayAuthorLogins.clear();
|
|
54
|
-
for (const login of logins) {
|
|
55
|
-
const normalized = normalizeAuthorLogin(login);
|
|
56
|
-
if (normalized) {
|
|
57
|
-
this.patchRelayAuthorLogins.add(normalized);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
48
|
}
|
|
61
49
|
async acceptGitHubWebhook(params) {
|
|
62
50
|
// Deduplicate
|
|
@@ -129,13 +117,17 @@ export class GitHubWebhookHandler {
|
|
|
129
117
|
this.logger.debug({ eventType: params.eventType }, "GitHub webhook: unrecognized event type or action");
|
|
130
118
|
return;
|
|
131
119
|
}
|
|
132
|
-
|
|
133
|
-
|
|
120
|
+
const project = this.config.projects.find((candidate) => candidate.github?.repoFullName === event.repoFullName);
|
|
121
|
+
if (!project) {
|
|
122
|
+
this.logger.debug({ repoFullName: event.repoFullName, triggerEvent: event.triggerEvent }, "GitHub webhook: no configured project for repository");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const resolved = this.resolveIssueForEvent(project, event);
|
|
126
|
+
const issue = resolved?.issue;
|
|
134
127
|
if (!issue) {
|
|
135
|
-
this.logger.debug({ branchName: event.branchName, triggerEvent: event.triggerEvent }, "GitHub webhook: no matching issue
|
|
128
|
+
this.logger.debug({ repoFullName: event.repoFullName, branchName: event.branchName, prNumber: event.prNumber, triggerEvent: event.triggerEvent }, "GitHub webhook: no matching tracked issue");
|
|
136
129
|
return;
|
|
137
130
|
}
|
|
138
|
-
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
139
131
|
const immediateCheckStatus = this.deriveImmediatePrCheckStatus(issue, event, project);
|
|
140
132
|
// Update PR state on the issue
|
|
141
133
|
this.db.issues.upsertIssue({
|
|
@@ -148,11 +140,15 @@ export class GitHubWebhookHandler {
|
|
|
148
140
|
...(event.prAuthorLogin !== undefined ? { prAuthorLogin: event.prAuthorLogin } : {}),
|
|
149
141
|
...(event.reviewState !== undefined ? { prReviewState: event.reviewState } : {}),
|
|
150
142
|
...(immediateCheckStatus !== undefined ? { prCheckStatus: immediateCheckStatus } : {}),
|
|
143
|
+
...(resolved.linkedBy === "issue_key" ? { branchName: event.branchName } : {}),
|
|
151
144
|
...(event.reviewState === "changes_requested"
|
|
152
145
|
? { lastBlockingReviewHeadSha: event.reviewCommitId ?? event.headSha ?? null }
|
|
153
146
|
: event.reviewState === "approved"
|
|
154
147
|
? { lastBlockingReviewHeadSha: null }
|
|
155
148
|
: {}),
|
|
149
|
+
...(event.triggerEvent === "pr_closed"
|
|
150
|
+
? buildClosedPrCleanupFields()
|
|
151
|
+
: {}),
|
|
156
152
|
});
|
|
157
153
|
await this.updateCiSnapshot(issue, event, project);
|
|
158
154
|
await this.updateFailureProvenance(issue, event, project);
|
|
@@ -226,18 +222,75 @@ export class GitHubWebhookHandler {
|
|
|
226
222
|
await this.handleTerminalPrEvent(freshIssue, event);
|
|
227
223
|
}
|
|
228
224
|
}
|
|
225
|
+
resolveIssueForEvent(project, event) {
|
|
226
|
+
if (event.prNumber !== undefined) {
|
|
227
|
+
const byPr = this.db.issues.getIssueByPrNumber(event.prNumber);
|
|
228
|
+
if (byPr && byPr.projectId === project.id) {
|
|
229
|
+
return { issue: byPr, linkedBy: "pr" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const byBranch = this.db.issues.getIssueByBranch(event.branchName);
|
|
233
|
+
if (byBranch && byBranch.projectId === project.id) {
|
|
234
|
+
return { issue: byBranch, linkedBy: "branch" };
|
|
235
|
+
}
|
|
236
|
+
const byIssueKey = this.resolveIssueByExplicitIssueKey(project, event);
|
|
237
|
+
if (byIssueKey) {
|
|
238
|
+
return { issue: byIssueKey, linkedBy: "issue_key" };
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
resolveIssueByExplicitIssueKey(project, event) {
|
|
243
|
+
const candidates = new Set();
|
|
244
|
+
const sources = [event.prTitle, event.prBody, event.branchName];
|
|
245
|
+
for (const prefix of project.issueKeyPrefixes) {
|
|
246
|
+
const normalizedPrefix = prefix.trim();
|
|
247
|
+
if (!normalizedPrefix)
|
|
248
|
+
continue;
|
|
249
|
+
const pattern = new RegExp(`\\b${escapeRegExp(normalizedPrefix)}-\\d+\\b`, "gi");
|
|
250
|
+
for (const source of sources) {
|
|
251
|
+
if (!source)
|
|
252
|
+
continue;
|
|
253
|
+
for (const match of source.matchAll(pattern)) {
|
|
254
|
+
candidates.add(match[0].toUpperCase());
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (candidates.size !== 1) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const [issueKey] = [...candidates];
|
|
262
|
+
if (!issueKey) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
266
|
+
return issue?.projectId === project.id ? issue : undefined;
|
|
267
|
+
}
|
|
229
268
|
resolveFactoryStateForEvent(issue, event, project) {
|
|
269
|
+
if (event.triggerEvent === "pr_closed") {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
const effectiveCurrentState = (issue.factoryState === "awaiting_input" || issue.factoryState === "delegated")
|
|
273
|
+
&& (event.prState === "open" || event.prNumber !== undefined)
|
|
274
|
+
? "pr_open"
|
|
275
|
+
: issue.factoryState;
|
|
230
276
|
if (event.triggerEvent === "check_failed"
|
|
231
277
|
&& this.isQueueEvictionFailure(issue, event, project)
|
|
232
278
|
&& issue.prState === "open"
|
|
233
279
|
&& issue.activeRunId === undefined
|
|
234
|
-
&& !
|
|
280
|
+
&& !isIssueTerminal(issue)) {
|
|
235
281
|
return "repairing_queue";
|
|
236
282
|
}
|
|
237
|
-
|
|
283
|
+
const resolved = resolveFactoryStateFromGitHub(event.triggerEvent, effectiveCurrentState, {
|
|
238
284
|
prReviewState: issue.prReviewState,
|
|
239
285
|
activeRunId: issue.activeRunId,
|
|
240
286
|
});
|
|
287
|
+
if (resolved !== undefined) {
|
|
288
|
+
return resolved;
|
|
289
|
+
}
|
|
290
|
+
if (effectiveCurrentState !== issue.factoryState) {
|
|
291
|
+
return effectiveCurrentState;
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
241
294
|
}
|
|
242
295
|
async updateCiSnapshot(issue, event, project) {
|
|
243
296
|
if (event.triggerEvent === "pr_merged") {
|
|
@@ -318,17 +371,17 @@ export class GitHubWebhookHandler {
|
|
|
318
371
|
return;
|
|
319
372
|
// Don't trigger on terminal issues — late-arriving webhooks (e.g.
|
|
320
373
|
// merge_group_failed after pr_merged) must not resurrect done issues.
|
|
321
|
-
if (
|
|
374
|
+
if (isIssueTerminal(issue))
|
|
322
375
|
return;
|
|
323
|
-
if (!
|
|
376
|
+
if (!issue.delegatedToPatchRelay) {
|
|
324
377
|
this.feed?.publish({
|
|
325
378
|
level: "info",
|
|
326
379
|
kind: "github",
|
|
327
380
|
issueKey: issue.issueKey,
|
|
328
381
|
projectId: issue.projectId,
|
|
329
382
|
stage: issue.factoryState,
|
|
330
|
-
status: "
|
|
331
|
-
summary: `Ignored ${event.triggerEvent}
|
|
383
|
+
status: "ignored_undelegated",
|
|
384
|
+
summary: `Ignored ${event.triggerEvent} because the issue is undelegated`,
|
|
332
385
|
});
|
|
333
386
|
return;
|
|
334
387
|
}
|
|
@@ -365,7 +418,6 @@ export class GitHubWebhookHandler {
|
|
|
365
418
|
}),
|
|
366
419
|
dedupeKey: failureContext.failureSignature,
|
|
367
420
|
});
|
|
368
|
-
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
369
421
|
const queuedRunType = hadPendingWake
|
|
370
422
|
? this.peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId)
|
|
371
423
|
: this.enqueuePendingSessionWake(issue.projectId, issue.linearIssueId);
|
|
@@ -423,7 +475,6 @@ export class GitHubWebhookHandler {
|
|
|
423
475
|
}),
|
|
424
476
|
dedupeKey: failureContext.failureSignature,
|
|
425
477
|
});
|
|
426
|
-
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
427
478
|
const queuedRunType = hadPendingWake
|
|
428
479
|
? this.peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId)
|
|
429
480
|
: this.enqueuePendingSessionWake(issue.projectId, issue.linearIssueId);
|
|
@@ -469,7 +520,6 @@ export class GitHubWebhookHandler {
|
|
|
469
520
|
event.reviewerName ?? "unknown-reviewer",
|
|
470
521
|
].join("::"),
|
|
471
522
|
});
|
|
472
|
-
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
473
523
|
const queuedRunType = hadPendingWake
|
|
474
524
|
? this.peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId)
|
|
475
525
|
: this.enqueuePendingSessionWake(issue.projectId, issue.linearIssueId);
|
|
@@ -521,11 +571,14 @@ export class GitHubWebhookHandler {
|
|
|
521
571
|
: "Pull request closed during active run",
|
|
522
572
|
});
|
|
523
573
|
}
|
|
574
|
+
const terminalFactoryState = event.triggerEvent === "pr_merged"
|
|
575
|
+
? "done"
|
|
576
|
+
: resolveClosedPrFactoryState(issue);
|
|
524
577
|
this.db.issues.upsertIssue({
|
|
525
578
|
projectId: issue.projectId,
|
|
526
579
|
linearIssueId: issue.linearIssueId,
|
|
527
580
|
activeRunId: null,
|
|
528
|
-
factoryState:
|
|
581
|
+
factoryState: terminalFactoryState,
|
|
529
582
|
});
|
|
530
583
|
};
|
|
531
584
|
const activeLease = this.db.issueSessions.getActiveIssueSessionLease(issue.projectId, issue.linearIssueId);
|
|
@@ -537,6 +590,17 @@ export class GitHubWebhookHandler {
|
|
|
537
590
|
}
|
|
538
591
|
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
539
592
|
const updatedIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
593
|
+
if (event.triggerEvent === "pr_closed" && resolveClosedPrDisposition(issue) === "redelegate") {
|
|
594
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
595
|
+
projectId: issue.projectId,
|
|
596
|
+
linearIssueId: issue.linearIssueId,
|
|
597
|
+
eventType: "delegated",
|
|
598
|
+
dedupeKey: `github_pr_closed:implementation:${issue.linearIssueId}`,
|
|
599
|
+
});
|
|
600
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
601
|
+
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
540
604
|
if (event.triggerEvent === "pr_merged") {
|
|
541
605
|
await this.completeLinearIssueAfterMerge(updatedIssue);
|
|
542
606
|
}
|
|
@@ -687,7 +751,7 @@ export class GitHubWebhookHandler {
|
|
|
687
751
|
if (!signature)
|
|
688
752
|
return false;
|
|
689
753
|
const pendingWake = this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
|
|
690
|
-
if (pendingWake?.runType === runType) {
|
|
754
|
+
if (pendingWake?.runType === runType && pendingWake.eventIds.length > 0) {
|
|
691
755
|
const existing = pendingWake.context;
|
|
692
756
|
if (existing?.failureSignature === signature
|
|
693
757
|
&& (headSha === undefined || existing.failureHeadSha === headSha || existing.headSha === headSha)) {
|
|
@@ -923,8 +987,6 @@ export class GitHubWebhookHandler {
|
|
|
923
987
|
const issue = this.db.issues.getIssueByPrNumber(prNumber);
|
|
924
988
|
if (!issue)
|
|
925
989
|
return;
|
|
926
|
-
if (!this.isPatchRelayOwnedPr(issue))
|
|
927
|
-
return;
|
|
928
990
|
this.feed?.publish({
|
|
929
991
|
level: "info",
|
|
930
992
|
kind: "comment",
|
|
@@ -987,30 +1049,9 @@ export class GitHubWebhookHandler {
|
|
|
987
1049
|
this.enqueueIssue(projectId, issueId);
|
|
988
1050
|
return wake.runType;
|
|
989
1051
|
}
|
|
990
|
-
isPatchRelayOwnedPr(issue) {
|
|
991
|
-
const author = normalizeAuthorLogin(issue.prAuthorLogin);
|
|
992
|
-
if (author) {
|
|
993
|
-
if (this.patchRelayAuthorLogins.size > 0) {
|
|
994
|
-
return this.patchRelayAuthorLogins.has(author);
|
|
995
|
-
}
|
|
996
|
-
return author.includes("patchrelay");
|
|
997
|
-
}
|
|
998
|
-
// Transitional fallback for rows written before author tracking existed.
|
|
999
|
-
return issue.prNumber !== undefined && issue.branchOwner === "patchrelay";
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
function normalizeAuthorLogin(login) {
|
|
1003
|
-
const normalized = login?.trim().toLowerCase();
|
|
1004
|
-
return normalized ? normalized : undefined;
|
|
1005
1052
|
}
|
|
1006
|
-
function
|
|
1007
|
-
return [
|
|
1008
|
-
process.env.PATCHRELAY_GITHUB_BOT_LOGIN,
|
|
1009
|
-
process.env.PATCHRELAY_GITHUB_BOT_NAME,
|
|
1010
|
-
]
|
|
1011
|
-
.flatMap((value) => (value ?? "").split(","))
|
|
1012
|
-
.map((value) => normalizeAuthorLogin(value))
|
|
1013
|
-
.filter((value) => Boolean(value));
|
|
1053
|
+
function escapeRegExp(value) {
|
|
1054
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1014
1055
|
}
|
|
1015
1056
|
function buildGitHubReviewUrl(repoFullName, prNumber, reviewId) {
|
|
1016
1057
|
if (!repoFullName || prNumber === undefined || reviewId === undefined) {
|
package/dist/github-webhooks.js
CHANGED
|
@@ -61,6 +61,8 @@ function normalizePullRequestEvent(payload, repoFullName) {
|
|
|
61
61
|
repoFullName,
|
|
62
62
|
branchName: pr.head.ref,
|
|
63
63
|
headSha: pr.head.sha,
|
|
64
|
+
prTitle: pr.title ?? undefined,
|
|
65
|
+
prBody: pr.body ?? undefined,
|
|
64
66
|
prNumber: pr.number,
|
|
65
67
|
prUrl: pr.html_url,
|
|
66
68
|
prState,
|
|
@@ -98,6 +100,8 @@ function normalizePullRequestReviewEvent(payload, repoFullName) {
|
|
|
98
100
|
repoFullName,
|
|
99
101
|
branchName: pr.head.ref,
|
|
100
102
|
headSha: pr.head.sha,
|
|
103
|
+
prTitle: pr.title ?? undefined,
|
|
104
|
+
prBody: pr.body ?? undefined,
|
|
101
105
|
prNumber: pr.number,
|
|
102
106
|
prUrl: pr.html_url,
|
|
103
107
|
prState: "open",
|
|
@@ -3,6 +3,7 @@ import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
|
3
3
|
import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
4
4
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
5
5
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
6
|
+
import { buildClosedPrCleanupFields, resolveClosedPrDisposition } from "./pr-state.js";
|
|
6
7
|
import { execCommand } from "./utils.js";
|
|
7
8
|
const DEFAULT_REVIEW_FIX_BUDGET = 12;
|
|
8
9
|
function isFailingCheckStatus(status) {
|
|
@@ -88,15 +89,6 @@ function hasFailureProvenance(issue) {
|
|
|
88
89
|
|| issue.lastAttemptedFailureHeadSha
|
|
89
90
|
|| issue.lastAttemptedFailureSignature);
|
|
90
91
|
}
|
|
91
|
-
export function resolveBranchOwnerForStateTransition(newState, pendingRunType) {
|
|
92
|
-
if (pendingRunType)
|
|
93
|
-
return "patchrelay";
|
|
94
|
-
if (newState === "awaiting_queue")
|
|
95
|
-
return "patchrelay";
|
|
96
|
-
if (newState === "repairing_ci" || newState === "repairing_queue")
|
|
97
|
-
return "patchrelay";
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
92
|
export class IdleIssueReconciler {
|
|
101
93
|
db;
|
|
102
94
|
config;
|
|
@@ -152,6 +144,8 @@ export class IdleIssueReconciler {
|
|
|
152
144
|
await this.reconcileFromGitHub(issue);
|
|
153
145
|
}
|
|
154
146
|
for (const issue of this.db.issues.listBlockedDelegatedIssues()) {
|
|
147
|
+
if (!issue.delegatedToPatchRelay)
|
|
148
|
+
continue;
|
|
155
149
|
const unresolved = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
156
150
|
if (unresolved === 0) {
|
|
157
151
|
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
@@ -205,10 +199,6 @@ export class IdleIssueReconciler {
|
|
|
205
199
|
}
|
|
206
200
|
: {}),
|
|
207
201
|
});
|
|
208
|
-
const branchOwner = resolveBranchOwnerForStateTransition(newState, options?.pendingRunType);
|
|
209
|
-
if (branchOwner) {
|
|
210
|
-
this.db.issues.setBranchOwner(issue.projectId, issue.linearIssueId, branchOwner);
|
|
211
|
-
}
|
|
212
202
|
if (options?.pendingRunType) {
|
|
213
203
|
this.appendWakeEvent(issue, options.pendingRunType, options.pendingRunContext, "idle_reconciliation");
|
|
214
204
|
}
|
|
@@ -253,6 +243,9 @@ export class IdleIssueReconciler {
|
|
|
253
243
|
});
|
|
254
244
|
}
|
|
255
245
|
async routeFailedIssue(issue) {
|
|
246
|
+
if (!issue.delegatedToPatchRelay) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
256
249
|
issue = await this.refreshMissingFailureProvenance(issue);
|
|
257
250
|
issue = await this.reclassifyStaleBranchFailure(issue);
|
|
258
251
|
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
@@ -439,12 +432,33 @@ export class IdleIssueReconciler {
|
|
|
439
432
|
return;
|
|
440
433
|
}
|
|
441
434
|
if (pr.state === "CLOSED") {
|
|
442
|
-
|
|
443
|
-
this.db.issues.upsertIssue({
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
435
|
+
const closedPrDisposition = resolveClosedPrDisposition(issue);
|
|
436
|
+
this.db.issues.upsertIssue({
|
|
437
|
+
projectId: issue.projectId,
|
|
438
|
+
linearIssueId: issue.linearIssueId,
|
|
439
|
+
prState: "closed",
|
|
440
|
+
...buildClosedPrCleanupFields(),
|
|
447
441
|
});
|
|
442
|
+
if (closedPrDisposition === "done") {
|
|
443
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed for an already completed issue; preserving done state");
|
|
444
|
+
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (closedPrDisposition === "terminal") {
|
|
448
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, factoryState: issue.factoryState }, "Reconciliation: PR was closed on a terminal issue; preserving terminal state");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (issue.delegatedToPatchRelay) {
|
|
452
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed on unfinished delegated work, re-delegating for implementation");
|
|
453
|
+
this.advanceIdleIssue(issue, "delegated", {
|
|
454
|
+
pendingRunType: "implementation",
|
|
455
|
+
clearFailureProvenance: true,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed while undelegated; preserving paused local-work state");
|
|
460
|
+
this.advanceIdleIssue(issue, "delegated", { clearFailureProvenance: true });
|
|
461
|
+
}
|
|
448
462
|
return;
|
|
449
463
|
}
|
|
450
464
|
const headAdvanced = Boolean(pr.headRefOid && pr.headRefOid !== previousHeadSha);
|
|
@@ -464,7 +478,8 @@ export class IdleIssueReconciler {
|
|
|
464
478
|
return;
|
|
465
479
|
}
|
|
466
480
|
}
|
|
467
|
-
if (
|
|
481
|
+
if (issue.delegatedToPatchRelay
|
|
482
|
+
&& isReviewDecisionReviewRequired(pr.reviewDecision)
|
|
468
483
|
&& gateCheckStatus === "success"
|
|
469
484
|
&& hasCompletedReviewQuillVerdict(pr.statusCheckRollup)) {
|
|
470
485
|
this.logger.warn({ issueKey: issue.issueKey, prNumber: issue.prNumber, reviewDecision: pr.reviewDecision }, "Reconciliation: review-quill completed without a decisive GitHub review; escalating for operator input");
|
|
@@ -492,7 +507,8 @@ export class IdleIssueReconciler {
|
|
|
492
507
|
mergeConflictDetected,
|
|
493
508
|
downstreamOwned,
|
|
494
509
|
});
|
|
495
|
-
if (
|
|
510
|
+
if (issue.delegatedToPatchRelay
|
|
511
|
+
&& (issue.factoryState === "escalated" || issue.factoryState === "failed")
|
|
496
512
|
&& (reactiveIntent?.runType === "review_fix" || reactiveIntent?.runType === "branch_upkeep")) {
|
|
497
513
|
if (issue.reviewFixAttempts >= DEFAULT_REVIEW_FIX_BUDGET) {
|
|
498
514
|
this.logger.debug({
|
|
@@ -521,7 +537,7 @@ export class IdleIssueReconciler {
|
|
|
521
537
|
});
|
|
522
538
|
return;
|
|
523
539
|
}
|
|
524
|
-
if (reactiveIntent?.runType === "branch_upkeep" && mergeConflictDetected) {
|
|
540
|
+
if (issue.delegatedToPatchRelay && reactiveIntent?.runType === "branch_upkeep" && mergeConflictDetected) {
|
|
525
541
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable, mergeStateStatus: pr.mergeStateStatus }, "Reconciliation: PR still needs branch upkeep after requested changes");
|
|
526
542
|
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
527
543
|
pendingRunType: reactiveIntent.runType,
|
|
@@ -538,7 +554,7 @@ export class IdleIssueReconciler {
|
|
|
538
554
|
});
|
|
539
555
|
return;
|
|
540
556
|
}
|
|
541
|
-
if (reactiveIntent?.runType === "queue_repair" && mergeConflictDetected) {
|
|
557
|
+
if (issue.delegatedToPatchRelay && reactiveIntent?.runType === "queue_repair" && mergeConflictDetected) {
|
|
542
558
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable }, "Reconciliation: PR needs queue repair from fresh GitHub truth");
|
|
543
559
|
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
544
560
|
pendingRunType: reactiveIntent.runType,
|
|
@@ -46,7 +46,9 @@ export class ImplementationOutcomePolicy {
|
|
|
46
46
|
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
47
47
|
...(pr.author?.login ? { prAuthorLogin: pr.author.login } : {}),
|
|
48
48
|
}, "published PR verification refresh");
|
|
49
|
-
|
|
49
|
+
if (pr.state?.toLowerCase() !== "closed") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
}
|
|
@@ -138,11 +138,13 @@ export class IssueOverviewQuery {
|
|
|
138
138
|
const liveThread = await this.readLiveThread(activeRun);
|
|
139
139
|
const failureContext = parseGitHubFailureContext(issueRecord?.lastGitHubFailureContextJson);
|
|
140
140
|
const waitingReason = session.waitingReason ?? derivePatchRelayWaitingReason({
|
|
141
|
+
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
141
142
|
...(activeRun ? { activeRunType: activeRun.runType } : {}),
|
|
142
143
|
blockedByKeys,
|
|
143
144
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
144
145
|
pendingRunType: issueRecord?.pendingRunType,
|
|
145
146
|
prNumber: session.prNumber,
|
|
147
|
+
prState: issueRecord?.prState,
|
|
146
148
|
prHeadSha: issueRecord?.prHeadSha ?? session.prHeadSha,
|
|
147
149
|
prReviewState: issueRecord?.prReviewState,
|
|
148
150
|
prCheckStatus: issueRecord?.prCheckStatus,
|
|
@@ -153,17 +155,23 @@ export class IssueOverviewQuery {
|
|
|
153
155
|
id: issueRecord?.id ?? session.id,
|
|
154
156
|
projectId: session.projectId,
|
|
155
157
|
linearIssueId: session.linearIssueId,
|
|
158
|
+
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay ?? true,
|
|
156
159
|
...(session.issueKey ? { issueKey: session.issueKey } : {}),
|
|
157
160
|
...(issueRecord?.title ? { title: issueRecord.title } : {}),
|
|
158
161
|
...(issueRecord?.url ? { issueUrl: issueRecord.url } : {}),
|
|
159
162
|
...(issueRecord?.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
|
|
160
163
|
sessionState: session.sessionState,
|
|
161
164
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
165
|
+
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
|
166
|
+
...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
|
|
167
|
+
...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
|
|
168
|
+
...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
|
|
162
169
|
blockedByCount: unresolvedBlockedBy.length,
|
|
163
170
|
blockedByKeys,
|
|
164
171
|
readyForExecution: isIssueSessionReadyForExecution({
|
|
165
172
|
sessionState: session.sessionState,
|
|
166
173
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
174
|
+
delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
|
|
167
175
|
...(activeRun ? { activeRunId: activeRun.id } : {}),
|
|
168
176
|
blockedByCount: unresolvedBlockedBy.length,
|
|
169
177
|
hasPendingWake: this.db.issueSessions.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
|
|
@@ -26,6 +26,7 @@ export function syncIssueSessionFromIssue(params) {
|
|
|
26
26
|
});
|
|
27
27
|
const lastWakeReason = options?.lastWakeReason
|
|
28
28
|
?? deriveIssueSessionWakeReason({
|
|
29
|
+
delegatedToPatchRelay: issue.delegatedToPatchRelay,
|
|
29
30
|
pendingRunType: issue.pendingRunType,
|
|
30
31
|
factoryState: issue.factoryState,
|
|
31
32
|
prNumber: issue.prNumber,
|
package/dist/issue-session.js
CHANGED
|
@@ -14,6 +14,8 @@ export function deriveIssueSessionWaitingReason(params) {
|
|
|
14
14
|
return derivePatchRelayWaitingReason(params);
|
|
15
15
|
}
|
|
16
16
|
export function deriveIssueSessionWakeReason(params) {
|
|
17
|
+
if (params.delegatedToPatchRelay === false)
|
|
18
|
+
return undefined;
|
|
17
19
|
if (params.pendingRunType === "implementation")
|
|
18
20
|
return "delegated";
|
|
19
21
|
if (params.pendingRunType === "review_fix")
|
|
@@ -27,6 +29,7 @@ export function deriveIssueSessionWakeReason(params) {
|
|
|
27
29
|
if (params.factoryState === "awaiting_input")
|
|
28
30
|
return "waiting_for_human_reply";
|
|
29
31
|
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
32
|
+
delegatedToPatchRelay: params.delegatedToPatchRelay,
|
|
30
33
|
prNumber: params.prNumber,
|
|
31
34
|
prState: params.prState,
|
|
32
35
|
prReviewState: params.prReviewState,
|
|
@@ -38,6 +41,8 @@ export function deriveIssueSessionWakeReason(params) {
|
|
|
38
41
|
return undefined;
|
|
39
42
|
}
|
|
40
43
|
export function deriveIssueSessionReactiveIntent(params) {
|
|
44
|
+
if (params.delegatedToPatchRelay === false)
|
|
45
|
+
return undefined;
|
|
41
46
|
if (params.activeRunId !== undefined)
|
|
42
47
|
return undefined;
|
|
43
48
|
if (params.prNumber === undefined)
|
|
@@ -75,6 +80,8 @@ export function deriveIssueSessionReactiveIntent(params) {
|
|
|
75
80
|
return undefined;
|
|
76
81
|
}
|
|
77
82
|
export function isIssueSessionReadyForExecution(params) {
|
|
83
|
+
if (params.delegatedToPatchRelay === false)
|
|
84
|
+
return false;
|
|
78
85
|
if (params.activeRunId !== undefined)
|
|
79
86
|
return false;
|
|
80
87
|
if (params.blockedByCount > 0)
|
|
@@ -92,6 +99,7 @@ export function isIssueSessionReadyForExecution(params) {
|
|
|
92
99
|
return false;
|
|
93
100
|
}
|
|
94
101
|
if (deriveIssueSessionReactiveIntent({
|
|
102
|
+
delegatedToPatchRelay: params.delegatedToPatchRelay,
|
|
95
103
|
prNumber: params.prNumber,
|
|
96
104
|
prState: params.prState,
|
|
97
105
|
prReviewState: params.prReviewState,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatRunTypeLabel } from "./agent-session-plan.js";
|
|
2
2
|
import { sanitizeOperatorFacingText } from "./presentation-text.js";
|
|
3
|
+
import { isClosedPrState } from "./pr-state.js";
|
|
3
4
|
function lowerRunTypeLabel(runType) {
|
|
4
5
|
return formatRunTypeLabel(runType).toLowerCase();
|
|
5
6
|
}
|
|
@@ -188,23 +189,60 @@ export function buildMergePrepEscalationActivity(attempts) {
|
|
|
188
189
|
export function summarizeIssueStateForLinear(issue) {
|
|
189
190
|
switch (issue.sessionState) {
|
|
190
191
|
case "waiting_input":
|
|
191
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
192
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is waiting for input.` : "Waiting for input.");
|
|
192
193
|
case "running":
|
|
193
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
|
|
194
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} is actively running.` : "Actively running.");
|
|
194
195
|
case "idle":
|
|
196
|
+
if (!issue.delegatedToPatchRelay) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
195
199
|
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} is idle.` : "Idle.");
|
|
196
200
|
case "done":
|
|
197
|
-
|
|
201
|
+
if (issue.prNumber && issue.prState === "merged")
|
|
202
|
+
return `PR #${issue.prNumber} has merged.`;
|
|
203
|
+
if (issue.prNumber && isClosedPrState(issue.prState))
|
|
204
|
+
return `Completed without merging PR #${issue.prNumber}.`;
|
|
205
|
+
return issue.prNumber ? `Completed with PR #${issue.prNumber}.` : "Completed.";
|
|
198
206
|
case "failed":
|
|
199
|
-
return issue.waitingReason ?? (issue.prNumber ? `PR #${issue.prNumber} needs help to recover.` : "Needs help to recover.");
|
|
207
|
+
return issue.waitingReason ?? (issue.prNumber && !isClosedPrState(issue.prState) ? `PR #${issue.prNumber} needs help to recover.` : "Needs help to recover.");
|
|
200
208
|
}
|
|
201
209
|
switch (issue.factoryState) {
|
|
210
|
+
case "delegated":
|
|
211
|
+
if (!issue.delegatedToPatchRelay) {
|
|
212
|
+
return "PatchRelay is queued to start work, but automation is paused.";
|
|
213
|
+
}
|
|
214
|
+
return "Queued to start work.";
|
|
215
|
+
case "implementing":
|
|
216
|
+
if (!issue.delegatedToPatchRelay) {
|
|
217
|
+
return "Implementation is paused because the issue is undelegated.";
|
|
218
|
+
}
|
|
219
|
+
return "Implementation in progress.";
|
|
202
220
|
case "pr_open":
|
|
221
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
222
|
+
return `PR #${issue.prNumber} is awaiting review while PatchRelay is paused.`;
|
|
223
|
+
}
|
|
203
224
|
return issue.prNumber ? `PR #${issue.prNumber} is awaiting review.` : "Awaiting review.";
|
|
225
|
+
case "changes_requested":
|
|
226
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
227
|
+
return `PR #${issue.prNumber} has requested changes while PatchRelay is paused.`;
|
|
228
|
+
}
|
|
229
|
+
return issue.prNumber ? `PR #${issue.prNumber} has requested changes.` : "Requested changes received.";
|
|
230
|
+
case "repairing_ci":
|
|
231
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
232
|
+
return `PR #${issue.prNumber} has failing CI while PatchRelay is paused.`;
|
|
233
|
+
}
|
|
234
|
+
return issue.prNumber ? `PR #${issue.prNumber} has failing CI.` : "Failing CI.";
|
|
204
235
|
case "awaiting_queue":
|
|
236
|
+
if (!issue.delegatedToPatchRelay && issue.prNumber) {
|
|
237
|
+
return `PR #${issue.prNumber} is approved and awaiting merge while PatchRelay is paused.`;
|
|
238
|
+
}
|
|
205
239
|
return issue.prNumber ? `PR #${issue.prNumber} is approved and awaiting merge.` : "Approved and awaiting merge.";
|
|
206
240
|
case "done":
|
|
207
|
-
|
|
241
|
+
if (issue.prNumber && issue.prState === "merged")
|
|
242
|
+
return `PR #${issue.prNumber} has merged.`;
|
|
243
|
+
if (issue.prNumber && isClosedPrState(issue.prState))
|
|
244
|
+
return `Completed without merging PR #${issue.prNumber}.`;
|
|
245
|
+
return issue.prNumber ? `Completed with PR #${issue.prNumber}.` : "Completed.";
|
|
208
246
|
default:
|
|
209
247
|
return undefined;
|
|
210
248
|
}
|
|
@@ -29,9 +29,17 @@ export class LinearSessionSync {
|
|
|
29
29
|
if (!linear)
|
|
30
30
|
return;
|
|
31
31
|
const trackedIssue = this.db.getTrackedIssue(syncedIssue.projectId, syncedIssue.linearIssueId);
|
|
32
|
+
const visibleIssue = trackedIssue
|
|
33
|
+
? {
|
|
34
|
+
...trackedIssue,
|
|
35
|
+
delegatedToPatchRelay: syncedIssue.delegatedToPatchRelay,
|
|
36
|
+
prNumber: syncedIssue.prNumber,
|
|
37
|
+
prUrl: syncedIssue.prUrl,
|
|
38
|
+
}
|
|
39
|
+
: syncedIssue;
|
|
32
40
|
await syncActiveWorkflowState({ db: this.db, issue: syncedIssue, linear, ...(trackedIssue ? { trackedIssue } : {}), ...(options ? { options } : {}) });
|
|
33
41
|
await this.agentSessions.syncSessionPlan(syncedIssue, linear, options);
|
|
34
|
-
if (shouldSyncVisibleIssueComment(
|
|
42
|
+
if (shouldSyncVisibleIssueComment(visibleIssue, Boolean(syncedIssue.agentSessionId))) {
|
|
35
43
|
await syncVisibleStatusComment({
|
|
36
44
|
db: this.db,
|
|
37
45
|
issue: syncedIssue,
|